fix-ios/25-feb-26 #56

Merged
bagasbanuna merged 9 commits from fix-ios/25-feb-26 into staging 2026-02-25 16:53:11 +08:00
43 changed files with 2960 additions and 2088 deletions

518
QWEN.md
View File

@@ -2,56 +2,68 @@
## Project Overview
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named "HIPMI Badung Connect" and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named **"HIPMI Badung Connect"** and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
### Key Technologies
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
- **Framework**: Expo (v54.0.0) with React Native (v0.81.5)
- **Language**: TypeScript
- **Architecture**: File-based routing with Expo Router
- **State Management**: Context API
- **State Management**: Context API (AuthContext)
- **UI Components**: React Native Paper, custom components
- **Maps Integration**: Mapbox Maps for React Native
- **Push Notifications**: React Native Firebase Messaging
- **Build System**: Metro bundler
- **Package Manager**: Bun
### Project Structure
```
hipmi-mobile/
├── app/ # Main application screens and routing
├── app/ # Main application screens and routing (Expo Router)
│ ├── _layout.tsx # Root layout component
│ ├── index.tsx # Entry point (Login screen)
│ └── ...
│ └── (application)/ # Main app screens
│ ├── admin/ # Admin panel screens
│ ├── (user)/ # User screens
│ └── ...
├── components/ # Reusable UI components
│ ├── _ShareComponent/ # Shared components (NewWrapper, Admin components)
│ ├── _Icon/ # Icon components
│ └── ...
├── context/ # State management (AuthContext)
├── screens/ # Screen components organized by feature
│ ├── Admin/ # Admin panel screens
│ │ ├── Donation/ # Donation management screens
│ │ ├── Voting/ # Voting management screens
│ │ ├── Event/ # Event management screens
│ │ └── ...
│ ├── Authentication/ # Login, registration flows
│ ├── Collaboration/ # Collaboration features
│ ├── Event/ # Event management
│ ├── Forum/ # Forum functionality
│ ├── Home/ # Home screen
│ ├── Maps/ # Map integration
│ ├── Profile/ # User profile
│ ├── RootLayout/ # Root layout components
│ └── ...
├── assets/ # Images, icons, and static assets
├── constants/ # Constants and configuration values
├── helpers/ # Helper functions (pagination, etc.)
├── hooks/ # Custom React hooks
├── lib/ # Utility libraries
├── navigation/ # Navigation configuration
├── service/ # API services and business logic
│ ├── api-admin/ # Admin API endpoints
│ ├── api-client/ # Client API endpoints
│ └── api-config.ts # Axios configuration
├── hooks/ # Custom React hooks
│ ├── use-pagination.tsx # Pagination hook
│ └── ...
├── helpers/ # Helper functions
│ ├── paginationHelpers.tsx # Pagination UI helpers
│ └── ...
├── types/ # TypeScript type definitions
── utils/ # Helper functions
── utils/ # Utility functions
├── constants/ # Constants and configuration values
├── styles/ # Global styles
├── assets/ # Images, icons, and static assets
└── docs/ # Documentation files
```
## Building and Running
### Prerequisites
- Node.js (with bun as the package manager)
- Expo CLI
- iOS Simulator or Android Emulator (for native builds)
- Android Studio (for Android builds)
- Xcode (for iOS builds, macOS only)
- **Node.js**: v18+ with Bun package manager
- **Expo CLI**: Installed globally or via npx
- **iOS**: Xcode (macOS only) for iOS simulator/builds
- **Android**: Android Studio for Android emulator/builds
### Setup and Development
@@ -63,16 +75,27 @@ hipmi-mobile/
2. **Run Development Server**
```bash
bun run start
```
Or use the shorthand:
```bash
# or
bunx expo start
```
3. **Platform-Specific Commands**
- iOS: `bun run ios` or `bunx expo start --ios`
- Android: `bun run android` or `bunx expo start --android`
- Web: `bun run web` or `bunx expo start --web`
```bash
# iOS Simulator
bun run ios
# or
bunx expo start --ios
# Android Emulator
bun run android
# or
bunx expo start --android
# Web Browser
bun run web
# or
bunx expo start --web
```
4. **Linting**
```bash
@@ -83,13 +106,13 @@ hipmi-mobile/
#### EAS Build (Production)
```bash
# Production build
# Production build (App Store / Play Store)
eas build --profile production
# Preview build
# Preview build (Internal distribution)
eas build --profile preview
# Development build
# Development build (Development client)
eas build --profile development
```
@@ -100,7 +123,7 @@ npx expo prebuild
# iOS specific
bunx expo prebuild --platform ios
open ios/HIPMIBADUNG.xcworkspace
open ios/HIPMIBadungConnect.xcworkspace
# Android specific
bunx expo prebuild --platform android
@@ -110,6 +133,12 @@ bunx expo prebuild --platform android
```bash
# Patch version update
npm version patch
# Update iOS build number
bunx expo prebuild --platform ios
# Update Android version code
bunx expo prebuild --platform android
```
### Android Debugging
@@ -126,157 +155,336 @@ 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
Create a `.env` file in the project root with:
Create a `.env` file in the project root with these variables.
```env
API_BASE_URL=https://your-api-base-url.com
BASE_URL=https://your-app-url.com
DEEP_LINK_URL=hipmimobile://
```
## EAS Build Configuration
These are loaded in `app.config.js` and accessible via `Constants.expoConfig.extra`.
The project uses Expo Application Services (EAS) for building and deploying:
- **Development**: Development builds with development client
- **Preview**: Internal distribution builds (APK for Android)
- **Production**: App store builds (App Bundle for Android, IPA for iOS)
## Architecture Patterns
Configuration is in `eas.json`.
### 1. Separation of Concerns
## Features and Functionality
**Route Files** (`app/`) should be minimal (max 5 lines):
```typescript
import { Admin_ScreenXXX } from "@/screens/Admin/XXX/ScreenXXX";
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 with fund disbursement tracking
- **Job Board**: Employment opportunities
- **Investment**: Investment-related features
- **Voting**: Voting systems
- **Portfolio**: Member portfolio showcase
- **Notifications**: Push notifications via Firebase
export default function AdminXXX() {
return <Admin_ScreenXXX />;
}
```
**Screen Components** (`screens/`) contain all business logic:
```typescript
export function Admin_ScreenXXX() {
// Logic, hooks, state management
return <NewWrapper ... />;
}
```
### 2. Pagination Pattern
Using `usePagination` hook with infinite scroll:
```typescript
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiXXX({ page: String(page) });
if (response.success) {
return { data: response.data };
}
return { data: [] };
},
pageSize: PAGINATION_DEFAULT_TAKE, // 10
searchQuery: search,
dependencies: [dependency],
});
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada data",
skeletonCount: PAGINATION_DEFAULT_TAKE,
});
```
### 3. Wrapper Components
**NewWrapper** (preferred for lists):
```typescript
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
headerComponent={headerComponent}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={<RefreshControl ... />}
/>
```
**AdminBasicBox** (for card layouts):
```typescript
<AdminBasicBox
onPress={() => router.push(`/path/${item.id}`)}
style={{ marginHorizontal: 10, marginVertical: 5 }}
>
<StackCustom gap={0}>
<GridSpan_4_8 label="Label" value={<TextCustom>Value</TextCustom>} />
</StackCustom>
</AdminBasicBox>
```
### 4. API Service Structure
```typescript
// service/api-admin/api-xxx.ts
export async function apiXXX({ page = "1" }: { page?: string }) {
try {
const response = await apiConfig.get(`/mobile/admin/xxx?page=${page}`);
return response.data;
} catch (error) {
throw error;
}
}
```
**Important**: All list APIs should support pagination with `page` parameter (default: "1").
### 5. Authentication Flow
Managed by `AuthContext`:
- `loginWithNomor()` - Send phone number, receive OTP
- `validateOtp()` - Validate OTP, get token
- `registerUser()` - Register new user
- `logout()` - Clear session and logout
- `userData()` - Fetch user data by token
## Development Conventions
### Coding Standards
- TypeScript is used throughout the project for type safety
- Component-based architecture with reusable components
- Context API for state management (AuthContext)
- File-based routing with Expo Router
- Consistent naming conventions using camelCase for variables and PascalCase for components
- Path aliases: `@/*` maps to project root
- **TypeScript**: Strict mode enabled
- **Naming**:
- Components: PascalCase (`Admin_ScreenDonationStatus`)
- Files: PascalCase for components (`ScreenDonationStatus.tsx`)
- Variables: camelCase
- Constants: UPPER_SNAKE_CASE
- **Path Aliases**: `@/*` maps to project root
- **Imports**: Group imports by type (components, hooks, services, etc.)
### Architecture Patterns
### Component Structure
```typescript
// 1. Imports (grouped)
import { ... } from "@/components";
import { ... } from "@/hooks";
import { ... } from "@/service";
#### 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 />;
}
```
// 2. Types/Interfaces
interface Props { ... }
#### 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
// 3. Main Component
export function ComponentName() {
// State
// Hooks
// Functions
// Render
}
```
### Testing
- Linting is configured with ESLint
- Standard Expo linting configuration
- Linting: `bun run lint`
- No formal test suite configured yet
### Security
- Firebase is integrated for authentication and messaging
- Camera and location permissions are properly configured
- Deep linking is secured with app domain associations
- Auth tokens stored in AsyncStorage
### Git Workflow
- Feature branches: `feature/xxx` or `fixed-admin/xxx`
- Commit messages: Clear and descriptive
- Use CHANGE_LOG.md for tracking changes
## Key Dependencies
## Key Features
### Core Dependencies
- `@react-navigation/*`: Navigation solution for React Native
- `@react-native-firebase/*`: Firebase integration for React Native
- `@rnmapbox/maps`: Mapbox integration for React Native
- `expo-router`: File-based routing for Expo applications
- `react-native-paper`: Material Design components for React Native
- `react-native-toast-message`: Toast notifications
- `react-native-otp-entry`: OTP input components
- `react-native-qrcode-svg`: QR code generation
- `axios`: HTTP client for API calls
- `lodash`: Utility library
- `moti`: Animation library
### Authentication
- Phone number login with OTP
- User registration
- Terms & Conditions acceptance
- Session persistence with AsyncStorage
### Development Dependencies
- `@types/*`: TypeScript type definitions
- `eslint-config-expo`: Expo-specific ESLint configuration
- `typescript`: Type checking
### Admin Module
- **Dashboard**: Overview and statistics
- **User Access**: User management
- **Event**: Event CRUD with status management
- **Voting**: Voting management (publish/review/reject)
- **Donation**: Donation management with categories and transaction tracking
- **Collaboration**: Collaboration requests
- **Investment**: Investment management
- **Maps**: Location-based features
- **App Information**: Bank and business field management
## Platform Support
### User Module
- **Home**: Main dashboard
- **Forum**: Discussion forums
- **Profile**: User profile management
- **Portfolio**: Member portfolio
- **Notifications**: Push notifications via Firebase
The application is configured to support:
- **iOS**:
- 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
## API Configuration
## Special Configurations
### Base URLs
```typescript
// From app.config.js extra
API_BASE_URL: process.env.API_BASE_URL
BASE_URL: process.env.BASE_URL
DEEP_LINK_URL: process.env.DEEP_LINK_URL
```
### Axios Interceptor
All API calls use `apiConfig` with automatic token injection:
```typescript
apiConfig.interceptors.request.use(async (config) => {
const token = await AsyncStorage.getItem("authToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
```
## Platform Configuration
### iOS
- **Bundle ID**: `com.anonymous.hipmi-mobile`
- **Build Number**: 21
- **Google Services**: Configured
- **Associated Domains**: `applinks:cld-dkr-staging-hipmi.wibudev.com`
- **Tablet Support**: Enabled
### Android
- **Package**: `com.bip.hipmimobileapp`
- **Version Code**: 4
- **Google Services**: Configured (`google-services.json`)
- **Deep Links**: HTTPS intent filters configured
- **Edge-to-Edge**: Enabled
### Web
- **Output**: Static
- **Bundler**: Metro
## Special Integrations
### Firebase
- Authentication
- Push Notifications (FCM)
- Configured for both iOS and Android
### Mapbox
- Map integration via `@rnmapbox/maps`
- Location permissions configured
### 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.
- HTTPS: `cld-dkr-staging-hipmi.wibudev.com`
- Configured for both platforms
### Camera
Camera permissions configured for both iOS and Android with microphone access for recording.
- Camera and microphone permissions
- QR code generation support
## 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 a New Admin Screen
### 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
1. **Create Screen Component** (`screens/Admin/Feature/ScreenXXX.tsx`):
```typescript
export function Admin_ScreenXXX() {
const pagination = usePagination({...});
const renderItem = useCallback(...);
const headerComponent = useMemo(...);
return <NewWrapper ... />;
}
```
### 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
2. **Create Box Component** (optional, for custom item rendering):
```typescript
export default function Admin_BoxXXX({ item }: { item: any }) {
return (
<AdminBasicBox onPress={() => router.push(...)}>
...
</AdminBasicBox>
);
}
```
3. **Update API** (add pagination if needed):
```typescript
export async function apiXXX({ page = "1" }: { page?: string }) {
// ...
}
```
4. **Create Route File** (`app/(application)/admin/feature/xxx.tsx`):
```typescript
import { Admin_ScreenXXX } from "@/screens/Admin/Feature/ScreenXXX";
export default function AdminXXX() {
return <Admin_ScreenXXX />;
}
```
### Updating API Endpoints
1. Add function in appropriate service file
2. Include `page` parameter for list endpoints
3. Use `apiConfig` axios instance
4. Handle errors properly
## Troubleshooting
### Build Issues
```bash
# Clean and rebuild
rm -rf node_modules
bun install
bunx expo prebuild --clean
# iOS specific
cd ios && pod install && cd ..
# Android specific
cd android && ./gradlew clean && cd ..
```
### Cache Issues
```bash
# Clear Expo cache
bunx expo start -c
# Clear Metro cache
bunx expo start --clear
```
### Dependency Issues
```bash
# Reinstall dependencies
rm -rf node_modules bun.lock
bun install
```
## Documentation Files
- `docs/CHANGE_LOG.md` - Change log for recent updates
- `docs/COMMIT_NOTES.md` - Commit notes and guidelines
- `docs/hipmi-note.md` - Build and deployment notes
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns
## Resources
- [Expo Documentation](https://docs.expo.dev/)
- [React Native Documentation](https://reactnative.dev/)
- [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)

View File

@@ -77,7 +77,6 @@ export default {
},
],
"expo-font",
"@rnmapbox/maps",
"@react-native-firebase/app",
[
"expo-notifications",
@@ -87,6 +86,7 @@ export default {
iosDisplayInForeground: true,
},
],
"@maplibre/maplibre-react-native",
],
experiments: {

View File

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

View File

@@ -115,6 +115,7 @@ export default function Application() {
}
footerComponent={
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,

View File

@@ -1,6 +1,4 @@
import MapsView from "@/screens/Maps/MapsView";
import MapsView2 from "@/screens/Maps/MapsView2";
import { Text, View } from "react-native";
export interface LocationItem {
id: string | number;
@@ -13,8 +11,14 @@ export interface LocationItem {
export default function Maps() {
return (
<>
<MapsView />
{/* <MapsView2 />, */}
{/* <Stack.Screen
options={{
title: "Maps",
headerLeft: () => <BackButton />,
}}
/> */}
{/* {Platform.OS === "ios" ? <MapsView /> : <MapsView2 />} */}
<MapsView2 />
{/* <View style={{ flex: 1, backgroundColor: "gray" }}><Text style={{ color: "white" }}>Map disabled</Text></View> */}
</>
);

View File

@@ -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";
@@ -94,11 +94,14 @@ export default function Portofolio() {
data={data}
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
/>
<Portofolio_BusinessLocation
data={data?.BusinessMaps}
imageId={data?.logoId}
setOpenDrawerLocation={setOpenDrawerLocation}
/>
{data?.BusinessMaps && (
<Portofolio_BusinessLocation
data={data?.BusinessMaps}
imageId={data?.logoId}
setOpenDrawerLocation={setOpenDrawerLocation}
/>
)}
<Portofolio_SocialMediaSection
data={data?.Portofolio_MediaSosial}
/>
@@ -135,10 +138,12 @@ export default function Portofolio() {
closeDrawer={() => setOpenDrawerLocation(false)}
height={"auto"}
>
<DummyLandscapeImage
height={200}
imageId={data?.BusinessMaps?.imageId}
/>
{data?.BusinessMaps?.imageId && (
<DummyLandscapeImage
height={200}
imageId={data?.BusinessMaps?.imageId}
/>
)}
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView

View File

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

View File

@@ -1,91 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { IconOpenTo } from "@/components/_Icon/IconOpenTo";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import { apiAdminForumCommentById } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Divider } from "react-native-paper";
import { Admin_ScreenForumListComment } from "@/screens/Admin/Forum/ScreenForumListComment";
export default function AdminForumListComment() {
const { id } = useLocalSearchParams();
const [listComment, setListComment] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadComment();
}, [id])
);
const onLoadComment = async () => {
try {
setLoadList(true);
const response = await apiAdminForumCommentById({
id: id as string,
category: "get-all",
});
if (response.success) {
setListComment(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
setListComment([]);
} finally {
setLoadList(false);
}
};
return (
<>
<ViewWrapper
headerComponent={<AdminBackButtonAntTitle title="Daftar Komentar" />}
>
<StackCustom>
<AdminTitleTable title1="Aksi" title2="Report" title3="Komentar" />
<Divider />
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listComment) ? (
<TextCustom align="center" color="gray">
Tidak ada komentar
</TextCustom>
) : (
listComment?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<IconOpenTo
onPress={() => {
router.push(
`/admin/forum/${item.id}/list-report-comment`
);
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.countReport || 0}
</TextCustom>
}
value3={
<TextCustom truncate={2}>{item?.komentar || "-"}</TextCustom>
}
/>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenForumListComment />;
}

View File

@@ -1,262 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
AlertDefaultSystem,
BaseBox,
CenterCustom,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconDot, IconView } from "@/components/_Icon/IconComponent";
import { IconTrash } from "@/components/_Icon/IconTrash";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminForumCommentById,
apiAdminForumDeactivateComment,
apiAdminForumListReportCommentById,
} from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import Toast from "react-native-toast-message";
import { Admin_ScreenForumDetailReportComment } from "@/screens/Admin/Forum/ScreenForumDetailReportComment";
export default function AdminForumReportComment() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [data, setData] = useState<any | null>(null);
const [listReport, setListReport] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerAction, setOpenDrawerAction] = useState(false);
const [selectedReport, setSelectedReport] = useState({
id: "",
username: "",
kategori: "",
keterangan: "",
deskripsi: "",
});
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadList(true);
const response = await apiAdminForumCommentById({
id: id as string,
category: "get-one",
});
const responseReport = await apiAdminForumListReportCommentById({
id: id as string,
});
if (response.success) {
setData(response.data);
}
if (responseReport.success) {
setListReport(responseReport.data);
}
} catch (error) {
console.log("[ERROR]", error);
setData(null);
setListReport([]);
} finally {
setLoadList(false);
}
};
return (
<>
<ViewWrapper
headerComponent={
<AdminBackButtonAntTitle
title="Report Komentar"
rightComponent={
<ActionIcon
icon={<IconDot size={16} color={MainColor.darkblue} />}
onPress={() => setOpenDrawer(true)}
/>
}
/>
}
>
<BaseBox>
<StackCustom gap={"sm"}>
<GridSpan_NewComponent
text1={<TextCustom bold>Username</TextCustom>}
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
/>
<GridSpan_NewComponent
text1={<TextCustom bold>Komentar</TextCustom>}
text2={<TextCustom>{data?.komentar || "-"}</TextCustom>}
/>
</StackCustom>
</BaseBox>
<AdminComp_BoxTitle title="Daftar Report Komentar" />
<StackCustom gap={"sm"}>
<GridSpan_NewComponent
text1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
text2={<TextCustom bold>Pelapor</TextCustom>}
text3={<TextCustom bold>Kategori Report</TextCustom>}
/>
<Divider />
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listReport) ? (
<TextCustom align="center" color="gray">
Tidak ada report
</TextCustom>
) : (
listReport?.map((item: any, index: number) => (
<View key={index}>
<GridSpan_NewComponent
text1={
<CenterCustom>
<ActionIcon
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item.id,
username: item.User?.username,
kategori: item.ForumMaster_KategoriReport?.title,
keterangan:
item.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item.deskripsi,
});
}}
/>
</CenterCustom>
}
text2={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
text3={
<TextCustom truncate={2}>
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
<Divider />
</View>
))
)}
</StackCustom>
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconTrash />,
label: "Hapus Komentar",
value: "delete",
path: "",
color: MainColor.red,
},
]}
onPressItem={(item) => {
AlertDefaultSystem({
title: "Hapus Komentar",
message: "Apakah Anda yakin ingin menghapus komentar ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: async () => {
const deleteComment = await apiAdminForumDeactivateComment({
id: id as string,
data: {
senderId: user?.id as string,
},
});
// if (!deleteComment.success) {
// Toast.show({
// type: "error",
// text1: "Komentar gagal dihapus",
// });
// return;
// }
setOpenDrawer(false);
Toast.show({
type: "success",
text1: "Komentar berhasil dihapus",
});
router.back();
},
});
}}
/>
</DrawerCustom>
<DrawerCustom
isVisible={openDrawerAction}
closeDrawer={() => setOpenDrawerAction(false)}
height={"auto"}
>
<StackCustom>
<GridSpan_4_8
label={<TextCustom bold>Pelapor</TextCustom>}
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
/>
{selectedReport?.kategori && (
<>
<GridSpan_4_8
label={<TextCustom bold>Kategori Report</TextCustom>}
value={
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom bold>Keterangan</TextCustom>}
value={
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
}
/>
</>
)}
{selectedReport?.deskripsi && (
<GridSpan_4_8
label={<TextCustom bold>Deskripsi</TextCustom>}
value={
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
}
/>
)}
</StackCustom>
</DrawerCustom>
</>
);
return <Admin_ScreenForumDetailReportComment />;
}

View File

@@ -1,283 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
AlertDefaultSystem,
BadgeCustom,
BaseBox,
CenterCustom,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconDot, IconView } from "@/components/_Icon/IconComponent";
import { IconTrash } from "@/components/_Icon/IconTrash";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminForumDeactivatePosting,
apiAdminForumListReportPostingById,
apiAdminForumPostingById,
} from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import Toast from "react-native-toast-message";
import { Admin_ScreenForumDetailReportPosting } from "@/screens/Admin/Forum/ScreenForumDetailReportPosting";
export default function AdminForumReportPosting() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawerPage, setOpenDrawerPage] = useState(false);
const [openDrawerAction, setOpenDrawerAction] = useState(false);
const [data, setData] = useState<any | null>(null);
const [listReport, setListReport] = useState<any[] | null>(null);
const [loadListReport, setLoadListReport] = useState(false);
const [selectedReport, setSelectedReport] = useState({
id: "",
username: "",
kategori: "",
keterangan: "",
deskripsi: "",
});
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadListReport(true);
const response = await apiAdminForumPostingById({
id: id as string,
});
const responseReport = await apiAdminForumListReportPostingById({
id: id as string,
});
if (response.success) {
setData(response.data);
}
if (responseReport.success) {
setListReport(responseReport.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadListReport(false);
}
};
return (
<>
<ViewWrapper
headerComponent={
<AdminBackButtonAntTitle
title="Report Posting"
rightComponent={
<ActionIcon
icon={<IconDot size={16} color={MainColor.darkblue} />}
onPress={() => setOpenDrawerPage(true)}
/>
}
/>
}
>
<BaseBox>
<StackCustom gap={"sm"}>
<GridSpan_NewComponent
text1={<TextCustom bold>Username</TextCustom>}
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
/>
<GridSpan_NewComponent
text1={<TextCustom bold>Status</TextCustom>}
text2={
data && data?.ForumMaster_StatusPosting?.status ? (
<BadgeCustom
color={
data?.ForumMaster_StatusPosting?.status === "Open"
? MainColor.green
: MainColor.red
}
>
{data?.ForumMaster_StatusPosting?.status === "Open"
? "Open"
: "Close"}
</BadgeCustom>
) : (
<TextCustom>{"-"}</TextCustom>
)
}
/>
<GridSpan_NewComponent
text1={<TextCustom bold>Postingan</TextCustom>}
text2={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
/>
</StackCustom>
</BaseBox>
<AdminComp_BoxTitle title="Daftar Report Posting" />
<StackCustom gap={"sm"}>
<GridSpan_NewComponent
text1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
text2={<TextCustom bold>Pelapor</TextCustom>}
text3={<TextCustom bold>Kategori Report</TextCustom>}
/>
<Divider />
{loadListReport ? (
<LoaderCustom />
) : _.isEmpty(listReport) ? (
<TextCustom align="center" color={"gray"}>
Belum ada report
</TextCustom>
) : (
listReport?.map((item: any, index: number) => (
<View key={index}>
<GridSpan_NewComponent
text1={
<CenterCustom>
<ActionIcon
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item?.id,
username: item?.User?.username,
kategori: item?.ForumMaster_KategoriReport?.title,
keterangan:
item?.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item?.deskripsi,
});
}}
/>
</CenterCustom>
}
text2={
<TextCustom truncate>
{item?.User?.username || "-"}
</TextCustom>
}
text3={
<TextCustom truncate={2}>
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
<Divider />
</View>
))
)}
</StackCustom>
</ViewWrapper>
<DrawerCustom
isVisible={openDrawerPage}
closeDrawer={() => setOpenDrawerPage(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconTrash />,
label: "Hapus Posting",
value: "delete",
path: "",
color: MainColor.red,
},
]}
onPressItem={(item) => {
AlertDefaultSystem({
title: "Hapus Posting",
message: "Apakah Anda yakin ingin menghapus posting ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: async () => {
const response = await apiAdminForumDeactivatePosting({
id: id as string,
data: {
senderId: user?.id as string,
},
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Posting gagal dihapus",
});
return;
}
setOpenDrawerPage(false);
Toast.show({
type: "success",
text1: "Posting berhasil dihapus",
});
router.back();
},
});
}}
/>
</DrawerCustom>
<DrawerCustom
isVisible={openDrawerAction}
closeDrawer={() => setOpenDrawerAction(false)}
height={"auto"}
>
<StackCustom>
<GridSpan_4_8
label={<TextCustom bold>Pelapor</TextCustom>}
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
/>
{selectedReport?.kategori && (
<>
<GridSpan_4_8
label={<TextCustom bold>Kategori Report</TextCustom>}
value={
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
}
/>
<GridSpan_4_8
label={<TextCustom bold>Keterangan</TextCustom>}
value={
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
}
/>
</>
)}
{selectedReport?.deskripsi && (
<GridSpan_4_8
label={<TextCustom bold>Deskripsi</TextCustom>}
value={
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
}
/>
)}
</StackCustom>
</DrawerCustom>
</>
);
export default function AdminForumDetailReportPosting() {
return <Admin_ScreenForumDetailReportPosting />;
}

View File

@@ -1,121 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ClickableCustom,
LoaderCustom,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import { Admin_ScreenForumPosting } from "@/screens/Admin/Forum/ScreenForumPosting";
export default function AdminForumPosting() {
const [list, setList] = useState<any | null>(null);
const [loadList, setLoadList] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, [search])
);
const handlerLoadList = async () => {
try {
setLoadList(true);
const response = await apiAdminForum({
category: "posting",
search: search,
});
console.log("DATA", JSON.stringify(response, null, 2));
if (response.success) {
setList(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari postingan"
value={search}
onChangeText={setSearch}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
<AdminComp_BoxTitle title={"Posting"} rightComponent={rightComponent} />
<GridSpan_NewComponent
text1={<TextCustom bold truncate>Username</TextCustom>}
text2={<TextCustom bold truncate> Postingan</TextCustom>}
text3={<TextCustom bold align="center" truncate> Report Posting</TextCustom>}
text4={<TextCustom bold align="center" truncate> Komentar</TextCustom>}
/>
<Divider />
<Spacing />
<StackCustom>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<View key={index}>
<ClickableCustom
onPress={() => {
router.push(`/admin/forum/${item.id}`);
}}
>
<GridSpan_NewComponent
text1={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
}
text2={
<TextCustom truncate>
{item?.diskusi || "-"}
</TextCustom>
}
text3={
<TextCustom align="center" truncate={2}>
{item?.reportPosting || "-"}
</TextCustom>
}
text4={
<TextCustom align="center" truncate={2}>
{item?.komentar || "-"}
</TextCustom>
}
/>
</ClickableCustom>
<Divider />
</View>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenForumPosting />;
}

View File

@@ -1,137 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
ClickableCustom,
LoaderCustom,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconView } from "@/components/_Icon/IconComponent";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import { Admin_ScreenForumReportComment } from "@/screens/Admin/Forum/ScreenForumReportComment";
export default function AdminForumReportComment() {
const [listData, setListData] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
const [search, setSearch] = useState<string>("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
setLoadList(true);
const response = await apiAdminForum({
category: "report_comment",
search: search,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari Komentar"
value={search}
onChangeText={setSearch}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
<AdminComp_BoxTitle
title="Report Komentar"
rightComponent={rightComponent}
/>
<GridSpan_NewComponent
text1={
<TextCustom bold truncate>
Pelapor
</TextCustom>
}
text2={
<TextCustom bold truncate>
Komentar
</TextCustom>
}
text3={
<TextCustom bold truncate>
Jenis Laporan
</TextCustom>
}
/>
<Divider />
<Spacing />
<StackCustom gap={"lg"}>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<View key={index}>
<ClickableCustom
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`
);
}}
>
<GridSpan_NewComponent
text1={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
text2={
<TextCustom truncate={2}>
{item?.Forum_Komentar?.komentar || "-"}
</TextCustom>
}
text3={
<TextCustom truncate={2}>
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
</ClickableCustom>
<Spacing />
<Divider />
</View>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenForumReportComment />;
}

View File

@@ -1,124 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
ClickableCustom,
Divider,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconView } from "@/components/_Icon/IconComponent";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Admin_ScreenForumReportPosting } from "@/screens/Admin/Forum/ScreenForumReportPosting";
export default function AdminForumReportPosting() {
const [listData, setListData] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
const [search, setSearch] = useState<string>("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
setLoadList(true);
const response = await apiAdminForum({
category: "report_posting",
search: search,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari Postingan"
value={search}
onChangeText={setSearch}
/>
);
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
<AdminComp_BoxTitle
title="Report Posting"
rightComponent={rightComponent}
/>
<GridSpan_NewComponent
text1={
<TextCustom bold truncate>
Pelapor
</TextCustom>
}
text2={
<TextCustom bold truncate>
Postingan
</TextCustom>
}
/>
<Divider />
<StackCustom>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<View key={index}>
<ClickableCustom
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`
);
}}
>
<GridSpan_NewComponent
text1={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
text2={
<TextCustom truncate={1}>
{item?.Forum_Posting?.diskusi || "-"}
</TextCustom>
}
/>
</ClickableCustom>
<Divider />
</View>
))
)}
</StackCustom>
</ViewWrapper>
</>
);
return <Admin_ScreenForumReportPosting />;
}

View File

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

View File

@@ -60,7 +60,7 @@ export default function AdminInvestmentTransactionDetail() {
value: (data && data?.MasterBank?.namaBank) || "-",
},
{
label: "Jumlah Investasi",
label: "Nominal",
value: (data && `Rp. ${formatCurrencyDisplay(data?.nominal)}`) || "-",
},
{

View File

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

View File

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

View File

@@ -5,6 +5,7 @@
"name": "hipmi-mobile",
"dependencies": {
"@expo/vector-icons": "^15.0.2",
"@maplibre/maplibre-react-native": "^10.4.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-firebase/app": "^23.7.0",
@@ -14,7 +15,6 @@
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@rnmapbox/maps": "^10.2.7",
"@types/lodash": "^4.17.20",
"@types/react-native-vector-icons": "^6.4.18",
"axios": "^1.11.0",
@@ -580,6 +580,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@maplibre/maplibre-react-native": ["@maplibre/maplibre-react-native@10.4.2", "", { "dependencies": { "@turf/distance": "^7.1.0", "@turf/helpers": "^7.1.0", "@turf/length": "^7.1.0", "@turf/nearest-point-on-line": "^7.1.0", "debounce": "^2.2.0" }, "peerDependencies": { "@expo/config-plugins": ">=7", "@types/geojson": "^7946.0.0", "@types/react": ">=16.6.1", "react": ">=16.6.1", "react-native": ">=0.59.9" }, "optionalPeers": ["@expo/config-plugins", "@types/geojson", "@types/react"] }, "sha512-5qAfaEe66eMXyILklm2DMHwyaXwXxsZWVop4BqfU7AyTg13LHAcaMmLJNJ3jPkMtiJvjH2m8ywGnobdIg2I0lg=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
"@motionone/dom": ["@motionone/dom@10.12.0", "", { "dependencies": { "@motionone/animation": "^10.12.0", "@motionone/generators": "^10.12.0", "@motionone/types": "^10.12.0", "@motionone/utils": "^10.12.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw=="],
@@ -742,8 +744,6 @@
"@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="],
"@rnmapbox/maps": ["@rnmapbox/maps@10.2.10", "", { "dependencies": { "@turf/along": "6.5.0", "@turf/distance": "6.5.0", "@turf/helpers": "6.5.0", "@turf/length": "6.5.0", "@turf/nearest-point-on-line": "6.5.0", "@types/geojson": "^7946.0.7", "debounce": "^2.2.0" }, "peerDependencies": { "expo": ">=47.0.0", "mapbox-gl": "^2.9.0", "react": ">=17.0.0", "react-dom": ">= 17.0.0", "react-native": ">=0.69" }, "optionalPeers": ["expo", "mapbox-gl", "react-dom"] }, "sha512-OfjW0rHp5bUWfzBo5fZ7qdKwAzGoocXYTsSssSPVMxZ2Y7axuhcbmsO5bV6gg+BJs5RwEsghzwTIoGydBNUClA=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="],
@@ -764,29 +764,17 @@
"@tsconfig/node18": ["@tsconfig/node18@18.2.6", "", {}, "sha512-eAWQzAjPj18tKnDzmWstz4OyWewLUNBm9tdoN9LayzoboRktYx3Enk1ZXPmThj55L7c4VWYq/Bzq0A51znZfhw=="],
"@turf/along": ["@turf/along@6.5.0", "", { "dependencies": { "@turf/bearing": "^6.5.0", "@turf/destination": "^6.5.0", "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-LLyWQ0AARqJCmMcIEAXF4GEu8usmd4Kbz3qk1Oy5HoRNpZX47+i5exQtmIWKdqJ1MMhW26fCTXgpsEs5zgJ5gw=="],
"@turf/distance": ["@turf/distance@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@turf/invariant": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-9drWgd46uHPPyzgrcRQLgSvdS/SjVlQ6ZIBoRQagS5P2kSjUbcOXHIMeOSPwfxwlKhEtobLyr+IiR2ns1TfF8w=="],
"@turf/bbox": ["@turf/bbox@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-D5ErVWtfQbEPh11yzI69uxqrcJmbPU/9Y59f1uTapgwAwQHQztDWgsYpnL3ns8r1GmPWLP8sGJLVTIk2TZSiYA=="],
"@turf/helpers": ["@turf/helpers@7.3.4", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g=="],
"@turf/bearing": ["@turf/bearing@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A=="],
"@turf/invariant": ["@turf/invariant@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ=="],
"@turf/destination": ["@turf/destination@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ=="],
"@turf/length": ["@turf/length@7.3.4", "", { "dependencies": { "@turf/distance": "7.3.4", "@turf/helpers": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Dg1GnQ/B2go5NIWXt91N4L7XTjIgIWCftBSYIXkrpIM7QGjItzglek0Z5caytvb8ZRWXzZOGs8//+Q5we91WuQ=="],
"@turf/distance": ["@turf/distance@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg=="],
"@turf/meta": ["@turf/meta@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw=="],
"@turf/helpers": ["@turf/helpers@6.5.0", "", {}, "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw=="],
"@turf/invariant": ["@turf/invariant@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg=="],
"@turf/length": ["@turf/length@6.5.0", "", { "dependencies": { "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-5pL5/pnw52fck3oRsHDcSGrj9HibvtlrZ0QNy2OcW8qBFDNgZ4jtl6U7eATVoyWPKBHszW3dWETW+iLV7UARig=="],
"@turf/line-intersect": ["@turf/line-intersect@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/line-segment": "^6.5.0", "@turf/meta": "^6.5.0", "geojson-rbush": "3.x" } }, "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA=="],
"@turf/line-segment": ["@turf/line-segment@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw=="],
"@turf/meta": ["@turf/meta@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA=="],
"@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@6.5.0", "", { "dependencies": { "@turf/bearing": "^6.5.0", "@turf/destination": "^6.5.0", "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/line-intersect": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg=="],
"@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@7.3.4", "", { "dependencies": { "@turf/distance": "7.3.4", "@turf/helpers": "7.3.4", "@turf/invariant": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-DQrP3lRju83rIXFN68tUEpc7ki/eRwdwBkK2CTT4RAcyCxbcH2NGJPQv8dYiww/Ar77u1WLVn+aINXZH904dWw=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
@@ -1482,8 +1470,6 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"geojson-rbush": ["geojson-rbush@3.2.0", "", { "dependencies": { "@turf/bbox": "*", "@turf/helpers": "6.x", "@turf/meta": "6.x", "@types/geojson": "7946.0.8", "rbush": "^3.0.1" } }, "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -2082,14 +2068,10 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"rbush": ["rbush@3.0.1", "", { "dependencies": { "quickselect": "^2.0.0" } }, "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
@@ -2694,10 +2676,6 @@
"@testing-library/react-native/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
"@turf/bbox/@turf/helpers": ["@turf/helpers@7.3.4", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g=="],
"@turf/bbox/@turf/meta": ["@turf/meta@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -2790,8 +2768,6 @@
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"geojson-rbush/@types/geojson": ["@types/geojson@7946.0.8", "", {}, "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="],
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],

101
docs/PODS.back Normal file
View File

@@ -0,0 +1,101 @@
NOTE:
Untuk Development Selanjutnya:
Sekarang Anda bisa menjalankan:
1 # Untuk run iOS dev client
2 bun run ios
3
4 # Atau dengan Expo
5 bunx expo run:ios
Jika di masa depan terjadi error serupa, Anda bisa gunakan command ini:
1 cd ios
2 rm -rf Pods Podfile.lock
3 pod install
use_modular_headers!
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
prepare_react_native_project!
target 'HIPMIBadungConnect' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'npx',
'expo-modules-autolinking',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/..",
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
pod 'Firebase'
pod 'Firebase/Messaging'
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
post_install do |installer|
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
$MLRN.post_install(installer)
# @generated end @maplibre/maplibre-react-native:post-install
# Fix all script phases with incorrect paths
installer.pods_project.targets.each do |target|
target.build_phases.each do |phase|
next unless phase.respond_to?(:shell_script)
# Fix duplicated path issue
if phase.shell_script.include?('with-environment.sh')
# Remove any existing path and use proper relative path
phase.shell_script = phase.shell_script.gsub(
%r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},
'${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'
)
end
end
end
# Standard React Native post install
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
)
end
# @generated end post_installer
end

View File

@@ -55,10 +55,10 @@ Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
<!-- 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 source: app/(application)/admin/forum/[id]/list-comment.tsx
Folder tujuan: screens/Admin/Forum
Nama file utama: ScreenForumListComment.tsx
Nama function utama: Admin_ScreenForumListComment
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
@@ -66,8 +66,8 @@ Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada fil
<!-- Penerapan Pagination -->
Function fecth: apiAdminDonationDisbursementOfFundsListById
File function fetch: service/api-admin/api-admin-donation.ts
Function fecth: apiAdminForumCommentById
File function fetch: service/api-admin/api-admin-forum.ts
Terapkan pagination pada file "Nama file utama"
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
@@ -80,10 +80,13 @@ Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- END Prompt Admin Refactoring -->
<!-- Additional -->
File refrensi: screens/Admin/Voting/ScreenEventTypeOfEvent.tsx
File refrensi: screens/Admin/Forum/ScreenForumDetailReportPosting.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
Untuk refrensi tampilan Box bisa anda gunakan dari file: screens/Admin/Donation/BoxDonationCategory.tsx dan buatkan komponen yang mirip untuk list of donatur dengan nama file: BoxDonationListOfDonatur.tsx
Untuk refrensi tampilan Box bisa anda gunakan dari file: screens/Admin/Donation/BoxDonationListOfDonatur.tsx dan buatkan komponen yang mirip untuk list of donatur dengan nama file: BoxDonationListOfInvestor.tsx
<!-- Use Prompt Now -->
Terapkan NewWrapper pada file: screens/Admin/App-Information/InformationBankSection.tsx
@@ -108,9 +111,10 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1" ( stri
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,
File Utama: screens/Admin/Investment/ScreenInvestmentStatus.tsx
Folder tujuan: screens/Admin/Investment
Reffrensi: screens/Admin/Donation/BoxDonationStatus.tsx
Buatkan box component baru pada file "File Utama" di bagian renderItem agar lebih rapi buat file baru dengan nama BoxInvestmentStatus.tsx
<!-- END Create Box -->

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -35,13 +35,6 @@ target 'HIPMIBadungConnect' do
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
# @generated begin pre_installer - expo prebuild (DO NOT MODIFY) sync-c8812095000d6054b846ce74840f0ffb540c2757
pre_install do |installer|
# @generated begin @rnmapbox/maps-pre_installer - expo prebuild (DO NOT MODIFY) sync-ea4905840bf9fcea0acc62e92aa2e784f9d760f8
$RNMapboxMaps.pre_install(installer)
# @generated end @rnmapbox/maps-pre_installer
end
# @generated end pre_installer
use_react_native!(
:path => config[:reactNativePath],
@@ -56,9 +49,9 @@ target 'HIPMIBadungConnect' do
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
post_install do |installer|
# @generated begin @rnmapbox/maps-post_installer - expo prebuild (DO NOT MODIFY) sync-c4e8f90e96f6b6c6ea9241dd7b52ab5f57f7bf36
$RNMapboxMaps.post_install(installer)
# @generated end @rnmapbox/maps-post_installer
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
$MLRN.post_install(installer)
# @generated end @maplibre/maplibre-react-native:post-install
# Fix all script phases with incorrect paths
installer.pods_project.targets.each do |target|

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.2",
"@maplibre/maplibre-react-native": "^10.4.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-firebase/app": "^23.7.0",
@@ -21,7 +22,6 @@
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@rnmapbox/maps": "^10.2.7",
"@types/lodash": "^4.17.20",
"@types/react-native-vector-icons": "^6.4.18",
"axios": "^1.11.0",

View File

@@ -38,7 +38,7 @@ export default function Admin_BoxDonationStatus({
}
/>
<GridSpan_4_8
label={<TextCustom>Target</TextCustom>}
label={<TextCustom>Target Dana</TextCustom>}
value={
<TextCustom>
{item?.target

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import Icon from "react-native-vector-icons/FontAwesome";
import { stylesHome } from "./homeViewStyle";
import { router, useFocusEffect } from "expo-router";
import { apiJobGetAll } from "@/service/api-client/api-job";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
export default function Home_BottomFeatureSection() {
const [listData, setListData] = useState<any>([]);
@@ -35,6 +36,10 @@ export default function Home_BottomFeatureSection() {
}, [])
);
if (!listData || listData.length === 0) {
return <CustomSkeleton height={200}/>
}
return (
<>
<ClickableCustom onPress={() => router.push("/job")}>

View File

@@ -33,8 +33,10 @@ export const tabsHome: any = ({
activeIcon: "map",
label: "Maps",
path: "/maps",
isActive: Platform.OS === "ios" ? true : false,
disabled: Platform.OS === "ios" ? false : true,
// isActive: Platform.OS === "ios" ? true : false,
// disabled: Platform.OS === "ios" ? false : true,
isActive: true,
disabled: false,
},
{
id: "profile",

125
screens/Maps/DrawerMaps.tsx Normal file
View File

@@ -0,0 +1,125 @@
import {
DrawerCustom,
DummyLandscapeImage,
Spacing,
StackCustom,
TextCustom,
Grid,
ButtonCustom,
} from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
interface TypeDrawerMaps {
openDrawer: boolean;
setOpenDrawer: (value: boolean) => void;
selected: {
id: string;
bidangBisnis: string;
nomorTelepon: string;
alamatBisnis: string;
namePin: string;
imageId: string;
portofolioId: string;
latitude: number;
longitude: number;
};
}
export default function DrawerMaps({
openDrawer,
setOpenDrawer,
selected,
}: TypeDrawerMaps) {
return (
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<DummyLandscapeImage height={200} imageId={selected.imageId} />
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView
spanLeft={2}
spanRight={10}
leftItem={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftItem={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftItem={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightItem={<TextCustom>{selected.nomorTelepon}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftItem={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
/>
<Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom
onPress={() => {
setOpenDrawer(false);
router.push(`/portofolio/${selected.portofolioId}`);
}}
>
Detail
</ButtonCustom>
</Grid.Col>
<Grid.Col span={6} style={{ paddingLeft: 10 }}>
<ButtonCustom
onPress={() => {
openInDeviceMaps({
latitude: selected.latitude,
longitude: selected.longitude,
title: selected.namePin,
});
}}
>
Buka Maps
</ButtonCustom>
</Grid.Col>
</Grid>
</StackCustom>
</DrawerCustom>
);
}

View File

@@ -1,28 +1,166 @@
import { TextCustom, ViewWrapper } from "@/components";
import Mapbox from "@rnmapbox/maps";
import { View } from "react-native";
import { useCallback, useState } from "react";
import { Image, StyleSheet, View } from "react-native";
// Nonaktifkan telemetry (opsional, untuk privasi)
Mapbox.setTelemetryEnabled(false);
// Cek versi >= 10.x gunakan ini
import API_IMAGE from "@/constants/api-storage";
import { MainColor } from "@/constants/color-palet";
import { apiMapsGetAll } from "@/service/api-client/api-maps";
import {
Camera,
MapView,
PointAnnotation,
} from "@maplibre/maplibre-react-native";
import { useFocusEffect } from "expo-router";
import DrawerMaps from "./DrawerMaps";
// Gunakan style OSM gratis
const MAP_STYLE_URL = "https://tiles.stadiamaps.com/styles/osm_bright.json";
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
interface TypeMaps {
id: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
namePin: string;
latitude: number;
longitude: number;
authorId: string;
portofolioId: string;
imageId: string;
pinId: string | null;
Portofolio: {
id: string;
namaBisnis: string;
logoId: string;
alamatKantor: string;
tlpn: string;
MasterBidangBisnis: {
id: string;
name: string;
};
};
}
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
height: 300,
};
// Atau gunakan MapLibre default:
// const MAP_STYLE_URL = 'https://demotiles.maplibre.org/style.json';
export default function MapsView2() {
const [list, setList] = useState<TypeMaps[] | null>(null);
const [loadList, setLoadList] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const [selected, setSelected] = useState({
id: "",
bidangBisnis: "",
nomorTelepon: "",
alamatBisnis: "",
namePin: "",
imageId: "",
portofolioId: "",
latitude: 0,
longitude: 0,
});
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, []),
);
const handlerLoadList = async () => {
try {
setLoadList(true);
const response = await apiMapsGetAll();
if (response.success) {
// console.log("[RESPONSE]", JSON.stringify(response.data, null, 2));
setList(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
return (
<>
<ViewWrapper>
<View style={{ flex: 1 }}>
<Mapbox.MapView style={{ flex: 1 }}>
<Mapbox.Camera
centerCoordinate={[115.2126, -8.65]} // Bali
<View style={styles.container}>
<MapView style={styles.map} mapStyle={MAP_STYLE}>
<Camera
zoomLevel={12}
centerCoordinate={[defaultRegion.longitude, defaultRegion.latitude]}
/>
</Mapbox.MapView>
{list?.map((item: TypeMaps) => {
const imageUrl = API_IMAGE.GET({ fileId: item.Portofolio.logoId });
return (
<PointAnnotation
key={item.id}
id={item.id}
coordinate={[item.longitude, item.latitude] as [number, number]}
onSelected={() => {
setOpenDrawer(true);
setSelected({
id: item?.id,
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
nomorTelepon: item?.Portofolio?.tlpn,
alamatBisnis: item?.Portofolio?.alamatKantor,
namePin: item?.namePin,
imageId: item?.imageId,
portofolioId: item?.Portofolio?.id,
latitude: item?.latitude,
longitude: item?.longitude,
});
}}
>
<View style={styles.markerContainer}>
<Image
source={{ uri: imageUrl }}
style={styles.markerImage}
resizeMode="cover"
onError={(e: any) =>
console.log("Image error:", e.nativeEvent.error)
} // Tangkap error image
/>
</View>
</PointAnnotation>
);
})}
</MapView>
</View>
</ViewWrapper>
<DrawerMaps
openDrawer={openDrawer}
setOpenDrawer={setOpenDrawer}
selected={selected}
/>
</>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
markerContainer: {
width: 30,
height: 30,
borderRadius: 100,
overflow: "hidden", // Wajib agar borderRadius terapply pada Image
borderWidth: 1,
borderColor: MainColor.darkblue, // Opsional, biar lebih cantik
elevation: 4, // Shadow Android
shadowColor: "#000", // Shadow iOS
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
},
markerImage: {
width: "100%",
height: "100%",
},
});

View File

@@ -3,13 +3,15 @@ import { apiConfig } from "../api-config";
export async function apiAdminForum({
category,
search,
page = "1",
}: {
category: "dashboard" | "posting" | "report_posting" | "report_comment";
search?: string;
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/forum?category=${category}&search=${search}`
`/mobile/admin/forum?category=${category}&search=${search}&page=${page}`
);
return response.data;
} catch (error) {
@@ -28,13 +30,15 @@ export async function apiAdminForumPostingById({ id }: { id: string }) {
export async function apiAdminForumCommentById({
id,
category,
page = "1",
}: {
id: string;
category: "get-all" | "get-one";
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/forum/${id}/comment?category=${category}`
`/mobile/admin/forum/${id}/comment?category=${category}&page=${page}`
);
return response.data;
} catch (error) {
@@ -44,12 +48,14 @@ export async function apiAdminForumCommentById({
export async function apiAdminForumListReportCommentById({
id,
page = "1",
}: {
id: string;
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/forum/${id}/report-comment`
`/mobile/admin/forum/${id}/report-comment?page=${page}`
);
return response.data;
} catch (error) {
@@ -78,12 +84,14 @@ export async function apiAdminForumDeactivateComment({
export async function apiAdminForumListReportPostingById({
id,
page = "1",
}: {
id: string;
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/admin/forum/${id}/report-posting`
`/mobile/admin/forum/${id}/report-posting?page=${page}`
);
return response.data;
} catch (error) {

View File

@@ -4,14 +4,16 @@ import { apiConfig } from "../api-config";
export async function apiAdminInvestment({
category,
search,
page = "1",
}: {
category: "dashboard" | "publish" | "review" | "reject";
search?: string;
page?: string;
}) {
const propsQuery =
category === "dashboard"
? `category=${category}`
: `category=${category}&search=${search}`;
? `category=${category}&page=${page}`
: `category=${category}&search=${search}&page=${page}`;
try {
const response = await apiConfig.get(
@@ -57,15 +59,23 @@ export async function apiAdminInvestasiUpdateByStatus({
export async function apiAdminInvestmentListOfInvestor({
id,
status,
page = "1",
}: {
id: string;
status: "berhasil" | "gagal" | "proses" | "menunggu" | null;
page?: string;
}) {
const query = status && status !== null ? `?status=${status}` : "";
const queryParams = new URLSearchParams();
if (status && status !== null) {
queryParams.append("status", status);
}
queryParams.append("page", page);
try {
const response = await apiConfig.get(
`/mobile/admin/investment/${id}/investor${query}`
`/mobile/admin/investment/${id}/investor?${queryParams.toString()}`
);
return response.data;
} catch (error) {