Compare commits

..

22 Commits

Author SHA1 Message Date
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
fb697366fe Fixed admin user access
Admin Layout & Pages
- app/(application)/admin/_layout.tsx
- app/(application)/admin/user-access/index.tsx

Admin Components
- components/Drawer/NavbarMenu.tsx
- components/_ShareComponent/Admin/BoxTitlePage.tsx
- components/_ShareComponent/Admin/AdminBasicBox.tsx

Admin Screens
- screens/Admin/User-Access/

API – Admin User Access
- service/api-admin/api-admin-user-access.ts

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

### No issue
2026-02-12 17:35:28 +08:00
6d71c3a86f Navbar menu versi 3
Admin Layout
- app/(application)/admin/_layout.tsx

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

New Component
- components/Drawer/NavbarMenu_V3.tsx

### No Issue'
2026-02-12 14:55:05 +08:00
e030b8f486 Fixed navbar admin
User Layout
- app/(application)/(user)/home.tsx

Components
- components/Drawer/NavbarMenu_V2.tsx

Docs
- docs/admin-folder-structure.md

### Issue: saat masuk lebih dalam ke sub menu indikator aktif di navbar hilang
2026-02-12 11:48:01 +08:00
5c931b069c Fixed navbar admin
User & Admin Layout
- app/(application)/(user)/home.tsx
- app/(application)/admin/_layout.tsx

Components
- components/Drawer/NavbarMenu.tsx
- components/index.ts

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

Backup Component
- components/Drawer/NavbarMenu.back.tsx

New Components
- components/Drawer/NavbarMenu_V2.tsx
- components/_ShareComponent/BasicWrapper.tsx

New Admin Screen
- screens/Admin/listPageAdmin_V2.tsx

### No Issue
2026-02-11 17:40:08 +08:00
b2be7be533 Saya telah melakukan serangkaian perubahan penting dalam pengembangan aplikasi HIPMI Mobile, khususnya dalam modul
Donasi. Berikut adalah ringkasan perubahan yang telah dilakukan:

     1. Menerapkan sistem pagination pada berbagai komponen layar donasi:
        - ScreenBeranda.tsx
        - ScreenMyDonation.tsx
        - ScreenRecapOfNews.tsx
        - ScreenListOfNews.tsx
        - ScreenListOfDonatur.tsx
        - ScreenFundDisbursement.tsx

     2. Memperbarui fungsi-fungsi API untuk mendukung parameter page:
        - apiDonationGetAll
        - apiDonationGetNewsById
        - apiDonationListOfDonaturById
        - apiDonationDisbursementOfFundsListById

     3. Mengganti komponen wrapper lama (ViewWrapper) dengan NewWrapper yang mendukung sistem pagination

     4. Membuat komponen layar terpisah untuk meningkatkan modularitas kode

     5. Memperbaiki berbagai error yang terjadi, termasuk masalah dengan import komponen dan struktur JSX

### No Issue
2026-02-10 17:30:30 +08:00
2705f96b01 feat: implement pagination and NewWrapper on donation and investment screens
- Implement pagination on investment screens (ScreenMyHolding, ScreenInvestor, ScreenRecapOfNews, ScreenListOfNews)
- Implement pagination on donation screens (ScreenStatus)
- Update API functions to support pagination with page parameter (apiInvestmentGetAll, apiInvestmentGetInvestorById, apiInvestmentGetNews, apiDonationGetByStatus)
- Replace ViewWrapper with NewWrapper for better UI experience
- Update app directory files to use new modular components from screens directory
- Add pull-to-refresh and infinite scroll functionality
- Improve performance by loading data incrementally
- Apply NewWrapper to donation create and create-story screens

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-09 17:35:54 +08:00
38a6b424e8 Ringkasan Perubahan
1. Pembuatan dan Pembaruan Komponen Layar Investasi dengan Sistem Pagination

ScreenMyHolding.tsx
 - Diperbarui dari sistem loading data statis ke sistem pagination dinamis
 - Menggunakan hook usePagination untuk manajemen data
 - Mengganti ViewWrapper dengan NewWrapper untuk tampilan yang lebih modern
 - Menambahkan fitur pull-to-refresh dan infinite scroll
 - Menggunakan helper pagination dari createPaginationComponents

ScreenInvestor.tsx
 - Dibuat baru dengan sistem pagination
 - Menggunakan hook usePagination untuk manajemen data
 - Menggunakan NewWrapper sebagai komponen dasar
 - Menambahkan fitur pull-to-refresh dan infinite scroll

ScreenRecapOfNews.tsx
 - Dibuat baru dengan sistem pagination
 - Menggunakan hook usePagination untuk manajemen data
 - Menggunakan NewWrapper sebagai komponen dasar
 - Menambahkan fitur pull-to-refresh dan infinite scroll

ScreenListOfNews.tsx
 - Dibuat baru dengan sistem pagination
 - Menggunakan hook usePagination untuk manajemen data
 - Menggunakan NewWrapper sebagai komponen dasar
 - Menambahkan fitur pull-to-refresh dan infinite scroll

2. Perubahan pada Fungsi-Fungsi API untuk Mendukung Pagination

apiInvestmentGetAll
 - Sudah mendukung parameter page dengan default "1"

apiInvestmentGetInvestorById
 - Ditambahkan parameter page dengan default "1"
 - Memungkinkan pengambilan data investor secara bertahap

apiInvestmentGetNews
 - Ditambahkan parameter page dengan default "1"
 - Memungkinkan pengambilan data berita secara bertahap

3. Pembaruan File-File di Direktori app/ untuk Menggunakan Komponen-Komponen Baru

app/(application)/(user)/investment/[id]/investor.tsx
 - Diperbarui untuk menggunakan komponen Investment_ScreenInvestor
 - Menghilangkan logika lokal dan menggantinya dengan komponen modular

app/(application)/(user)/investment/[id]/(news)/recap-of-news.tsx
 - Diperbarui untuk menggunakan komponen Investment_ScreenRecapOfNews
 - Menghilangkan logika lokal dan menggantinya dengan komponen modular

app/(application)/(user)/investment/[id]/(news)/list-of-news.tsx
 - Diperbarui untuk menggunakan komponen Investment_ScreenListOfNews
 - Menghilangkan logika lokal dan menggantinya dengan komponen modular

4. Implementasi Sistem Pagination

usePagination Hook
 - Digunakan secara konsisten di semua komponen layar investasi
 - Menyediakan fitur load more dan refresh
 - Mengelola state loading, refreshing, dan data

NewWrapper Component
 - Digunakan sebagai pengganti ViewWrapper
 - Menyediakan dukungan bawaan untuk FlatList
 - Menyediakan dukungan untuk refreshControl dan onEndReached

Pagination Helpers
 - Menggunakan createPaginationComponents untuk menghasilkan komponen-komponen pagination
 - Menyediakan skeleton loading saat data sedang dimuat
 - Menyediakan pesan kosong saat tidak ada data

5. Manfaat dari Perubahan Ini

 1. Performa Lebih Baik: Dengan pagination, hanya sejumlah kecil data yang dimuat pada satu waktu, meningkatkan
    performa aplikasi
 2. Pengalaman Pengguna Lebih Baik: Fitur pull-to-refresh dan infinite scroll membuat navigasi lebih intuitif
 3. Kode Lebih Modular: Komponen-komponen baru dapat digunakan kembali dan dipelihara lebih mudah
 4. Struktur Kode Lebih Rapi: Pemisahan antara logika tampilan dan logika data membuat kode lebih terorganisir

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-09 14:42:56 +08:00
83fa277e03 Fix Loaddata Invesment & Clearing code
UI – Investment (User)
- app/(application)/(user)/investment/(tabs)/index.tsx
- app/(application)/(user)/investment/(tabs)/portofolio.tsx
- app/(application)/(user)/investment/(tabs)/transaction.tsx
- app/(application)/(user)/investment/[id]/(document)/list-of-document.tsx
- app/(application)/(user)/investment/[id]/(document)/recap-of-document.tsx
- app/(application)/(user)/investment/[id]/(transaction-flow)/invoice.tsx
- app/(application)/(user)/investment/[id]/(transaction-flow)/select-bank.tsx

Screens – Investment
- screens/Invesment/ButtonStatusSection.tsx
- screens/Invesment/Document/RecapBoxDetail.tsx
- screens/Invesment/Document/ScreenListDocument.tsx
- screens/Invesment/Document/ScreenRecap.tsx
- screens/Invesment/ScreenBursa.tsx
- screens/Invesment/ScreenPortofolio.tsx
- screens/Invesment/ScreenTransaction.tsx

Profile
- app/(application)/(user)/profile/[id]/detail-blocked.tsx

API Client
- service/api-client/api-investment.ts

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

### No issue
2026-02-06 17:27:12 +08:00
c570a19d84 Fix Wrapper
UI – Investment (User)
- app/(application)/(user)/investment/create.tsx
- app/(application)/(user)/investment/[id]/edit.tsx

### No Issue"
2026-02-05 17:30:17 +08:00
7415c8c8ce Fix path Notification admin
UI – Notifications (User)
- app/(application)/(user)/notifications/index.tsx
- screens/Notification/ScreenNotification_V2.tsx

UI – Notifications (Admin)
- screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx

API
- service/api-notifications.ts

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

### No Issue
2026-02-05 16:37:50 +08:00
72a3d42013 Fix Loaddata Voting
UI – Voting (User)
- app/(application)/(user)/voting/(tabs)/index.tsx
- app/(application)/(user)/voting/(tabs)/history.tsx
- app/(application)/(user)/voting/(tabs)/contribution.tsx
- app/(application)/(user)/voting/create.tsx
- app/(application)/(user)/voting/[id]/edit.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- app/(application)/(user)/voting/[id]/list-of-contributor.tsx

UI – Event
- app/(application)/(user)/event/[id]/edit.tsx
- screens/Event/ScreenStatus.tsx

UI – Voting Screens (New)
- screens/Voting/ScreenBeranda.tsx
- screens/Voting/ScreenContribution.tsx
- screens/Voting/ScreenHistory.tsx
- screens/Voting/ScreenListOfContributor.tsx
- screens/Voting/ScreenStatus.tsx
- screens/Voting/ButtonStatusSection.tsx

UI – Job
- screens/Job/ButtonStatusSection.tsx
- screens/Job/MainViewStatus2.tsx

API Client
- service/api-client/api-voting.ts

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

### No Issue
2026-02-05 15:06:14 +08:00
d0abd14047 Fix Loaddata Voting
Voting – User
- app/(application)/(user)/voting/(tabs)/status.tsx
- app/(application)/(user)/voting/create.tsx

Screens – Voting
- screens/Voting/ButtonStatusSection.tsx

API Client
- service/api-client/api-voting.ts

Global
- app/+not-found.tsx
- styles/global-styles.ts

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

Untracked (New Files)
- screens/Voting/ScreenStatus.tsx

### No issue
2026-02-04 17:44:57 +08:00
5b2be20469 Fix Loaddata pada event dan perbaikan tampilan pada NewWrapper
Event – User
- app/(application)/(user)/event/(tabs)/contribution.tsx
- app/(application)/(user)/event/(tabs)/index.tsx
- app/(application)/(user)/event/[id]/list-of-participants.tsx

Voting – User
- app/(application)/(user)/voting/(tabs)/history.tsx

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

Screens – Event
- screens/Event/BoxPublishSection.tsx
- screens/Event/ButtonStatusSection.tsx
- screens/Event/ScreenHistory.tsx
- screens/Event/ScreenStatus.tsx

Screens – Forum
- screens/Forum/ViewBeranda3.tsx

API Client
- service/api-client/api-event.ts

Styles
- styles/global-styles.ts

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

Untracked (New Files)
- screens/Event/ScreenBeranda.tsx
- screens/Event/ScreenContribution.tsx
- screens/Event/ScreenListOfParticipants.tsx

#### No Issue
2026-02-04 16:56:48 +08:00
60177a1087 Fix Component Datetime IOS
Components
- components/DateInput/DataTimeAndroid.tsx
- components/DateInput/DateTimeIOS.tsx
- components/Notification/NotificationInitializer.tsx

Screens
- screens/Event/ScreenStatus.tsx

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

### No Issue
2026-02-04 12:00:00 +08:00
771ae45f26 Fix load data event
Event – User
- app/(application)/(user)/event/(tabs)/history.tsx
- app/(application)/(user)/event/(tabs)/status.tsx
- app/(application)/(user)/event/[id]/edit.tsx
- app/(application)/(user)/event/create.tsx

Event – Screens (Untracked)
- screens/Event/ScreenHistory.tsx
- screens/Event/ScreenStatus.tsx

API
- service/api-client/api-event.ts

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

### No Issue
2026-02-03 17:45:27 +08:00
41f4a8ac99 Fix load data notification
Notification – User
- app/(application)/(user)/notifications/index.tsx
- screens/Notification/ScreenNotification_V1.tsx
- screens/Notification/ScreenNotification_V2.tsx

Notification – Admin
- app/(application)/admin/notification/index.tsx
- screens/Admin/Notification-Admin/

Job
- screens/Job/MainViewStatus2.tsx

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

Deleted
- screens/Notification/ScreenNotification.tsx

### No Issue
2026-02-03 16:59:09 +08:00
48196cd46b Fix Load data pada halaman yang membutuhkan infinite load
Job – User App
- app/(application)/(user)/job/(tabs)/index.tsx
- app/(application)/(user)/job/(tabs)/status.tsx
- app/(application)/(user)/job/(tabs)/archive.tsx
- app/(application)/(user)/job/create.tsx

Job – Screens
- screens/Job/ScreenBeranda.tsx
- screens/Job/ScreenBeranda2.tsx
- screens/Job/MainViewStatus.tsx
- screens/Job/MainViewStatus2.tsx
- screens/Job/ScreenArchive.tsx
- screens/Job/ScreenArchive2.tsx

API – Job (Client)
- service/api-client/api-job.ts

Notification
- screens/Notification/ScreenNotification.tsx

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

### No Issue
2026-02-02 17:09:58 +08:00
ec79a1fbcd Fix semua tampilan yang memiliki fungsi infitine load
UI – User Notifications
- app/(application)/(user)/notifications/index.tsx
- service/api-notifications.ts
- screens/Notification/

UI – Portofolio (User)
- app/(application)/(user)/portofolio/[id]/create.tsx
- app/(application)/(user)/portofolio/[id]/edit.tsx
- app/(application)/(user)/portofolio/[id]/list.tsx
- screens/Portofolio/BoxPortofolioView.tsx
- screens/Portofolio/ViewListPortofolio.tsx
- screens/Profile/PortofolioSection.tsx
- service/api-client/api-portofolio.ts

Forum & User Search
- screens/Forum/DetailForum2.tsx
- screens/Forum/ViewBeranda3.tsx
- screens/UserSeach/MainView_V2.tsx

Constants & Docs
- constants/constans-value.ts
- docs/prompt-for-qwen-code.md

### No Issue
2026-01-30 17:18:47 +08:00
ed16f1b204 Fix forum detail
Forum (User)
- app/(application)/(user)/forum/[id]/index.tsx
- screens/Forum/ViewForumku2.tsx
- service/api-client/api-forum.ts

Forum – New / Refactor
- screens/Forum/DetailForum.tsx
- screens/Forum/DetailForum2.tsx

Documentation
- docs/

Removed
- hipmi-note.md

### No Issue'
2026-01-29 17:36:17 +08:00
d693550a1f Fix Alur Login & Load data forum , user search
Admin – User Access
- app/(application)/admin/user-access/[id]/index.tsx

Authentication
- context/AuthContext.tsx
- screens/Authentication/EULASection.tsx
- screens/Authentication/LoginView.tsx

Forum
- screens/Forum/ViewBeranda3.tsx

Profile & UI Components
- components/Image/AvatarComp.tsx
- screens/Profile/AvatarAndBackground.tsx

### No Issue
2026-01-29 15:08:00 +08:00
b3bfbc0f7e Fix Infinite Load Data
Forum & User Search – User
- app/(application)/(user)/forum/index.tsx
- app/(application)/(user)/user-search/index.tsx

Global & Core
- app/+not-found.tsx
- screens/RootLayout/AppRoot.tsx
- constants/constans-value.ts

Component
- components/Image/AvatarComp.tsx

API Client
- service/api-client/api-user.ts

Untracked Files
- QWEN.md
- helpers/
- hooks/use-pagination.tsx
- screens/Forum/ViewBeranda3.tsx
- screens/UserSeach/

### No Issue
2026-01-29 11:36:24 +08:00
167 changed files with 11107 additions and 4813 deletions

169
QWEN.md Normal file
View File

@@ -0,0 +1,169 @@
# HIPMI Mobile Application - Development Context
## 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.
### Key Technologies
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
- **Language**: TypeScript
- **Architecture**: File-based routing with Expo Router
- **State Management**: Context API
- **UI Components**: React Native Paper, custom components
- **Maps Integration**: Mapbox Maps for React Native
- **Push Notifications**: React Native Firebase Messaging
- **Build System**: Metro bundler
### Project Structure
```
hipmi-mobile/
├── app/ # Main application screens and routing
│ ├── _layout.tsx # Root layout component
│ ├── index.tsx # Entry point (Login screen)
│ └── ...
├── components/ # Reusable UI components
├── context/ # State management (AuthContext)
├── screens/ # Screen components organized by feature
│ ├── Admin/ # Admin panel screens
│ ├── Authentication/ # Login, registration flows
│ ├── Collaboration/ # Collaboration features
│ ├── Event/ # Event management
│ ├── Forum/ # Forum functionality
│ ├── Home/ # Home screen
│ ├── Maps/ # Map integration
│ ├── Profile/ # User profile
│ └── ...
├── assets/ # Images, icons, and static assets
├── constants/ # Constants and configuration values
├── hooks/ # Custom React hooks
├── lib/ # Utility libraries
├── navigation/ # Navigation configuration
├── service/ # API services and business logic
├── types/ # TypeScript type definitions
└── utils/ # Helper functions
```
## Building and Running
### Prerequisites
- Node.js (with bun as the package manager)
- Expo CLI
- iOS Simulator or Android Emulator (for native builds)
### Setup and Development
1. **Install Dependencies**
```bash
bun install
```
2. **Run Development Server**
```bash
bun run start
```
Or use the shorthand:
```bash
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`
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
### 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
## Features and Functionality
The application appears to include several key modules:
- **Authentication**: Login, registration, and verification flows
- **Admin Panel**: Administrative functions
- **Collaboration**: Tools for member collaboration
- **Events**: Event management and calendar
- **Forum**: Discussion forums
- **Maps**: Location-based services with Mapbox integration
- **Donations**: Donation functionality
- **Job Board**: Employment opportunities
- **Investment**: Investment-related features
- **Voting**: Voting systems
- **Portfolio**: Member portfolio showcase
- **Notifications**: Push notifications via Firebase
## Development Conventions
### Coding Standards
- TypeScript is used throughout the project for type safety
- Component-based architecture with reusable components
- Context API for state management
- File-based routing with Expo Router
- Consistent naming conventions using camelCase for variables and PascalCase for components
### Testing
- Linting is configured with ESLint
- Standard Expo linting configuration is used
### Security
- Firebase is integrated for authentication and messaging
- Camera and location permissions are properly configured
- Deep linking is secured with app domain associations
## Key Dependencies
### 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
### Development Dependencies
- `@types/*`: TypeScript type definitions
- `eslint-config-expo`: Expo-specific ESLint configuration
- `typescript`: Type checking
## Platform Support
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
## Special Configurations
### iOS Configuration
- Bundle identifier: `com.anonymous.hipmi-mobile`
- Supports tablets
- Google Services integration
- Location permission handling
- Associated domains for deep linking
### Android Configuration
- Package name: `com.bip.hipmimobileapp`
- Adaptive icons
- Edge-to-edge display enabled
- Intent filters for HTTPS deep linking
- Google Services integration
### Maps Integration
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
### Push Notifications
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.

View File

@@ -1,56 +1,9 @@
import {
FloatingButton,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import Donation_BoxPublish from "@/screens/Donation/BoxPublish";
import { apiDonationGetAll } from "@/service/api-client/api-donation";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Donation_ScreenBeranda from "@/screens/Donation/ScreenBeranda";
export default function DonationBeranda() {
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadList(true);
const response = await apiDonationGetAll({
category: "beranda"
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/donation/create")} />
}
>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">Belum ada donasi</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Donation_BoxPublish data={item} key={index} id={item.id} />
))
)}
</ViewWrapper>
<>
<Donation_ScreenBeranda />
</>
);
}

View File

@@ -1,148 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
DummyLandscapeImage,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiDonationGetAll } from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { Href, router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
import Donation_ScreenMyDonation from "@/screens/Donation/ScreenMyDonation";
export default function DonationMyDonation() {
const { user } = useAuth();
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id]),
);
const onLoadData = async () => {
if (!user?.id) {
Toast.show({
type: "error",
text1: "Load data gagal, user tidak ditemukan",
});
return;
}
try {
setLoadList(true);
const response = await apiDonationGetAll({
category: "my-donation",
authorId: user?.id,
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const handlerColor = (status: string) => {
if (status === "menunggu") {
return "orange";
} else if (status === "proses") {
return "white";
} else if (status === "berhasil") {
return "green";
} else if (status === "gagal") {
return "red";
}
};
const handlePress = ({
invoiceId,
donationId,
status,
}: {
invoiceId: string;
donationId: string;
status: string;
}) => {
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
if (status === "menunggu") {
router.push(`${url}/invoice`);
} else if (status === "proses") {
router.push(`${url}/process`);
} else if (status === "berhasil") {
router.push(`${url}/success`);
} else if (status === "gagal") {
router.push(`${url}/failed`);
}
};
return (
<ViewWrapper hideFooter>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Belum ada transaksi
</TextCustom>
) : (
list?.map((item, index) => (
<BaseBox
key={index}
paddingTop={7}
paddingBottom={7}
onPress={() => {
handlePress({
status: _.lowerCase(item.statusInvoice),
invoiceId: item.id,
donationId: item.donasiId,
});
}}
>
<Grid>
<Grid.Col span={5}>
<DummyLandscapeImage
height={100}
unClickPath
imageId={item.imageId}
/>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={6}>
<StackCustom>
<TextCustom truncate={2} bold>
{item.title || "-"}
</TextCustom>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(item.nominal)}
</TextCustom>
<BadgeCustom
variant="light"
color={handlerColor(_.lowerCase(item.statusInvoice))}
fullWidth
>
{item.statusInvoice}
</BadgeCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
);
return <Donation_ScreenMyDonation />;
}

View File

@@ -1,82 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Donation_ScreenStatus from "@/screens/Donation/ScreenStatus";
import { useLocalSearchParams } from "expo-router";
export default function DonationStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish",
);
const [listData, setListData] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [activeCategory]),
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
setListData(null);
} finally {
setLoadList(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<ViewWrapper hideFooter headerComponent={scrollComponent}>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<Donasi_BoxStatus
key={index}
data={item}
status={activeCategory as string}
/>
))
)}
</ViewWrapper>
<Donation_ScreenStatus initialStatus={status || "publish"} />
);
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
@@ -31,7 +32,7 @@ export default function DonationEditNews() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [news])
}, [news]),
);
const onLoadData = async () => {
@@ -104,7 +105,21 @@ export default function DonationEditNews() {
};
return (
<ViewWrapper>
<ViewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data?.title || !data?.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
<LandscapeFrameUploaded
@@ -148,15 +163,6 @@ export default function DonationEditNews() {
/>
<Spacing />
<ButtonCustom
disabled={!data?.title || !data?.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>

View File

@@ -1,8 +1,10 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
NewWrapper,
Spacing,
StackCustom,
TextAreaCustom,
@@ -53,7 +55,7 @@ export default function DonationAddNews() {
text1: "Gagal menambah berita",
});
return
return;
}
Toast.show({
@@ -70,7 +72,21 @@ export default function DonationAddNews() {
};
return (
<ViewWrapper>
<NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data.title || !data.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
<LandscapeFrameUploaded image={image?.uri} />
@@ -116,17 +132,7 @@ export default function DonationAddNews() {
/>
<Spacing />
<ButtonCustom
disabled={!data.title || !data.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,110 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
BaseBox,
DrawerCustom,
Grid,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenListOfNews from "@/screens/Donation/ScreenListOfNews";
export default function DonationRecapOfNews() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetNewsById({
id: id as string,
category: "get-all",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Daftar Kabar",
headerLeft: () => <BackButton />,
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada kabar
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title || "-"}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconPlus />,
label: "Tambah Berita",
path: `/donation/${id}/(news)/add-news`,
},
]}
onPressItem={(item) => {
console.log("PATH ", item.path);
router.navigate(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
);
return <Donation_ScreenListOfNews donationId={id as string} />;
}

View File

@@ -1,112 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
BaseBox,
DotButton,
DrawerCustom,
Grid,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenRecapOfNews from "@/screens/Donation/ScreenRecapOfNews";
export default function DonationRecapOfNews() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetNewsById({
id: id as string,
category: "get-all",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Rekap Kabar",
headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada kabar
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title || "-"}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconPlus />,
label: "Tambah Berita",
path: `/donation/${id}/(news)/add-news`,
},
]}
onPressItem={(item) => {
console.log("PATH ", item.path);
router.navigate(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
);
return <Donation_ScreenRecapOfNews donationId={id as string} />;
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
Grid,
@@ -35,7 +36,7 @@ export default function DonationInvoice() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [invoiceId])
}, [invoiceId]),
);
const onLoadData = async () => {
@@ -100,7 +101,22 @@ export default function DonationInvoice() {
return (
<>
<ViewWrapper>
<ViewWrapper
hideFooter
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={!image}
isLoading={isLoading}
onPress={() => {
handlerUpdateInvoice();
}}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom>
<InformationBox
text={`Mohon transfer donasi anda ke rekening dibawah`}
@@ -204,16 +220,6 @@ export default function DonationInvoice() {
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
<ButtonCustom
disabled={!image}
isLoading={isLoading}
onPress={() => {
handlerUpdateInvoice();
}}
>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>

View File

@@ -4,11 +4,12 @@ import {
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
Spacing,
ViewWrapper,
} from "@/components";
import { IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection";
@@ -26,18 +27,19 @@ import {
} from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { RefreshControl } from "react-native";
export default function DonasiDetailStatus() {
const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
const [data, setData] = useState<any | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -80,6 +82,17 @@ export default function DonasiDetailStatus() {
});
};
const onRefresh = useCallback(() => {
try {
setRefreshing(true);
onLoadData();
} catch (error) {
console.log("Error refresh");
} finally {
setRefreshing(false);
}
}, []);
return (
<>
<Stack.Screen
@@ -94,31 +107,50 @@ export default function DonasiDetailStatus() {
) : null,
}}
/>
<ViewWrapper>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
bottomSection={
status === "publish" && (
<Donation_ProgressSection
<NewWrapper
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
>
{!data ? (
<CustomSkeleton height={400} />
) : (
<>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
showSisaHari={status === "publish" ? true : false}
bottomSection={
status === "publish" && (
<Donation_ProgressSection
id={id as string}
progres={Number(data?.progres) || 0}
/>
)
}
/>
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
<Spacing />
{data && (
<Donation_ButtonStatusSection
id={id as string}
progres={Number(data?.progres) || 0}
status={status as string}
/>
)
}
/>
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
<Spacing />
<Donation_ButtonStatusSection
id={id as string}
status={status as string}
/>
<Spacing />
</ViewWrapper>
)}
<Spacing />
</>
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}

View File

@@ -1,16 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import API_IMAGE from "@/constants/api-storage";
import DIRECTORY_ID from "@/constants/directory-id";
import {
@@ -60,7 +63,7 @@ export default function DonationEdit() {
useCallback(() => {
onLoadData();
onLoadList();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -79,7 +82,6 @@ export default function DonationEdit() {
imageId: response.data.imageId,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
@@ -182,10 +184,24 @@ export default function DonationEdit() {
};
return (
<ViewWrapper>
<NewWrapper
hideFooter
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
{!data || loadList ? (
<LoaderCustom />
<ListSkeletonComponent />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
@@ -260,17 +276,9 @@ export default function DonationEdit() {
/>
<Spacing />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</StackCustom>
)}
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,124 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
ButtonCenteredOnly,
Grid,
InformationBox,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import {
apiDonationDisbursementOfFundsListById,
apiDonationGetOne,
} from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import dayjs from "dayjs";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import React, { useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenFundDisbursement from "@/screens/Donation/ScreenFundDisbursement";
export default function DonationFundDisbursement() {
const { id } = useLocalSearchParams();
const [data, setData] = useState({
totalPencairan: 0,
akumulasiPencairan: 0,
});
const [listData, setListData] = React.useState<any[] | null>(null);
const [loadData, setLoadData] = React.useState(false);
useFocusEffect(
React.useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadData(true);
const responseData = await apiDonationGetOne({
id: id as string,
category: "permanent",
});
if (responseData.success) {
setData({
totalPencairan: responseData.data.totalPencairan,
akumulasiPencairan: responseData.data.akumulasiPencairan,
});
}
const responseList = await apiDonationDisbursementOfFundsListById({
id: id as string,
});
if (responseList.success) {
setListData(responseList.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return (
<>
<ViewWrapper>
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
<BaseBox>
<Grid>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
</TextCustom>
<TextCustom size="small">Total Pencairan Dana</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
{data?.akumulasiPencairan} kali
</TextCustom>
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<BaseBox key={index}>
<StackCustom>
<Grid>
<Grid.Col span={8}>
<TextCustom bold>{item?.title}</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>{dayjs(item?.createdAt).format("DD MMM YYYY")}</TextCustom>
</Grid.Col>
</Grid>
<TextCustom>{item?.deskripsi}</TextCustom>
<ButtonCenteredOnly
onPress={() => {
router.navigate(`/(application)/(image)/preview-image/${item?.imageId}`);
}}
icon="file-text"
>
Bukti Transaksi
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper>
</>
);
return <Donation_ScreenFundDisbursement donationId={id as string} />;
}

View File

@@ -6,10 +6,12 @@ import {
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
StackCustom,
ViewWrapper,
} from "@/components";
import { IconNews } from "@/components/_Icon";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { useAuth } from "@/hooks/use-auth";
import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData";
import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising";
@@ -34,7 +36,7 @@ export default function DonasiDetailBeranda() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -75,10 +77,10 @@ export default function DonasiDetailBeranda() {
<>
<BoxButtonOnFooter>
<ButtonCustom
disabled={value?.reminder}
disabled={value?.reminder || !data}
onPress={() => router.navigate(`/donation/${id}/(transaction-flow)`)}
>
{value?.reminder ? "Waktu berakhir" : "Donasi"}
{!data ? "Loading..." : value?.reminder ? "Waktu berakhir" : "Donasi"}
</ButtonCustom>
</BoxButtonOnFooter>
</>
@@ -96,21 +98,30 @@ export default function DonasiDetailBeranda() {
) : null,
}}
/>
<ViewWrapper footerComponent={buttonSection}>
<StackCustom>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
bottomSection={<Donation_ProgressSection id={id as string} progres={Number(data?.progres) || 0} />}
/>
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
</StackCustom>
</ViewWrapper>
<NewWrapper footerComponent={buttonSection}>
{!data ? (
<CustomSkeleton height={400} />
) : (
<StackCustom>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
bottomSection={
<Donation_ProgressSection
id={id as string}
progres={Number(data?.progres) || 0}
/>
}
/>
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
</StackCustom>
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}

View File

@@ -1,94 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { apiAdminDonationListOfDonaturById } from "@/service/api-admin/api-admin-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { FontAwesome6 } from "@expo/vector-icons";
import dayjs from "dayjs";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenListOfDonatur from "@/screens/Donation/ScreenListOfDonatur";
export default function Donation_ListOfDonatur() {
export default function DonationListOfDonatur() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[] | null>(null);
const [loadData, setLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminDonationListOfDonaturById({
id: id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return (
<>
<ViewWrapper>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom bold align="center">
Belum ada donatur
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<BaseBox key={index}>
<Grid>
<Grid.Col
span={3}
style={{ alignItems: "center", justifyContent: "center" }}
>
<FontAwesome6
name="face-smile-wink"
size={50}
style={{ color: MainColor.yellow }}
/>
</Grid.Col>
<Grid.Col span={9}>
<TextCustom bold size="large">
{item?.Author?.username || "-"}
</TextCustom>
<Spacing/>
<StackCustom gap={"xs"}>
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
<TextCustom bold size="large" color="yellow">
Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
<TextCustom>
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
</TextCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
</>
);
return <Donation_ScreenListOfDonatur donationId={id as string} />;
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
@@ -8,8 +9,8 @@ import {
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import {
@@ -112,7 +113,23 @@ export default function DonationCreateStory() {
};
return (
<ViewWrapper>
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
</>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Cerita Anda adalah kunci untuk menginspirasi kebaikan. Jelaskan dengan jujur dan jelas tujuan penggalangan dana ini agar calon donatur memahami dampak positif yang dapat mereka wujudkan melalui kontribusi mereka." />
<TextAreaCustom
@@ -166,18 +183,8 @@ export default function DonationCreateStory() {
value={data.rekening}
onChangeText={(value) => setData({ ...data, rekening: value })}
/>
<Spacing />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,4 +1,5 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
@@ -8,8 +9,8 @@ import {
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiDonationCreate } from "@/service/api-client/api-donation";
import { apiMasterDonation } from "@/service/api-client/api-master";
@@ -43,7 +44,7 @@ export default function DonationCreate() {
useFocusEffect(
useCallback(() => {
onLoadList();
}, [])
}, []),
);
const onLoadList = async () => {
@@ -125,7 +126,24 @@ export default function DonationCreate() {
};
return (
<ViewWrapper>
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
}}
>
Selanjutnya
</ButtonCustom>
</BoxButtonOnFooter>
</>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
@@ -201,20 +219,8 @@ export default function DonationCreate() {
onChange={(value: any) => setData({ ...data, durasiId: value })}
/>
)}
<Spacing />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
}}
>
Selanjutnya
</ButtonCustom>
<Spacing />
</StackCustom>
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,115 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import {
apiEventGetAll
} from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useState } from "react";
import Event_ScreenContribution from "@/screens/Event/ScreenContribution";
export default function EventContribution() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
async function onLoadData() {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: "contribution",
userId: user?.id,
});
console.log("[DATA] ", JSON.stringify(response.data, null, 2));
if (response.success) {
setListData(response.data);
// const responseListParticipants = await apiEventListOfParticipants({
// id: response?.data?.Event?.id,
// });
// console.log(
// "[LIST PARTICIPANTS]",
// JSON.stringify(responseListParticipants.data, null, 2)
// );
// if (responseListParticipants.success) {
// setListParticipants(responseListParticipants.data);
// }
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
return (
<ViewWrapper hideFooter>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada kontribusi</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BoxWithHeaderSection
key={index}
href={`/event/${item?.Event?.id}/contribution`}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Event?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
name={item?.Event?.Author?.username}
rightComponent={
<TextCustom truncate>
{dateTimeView({
date: item?.Event?.tanggal,
withoutTime: true,
})}
</TextCustom>
}
/>
<TextCustom bold align="center" size="xlarge" truncate={2}>
{item?.Event?.title}
</TextCustom>
<Spacing height={0} />
{/* <Grid>
{item?.Event?.Event_Peserta?.map(
(item2: any, index2: number) => (
<Grid.Col
style={{ alignItems: "center" }}
span={12 / item?.Event?.Event_Peserta?.length}
key={index2}
>
<AvatarComp
size="base"
href={`/profile/${item2?.User?.Profile?.id}`}
fileId={item2?.User?.Profile?.imageId}
/>
</Grid.Col>
)
)}
</Grid> */}
</StackCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper>
<>
<Event_ScreenContribution />
</>
);
}

View File

@@ -1,104 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import _ from "lodash";
import { useEffect, useState } from "react";
import { View } from "react-native";
import Event_ScreenHistory from "@/screens/Event/ScreenHistory";
export default function EventHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useEffect(() => {
onLoadData({ userId: user?.id });
}, [user?.id, activeCategory]);
async function onLoadData({ userId }: { userId?: string }) {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
userId: userId,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
};
const headerComponent = (
<View
style={{
flexDirection: "row",
alignItems: "center",
padding: 5,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 50,
width: "100%",
}}
>
<ButtonCustom
backgroundColor={
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
}
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
style={{ width: "49%" }}
onPress={() => handlePress("all")}
>
Semua Riwayat
</ButtonCustom>
<Spacing width={"2%"} />
<ButtonCustom
backgroundColor={
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === "main" ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress("main")}
>
Riwayat Saya
</ButtonCustom>
</View>
);
return (
<ViewWrapper headerComponent={headerComponent} hideFooter>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada riwayat</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Event_BoxPublishSection
key={index.toString()}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
href={`/event/${item.id}/history`}
/>
))
)}
</ViewWrapper>
<>
<Event_ScreenHistory />
</>
);
}

View File

@@ -1,63 +1,9 @@
import { LoaderCustom, TextCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import FloatingButton from "@/components/Button/FloatingButton";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Event_ScreenBeranda from "@/screens/Event/ScreenBeranda";
export default function EventBeranda() {
const [listData, setListData] = useState([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiEventGetAll({category: "beranda"});
// console.log("Response", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/event/create")} />
}
>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada event</TextCustom>
) : (
listData.map((item: any, index) => (
<Event_BoxPublishSection
key={index}
href={`/event/${item.id}/publish`}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
/>
))
)}
</ViewWrapper>
<>
<Event_ScreenBeranda />
</>
);
}

View File

@@ -1,101 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxWithHeaderSection,
Grid,
LoaderCustom,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Event_ScreenStatus from "@/screens/Event/ScreenStatus";
export default function EventStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory, id])
);
async function onLoadData() {
try {
setLoadingGetData(true);
const response = await apiEventGetByStatus({
id: id!,
status: activeCategory!,
});
// console.log("Response", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
console.log(error);
} finally {
setLoadingGetData(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const tabsComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<ViewWrapper headerComponent={tabsComponent}>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, i) => (
<BoxWithHeaderSection
key={i}
href={`/event/${item.id}/${activeCategory}/detail-event`}
>
<StackCustom gap={"xs"}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>
{new Date(item?.tanggal).toLocaleDateString()}
</TextCustom>
</Grid.Col>
</Grid>
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper>
<>
<Event_ScreenStatus />
</>
);
}

View File

@@ -1,7 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
@@ -10,6 +12,7 @@ import {
TextInputCustom,
ViewWrapper,
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import {
apiEventGetOne,
@@ -48,14 +51,14 @@ export default function EventEdit() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
async function onLoadData() {
try {
setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
setSelectedDate(new Date(response.data.tanggal));
@@ -100,6 +103,15 @@ export default function EventEdit() {
const startDate = new Date(selectedDate as any);
const endDate = new Date(selectedEndDate as any);
if (!startDate) {
Toast.show({
type: "info",
text1: "Info",
text2: "Tanggal mulai tidak valid",
});
return false;
}
if (startDate >= endDate) {
Toast.show({
type: "info",
@@ -146,7 +158,7 @@ export default function EventEdit() {
const validateDateRange = (
selectedDate: string | Date,
selectedEndDate: string | Date
selectedEndDate: string | Date,
): { isValid: boolean; error?: string } => {
const startDate = new Date(selectedDate);
const endDate = new Date(selectedEndDate);
@@ -174,9 +186,19 @@ export default function EventEdit() {
return (
<>
<ViewWrapper>
<NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
title="Update"
onPress={handlerSubmit}
/>
</BoxButtonOnFooter>
}
>
{isLoadData ? (
<LoaderCustom />
<ListSkeletonComponent />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
@@ -186,26 +208,15 @@ export default function EventEdit() {
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value) => {
console.log(value);
setData({ ...data, eventMaster_TipeAcaraId: value });
}}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
<TextAreaCustom
label="Deskripsi"
placeholder="Masukkan deskripsi event"
required
value={data?.lokasi}
onChangeText={(value) => setData({ ...data, lokasi: value })}
showCount
value={data?.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
<DateTimePickerCustom
minimumDate={new Date(Date.now())}
label="Tanggal & Waktu Mulai"
@@ -233,7 +244,7 @@ export default function EventEdit() {
{
validateDateRange(
selectedDate as any,
selectedEndDate as any
selectedEndDate as any,
).error
}
</TextCustom>
@@ -242,31 +253,37 @@ export default function EventEdit() {
{
validateDateRange(
selectedDate as any,
selectedEndDate as any
selectedEndDate as any,
).error
}
</TextCustom>
)}
<Spacing />
{/* <Spacing /> */}
</StackCustom>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukkan deskripsi event"
required
showCount
value={data?.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value) => {
console.log(value);
setData({ ...data, eventMaster_TipeAcaraId: value });
}}
/>
<ButtonCustom
isLoading={isLoading}
title="Update"
onPress={handlerSubmit}
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required
value={data?.lokasi}
onChangeText={(value) => setData({ ...data, lokasi: value })}
/>
</StackCustom>
)}
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -1,110 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import {
apiEventGetOne,
apiEventListOfParticipants,
} from "@/service/api-client/api-event";
import dayjs, { Dayjs } from "dayjs";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Event_ScreenListOfParticipants from "@/screens/Event/ScreenListOfParticipants";
export default function EventListOfParticipants() {
const { id } = useLocalSearchParams();
const [startDate, setStartDate] = useState<Dayjs | undefined>();
const [listData, setListData] = useState<any[] | null>(null);
const [loadtData, setLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
handlerLoadData();
}, [id])
);
const handlerLoadData = () => {
try {
onLoadData();
onLoadList();
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadData = async () => {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
const date = dayjs(response.data.tanggal);
setStartDate(date);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadList = async () => {
try {
setLoadData(true);
const response = await apiEventListOfParticipants({ id: id as string });
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return (
<ViewWrapper>
{loadtData && !listData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada peserta
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<BaseBox key={index}>
<AvatarUsernameAndOtherComponent
avatar={item?.User?.Profile?.imageId}
name={item?.User?.username}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
rightComponent={
startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
</View>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color="gray">-</BadgeCustom>
</View>
)
}
/>
</BaseBox>
))
)}
</ViewWrapper>
<>
<Event_ScreenListOfParticipants />
</>
);
}

View File

@@ -1,12 +1,13 @@
import {
BoxButtonOnFooter,
ButtonCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { useAuth } from "@/hooks/use-auth";
@@ -100,7 +101,6 @@ export default function EventCreate() {
setIsLoading(false);
}
};
const buttonSubmit = (
<ButtonCustom
@@ -112,7 +112,9 @@ export default function EventCreate() {
return (
<>
<ViewWrapper>
<NewWrapper
footerComponent={<BoxButtonOnFooter>{buttonSubmit}</BoxButtonOnFooter>}
>
<StackCustom gap={"xs"}>
<TextInputCustom
placeholder="Masukkan nama event"
@@ -121,24 +123,15 @@ export default function EventCreate() {
onChangeText={(value: any) => setData({ ...data, title: value })}
/>
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || null}
onChange={(value: any) =>
setData({ ...data, eventMaster_TipeAcaraId: value })
}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
<TextAreaCustom
label="Deskripsi"
placeholder="Masukkan deskripsi event"
required
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
showCount
value={data?.deskripsi || ""}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
/>
<DateTimePickerCustom
@@ -168,22 +161,28 @@ export default function EventCreate() {
</TextCustom>
)}
<Spacing />
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || null}
onChange={(value: any) =>
setData({ ...data, eventMaster_TipeAcaraId: value })
}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
/>
</StackCustom>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukkan deskripsi event"
required
showCount
value={data?.deskripsi || ""}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
/>
{buttonSubmit}
</StackCustom>
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -1,258 +1,11 @@
import {
ButtonCustom,
DrawerCustom,
LoaderCustom,
Spacing,
TextAreaCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { useAuth } from "@/hooks/use-auth";
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar";
import {
apiForumCreateComment,
apiForumGetComment,
apiForumGetOne,
apiForumUpdateStatus,
} from "@/service/api-client/api-forum";
import { TypeForum_CommentProps } from "@/types/type-forum";
import { isBadContent } from "@/utils/badWordsIndonesia";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import DetailForum from "@/screens/Forum/DetailForum";
import DetailForum2 from "@/screens/Forum/DetailForum2";
export default function ForumDetail() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any | null>(null);
const [listComment, setListComment] = useState<TypeForum_CommentProps[] | null>(null);
const [isLoadingComment, setLoadingComment] = useState(false);
// Status
const [status, setStatus] = useState("");
const [text, setText] = useState("");
const [authorId, setAuthorId] = useState("");
const [dataId, setDataId] = useState("");
// Comentar
const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false);
const [commentId, setCommentId] = useState("");
const [commentAuthorId, setCommentAuthorId] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
const onLoadData = async (id: string) => {
try {
const response = await apiForumGetOne({ id });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
useEffect(() => {
onLoadListComment(id as string);
}, [id]);
const onLoadListComment = async (id: string) => {
try {
const response = await apiForumGetComment({
id: id as string,
});
setListComment(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
// Update Status
const handlerUpdateStatus = async (value: any) => {
try {
const response = await apiForumUpdateStatus({
id: id as string,
data: value,
});
if (response.success) {
setStatus(response.data);
setData({
...data,
ForumMaster_StatusPosting: {
status: response.data,
},
});
}
} catch (error) {
console.log("[ERROR]", error);
}
};
// Create Commentar
const handlerCreateCommentar = async () => {
if (isBadContent(text)) {
AlertWarning({});
return;
}
const newData = {
comment: text,
authorId: user?.id,
};
try {
setLoadingComment(true);
const response = await apiForumCreateComment({
id: id as string,
data: newData,
});
if (response.success) {
setText("");
const newComment = {
id: response.data.id,
isActive: response.data.isActive,
komentar: response.data.komentar,
createdAt: response.data.createdAt,
authorId: response.data.authorId,
Author: response.data.Author,
};
setListComment((prev) => [newComment, ...(prev || [])]);
setData({
...data,
count: data.count + 1,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingComment(false);
}
};
return (
<>
<ViewWrapper>
{!data && !listComment ? (
<LoaderCustom />
) : (
<>
{/* Box Posting */}
<Forum_BoxDetailSection
data={data}
onSetData={() => {
setOpenDrawer(true);
setStatus(data.ForumMaster_StatusPosting?.status);
setAuthorId(data.Author?.id);
setDataId(data.id);
}}
/>
{/* Area Commentar */}
{data?.ForumMaster_StatusPosting?.status === "Open" && (
<>
<TextAreaCustom
placeholder="Ketik diskusi anda..."
maxLength={1000}
showCount
value={text}
onChangeText={setText}
style={{
marginBottom: 0,
}}
/>
<ButtonCustom
isLoading={isLoadingComment}
style={{
alignSelf: "flex-end",
}}
onPress={() => {
handlerCreateCommentar();
}}
>
Balas
</ButtonCustom>
</>
)}
<Spacing height={40} />
{/* List Commentar */}
{_.isEmpty(listComment) ? (
<TextCustom align="center" color="gray" size={"small"}>
Tidak ada komentar
</TextCustom>
) : (
<TextCustom color="gray">Komentar :</TextCustom>
)}
<Spacing height={5} />
{listComment?.map((item: any, index: number) => (
<Forum_CommentarBoxSection
key={index}
data={item}
onSetData={(value) => {
setCommentId(value.setCommentId);
setOpenDrawerCommentar(value.setOpenDrawer);
setCommentAuthorId(value.setCommentAuthorId);
}}
/>
))}
</>
)}
</ViewWrapper>
{/* Posting Drawer */}
<DrawerCustom
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={dataId}
authorUsername={data?.Author?.username as string}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
authorId={authorId}
handlerUpdateStatus={(value: any) => {
handlerUpdateStatus(value);
}}
/>
</DrawerCustom>
{/* Commentar Drawer */}
<DrawerCustom
height={"auto"}
isVisible={openDrawerCommentar}
closeDrawer={() => setOpenDrawerCommentar(false)}
>
<Forum_MenuDrawerCommentar
id={commentId as string}
commentId={commentId}
commentAuthorId={commentAuthorId}
setIsDrawerOpen={() => {
setOpenDrawerCommentar(false);
}}
listComment={listComment}
setListComment={setListComment}
countComment={data?.count}
setCountComment={(val: any) => {
setData((prev: any) => ({
...prev,
count: val,
}));
}}
/>
</DrawerCustom>
{/* <DetailForum />; */}
<DetailForum2 />
</>
);
)
}

View File

@@ -1,12 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
import Forum_ViewBeranda from "@/screens/Forum/ViewBeranda";
import Forum_ViewBeranda2 from "@/screens/Forum/ViewBeranda2";
import Forum_ViewBeranda3 from "@/screens/Forum/ViewBeranda3";
export default function Forum() {
return (
<>
{/* <Forum_ViewBeranda /> */}
<Forum_ViewBeranda2 />
{/* <Forum_ViewBeranda2 /> */}
<Forum_ViewBeranda3 />
</>
);
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { StackCustom, ViewWrapper } from "@/components";
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
@@ -29,14 +29,14 @@ export default function Application() {
checkVersion();
userData(token as string);
syncUnreadCount();
}, [user?.id, token])
}, [user?.id, token]),
);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log(
"[Profile ID]>>",
JSON.stringify(response?.data?.Profile?.id, null, 2)
JSON.stringify(response?.data?.Profile?.id, null, 2),
);
setData(response.data);
@@ -61,12 +61,29 @@ export default function Application() {
if (data && data?.active === false) {
console.log("User is not active");
return <Redirect href={`/waiting-room`} />;
return (
<BasicWrapper>
<Redirect href={`/waiting-room`} />
</BasicWrapper>
);
}
if (data && data?.Profile === null) {
console.log("Profile is null");
return <Redirect href={`/profile/create`} />;
return (
<BasicWrapper>
<Redirect href={`/profile/create`} />
</BasicWrapper>
);
}
if (data && data?.masterUserRoleId !== "1") {
console.log("User is not admin");
return (
<BasicWrapper>
<Redirect href={`/admin/dashboard`} />
</BasicWrapper>
);
}
return (
@@ -89,7 +106,12 @@ export default function Application() {
/>
<ViewWrapper
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
footerComponent={
<TabSection

View File

@@ -1,56 +1,9 @@
import {
FloatingButton,
LoaderCustom,
ViewWrapper
} from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import Investment_BoxBerandaSection from "@/screens/Invesment/BoxBerandaSection";
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Investment_ScreenBursa from "@/screens/Invesment/ScreenBursa";
export default function InvestmentBursa() {
const [list, setList] = useState<any[] | null>(null);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [])
);
const onLoadList = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetAll({
category: "bursa"
});
// console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/investment/create")} />
}
>
{loadingList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<NoDataText />
) : (
list?.map((item: any, index: number) => (
<Investment_BoxBerandaSection id={item.id} data={item} key={index} />
))
)}
</ViewWrapper>
<>
<Investment_ScreenBursa />
</>
);
}

View File

@@ -1,83 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
LoaderCustom,
ProgressCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { useAuth } from "@/hooks/use-auth";
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useState } from "react";
import { View } from "react-native";
import Investment_ScreenMyHolding from "@/screens/Invesment/ScreenMyHolding";
export default function InvestmentMyHolding() {
const { user } = useAuth();
const [list, setList] = useState<any[] | null>(null);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [user?.id])
);
const onLoadList = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetAll({
category: "my-holding",
authorId: user?.id,
});
console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
};
return (
<ViewWrapper hideFooter>
{loadingList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<NoDataText />
) : (
list?.map((item, index) => (
<BaseBox
key={index}
paddingTop={7}
paddingBottom={7}
onPress={() =>
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
}
>
<StackCustom>
<TextCustom truncate={2}>{item?.title}</TextCustom>
<TextCustom>
Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
<TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
<ProgressCustom
label={`${item.progress}%`}
value={Number(item.progress)}
size="lg"
animated
color="primary"
/>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper>
<>
<Investment_ScreenMyHolding />
</>
);
}

View File

@@ -1,82 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Investment_StatusBox from "@/screens/Invesment/StatusBox";
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Investment_ScreenPortofolio from "@/screens/Invesment/ScreenPortofolio";
export default function InvestmentPortofolio() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState<any[]>([]);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id, activeCategory])
);
const onLoadData = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{loadingList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Investment_StatusBox
key={index}
data={item}
status={activeCategory as string}
href={`/investment/${item.id}/${activeCategory}/detail`}
/>
))
)}
</ViewWrapper>
<>
<Investment_ScreenPortofolio />
</>
);
}

View File

@@ -1,124 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { useAuth } from "@/hooks/use-auth";
import { apiInvestmentGetInvoice } from "@/service/api-client/api-investment";
import { GStyles } from "@/styles/global-styles";
import { formatChatTime } from "@/utils/formatChatTime";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Investment_ScreenTransaction from "@/screens/Invesment/ScreenTransaction";
export default function InvestmentTransaction() {
const { user } = useAuth();
const [list, setList] = useState<any>([]);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [user?.id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetInvoice({
authorId: user?.id as string,
category: "transaction",
});
console.log("[RESPONSE LIST]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const handlerColor = (status: string) => {
if (status === "menunggu") {
return "orange";
} else if (status === "proses") {
return "white";
} else if (status === "berhasil") {
return "green";
} else if (status === "gagal") {
return "red";
}
};
const handlePress = ({ id, status }: { id: string; status: string }) => {
if (status === "menunggu") {
router.push(`/investment/${id}/(transaction-flow)/invoice`);
} else if (status === "proses") {
router.push(`/investment/${id}/(transaction-flow)/process`);
} else if (status === "berhasil") {
router.push(`/investment/${id}/(transaction-flow)/success`);
} else if (status === "gagal") {
router.push(`/investment/${id}/(transaction-flow)/failed`);
}
};
return (
<ViewWrapper hideFooter>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<NoDataText/>
) : (
list.map((item: any, i: number) => (
<BaseBox
key={i}
paddingTop={7}
paddingBottom={7}
onPress={() => {
handlePress({
id: item.id,
status: _.lowerCase(item.statusInvoice),
});
}}
>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"xs"}>
<TextCustom truncate>{item?.title || "-"}</TextCustom>
<TextCustom color="gray" size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={5} style={{ alignItems: "flex-end" }}>
<StackCustom gap={"xs"}>
<TextCustom bold truncate>
Rp. {formatCurrencyDisplay(item?.nominal) || "-"}
</TextCustom>
<BadgeCustom
variant="light"
color={handlerColor(_.lowerCase(item.statusInvoice))}
style={GStyles.alignSelfFlexEnd}
>
{item?.statusInvoice || "-"}
</BadgeCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
<>
<Investment_ScreenTransaction />
</>
);
}

View File

@@ -1,58 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import { apiInvestmentGetDocument } from "@/service/api-client/api-investment";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Investment_ScreenListOfDocument from "@/screens/Invesment/Document/ScreenListDocument";
export default function InvestmentListOfDocument() {
const { id } = useLocalSearchParams();
console.log("ID >> ", id);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadListDocument();
}, [id])
);
const onLoadListDocument = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return (
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Investment_BoxDetailDocument
key={index}
title={item.title}
href={`/(file)/${item.fileId}`}
/>
))
)}
</ViewWrapper>
<>
<Investment_ScreenListOfDocument />
</>
);
}

View File

@@ -1,213 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import {
apiInvestmentDeleteDocument,
apiInvestmentGetDocument,
} from "@/service/api-client/api-investment";
import { AntDesign, Ionicons } from "@expo/vector-icons";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
import Investment_ScreenRecapOfDocument from "@/screens/Invesment/Document/ScreenRecapOfDocument";
export default function InvestmentRecapOfDocument() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerBox, setOpenDrawerBox] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
const [selectId, setSelectId] = useState<string | null>(null);
useFocusEffect(
useCallback(() => {
onLoadListDocument();
}, [id])
);
const onLoadListDocument = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
const handlerDeleteDocument = async () => {
try {
const response = await apiInvestmentDeleteDocument({
id: selectId as string,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil dihapus",
});
setList((prev: any[] | null) => {
if (!prev) return null;
return prev.filter((item: any) => item.id !== selectId);
});
setOpenDrawerBox(false);
setSelectId(null);
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal menghapus data",
});
}
};
return (
<>
<Stack.Screen
options={{
title: "Rekap Dokumen",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton
onPress={() => {
setOpenDrawer(true);
setOpenDrawerBox(false);
}}
/>
),
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Investment_BoxDetailDocument
key={index}
title={item.title}
leftIcon={
<Ionicons
name="ellipsis-horizontal-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
style={{
zIndex: 10,
alignSelf: "flex-end",
}}
onPress={() => {
setSelectId(item.id);
setOpenDrawerBox(true);
}}
/>
}
href={`/(file)/${item.fileId}`}
/>
))
)}
</ViewWrapper>
{/* Drawer On Header */}
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: (
<AntDesign
name="plus-circle"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
label: "Tambah Dokumen",
path: `/investment/${id}/(document)/add-document`,
},
]}
onPressItem={(item) => {
router.push(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
{/* Drawer On Box */}
<DrawerCustom
isVisible={openDrawerBox}
closeDrawer={() => setOpenDrawerBox(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconEdit />,
label: "Edit Dokumen",
path: `/investment/${selectId}/(document)/edit-document`,
},
{
icon: (
<Ionicons
name="trash-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
label: "Hapus Dokumen",
path: "" as any,
color: MainColor.red,
},
]}
onPressItem={(item) => {
if (item.path === ("" as any)) {
AlertDefaultSystem({
title: "Hapus Dokumen",
message: "Apakah anda yakin ingin menghapus dokumen ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {
handlerDeleteDocument();
},
});
} else {
router.push(item.path as any);
}
setOpenDrawerBox(false);
}}
/>
</DrawerCustom>
<Investment_ScreenRecapOfDocument />
</>
);
}

View File

@@ -1,100 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
BaseBox,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Investment_ScreenListOfNews from "@/screens/Invesment/News/ScreenListOfNews";
import { useLocalSearchParams } from "expo-router";
export default function InvestmentListOfNews() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetNews({
id: id as string,
category: "all-news",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Daftar Berita",
headerLeft: () => <BackButton />,
// headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<BaseBox
key={index}
paddingBlock={5}
href={`/investment/[id]/(news)/${item.id}`}
>
<TextCustom bold>{item.title}</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tambah Berita",
path: `/investment/${id}/add-news`,
icon: <IconPlus />,
},
]}
onPressItem={(item) => {
router.push(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
<Investment_ScreenListOfNews investmentId={id as string} />
);
}

View File

@@ -1,101 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
BaseBox,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { apiInvestmentGetNews } from "@/service/api-client/api-investment";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Investment_ScreenRecapOfNews from "@/screens/Invesment/News/ScreenRecapOfNews";
import { useLocalSearchParams } from "expo-router";
export default function InvestmentRecapOfNews() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetNews({
id: id as string,
category: "all-news",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Rekap Berita",
headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<BaseBox
key={index}
paddingBlock={5}
href={`/investment/[id]/(news)/${item.id}`}
>
<TextCustom bold>{item.title}</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tambah Berita",
path: `/investment/${id}/add-news`,
icon: <IconPlus />,
},
]}
onPressItem={(item) => {
router.push(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
<Investment_ScreenRecapOfNews investmentId={id as string} />
);
}

View File

@@ -1,230 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
Grid,
InformationBox,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import CopyButton from "@/components/Button/CoyButton";
import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id";
import {
apiInvestmentGetInvoice,
apiInvestmentUpdateInvoice,
} from "@/service/api-client/api-investment";
import { uploadFileService } from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
import Investment_ScreenInvoice from "@/screens/Invesment/ScreenInvoice";
export default function InvestmentInvoice() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>({});
const [image, setImage] = useState<IFileData>({
name: "",
uri: "",
size: 0,
});
const [isLoading, setIsLoading] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetInvoice({
id: id as string,
category: "invoice",
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlerSubmitUpdate = async () => {
try {
setIsLoading(true);
const responseUploadImage = await uploadFileService({
dirId: DIRECTORY_ID.investasi_bukti_transfer,
imageUri: image?.uri,
});
if (!responseUploadImage?.data?.id) {
Toast.show({
type: "error",
text1: "Gagal mengunggah bukti transfer",
});
return;
}
const response = await apiInvestmentUpdateInvoice({
id: id as string,
data: {
imageId: responseUploadImage?.data?.id,
},
status: "proses",
});
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil mengunggah bukti transfer",
});
router.push(`/investment/${id}/(transaction-flow)/process`);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return (
<>
<ViewWrapper>
<StackCustom>
<InformationBox text="Mohon transfer ke rekening dibawah" />
<BaseBox>
<StackCustom gap={"xs"}>
<Grid>
<Grid.Col span={4}>
<TextCustom>Bank</TextCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom>{data?.MasterBank?.namaBank}</TextCustom>
</Grid.Col>
</Grid>
<Spacing height={10} />
<Grid>
<Grid.Col span={4}>
<TextCustom>Nama Akun</TextCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom>{data?.MasterBank?.namaAkun}</TextCustom>
</Grid.Col>
</Grid>
<BaseBox backgroundColor={MainColor.soft_darkblue}>
<Grid containerStyle={{ justifyContent: "center" }}>
<Grid.Col
span={8}
style={{
justifyContent: "center",
}}
>
<TextCustom size="xlarge" bold color="yellow">
{data?.MasterBank?.norek}
</TextCustom>
</Grid.Col>
<Grid.Col
span={4}
style={{
alignItems: "flex-end",
}}
>
<CopyButton textToCopy={data?.MasterBank?.norek} />
</Grid.Col>
</Grid>
</BaseBox>
</StackCustom>
</BaseBox>
<BaseBox>
<StackCustom gap={"xs"}>
<TextCustom>Jumlah Transaksi</TextCustom>
<Spacing height={10} />
<BaseBox backgroundColor={MainColor.soft_darkblue}>
<Grid containerStyle={{ justifyContent: "center" }}>
<Grid.Col
span={8}
style={{
justifyContent: "center",
}}
>
<TextCustom size="xlarge" bold color="yellow">
Rp. {formatCurrencyDisplay(data?.nominal)}
</TextCustom>
</Grid.Col>
<Grid.Col
span={4}
style={{
alignItems: "flex-end",
}}
>
<CopyButton textToCopy={data?.nominal} />
</Grid.Col>
</Grid>
</BaseBox>
</StackCustom>
</BaseBox>
<BaseBox>
<StackCustom>
<TextCustom align="center">
Upload bukti transfer anda.
</TextCustom>
{image ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 10,
paddingInline: 20,
}}
>
<TextCustom bold align="center" truncate>
{image?.name}
</TextCustom>
</View>
) : (
<TextCustom align="center">
Tidak ada gambar yang diunggah
</TextCustom>
)}
<ButtonCenteredOnly
onPress={() => {
pickFile({
allowedType: "image",
setImageUri(file: any) {
setImage(file);
},
});
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
<ButtonCustom
isLoading={isLoading}
disabled={!image || isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Saya Sudah Transfer
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>
<Investment_ScreenInvoice />
</>
);
}

View File

@@ -56,7 +56,6 @@ export default function InvestmentSelectBank() {
});
if (response.success) {
console.log("[RESPONSE >>]", response);
const invoiceId = response.data.id;
const delStorage = await AsyncStorage.removeItem(

View File

@@ -73,7 +73,6 @@ export default function InvestmentDetailStatus() {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({

View File

@@ -1,15 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
TextInputCustom
} from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
@@ -198,7 +199,15 @@ export default function InvestmentEdit() {
};
return (
<ViewWrapper>
<NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded
@@ -253,7 +262,8 @@ export default function InvestmentEdit() {
/>
<TextInputCustom
disabled
iconLeft="Rp."
// disabled
required
placeholder="0"
label="Total Lembar"
@@ -339,11 +349,7 @@ export default function InvestmentEdit() {
)}
<Spacing />
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing height={50} />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -72,7 +72,6 @@ export default function InvestmentDetail() {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({

View File

@@ -1,67 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { apiInvestmentGetInvestorById } from "@/service/api-client/api-investment";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Investment_ScreenInvestor from "@/screens/Invesment/ScreenInvestor";
import { useLocalSearchParams } from "expo-router";
export default function InvestmentInvestor() {
const { id } = useLocalSearchParams();
const [list, setList] = useState<any[] | null>(null);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetInvestorById({
id: id as string,
})
console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
}
return (
<>
<ViewWrapper>
{loadingList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<NoDataText />
) : (
list?.map((item: any, index: number) => (
<BoxWithHeaderSection key={index}>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
name={item?.Author?.username}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
/>
<TextCustom bold>
Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper>
</>
<Investment_ScreenInvestor investmentId={id as string} />
);
}

View File

@@ -1,18 +1,19 @@
import {
BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
CenterCustom,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
@@ -184,7 +185,19 @@ export default function InvestmentCreate() {
// const [coba, setCoba] = useState("");
return (
<ViewWrapper>
<NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded image={image as string} />
@@ -264,7 +277,9 @@ export default function InvestmentCreate() {
<StackCustom gap={0}>
<TextInputCustom
disabled
iconLeft="Rp."
// disabled
editable={false}
required
placeholder="0"
label="Total Lembar"
@@ -291,7 +306,7 @@ export default function InvestmentCreate() {
/>
{loadingMaster ? (
<LoaderCustom />
<CustomSkeleton height={50} />
) : (
<SelectCustom
required
@@ -313,7 +328,7 @@ export default function InvestmentCreate() {
)}
{loadingMaster ? (
<LoaderCustom />
<CustomSkeleton height={50} />
) : (
<SelectCustom
required
@@ -335,7 +350,7 @@ export default function InvestmentCreate() {
)}
{loadingMaster ? (
<LoaderCustom />
<CustomSkeleton height={50} />
) : (
<SelectCustom
required
@@ -357,15 +372,8 @@ export default function InvestmentCreate() {
)}
<Spacing />
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing height={50} />
</ViewWrapper>
{/* <Spacing height={50} /> */}
</NewWrapper>
);
}

View File

@@ -1,57 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2";
export default function JobArchive() {
const { user } = useAuth();
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({
category: "archive",
authorId: user?.id,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper hideFooter>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
) : (
listData.map((item, index) => (
<BaseBox
key={index}
paddingTop={20}
paddingBottom={20}
href={`/job/${item.id}/archive`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title || "-"}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
<>
<Job_ScreenArchive2 />
</>
);
}

View File

@@ -1,83 +1,10 @@
import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
FloatingButton,
LoaderCustom,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda";
import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2";
export default function JobBeranda() {
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData(search);
}, [search])
);
const onLoadData = async (search: string) => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({ search, category: "beranda" });
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handleSearch = (search: string) => {
setSearch(search);
onLoadData(search);
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/job/create")} />
}
headerComponent={
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
}
>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada lowongan</TextCustom>
) : (
listData.map((item, index) => (
<BoxWithHeaderSection
key={index}
onPress={() => router.push(`/job/${item.id}`)}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
name={item?.Author?.username}
/>
<TextCustom truncate={2} align="center" bold size="large">
{item?.title || "-"}
</TextCustom>
</StackCustom>
<Spacing />
</BoxWithHeaderSection>
))
)}
<Spacing />
</ViewWrapper>
<>
<Job_ScreenBeranda2 />
</>
);
}

View File

@@ -1,91 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiJobGetByStatus } from "@/service/api-client/api-job";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Job_MainViewStatus from "@/screens/Job/MainViewStatus";
import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2";
export default function JobStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
console.log("STATUS", status);
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState<any[]>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id, activeCategory])
);
const onLoadData = async () => {
try {
setIsLoadList(true);
const response = await apiJobGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<>
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">
Tidak ada data {activeCategory}
</TextCustom>
) : (
listData.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
{/* <Job_MainViewStatus /> */}
<Job_MainViewStatus2 />
</>
);
}

View File

@@ -1,13 +1,14 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
NewWrapper,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper
TextInputCustom
} from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
@@ -99,16 +100,17 @@ export default function JobCreate() {
const buttonSubmit = () => {
return (
<>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
Simpan
</ButtonCustom>
<Spacing />
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
</>
);
};
return (
<ViewWrapper>
<NewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
@@ -160,9 +162,7 @@ export default function JobCreate() {
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
{buttonSubmit()}
</StackCustom>
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,248 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1";
import ScreenNotification_V2 from "@/screens/Notification/ScreenNotification_V2";
const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find(
(c) => c.value === value
);
return category?.label;
};
const fixPath = ({
deepLink,
categoryApp,
}: {
deepLink: string;
categoryApp: string;
}) => {
if (categoryApp === "OTHER") {
return deepLink;
}
const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
categoryApp
)}`;
console.log("Fix Path", fixedPath);
return fixedPath;
};
const BoxNotification = ({
data,
activeCategory,
}: {
data: any;
activeCategory: string | null;
}) => {
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
const { markAsRead } = useNotificationStore();
export default function Notification() {
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
// console.log(
// "Notification >",
// selectedCategory(activeCategory as string)
// );
const newPath = fixPath({
deepLink: data.deepLink,
categoryApp: data.kategoriApp,
});
router.navigate(newPath as any);
selectedCategory(activeCategory as string);
if (!data.isRead) {
markAsRead(data.id);
}
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function Notifications() {
const { user } = useAuth();
const { category } = useLocalSearchParams<{ category?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
category || "event"
);
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
useFocusEffect(
useCallback(() => {
fecthData();
}, [activeCategory])
);
const fecthData = async () => {
try {
setLoading(true);
const response = await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
});
if (response.success) {
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("Error Notification", error);
} finally {
setLoading(false);
}
};
const onRefresh = () => {
setRefreshing(true);
fecthData();
setRefreshing(false);
};
return (
<>
<Stack.Screen
options={{
title: "Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
path: "",
},
]}
onPressItem={(item: any) => {
console.log("Item", item.value);
if (item.value === "read-all") {
AlertDefaultSystem({
title: "Tandai Semua Dibaca",
message:
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
markAsReadAll(user?.id as any);
const data = _.cloneDeep(listData);
data.forEach((e) => {
e.isRead = true;
});
setListData(data);
onRefresh();
setOpenDrawer(false);
},
});
}
}}
/>
</DrawerCustom>
<ScreenNotification_V2 />
{/* <ScreenNotification_V1 /> */}
</>
);
}

View File

@@ -7,6 +7,7 @@ import {
CenterCustom,
Grid,
InformationBox,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
@@ -120,7 +121,7 @@ export default function PortofolioCreate() {
};
return (
<ViewWrapper
<NewWrapper
footerComponent={
<Portofolio_ButtonCreate
id={id as string}
@@ -357,8 +358,8 @@ export default function PortofolioCreate() {
setDataMedsos({ ...dataMedsos, youtube: value })
}
/>
<Spacing />
{/* <Spacing /> */}
</StackCustom>
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -4,14 +4,15 @@ import {
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
@@ -238,7 +239,7 @@ export default function PortofolioEdit() {
return !dataArray.some(
(item: any) =>
!item.MasterSubBidangBisnis.id ||
item.MasterSubBidangBisnis.id.trim() === ""
item.MasterSubBidangBisnis.id.trim() === "",
);
}
@@ -319,16 +320,16 @@ export default function PortofolioEdit() {
if (!bidangBisnis || !subBidangBisnis) {
return (
<>
<ViewWrapper>
<ActivityIndicator size="large" color={MainColor.yellow} />
</ViewWrapper>
<NewWrapper>
<ListSkeletonComponent height={80} />
</NewWrapper>
</>
);
}
return (
<>
<ViewWrapper footerComponent={buttonUpdate}>
<NewWrapper footerComponent={buttonUpdate}>
<StackCustom gap={"xs"}>
<TextInputCustom
required
@@ -471,7 +472,7 @@ export default function PortofolioEdit() {
/>
<Spacing />
</StackCustom>
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -1,28 +1,9 @@
import { TextCustom, ViewWrapper } from "@/components";
import Portofolio_BoxView from "@/screens/Portofolio/BoxPortofolioView";
import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import ViewListPortofolio from "@/screens/Portofolio/ViewListPortofolio";
export default function ListPortofolio() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any[]>([]);
useFocusEffect(
useCallback(() => {
onLoadPortofolio(id as string);
}, [id])
);
const onLoadPortofolio = async (id: string) => {
const response = await apiGetPortofolio({ id: id });
setData(response.data);
};
return (
<ViewWrapper>
{data ? data?.map((item: any, index: number) => (
<Portofolio_BoxView key={index} data={item} />
)) : <TextCustom>Tidak ada portofolio</TextCustom>}
</ViewWrapper>
<>
<ViewListPortofolio />
</>
);
}

View File

@@ -29,7 +29,6 @@ export default function ProfileDetailBlocked() {
const fetchData = async () => {
const response = await apiGetBlockedById({ id: String(id) });
// console.log("[RESPONSE >>]", JSON.stringify(response, null, 2));
setData(response.data);
};

View File

@@ -1,115 +1,11 @@
import {
AvatarComp,
ClickableCustom,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiAllUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import UserSearchMainView from "@/screens/UserSeach/MainView";
import UserSearchMainView_V2 from "@/screens/UserSeach/MainView_V2";
export default function UserSearch() {
const [data, setData] = useState<any[]>([]);
const [search, setSearch] = useState<string>("");
const [isLoadList, setIsLoadList] = useState(false);
useEffect(() => {
onLoadData(search);
}, [search]);
const onLoadData = async (search: string) => {
try {
setIsLoadList(true);
const response = await apiAllUser({ search: search });
console.log("[DATA USER] >", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("Error fetching data", error);
} finally {
setIsLoadList(false);
}
};
const handleSearch = (search: string) => {
setSearch(search);
onLoadData(search);
};
return (
<>
<ViewWrapper
headerComponent={
<TextInputCustom
value={search}
onChangeText={handleSearch}
iconLeft={
<Ionicons
name="search"
size={ICON_SIZE_SMALL}
color={MainColor.placeholder}
/>
}
placeholder="Cari Pengguna"
borderRadius={50}
containerStyle={{ marginBottom: 0 }}
/>
}
>
<StackCustom>
{isLoadList ? (
<LoaderCustom />
) : !_.isEmpty(data) ? (
data?.map((e, index) => {
return (
<ClickableCustom
key={index}
onPress={() => {
console.log("Ke Profile");
router.push(`/profile/${e?.Profile?.id}`);
}}
>
<Grid>
<Grid.Col span={2}>
<AvatarComp fileId={e?.Profile?.imageId} size="base" />
</Grid.Col>
<Grid.Col span={9}>
<StackCustom gap={"sm"}>
<TextCustom size="large">{e?.username}</TextCustom>
<TextCustom size="small">+{e?.nomor}</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
}}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</ClickableCustom>
);
})
) : (
<TextCustom align="center">Tidak ditemukan</TextCustom>
)}
</StackCustom>
<Spacing height={50} />
</ViewWrapper>
{/* <UserSearchMainView /> */}
<UserSearchMainView_V2 />
</>
);
}

View File

@@ -1,59 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState, useCallback } from "react";
import Voting_ScreenContribution from "@/screens/Voting/ScreenContribution";
export default function VotingContribution() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<ViewWrapper hideFooter>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontribusi</TextCustom>
) : listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}/contribution`}
/>
))}
</ViewWrapper>
);
return <Voting_ScreenContribution />;
}

View File

@@ -1,77 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { useAuth } from "@/hooks/use-auth";
import { useCallback, useState } from "react";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import Voting_ScreenHistory from "@/screens/Voting/ScreenHistory";
export default function VotingHistory() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
};
return (
<ViewWrapper
hideFooter
headerComponent={
<TabsTwoButtonCustom
leftValue="all"
rightValue="main"
leftText="Semua Riwayat"
rightText="Riwayat Saya"
activeCategory={activeCategory}
handlePress={handlePress}
/>
}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada riwayat</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
key={index}
id={item.id}
data={item}
href={`/voting/${item.id}/history`}
/>
))
)}
</ViewWrapper>
<>
<Voting_ScreenHistory />
</>
);
}

View File

@@ -1,71 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
FloatingButton,
LoaderCustom,
SearchInput,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Voting_ScreenBeranda from "@/screens/Voting/ScreenBeranda";
export default function VotingBeranda() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
search,
category: "beranda",
userLoginId: user?.id,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/voting/create")} />
}
headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}`}
/>
))
)}
</ViewWrapper>
<>
<Voting_ScreenBeranda />
</>
);
}

View File

@@ -1,106 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
LoaderCustom,
ScrollableCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Voting_ScreenStatus from "@/screens/Voting/ScreenStatus";
export default function VotingStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory, id])
);
async function onLoadData() {
try {
setLoadingGetData(true);
const response = await apiVotingGetByStatus({
id: id as string,
status: activeCategory!,
});
setListData(response.data);
} catch (error) {
console.log(error);
} finally {
setLoadingGetData(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, i: number) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/voting/${item.id}/${activeCategory}/detail`}
>
<StackCustom>
<TextCustom align="center" bold truncate={2} size="large">
{item?.title || ""}
</TextCustom>
<BadgeCustom
style={{ width: "70%", alignSelf: "center" }}
variant="light"
>
{item?.awalVote &&
dateTimeView({
date: item?.awalVote,
withoutTime: true,
})}{" "}
-{" "}
{item?.akhirVote &&
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
</BadgeCustom>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper>
<>
<Voting_ScreenStatus />
</>
);
}

View File

@@ -51,8 +51,6 @@ export default function VotingDetailStatus() {
setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
}

View File

@@ -5,13 +5,14 @@ import {
ButtonCustom,
CenterCustom,
LoaderCustom,
NewWrapper,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
TextInputCustom
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
@@ -34,7 +35,7 @@ interface IEditData {
Voting_DaftarNamaVote?: [
{
value?: string;
}
},
];
}
@@ -47,7 +48,7 @@ export default function VotingEdit() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -188,9 +189,9 @@ export default function VotingEdit() {
};
return (
<ViewWrapper footerComponent={buttonSubmit()}>
<NewWrapper footerComponent={buttonSubmit()}>
{loadingGetData ? (
<LoaderCustom />
<ListSkeletonComponent />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
@@ -210,7 +211,7 @@ export default function VotingEdit() {
onChangeText={(text) => setData({ ...data, deskripsi: text })}
/>
<Spacing />
<DateTimePickerCustom
minimumDate={new Date(Date.now())}
@@ -255,7 +256,7 @@ export default function VotingEdit() {
}
</TextCustom>
)}
<Spacing />
</StackCustom>
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
@@ -270,7 +271,7 @@ export default function VotingEdit() {
...(data as any),
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
(item: any, i: any) =>
i === index ? { ...item, value } : item
i === index ? { ...item, value } : item,
),
})
}
@@ -327,6 +328,6 @@ export default function VotingEdit() {
<Spacing />
</StackCustom>
)}
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,69 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Voting_ScreenListOfContributor from "@/screens/Voting/ScreenListOfContributor";
export default function Voting_ListOfContributor() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setIsLoadData(true);
const response = await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontributor</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId || ""}
name={item?.Author?.username || "Username"}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
rightComponent={
<BadgeCustom style={{ alignSelf: "flex-end" }}>
{item?.Voting_DaftarNamaVote?.value}
</BadgeCustom>
}
/>
</BaseBox>
))
)}
</ViewWrapper>
);
export default function VotingListOfContributor() {
return <Voting_ScreenListOfContributor />;
}

View File

@@ -3,11 +3,12 @@ import {
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
NewWrapper,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet";
@@ -79,7 +80,7 @@ export default function VotingCreate() {
type: "success",
text1: "Data berhasil disimpan",
});
router.replace("/(application)/(user)/voting/(tabs)/status?status=review");
router.replace("/voting/(tabs)/status?status=review");
} else {
Toast.show({
type: "error",
@@ -106,7 +107,7 @@ export default function VotingCreate() {
};
return (
<ViewWrapper footerComponent={buttonSubmit()}>
<NewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}>
<TextInputCustom
label="Judul Voting"
@@ -142,7 +143,6 @@ export default function VotingCreate() {
}
/>
{listVote.map((item, index) => (
<TextInputCustom
key={index}
@@ -153,8 +153,8 @@ export default function VotingCreate() {
onChangeText={(value: any) =>
setListVote(
listVote.map((item, i) =>
i === index ? { ...item, value } : item
)
i === index ? { ...item, value } : item,
),
)
}
/>
@@ -198,6 +198,6 @@ export default function VotingCreate() {
<Spacing />
</StackCustom>
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -8,6 +8,8 @@ import {
} from "@/components";
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
import NavbarMenu from "@/components/Drawer/NavbarMenu";
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
import NavbarMenu_V3 from "@/components/Drawer/NavbarMenu_V3";
import { AccentColor, MainColor } from "@/constants/color-palet";
import {
ICON_SIZE_MEDIUM,
@@ -20,6 +22,10 @@ import {
adminListMenu,
superAdminListMenu,
} from "@/screens/Admin/listPageAdmin";
import {
adminListMenu_V2,
superAdminListMenu_V2,
} from "@/screens/Admin/listPageAdmin_V2";
import { GStyles } from "@/styles/global-styles";
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
@@ -148,6 +154,24 @@ export default function AdminLayout() {
}
onClose={() => setOpenDrawerNavbar(false)}
/>
{/* <NavbarMenu_V2
items={
user?.masterUserRoleId === "2"
? adminListMenu_V2
: superAdminListMenu_V2
}
onClose={() => setOpenDrawerNavbar(false)}
/> */}
{/* <NavbarMenu_V3
items={
user?.masterUserRoleId === "2"
? adminListMenu_V2
: superAdminListMenu_V2
}
onClose={() => setOpenDrawerNavbar(false)}
/> */}
</StackCustom>
</DrawerAdmin>
@@ -198,7 +222,7 @@ export default function AdminLayout() {
// size={ICON_SIZE_SMALL}
// color={MainColor.white}
// />
<AdminNotificationBell/>
<AdminNotificationBell />
),
path: "/admin/notification",
},

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,213 +1,10 @@
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find(
(c) => c.value === value
);
return category?.label;
};
const BoxNotification = ({
data,
activeCategory,
}: {
data: any;
activeCategory: string | null;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
console.log(
"Notification >",
selectedCategory(activeCategory as string)
);
router.push(data.deepLink);
markAsRead(data.id);
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
import Admin_ScreenNotification from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin";
import Admin_ScreenNotification2 from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin2";
export default function AdminNotification() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
useFocusEffect(
useCallback(() => {
fecthData();
}, [activeCategory])
);
const fecthData = async () => {
try {
setLoading(true);
const response = await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
});
if (response.success) {
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("Error Notification", error);
} finally {
setLoading(false);
}
};
const onRefresh = () => {
setRefreshing(true);
fecthData();
setRefreshing(false);
};
return (
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
path: "",
},
]}
onPressItem={(item: any) => {
console.log("Item", item.value);
if (item.value === "read-all") {
AlertDefaultSystem({
title: "Tandai Semua Dibaca",
message:
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
markAsReadAll(user?.id as any);
const data = _.cloneDeep(listData);
data.forEach((e) => {
e.isRead = true;
});
setListData(data);
onRefresh();
setOpenDrawer(false);
},
});
}
}}
/>
</DrawerCustom>
<Admin_ScreenNotification2 />
</>
);
}

View File

@@ -10,14 +10,10 @@ import {
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { useAuth } from "@/hooks/use-auth";
import { routeUser } from "@/lib/routeApp";
import {
apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus,
} from "@/service/api-admin/api-admin-user-access";
import {
apiNotificationsSendById
} from "@/service/api-notifications";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
@@ -70,20 +66,6 @@ export default function AdminUserAccessDetail() {
text1: "Update aktifasi berhasil ",
});
if (data.active === false) {
await apiNotificationsSendById({
data: {
title: "Akun anda telah diaktifkan",
body: "Selamat menjelajahi HIConnect",
userLoginId: user?.id || "",
kategoriApp: "OTHER",
type: "announcement",
deepLink: routeUser.home,
},
id: id as string,
});
}
router.back();
} catch (error) {
console.log("[ERROR UPDATE STATUS]", error);

View File

@@ -1,139 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
CenterCustom,
Divider,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
import { Ionicons } from "@expo/vector-icons";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Admin_ScreenUserAccess } from "@/screens/Admin/User-Access/ScreenUserAccess";
export default function AdminUserAccess() {
const [listData, setListData] = useState<any[] | null>(null);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
const response = await apiAdminUserAccessGetAll({
search: search,
category: "only-user",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR LOAD DATA]", error);
}
};
const rightComponent = () => {
return (
<>
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari User"
onChangeText={(text) => setSearch(text)}
/>
</>
);
};
return (
<>
<ViewWrapper
headerComponent={
<AdminComp_BoxTitle
title="User Access"
rightComponent={rightComponent()}
/>
}
>
<GridViewCustomSpan
span1={2}
span2={5}
span3={5}
component1={
<TextCustom align="center" bold>
Aksi
</TextCustom>
}
component2={<TextCustom bold>Username</TextCustom>}
component3={
<TextCustom align="center" bold>
Status Akses
</TextCustom>
}
/>
<Divider />
<StackCustom>
{_.isEmpty(listData) ? (
<TextCustom align="center" color="gray" size={"small"}>
Tidak ada data
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<GridViewCustomSpan
key={index}
span1={2}
span2={5}
span3={5}
component1={
<CenterCustom>
<Ionicons
onPress={() =>
router.push(`/admin/user-access/${item?.id}`)
}
name="open"
size={ICON_SIZE_XLARGE}
color={MainColor.yellow}
/>
</CenterCustom>
// <ButtonCustom
// onPress={() =>
// router.push(`/admin/user-access/${item?.id}`)
// }
// >
// Detail
// </ButtonCustom>
}
component2={
<TextCustom bold truncate>
{item?.username || "-"}
</TextCustom>
}
component3={
<CenterCustom>
{item?.active ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</CenterCustom>
}
style3={{ alignItems: "center", justifyContent: "center" }}
/>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenUserAccess />;
}

View File

@@ -1,11 +1,21 @@
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
import { Stack } from "expo-router";
import { router, Stack } from "expo-router";
export default function NotFoundScreen() {
// Setelah (dengan penanganan):
const handleBack = () => {
if (router.canGoBack()) {
router.back();
} else {
// Alternatif action ketika tidak bisa kembali
router.replace('/'); // atau navigasi ke halaman default
}
};
return (
<>
<Stack.Screen
options={{ headerShown: false, headerLeft: () => <BackButton /> }}
options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
/>
<ViewWrapper>
<StackCustom
@@ -17,7 +27,7 @@ export default function NotFoundScreen() {
404
</TextCustom>
<TextCustom size="large" bold>
Sorry, File Not Found
Sorry, Page Not Found
</TextCustom>
</StackCustom>
</ViewWrapper>

View File

@@ -1,4 +1,3 @@
// DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
@@ -7,7 +6,14 @@ import DateTimePicker, {
DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
import React, { useCallback, useState } from "react";
import { Pressable, StyleProp, Text, View, ViewStyle } from "react-native";
import {
Keyboard,
Pressable,
StyleProp,
Text,
View,
ViewStyle,
} from "react-native";
import Grid from "../Grid/GridCustom";
import TextCustom from "../Text/TextCustom";
@@ -53,7 +59,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
const [selectedDate, setSelectedDate] = useState<Date>(value as any);
const [selectedTime, setSelectedTime] = useState<Date>(value as any);
console.log("Date Android", value)
console.log("Date Android", value);
// Fungsi untuk menggabungkan tanggal dan waktu
const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
@@ -62,7 +68,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
time.getHours(),
time.getMinutes(),
time.getSeconds(),
time.getMilliseconds()
time.getMilliseconds(),
);
return combined;
}, []);
@@ -92,10 +98,12 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
};
const toggleDatePicker = () => {
Keyboard.dismiss();
setShowDate(!showDate);
};
const toggleTimePicker = () => {
Keyboard.dismiss();
setShowTime(!showTime);
};

View File

@@ -1,13 +1,22 @@
// DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import DateTimePicker, {
DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
import dayjs from "dayjs";
import React, { useState } from "react";
import { Button, StyleProp, Text, View, ViewStyle } from "react-native";
import React, { useState, useRef } from "react";
import {
Button,
StyleProp,
Text,
View,
ViewStyle,
Keyboard,
TouchableOpacity,
Modal,
} from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom";
import TextCustom from "../Text/TextCustom";
@@ -50,20 +59,35 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
}) => {
const [show, setShow] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
value as any
value as any,
);
// State sementara untuk menyimpan nilai yang dipilih
const [tempSelectedDate, setTempSelectedDate] = useState<Date | undefined>(
value as any,
);
const handleConfirm = (event: any, date?: Date) => {
if (event.type === "set" && date !== undefined) {
setSelectedDate(date);
onChange(date as any);
// Hanya perbarui state sementara, bukan state utama
setTempSelectedDate(date);
}
};
const handlePress = () => {
// Sembunyikan keyboard sebelum menampilkan date picker
Keyboard.dismiss();
// Set state sementara ke nilai saat ini
setTempSelectedDate(selectedDate);
setShow(!show);
};
// Fungsi untuk menangani klik di luar area picker
const handleOutsidePress = () => {
if (show) {
setShow(false);
}
};
return (
<>
<ClickableCustom
@@ -112,84 +136,125 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
))}
</ClickableCustom>
{show && (
<>
<View
<Modal
animationType="fade"
transparent={true}
visible={show}
onRequestClose={() => setShow(false)}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.5)", // Efek blur dengan background semi-transparan
}}
>
<TouchableOpacity
style={{
position: "absolute",
zIndex: 15,
backgroundColor: "white",
borderRadius: 8,
padding: 10,
// top: 0,
top: 0,
bottom: 0,
left: 0,
right: 0,
borderColor: "#ccc",
}}
activeOpacity={1}
onPress={handleOutsidePress}
>
<View style={{ flex: 1 }} />
</TouchableOpacity>
<View
style={{
zIndex: 15,
backgroundColor: MainColor.white_gray,
borderRadius: 16,
padding: 20,
marginHorizontal: 20,
width: "95%",
maxWidth: 400,
borderColor: MainColor.placeholder,
borderWidth: 1,
}}
onStartShouldSetResponder={() => true} // Mencegah event bubbling ke TouchableOpacity induk
onResponderRelease={() => {}} // Handler kosong untuk mencegah event bubbling
>
{/* <View style={{ alignItems: "flex-start" }}>
<Ionicons
name="close"
size={20}
color="black"
onPress={() => {
setShow(false);
setSelectedDate(undefined);
{/* <View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
}}
>
<Text
style={{
fontSize: 18,
fontWeight: "bold",
color: MainColor.black,
}}
/>
>
Pilih Tanggal & Waktu
</Text>
<TouchableOpacity onPress={() => setShow(false)}>
<Ionicons
name="close"
size={24}
color={AccentColor.blackgray}
/>
</TouchableOpacity>
</View> */}
<DateTimePicker
value={selectedDate || new Date()}
value={tempSelectedDate || new Date()}
mode={"datetime"}
display="spinner"
display="inline"
onChange={handleConfirm}
minimumDate={minimumDate}
maximumDate={maximumDate}
themeVariant="light"
/>
<View style={{ flexDirection: "row", gap: 10 }}>
<ClickableCustom
<View style={{ flexDirection: "row", gap: 10, marginTop: 15 }}>
<TouchableOpacity
onPress={() => {
setShow(false)
setSelectedDate(undefined)
setShow(false);
// Kembalikan ke nilai sebelumnya jika batal
setTempSelectedDate(selectedDate);
}}
style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 12,
borderRadius: 10,
backgroundColor: MainColor.placeholder,
marginTop: 10,
width: "48%",
}}
>
<TextCustom color="black">Batal</TextCustom>
</ClickableCustom>
</TouchableOpacity>
<ClickableCustom
<TouchableOpacity
onPress={() => {
setShow(false)
onChange(selectedDate as any)
// Simpan nilai yang dipilih ke state utama hanya saat OK ditekan
setSelectedDate(tempSelectedDate);
onChange(tempSelectedDate as any);
setShow(false);
}}
style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 12,
borderRadius: 10,
backgroundColor: MainColor.darkblue,
marginTop: 10,
width: "48%",
}}
>
<TextCustom>OK</TextCustom>
</ClickableCustom>
</TouchableOpacity>
</View>
</View>
</>
)}
</View>
</Modal>
</>
);
};

View File

@@ -0,0 +1,276 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router, usePathname } from "expo-router";
import React, { useEffect, useRef, useState } from "react";
import {
Animated,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export interface NavbarItem {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
}[];
initiallyOpened?: boolean;
}
interface NavbarMenuProps {
items: NavbarItem[];
onClose?: () => void;
}
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [activeLink, setActiveLink] = useState<string | null>(null);
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
// Normalisasi path: hapus trailing slash
const normalizePath = (path: string) => path.replace(/\/+$/, "");
const normalizedPathname = pathname ? normalizePath(pathname) : "";
// Set activeLink saat pathname berubah
useEffect(() => {
if (normalizedPathname) {
setActiveLink(normalizedPathname);
}
}, [normalizedPathname]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
);
};
return (
<View
style={{
// flex: 1,
// backgroundColor: MainColor.black,
marginBottom: 20,
}}
>
<ScrollView
contentContainerStyle={{
paddingVertical: 10, // Opsional: tambahkan padding
}}
// showsVerticalScrollIndicator={false} // Opsional: sembunyikan indikator scroll
>
{items.map((item) => (
<MenuItem
key={item.label}
item={item}
onClose={onClose}
activeLink={activeLink}
setActiveLink={setActiveLink}
isOpen={openKeys.includes(item.label)}
toggleOpen={() => toggleOpen(item.label)}
/>
))}
</ScrollView>
</View>
);
}
// Komponen Item Menu
function MenuItem({
item,
onClose,
activeLink,
setActiveLink,
isOpen,
toggleOpen,
}: {
item: NavbarItem;
onClose?: () => void;
activeLink: string | null;
setActiveLink: (link: string | null) => void;
isOpen: boolean;
toggleOpen: () => void;
}) {
const isActive = activeLink === item.link;
const animatedHeight = useRef(new Animated.Value(0)).current;
// Animasi saat isOpen berubah
React.useEffect(() => {
Animated.timing(animatedHeight, {
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links, animatedHeight]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
<View>
{/* Parent Item */}
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
<Ionicons
name={item.icon}
size={16}
color={MainColor.white}
style={styles.icon}
/>
<Text style={styles.parentText}>{item.label}</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={MainColor.white}
/>
</TouchableOpacity>
{/* Submenu (Animated) */}
<Animated.View
style={[
styles.submenu,
// {
// backgroundColor: "red",
// },
{
height: animatedHeight,
opacity: animatedHeight.interpolate({
inputRange: [0, item.links.length * 40],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
]}
>
{item.links.map((subItem, index) => {
const isSubActive = activeLink === subItem.link;
return (
<TouchableOpacity
key={index}
style={[styles.subItem, isSubActive && styles.subItemActive]}
onPress={() => {
setActiveLink(subItem.link);
onClose?.();
router.push(subItem.link as any);
}}
>
<Ionicons
name="radio-button-on-outline"
size={16}
color={isSubActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.subText,
isSubActive && { color: MainColor.yellow },
]}
>
{subItem.label}
</Text>
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
// Menu tanpa submenu
return (
<TouchableOpacity
style={[styles.singleItem, isActive && styles.singleItemActive]}
onPress={() => {
setActiveLink(item.link || null);
onClose?.();
router.push(item.link as any);
}}
>
<Ionicons
name={item.icon}
size={16}
color={isActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.singleText,
{ color: isActive ? MainColor.yellow : MainColor.white },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
}
// Styles
const styles = StyleSheet.create({
container: {
marginBottom: 5,
},
parentItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
// backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
justifyContent: "space-between",
},
parentText: {
flex: 1,
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
singleItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
// backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
},
singleItemActive: {
backgroundColor: AccentColor.blue,
},
singleText: {
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
icon: {
width: 24,
textAlign: "center",
paddingRight: 10,
},
submenu: {
overflow: "hidden",
marginLeft: 30,
marginTop: 5,
},
subItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 6,
marginBottom: 4,
},
subItemActive: {
backgroundColor: AccentColor.blue,
},
subText: {
color: MainColor.white,
fontSize: 16,
fontWeight: "500",
},
});

View File

@@ -28,7 +28,7 @@ interface NavbarMenuProps {
onClose?: () => void;
}
export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [activeLink, setActiveLink] = useState<string | null>(null);
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
@@ -41,13 +41,41 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
useEffect(() => {
if (normalizedPathname) {
setActiveLink(normalizedPathname);
// Temukan menu induk yang sesuai dengan path saat ini dan buka dropdown-nya
for (const item of items) {
// Cocokkan dengan link langsung
if (item.link && normalizedPathname.startsWith(item.link)) {
setOpenKeys(prev => {
if (!prev.includes(item.label)) {
return [...prev, item.label];
}
return prev;
});
break; // Hentikan loop setelah menemukan kecocokan pertama
}
// Cocokkan dengan submenu
if (item.links && item.links.length > 0) {
const matchingSubItem = item.links.find(link => normalizedPathname.startsWith(link.link));
if (matchingSubItem) {
setOpenKeys(prev => {
if (!prev.includes(item.label)) {
return [...prev, item.label];
}
return prev;
});
break; // Hentikan loop setelah menemukan kecocokan pertama
}
}
}
}
}, [normalizedPathname]);
}, [normalizedPathname, items]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
);
};
@@ -97,35 +125,71 @@ function MenuItem({
isOpen: boolean;
toggleOpen: () => void;
}) {
// Cek apakah menu ini atau submenu-nya yang aktif
const isActive = activeLink === item.link;
// Cek apakah path saat ini cocok dengan salah satu submenu
const isSubmenuActive = item.links && item.links.some(subItem => activeLink === subItem.link);
// Cek apakah path saat ini adalah detail dari submenu ini (misalnya /admin/event/123/detail)
const isDetailPageOfThisMenu = item.links && item.links.length > 0 && activeLink &&
item.links.some(link => {
const linkPath = link.link.replace(/\/+$/, "");
return activeLink.startsWith(linkPath + "/");
});
// Gabungkan status aktif untuk menentukan apakah menu ini harus aktif
const isMenuActive = isActive || isSubmenuActive || isDetailPageOfThisMenu;
const animatedHeight = useRef(new Animated.Value(0)).current;
// Animasi saat isOpen berubah
React.useEffect(() => {
// Jika ini adalah halaman detail dari menu ini, buka dropdown secara otomatis
const shouldAutoOpen = isDetailPageOfThisMenu && !isOpen;
Animated.timing(animatedHeight, {
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 40 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links, animatedHeight]);
// Jika perlu membuka dropdown otomatis, panggil toggleOpen
if (shouldAutoOpen) {
toggleOpen();
}
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu, toggleOpen]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
<View>
{/* Parent Item */}
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
<TouchableOpacity
style={[
styles.parentItem,
isMenuActive && styles.parentItemActive,
]}
onPress={toggleOpen}
>
<Ionicons
name={item.icon}
size={16}
color={MainColor.white}
color={isMenuActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text style={styles.parentText}>{item.label}</Text>
<Text
style={[
styles.parentText,
isMenuActive && { color: MainColor.yellow },
]}
>
{item.label}
</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={MainColor.white}
color={isMenuActive ? MainColor.yellow : MainColor.white}
/>
</TouchableOpacity>
@@ -222,6 +286,9 @@ const styles = StyleSheet.create({
marginBottom: 5,
justifyContent: "space-between",
},
parentItemActive: {
backgroundColor: AccentColor.blue,
},
parentText: {
flex: 1,
fontSize: 16,

View File

@@ -0,0 +1,550 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router, usePathname } from "expo-router";
import React, { useEffect, useRef, useState } from "react";
import {
Animated,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export interface NavbarItem_V2 {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
detailPattern?: string; // NEW: Pattern untuk match detail pages
}[];
initiallyOpened?: boolean;
}
interface NavbarMenuProps {
items: NavbarItem_V2[];
onClose?: () => void;
}
export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [openKeys, setOpenKeys] = useState<string[]>([]);
// Normalisasi path: hapus trailing slash
const normalizePath = (path: string) => path.replace(/\/+$/, "");
const normalizedPathname = pathname ? normalizePath(pathname) : "";
// Auto-open parent menu jika submenu aktif
useEffect(() => {
if (!normalizedPathname || !items || items.length === 0) {
return;
}
try {
const newOpenKeys: string[] = [];
// Helper function yang sama dengan di MenuItem
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
const normalizedLink = linkPath.replace(/\/+$/, "");
// Exact match
if (normalizedPathname === normalizedLink) return true;
// Detail pattern match
if (detailPattern) {
const patternRegex = new RegExp(
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
);
if (patternRegex.test(normalizedPathname)) {
return true;
}
}
// Detail page match (fallback)
if (normalizedPathname.startsWith(normalizedLink + "/")) {
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
const segments = remainder.split("/").filter(s => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Actions
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
// Status types
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
// General pages
'category', 'history', 'dashboard', 'index',
// Event specific
'type-of-event', 'type-create', 'type-update',
// Forum specific
'posting', 'report-posting', 'report-comment',
// Collaboration
'group',
// App Information
'business-field', 'information-bank', 'sticker',
'bidang-update', 'sub-bidang-update',
// Transaction/Finance related
'transaction-detail', 'transaction', 'payment',
'disbursement-of-funds', 'detail-disbursement-of-funds',
'list-disbursement-of-funds',
// List pages (CRITICAL!)
'list-of-investor', 'list-of-donatur', 'list-of-participants',
'list-comment', 'list-report-comment', 'list-report-posting',
// Input/Form pages
'reject-input',
// Category pages
'category-create', 'category-update'
];
const hasIdSegment = segments.some(segment => {
if (commonWords.includes(segment.toLowerCase())) {
return false;
}
const isPureNumber = /^\d+$/.test(segment);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
const hasNumber = /\d/.test(segment);
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
return hasIdSegment;
}
return false;
};
items.forEach((item) => {
if (item.links && item.links.length > 0) {
// Check jika ada submenu yang match dengan current path
const hasActiveSubmenu = item.links.some((subItem) => {
return checkPathMatch(subItem.link, subItem.detailPattern);
});
if (hasActiveSubmenu) {
newOpenKeys.push(item.label);
}
}
});
setOpenKeys(newOpenKeys);
} catch (error) {
console.error("Error in NavbarMenu useEffect:", error);
}
}, [normalizedPathname, items]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
);
};
return (
<View
style={{
marginBottom: 20,
}}
>
<ScrollView
contentContainerStyle={{
paddingVertical: 10,
}}
>
{items && items.length > 0 ? (
items.map((item) => (
<MenuItem
key={item.label}
item={item}
onClose={onClose}
currentPath={normalizedPathname}
isOpen={openKeys.includes(item.label)}
toggleOpen={() => toggleOpen(item.label)}
/>
))
) : null}
</ScrollView>
</View>
);
}
// Komponen Item Menu
function MenuItem({
item,
onClose,
currentPath,
isOpen,
toggleOpen,
}: {
item: NavbarItem_V2;
onClose?: () => void;
currentPath: string;
isOpen: boolean;
toggleOpen: () => void;
}) {
const animatedHeight = useRef(new Animated.Value(0)).current;
// Helper function untuk check apakah path aktif
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
if (!linkPath) return false;
const normalizedLink = linkPath.replace(/\/+$/, "");
// 1. Match exact - prioritas tertinggi
if (currentPath === normalizedLink) return true;
// 2. Jika ada detailPattern, cek pattern dulu
if (detailPattern) {
// detailPattern contoh: "/admin/job/*/review"
// akan match dengan:
// - /admin/job/123/review ✅
// - /admin/job/123/review/transaction-detail ✅
// - /admin/job/123/review/anything/nested ✅
const patternRegex = new RegExp(
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
);
const isMatch = patternRegex.test(currentPath);
// Debug log untuk pattern matching
if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
console.log('🔍 Pattern Match Check:', {
currentPath,
detailPattern,
regex: patternRegex.toString(),
isMatch
});
}
if (isMatch) {
return true;
}
}
// 3. Match untuk detail pages (fallback)
if (currentPath.startsWith(normalizedLink + "/")) {
const remainder = currentPath.substring(normalizedLink.length + 1);
const segments = remainder.split("/").filter(s => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Actions
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
// Status types
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
// General pages
'category', 'history', 'dashboard', 'index',
// Event specific
'type-of-event', 'type-create', 'type-update',
// Forum specific
'posting', 'report-posting', 'report-comment',
// Collaboration
'group',
// App Information
'business-field', 'information-bank', 'sticker',
'bidang-update', 'sub-bidang-update',
// Transaction/Finance related
'transaction-detail', 'transaction', 'payment',
'disbursement-of-funds', 'detail-disbursement-of-funds',
'list-disbursement-of-funds',
// List pages (CRITICAL!)
'list-of-investor', 'list-of-donatur', 'list-of-participants',
'list-comment', 'list-report-comment', 'list-report-posting',
// Input/Form pages
'reject-input',
// Category pages
'category-create', 'category-update'
];
const hasIdSegment = segments.some(segment => {
if (commonWords.includes(segment.toLowerCase())) {
return false;
}
const isPureNumber = /^\d+$/.test(segment);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
const hasNumber = /\d/.test(segment);
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
return hasIdSegment;
}
return false;
};
// Check apakah menu item ini atau submenu-nya yang aktif
const isActive = isPathActive(item.link);
const hasActiveSubmenu =
item.links?.some((subItem) => isPathActive(subItem.link, subItem.detailPattern)) || false;
// Animasi saat isOpen berubah
useEffect(() => {
Animated.timing(animatedHeight, {
toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links, animatedHeight]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
// PRE-CALCULATE semua active states untuk submenu
const submenuActiveStates = item.links.map(subItem => ({
subItem,
isActive: isPathActive(subItem.link, subItem.detailPattern),
pathLength: subItem.link.length
}));
return (
<View>
{/* Parent Item */}
<TouchableOpacity
style={[
styles.parentItem,
hasActiveSubmenu && styles.parentItemActive,
]}
onPress={toggleOpen}
>
<Ionicons
name={item.icon}
size={16}
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.parentText,
hasActiveSubmenu && { color: MainColor.yellow },
]}
>
{item.label}
</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
/>
</TouchableOpacity>
{/* Submenu (Animated) */}
<Animated.View
style={[
styles.submenu,
{
height: animatedHeight,
opacity: animatedHeight.interpolate({
inputRange: [0, item.links.length * 44],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
]}
>
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
if (other.subItem.link === subItem.link) return false; // Skip self
const isOtherLonger = other.pathLength > pathLength;
// Debug log untuk Dashboard
if (subItem.label === "Dashboard" && isSubActive) {
console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
dashboardLink: subItem.link,
dashboardLength: pathLength,
otherLabel: other.subItem.label,
otherLink: other.subItem.link,
otherPattern: other.subItem.detailPattern,
otherLength: other.pathLength,
otherIsActive: other.isActive,
isOtherLonger,
willDisableDashboard: other.isActive && isOtherLonger,
currentURL: currentPath
});
}
// Conflict log
if (isSubActive && other.isActive) {
console.log('🔍 CONFLICT DETECTED:', {
current: subItem.label,
currentPath: subItem.link,
currentLength: pathLength,
other: other.subItem.label,
otherPath: other.subItem.link,
otherLength: other.pathLength,
isOtherLonger,
shouldDisableCurrent: isOtherLonger,
currentURL: currentPath
});
}
return other.isActive && isOtherLonger;
});
// Final decision
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
// Debug final
if (isSubActive) {
console.log('✅ Active check:', {
label: subItem.label,
link: subItem.link,
isSubActive,
hasMoreSpecificMatch,
finalIsActive
});
}
return (
<TouchableOpacity
key={index}
style={[styles.subItem, finalIsActive && styles.subItemActive]}
onPress={() => {
onClose?.();
router.push(subItem.link as any);
}}
>
<Ionicons
name="radio-button-on-outline"
size={16}
color={finalIsActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.subText,
finalIsActive && { color: MainColor.yellow },
]}
>
{subItem.label}
</Text>
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
// Menu tanpa submenu
return (
<TouchableOpacity
style={[styles.singleItem, isActive && styles.singleItemActive]}
onPress={() => {
onClose?.();
router.push(item.link as any);
}}
>
<Ionicons
name={item.icon}
size={16}
color={isActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.singleText,
{ color: isActive ? MainColor.yellow : MainColor.white },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
}
// Styles
const styles = StyleSheet.create({
container: {
marginBottom: 5,
},
parentItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 5,
justifyContent: "space-between",
},
parentItemActive: {
backgroundColor: AccentColor.blue,
},
parentText: {
flex: 1,
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
singleItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 5,
},
singleItemActive: {
backgroundColor: AccentColor.blue,
},
singleText: {
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
icon: {
width: 24,
textAlign: "center",
paddingRight: 10,
},
submenu: {
overflow: "hidden",
marginLeft: 30,
marginTop: 5,
},
subItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 6,
marginBottom: 4,
},
subItemActive: {
backgroundColor: AccentColor.blue,
},
subText: {
color: MainColor.white,
fontSize: 16,
fontWeight: "500",
},
});

View File

@@ -0,0 +1,871 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router, usePathname } from "expo-router";
import React, { useEffect, useRef, useState } from "react";
import {
Animated,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export interface NavbarItem_V3 {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
detailPattern?: string; // NEW: Pattern untuk match detail pages
}[];
initiallyOpened?: boolean;
}
interface NavbarMenuProps {
items: NavbarItem_V3[];
onClose?: () => void;
}
export default function NavbarMenu_V3({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [openKeys, setOpenKeys] = useState<string[]>([]);
// Normalisasi path: hapus trailing slash
const normalizePath = (path: string) => path.replace(/\/+$/, "");
const normalizedPathname = pathname ? normalizePath(pathname) : "";
// Auto-open parent menu jika submenu aktif
useEffect(() => {
if (!normalizedPathname || !items || items.length === 0) {
return;
}
try {
const newOpenKeys: string[] = [];
// Helper function yang sama dengan di MenuItem
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
const normalizedLink = linkPath.replace(/\/+$/, "");
// Exact match
if (normalizedPathname === normalizedLink) return true;
// Detail pattern match
if (detailPattern) {
const patternRegex = new RegExp(
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
);
if (patternRegex.test(normalizedPathname)) {
return true;
}
}
// Detail page match (fallback)
if (normalizedPathname.startsWith(normalizedLink + "/")) {
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
const segments = remainder.split("/").filter(s => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Actions
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
// Status types
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
// General pages
'category', 'history', 'dashboard', 'index',
// Event specific
'type-of-event', 'type-create', 'type-update',
// Forum specific
'posting', 'report-posting', 'report-comment',
// Collaboration
'group',
// App Information
'business-field', 'information-bank', 'sticker',
'bidang-update', 'sub-bidang-update',
// Transaction/Finance related
'transaction-detail', 'transaction', 'payment',
'disbursement-of-funds', 'detail-disbursement-of-funds',
'list-disbursement-of-funds',
// List pages (CRITICAL!)
'list-of-investor', 'list-of-donatur', 'list-of-participants',
'list-comment', 'list-report-comment', 'list-report-posting',
// Input/Form pages
'reject-input',
// Category pages
'category-create', 'category-update'
];
const hasIdSegment = segments.some(segment => {
if (commonWords.includes(segment.toLowerCase())) {
return false;
}
const isPureNumber = /^\d+$/.test(segment);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
const hasNumber = /\d/.test(segment);
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
return hasIdSegment;
}
return false;
};
// Calculate all potential matches for conflict resolution
const allMatches = items.flatMap(item => {
if (!item.links || item.links.length === 0) return [];
return item.links
.filter(subItem => checkPathMatch(subItem.link, subItem.detailPattern))
.map(subItem => ({
parentLabel: item.label,
subItem,
pathLength: subItem.link.length
}));
});
// Find the most specific match for each parent
const uniqueParents = new Map<string, { parentLabel: string, longestPathLength: number }>();
allMatches.forEach(match => {
const existing = uniqueParents.get(match.parentLabel);
if (!existing || match.pathLength > existing.longestPathLength) {
uniqueParents.set(match.parentLabel, {
parentLabel: match.parentLabel,
longestPathLength: match.pathLength
});
}
});
// Add only the parents with the most specific matches
newOpenKeys.push(...Array.from(uniqueParents.values()).map(item => item.parentLabel));
// Additionally, if no specific submenu match was found but the current path
// starts with one of the parent menu links, add that parent
if (newOpenKeys.length === 0) {
// Find the parent whose link is the longest prefix of the current path
let longestMatchParent = null;
let longestMatchLength = 0;
items.forEach(item => {
if (item.links && item.links.length > 0) {
item.links.forEach(link => {
const linkPath = link.link.replace(/\/+$/, "");
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
longestMatchLength = linkPath.length;
longestMatchParent = item.label;
}
});
}
});
if (longestMatchParent) {
newOpenKeys.push(longestMatchParent);
}
}
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
const isOnDetailPage = (() => {
// Check if current path has ID-like segments or detail keywords
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Actions
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
// Status types
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
// General pages
'category', 'history', 'dashboard', 'index',
// Event specific
'type-of-event', 'type-create', 'type-update',
// Forum specific
'posting', 'report-posting', 'report-comment',
// Collaboration
'group',
// App Information
'business-field', 'information-bank', 'sticker',
'bidang-update', 'sub-bidang-update',
// Transaction/Finance related
'transaction-detail', 'transaction', 'payment',
'disbursement-of-funds', 'detail-disbursement-of-funds',
'list-disbursement-of-funds',
// List pages (CRITICAL!)
'list-of-investor', 'list-of-donatur', 'list-of-participants',
'list-comment', 'list-report-comment', 'list-report-posting',
// Input/Form pages
'reject-input',
// Category pages
'category-create', 'category-update'
];
const hasIdSegment = segments.some(segment => {
if (commonWords.includes(segment.toLowerCase())) {
return false;
}
const isPureNumber = /^\d+$/.test(segment);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
const hasNumber = /\d/.test(segment);
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
return hasIdSegment;
})();
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
const isOnDetailPageGlobal = (() => {
// Check if current path has ID-like segments or detail keywords
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Actions
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
// Status types
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
// General pages
'category', 'history', 'dashboard', 'index',
// Event specific
'type-of-event', 'type-create', 'type-update',
// Forum specific
'posting', 'report-posting', 'report-comment',
// Collaboration
'group',
// App Information
'business-field', 'information-bank', 'sticker',
'bidang-update', 'sub-bidang-update',
// Transaction/Finance related
'transaction-detail', 'transaction', 'payment',
'disbursement-of-funds', 'detail-disbursement-of-funds',
'list-disbursement-of-funds',
// List pages (CRITICAL!)
'list-of-investor', 'list-of-donatur', 'list-of-participants',
'list-comment', 'list-report-comment', 'list-report-posting',
// Input/Form pages
'reject-input',
// Category pages
'category-create', 'category-update'
];
// Check if any segment is a common word
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
const hasIdSegment = segments.some(segment => {
const isPureNumber = /^\d+$/.test(segment);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
const hasNumber = /\d/.test(segment);
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
// A detail page is one that has either common words or ID segments
return hasCommonWord || hasIdSegment;
})();
// NEW: Only open parent menu if the current path is a detail page of the most relevant parent
if (isOnDetailPageGlobal && newOpenKeys.length === 0) {
// Find the parent whose link is the longest prefix of the current path
let longestMatchParent = null;
let longestMatchLength = 0;
items.forEach(item => {
if (item.links && item.links.length > 0) {
item.links.forEach(link => {
const linkPath = link.link.replace(/\/+$/, "");
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
longestMatchLength = linkPath.length;
longestMatchParent = item.label;
}
});
}
});
if (longestMatchParent) {
newOpenKeys.push(longestMatchParent);
}
}
setOpenKeys(newOpenKeys);
} catch (error) {
console.error("Error in NavbarMenu useEffect:", error);
}
}, [normalizedPathname, items]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
);
};
return (
<View
style={{
marginBottom: 20,
}}
>
<ScrollView
contentContainerStyle={{
paddingVertical: 10,
}}
>
{items && items.length > 0 ? (
items.map((item) => (
<MenuItem
key={item.label}
item={item}
items={items}
onClose={onClose}
currentPath={normalizedPathname}
isOpen={openKeys.includes(item.label)}
toggleOpen={() => toggleOpen(item.label)}
/>
))
) : null}
</ScrollView>
</View>
);
}
// Komponen Item Menu
function MenuItem({
item,
items,
onClose,
currentPath,
isOpen,
toggleOpen,
}: {
item: NavbarItem_V3;
items: NavbarItem_V3[];
onClose?: () => void;
currentPath: string;
isOpen: boolean;
toggleOpen: () => void;
}) {
const animatedHeight = useRef(new Animated.Value(0)).current;
// Helper function untuk check apakah path aktif
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
if (!linkPath) return false;
const normalizedLink = linkPath.replace(/\/+$/, "");
// 1. Match exact - prioritas tertinggi
if (currentPath === normalizedLink) return true;
// 2. Jika ada detailPattern, cek pattern dulu
if (detailPattern) {
// detailPattern contoh: "/admin/job/*/review"
// akan match dengan:
// - /admin/job/123/review ✅
// - /admin/job/123/review/transaction-detail ✅
// - /admin/job/123/review/anything/nested ✅
const patternRegex = new RegExp(
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
);
const isMatch = patternRegex.test(currentPath);
// Debug log untuk pattern matching
// if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
// console.log('🔍 Pattern Match Check:', {
// currentPath,
// detailPattern,
// regex: patternRegex.toString(),
// isMatch
// });
// }
if (isMatch) {
return true;
}
}
// 3. Match untuk detail pages (fallback)
if (currentPath.startsWith(normalizedLink + "/")) {
const remainder = currentPath.substring(normalizedLink.length + 1);
const segments = remainder.split("/").filter(s => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Actions
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
// Status types
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
// General pages
'category', 'history', 'index',
// Event specific
'type-of-event', 'type-create', 'type-update',
// Forum specific
'posting', 'report-posting', 'report-comment',
// Collaboration
'group',
// App Information
'business-field', 'information-bank', 'sticker',
'bidang-update', 'sub-bidang-update',
// Transaction/Finance related
'transaction-detail', 'transaction', 'payment',
'disbursement-of-funds', 'detail-disbursement-of-funds',
'list-disbursement-of-funds',
// List pages (CRITICAL!)
'list-of-investor', 'list-of-donatur', 'list-of-participants',
'list-comment', 'list-report-comment', 'list-report-posting',
// Input/Form pages
'reject-input',
// Category pages
'category-create', 'category-update'
];
const hasCommonWord = segments.some(segment =>
commonWords.includes(segment.toLowerCase())
);
// Hanya anggap sebagai detail page jika mengandung commonWords
return hasCommonWord;
}
return false;
};
// Check apakah menu item ini atau submenu-nya yang aktif
const isActive = isPathActive(item.link);
// NEW LOGIC: Check if user is on a detail page (contains ID segments or specific keywords)
const isOnDetailPage = (() => {
// Check if current path has ID-like segments or detail keywords
const segments = currentPath.split('/').filter(s => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Actions
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
// Status types
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
// General pages
'category', 'history', 'dashboard', 'index',
// Event specific
'type-of-event', 'type-create', 'type-update',
// Forum specific
'posting', 'report-posting', 'report-comment',
// Collaboration
'group',
// App Information
'business-field', 'information-bank', 'sticker',
'bidang-update', 'sub-bidang-update',
// Transaction/Finance related
'transaction-detail', 'transaction', 'payment',
'disbursement-of-funds', 'detail-disbursement-of-funds',
'list-disbursement-of-funds',
// List pages (CRITICAL!)
'list-of-investor', 'list-of-donatur', 'list-of-participants',
'list-comment', 'list-report-comment', 'list-report-posting',
// Input/Form pages
'reject-input',
// Category pages
'category-create', 'category-update'
];
// Check if any segment is a common word
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
const hasIdSegment = segments.some(segment => {
const isPureNumber = /^\d+$/.test(segment);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
const hasNumber = /\d/.test(segment);
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
// A detail page is one that has either common words or ID segments
return hasCommonWord || hasIdSegment;
})();
// Calculate all submenu active states for conflict resolution
const submenuActiveStates = item.links?.map(subItem => ({
subItem,
isActive: isPathActive(subItem.link, subItem.detailPattern),
pathLength: subItem.link.length
})) || [];
// Determine if any submenu is active considering conflicts
const hasActiveSubmenu = submenuActiveStates.some(({ isActive: isSubActive, pathLength, subItem }) => {
if (!isSubActive) return false;
// Check if there's a more specific match elsewhere
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
if (other.subItem.link === subItem.link) return false; // Skip self
return other.isActive && other.pathLength > pathLength;
});
return isSubActive && !hasMoreSpecificMatch;
}) || false;
// For parent menu detection, if current path contains common words,
// check if this parent menu's link is a prefix of the current path
const isParentOfDetailPage = !isActive && !hasActiveSubmenu && item.links && item.links.length > 0 &&
item.links.some(link => currentPath.startsWith(link.link.replace(/\/+$/, "") + "/"));
// Determine if this is the most relevant parent menu for the current path
const isMostRelevantParent = isParentOfDetailPage && (() => {
let longestMatchLength = 0;
let mostRelevantParent = null;
// Find the parent with the longest matching prefix
items.forEach(parentItem => {
if (parentItem.links && parentItem.links.length > 0) {
parentItem.links.forEach(link => {
const linkPath = link.link.replace(/\/+$/, "");
if (currentPath.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
longestMatchLength = linkPath.length;
mostRelevantParent = parentItem.label;
}
});
}
});
return mostRelevantParent === item.label;
})();
// NEW LOGIC: If we're on a detail page, NO submenu should be active regardless of pattern matching
const hasActiveSubmenuOnDetailPage = isOnDetailPage ? false : hasActiveSubmenu;
// NEW LOGIC: If user is on a detail page that belongs to this parent menu,
// activate only the parent menu (open dropdown) without activating any submenu
const isDetailPageOfThisMenu = !isActive && !hasActiveSubmenuOnDetailPage &&
item.links && item.links.length > 0 &&
item.links.some(link => {
const linkPath = link.link.replace(/\/+$/, "");
return currentPath.startsWith(linkPath + "/");
}) &&
!isMostRelevantParent; // Only apply this logic if this isn't the most relevant parent
// NEW LOGIC: Check if this is a page that doesn't belong to any specific menu in the navbar
const isUnlistedPage = !isActive && !hasActiveSubmenu && !isMostRelevantParent && !isDetailPageOfThisMenu && isOnDetailPage;
// NEW LOGIC: If we're on a detail page and this menu is not the relevant parent or detail page owner,
// then it should not be highlighted even if it would normally be the most relevant
const isOnDetailPageAndNotRelevant = isOnDetailPage && !isMostRelevantParent && !isDetailPageOfThisMenu && !isActive;
// NEW LOGIC: If this is an unlisted page, no menu should be highlighted
const isUnlistedPageAndNotRelevant = isUnlistedPage;
// FINAL LOGIC: Only activate this menu if:
// 1. It's the exact match for current path, OR
// 2. It's the most relevant parent, OR
// 3. It's a detail page of this menu
// But NOT if we're on a detail page and this isn't the relevant parent
// And NOT if this is an unlisted page
const isActuallyRelevant = (isActive || isMostRelevantParent || isDetailPageOfThisMenu) && !isOnDetailPageAndNotRelevant && !isUnlistedPageAndNotRelevant;
// Animasi saat isOpen berubah
useEffect(() => {
Animated.timing(animatedHeight, {
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 44 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
<View>
{/* Parent Item */}
<TouchableOpacity
style={[
styles.parentItem,
isActuallyRelevant && styles.parentItemActive,
]}
onPress={toggleOpen}
>
<Ionicons
name={item.icon}
size={16}
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.parentText,
isActuallyRelevant && { color: MainColor.yellow },
]}
>
{item.label}
</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
/>
</TouchableOpacity>
{/* Submenu (Animated) */}
<Animated.View
style={[
styles.submenu,
{
height: animatedHeight,
opacity: animatedHeight.interpolate({
inputRange: [0, item.links.length * 44],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
]}
>
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
if (other.subItem.link === subItem.link) return false; // Skip self
const isOtherLonger = other.pathLength > pathLength;
// Debug log untuk Dashboard
// if (subItem.label === "Dashboard" && isSubActive) {
// console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
// dashboardLink: subItem.link,
// dashboardLength: pathLength,
// otherLabel: other.subItem.label,
// otherLink: other.subItem.link,
// otherPattern: other.subItem.detailPattern,
// otherLength: other.pathLength,
// otherIsActive: other.isActive,
// isOtherLonger,
// willDisableDashboard: other.isActive && isOtherLonger,
// currentURL: currentPath
// });
// }
// Conflict log
// if (isSubActive && other.isActive) {
// console.log('🔍 CONFLICT DETECTED:', {
// current: subItem.label,
// currentPath: subItem.link,
// currentLength: pathLength,
// other: other.subItem.label,
// otherPath: other.subItem.link,
// otherLength: other.pathLength,
// isOtherLonger,
// shouldDisableCurrent: isOtherLonger,
// currentURL: currentPath
// });
// }
return other.isActive && isOtherLonger;
});
// Final decision
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
// NEW: If this is a detail page (regardless of which menu), don't highlight any submenu items
// Also don't highlight if this is an unlisted page
const shouldHighlight = (isOnDetailPage || isUnlistedPage) ? false : finalIsActive;
// Debug final
// if (isSubActive) {
// console.log('✅ Active check:', {
// label: subItem.label,
// link: subItem.link,
// isSubActive,
// hasMoreSpecificMatch,
// finalIsActive,
// shouldHighlight,
// isOnDetailPage,
// isUnlistedPage
// });
// }
return (
<TouchableOpacity
key={index}
style={[styles.subItem, shouldHighlight && styles.subItemActive]}
onPress={() => {
onClose?.();
router.push(subItem.link as any);
}}
>
<Ionicons
name="radio-button-on-outline"
size={16}
color={shouldHighlight ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.subText,
shouldHighlight && { color: MainColor.yellow },
]}
>
{subItem.label}
</Text>
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
// Menu tanpa submenu
return (
<TouchableOpacity
style={[styles.singleItem, isActive && styles.singleItemActive]}
onPress={() => {
onClose?.();
router.push(item.link as any);
}}
>
<Ionicons
name={item.icon}
size={16}
color={isActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.singleText,
{ color: isActive ? MainColor.yellow : MainColor.white },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
}
// Styles
const styles = StyleSheet.create({
container: {
marginBottom: 5,
},
parentItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 5,
justifyContent: "space-between",
},
parentItemActive: {
backgroundColor: AccentColor.blue,
},
parentText: {
flex: 1,
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
singleItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 5,
},
singleItemActive: {
backgroundColor: AccentColor.blue,
},
singleText: {
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
icon: {
width: 24,
textAlign: "center",
paddingRight: 10,
},
submenu: {
overflow: "hidden",
marginLeft: 30,
marginTop: 5,
},
subItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 6,
marginBottom: 4,
},
subItemActive: {
backgroundColor: AccentColor.blue,
},
subText: {
color: MainColor.white,
fontSize: 16,
fontWeight: "500",
},
});

View File

@@ -30,7 +30,6 @@ export default function AvatarComp({
href = `/(application)/(image)/preview-image/${fileId}`,
}: AvatarCompProps) {
const dimension = sizeMap[size];
const avatarImage = () => {
return (
<Avatar.Image
@@ -52,8 +51,9 @@ export default function AvatarComp({
<TouchableOpacity
activeOpacity={0.9}
onPress={
href && fileId ? () => router.navigate(href as any) : onPress
href || fileId ? () => router.navigate(href as any) : onPress
}
disabled={!fileId}
>
{avatarImage()}
</TouchableOpacity>

View File

@@ -49,9 +49,8 @@ export default function NotificationInitializer() {
const fcmToken = await getToken(messagingInstance);
if (!fcmToken) {
console.warn("Tidak bisa mendapatkan FCM token");
console.log("Tidak bisa mendapatkan FCM token");
// logout();
return;
}
console.log("✅ FCM Token:", fcmToken);

View File

@@ -0,0 +1,20 @@
import BaseBox from "@/components/Box/BaseBox";
import TextCustom from "@/components/Text/TextCustom";
import { AccentColor } from "@/constants/color-palet";
import { StyleProp, ViewStyle } from "react-native";
interface Props {
children: React.ReactNode;
onPress?: () => void;
style?: StyleProp<ViewStyle>;
}
export default function AdminBasicBox({ children, onPress, style }: Props) {
return (
<>
<BaseBox onPress={onPress} style={style}>
{children}
</BaseBox>
</>
);
}

View File

@@ -1,7 +1,9 @@
import BaseBox from "@/components/Box/BaseBox";
import Grid from "@/components/Grid/GridCustom";
import TextCustom from "@/components/Text/TextCustom";
import { AccentColor } from "@/constants/color-palet";
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
import { View } from "react-native";
export default function AdminComp_BoxTitle({
title,
@@ -12,13 +14,33 @@ export default function AdminComp_BoxTitle({
}) {
return (
<>
<BaseBox
{/* <BaseBox
style={{ flexDirection: "row", justifyContent: "space-between" }}
paddingTop={5}
paddingBottom={5}
backgroundColor={AccentColor.blue}
> */}
<View
style={{
backgroundColor: AccentColor.darkblue,
borderColor: AccentColor.blue,
paddingBlock: 5,
paddingInline: 10,
borderWidth: 1,
borderRadius: 10,
}}
>
<Grid>
<Grid.Col span={rightComponent ? 6 : 12} style={{ justifyContent: "center" }}>
<Grid
// containerStyle={{
// bottom: 0,
// left: 0,
// right: 0,
// }}
>
<Grid.Col
span={rightComponent ? 6 : 12}
style={{ justifyContent: "center" }}
>
<TextCustom
// style={{ alignSelf: "center" }}
bold
@@ -39,7 +61,8 @@ export default function AdminComp_BoxTitle({
</Grid.Col>
)}
</Grid>
</BaseBox>
</View>
{/* </BaseBox> */}
</>
);
}

View File

@@ -0,0 +1,16 @@
import { MainColor } from "@/constants/color-palet";
import { View } from "react-native";
export default function BasicWrapper({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
{children}
</View>
</>
);
}

View File

@@ -101,12 +101,14 @@ const NewWrapper = (props: NewWrapperProps) => {
renderItem={listProps.renderItem}
keyExtractor={
listProps.keyExtractor ||
((item) => {
((item, index) => {
if (item.id == null) {
console.warn("Item tanpa 'id':", item);
return `fallback-${JSON.stringify(item)}`;
return `fallback-${index}-${JSON.stringify(item)}`;
}
return String(item.id);
// Gabungkan ID dengan indeks untuk mencegah duplikasi
return `${String(item.id)}-${index}`;
})
}

View File

@@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput";
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView";
import NewWrapper from "./_ShareComponent/NewWrapper";
import BasicWrapper from "./_ShareComponent/BasicWrapper";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
// Loader
@@ -121,6 +122,7 @@ export {
GridComponentView,
Spacing,
NewWrapper,
BasicWrapper,
// Stack
StackCustom,
TabBarBackground,

View File

@@ -19,11 +19,12 @@ export {
PADDING_SMALL,
PADDING_MEDIUM,
PADDING_LARGE,
PAGINATION_DEFAULT_TAKE
};
// OS Height
const OS_ANDROID_HEIGHT = 115
const OS_IOS_HEIGHT = 70
const OS_IOS_HEIGHT = 90
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
// Text Size
@@ -51,3 +52,5 @@ const PADDING_SMALL = 12
const PADDING_MEDIUM = 16
const PADDING_LARGE = 20
// Pagination
const PAGINATION_DEFAULT_TAKE = 10;

View File

@@ -30,7 +30,10 @@ type AuthContextType = {
termsOfServiceAccepted: boolean;
}) => Promise<void>;
userData: (token: string) => Promise<any>;
acceptedTerms: (nomor: string, onSetModalVisible: (visible: boolean) => void) => Promise<any>;
acceptedTerms: (
nomor: string,
onSetModalVisible: (visible: boolean) => void,
) => Promise<any>;
};
// --- Create Context ---
@@ -80,34 +83,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
console.log("[RESPONSE AUTH]", JSON.stringify(response, null, 2));
if (response.success && response.isAcceptTerms) {
await AsyncStorage.setItem("kode_otp", response.kodeId);
router.push(`/verification?nomor=${nomor}`);
return true;
} else {
// router.push(`/eula?nomor=${nomor}`);
return false;
}
// if (response.success) {
// if (response.isAcceptTerms) {
// Toast.show({
// type: "success",
// text1: "Sukses",
// text2: "Kode OTP berhasil dikirim",
// });
// await AsyncStorage.setItem("kode_otp", response.kodeId);
// router.push(`/verification?nomor=${nomor}`);
// return false
// } else {
// // router.push(`/eula?nomor=${nomor}`);
// return true
// }
// } else {
// router.push(`/eula?nomor=${nomor}`);
// return true;
// }
} catch (error: any) {
throw new Error(error.response?.data?.message || "Gagal kirim OTP");
} finally {
@@ -266,29 +247,24 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
};
// --- 6. Accept Terms ---
const acceptedTerms = async (nomor: string, onSetModalVisible: (visible: boolean) => void) => {
const acceptedTerms = async (
nomor: string,
onSetModalVisible: (visible: boolean) => void,
) => {
try {
setIsLoading(true);
const response = await apiUpdatedTermCondition({ nomor: nomor });
if (response.success) {
router.replace(`/verification?nomor=${nomor}`);
return `/verification?nomor=${nomor}`;
} else {
if (response.status === 404) {
router.replace(`/register?nomor=${nomor}`);
} else {
Toast.show({
type: "error",
text1: "Error",
text2: response.message,
});
}
return `/register?nomor=${nomor}`;
}
} catch (error) {
console.log("Error accept terms", error);
} finally {
setIsLoading(false);
// onSetModalVisible(false);
onSetModalVisible(false);
}
};

View File

@@ -0,0 +1,177 @@
# Struktur Folder Admin Aplikasi HIPMI Mobile
Dokumen ini menjelaskan struktur folder dan file untuk bagian admin dari aplikasi HIPMI Mobile yang terletak di `app/(application)/admin`.
## File dan Folder Tingkat Atas
### Folder
- `app-information` - Manajemen informasi aplikasi
- `collaboration` - Manajemen modul kolaborasi
- `donation` - Manajemen modul donasi
- `event` - Manajemen modul acara
- `forum` - Manajemen modul forum
- `investment` - Manajemen modul investasi
- `job` - Manajemen modul lowongan kerja
- `notification` - Manajemen notifikasi
- `super-admin` - Fungsi super admin
- `user-access` - Manajemen akses pengguna
- `voting` - Manajemen modul voting
### File
- `_layout.tsx` - Komponen tata letak untuk bagian admin
- `dashboard.tsx` - Tampilan dasbor admin
- `maps.tsx` - Fungsionalitas peta untuk admin
## Struktur Folder Terperinci
### app-information/
```
app-information/
├── business-field/
│ ├── [id]/
│ │ ├── bidang-update.tsx
│ │ ├── index.tsx
│ │ └── sub-bidang-update.tsx
│ └── create.tsx
├── information-bank/
│ ├── [id]/
│ │ └── index.tsx
│ └── create.tsx
├── sticker/
│ ├── [id]/
│ │ └── index.tsx
│ └── create.tsx
└── index.tsx
```
### collaboration/
```
collaboration/
├── [id]/
│ ├── [status].tsx
│ ├── group.tsx
│ └── reject-input.tsx
├── group.tsx
├── index.tsx
├── publish.tsx
└── reject.tsx
```
### donation/
```
donation/
├── [id]/
│ ├── [status]/
│ │ ├── index.tsx
│ │ └── transaction-detail.tsx
│ ├── detail-disbursement-of-funds.tsx
│ ├── disbursement-of-funds.tsx
│ ├── list-disbursement-of-funds.tsx
│ ├── list-of-donatur.tsx
│ └── reject-input.tsx
├── [status]/
│ └── status.tsx
├── category-create.tsx
├── category-update.tsx
├── category.tsx
└── index.tsx
```
### event/
```
event/
├── [id]/
│ ├── [status]/
│ │ └── index.tsx
│ ├── list-of-participants.tsx
│ └── reject-input.tsx
├── [status]/
│ └── status.tsx
├── index.tsx
├── type-create.tsx
├── type-of-event.tsx
└── type-update.tsx
```
### forum/
```
forum/
├── [id]/
│ ├── index.tsx
│ ├── list-comment.tsx
│ ├── list-report-comment.tsx
│ └── list-report-posting.tsx
├── index.tsx
├── posting.tsx
├── report-comment.tsx
└── report-posting.tsx
```
### investment/
```
investment/
├── [id]/
│ ├── [status]/
│ │ ├── index.tsx
│ │ └── transaction-detail.tsx
│ ├── list-of-investor.tsx
│ └── reject-input.tsx
├── [status]/
│ └── status.tsx
└── index.tsx
```
### job/
```
job/
├── [id]/
│ ├── [status]/
│ │ ├── index.tsx
│ │ └── reject-input.tsx
├── [status]/
│ └── status.tsx
└── index.tsx
```
### notification/
```
notification/
└── index.tsx
```
### super-admin/
```
super-admin/
├── [id]/
│ └── index.tsx
└── index.tsx
```
### user-access/
```
user-access/
├── [id]/
│ └── index.tsx
└── index.tsx
```
### voting/
```
voting/
├── [id]/
│ ├── [status]/
│ │ ├── index.tsx
│ │ └── reject-input.tsx
├── [status]/
│ └── status.tsx
├── history.tsx
└── index.tsx
```
## Rute Dinamis
Bagian admin menggunakan rute dinamis yang ditunjukkan dengan kurung siku `[ ]`:
- `[id]` - Rute dinamis untuk ID item tertentu
- `[status]` - Rute dinamis untuk tampilan berdasarkan status
Ini memungkinkan routing yang fleksibel berdasarkan parameter tertentu seperti ID item atau status.

View File

@@ -0,0 +1,107 @@
<!-- ===================== Start Penerapan Pagination Dari Source ===================== -->
File source: app/(application)/(user)/donation/[id]/fund-disbursement.tsx
Folder tujuan: screens/Donation
Nama file utama: ScreenFundDisbursement.tsx
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Donation_ScreenFundDisbursement" kemudian clean code, import dan panggil function tersebut pada file "File source"
Selanjutnya terapkan pagination pada file "Nama file utama"
Function fecth: apiDonationDisbursementOfFundsListById
File function fetch: service/api-client/api-donation.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Terapkan pagination pada file "Nama file utama"
Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additional Prompt -->
File refrensi: screens/Donation/ScreenListOfNews.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
<!-- ===================== End Penerapan Pagination ` ===================== -->
<!-- ===================== Start Penerapan NewWrapper & Pagination ===================== -->
File utama: screens/Donation/ScreenFundDisbursement.tsx
Function fecth: apiDonationDisbursementOfFundsListById
File function fetch: service/api-client/api-donation.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Terapkan pagination pada file "File utama"
Analisa juga file "File utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additinal prompt -->
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
<!-- Start Penerapan NewWrapper -->
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 -->
<!-- Pindah kode ke Screen Component -->
File source: app/(application)/admin/app-information/business-field/[id]/index.tsx
Folder tujuan: screens/Admin/App-Information
Nama file utama: ScreenBusinessFieldDetail.tsx
Nama function utama: Admin_ScreenBusinessFieldDetail
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
<!-- Penerapan Pagination -->
Function fecth: apiAdminMasterBank
File function fetch: service/api-admin/api-master-admin.ts
Terapkan pagination pada file "Nama file utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- END Prompt Admin Refactoring -->
<!-- Use Prompt Now -->
Terapkan NewWrapper pada file: screens/Admin/App-Information/InformationBankSection.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
Function fecth: apiAdminMasterBank
File function fetch: service/api-admin/api-master-admin.ts
Terapkan pagination pada file "Nama file utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
<!-- Baru -->
File Utama: screens/Admin/App-Information/InformationBankSection.tsx
Terapkan FlatList dan pagination pada file "File Utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Function fecth: apiAdminMasterBank
File function fetch: service/api-admin/api-master-admin.ts
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
<!-- END Use Prompt Now -->

517
helpers/PaginationGuide.md Normal file
View File

@@ -0,0 +1,517 @@
# 📱 Reusable Pagination untuk React Native + Expo
Komponen pagination yang terintegrasi dengan **NewWrapper** untuk infinite scroll, pull-to-refresh, skeleton loading, dan empty state.
---
## 📦 File Structure
```
/hooks/
└── usePagination.tsx # Custom hook untuk logika pagination
/helpers/
└── paginationHelpers.tsx # Helper functions untuk komponen UI
/components/
└── NewWrapper.tsx # Komponen wrapper utama (existing)
```
---
## 🚀 Cara Penggunaan
### **Step 1: Import Hook dan Helpers**
```tsx
import { usePagination } from "@/hooks/usePagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
```
### **Step 2: Setup Pagination Hook**
```tsx
const pagination = usePagination({
// ✅ Fungsi untuk fetch data (harus return { data: T[] })
fetchFunction: async (page, searchQuery) => {
return await apiForumGetAll({
category: "beranda",
search: searchQuery || "",
userLoginId: user.id,
page: String(page),
});
},
// ✅ Page size (harus sama dengan API)
pageSize: 5,
// ✅ Query pencarian
searchQuery: search,
// ✅ Dependencies (reload saat berubah)
dependencies: [user?.id, category],
// ⚙️ Optional callbacks
onDataFetched: (data) => console.log("Loaded:", data.length),
onError: (error) => console.error("Error:", error),
});
```
### **Step 3: Generate Komponen Pagination**
```tsx
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Tidak ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: 5,
skeletonHeight: 200,
});
```
### **Step 4: Gunakan dengan NewWrapper**
```tsx
<NewWrapper
// Props dari pagination hook
listData={pagination.listData}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
// Komponen dari helpers
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
// Render item
renderItem={({ item }) => <YourComponent data={item} />}
// Props lain dari NewWrapper
headerComponent={<SearchInput />}
floatingButton={<FloatingButton />}
/>
```
---
## 📖 Contoh Implementasi Lengkap
### **Contoh 1: Forum Page (Basic)**
```tsx
import { usePagination } from "@/hooks/usePagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
export default function ForumPage() {
const { user } = useAuth();
const [search, setSearch] = useState("");
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
if (!user?.id) return { data: [] };
return await apiForumGetAll({
category: "beranda",
search: searchQuery || "",
userLoginId: user.id,
page: String(page),
});
},
pageSize: 5,
searchQuery: search,
dependencies: [user?.id],
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Tidak ada diskusi",
emptySearchMessage: "Tidak ada hasil pencarian",
});
return (
<NewWrapper
headerComponent={
<SearchInput
placeholder="Cari diskusi..."
onChangeText={_.debounce(setSearch, 500)}
/>
}
listData={pagination.listData}
renderItem={({ item }) => <ForumItem data={item} />}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}
```
### **Contoh 2: Product Page (Dengan Filter)**
```tsx
export default function ProductPage() {
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
return await apiProductGetAll({
page: String(page),
search: searchQuery || "",
category: category !== "all" ? category : undefined,
});
},
pageSize: 10,
searchQuery: search,
dependencies: [category], // Reload saat category berubah
});
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada produk",
skeletonCount: 8,
skeletonHeight: 100,
});
return (
<NewWrapper
headerComponent={
<View>
<SearchInput onChangeText={setSearch} />
<CategoryFilter value={category} onChange={setCategory} />
</View>
}
listData={pagination.listData}
renderItem={({ item }) => <ProductCard product={item} />}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}
```
---
## ⚙️ API Reference
### **usePagination Hook**
#### Props
| Prop | Type | Required | Default | Deskripsi |
|------|------|----------|---------|-----------|
| `fetchFunction` | `(page, search?) => Promise<{data: T[]}>` | ✅ | - | Fungsi fetch data dari API |
| `pageSize` | `number` | ❌ | `5` | Jumlah data per halaman |
| `searchQuery` | `string` | ❌ | `""` | Query pencarian |
| `dependencies` | `any[]` | ❌ | `[]` | Dependencies untuk trigger reload |
| `onDataFetched` | `(data: T[]) => void` | ❌ | - | Callback saat data berhasil di-fetch |
| `onError` | `(error: any) => void` | ❌ | - | Callback saat terjadi error |
#### Return Value
```tsx
{
listData: T[]; // Array data untuk NewWrapper
loading: boolean; // Loading state
refreshing: boolean; // Refreshing state
hasMore: boolean; // Apakah masih ada data
page: number; // Current page
onRefresh: () => void; // Function untuk refresh
loadMore: () => void; // Function untuk load more
reset: () => void; // Function untuk reset state
setListData: (data) => void; // Function untuk set data manual
}
```
---
### **createPaginationComponents Helper**
#### Props
| Prop | Type | Required | Default | Deskripsi |
|------|------|----------|---------|-----------|
| `loading` | `boolean` | ✅ | - | Loading state |
| `refreshing` | `boolean` | ✅ | - | Refreshing state |
| `listData` | `any[]` | ✅ | - | List data |
| `searchQuery` | `string` | ❌ | `""` | Query pencarian |
| `emptyMessage` | `string` | ❌ | `"Tidak ada data"` | Pesan empty state |
| `emptySearchMessage` | `string` | ❌ | `"Tidak ada hasil pencarian"` | Pesan empty saat search |
| `skeletonCount` | `number` | ❌ | `5` | Jumlah skeleton items |
| `skeletonHeight` | `number` | ❌ | `200` | Tinggi skeleton items |
| `loadingFooterText` | `string` | ❌ | - | Text loading footer |
#### Return Value
```tsx
{
ListEmptyComponent: React.ReactElement; // Component untuk empty state
ListFooterComponent: React.ReactElement; // Component untuk loading footer
}
```
---
### **Helper Functions Lain**
#### `createSkeletonList(options)`
Generate skeleton list untuk loading state.
```tsx
const SkeletonComponent = createSkeletonList({
count: 5,
height: 200
});
```
#### `createEmptyState(options)`
Generate empty state component.
```tsx
const EmptyComponent = createEmptyState({
message: "Tidak ada data",
searchMessage: "Tidak ada hasil pencarian",
searchQuery: search
});
```
#### `createLoadingFooter(options)`
Generate loading footer component.
```tsx
const FooterComponent = createLoadingFooter({
show: loading && listData.length > 0,
text: "Memuat data..."
});
```
---
## 🎨 Custom Components
### **Custom Empty State**
```tsx
import { createSkeletonList } from "@/helpers/paginationHelpers";
const CustomEmpty = (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>🔍</Text>
<TextCustom>Data tidak ditemukan</TextCustom>
</View>
);
const ListEmptyComponent =
pagination.loading && pagination.listData.length === 0
? createSkeletonList({ count: 5, height: 200 })
: CustomEmpty;
<NewWrapper
ListEmptyComponent={ListEmptyComponent}
// ...
/>
```
### **Custom Loading Footer**
```tsx
import { createLoadingFooter } from "@/helpers/paginationHelpers";
const CustomFooter = createLoadingFooter({
show: pagination.loading && !pagination.refreshing && pagination.listData.length > 0,
customComponent: (
<View style={{ padding: 20, alignItems: "center" }}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={{ marginTop: 8 }}>Loading more...</Text>
</View>
)
});
<NewWrapper
ListFooterComponent={CustomFooter}
// ...
/>
```
---
## ✨ Fitur-Fitur
**Infinite Scroll** - Auto load saat scroll ke bawah
**Pull to Refresh** - Swipe down untuk refresh
**Skeleton Loading** - Smooth loading animation
**Empty State** - Tampilan saat data kosong
**Search Integration** - Support search dengan debounce
**Multi Dependencies** - Reload berdasarkan filter apapun
**Error Handling** - Built-in error handling
**TypeScript** - Full type safety
**Fully Customizable** - Custom components untuk semua state
---
## 🎯 Best Practices
### 1. **Gunakan Debounce untuk Search**
```tsx
<SearchInput
onChangeText={_.debounce((text) => setSearch(text), 500)}
/>
```
### 2. **Sesuaikan Page Size dengan API**
```tsx
const pagination = usePagination({
pageSize: 5, // Harus sama dengan takeData di API
});
```
### 3. **Tambahkan Dependencies yang Relevan**
```tsx
const pagination = usePagination({
dependencies: [userId, category, sortBy], // Reload saat berubah
});
```
### 4. **Handle Error dengan Baik**
```tsx
const pagination = usePagination({
onError: (error) => {
console.error("Error:", error);
Alert.alert("Error", "Gagal memuat data");
},
});
```
### 5. **Pastikan API Return Format yang Benar**
```tsx
// ❌ SALAH
fetchFunction: async () => [data1, data2];
// ✅ BENAR
fetchFunction: async () => ({ data: [data1, data2] });
```
---
## 🔧 Troubleshooting
### **Data tidak muncul?**
- Pastikan `fetchFunction` return `{ data: T[] }`
- Cek apakah API return format yang benar
- Pastikan `pageSize` sesuai dengan API
### **Infinite scroll tidak jalan?**
- Pastikan API return data sesuai `pageSize`
- Cek `hasMore` state
- Pastikan `onEndReachedThreshold` tidak terlalu kecil (default 0.5)
### **Skeleton terus muncul?**
- Cek `loading` state
- Pastikan `fetchFunction` resolve dengan benar
- Cek error di console
### **Refresh tidak bekerja?**
- Pastikan `RefreshControl` menggunakan `pagination.refreshing` dan `pagination.onRefresh`
- Cek apakah API dipanggil saat pull-to-refresh
---
## 📝 Migration Guide
### **Dari Code Lama ke Code Baru**
#### **BEFORE:**
```tsx
const [listData, setListData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const fetchData = async (pageNumber, clear) => {
// ... 30+ lines of code
};
useEffect(() => {
setPage(1);
setListData([]);
setHasMore(true);
fetchData(1, true);
}, [search, user?.id]);
const onRefresh = useCallback(() => {
fetchData(1, true);
}, [search, user?.id]);
const loadMore = useCallback(() => {
if (hasMore && !loading && !refreshing) {
fetchData(page + 1, false);
}
}, [hasMore, loading, refreshing, page, search, user?.id]);
// ... skeleton, empty, footer components
```
#### **AFTER:**
```tsx
const pagination = usePagination({
fetchFunction: async (page, search) => await apiGetData({ page, search }),
pageSize: 5,
searchQuery: search,
dependencies: [user?.id]
});
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
});
```
**Result:** 50+ lines → 15 lines! 🎉
---
## 👨‍💻 Author
Created by Full-Stack Developer
React Native + Expo Specialist
---
## 📄 License
MIT License - Feel free to use in your projects!

View File

@@ -0,0 +1,280 @@
import { View } from "react-native";
import { LoaderCustom, TextCustom, StackCustom } from "@/components";
import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom";
import _ from "lodash";
/**
* Pagination Helpers
*
* Helper functions untuk membuat komponen-komponen pagination
* yang sering digunakan (Skeleton, Empty State, Loading Footer)
*/
interface SkeletonListOptions {
/**
* Jumlah skeleton items
* @default 5
*/
count?: number;
/**
* Tinggi setiap skeleton item
* @default 200
*/
height?: number;
}
/**
* Generate Skeleton List Component untuk loading state
*
* @example
* ```tsx
* <NewWrapper
* listData={listData}
* ListEmptyComponent={
* loading && _.isEmpty(listData)
* ? createSkeletonList({ count: 5, height: 200 })
* : createEmptyState({ message: "Tidak ada data" })
* }
* />
* ```
*/
export const createSkeletonList = (options: SkeletonListOptions = {}) => {
const { count = 5, height = 200 } = options;
return (
<View style={{ flex: 1 }}>
<StackCustom>
{Array.from({ length: count }).map((_, i) => (
<SkeletonCustom height={height} key={i} />
))}
</StackCustom>
</View>
);
};
interface EmptyStateOptions {
/**
* Pesan untuk empty state
* @default "Tidak ada data"
*/
message?: string;
/**
* Pesan untuk empty state saat search
*/
searchMessage?: string;
/**
* Query pencarian (untuk menentukan pesan mana yang ditampilkan)
*/
searchQuery?: string;
/**
* Custom component untuk empty state
*/
customComponent?: React.ReactElement;
}
/**
* Generate Empty State Component
*
* @example
* ```tsx
* ListEmptyComponent={
* createEmptyState({
* message: "Tidak ada diskusi",
* searchMessage: "Tidak ada hasil pencarian",
* searchQuery: search
* })
* }
* ```
*/
export const createEmptyState = (options: EmptyStateOptions = {}) => {
const {
message = "Tidak ada data",
searchMessage = "Tidak ada hasil pencarian",
searchQuery = "",
customComponent,
} = options;
if (customComponent) return customComponent;
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
}}
>
<TextCustom align="center" color="gray">
{searchQuery ? searchMessage : message}
</TextCustom>
</View>
);
};
interface LoadingFooterOptions {
/**
* Tampilkan loading footer
*/
show: boolean;
/**
* Custom text untuk loading footer
*/
text?: string;
/**
* Custom component untuk loading footer
*/
customComponent?: React.ReactElement;
}
/**
* Generate Loading Footer Component
*
* @example
* ```tsx
* ListFooterComponent={
* createLoadingFooter({
* show: loading && !refreshing && listData.length > 0,
* text: "Memuat data..."
* })
* }
* ```
*/
export const createLoadingFooter = (options: LoadingFooterOptions) => {
const { show, text, customComponent } = options;
if (!show) return null;
if (customComponent) return customComponent;
return (
<View style={{ paddingVertical: 16, alignItems: "center" }}>
{text ? (
<TextCustom color="gray">
{text}
</TextCustom>
) : (
<LoaderCustom />
)}
</View>
);
};
interface PaginationComponentsOptions {
/**
* Loading state
*/
loading: boolean;
/**
* Refreshing state
*/
refreshing: boolean;
/**
* List data
*/
listData: any[];
/**
* Query pencarian
*/
searchQuery?: string;
/**
* Pesan empty state
*/
emptyMessage?: string;
/**
* Pesan empty state saat search
*/
emptySearchMessage?: string;
/**
* Jumlah skeleton items
*/
skeletonCount?: number;
/**
* Tinggi skeleton items
*/
skeletonHeight?: number;
/**
* Text loading footer
*/
loadingFooterText?: string;
/**
* Loading pertama
*/
isInitialLoad?: boolean;
}
/**
* Generate semua komponen pagination sekaligus
*
* @example
* ```tsx
* const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
* loading,
* refreshing,
* listData,
* searchQuery: search,
* emptyMessage: "Tidak ada diskusi",
* emptySearchMessage: "Tidak ada hasil pencarian",
* skeletonCount: 5,
* skeletonHeight: 200
* });
*
* <NewWrapper
* listData={listData}
* ListEmptyComponent={ListEmptyComponent}
* ListFooterComponent={ListFooterComponent}
* />
* ```
*/
export const createPaginationComponents = (
options: PaginationComponentsOptions
) => {
const {
loading,
refreshing,
listData,
searchQuery = "",
emptyMessage = "Tidak ada data",
emptySearchMessage = "Tidak ada hasil pencarian",
skeletonCount = 5,
skeletonHeight = 200,
loadingFooterText,
isInitialLoad,
} = options;
// Empty Compotnent: Skeleton saat loading pertama, Empty State saat data kosong
const ListEmptyComponent =
loading && _.isEmpty(listData)
? createSkeletonList({ count: skeletonCount, height: skeletonHeight })
: createEmptyState({
message: emptyMessage,
searchMessage: emptySearchMessage,
searchQuery,
});
// Footer Component: Loading indicator saat load more
const ListFooterComponent = createLoadingFooter({
show: loading && !refreshing && listData.length > 0,
text: loadingFooterText,
});
return {
ListEmptyComponent,
ListFooterComponent,
};
};

184
hooks/use-pagination.tsx Normal file
View File

@@ -0,0 +1,184 @@
import { useState, useCallback, useEffect } from "react";
interface UsePaginationProps<T> {
/**
* Fungsi API untuk fetch data
* @param page - nomor halaman
* @param search - query pencarian (opsional)
* @returns Promise dengan response API (bukan langsung array)
*/
fetchFunction: (page: number, search?: string) => Promise<{ data: T[] }>;
/**
* Jumlah data per halaman (harus sama dengan API)
* @default 5
*/
pageSize?: number;
/**
* Query pencarian
*/
searchQuery?: string;
/**
* Dependencies tambahan untuk trigger reload
* Contoh: [userId, categoryId]
*/
dependencies?: any[];
/**
* Callback saat data berhasil di-fetch
*/
onDataFetched?: (data: T[]) => void;
/**
* Callback saat terjadi error
*/
onError?: (error: any) => void;
}
interface UsePaginationReturn<T> {
// Data state
listData: T[];
loading: boolean;
refreshing: boolean;
hasMore: boolean;
page: number;
// Actions
onRefresh: () => void;
loadMore: () => void;
reset: () => void;
setListData: React.Dispatch<React.SetStateAction<T[]>>;
isInitialLoad: boolean;
}
/**
* Custom Hook untuk menangani pagination dengan infinite scroll
*
* Hook ini mengembalikan props yang siap digunakan langsung dengan NewWrapper
*
* @example
* ```tsx
* const pagination = usePagination({
* fetchFunction: async (page, search) => {
* return await apiForumGetAll({
* category: "beranda",
* search: search || "",
* userLoginId: user.id,
* page: String(page),
* });
* },
* pageSize: 5,
* searchQuery: search,
* dependencies: [user?.id]
* });
*
* // Lalu gunakan langsung di NewWrapper:
* <NewWrapper
* listData={pagination.listData}
* refreshControl={<RefreshControl refreshing={pagination.refreshing} onRefresh={pagination.onRefresh} />}
* onEndReached={pagination.loadMore}
* // ... props lainnya
* />
* ```
*/
export function usePagination<T = any>({
fetchFunction,
pageSize = 5,
searchQuery = "",
dependencies = [],
onDataFetched,
onError,
}: UsePaginationProps<T>): UsePaginationReturn<T> {
const [listData, setListData] = useState<T[]>([]);
const [loading, setLoading] = useState(true); // Set true untuk initial load
const [isInitialLoad, setIsInitialLoad] = useState(true); // Track initial load
const [refreshing, setRefreshing] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
/**
* Fungsi utama untuk fetch data
*/
const fetchData = async (pageNumber: number, clear: boolean) => {
// Cegah multiple call
if (!clear && (loading || refreshing)) return;
const isRefresh = clear;
if (isRefresh) setRefreshing(true);
if (!isRefresh) setLoading(true);
try {
const response = await fetchFunction(pageNumber, searchQuery);
const newData = response.data || [];
// console.log("newData", newData);
setListData((prev) => {
const current = Array.isArray(prev) ? prev : [];
return clear ? newData : [...current, ...newData];
});
// setTimeout(() => {
// }, 4000);
setHasMore(newData.length === pageSize);
setPage(pageNumber);
// Callback jika ada
onDataFetched?.(newData);
} catch (error) {
console.error("[usePagination] Error fetching data:", error);
setHasMore(false);
onError?.(error);
} finally {
setRefreshing(false);
setLoading(false);
setIsInitialLoad(false); // Set false setelah initial load
}
};
/**
* Reset dan reload saat search atau dependencies berubah
*/
useEffect(() => {
reset();
fetchData(1, true);
}, [searchQuery, ...dependencies]);
/**
* Pull-to-refresh
*/
const onRefresh = useCallback(() => {
fetchData(1, true);
}, [searchQuery, ...dependencies]);
/**
* Load more (infinite scroll)
*/
const loadMore = useCallback(() => {
if (hasMore && !loading && !refreshing) {
fetchData(page + 1, false);
}
}, [hasMore, loading, refreshing, page, searchQuery, ...dependencies]);
/**
* Reset state pagination
*/
const reset = useCallback(() => {
setPage(1);
setListData([]);
setHasMore(true);
}, []);
return {
listData,
loading,
refreshing,
hasMore,
page,
onRefresh,
loadMore,
reset,
setListData,
isInitialLoad
};
}

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,213 @@
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find(
(c) => c.value === value
);
return category?.label;
};
const BoxNotification = ({
data,
activeCategory,
}: {
data: any;
activeCategory: string | null;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
console.log(
"Notification >",
selectedCategory(activeCategory as string)
);
router.push(data.deepLink);
markAsRead(data.id);
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function Admin_ScreenNotification() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
useFocusEffect(
useCallback(() => {
fecthData();
}, [activeCategory])
);
const fecthData = async () => {
try {
setLoading(true);
const response = await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
});
if (response.success) {
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("Error Notification", error);
} finally {
setLoading(false);
}
};
const onRefresh = () => {
setRefreshing(true);
fecthData();
setRefreshing(false);
};
return (
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
path: "",
},
]}
onPressItem={(item: any) => {
console.log("Item", item.value);
if (item.value === "read-all") {
AlertDefaultSystem({
title: "Tandai Semua Dibaca",
message:
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
markAsReadAll(user?.id as any);
const data = _.cloneDeep(listData);
data.forEach((e) => {
e.isRead = true;
});
setListData(data);
onRefresh();
setOpenDrawer(false);
},
});
}
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,256 @@
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { usePagination } from "@/hooks/use-pagination";
import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState } from "react";
import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find(
(c) => c.value === value,
);
return category?.label;
};
const fixPath = ({
deepLink,
categoryApp,
}: {
deepLink: string;
categoryApp: string;
}) => {
// Jika categoryApp adalah "OTHER", kembalikan deepLink tanpa perubahan
if (categoryApp === "OTHER") {
return deepLink;
}
// Jika dalam deepLink terdapat "/admin/", kembalikan path tersebut tanpa modifikasi tambahan
if (deepLink.includes("/admin/")) {
return deepLink;
}
console.log("Category App", categoryApp);
console.log("Deep Link", deepLink);
const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
categoryApp,
)}`;
console.log("Fix Path", fixedPath);
return fixedPath;
};
const BoxNotification = ({
data,
activeCategory,
setListData,
}: {
data: any;
activeCategory: string | null;
setListData: (data: any) => void;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
const newPath = fixPath({
deepLink: data.deepLink,
categoryApp: data.kategoriApp,
});
selectedCategory(activeCategory as string);
router.navigate(newPath as any);
if (!data.isRead) {
markAsRead(data.id);
setListData((prev: any) =>
prev.map((item: any) =>
item.id === data.id ? { ...item, isRead: true } : item,
),
);
}
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function Admin_ScreenNotification2() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!user?.id) return { data: [] };
return await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory],
onError: (error) =>
console.error("[ERROR] Fetch admin notifications:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada notifikasi",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item notification
const renderNotificationItem = ({ item }: { item: any }) => (
<View key={item.id}>
<BoxNotification
data={item}
activeCategory={activeCategory as any}
setListData={pagination.setListData}
/>
</View>
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// Reset pagination saat kategori berubah
pagination.reset();
};
return (
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
listData={pagination.listData}
renderItem={renderNotificationItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
path: "",
},
]}
onPressItem={(item: any) => {
console.log("Item", item.value);
if (item.value === "read-all") {
AlertDefaultSystem({
title: "Tandai Semua Dibaca",
message:
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
markAsReadAll(user?.id as any);
const data = _.cloneDeep(pagination.listData);
data.forEach((e) => {
e.isRead = true;
});
pagination.setListData(data);
pagination.onRefresh();
setOpenDrawer(false);
},
});
}
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,127 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
CenterCustom,
Grid,
SearchInput,
StackCustom,
TextCustom,
} from "@/components";
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
export function Admin_ScreenUserAccess() {
const [search, setSearch] = useState("");
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
return await apiAdminUserAccessGetAll({
search: searchQuery || "",
category: "only-user",
page: String(page),
});
// Pastikan mengembalikan struktur data yang sesuai dengan yang diharapkan oleh usePagination
},
pageSize: PAGINATION_DEFAULT_TAKE,
searchQuery: search,
dependencies: [],
onError: (error) => {
console.log("Error fetching data user access", error);
},
});
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Tidak ada data pengguna",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
isInitialLoad: pagination.isInitialLoad,
});
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
const rightComponent = () => {
return (
<>
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari User"
onChangeText={(text) => setSearch(text)}
/>
</>
);
};
const renderItem = ({ item, index }: { item: any; index: number }) => (
<AdminBasicBox
key={index}
onPress={() => router.push(`/admin/user-access/${item?.id}`)}
style={{ marginHorizontal: 10, marginVertical: 5 }}
>
<Grid>
<Grid.Col span={8}>
<StackCustom gap={"xs"}>
<TextCustom bold truncate>
{item?.username || "-"}
</TextCustom>
<TextCustom size={"small"} bold truncate color="gray">
{item?.nomor || "-"}
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<CenterCustom>
{item?.active ? (
<BadgeCustom color="green">Aktif</BadgeCustom>
) : (
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
)}
</CenterCustom>
</Grid.Col>
</Grid>
</AdminBasicBox>
);
return (
<NewWrapper
headerComponent={
<AdminComp_BoxTitle
title="User Access"
rightComponent={rightComponent()}
/>
}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
renderItem={renderItem}
listData={pagination.listData}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -0,0 +1,461 @@
import { NavbarItem_V2 } from "@/components/Drawer/NavbarMenu_V2";
export { adminListMenu_V2, superAdminListMenu_V2 }
const adminListMenu_V2: NavbarItem_V2[] = [
{
label: "Main Dashboard",
icon: "home",
link: "/admin/dashboard",
},
{
label: "Investasi",
icon: "wallet",
links: [
{
label: "Dashboard",
link: "/admin/investment",
// Dashboard tidak perlu detailPattern, akan auto-match dengan /admin/investment/123/...
},
{
label: "Publish",
link: "/admin/investment/publish/status",
detailPattern: "/admin/investment/*/publish", // Match: /admin/investment/123/publish
},
{
label: "Review",
link: "/admin/investment/review/status",
detailPattern: "/admin/investment/*/review", // Match: /admin/investment/123/review
},
{
label: "Reject",
link: "/admin/investment/reject/status",
detailPattern: "/admin/investment/*/reject", // Match: /admin/investment/123/reject
},
],
},
{
label: "Donasi",
icon: "hand-right",
links: [
{
label: "Dashboard",
link: "/admin/donation",
},
{
label: "Publish",
link: "/admin/donation/publish/status",
detailPattern: "/admin/donation/*/publish",
},
{
label: "Review",
link: "/admin/donation/review/status",
detailPattern: "/admin/donation/*/review",
},
{
label: "Reject",
link: "/admin/donation/reject/status",
detailPattern: "/admin/donation/*/reject",
},
{
label: "Kategori",
link: "/admin/donation/category",
},
],
},
{
label: "Event",
icon: "calendar-clear",
links: [
{
label: "Dashboard",
link: "/admin/event",
},
{
label: "Publish",
link: "/admin/event/publish/status",
detailPattern: "/admin/event/*/publish",
},
{
label: "Review",
link: "/admin/event/review/status",
detailPattern: "/admin/event/*/review",
},
{
label: "Reject",
link: "/admin/event/reject/status",
detailPattern: "/admin/event/*/reject",
},
{
label: "Tipe Acara",
link: "/admin/event/type-of-event",
},
{
label: "Riwayat",
link: "/admin/event/history/status",
detailPattern: "/admin/event/*/history",
},
],
},
{
label: "Voting",
icon: "accessibility-outline",
links: [
{
label: "Dashboard",
link: "/admin/voting",
},
{
label: "Publish",
link: "/admin/voting/publish/status",
detailPattern: "/admin/voting/*/publish",
},
{
label: "Review",
link: "/admin/voting/review/status",
detailPattern: "/admin/voting/*/review",
},
{
label: "Reject",
link: "/admin/voting/reject/status",
detailPattern: "/admin/voting/*/reject",
},
{
label: "Riwayat",
link: "/admin/voting/history",
detailPattern: "/admin/voting/*/history",
},
],
},
{
label: "Job",
icon: "desktop-outline",
links: [
{
label: "Dashboard",
link: "/admin/job",
},
{
label: "Publish",
link: "/admin/job/publish/status",
detailPattern: "/admin/job/*/publish",
},
{
label: "Review",
link: "/admin/job/review/status",
detailPattern: "/admin/job/*/review",
},
{
label: "Reject",
link: "/admin/job/reject/status",
detailPattern: "/admin/job/*/reject",
},
],
},
{
label: "Forum",
icon: "chatbubble-ellipses-outline",
links: [
{
label: "Dashboard",
link: "/admin/forum",
},
{
label: "Posting",
link: "/admin/forum/posting",
},
{
label: "Report Posting",
link: "/admin/forum/report-posting",
},
{
label: "Report Komentar",
link: "/admin/forum/report-comment",
},
],
},
{
label: "Collaboration",
icon: "people",
links: [
{
label: "Dashboard",
link: "/admin/collaboration",
},
{
label: "Publish",
link: "/admin/collaboration/publish",
},
{
label: "Group",
link: "/admin/collaboration/group",
},
{
label: "Reject",
link: "/admin/collaboration/reject",
},
],
},
{
label: "Maps",
icon: "map",
link: "/admin/maps",
},
{
label: "App Information",
icon: "information-circle",
link: "/admin/app-information",
},
{
label: "User Access",
icon: "people",
link: "/admin/user-access",
},
];
const superAdminListMenu_V2: NavbarItem_V2[] = [
{
label: "Main Dashboard",
icon: "home",
link: "/admin/dashboard",
},
{
label: "Investasi",
icon: "wallet",
links: [
{
label: "Dashboard",
link: "/admin/investment",
},
{
label: "Publish",
link: "/admin/investment/publish/status",
detailPattern: "/admin/investment/*/publish",
},
{
label: "Review",
link: "/admin/investment/review/status",
detailPattern: "/admin/investment/*/review",
},
{
label: "Reject",
link: "/admin/investment/reject/status",
detailPattern: "/admin/investment/*/reject",
},
],
},
{
label: "Donasi",
icon: "hand-right",
links: [
{
label: "Dashboard",
link: "/admin/donation",
},
{
label: "Publish",
link: "/admin/donation/publish/status",
detailPattern: "/admin/donation/*/publish",
},
{
label: "Review",
link: "/admin/donation/review/status",
detailPattern: "/admin/donation/*/review",
},
{
label: "Reject",
link: "/admin/donation/reject/status",
detailPattern: "/admin/donation/*/reject",
},
{
label: "Kategori",
link: "/admin/donation/category",
},
],
},
{
label: "Event",
icon: "calendar-clear",
links: [
{
label: "Dashboard",
link: "/admin/event",
},
{
label: "Publish",
link: "/admin/event/publish/status",
detailPattern: "/admin/event/*/publish",
},
{
label: "Review",
link: "/admin/event/review/status",
detailPattern: "/admin/event/*/review",
},
{
label: "Reject",
link: "/admin/event/reject/status",
detailPattern: "/admin/event/*/reject",
},
{
label: "Tipe Acara",
link: "/admin/event/type-of-event",
},
{
label: "Riwayat",
link: "/admin/event/history/status",
detailPattern: "/admin/event/*/history",
},
],
},
{
label: "Voting",
icon: "accessibility-outline",
links: [
{
label: "Dashboard",
link: "/admin/voting",
},
{
label: "Publish",
link: "/admin/voting/publish/status",
detailPattern: "/admin/voting/*/publish",
},
{
label: "Review",
link: "/admin/voting/review/status",
detailPattern: "/admin/voting/*/review",
},
{
label: "Reject",
link: "/admin/voting/reject/status",
detailPattern: "/admin/voting/*/reject",
},
{
label: "Riwayat",
link: "/admin/voting/history",
detailPattern: "/admin/voting/*/history",
},
],
},
{
label: "Job",
icon: "desktop-outline",
links: [
{
label: "Dashboard",
link: "/admin/job",
},
{
label: "Publish",
link: "/admin/job/publish/status",
detailPattern: "/admin/job/*/publish",
},
{
label: "Review",
link: "/admin/job/review/status",
detailPattern: "/admin/job/*/review",
},
{
label: "Reject",
link: "/admin/job/reject/status",
detailPattern: "/admin/job/*/reject",
},
],
},
{
label: "Forum",
icon: "chatbubble-ellipses-outline",
links: [
{
label: "Dashboard",
link: "/admin/forum",
},
{
label: "Posting",
link: "/admin/forum/posting",
},
{
label: "Report Posting",
link: "/admin/forum/report-posting",
},
{
label: "Report Komentar",
link: "/admin/forum/report-comment",
},
],
},
{
label: "Collaboration",
icon: "people",
links: [
{
label: "Dashboard",
link: "/admin/collaboration",
},
{
label: "Publish",
link: "/admin/collaboration/publish",
},
{
label: "Group",
link: "/admin/collaboration/group",
},
{
label: "Reject",
link: "/admin/collaboration/reject",
},
],
},
{
label: "Maps",
icon: "map",
link: "/admin/maps",
},
{
label: "App Information",
icon: "information-circle",
link: "/admin/app-information",
},
{
label: "User Access",
icon: "people",
link: "/admin/user-access",
},
{
label: "Super Admin",
icon: "globe",
link: "/admin/super-admin",
},
];
/*
=================================================================================
PENJELASAN detailPattern:
=================================================================================
detailPattern digunakan untuk match dengan URL detail page yang strukturnya:
/admin/{module}/[id]/[status]
Contoh untuk Job Review:
- Link: /admin/job/review/status (halaman list review)
- detailPattern: /admin/job/* /review (detail dari review)
- Match dengan: /admin/job/123/review, /admin/job/456/review, dll
Wildcard "*" akan match dengan ID apapun (angka, UUID, alphanumeric).
Modul yang PERLU detailPattern:
✅ Investasi - Publish, Review, Reject (ada [id]/[status])
✅ Donasi - Publish, Review, Reject (ada [id]/[status])
✅ Event - Publish, Review, Reject, Riwayat (ada [id]/[status])
✅ Voting - Publish, Review, Reject, Riwayat (ada [id]/[status])
✅ Job - Publish, Review, Reject (ada [id]/[status])
Modul yang TIDAK PERLU detailPattern:
❌ Forum - posting, report-posting, report-comment (struktur berbeda)
❌ Collaboration - struktur berbeda
❌ Maps, App Information, User Access - single page
❌ Dashboard submenu - auto-match dengan parent path
=================================================================================
*/

View File

@@ -7,13 +7,21 @@ import {
StyleSheet,
} from "react-native";
import { useState, useRef } from "react";
import { useLocalSearchParams, useRouter } from "expo-router";
import { router, useLocalSearchParams, useRouter } from "expo-router";
import { SafeAreaView } from "react-native-safe-area-context";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Toast from "react-native-toast-message";
export default function EULASection({ nomor, onSetModalVisible }: { nomor: string, onSetModalVisible: (visible: boolean) => void }) {
export default function EULASection({
nomor,
onSetModalVisible,
setLoadingTerm,
}: {
nomor: string;
onSetModalVisible: (visible: boolean) => void;
setLoadingTerm: (loading: boolean) => void;
}) {
const { acceptedTerms } = useAuth();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isAtBottom, setIsAtBottom] = useState(false);
@@ -35,12 +43,26 @@ export default function EULASection({ nomor, onSetModalVisible }: { nomor: strin
if (!isAtBottom) return;
setIsLoading(true);
await acceptedTerms(nomor as string, onSetModalVisible);
const responseAccept = await acceptedTerms(
nomor as string,
onSetModalVisible,
);
console.log("Accept terms", responseAccept);
setLoadingTerm(true);
setTimeout(() => {
router.replace(responseAccept);
}, 500);
} catch (error) {
console.log("Error accept terms", error);
Toast.show({
type: "error",
text1: "Error",
text2: "Terjadi kesalahan saat menerima syarat dan ketentuan",
});
} finally {
setIsLoading(false);
}
};

View File

@@ -1,18 +1,16 @@
import { NewWrapper, TextCustom } from "@/components";
import { NewWrapper } from "@/components";
import ButtonCustom from "@/components/Button/ButtonCustom";
import ModalReactNative from "@/components/Modal/ModalReactNative";
import Spacing from "@/components/_ShareComponent/Spacing";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiVersion, BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { openBrowser } from "@/utils/openBrower";
import versionBadge from "@/utils/viersionBadge";
import VersionBadge from "@/utils/viersionBadge";
import { Redirect } from "expo-router";
import { useEffect, useState } from "react";
import { Modal, RefreshControl, Text, View } from "react-native";
import { RefreshControl, Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import Toast from "react-native-toast-message";
import EULASection from "./EULASection";
@@ -26,6 +24,7 @@ export default function LoginView() {
const [refreshing, setRefreshing] = useState<boolean>(false);
const [modalVisible, setModalVisible] = useState(false);
const [numberToEULA, setNumberToEULA] = useState<string>("");
const [loadingTerm, setLoadingTerm] = useState<boolean>(false);
const { loginWithNomor, token, isAdmin, isUserActive } = useAuth();
@@ -90,7 +89,6 @@ export default function LoginView() {
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
try {
setLoading(true);
@@ -137,7 +135,6 @@ export default function LoginView() {
}
>
<View style={GStyles.authContainer}>
<View>
<View style={GStyles.authContainerTitle}>
<Text style={GStyles.authSubTitle}>WELCOME TO</Text>
@@ -172,21 +169,15 @@ export default function LoginView() {
<Spacing />
<ButtonCustom onPress={handleLogin} isLoading={loading}>
<ButtonCustom
onPress={handleLogin}
disabled={loadingTerm}
isLoading={loading || loadingTerm}
>
Login
</ButtonCustom>
<Spacing height={50} />
{/* <ButtonCustom
onPress={() => {
setModalVisible(true);
console.log("Show modal", modalVisible);
}}
>
Show Modal
</ButtonCustom> */}
{/* <CheckboxCustom value={term} onChange={() => setTerm(!term)} /> */}
<Text
style={{ ...GStyles.textLabel, textAlign: "center", fontSize: 12 }}
>
@@ -208,7 +199,11 @@ export default function LoginView() {
</View>
<ModalReactNative isVisible={modalVisible}>
<EULASection nomor={numberToEULA || ""} onSetModalVisible={setModalVisible} />
<EULASection
nomor={numberToEULA || ""}
onSetModalVisible={setModalVisible}
setLoadingTerm={setLoadingTerm}
/>
</ModalReactNative>
</NewWrapper>
);

View File

@@ -0,0 +1,21 @@
import { BaseBox, Grid, Spacing, TextCustom } from "@/components";
import { formatChatTime } from "@/utils/formatChatTime";
export default function Donation_BoxNews({item}: {item: any}){
return <>
<BaseBox href={`/donation/[id]/(news)/${item?.id}`}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title || "-"}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
</>
}

View File

@@ -16,6 +16,9 @@ export default function Donation_ButtonStatusSection({
}) {
const [isLoading, setLoading] = useState(false);
const [isLoadingDelete, setLoadingDelete] = useState(false);
const path: any = (status: string) => {
return `/donation/(tabs)/status?status=${status}`;
};
const handleBatalkanReview = async () => {
AlertDefaultSystem({
title: "Batalkan Review",
@@ -43,7 +46,7 @@ export default function Donation_ButtonStatusSection({
text1: response.message,
});
router.back();
router.push(path("draft"));
} catch (error) {
console.log("[ERROR]", error);
} finally {
@@ -80,7 +83,7 @@ export default function Donation_ButtonStatusSection({
text1: response.message,
});
router.back();
router.replace(path("review"));
} catch (error) {
console.log("[ERROR]", error);
} finally {
@@ -117,7 +120,7 @@ export default function Donation_ButtonStatusSection({
text1: response.message,
});
router.back();
router.replace(path("draft"));
} catch (error) {
console.log("[ERROR]", error);
} finally {

View File

@@ -12,11 +12,13 @@ import { View } from "react-native";
export default function Donation_ComponentBoxDetailData({
bottomSection,
data,
showSisaHari = true,
sisaHari,
reminder,
}: {
bottomSection?: React.ReactNode;
data: any;
showSisaHari?: boolean;
sisaHari: number;
reminder: boolean;
}) {
@@ -34,9 +36,9 @@ export default function Donation_ComponentBoxDetailData({
<TextCustom bold color="red">
Waktu berakhir
</TextCustom>
) : (
) : showSisaHari ? (
<TextCustom>Sisa hari: {sisaHari}</TextCustom>
)}
) : null}
</View>
<Grid>

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