Compare commits

...

10 Commits

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

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

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

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

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

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

### No Issue"
2026-02-18 17:19:46 +08:00
76845b71b4 ## Perubahan Tampilan Admin
### File Baru (4)
- `screens/Admin/Voting/ScreenVotingStatus.tsx`
- `screens/Admin/Voting/ScreenVotingHistory.tsx`
- `screens/Admin/Voting/ScreenEventTypeOfEvent.tsx`
- `screens/Admin/Voting/BoxVotingStatus.tsx`

### File Diubah (3)
- `app/(application)/admin/voting/[status]/status.tsx` → 5 baris
- `app/(application)/admin/voting/history.tsx` → 5 baris
- `app/(application)/admin/event/type-of-event.tsx` → 5 baris

### API Updates (2)
- `service/api-admin/api-admin-voting.ts` → tambah param `page`
- `service/api-admin/api-master-admin.ts` → tambah param `page`

## Fitur Baru
- Pagination (infinite scroll)
- Pull-to-Refresh
- Skeleton Loading
- Empty State
- Search Functionality

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

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

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

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

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

    Tujuan dari Perubahan Ini

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

    File yang Terpengaruh

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

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

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

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

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

    File-file yang Diubah

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

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

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

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

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

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

Styles
- styles/global-styles.ts

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

### No Issue
2026-02-13 17:38:48 +08:00
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
76 changed files with 5576 additions and 3048 deletions

169
QWEN.md
View File

@@ -35,6 +35,7 @@ hipmi-mobile/
│ └── ...
├── assets/ # Images, icons, and static assets
├── constants/ # Constants and configuration values
├── helpers/ # Helper functions (pagination, etc.)
├── hooks/ # Custom React hooks
├── lib/ # Utility libraries
├── navigation/ # Navigation configuration
@@ -49,6 +50,8 @@ hipmi-mobile/
- Node.js (with bun as the package manager)
- Expo CLI
- iOS Simulator or Android Emulator (for native builds)
- Android Studio (for Android builds)
- Xcode (for iOS builds, macOS only)
### Setup and Development
@@ -76,28 +79,79 @@ hipmi-mobile/
bun run lint
```
### Environment Variables
The application uses environment variables defined in the app.config.js file:
### Build Commands
#### EAS Build (Production)
```bash
# Production build
eas build --profile production
# Preview build
eas build --profile preview
# Development build
eas build --profile development
```
#### Local Native Builds
```bash
# Generate native folders (iOS & Android)
npx expo prebuild
# iOS specific
bunx expo prebuild --platform ios
open ios/HIPMIBADUNG.xcworkspace
# Android specific
bunx expo prebuild --platform android
```
#### Version Management
```bash
# Patch version update
npm version patch
```
### Android Debugging
```bash
# List connected devices
adb devices
# Install APK to device/emulator
adb install android/app/build/outputs/apk/debug/app-debug.apk
# Install to specific device
adb -s <device_id> install android/app/build/outputs/apk/debug/app-debug.apk
```
## Environment Variables
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
Create a `.env` file in the project root with these variables.
## 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
- **Development**: Development builds with development client
- **Preview**: Internal distribution builds (APK for Android)
- **Production**: App store builds (App Bundle for Android, IPA for iOS)
Configuration is in `eas.json`.
## Features and Functionality
The application appears to include several key modules:
- **Authentication**: Login, registration, and verification flows
- **Admin Panel**: Administrative functions
The application includes several key modules:
- **Authentication**: Login with phone number, OTP verification, registration, terms acceptance
- **Admin Panel**: Administrative functions for managing content and users
- **Collaboration**: Tools for member collaboration
- **Events**: Event management and calendar
- **Forum**: Discussion forums
- **Maps**: Location-based services with Mapbox integration
- **Donations**: Donation functionality
- **Donations**: Donation functionality with fund disbursement tracking
- **Job Board**: Employment opportunities
- **Investment**: Investment-related features
- **Voting**: Voting systems
@@ -109,18 +163,50 @@ The application appears to include several key modules:
### Coding Standards
- TypeScript is used throughout the project for type safety
- Component-based architecture with reusable components
- Context API for state management
- Context API for state management (AuthContext)
- File-based routing with Expo Router
- Consistent naming conventions using camelCase for variables and PascalCase for components
- Path aliases: `@/*` maps to project root
### Architecture Patterns
#### Screen Components
- Screen components are stored in `/screens` directory organized by feature
- Route files in `/app` import and use screen components
- Example pattern:
```tsx
// app/some-route.tsx
import SomeScreen from "@/screens/Feature/ScreenSome";
export default function SomeRoute() {
return <SomeScreen />;
}
```
#### Wrapper Components
- `NewWrapper` component is used for consistent screen layouts
- Located at `components/_ShareComponent/NewWrapper.tsx`
#### Pagination Pattern
- Use `hooks/use-pagination.tsx` and `helpers/paginationHelpers.tsx`
- Helper functions: `createSkeletonList`, `createEmptyState`, `createLoadingFooter`, `createPaginationComponents`
- API functions should accept `page` parameter (default: "1")
### API Service Structure
- Base API configuration: `service/api-config.ts`
- Client APIs: `service/api-client/`
- Admin APIs: `service/api-admin/`
- All API calls use axios with interceptors for auth token injection
### Testing
- Linting is configured with ESLint
- Standard Expo linting configuration is used
- Standard Expo linting configuration
### Security
- Firebase is integrated for authentication and messaging
- Camera and location permissions are properly configured
- Deep linking is secured with app domain associations
- Auth tokens stored in AsyncStorage
## Key Dependencies
@@ -133,6 +219,9 @@ The application appears to include several key modules:
- `react-native-toast-message`: Toast notifications
- `react-native-otp-entry`: OTP input components
- `react-native-qrcode-svg`: QR code generation
- `axios`: HTTP client for API calls
- `lodash`: Utility library
- `moti`: Animation library
### Development Dependencies
- `@types/*`: TypeScript type definitions
@@ -142,28 +231,52 @@ The application appears to include several key modules:
## 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
- **iOS**:
- Bundle identifier: `com.anonymous.hipmi-mobile`
- Supports tablets
- Build number: 21
- Google Services integration
- Associated domains for deep linking
- **Android**:
- Package name: `com.bip.hipmimobileapp`
- Version code: 4
- Adaptive icons
- Edge-to-edge display enabled
- Intent filters for HTTPS 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
### Deep Linking
- Scheme: `hipmimobile://`
- Associated domains: `applinks:cld-dkr-staging-hipmi.wibudev.com`
- Configured for both iOS and Android
### 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.
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
### Camera
Camera permissions configured for both iOS and Android with microphone access for recording.
## Common Development Tasks
### Adding a New Screen
1. Create screen component in appropriate `/screens` subdirectory
2. Add route in `/app` directory if needed
3. Configure navigation in `AppRoot.tsx` if custom header is needed
### Adding API Endpoint
1. Add function in appropriate service file (`service/api-client/` or `service/api-admin/`)
2. Use `apiConfig` axios instance for requests
3. Include proper error handling
### Refactoring Pattern (from docs/prompt-for-qwen-code.md)
When moving code from route files to screen components:
1. Create new file in `screens/<Feature>/` directory
2. Rename function with prefix (e.g., `Admin_`, `Donation_`)
3. Use `NewWrapper` component for consistent layout
4. Apply pagination helpers if displaying lists
5. Import and call from original route file

View File

@@ -21,7 +21,7 @@ export default {
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "20",
buildNumber: "21",
},
android: {

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";
@@ -61,14 +61,31 @@ export default function Application() {
if (data && data?.active === false) {
console.log("User is not active");
return <Redirect href={`/waiting-room`} />;
return (
<BasicWrapper>
<Redirect href={`/waiting-room`} />
</BasicWrapper>
);
}
if (data && data?.Profile === null) {
console.log("Profile is null");
return <Redirect href={`/profile/create`} />;
return (
<BasicWrapper>
<Redirect href={`/profile/create`} />
</BasicWrapper>
);
}
// if (data && data?.masterUserRoleId !== "1") {
// console.log("User is not admin");
// return (
// <BasicWrapper>
// <Redirect href={`/admin/dashboard`} />
// </BasicWrapper>
// );
// }
return (
<>
<Stack.Screen

View File

@@ -1,12 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
@@ -49,7 +49,7 @@ export default function Portofolio() {
useCallback(() => {
onLoadData(id as string);
onLoadUserByToken();
}, [id])
}, [id]),
);
async function onLoadData(id: string) {
@@ -144,27 +144,27 @@ export default function Portofolio() {
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
rightItem={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={
rightItem={
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
}
/>
@@ -172,26 +172,26 @@ export default function Portofolio() {
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.tlpn}</TextCustom>}
rightItem={<TextCustom>{data?.tlpn}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.alamatKantor}</TextCustom>}
rightItem={<TextCustom>{data?.alamatKantor}</TextCustom>}
/>
<Spacing />

View File

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

View File

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

View File

@@ -1,126 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
CenterCustom,
Divider,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconView } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import dayjs from "dayjs";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import React, { useCallback } from "react";
import { View } from "react-native";
import { Admin_ScreenDonationListDisbursementOfFunds } from "@/screens/Admin/Donation/ScreenDonationListDisbursementOfFunds";
export default function AdminDonasiListOfDisbursementOfFunds() {
const { id } = useLocalSearchParams();
const [listData, setListData] = React.useState<any[] | null>(null);
const [loadData, setLoadData] = React.useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminDonationDisbursementOfFundsListById({
id: id as string,
category: "get-all",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return (
<>
<ViewWrapper
headerComponent={
<AdminBackButtonAntTitle title="Daftar Pencairan Dana" />
}
>
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
component2={
<TextCustom bold align="center">
Tanggal
</TextCustom>
}
component3={
<TextCustom bold align="center">
Nominal
</TextCustom>
}
/>
<Divider />
<StackCustom>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<View key={index}>
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<CenterCustom>
<ActionIcon
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
router.push(
`/admin/donation/${item?.id}/detail-disbursement-of-funds`
);
}}
/>
</CenterCustom>
}
component2={
<TextCustom align="center" truncate>
{dayjs(item?.createdAt).format("DD-MM-YYYY")}
</TextCustom>
}
component3={
<TextCustom align="center" truncate>
Rp. {formatCurrencyDisplay(item?.nominalCair)}
</TextCustom>
}
/>
</View>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenDonationListDisbursementOfFunds />;
}

View File

@@ -1,186 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
BadgeCustom,
CenterCustom,
LoaderCustom,
SelectCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconView } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminDonationListOfDonatur } from "@/service/api-admin/api-admin-donation";
import { apiMasterTransaction } from "@/service/api-client/api-master";
import { colorBadgeTransaction } from "@/utils/colorBadge";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import React, { useEffect } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import { Admin_ScreenDonationListOfDonatur } from "@/screens/Admin/Donation/ScreenDonationListOfDonatur";
export default function AdminDonasiListOfDonatur() {
const { id } = useLocalSearchParams();
const [listData, setListData] = React.useState<any[] | null>(null);
const [loadData, setLoadData] = React.useState(false);
const [master, setMaster] = React.useState<any[]>([]);
const [selectValue, setSelectValue] = React.useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = React.useState<string | null>(
null
);
useFocusEffect(
React.useCallback(() => {
onLoadData();
}, [id, selectValue])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminDonationListOfDonatur({
id: id as string,
status: selectedStatus as any,
});
// console.log("[LIST OF DONATUR]", JSON.stringify(response, null, 2));
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
setListData([]);
} finally {
setLoadData(false);
}
};
useEffect(() => {
onLoadMaster();
}, []);
const onLoadMaster = async () => {
try {
const response = await apiMasterTransaction();
if (response.success) {
setMaster(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
setMaster([]);
}
};
const searchComponent = (
<View style={{ flexDirection: "row", gap: 5 }}>
<SelectCustom
placeholder="Pilih status transaksi"
data={
_.isEmpty(master)
? []
: master?.map((item: any) => ({
label: item.name,
value: item.id,
}))
}
value={selectValue}
onChange={(value: any) => {
setSelectValue(value);
const nameSelected = master.find((item: any) => item.id === value);
const statusChooses = _.lowerCase(nameSelected?.name);
setSelectedStatus(statusChooses);
}}
styleContainer={{ width: "100%", marginBottom: 0 }}
allowClear
/>
</View>
);
return (
<>
<ViewWrapper
headerComponent={
<AdminBackButtonAntTitle newComponent={searchComponent} />
}
>
<StackCustom>
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
component2={
<TextCustom bold align="center">
Donatur
</TextCustom>
}
component3={
<TextCustom bold align="center">
Status
</TextCustom>
}
/>
<Divider />
<StackCustom>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<View key={index}>
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<CenterCustom>
<ActionIcon
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
router.push(
`/admin/donation/${item?.id}/${_.lowerCase(
item?.DonasiMaster_StatusInvoice?.name
)}/transaction-detail`
);
}}
/>
</CenterCustom>
}
component2={
<TextCustom bold align="center" truncate>
{item?.Author?.username || "-"}
</TextCustom>
}
component3={
<BadgeCustom
style={{ alignSelf: "center" }}
color={colorBadgeTransaction({
status: item?.DonasiMaster_StatusInvoice?.name,
})}
>
{item?.DonasiMaster_StatusInvoice?.name}
</BadgeCustom>
}
/>
</View>
))
)}
</StackCustom>
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenDonationListOfDonatur />;
}

View File

@@ -1,117 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminDonation } from "@/service/api-admin/api-admin-donation";
import { Octicons } from "@expo/vector-icons";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Divider } from "react-native-paper";
import { Admin_ScreenDonationStatus } from "@/screens/Admin/Donation/ScreenDonationStatus";
export default function AdminDonationStatus() {
const { status } = useLocalSearchParams();
console.log("[STATUS]", status);
const [data, setData] = useState<any | null>(null);
const [search, setSearch] = useState<string>("");
const [loadData, setLoadData] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [status, search])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminDonation({
category: status as "publish" | "review" | "reject",
search,
});
console.log("[RES]", JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
setData([]);
} finally {
setLoadData(false);
}
};
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={(value) => setSearch(value)}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Donasi" />}>
<StackCustom gap={"sm"}>
<AdminComp_BoxTitle
title={`${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
<AdminTitleTable
title1="Aksi"
title2="Username"
title3="Judul Donasi"
/>
<Divider />
{loadData ? (
<LoaderCustom />
) : _.isEmpty(data) ? (
<TextCustom align="center" size="small" color="gray">
Belum ada data
</TextCustom>
) : (
data?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/donation/${item.id}/${status}`);
}}
/>
}
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
value3={
<TextCustom truncate={2}>
{item?.title || "-"}
</TextCustom>
}
/>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenDonationStatus />;
}

View File

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

View File

@@ -1,135 +1,5 @@
import {
BadgeCustom,
CenterCustom,
ClickableCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { apiAdminMasterDonationCategory } from "@/service/api-admin/api-master-admin";
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
import { Divider } from "react-native-paper";
import { Admin_ScreenDonationCategory } from "@/screens/Admin/Donation/ScreenDonationCategory";
export default function AdminDonationCategory() {
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
useFocusEffect(
useCallback(() => {
fetchMaster();
}, [])
);
const fetchMaster = async () => {
try {
setLoading(true);
const response = await apiAdminMasterDonationCategory();
if (response.success) {
console.log(JSON.stringify(response.data, null, 2));
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("[Error]", error);
} finally {
setLoading(false);
}
};
const onRefresh = async () => {
setRefreshing(true);
await fetchMaster();
setRefreshing(false);
};
return (
<>
<ViewWrapper
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
headerComponent={<AdminTitlePage title="Donasi" />}
>
<AdminComp_BoxTitle
title="Kategori"
rightComponent={
<AdminActionIconPlus
onPress={() => {
router.push(`/admin/donation/category-create`);
}}
/>
}
/>
<View>
<GridSpan_4_8
label={<TextCustom bold>Status</TextCustom>}
value={<TextCustom bold>Kategori</TextCustom>}
/>
{/* <Grid>
<Grid.Col style={{ paddingLeft: 10 }} span={4}>
<TextCustom bold>Status</TextCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom bold>Kategori</TextCustom>
</Grid.Col>
</Grid> */}
<Divider />
<Spacing />
<StackCustom>
{listData.map((item, index) => (
<ClickableCustom
onPress={() => {
router.push(`/admin/donation/category-update?id=${item.id}`);
}}
key={index}
>
<GridSpan_4_8
label={
<CenterCustom>
<BadgeCustom
color={colorActivationForBadge({
status: item.active,
})}
>
{item.active ? "Aktif" : "Tidak Aktif"}
</BadgeCustom>
</CenterCustom>
}
value={<TextCustom>{item.name}</TextCustom>}
/>
{/* <Grid containerStyle={{ paddingBottom: 10 }}>
<Grid.Col span={4} style={{ paddingLeft: 10 }}>
<CenterCustom>
<BadgeCustom
color={item.active ? MainColor.green : MainColor.red}
>
{item.active ? "Aktif" : "Tidak Aktif"}
</BadgeCustom>
</CenterCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom bold>{item.name}</TextCustom>
</Grid.Col>
</Grid> */}
<Divider />
</ClickableCustom>
))}
</StackCustom>
</View>
</ViewWrapper>
</>
);
return <Admin_ScreenDonationCategory />;
}

View File

@@ -197,7 +197,7 @@ export default function AdminEventDetail() {
/>
)}
<TextCustom align="center">{isDevLink}</TextCustom>
{/* <TextCustom align="center">{isDevLink}</TextCustom> */}
</StackCustom>
</BaseBox>
)}

View File

@@ -1,105 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
import dayjs, { Dayjs } from "dayjs";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { View } from "moti";
import { useCallback, useState } from "react";
import { Admin_ScreenEventListOfParticipants } from "@/screens/Admin/Event/ScreenEventListOfParticipants";
export default function AdminEventListOfParticipants() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[] | null>(null);
const [loadData, setLoadData] = useState(false);
const [startDate, setStartDate] = useState<Dayjs | undefined>();
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminEventListOfParticipants({
id: id as string,
});
console.log("[DATA]", JSON.stringify(response, null, 2));
if (response.success) {
setListData(response.data);
setStartDate(dayjs(response.data.Event.tanggal));
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return (
<>
<ViewWrapper
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada peserta
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<BaseBox key={index}>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"sm"}>
<TextCustom bold truncate>
{item?.User?.username}
</TextCustom>
<TextCustom>+{item?.User?.nomor}</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
{startDate &&
startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
<BadgeCustom
style={{ alignSelf: "flex-end" }}
color={item?.isPresent ? "green" : "red"}
>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom
style={{ alignSelf: "flex-end" }}
color="gray"
>
-
</BadgeCustom>
</View>
)}
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
</>
);
return <Admin_ScreenEventListOfParticipants />;
}

View File

@@ -1,135 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
ClickableCustom,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminEvent } from "@/service/api-admin/api-admin-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { Octicons } from "@expo/vector-icons";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Divider } from "react-native-paper";
import { Admin_ScreenEventStatus } from "@/screens/Admin/Event/ScreenEventStatus";
export default function AdminEventStatus() {
const { status } = useLocalSearchParams();
console.log("[STATUS EVENT]", status);
const [listData, setListData] = useState<any[] | null>(null);
const [loadData, setLoadData] = useState(false);
const [search, setSearch] = useState<string>("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [status, search])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminEvent({
category: status as "publish" | "review" | "reject" | "history" as any,
search,
});
console.log(
`[RES LIST BY STATUS: ${status}]`,
JSON.stringify(response, null, 2)
);
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={(value) => setSearch(value)}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
<AdminComp_BoxTitle
title={`${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
<StackCustom gap={"sm"}>
<AdminTitleTable
title1="Username"
title2="Tanggal"
title3="Judul Event"
/>
<Divider />
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" size="small" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<ClickableCustom
key={index}
onPress={() => {
router.push(`/admin/event/${item.id}/${status}`);
}}
>
<AdminTableValue
key={index}
value1={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
// <ActionIcon
// icon={
// <Octicons
// name="eye"
// size={ICON_SIZE_BUTTON}
// color="black"
// />
// }
// onPress={() => {
// router.push(`/admin/event/${item.id}/${status}`);
// }}
// />
}
value2={
<TextCustom truncate={1}>
{dateTimeView({ date: item?.tanggal })}
</TextCustom>
}
value3={
<TextCustom truncate={2}>{item?.title || "-"}</TextCustom>
}
/>
<Divider/>
</ClickableCustom>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenEventStatus />;
}

View File

@@ -1,129 +1,5 @@
import {
ActionIcon,
BadgeCustom,
CenterCustom,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminMasterTypeOfEvent } from "@/service/api-admin/api-master-admin";
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import { Admin_ScreenEventTypeOfEvent } from "@/screens/Admin/Voting/ScreenEventTypeOfEvent";
export default function AdminEventTypeOfEvent() {
const [listData, setListData] = useState<any[] | null>(null);
const [loadData, setLoadData] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminMasterTypeOfEvent();
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]",error);
setListData([]);
} finally {
setLoadData(false);
}
};
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
<AdminComp_BoxTitle
title="Tipe Acara"
rightComponent={
<AdminActionIconPlus
onPress={() => {
router.push(`/admin/event/type-create`);
}}
/>
}
/>
<>
<GridViewCustomSpan
span1={2}
span2={5}
span3={5}
component1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
component2={<TextCustom bold align="center">Status</TextCustom>}
component3={<TextCustom bold>Tipe Acara</TextCustom>}
/>
<Divider />
<Spacing />
<StackCustom>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<View key={index}>
<GridViewCustomSpan
span1={2}
span2={5}
span3={5}
component1={
<CenterCustom>
<ActionIcon
icon={
<IconEdit size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
router.push(`/admin/event/type-update?id=${item.id}`);
}}
/>
</CenterCustom>
}
style2={{ alignItems: "center" }}
component2={
<CenterCustom>
<BadgeCustom
color={colorActivationForBadge({
status: item?.active,
})}
>
{item?.active ? "Aktif" : "Tidak Aktif"}
</BadgeCustom>
</CenterCustom>
}
component3={<TextCustom >{item.name}</TextCustom>}
/>
</View>
))
)}
</StackCustom>
</>
</ViewWrapper>
</>
);
return <Admin_ScreenEventTypeOfEvent />;
}

View File

@@ -1,13 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BadgeCustom,
BaseBox,
ButtonCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
AlertDefaultSystem,
BadgeCustom,
BaseBox,
ButtonCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
@@ -15,8 +15,8 @@ import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminInvestmentGetOneInvoiceById,
apiAdminInvestmentUpdateInvoice,
apiAdminInvestmentGetOneInvoiceById,
apiAdminInvestmentUpdateInvoice,
} from "@/service/api-admin/api-admin-investment";
import { colorBadgeTransaction } from "@/utils/colorBadge";
import { dateTimeView } from "@/utils/dateTimeView";
@@ -158,7 +158,7 @@ export default function AdminInvestmentTransactionDetail() {
spanRight={6}
styleLeft={{ paddingRight: 10 }}
styleRight={{ paddingLeft: 10 }}
leftIcon={
leftItem={
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
@@ -181,7 +181,7 @@ export default function AdminInvestmentTransactionDetail() {
Tolak
</ButtonCustom>
}
rightIcon={
rightItem={
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}

View File

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

View File

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

View File

@@ -1,117 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminJob } from "@/service/api-admin/api-admin-job";
import { Octicons } from "@expo/vector-icons";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Divider } from "react-native-paper";
import { Admin_ScreenJobStatus } from "@/screens/Admin/Job/ScreenJobStatus";
export default function AdminJobStatus() {
const { status } = useLocalSearchParams();
const [list, setList] = useState<any | null>(null);
const [loadList, setLoadList] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, [status, search])
);
const handlerLoadList = async () => {
try {
setLoadList(true);
const response = await apiAdminJob({
category: status as "publish" | "review" | "reject",
search,
});
if (response.success) {
setList(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const rightComponent = (
<SearchInput
placeholder="Cari"
onChangeText={setSearch}
value={search}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Job Vacancy" />}>
<AdminComp_BoxTitle
title={`${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
<StackCustom>
<AdminTitleTable
title1="Aksi"
title2="Username"
title3="Judul Pekerjaan"
/>
{/* <Spacing /> */}
<Divider />
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/job/${item.id}/${status}`);
}}
/>
}
value2={
<TextCustom align="center" truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
}
value3={
<TextCustom truncate={2} align="center">
{item?.title || "-"}
</TextCustom>
}
/>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenJobStatus />;
}

View File

@@ -1,4 +1,13 @@
import { ButtonCustom, DrawerCustom, DummyLandscapeImage, Grid, Spacing, StackCustom, TextCustom, ViewWrapper } from "@/components";
import {
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import API_IMAGE from "@/constants/api-storage";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
@@ -45,7 +54,7 @@ export default function AdminMaps() {
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, [])
}, []),
);
const handlerLoadList = async () => {
@@ -144,52 +153,52 @@ export default function AdminMaps() {
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
rightItem={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
/>
<Grid>

View File

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

View File

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

View File

@@ -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

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

View File

@@ -1,117 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
import { Octicons } from "@expo/vector-icons";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Divider } from "react-native-paper";
import { Admin_ScreenVotingStatus } from "@/screens/Admin/Voting/ScreenVotingStatus";
export default function AdminVotingStatus() {
const { status } = useLocalSearchParams();
const [list, setList] = useState<any | null>(null);
const [loadList, setLoadList] = useState(false);
const [search, setSearch] = useState<string>("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [status, search])
);
const onLoadData = async () => {
try {
setLoadList(true);
const response = await apiAdminVoting({
category: status as "publish" | "review" | "reject" as any,
search,
});
if (response.success) {
setList(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={setSearch}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
<AdminComp_BoxTitle
title={`${_.startCase(status as string)}`}
rightComponent={rightComponent}
/>
<StackCustom gap={"sm"}>
<AdminTitleTable
title1="Aksi"
title2="Username"
title3="Judul Voting"
/>
<Divider />
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" bold color="gray">
Belum ada data
</TextCustom>
) : (
list.map((item: any, i: number) => (
<AdminTableValue
key={i}
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/voting/${item.id}/${status}`);
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
}
value3={
<TextCustom truncate={2}>
{item?.title || "-"}
</TextCustom>
}
/>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenVotingStatus />;
}

View File

@@ -1,104 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
import { Octicons } from "@expo/vector-icons";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Divider } from "react-native-paper";
import { Admin_ScreenVotingHistory } from "@/screens/Admin/Voting/ScreenVotingHistory";
export default function AdminVotingHistory() {
const [list, setList] = useState<any | null>(null);
const [loadList, setLoadList] = useState(false);
const [search, setSearch] = useState<string>("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [ search])
);
const onLoadData = async () => {
try {
setLoadList(true);
const response = await apiAdminVoting({
category: "history",
search,
});
if (response.success) {
setList(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
value={search}
onChangeText={setSearch}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
<AdminComp_BoxTitle
title="Riwayat"
rightComponent={rightComponent}
/>
<StackCustom gap={"sm"}>
<AdminTitleTable
title1="Aksi"
title2="Username"
title3="Judul Voting"
/>
<Divider />
{loadList ? <LoaderCustom/> : _.isEmpty(list) ? <TextCustom align="center" bold color="gray">Belum ada data</TextCustom> : list.map((item: any, i: number) => (
<AdminTableValue
key={i}
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/voting/${item.id}/history`);
}}
/>
}
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
value3={
<TextCustom truncate={2}>
{item?.title || "-"}
</TextCustom>
}
/>
))}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenVotingHistory />;
}

1524
bun.lock

File diff suppressed because it is too large Load Diff

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

@@ -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

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

View File

@@ -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,

28
docs/CHANGE_LOG.md Normal file
View File

@@ -0,0 +1,28 @@
# CHANGE LOG - fixed-admin/18-feb-26
## Perubahan Tampilan Admin
### File Baru (4)
- `screens/Admin/Voting/ScreenVotingStatus.tsx`
- `screens/Admin/Voting/ScreenVotingHistory.tsx`
- `screens/Admin/Voting/ScreenEventTypeOfEvent.tsx`
- `screens/Admin/Voting/BoxVotingStatus.tsx`
### File Diubah (3)
- `app/(application)/admin/voting/[status]/status.tsx` → 5 baris
- `app/(application)/admin/voting/history.tsx` → 5 baris
- `app/(application)/admin/event/type-of-event.tsx` → 5 baris
### API Updates (2)
- `service/api-admin/api-admin-voting.ts` → tambah param `page`
- `service/api-admin/api-master-admin.ts` → tambah param `page`
## Fitur Baru
- Pagination (infinite scroll)
- Pull-to-Refresh
- Skeleton Loading
- Empty State
- Search Functionality
## Stats
+305 baris, -531 baris (net: -226)

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

@@ -22,8 +22,8 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additional Prompt -->
File refrensi: screens/Donation/ScreenListOfNews.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
File refrensi: screens/Admin/Event/ScreenEventStatus.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
<!-- ===================== End Penerapan Pagination ` ===================== -->
@@ -45,24 +45,6 @@ Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additinal prompt -->
Masukan kode berikut di prop ListHeaderComponent:
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
<BaseBox>
<Grid>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
</TextCustom>
<TextCustom size="small">Total Pencairan Dana</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
{data?.akumulasiPencairan} kali
</TextCustom>
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
@@ -71,19 +53,65 @@ Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
<!-- End Penerapan NewWrapper -->
Bantu saya untuk memperbaiki logika path yang ada di dalam file "screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx" , pada function fixPath
Saya ingin jika didalam deeplink ada "/admin/..." contoh "/admin/event/review/status" maka path yang akan di redirect adalah "/admin/event/review/status"
jika tidak maka terapkan sesuai dengan logika yang sudah ada
<!-- START Prompt Admin Refactoring -->
<!-- Pindah kode ke Screen Component -->
File source: app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx
Folder tujuan: screens/Admin/Donation
Nama file utama: ScreenDonationListDisbursementOfFunds.tsx
Nama function utama: Admin_ScreenDonationListDisbursementOfFunds
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Bagaimana menangani bug berikut pada file berikut: screens/Invesment/Document/ScreenRecap.tsx
Ini adalah halaman yang memiliki fungsi pagination , saya membuat data dummy dimana menghasilkan data urut 1-9, saya mencoba memuat halaman setiap page nya 4 saja untuk percobaan.
Saat awal muncul komponent box dengan data 9 - 6, kemudian saya hapus data ke 8 . lalu saya coba scroll ke bawah seharusnya angka akan tetap urut 9, 7, 6, 5, 4 ... 1. Tapi dalam case ini setelah 8 di hapus kemudian saya scroll box ke 5 tidak muncul saat di scroll. Apakah anda mengerti maksud saya ?
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
<!-- COMMIT & PUSH-->
Branch: loaddata/10-feb-26
Jalankan perintah ini: git checkout -b "Branch"
Setelah itu jalankan perintah ini: git add .
Setelah itu jalankan perintah ini: git commit -m "
<Berikan semua catatan perubahan pada branch ini, tampilan pada saya dan pastikan dalam bahasa indonesia. Saya akan cek baru saya akan berikan perintah push>
"
Setelah itu jalankan perintah ini: git push origin "Branch"
<!-- Penerapan Pagination -->
Function fecth: apiAdminDonationDisbursementOfFundsListById
File function fetch: service/api-admin/api-admin-donation.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 )
Kemudian rapikan code nya pisah komponen seperti render item dan lainnya agar lebih rapi dan di dalam return panggil komponen tersebut
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- END Prompt Admin Refactoring -->
<!-- Additional -->
File refrensi: screens/Admin/Voting/ScreenEventTypeOfEvent.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
Untuk refrensi tampilan Box bisa anda gunakan dari file: screens/Admin/Donation/BoxDonationCategory.tsx dan buatkan komponen yang mirip untuk list of donatur dengan nama file: BoxDonationListOfDonatur.tsx
<!-- Use Prompt Now -->
Terapkan NewWrapper pada file: screens/Admin/App-Information/InformationBankSection.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
Function fecth: apiAdminMasterBank
File function fetch: service/api-admin/api-master-admin.ts
Terapkan pagination pada file "Nama file utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
<!-- Create FlatList -->
File Utama: screens/Admin/App-Information/InformationBankSection.tsx
Terapkan FlatList dan pagination pada file "File Utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Function fecth: apiAdminMasterBank
File function fetch: service/api-admin/api-master-admin.ts
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
<!-- Create Box -->
File Utama: screens/Admin/Donation/Admin_ScreenDonationStatus.tsx
Folder tujuan: screens/Admin/Donation
Buat box component baru pada file "File Utama" di bagian renderItem,
<!-- END Create Box -->
<!-- END Use Prompt Now -->

View File

@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>20</string>
<string>21</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@@ -33,7 +33,7 @@
"expo-dev-client": "~6.0.12",
"expo-device": "^8.0.9",
"expo-document-picker": "~14.0.7",
"expo-file-system": "^19.0.15",
"expo-file-system": "^19.0.21",
"expo-font": "~14.0.8",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
@@ -52,7 +52,7 @@
"moti": "^0.30.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native": "0.81.5",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

@@ -1,12 +1,12 @@
import {
ViewWrapper,
DrawerCustom,
DummyLandscapeImage,
Spacing,
StackCustom,
TextCustom,
Grid,
ButtonCustom,
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import API_IMAGE from "@/constants/api-storage";
@@ -15,8 +15,8 @@ import { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useFocusEffect, router } from "expo-router";
import { useState, useCallback } from "react";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { View } from "react-native";
import MapView, { Marker } from "react-native-maps";
@@ -47,7 +47,7 @@ export default function MapsView() {
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, [])
}, []),
);
const handlerLoadList = async () => {
@@ -146,52 +146,52 @@ export default function MapsView() {
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.nomorTelepon}</TextCustom>}
rightItem={<TextCustom>{selected.nomorTelepon}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
leftItem={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
/>
<Grid>

View File

@@ -4,13 +4,15 @@ import { apiConfig } from "../api-config";
export async function apiAdminDonation({
category,
search,
page = "1",
}: {
category: "dashboard" | "publish" | "review" | "reject";
search?: string;
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/donation?category=${category}&search=${search}`
`/mobile/admin/donation?category=${category}&search=${search}&page=${page}`
);
return response.data;
} catch (error) {
@@ -131,12 +133,16 @@ export async function apiAdminDonationDisbursementOfFundsCreated({
export async function apiAdminDonationDisbursementOfFundsListById({
id,
category,
page = "1",
}: {
id: string;
category: "get-all" | "get-one"
category: "get-all" | "get-one";
page?: string;
}) {
try {
const response = await apiConfig.get(`/mobile/admin/donation/${id}/disbursement?category=${category}`);
const response = await apiConfig.get(
`/mobile/admin/donation/${id}/disbursement?category=${category}&page=${page}`
);
return response.data;
} catch (error) {
throw error;

View File

@@ -3,13 +3,15 @@ import { apiConfig } from "../api-config";
export async function apiAdminEvent({
category,
search,
page = "1",
}: {
category: "dashboard" | "history" | "publish" | "review" | "type-of-event";
search?: string;
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/event?category=${category}&search=${search}`
`/mobile/admin/event?category=${category}&search=${search}&page=${page}`
);
return response.data;
} catch (error) {
@@ -48,10 +50,18 @@ export async function apiAdminEventUpdateStatus({
}
}
export async function apiAdminEventListOfParticipants({ id }: { id: string }) {
export async function apiAdminEventListOfParticipants({
id,
page = "1",
search = ""
}: {
id: string;
page?: string;
search?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/event/${id}/participants`
`/mobile/admin/event/${id}/participants?page=${page}&search=${search}`
);
return response.data;
} catch (error) {

View File

@@ -3,13 +3,15 @@ import { apiConfig } from "../api-config";
export async function apiAdminJob({
category,
search,
page = "1",
}: {
category: "dashboard" | "publish" | "review" | "reject";
search?: string;
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/job?category=${category}&search=${search}`
`/mobile/admin/job?category=${category}&search=${search}&page=${page}`
);
return response.data;
} catch (error) {

View File

@@ -3,15 +3,31 @@ import { apiConfig } from "../api-config";
export const apiAdminUserAccessGetAll = async ({
search,
category,
page = "1",
}: {
search?: string;
category: "only-user" | "only-admin" | "all-role";
page?: string;
}) => {
try {
const response = await apiConfig.get(`/mobile/admin/user?category=${category}&search=${search}`);
return response.data;
const response = await apiConfig.get(
`/mobile/admin/user?category=${category}&search=${search}&page=${page}`,
);
// Pastikan mengembalikan struktur data yang konsisten
return {
success: response.data.success,
message: response.data.message,
data: response.data.data || [], // Gunakan data yang sebenarnya atau array kosong
pagination: response.data.pagination, // Jika ada info pagination
};
} catch (error) {
console.log(error);
return {
success: false,
message: "Error fetching data",
data: [],
pagination: null,
};
}
};
@@ -36,12 +52,15 @@ export const apiAdminUserAccessUpdateStatus = async ({
category: "access" | "role";
}) => {
try {
const response = await apiConfig.put(`/mobile/admin/user/${id}?category=${category}`, {
data: {
active,
role,
const response = await apiConfig.put(
`/mobile/admin/user/${id}?category=${category}`,
{
data: {
active,
role,
},
},
});
);
return response.data;
} catch (error) {
console.log(error);

View File

@@ -4,13 +4,15 @@ import { apiConfig } from "../api-config";
export async function apiAdminVoting({
category,
search,
page = "1",
}: {
category: "dashboard" | "history" | "publish" | "review" | "report";
search?: string;
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/voting?category=${category}&search=${search}`
`/mobile/admin/voting?category=${category}&search=${search}&page=${page}`
);
return response.data;
} catch (error) {

View File

@@ -1,9 +1,11 @@
import { apiConfig } from "../api-config";
// ================== START MASTER BANK ================== //
export async function apiAdminMasterBank() {
export async function apiAdminMasterBank({ page = "1" }: { page?: string }) {
try {
const response = await apiConfig.get(`/mobile/admin/master/bank`);
const response = await apiConfig.get(
`/mobile/admin/master/bank?page=${page}`,
);
return response.data;
} catch (error) {
throw error;
@@ -51,9 +53,15 @@ export async function apiAdminMasterBankCreate({ data }: { data: any }) {
// ================== START BUSINNES FIELD ================== //
export async function apiAdminMasterBusinessField() {
export async function apiAdminMasterBusinessField({
page = "1",
}: {
page: string;
}) {
try {
const response = await apiConfig.get(`/mobile/admin/master/business-field`);
const response = await apiConfig.get(
`/mobile/admin/master/business-field?page=${page}`,
);
return response.data;
} catch (error) {
throw error;
@@ -64,16 +72,19 @@ export async function apiAdminMasterBusinessFieldById({
id,
subBidangId,
category,
page = "1"
}: {
id: string;
subBidangId?: string | null;
category: "bidang" | "sub-bidang" | "all";
category: "bidang" | "sub-bidang" | "all" | "only-sub-bidang"
page?: string
}) {
const queryCategory = category ? `?category=${category}` : "";
const querySubBidang = subBidangId ? `&subBidangId=${subBidangId}` : "";
const queryPage = page ? `&page=${page}` : "";
try {
const response = await apiConfig.get(
`/mobile/admin/master/business-field/${id}${queryCategory}${querySubBidang}`
`/mobile/admin/master/business-field/${id}${queryCategory}${querySubBidang}${queryPage}`,
);
return response.data;
} catch (error) {
@@ -95,7 +106,7 @@ export async function apiAdminMasterBusinessFieldUpdate({
`/mobile/admin/master/business-field/${id}?category=${category}`,
{
data: data,
}
},
);
return response.data;
} catch (error) {
@@ -113,7 +124,7 @@ export async function apiAdminMasterBusinessFieldCreate({
`/mobile/admin/master/business-field`,
{
data: data,
}
},
);
return response.data;
} catch (error) {
@@ -124,9 +135,9 @@ export async function apiAdminMasterBusinessFieldCreate({
// ================== END BUSINNES FIELD ================== //
// ================== START EVENT ================== //
export async function apiAdminMasterTypeOfEvent() {
export async function apiAdminMasterTypeOfEvent({ page = "1" }: { page?: string }) {
try {
const response = await apiConfig.get(`/mobile/admin/master/type-of-event`);
const response = await apiConfig.get(`/mobile/admin/master/type-of-event?page=${page}`);
return response.data;
} catch (error) {
throw error;
@@ -139,7 +150,7 @@ export async function apiEventCreateTypeOfEvent({ data }: { data: string }) {
`/mobile/admin/master/type-of-event`,
{
data: data,
}
},
);
return response.data;
} catch (error) {
@@ -150,7 +161,7 @@ export async function apiEventCreateTypeOfEvent({ data }: { data: string }) {
export async function apiAdminMasterTypeOfEventGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(
`/mobile/admin/master/type-of-event/${id}`
`/mobile/admin/master/type-of-event/${id}`,
);
return response.data;
} catch (error) {
@@ -170,7 +181,7 @@ export async function apiAdminMasterTypeOfEventUpdate({
`/mobile/admin/master/type-of-event/${id}`,
{
data: data,
}
},
);
return response.data;
} catch (error) {
@@ -182,9 +193,9 @@ export async function apiAdminMasterTypeOfEventUpdate({
// ================== START DONATION ================== //
export async function apiAdminMasterDonationCategory() {
export async function apiAdminMasterDonationCategory({ page = "1" }: { page?: string }) {
try {
const response = await apiConfig.get(`/mobile/admin/master/donation`);
const response = await apiConfig.get(`/mobile/admin/master/donation?page=${page}`);
return response.data;
} catch (error) {
throw error;
@@ -216,7 +227,7 @@ export async function apiAdminMasterDonationCategoryUpdate({
`/mobile/admin/master/donation/${id}`,
{
data: data,
}
},
);
return response.data;
} catch (error) {
@@ -240,3 +251,27 @@ export async function apiAdminMasterDonationCategoryCreate({
}
// ================== END DONATION ================== //
// ================== START FECTH APP INFORMATION ================== //
export async function apiFetchAdminMasterAppInformation({
page = "1",
category,
}: {
page: string;
category?: "bank" | "business" | string
}) {
if (category === "bank") {
const response = await apiAdminMasterBank({ page });
// TODO: implement bank logic
return response;
} else if (category === "business") {
const response = await apiAdminMasterBusinessField({ page });
// TODO: implement business logic
return response
} else {
throw new Error("Category is required");
}
}
// ================== END FECTH APP INFORMATION ================== //

View File

@@ -16,7 +16,7 @@ export const GStyles = StyleSheet.create({
// =============== Main Styles =============== //
container: {
flex: 1,
paddingInline: PADDING_MEDIUM,
paddingInline: PADDING_SMALL,
paddingTop: PADDING_EXTRA_SMALL,
paddingBottom: 5,
// paddingBlock: PADDING_EXTRA_SMALL,