Compare commits
54 Commits
amalia/23-
...
join
| Author | SHA1 | Date | |
|---|---|---|---|
| e2ffef1085 | |||
| cb2a57ee8e | |||
| f3b677f847 | |||
| 6ffe599ad0 | |||
| 4a92def490 | |||
| 0bad792ce8 | |||
| 6c9623954c | |||
| f39d5a4c85 | |||
| 6021d17b5a | |||
| 2d86a77a48 | |||
| b7165c5990 | |||
| 7dc51bd2b9 | |||
| de5ad545a7 | |||
| 47cb146c5a | |||
| 8b8ea61a13 | |||
| 5dac451754 | |||
| ccf8ee1caf | |||
| 887e787a99 | |||
| 772551a917 | |||
| 555b9e4037 | |||
| d4b4db4251 | |||
| 17d92cba25 | |||
| e1b62be6da | |||
| b2b125c410 | |||
| 1cfecbbdd5 | |||
| 21006e8eee | |||
| 91231d60e4 | |||
| 7174e27be1 | |||
| 9d4b931aa6 | |||
| 166d8f1c16 | |||
| 7060a2d165 | |||
| d6217aecf1 | |||
| 608381673f | |||
| 3cc7f76346 | |||
| 868b712fbb | |||
| a53b99b39d | |||
| 25d521f013 | |||
| aee0823cb1 | |||
| 2a0e1f4c1f | |||
| ef08c821fa | |||
| fd5d582092 | |||
| 7729dc38f8 | |||
| 8c6ff06216 | |||
| 214a243e44 | |||
| 449f6f96cc | |||
| e351f54f6c | |||
| d58a35bde2 | |||
| e2a601c590 | |||
| f0373ef479 | |||
| 700192dd8d | |||
| 27b0b7d51f | |||
| 65278df750 | |||
| 8b98fee632 | |||
| e254cf8ed2 |
33
CLAUDE.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Desa+** is a React Native (Expo) mobile app for village administration — managing announcements, projects, discussions, members, divisions, and documents. Primary platforms are Android and iOS.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start # Start Expo dev server
|
||||||
|
npm run android # Run on Android
|
||||||
|
npm run ios # Run on iOS
|
||||||
|
npm run lint # Expo lint
|
||||||
|
npm run test # Jest tests
|
||||||
|
npm run build:android # Production Android build via EAS (bumps version first)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a single test file:
|
||||||
|
```bash
|
||||||
|
bunx jest path/to/test.tsx --no-coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
> Project uses **Bun** as the package manager (`bun.lock` present). Use `bun add` / `bunx` instead of `npm install` / `npx`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
See @docs/ARCHITECTURE.md
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
See @docs/CONVENTIONS.md
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Desa+
|
# Desa+
|
||||||
|
|
||||||
Desa+ adalah aplikasi mobile berbasis React Native yang dikembangkan dengan Expo untuk membantu pengelolaan dan komunikasi di lingkungan desa/kelurahan. Aplikasi ini menyediakan berbagai fitur untuk memudahkan administrasi desa, komunikasi antar warga, dan pengelolaan informasi penting.
|
Desa+ (Desa Plus) adalah aplikasi mobile berbasis React Native yang dikembangkan dengan Expo untuk membantu pengelolaan dan komunikasi di lingkungan desa/kelurahan. Aplikasi ini menyediakan berbagai fitur untuk memudahkan administrasi desa, komunikasi antar warga, dan pengelolaan informasi penting.
|
||||||
|
|
||||||
## Fitur Utama
|
## Fitur Utama
|
||||||
|
|
||||||
|
|||||||
77
__tests__/ErrorBoundary-test.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, fireEvent } from '@testing-library/react-native';
|
||||||
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||||
|
|
||||||
|
// Komponen yang sengaja throw error saat render
|
||||||
|
const BrokenComponent = () => {
|
||||||
|
throw new Error('Test error boundary!');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Komponen normal
|
||||||
|
const NormalComponent = () => <></>;
|
||||||
|
|
||||||
|
// Suppress React's error boundary console output selama test
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
(console.error as jest.Mock).mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ErrorBoundary', () => {
|
||||||
|
it('merender children dengan normal jika tidak ada error', () => {
|
||||||
|
// Tidak boleh throw dan tidak menampilkan teks error
|
||||||
|
const { queryByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<NormalComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
expect(queryByText('Terjadi Kesalahan')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('menampilkan UI fallback ketika child throw error', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
expect(getByText('Terjadi Kesalahan')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('menampilkan pesan error yang dilempar', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
expect(getByText('Test error boundary!')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merender custom fallback jika prop fallback diberikan', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary fallback={<></>}>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
// Custom fallback fragment kosong — pastikan teks default tidak muncul
|
||||||
|
expect(() => getByText('Terjadi Kesalahan')).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mereset error state saat tombol Coba Lagi ditekan', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrokenComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = getByText('Coba Lagi');
|
||||||
|
expect(button).toBeTruthy();
|
||||||
|
|
||||||
|
// Tekan tombol reset — hasError kembali false, BrokenComponent throw lagi
|
||||||
|
// sehingga fallback muncul kembali (membuktikan reset berjalan)
|
||||||
|
fireEvent.press(button);
|
||||||
|
expect(getByText('Terjadi Kesalahan')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -92,8 +92,8 @@ android {
|
|||||||
applicationId 'mobiledarmasaba.app'
|
applicationId 'mobiledarmasaba.app'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 6
|
versionCode 16
|
||||||
versionName "1.0.2"
|
versionName "2.1.0"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 904 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,9 +1,9 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||||
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
|
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="android:statusBarColor">#ffffff</item>
|
<item name="android:statusBarColor">#ffffff</item>
|
||||||
|
<item name="android:windowOptOutEdgeToEdgeEnforcement" tools:targetApi="35">true</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default {
|
|||||||
expo: {
|
expo: {
|
||||||
name: "Desa+",
|
name: "Desa+",
|
||||||
slug: "mobile-darmasaba",
|
slug: "mobile-darmasaba",
|
||||||
version: "2.0.5", // Versi aplikasi (App Store)
|
version: "2.1.0", // Versi aplikasi (App Store)
|
||||||
jsEngine: "jsc",
|
jsEngine: "jsc",
|
||||||
orientation: "portrait",
|
orientation: "portrait",
|
||||||
icon: "./assets/images/logo-icon-small.png",
|
icon: "./assets/images/logo-icon-small.png",
|
||||||
@@ -14,7 +14,7 @@ export default {
|
|||||||
ios: {
|
ios: {
|
||||||
supportsTablet: true,
|
supportsTablet: true,
|
||||||
bundleIdentifier: "mobiledarmasaba.app",
|
bundleIdentifier: "mobiledarmasaba.app",
|
||||||
buildNumber: "7",
|
buildNumber: "9",
|
||||||
infoPlist: {
|
infoPlist: {
|
||||||
ITSAppUsesNonExemptEncryption: false,
|
ITSAppUsesNonExemptEncryption: false,
|
||||||
CFBundleDisplayName: "Desa+"
|
CFBundleDisplayName: "Desa+"
|
||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
package: "mobiledarmasaba.app",
|
package: "mobiledarmasaba.app",
|
||||||
versionCode: 15,
|
versionCode: 16,
|
||||||
adaptiveIcon: {
|
adaptiveIcon: {
|
||||||
foregroundImage: "./assets/images/logo-icon-small.png",
|
foregroundImage: "./assets/images/logo-icon-small.png",
|
||||||
backgroundColor: "#ffffff"
|
backgroundColor: "#ffffff"
|
||||||
@@ -85,6 +85,8 @@ export default {
|
|||||||
FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
|
FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
|
||||||
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
||||||
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
|
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
|
||||||
|
URL_MONITORING: process.env.URL_MONITORING,
|
||||||
|
KEY_API_MONITORING: process.env.KEY_API_MONITORING,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ import HeaderRightPositionList from "@/components/position/headerRightPositionLi
|
|||||||
import HeaderRightProjectList from "@/components/project/headerProjectList";
|
import HeaderRightProjectList from "@/components/project/headerProjectList";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import ToastCustom from "@/components/toastCustom";
|
import ToastCustom from "@/components/toastCustom";
|
||||||
import { apiReadOneNotification } from "@/lib/api";
|
import ModalUpdateMaintenance from "@/components/ModalUpdateMaintenance";
|
||||||
|
import { apiGetVersion, apiReadOneNotification } from "@/lib/api";
|
||||||
import { pushToPage } from "@/lib/pushToPage";
|
import { pushToPage } from "@/lib/pushToPage";
|
||||||
import store from "@/lib/store";
|
import store from "@/lib/store";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import Constants from "expo-constants";
|
||||||
import { getApp } from "@react-native-firebase/app";
|
import { getApp } from "@react-native-firebase/app";
|
||||||
import { getMessaging, onMessage } from "@react-native-firebase/messaging";
|
import { getMessaging, onMessage } from "@react-native-firebase/messaging";
|
||||||
import { Redirect, router, Stack, usePathname } from "expo-router";
|
import { Redirect, router, Stack, usePathname } from "expo-router";
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Easing, Notifier, NotifierComponents } from 'react-native-notifier';
|
import { Easing, Notifier, NotifierComponents } from 'react-native-notifier';
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
@@ -27,12 +29,86 @@ export default function RootLayout() {
|
|||||||
const { token, decryptToken, isLoading } = useAuthSession()
|
const { token, decryptToken, isLoading } = useAuthSession()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
|
const [modalUpdateMaintenance, setModalUpdateMaintenance] = useState(false)
|
||||||
|
const [modalType, setModalType] = useState<'update' | 'maintenance'>('update')
|
||||||
|
const [isForceUpdate, setIsForceUpdate] = useState(false)
|
||||||
|
const [updateMessage, setUpdateMessage] = useState('')
|
||||||
|
|
||||||
|
const currentVersion = Constants.expoConfig?.version ?? '0.0.0'
|
||||||
|
|
||||||
|
const compareVersions = (v1: string, v2: string) => {
|
||||||
|
const parts1 = v1.split('.').map(Number);
|
||||||
|
const parts2 = v2.split('.').map(Number);
|
||||||
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||||
|
const p1 = parts1[i] || 0;
|
||||||
|
const p2 = parts2[i] || 0;
|
||||||
|
if (p1 < p2) return -1;
|
||||||
|
if (p1 > p2) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkVersion = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiGetVersion();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const maintenance = response.data.find((item: any) => item.id === 'mobile_maintenance')?.value === 'true';
|
||||||
|
const latestVersion = response.data.find((item: any) => item.id === 'mobile_latest_version')?.value || '0.0.0';
|
||||||
|
const minVersion = response.data.find((item: any) => item.id === 'mobile_minimum_version')?.value || '0.0.0';
|
||||||
|
const message = response.data.find((item: any) => item.id === 'mobile_message_update')?.value || '';
|
||||||
|
|
||||||
|
if (maintenance) {
|
||||||
|
setModalType('maintenance');
|
||||||
|
setModalUpdateMaintenance(true);
|
||||||
|
setIsForceUpdate(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compareVersions(currentVersion, minVersion) === -1) {
|
||||||
|
setModalType('update');
|
||||||
|
setIsForceUpdate(true);
|
||||||
|
setUpdateMessage(message);
|
||||||
|
setModalUpdateMaintenance(true);
|
||||||
|
} else if (compareVersions(currentVersion, latestVersion) === -1) {
|
||||||
|
// Check if this soft update version was already dismissed
|
||||||
|
const dismissedVersion = await AsyncStorage.getItem('dismissed_update_version');
|
||||||
|
if (dismissedVersion !== latestVersion) {
|
||||||
|
setModalType('update');
|
||||||
|
setIsForceUpdate(false);
|
||||||
|
setUpdateMessage(message);
|
||||||
|
setModalUpdateMaintenance(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check version:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkVersion();
|
||||||
|
}, [currentVersion]);
|
||||||
|
|
||||||
|
const handleDismissUpdate = async () => {
|
||||||
|
if (!isForceUpdate) {
|
||||||
|
try {
|
||||||
|
const response = await apiGetVersion();
|
||||||
|
const latestVersion = response.data.find((item: any) => item.id === 'mobile_latest_version')?.value;
|
||||||
|
if (latestVersion) {
|
||||||
|
await AsyncStorage.setItem('dismissed_update_version', latestVersion);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
setModalUpdateMaintenance(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleReadNotification(id: string, category: string, idContent: string, title: string) {
|
async function handleReadNotification(id: string, category: string, idContent: string, title: string) {
|
||||||
try {
|
try {
|
||||||
if (title != "Komentar Baru") {
|
if (title !== "Komentar Baru") {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiReadOneNotification({ user: hasil, id: id })
|
await apiReadOneNotification({ user: hasil, id: id })
|
||||||
}
|
}
|
||||||
pushToPage(category, idContent)
|
pushToPage(category, idContent)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -127,8 +203,6 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="feature" options={{ title: 'Fitur' }} />
|
<Stack.Screen name="feature" options={{ title: 'Fitur' }} />
|
||||||
<Stack.Screen name="search" options={{ title: 'Pencarian' }} />
|
<Stack.Screen name="search" options={{ title: 'Pencarian' }} />
|
||||||
<Stack.Screen name="notification" options={{
|
<Stack.Screen name="notification" options={{
|
||||||
title: 'Notifikasi',
|
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Notifikasi',
|
headerTitle: 'Notifikasi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
header: () => (
|
header: () => (
|
||||||
@@ -137,10 +211,8 @@ export default function RootLayout() {
|
|||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="profile" options={{ title: 'Profile' }} />
|
<Stack.Screen name="profile" options={{ title: 'Profile' }} />
|
||||||
<Stack.Screen name="setting/index" options={{
|
<Stack.Screen name="setting/index" options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Pengaturan',
|
title: 'Pengaturan',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderRightProjectList />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader title="Pengaturan"
|
<AppHeader title="Pengaturan"
|
||||||
showBack={true}
|
showBack={true}
|
||||||
@@ -149,10 +221,8 @@ export default function RootLayout() {
|
|||||||
)
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="member/index" options={{
|
<Stack.Screen name="member/index" options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Anggota',
|
title: 'Anggota',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderMemberList />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader title="Anggota"
|
<AppHeader title="Anggota"
|
||||||
showBack={true}
|
showBack={true}
|
||||||
@@ -162,10 +232,8 @@ export default function RootLayout() {
|
|||||||
)
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="discussion/index" options={{
|
<Stack.Screen name="discussion/index" options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Diskusi Umum',
|
title: 'Diskusi Umum',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderDiscussionGeneral />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader
|
<AppHeader
|
||||||
title="Diskusi Umum"
|
title="Diskusi Umum"
|
||||||
@@ -176,10 +244,8 @@ export default function RootLayout() {
|
|||||||
)
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="project/index" options={{
|
<Stack.Screen name="project/index" options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Kegiatan',
|
title: 'Kegiatan',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderRightProjectList />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader title="Kegiatan"
|
<AppHeader title="Kegiatan"
|
||||||
showBack={true}
|
showBack={true}
|
||||||
@@ -189,10 +255,8 @@ export default function RootLayout() {
|
|||||||
)
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="division/index" options={{
|
<Stack.Screen name="division/index" options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
title: 'Divisi',
|
title: 'Divisi',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderRightDivisionList />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader title="Divisi"
|
<AppHeader title="Divisi"
|
||||||
showBack={true}
|
showBack={true}
|
||||||
@@ -203,10 +267,8 @@ export default function RootLayout() {
|
|||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="division/[id]/(fitur-division)" options={{ headerShown: false }} />
|
<Stack.Screen name="division/[id]/(fitur-division)" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="group/index" options={{
|
<Stack.Screen name="group/index" options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Lembaga Desa',
|
headerTitle: 'Lembaga Desa',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderRightGroupList />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader title="Lembaga Desa"
|
<AppHeader title="Lembaga Desa"
|
||||||
showBack={true}
|
showBack={true}
|
||||||
@@ -216,10 +278,8 @@ export default function RootLayout() {
|
|||||||
)
|
)
|
||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="position/index" options={{
|
<Stack.Screen name="position/index" options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Jabatan',
|
headerTitle: 'Jabatan',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderRightPositionList />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader title="Jabatan"
|
<AppHeader title="Jabatan"
|
||||||
showBack={true}
|
showBack={true}
|
||||||
@@ -230,10 +290,8 @@ export default function RootLayout() {
|
|||||||
}} />
|
}} />
|
||||||
<Stack.Screen name="announcement/index"
|
<Stack.Screen name="announcement/index"
|
||||||
options={{
|
options={{
|
||||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
|
||||||
headerTitle: 'Pengumuman',
|
headerTitle: 'Pengumuman',
|
||||||
headerTitleAlign: 'center',
|
headerTitleAlign: 'center',
|
||||||
// headerRight: () => <HeaderRightAnnouncementList />
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader title="Pengumuman"
|
<AppHeader title="Pengumuman"
|
||||||
showBack={true}
|
showBack={true}
|
||||||
@@ -246,6 +304,13 @@ export default function RootLayout() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style={'light'} translucent={false} backgroundColor="black" />
|
<StatusBar style={'light'} translucent={false} backgroundColor="black" />
|
||||||
<ToastCustom />
|
<ToastCustom />
|
||||||
|
<ModalUpdateMaintenance
|
||||||
|
visible={modalUpdateMaintenance}
|
||||||
|
type={modalType}
|
||||||
|
isForceUpdate={isForceUpdate}
|
||||||
|
customDescription={updateMessage}
|
||||||
|
onDismiss={handleDismissUpdate}
|
||||||
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import AppHeader from "@/components/AppHeader";
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
import BorderBottomItem from "@/components/borderBottomItem";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
|
import ErrorView from "@/components/ErrorView";
|
||||||
import { ConstEnv } from "@/constants/ConstEnv";
|
import { ConstEnv } from "@/constants/ConstEnv";
|
||||||
import { isImageFile } from "@/constants/FileExtensions";
|
import { isImageFile } from "@/constants/FileExtensions";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
@@ -65,6 +66,7 @@ export default function DetailAnnouncement() {
|
|||||||
const [loadingOpen, setLoadingOpen] = useState(false)
|
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||||
const [preview, setPreview] = useState(false)
|
const [preview, setPreview] = useState(false)
|
||||||
const [chooseFile, setChooseFile] = useState<FileData>()
|
const [chooseFile, setChooseFile] = useState<FileData>()
|
||||||
|
const [isError, setIsError] = useState(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the image preview modal for the selected image file
|
* Opens the image preview modal for the selected image file
|
||||||
@@ -79,6 +81,7 @@ export default function DetailAnnouncement() {
|
|||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
|
setIsError(false)
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
|
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
|
||||||
@@ -87,11 +90,15 @@ export default function DetailAnnouncement() {
|
|||||||
setDataMember(response.member)
|
setDataMember(response.member)
|
||||||
setDataFile(response.file)
|
setDataFile(response.file)
|
||||||
} else {
|
} else {
|
||||||
|
setIsError(true)
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
|
setIsError(true)
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -204,104 +211,110 @@ export default function DetailAnnouncement() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<View style={[Styles.p15, Styles.mb50]}>
|
{isError && !loading ? (
|
||||||
<View style={[Styles.wrapPaper, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
<View style={[Styles.mv50]}>
|
||||||
{
|
<ErrorView />
|
||||||
loading ?
|
|
||||||
<View>
|
|
||||||
<View style={[Styles.rowOnly]}>
|
|
||||||
<Skeleton width={30} height={30} borderRadius={10} />
|
|
||||||
<View style={[Styles.flex1, Styles.ph05]}>
|
|
||||||
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
</View>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<View style={[Styles.rowOnly, Styles.alignStart]}>
|
|
||||||
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
|
|
||||||
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[Styles.mt10]}>
|
|
||||||
{
|
|
||||||
hasHtmlTags(data?.desc) ?
|
|
||||||
<RenderHTML
|
|
||||||
contentWidth={contentWidth}
|
|
||||||
source={{ html: data?.desc }}
|
|
||||||
baseStyle={{ color: colors.text }}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<Text>{data?.desc}</Text>
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
{
|
) : (
|
||||||
dataFile.length > 0 && (
|
<View style={[Styles.p15, Styles.mb50]}>
|
||||||
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
<View style={[Styles.wrapPaper, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
<View style={[Styles.mb05]}>
|
{
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
loading ?
|
||||||
</View>
|
<View>
|
||||||
{dataFile.map((item, index) => (
|
<View style={[Styles.rowOnly]}>
|
||||||
<BorderBottomItem
|
<Skeleton width={30} height={30} borderRadius={10} />
|
||||||
key={`${item.id}-${index}`}
|
<View style={[Styles.flex1, Styles.ph05]}>
|
||||||
borderType="bottom"
|
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
|
||||||
icon={<MaterialCommunityIcons
|
</View>
|
||||||
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
|
|
||||||
size={25}
|
|
||||||
color={colors.text}
|
|
||||||
/>}
|
|
||||||
title={item.name + '.' + item.extension}
|
|
||||||
titleWeight="normal"
|
|
||||||
onPress={() => {
|
|
||||||
isImageFile(item.extension) ?
|
|
||||||
handleChooseFile(item)
|
|
||||||
: openFile(item)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
|
||||||
{
|
|
||||||
loading ?
|
|
||||||
arrSkeleton.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<View key={index}>
|
|
||||||
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
|
||||||
</View>
|
</View>
|
||||||
)
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
})
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
:
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
Object.keys(dataMember).map((v: any, i: any) => {
|
</View>
|
||||||
return (
|
:
|
||||||
<View key={i} style={[Styles.mb05]}>
|
<>
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
|
<View style={[Styles.rowOnly, Styles.alignStart]}>
|
||||||
|
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[Styles.mt10]}>
|
||||||
{
|
{
|
||||||
dataMember[v].map((item: any, x: any) => {
|
hasHtmlTags(data?.desc) ?
|
||||||
return (
|
<RenderHTML
|
||||||
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}>
|
contentWidth={contentWidth}
|
||||||
<Entypo name="dot-single" size={24} color={colors.text} />
|
source={{ html: data?.desc }}
|
||||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text>
|
baseStyle={{ color: colors.text }}
|
||||||
</View>
|
/>
|
||||||
)
|
:
|
||||||
})
|
<Text>{data?.desc}</Text>
|
||||||
}
|
}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
)
|
</>
|
||||||
})
|
}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
{
|
||||||
|
dataFile.length > 0 && (
|
||||||
|
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
|
<View style={[Styles.mb05]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
||||||
|
</View>
|
||||||
|
{dataFile.map((item, index) => (
|
||||||
|
<BorderBottomItem
|
||||||
|
key={`${item.id}-${index}`}
|
||||||
|
borderType={index === dataFile.length - 1 ? 'none' : 'bottom'}
|
||||||
|
icon={<MaterialCommunityIcons
|
||||||
|
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
|
||||||
|
size={25}
|
||||||
|
color={colors.text}
|
||||||
|
/>}
|
||||||
|
title={item.name + '.' + item.extension}
|
||||||
|
titleWeight="normal"
|
||||||
|
onPress={() => {
|
||||||
|
isImageFile(item.extension) ?
|
||||||
|
handleChooseFile(item)
|
||||||
|
: openFile(item)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
|
{
|
||||||
|
loading ?
|
||||||
|
arrSkeleton.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<View key={index}>
|
||||||
|
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
|
||||||
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
:
|
||||||
|
Object.keys(dataMember).map((v: any, i: any) => {
|
||||||
|
return (
|
||||||
|
<View key={i} style={[Styles.mb05]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
|
||||||
|
{
|
||||||
|
dataMember[v].map((item: any, x: any) => {
|
||||||
|
return (
|
||||||
|
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}>
|
||||||
|
<Entypo name="dot-single" size={24} color={colors.text} />
|
||||||
|
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<ImageViewing
|
<ImageViewing
|
||||||
|
|||||||
@@ -100,11 +100,16 @@ export default function CreateAnnouncement() {
|
|||||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
||||||
router.back();
|
router.back();
|
||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
|
const message = error?.response?.data?.message || "Tidak dapat terhubung ke server"
|
||||||
|
|
||||||
|
Toast.show({
|
||||||
|
type: 'small',
|
||||||
|
text1: message
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,9 +147,11 @@ export default function EditAnnouncement() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengubah data', })
|
Toast.show({ type: 'small', text1: 'Gagal mengubah data', })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengubah data', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -309,7 +311,7 @@ export default function EditAnnouncement() {
|
|||||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>Divisi</Text>
|
<Text style={[Styles.textDefaultSemiBold]}>Divisi</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.borderAll, Styles.round05, Styles.p10, {backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
<View style={[Styles.borderAll, Styles.round05, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||||
{
|
{
|
||||||
dataMember.map((item: { name: any; Division: any }, index: any) => {
|
dataMember.map((item: { name: any; Division: any }, index: any) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import BorderBottomItem from "@/components/borderBottomItem";
|
|||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import SkeletonContent from "@/components/skeletonContent";
|
||||||
import Text from '@/components/Text';
|
import Text from '@/components/Text';
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetAnnouncement } from "@/lib/api";
|
import { apiGetAnnouncement } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { MaterialIcons } from "@expo/vector-icons";
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -20,68 +20,60 @@ type Props = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function Announcement() {
|
export default function Announcement() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const update = useSelector((state: any) => state.announcementUpdate)
|
const update = useSelector((state: any) => state.announcementUpdate)
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query Infinite Query
|
||||||
try {
|
const {
|
||||||
setWaiting(true)
|
data,
|
||||||
setLoading(loading)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch,
|
||||||
|
isRefetching
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['announcements', search],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetAnnouncement({ user: hasil, search: search, page: thisPage })
|
const response = await apiGetAnnouncement({
|
||||||
if (thisPage == 1) {
|
user: hasil,
|
||||||
setData(response.data)
|
search: search,
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
page: pageParam
|
||||||
setData([...data, ...response.data])
|
})
|
||||||
} else {
|
return response.data
|
||||||
return;
|
},
|
||||||
}
|
initialPageParam: 1,
|
||||||
} catch (error) {
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
console.error(error)
|
return lastPage.length > 0 ? allPages.length + 1 : undefined
|
||||||
} finally {
|
},
|
||||||
setLoading(false)
|
})
|
||||||
setWaiting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Trigger refetch when Redux state 'update' changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1)
|
refetch()
|
||||||
}, [update])
|
}, [update, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
// Flatten data from pages
|
||||||
handleLoad(true, 1)
|
const flattenedData = useMemo(() => {
|
||||||
}, [search])
|
return data?.pages.flat() || []
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const loadMoreData = () => {
|
const loadMoreData = () => {
|
||||||
if (waiting) return
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
setTimeout(() => {
|
fetchNextPage()
|
||||||
handleLoad(false, page + 1)
|
}
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
setRefreshing(true)
|
|
||||||
handleLoad(false, 1)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
id: data[index].id,
|
id: flattenedData[index].id,
|
||||||
title: data[index].title,
|
title: flattenedData[index].title,
|
||||||
desc: data[index].desc,
|
desc: flattenedData[index].desc,
|
||||||
createdAt: data[index].createdAt,
|
createdAt: flattenedData[index].createdAt,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,18 +83,18 @@ export default function Announcement() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[Styles.flex2, Styles.mt05]}>
|
<View style={[Styles.flex2, Styles.mt05]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading && !flattenedData.length ?
|
||||||
arrSkeleton.map((item, index) => {
|
arrSkeleton.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContent key={index} />
|
<SkeletonContent key={index} />
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
data.length > 0
|
flattenedData.length > 0
|
||||||
?
|
?
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flattenedData}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flattenedData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -112,9 +104,7 @@ export default function Announcement() {
|
|||||||
borderType="bottom"
|
borderType="bottom"
|
||||||
bgColor="transparent"
|
bgColor="transparent"
|
||||||
icon={
|
icon={
|
||||||
// <View style={[Styles.iconContent]}>
|
|
||||||
<MaterialIcons name="campaign" size={25} color={colors.text} />
|
<MaterialIcons name="campaign" size={25} color={colors.text} />
|
||||||
// </View>
|
|
||||||
}
|
}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
|
desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
|
||||||
@@ -122,14 +112,14 @@ export default function Announcement() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
keyExtractor={(item, index) => String(index)}
|
keyExtractor={(item, index) => String(item.id || index)}
|
||||||
onEndReached={loadMoreData}
|
onEndReached={loadMoreData}
|
||||||
onEndReachedThreshold={0.5}
|
onEndReachedThreshold={0.5}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={isRefetching && !isFetchingNextPage}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={refetch}
|
||||||
tintColor={colors.icon}
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,9 +106,11 @@ export default function EditBanner() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,9 +88,11 @@ export default function CreateBanner() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
|
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan data', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import styles from "@/components/AppHeader"
|
||||||
import AppHeader from "@/components/AppHeader"
|
import AppHeader from "@/components/AppHeader"
|
||||||
import HeaderRightBannerList from "@/components/banner/headerBannerList"
|
import HeaderRightBannerList from "@/components/banner/headerBannerList"
|
||||||
import BorderBottomItem from "@/components/borderBottomItem"
|
import BorderBottomItem from "@/components/borderBottomItem"
|
||||||
@@ -5,6 +6,7 @@ import DrawerBottom from "@/components/drawerBottom"
|
|||||||
import MenuItemRow from "@/components/menuItemRow"
|
import MenuItemRow from "@/components/menuItemRow"
|
||||||
import ModalConfirmation from "@/components/ModalConfirmation"
|
import ModalConfirmation from "@/components/ModalConfirmation"
|
||||||
import ModalLoading from "@/components/modalLoading"
|
import ModalLoading from "@/components/modalLoading"
|
||||||
|
import Skeleton from "@/components/skeleton"
|
||||||
import Text from "@/components/Text"
|
import Text from "@/components/Text"
|
||||||
import { ConstEnv } from "@/constants/ConstEnv"
|
import { ConstEnv } from "@/constants/ConstEnv"
|
||||||
import Styles from "@/constants/Styles"
|
import Styles from "@/constants/Styles"
|
||||||
@@ -13,11 +15,12 @@ import { setEntities } from "@/lib/bannerSlice"
|
|||||||
import { useAuthSession } from "@/providers/AuthProvider"
|
import { useAuthSession } from "@/providers/AuthProvider"
|
||||||
import { useTheme } from "@/providers/ThemeProvider"
|
import { useTheme } from "@/providers/ThemeProvider"
|
||||||
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
|
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import * as FileSystem from 'expo-file-system'
|
import * as FileSystem from 'expo-file-system'
|
||||||
import { startActivityAsync } from 'expo-intent-launcher'
|
import { startActivityAsync } from 'expo-intent-launcher'
|
||||||
import { router, Stack } from "expo-router"
|
import { router, Stack } from "expo-router"
|
||||||
import * as Sharing from 'expo-sharing'
|
import * as Sharing from 'expo-sharing'
|
||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Alert, Image, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
import { Alert, Image, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native"
|
||||||
import ImageViewing from 'react-native-image-viewing'
|
import ImageViewing from 'react-native-image-viewing'
|
||||||
import * as mime from 'react-native-mime-types'
|
import * as mime from 'react-native-mime-types'
|
||||||
@@ -43,34 +46,51 @@ export default function BannerList() {
|
|||||||
const [loadingOpen, setLoadingOpen] = useState(false)
|
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||||
const [viewImg, setViewImg] = useState(false)
|
const [viewImg, setViewImg] = useState(false)
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const handleDeleteEntity = async () => {
|
// 1. Fetching logic with useQuery
|
||||||
try {
|
const { data: bannersRes, isLoading } = useQuery({
|
||||||
const hasil = await decryptToken(String(token?.current));
|
queryKey: ['banners'],
|
||||||
const deletedEntity = await apiDeleteBanner({ user: hasil }, dataId);
|
queryFn: async () => {
|
||||||
if (deletedEntity.success) {
|
const hasil = await decryptToken(String(token?.current))
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil menghapus data', })
|
const response = await apiGetBanner({ user: hasil })
|
||||||
apiGetBanner({ user: hasil }).then((data) =>
|
return response.data || []
|
||||||
dispatch(setEntities(data.data))
|
},
|
||||||
);
|
enabled: !!token?.current,
|
||||||
} else {
|
staleTime: 0,
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menghapus data', })
|
})
|
||||||
}
|
|
||||||
} catch (error) {
|
// Sync results with Redux
|
||||||
console.error(error)
|
useEffect(() => {
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
if (bannersRes) {
|
||||||
} finally {
|
dispatch(setEntities(bannersRes))
|
||||||
setModal(false)
|
|
||||||
}
|
}
|
||||||
|
}, [bannersRes, dispatch])
|
||||||
|
|
||||||
|
// 2. Deletion logic with useMutation
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
return await apiDeleteBanner({ user: hasil }, id)
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
Toast.show({ type: 'small', text1: 'Berhasil menghapus data' })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['banners'] })
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDeleteEntity = () => {
|
||||||
|
deleteMutation.mutate(dataId)
|
||||||
|
setModal(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
await queryClient.invalidateQueries({ queryKey: ['banners'] })
|
||||||
apiGetBanner({ user: hasil }).then((data) =>
|
|
||||||
dispatch(setEntities(data.data))
|
|
||||||
);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,36 +158,40 @@ export default function BannerList() {
|
|||||||
}
|
}
|
||||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||||
>
|
>
|
||||||
{
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
entities.length > 0
|
{
|
||||||
?
|
isLoading ? (
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<>
|
||||||
{entities.map((index: any, key: number) => (
|
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
|
||||||
<BorderBottomItem
|
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
|
||||||
key={key}
|
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
|
||||||
onPress={() => {
|
</>
|
||||||
setDataId(index.id)
|
) :
|
||||||
setSelectFile(index)
|
entities.length > 0 ?
|
||||||
setModal(true)
|
entities.map((index: any, key: number) => (
|
||||||
}}
|
<BorderBottomItem
|
||||||
borderType="all"
|
key={key}
|
||||||
icon={
|
onPress={() => {
|
||||||
<Image
|
setDataId(index.id)
|
||||||
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
|
setSelectFile(index)
|
||||||
style={[Styles.imgListBanner]}
|
setModal(true)
|
||||||
/>
|
}}
|
||||||
}
|
borderType="all"
|
||||||
title={index.title}
|
icon={
|
||||||
/>
|
<Image
|
||||||
))}
|
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
|
||||||
</View>
|
style={[Styles.imgListBanner]}
|
||||||
:
|
/>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
}
|
||||||
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
|
title={index.title}
|
||||||
</View>
|
/>
|
||||||
}
|
))
|
||||||
|
:
|
||||||
|
<View style={[Styles.p15, Styles.mb100]}>
|
||||||
|
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">
|
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">
|
||||||
|
|||||||
@@ -158,8 +158,11 @@ export default function DetailDiscussionGeneral() {
|
|||||||
Toast.show({ type: 'small', text1: response.message })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSendKomentar(false)
|
setLoadingSendKomentar(false)
|
||||||
}
|
}
|
||||||
@@ -175,8 +178,11 @@ export default function DetailDiscussionGeneral() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSendKomentar(false)
|
setLoadingSendKomentar(false)
|
||||||
handleViewEditKomentar()
|
handleViewEditKomentar()
|
||||||
@@ -193,8 +199,11 @@ export default function DetailDiscussionGeneral() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSendKomentar(false)
|
setLoadingSendKomentar(false)
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
|
|||||||
@@ -84,9 +84,11 @@ export default function AddMemberDiscussionDetail() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal menambahkan anggota', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,9 +156,11 @@ export default function CreateDiscussionGeneral() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,9 +157,11 @@ export default function EditDiscussionGeneral() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengubah data', })
|
Toast.show({ type: 'small', text1: 'Gagal mengubah data', })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import { apiGetDiscussionGeneral } from "@/lib/api";
|
|||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign, Feather, Ionicons, MaterialIcons } from "@expo/vector-icons";
|
import { AntDesign, Feather, Ionicons, MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -32,70 +33,76 @@ export default function Discussion() {
|
|||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [nameGroup, setNameGroup] = useState('')
|
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
||||||
const [loading, setLoading] = useState(true)
|
const queryClient = useQueryClient()
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query for Discussions with Infinite Scroll
|
||||||
try {
|
const {
|
||||||
setWaiting(true)
|
data,
|
||||||
setLoading(loading)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['discussions', { status, search, group }],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetDiscussionGeneral({ user: hasil, active: status, search: search, group: String(group), page: thisPage })
|
const response = await apiGetDiscussionGeneral({
|
||||||
if (thisPage == 1) {
|
user: hasil,
|
||||||
setData(response.data)
|
active: status,
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
search: search,
|
||||||
setData([...data, ...response.data])
|
group: String(group),
|
||||||
} else {
|
page: pageParam
|
||||||
return;
|
})
|
||||||
}
|
return response;
|
||||||
setNameGroup(response.filter.name)
|
},
|
||||||
} catch (error) {
|
initialPageParam: 1,
|
||||||
console.error(error)
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
} finally {
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
setLoading(false)
|
},
|
||||||
setWaiting(false)
|
enabled: !!token?.current,
|
||||||
}
|
staleTime: 0,
|
||||||
}
|
})
|
||||||
|
|
||||||
|
// Flatten pages into a single data array
|
||||||
|
const flatData = useMemo(() => {
|
||||||
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
// Get nameGroup from the first available page
|
||||||
|
const nameGroup = useMemo(() => {
|
||||||
|
return data?.pages[0]?.filter?.name || "";
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
// Refetch when manual update state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1)
|
refetch()
|
||||||
}, [update])
|
}, [update, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true, 1)
|
|
||||||
}, [status, search, group])
|
|
||||||
|
|
||||||
|
|
||||||
const loadMoreData = () => {
|
|
||||||
if (waiting) return
|
|
||||||
setTimeout(() => {
|
|
||||||
handleLoad(false, page + 1)
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
await queryClient.invalidateQueries({ queryKey: ['discussions'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadMoreData = () => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
id: data[index].id,
|
id: flatData[index]?.id,
|
||||||
title: data[index].title,
|
title: flatData[index]?.title,
|
||||||
desc: data[index].desc,
|
desc: flatData[index]?.desc,
|
||||||
status: data[index].status,
|
status: flatData[index]?.status,
|
||||||
total_komentar: data[index].total_komentar,
|
total_komentar: flatData[index]?.total_komentar,
|
||||||
createdAt: data[index].createdAt,
|
createdAt: flatData[index]?.createdAt,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -132,18 +139,18 @@ export default function Discussion() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[Styles.flex2, Styles.mt05]}>
|
<View style={[Styles.flex2, Styles.mt05]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
arrSkeleton.map((item: any, i: number) => {
|
arrSkeleton.map((item: any, i: number) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContent key={i} />
|
<SkeletonContent key={i} />
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
data.length > 0
|
flatData.length > 0
|
||||||
?
|
?
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -153,16 +160,14 @@ export default function Discussion() {
|
|||||||
onPress={() => { router.push(`/discussion/${item.id}`) }}
|
onPress={() => { router.push(`/discussion/${item.id}`) }}
|
||||||
borderType="bottom"
|
borderType="bottom"
|
||||||
icon={
|
icon={
|
||||||
// <View style={[Styles.iconContent]}>
|
|
||||||
<MaterialIcons name="chat" size={25} color={colors.text} />
|
<MaterialIcons name="chat" size={25} color={colors.text} />
|
||||||
// </View>
|
|
||||||
}
|
}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
subtitle={
|
subtitle={
|
||||||
status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
|
status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
|
||||||
}
|
}
|
||||||
rightTopInfo={item.createdAt}
|
rightTopInfo={item.createdAt}
|
||||||
desc={item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
|
desc={item.desc?.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
|
||||||
leftBottomInfo={
|
leftBottomInfo={
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} />
|
<Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} />
|
||||||
|
|||||||
@@ -67,8 +67,11 @@ export default function MemberDiscussionDetail() {
|
|||||||
await apiDeleteMemberDiscussionGeneral({ user: hasil, idUser: chooseUser.idUser }, id)
|
await apiDeleteMemberDiscussionGeneral({ user: hasil, idUser: chooseUser.idUser }, id)
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengeluarkan anggota dari diskusi', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengeluarkan anggota dari diskusi', })
|
||||||
handleLoad(false)
|
handleLoad(false)
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengeluarkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,9 +92,11 @@ export default function AddMemberCalendarEvent() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,9 +57,11 @@ export default function EditEventCalendar() {
|
|||||||
setData({ ...response.data, dateStart: moment(response.data.dateStartFormat, 'DD-MM-YYYY').format('DD-MM-YYYY') })
|
setData({ ...response.data, dateStart: moment(response.data.dateStartFormat, 'DD-MM-YYYY').format('DD-MM-YYYY') })
|
||||||
setIdCalendar(response.data.idCalendar)
|
setIdCalendar(response.data.idCalendar)
|
||||||
setChoose({ val: response.data.repeatEventTyper, label: valueTypeEventRepeat.find((item) => item.id == response.data.repeatEventTyper)?.name || "" })
|
setChoose({ val: response.data.repeatEventTyper, label: valueTypeEventRepeat.find((item) => item.id == response.data.repeatEventTyper)?.name || "" })
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mendapatkan data', })
|
const message = error?.response?.data?.message || "Gagal mendapatkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,9 +156,11 @@ export default function EditEventCalendar() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah acara"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,9 +137,11 @@ export default function DetailEventCalendar() {
|
|||||||
dispatch(setUpdateCalendar({ ...update, member: !update.member }));
|
dispatch(setUpdateCalendar({ ...update, member: !update.member }));
|
||||||
}
|
}
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menghapus anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModalMember(false)
|
setModalMember(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ export default function CreateCalendarAddMember() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal membuat acara"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ export default function DiscussionDivisionEdit() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,8 +202,11 @@ export default function DiscussionDetail() {
|
|||||||
setKomentar("")
|
setKomentar("")
|
||||||
updateTrigger()
|
updateTrigger()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menambahkan komentar"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSend(false);
|
setLoadingSend(false);
|
||||||
}
|
}
|
||||||
@@ -222,8 +225,11 @@ export default function DiscussionDetail() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengedit komentar"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSend(false);
|
setLoadingSend(false);
|
||||||
handleViewEditKomentar()
|
handleViewEditKomentar()
|
||||||
@@ -243,8 +249,11 @@ export default function DiscussionDetail() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus komentar"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSend(false)
|
setLoadingSend(false)
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
|
|||||||
@@ -81,9 +81,11 @@ export default function CreateDiscussionDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,9 +235,11 @@ export default function DocumentDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah nama"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRename(false)
|
setLoadingRename(false)
|
||||||
setRename(false)
|
setRename(false)
|
||||||
@@ -258,9 +260,11 @@ export default function DocumentDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menghapus"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,9 +288,11 @@ export default function DocumentDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal membagikan"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setShare(false);
|
setShare(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,9 +120,11 @@ export default function TaskDivisionAddFile() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan file"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ export default function AddMemberTask() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,9 +133,11 @@ export default function TaskDivisionAddTask() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,9 +62,11 @@ export default function TaskDivisionCancel() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal membatalkan kegiatan"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ export default function TaskDivisionEdit() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,11 @@ export default function TaskDivisionReport() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,9 +105,11 @@ export default function CreateTaskDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,9 +125,11 @@ export default function UpdateProjectTaskDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSubmit(false)
|
setLoadingSubmit(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ export default function AddMemberDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,9 +56,11 @@ export default function EditDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,9 +78,11 @@ export default function InformationDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengeluarkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
}
|
}
|
||||||
@@ -96,9 +98,11 @@ export default function InformationDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah status admin"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,8 +94,11 @@ export default function ReportDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, });
|
Toast.show({ type: 'small', text1: response.message, });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ export default function CreateDivision() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingBtn(false)
|
setLoadingBtn(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,9 +66,11 @@ export default function CreateDivisionAddAdmin() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ import {
|
|||||||
Ionicons,
|
Ionicons,
|
||||||
MaterialCommunityIcons
|
MaterialCommunityIcons
|
||||||
} from "@expo/vector-icons";
|
} from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
|
import { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -40,23 +41,23 @@ export default function ListDivision() {
|
|||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [nameGroup, setNameGroup] = useState("")
|
const queryClient = useQueryClient()
|
||||||
const [data, setData] = useState<Props[]>([])
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
// ... state same ...
|
const [category, setCategory] = useState<'divisi-saya' | 'semua'>(cat == 'semua' ? 'semua' : 'divisi-saya')
|
||||||
const update = useSelector((state: any) => state.divisionUpdate)
|
const update = useSelector((state: any) => state.divisionUpdate)
|
||||||
const arrSkeleton = Array.from({ length: 3 }, (_, index) => index)
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
|
||||||
const [category, setCategory] = useState<'divisi-saya' | 'semua'>('divisi-saya')
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query for Divisions with Infinite Scroll
|
||||||
try {
|
const {
|
||||||
setWaiting(true)
|
data,
|
||||||
setLoading(loading)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['divisions', { status, search, group, category }],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDivision({
|
const response = await apiGetDivision({
|
||||||
user: hasil,
|
user: hasil,
|
||||||
@@ -64,54 +65,52 @@ export default function ListDivision() {
|
|||||||
search: search,
|
search: search,
|
||||||
group: String(group),
|
group: String(group),
|
||||||
kategori: category,
|
kategori: category,
|
||||||
page: thisPage
|
page: pageParam
|
||||||
});
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
if (response.success) {
|
// Refetch when manual update state changes
|
||||||
if (thisPage == 1) {
|
|
||||||
setData(response.data);
|
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
|
||||||
setData([...data, ...response.data]);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setNameGroup(response.filter.name);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setWaiting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1);
|
refetch()
|
||||||
}, [update]);
|
}, [update, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
// Flatten pages into a single data array
|
||||||
handleLoad(true, 1);
|
const flatData = useMemo(() => {
|
||||||
}, [status, search, group, category]);
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const loadMoreData = () => {
|
// Get nameGroup from the first available page
|
||||||
if (waiting) return
|
const nameGroup = useMemo(() => {
|
||||||
setTimeout(() => {
|
return data?.pages[0]?.filter?.name || "";
|
||||||
handleLoad(false, page + 1)
|
}, [data])
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
await queryClient.invalidateQueries({ queryKey: ['divisions'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadMoreData = () => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2]
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
id: data[index].id,
|
id: flatData[index]?.id,
|
||||||
name: data[index].name,
|
name: flatData[index]?.name,
|
||||||
desc: data[index].desc,
|
desc: flatData[index]?.desc,
|
||||||
jumlah_member: data[index].jumlah_member,
|
jumlah_member: flatData[index]?.jumlah_member,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -206,7 +205,7 @@ export default function ListDivision() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2 }, Styles.mt10]}>
|
<View style={[{ flex: 2 }, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
isList ?
|
isList ?
|
||||||
arrSkeleton.map((item, index) => (
|
arrSkeleton.map((item, index) => (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
@@ -216,7 +215,7 @@ export default function ListDivision() {
|
|||||||
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
|
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
|
||||||
))
|
))
|
||||||
:
|
:
|
||||||
data.length == 0 ? (
|
flatData.length == 0 ? (
|
||||||
<View style={[Styles.mt15]}>
|
<View style={[Styles.mt15]}>
|
||||||
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text>
|
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -224,9 +223,9 @@ export default function ListDivision() {
|
|||||||
isList ? (
|
isList ? (
|
||||||
<View style={[Styles.h100]}>
|
<View style={[Styles.h100]}>
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
style={[{ paddingBottom: 100 }]}
|
style={[{ paddingBottom: 100 }]}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -241,7 +240,6 @@ export default function ListDivision() {
|
|||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
titleWeight="normal"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -261,9 +259,9 @@ export default function ListDivision() {
|
|||||||
) : (
|
) : (
|
||||||
<View style={[Styles.h100]}>
|
<View style={[Styles.h100]}>
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
style={[{ paddingBottom: 100 }]}
|
style={[{ paddingBottom: 100 }]}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -112,8 +112,11 @@ export default function Report() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,9 +188,14 @@ export default function EditProfile() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengupdate data', })
|
const message = error?.response?.data?.message || "Gagal mengupdate data"
|
||||||
|
|
||||||
|
Toast.show({
|
||||||
|
type: 'small',
|
||||||
|
text1: message
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { setUpdateGroup } from "@/lib/groupSlice";
|
|||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign, Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { AntDesign, Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
@@ -31,16 +32,14 @@ export default function Index() {
|
|||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
const [status, setStatus] = useState<'true' | 'false'>('true')
|
||||||
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||||
const [idChoose, setIdChoose] = useState('')
|
const [idChoose, setIdChoose] = useState('')
|
||||||
const [activeChoose, setActiveChoose] = useState(true)
|
const [activeChoose, setActiveChoose] = useState(true)
|
||||||
const [titleChoose, setTitleChoose] = useState('')
|
const [titleChoose, setTitleChoose] = useState('')
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
@@ -49,12 +48,38 @@ export default function Index() {
|
|||||||
title: false,
|
title: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TanStack Query for Groups
|
||||||
|
const {
|
||||||
|
data: queryData,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['groups', { status, search }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const response = await apiGetGroup({
|
||||||
|
user: hasil,
|
||||||
|
active: status,
|
||||||
|
search: search
|
||||||
|
})
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = useMemo(() => queryData?.data || [], [queryData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch()
|
||||||
|
}, [update, refetch])
|
||||||
|
|
||||||
async function handleEdit() {
|
async function handleEdit() {
|
||||||
try {
|
try {
|
||||||
setLoadingSubmit(true)
|
setLoadingSubmit(true)
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose)
|
const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['groups'] })
|
||||||
dispatch(setUpdateGroup(!update))
|
dispatch(setUpdateGroup(!update))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -71,6 +96,7 @@ export default function Index() {
|
|||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose)
|
const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['groups'] })
|
||||||
dispatch(setUpdateGroup(!update))
|
dispatch(setUpdateGroup(!update))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -80,32 +106,9 @@ export default function Index() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
|
||||||
try {
|
|
||||||
setLoading(loading)
|
|
||||||
const hasil = await decryptToken(String(token?.current))
|
|
||||||
const response = await apiGetGroup({ user: hasil, active: status, search: search })
|
|
||||||
setData(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(false)
|
|
||||||
}, [update])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
}, [status, search])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false)
|
await queryClient.invalidateQueries({ queryKey: ['groups'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,6 +132,8 @@ export default function Index() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.mb10]}>
|
<View style={[Styles.mb10]}>
|
||||||
@@ -152,7 +157,7 @@ export default function Index() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2 }, Styles.mt10]}>
|
<View style={[{ flex: 2 }, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
arrSkeleton.map((item, index) => {
|
arrSkeleton.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import { apiGetProfile } from "@/lib/api";
|
|||||||
import { setEntities } from "@/lib/entitiesSlice";
|
import { setEntities } from "@/lib/entitiesSlice";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Dimensions, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
import { Alert, Dimensions, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -23,28 +24,66 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
const entities = useSelector((state: any) => state.entities)
|
const entities = useSelector((state: any) => state.entities)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { token, decryptToken, signOut } = useAuthSession()
|
const { token, decryptToken, signOut } = useAuthSession()
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: profile, isError } = useQuery({
|
||||||
handleUserLogin()
|
queryKey: ['profile'],
|
||||||
}, [dispatch]);
|
queryFn: async () => {
|
||||||
|
const hasil = await decryptToken(String(token?.current))
|
||||||
|
const data = await apiGetProfile({ id: hasil })
|
||||||
|
return data.data
|
||||||
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0, // Ensure it refetches every time the component mounts
|
||||||
|
})
|
||||||
|
|
||||||
async function handleUserLogin() {
|
// Sync to Redux for global usage
|
||||||
const hasil = await decryptToken(String(token?.current))
|
useEffect(() => {
|
||||||
apiGetProfile({ id: hasil })
|
if (profile) {
|
||||||
.then((data) => dispatch(setEntities(data.data)))
|
dispatch(setEntities(profile))
|
||||||
.catch((error) => {
|
}
|
||||||
signOut()
|
}, [profile, dispatch])
|
||||||
});
|
|
||||||
}
|
// Auto Sign Out if profile fetch fails (e.g. invalid/expired token)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isError) {
|
||||||
|
signOut()
|
||||||
|
}
|
||||||
|
}, [isError, signOut])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile && profile.isActive === false) {
|
||||||
|
Alert.alert(
|
||||||
|
'Akun Dinonaktifkan',
|
||||||
|
'Akun kamu telah dinonaktifkan. Silahkan hubungi admin untuk informasi lebih lanjut.',
|
||||||
|
[{ text: 'OK', onPress: signOut }]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [profile, signOut])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile && profile.villageIsActive === false) {
|
||||||
|
Alert.alert(
|
||||||
|
'Desa Dinonaktifkan',
|
||||||
|
'Desa kamu saat ini telah dinonaktifkan. Silahkan hubungi pengelola sistem untuk informasi lebih lanjut.',
|
||||||
|
[{ text: 'OK', onPress: signOut }]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [profile, signOut])
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleUserLogin()
|
// Invalidate all queries related to the home screen
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await queryClient.invalidateQueries({ queryKey: ['profile'] })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['banners'] })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['homeData'] })
|
||||||
|
|
||||||
|
// Artificial delay to show refresh indicator if sync is too fast
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,11 @@ export default function MemberDetail() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
Toast.show({ type: 'small', text1: response.message })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,9 +185,11 @@ export default function CreateMember() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,9 +211,11 @@ export default function EditMember() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import { apiGetUser } from "@/lib/api";
|
|||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign, Feather } from "@expo/vector-icons";
|
import { AntDesign, Feather } from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -37,73 +38,81 @@ export default function Index() {
|
|||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [nameGroup, setNameGroup] = useState('')
|
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const update = useSelector((state: any) => state.memberUpdate)
|
const update = useSelector((state: any) => state.memberUpdate)
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
const queryClient = useQueryClient()
|
||||||
const [loading, setLoading] = useState(true)
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query for Members with Infinite Scroll
|
||||||
try {
|
const {
|
||||||
setWaiting(true)
|
data,
|
||||||
setLoading(loading)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['members', { status, search, group }],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetUser({ user: hasil, active: status, search, group: String(group), page: thisPage })
|
const response = await apiGetUser({
|
||||||
if (thisPage == 1) {
|
user: hasil,
|
||||||
setData(response.data)
|
active: status,
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
search,
|
||||||
setData([...data, ...response.data])
|
group: String(group),
|
||||||
} else {
|
page: pageParam
|
||||||
return;
|
})
|
||||||
}
|
return response;
|
||||||
setNameGroup(response.filter.name)
|
},
|
||||||
} catch (error) {
|
initialPageParam: 1,
|
||||||
console.error(error)
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
} finally {
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
setLoading(false)
|
},
|
||||||
setWaiting(false)
|
enabled: !!token?.current,
|
||||||
}
|
staleTime: 0,
|
||||||
}
|
})
|
||||||
|
|
||||||
const loadMoreData = () => {
|
// Flatten pages into a single data array
|
||||||
if (waiting) return
|
const flatData = useMemo(() => {
|
||||||
setTimeout(() => {
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
handleLoad(false, page + 1)
|
}, [data])
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Get nameGroup from the first available page
|
||||||
|
const nameGroup = useMemo(() => {
|
||||||
|
return data?.pages[0]?.filter?.name || "";
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
// Refetch when manual update state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1)
|
refetch()
|
||||||
}, [update])
|
}, [update, refetch])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true, 1)
|
|
||||||
}, [group, search, status])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
await queryClient.invalidateQueries({ queryKey: ['members'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadMoreData = () => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
id: data[index].id,
|
id: flatData[index]?.id,
|
||||||
name: data[index].name,
|
name: flatData[index]?.name,
|
||||||
nik: data[index].nik,
|
nik: flatData[index]?.nik,
|
||||||
email: data[index].email,
|
email: flatData[index]?.email,
|
||||||
phone: data[index].phone,
|
phone: flatData[index]?.phone,
|
||||||
gender: data[index].gender,
|
gender: flatData[index]?.gender,
|
||||||
position: data[index].position,
|
position: flatData[index]?.position,
|
||||||
group: data[index].group,
|
group: flatData[index]?.group,
|
||||||
img: data[index].img,
|
img: flatData[index]?.img,
|
||||||
isActive: data[index].isActive,
|
isActive: flatData[index]?.isActive,
|
||||||
role: data[index].role,
|
role: flatData[index]?.role,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -136,18 +145,18 @@ export default function Index() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[{ flex: 2 }, Styles.mt10]}>
|
<View style={[{ flex: 2 }, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
arrSkeleton.map((item, index) => {
|
arrSkeleton.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
data.length > 0
|
flatData.length > 0
|
||||||
?
|
?
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
|
||||||
import BorderBottomItemVertical from "@/components/borderBottomItemVertical";
|
import BorderBottomItemVertical from "@/components/borderBottomItemVertical";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
@@ -10,7 +9,8 @@ import { pushToPage } from "@/lib/pushToPage";
|
|||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native";
|
import { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -27,64 +27,61 @@ type Props = {
|
|||||||
export default function Notification() {
|
export default function Notification() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const [loading, setLoading] = useState(false)
|
const queryClient = useQueryClient()
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const updateNotification = useSelector((state: any) => state.notificationUpdate)
|
const updateNotification = useSelector((state: any) => state.notificationUpdate)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query for Notifications with Infinite Scroll
|
||||||
try {
|
const {
|
||||||
setLoading(loading)
|
data,
|
||||||
setPage(thisPage)
|
fetchNextPage,
|
||||||
setWaiting(true)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['notifications'],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetNotification({ user: hasil, page: thisPage })
|
const response = await apiGetNotification({ user: hasil, page: pageParam })
|
||||||
if (thisPage == 1) {
|
return response;
|
||||||
setData(response.data)
|
},
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
initialPageParam: 1,
|
||||||
setData([...data, ...response.data])
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
} else {
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
return;
|
},
|
||||||
}
|
enabled: !!token?.current,
|
||||||
} catch (error) {
|
staleTime: 0,
|
||||||
console.error(error)
|
})
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setWaiting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Flatten pages into a single data array
|
||||||
|
const flatData = useMemo(() => {
|
||||||
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
|
}, [data])
|
||||||
|
|
||||||
const loadMoreData = () => {
|
// Refetch when manual update state changes
|
||||||
if (waiting) return
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
refetch()
|
||||||
handleLoad(false, page + 1)
|
}, [updateNotification, refetch])
|
||||||
}, 1000);
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setRefreshing(true)
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||||
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const loadMoreData = () => {
|
||||||
handleLoad(true, 1)
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
}, [])
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
};
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
|
||||||
id: data[index].id,
|
|
||||||
title: data[index].title,
|
|
||||||
desc: data[index].desc,
|
|
||||||
category: data[index].category,
|
|
||||||
idContent: data[index].idContent,
|
|
||||||
isRead: data[index].isRead,
|
|
||||||
createdAt: data[index].createdAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleReadNotification(id: string, category: string, idContent: string) {
|
async function handleReadNotification(id: string, category: string, idContent: string) {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiReadOneNotification({ user: hasil, id: id })
|
const response = await apiReadOneNotification({ user: hasil, id: id })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||||
pushToPage(category, idContent)
|
pushToPage(category, idContent)
|
||||||
dispatch(setUpdateNotification(!updateNotification))
|
dispatch(setUpdateNotification(!updateNotification))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -92,28 +89,33 @@ export default function Notification() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
setRefreshing(true)
|
|
||||||
handleLoad(false, 1)
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
id: flatData[index]?.id,
|
||||||
setRefreshing(false)
|
title: flatData[index]?.title,
|
||||||
};
|
desc: flatData[index]?.desc,
|
||||||
|
category: flatData[index]?.category,
|
||||||
|
idContent: flatData[index]?.idContent,
|
||||||
|
isRead: flatData[index]?.isRead,
|
||||||
|
createdAt: flatData[index]?.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.p15]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
arrSkeleton.map((item, index) => {
|
arrSkeleton.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
data.length > 0 ?
|
flatData.length > 0 ?
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import { setUpdatePosition } from "@/lib/positionSlice";
|
|||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
|
import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useLocalSearchParams } from "expo-router";
|
import { useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||||
import Toast from "react-native-toast-message";
|
import Toast from "react-native-toast-message";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
@@ -31,51 +32,53 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { colors } = useTheme()
|
const { colors } = useTheme()
|
||||||
const [status, setStatus] = useState<'true' | 'false'>('true')
|
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
|
||||||
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
||||||
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const [isModal, setModal] = useState(false)
|
const [isModal, setModal] = useState(false)
|
||||||
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
const [isVisibleEdit, setVisibleEdit] = useState(false)
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [nameGroup, setNameGroup] = useState('')
|
|
||||||
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
const [loadingSubmit, setLoadingSubmit] = useState(false)
|
||||||
const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' })
|
const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' })
|
||||||
const [error, setError] = useState({
|
const [error, setError] = useState({
|
||||||
name: false,
|
name: false,
|
||||||
});
|
});
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const update = useSelector((state: any) => state.positionUpdate)
|
const update = useSelector((state: any) => state.positionUpdate)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
// TanStack Query for Positions
|
||||||
try {
|
const {
|
||||||
setLoading(loading)
|
data: queryData,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['positions', { status, search, group }],
|
||||||
|
queryFn: async () => {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiGetPosition({ user: hasil, active: status, search: search, group: String(group) })
|
const response = await apiGetPosition({
|
||||||
setData(response.data)
|
user: hasil,
|
||||||
setNameGroup(response.filter.name)
|
active: status,
|
||||||
} catch (error) {
|
search: search,
|
||||||
console.error(error)
|
group: String(group)
|
||||||
} finally {
|
})
|
||||||
setLoading(false)
|
return response;
|
||||||
}
|
},
|
||||||
}
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = useMemo(() => queryData?.data || [], [queryData])
|
||||||
|
const nameGroup = useMemo(() => queryData?.filter?.name || "", [queryData])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false)
|
refetch()
|
||||||
}, [update])
|
}, [update, refetch])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
}, [status, search, group])
|
|
||||||
|
|
||||||
|
|
||||||
function handleChooseData(id: string, name: string, active: boolean, group: string) {
|
function handleChooseData(id: string, name: string, active: boolean, group: string) {
|
||||||
@@ -88,8 +91,11 @@ export default function Index() {
|
|||||||
const hasil = await decryptToken(String(token?.current))
|
const hasil = await decryptToken(String(token?.current))
|
||||||
const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id)
|
const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id)
|
||||||
dispatch(setUpdatePosition(!update))
|
dispatch(setUpdatePosition(!update))
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setModal(false)
|
setModal(false)
|
||||||
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
|
Toast.show({ type: 'small', text1: 'Berhasil mengupdate data', })
|
||||||
@@ -107,8 +113,11 @@ export default function Index() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSubmit(false)
|
setLoadingSubmit(false)
|
||||||
setVisibleEdit(false)
|
setVisibleEdit(false)
|
||||||
@@ -132,10 +141,11 @@ export default function Index() {
|
|||||||
handleEdit()
|
handleEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2, 3, 4]
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false)
|
await queryClient.invalidateQueries({ queryKey: ['positions'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -178,7 +188,7 @@ export default function Index() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[Styles.flex2, Styles.mt10]}>
|
<View style={[Styles.flex2, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
arrSkeleton.map((item, index) => {
|
arrSkeleton.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
|
|||||||
@@ -118,9 +118,11 @@ export default function ProjectAddFile() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ export default function AddMemberProject() {
|
|||||||
setIdGroup(responseGroup.data.idGroup)
|
setIdGroup(responseGroup.data.idGroup)
|
||||||
const responsemember = await apiGetUser({ user: hasil, active: "true", search: search, group: String(responseGroup.data.idGroup) })
|
const responsemember = await apiGetUser({ user: hasil, active: "true", search: search, group: String(responseGroup.data.idGroup) })
|
||||||
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
|
setData(responsemember.data.filter((i: any) => i.idUserRole != 'supadmin'))
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +88,11 @@ export default function AddMemberProject() {
|
|||||||
dispatch(setUpdateProject({ ...update, member: !update.member }))
|
dispatch(setUpdateProject({ ...update, member: !update.member }))
|
||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan anggota"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,11 @@ export default function ProjectAddTask() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,11 @@ export default function ProjectCancel() {
|
|||||||
Toast.show({ type: 'small', text1: 'Berhasil membatalkan kegiatan', })
|
Toast.show({ type: 'small', text1: 'Berhasil membatalkan kegiatan', })
|
||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal membatalkan kegiatan"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ export default function EditProject() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ export default function ReportProject() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,9 +151,11 @@ export default function CreateProject() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menambahkan data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
Ionicons,
|
Ionicons,
|
||||||
MaterialCommunityIcons,
|
MaterialCommunityIcons,
|
||||||
} from "@expo/vector-icons";
|
} from "@expo/vector-icons";
|
||||||
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -40,28 +41,29 @@ export default function ListProject() {
|
|||||||
cat?: string;
|
cat?: string;
|
||||||
year?: string;
|
year?: string;
|
||||||
}>();
|
}>();
|
||||||
const [statusFix, setStatusFix] = useState<'0' | '1' | '2' | '3'>('0')
|
const [statusFix, setStatusFix] = useState<'0' | '1' | '2' | '3'>(
|
||||||
|
(status == '1' || status == '2' || status == '3') ? status : '0'
|
||||||
|
)
|
||||||
const { token, decryptToken } = useAuthSession();
|
const { token, decryptToken } = useAuthSession();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const entityUser = useSelector((state: any) => state.user)
|
const entityUser = useSelector((state: any) => state.user)
|
||||||
const [search, setSearch] = useState("")
|
const [search, setSearch] = useState("")
|
||||||
const [nameGroup, setNameGroup] = useState("")
|
|
||||||
// ... state same ...
|
|
||||||
const [isYear, setYear] = useState("")
|
|
||||||
const [data, setData] = useState<Props[]>([])
|
|
||||||
const [isList, setList] = useState(false)
|
const [isList, setList] = useState(false)
|
||||||
const update = useSelector((state: any) => state.projectUpdate)
|
const update = useSelector((state: any) => state.projectUpdate)
|
||||||
const [loading, setLoading] = useState(true)
|
const queryClient = useQueryClient()
|
||||||
const arrSkeleton = Array.from({ length: 3 }, (_, index) => index)
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [waiting, setWaiting] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
async function handleLoad(loading: boolean, thisPage: number) {
|
// TanStack Query for Projects with Infinite Scroll
|
||||||
try {
|
const {
|
||||||
setLoading(loading)
|
data,
|
||||||
setWaiting(true)
|
fetchNextPage,
|
||||||
setPage(thisPage)
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isLoading,
|
||||||
|
refetch
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ['projects', { statusFix, search, group, cat, year }],
|
||||||
|
queryFn: async ({ pageParam = 1 }) => {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetProject({
|
const response = await apiGetProject({
|
||||||
user: hasil,
|
user: hasil,
|
||||||
@@ -69,60 +71,55 @@ export default function ListProject() {
|
|||||||
search: search,
|
search: search,
|
||||||
group: String(group),
|
group: String(group),
|
||||||
kategori: String(cat),
|
kategori: String(cat),
|
||||||
page: thisPage,
|
page: pageParam,
|
||||||
year: String(year)
|
year: String(year)
|
||||||
});
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage, allPages) => {
|
||||||
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
||||||
|
},
|
||||||
|
enabled: !!token?.current,
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
if (response.success) {
|
// Refetch when manual update state changes
|
||||||
setNameGroup(response.filter.name);
|
|
||||||
setYear(response.tahun)
|
|
||||||
if (thisPage == 1) {
|
|
||||||
setData(response.data);
|
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
|
||||||
setData([...data, ...response.data])
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setWaiting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLoad(false, 1);
|
refetch()
|
||||||
}, [update.data]);
|
}, [update.data, refetch])
|
||||||
|
|
||||||
|
// Flatten pages into a single data array
|
||||||
|
const flatData = useMemo(() => {
|
||||||
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
|
}, [data])
|
||||||
|
|
||||||
useEffect(() => {
|
// Get metadata from the first available page
|
||||||
handleLoad(true, 1);
|
const nameGroup = useMemo(() => data?.pages[0]?.filter?.name || "", [data])
|
||||||
}, [statusFix, search, group, cat, year]);
|
const isYear = useMemo(() => data?.pages[0]?.tahun || "", [data])
|
||||||
|
|
||||||
const loadMoreData = () => {
|
|
||||||
if (waiting) return
|
|
||||||
setTimeout(() => {
|
|
||||||
handleLoad(false, page + 1)
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
await queryClient.invalidateQueries({ queryKey: ['projects'] })
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const loadMoreData = () => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) {
|
||||||
|
fetchNextPage()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrSkeleton = [0, 1, 2]
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const getItem = (_data: unknown, index: number): Props => ({
|
||||||
id: data[index].id,
|
id: flatData[index]?.id,
|
||||||
title: data[index].title,
|
title: flatData[index]?.title,
|
||||||
desc: data[index].desc,
|
desc: flatData[index]?.desc,
|
||||||
status: data[index].status,
|
status: flatData[index]?.status,
|
||||||
member: data[index].member,
|
member: flatData[index]?.member,
|
||||||
progress: data[index].progress,
|
progress: flatData[index]?.progress,
|
||||||
createdAt: data[index].createdAt,
|
createdAt: flatData[index]?.createdAt,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -205,7 +202,6 @@ export default function ListProject() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[Styles.mt10]}>
|
<View style={[Styles.mt10]}>
|
||||||
{
|
{
|
||||||
// entityUser.role != 'cosupadmin' && entityUser.role != 'admin' &&
|
|
||||||
<View style={[Styles.rowOnly]}>
|
<View style={[Styles.rowOnly]}>
|
||||||
<Text style={[Styles.mr05]}>Filter :</Text>
|
<Text style={[Styles.mr05]}>Filter :</Text>
|
||||||
{
|
{
|
||||||
@@ -218,18 +214,13 @@ export default function ListProject() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
<LabelStatus size="small" category="secondary" text={isYear} style={[Styles.mr05]} />
|
<LabelStatus size="small" category="secondary" text={isYear} style={[Styles.mr05]} />
|
||||||
{/* {
|
|
||||||
(entityUser.role == 'user' || entityUser.role == 'coadmin')
|
|
||||||
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? <LabelStatus size="small" category="primary" text="Kegiatan Saya" /> : <LabelStatus size="small" category="primary" text="Semua Kegiatan" />
|
|
||||||
: ''
|
|
||||||
} */}
|
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.flex2, Styles.mt10]}>
|
<View style={[Styles.flex2, Styles.mt10]}>
|
||||||
{
|
{
|
||||||
loading ?
|
isLoading ?
|
||||||
isList ?
|
isList ?
|
||||||
arrSkeleton.map((item, index) => (
|
arrSkeleton.map((item, index) => (
|
||||||
<SkeletonTwoItem key={index} />
|
<SkeletonTwoItem key={index} />
|
||||||
@@ -239,13 +230,13 @@ export default function ListProject() {
|
|||||||
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
|
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
|
||||||
))
|
))
|
||||||
:
|
:
|
||||||
data.length > 0
|
flatData.length > 0
|
||||||
?
|
?
|
||||||
isList ? (
|
isList ? (
|
||||||
<View style={[Styles.h100]}>
|
<View style={[Styles.h100]}>
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -279,35 +270,12 @@ export default function ListProject() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* {
|
|
||||||
data.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
onPress={() => { router.push(`/project/${item.id}`); }}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={
|
|
||||||
<View
|
|
||||||
style={[Styles.iconContent, ColorsStatus.lightGreen]}
|
|
||||||
>
|
|
||||||
<AntDesign
|
|
||||||
name="areachart"
|
|
||||||
size={25}
|
|
||||||
color={"#384288"}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title={item.title}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
} */}
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={[Styles.h100]}>
|
<View style={[Styles.h100]}>
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
data={data}
|
data={flatData}
|
||||||
getItemCount={() => data.length}
|
getItemCount={() => flatData.length}
|
||||||
getItem={getItem}
|
getItem={getItem}
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||||
return (
|
return (
|
||||||
@@ -319,6 +287,7 @@ export default function ListProject() {
|
|||||||
content="page"
|
content="page"
|
||||||
title={item.title}
|
title={item.title}
|
||||||
headerColor="primary"
|
headerColor="primary"
|
||||||
|
titleTail={2}
|
||||||
>
|
>
|
||||||
<ProgressBar value={item.progress} category="list" />
|
<ProgressBar value={item.progress} category="list" />
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween]}>
|
||||||
@@ -358,43 +327,6 @@ export default function ListProject() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* {data.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<PaperGridContent
|
|
||||||
key={index}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/project/${item.id}`);
|
|
||||||
}}
|
|
||||||
content="page"
|
|
||||||
title={item.title}
|
|
||||||
headerColor="primary"
|
|
||||||
>
|
|
||||||
<ProgressBar value={item.progress} category="list" />
|
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
|
||||||
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>
|
|
||||||
{item.createdAt}
|
|
||||||
</Text>
|
|
||||||
<LabelStatus
|
|
||||||
size="default"
|
|
||||||
category={
|
|
||||||
item.status === 0 ? 'primary' :
|
|
||||||
item.status === 1 ? 'warning' :
|
|
||||||
item.status === 2 ? 'success' :
|
|
||||||
item.status === 3 ? 'error' :
|
|
||||||
'primary'
|
|
||||||
}
|
|
||||||
text={
|
|
||||||
item.status === 0 ? 'SEGERA' :
|
|
||||||
item.status === 1 ? 'DIKERJAKAN' :
|
|
||||||
item.status === 2 ? 'SELESAI' :
|
|
||||||
item.status === 3 ? 'DIBATALKAN' :
|
|
||||||
'SEGERA'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</PaperGridContent>
|
|
||||||
);
|
|
||||||
})} */}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
:
|
:
|
||||||
|
|||||||
@@ -118,9 +118,11 @@ export default function UpdateProjectTask() {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSubmit(false)
|
setLoadingSubmit(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,9 +65,14 @@ export default function Search() {
|
|||||||
setDataDivisi([])
|
setDataDivisi([])
|
||||||
setDataProject([])
|
setDataProject([])
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
return Toast.show({ type: 'small', text1: 'Gagal melakukan pencarian', })
|
const message = error?.response?.data?.message || "Gagal melakukan pencarian"
|
||||||
|
|
||||||
|
Toast.show({
|
||||||
|
type: 'small',
|
||||||
|
text1: message
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import Text from "@/components/Text";
|
|||||||
import ButtonSetting from "@/components/buttonSetting";
|
import ButtonSetting from "@/components/buttonSetting";
|
||||||
import DrawerBottom from "@/components/drawerBottom";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiRegisteredToken, apiUnregisteredToken } from "@/lib/api";
|
import { apiGetCheckToken, apiRegisteredToken, apiUnregisteredToken } from "@/lib/api";
|
||||||
import { checkPermission, getToken, openSettings, requestPermission } from "@/lib/useNotification";
|
import { checkPermission, getToken, openSettings } from "@/lib/useNotification";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import Constants from "expo-constants";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { AppState, AppStateStatus, Pressable, View } from "react-native";
|
import { AppState, AppStateStatus, Pressable, View } from "react-native";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
@@ -28,12 +30,14 @@ export default function ListSetting() {
|
|||||||
|
|
||||||
const [showLogoutModal, setShowLogoutModal] = useState(false)
|
const [showLogoutModal, setShowLogoutModal] = useState(false)
|
||||||
const [showThemeModal, setShowThemeModal] = useState(false)
|
const [showThemeModal, setShowThemeModal] = useState(false)
|
||||||
|
const prevOsPermission = useRef<boolean | undefined>(undefined);
|
||||||
|
|
||||||
const registerToken = async () => {
|
const registerToken = async () => {
|
||||||
try {
|
try {
|
||||||
|
await AsyncStorage.setItem('@notification_permission', "true");
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
await apiRegisteredToken({ user: entities.id, token });
|
await apiRegisteredToken({ user: entities.id, token, category: "register" });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error registering token:', error);
|
console.warn('Error registering token:', error);
|
||||||
@@ -42,9 +46,10 @@ export default function ListSetting() {
|
|||||||
|
|
||||||
const unregisterToken = async () => {
|
const unregisterToken = async () => {
|
||||||
try {
|
try {
|
||||||
|
await AsyncStorage.setItem('@notification_permission', "false");
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
await apiUnregisteredToken({ user: entities.id, token });
|
await apiUnregisteredToken({ user: entities.id, token, category: "unregister" });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error unregistering token:', error);
|
console.warn('Error unregistering token:', error);
|
||||||
@@ -52,15 +57,31 @@ export default function ListSetting() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkNotif = useCallback(async () => {
|
const checkNotif = useCallback(async () => {
|
||||||
const status = await checkPermission();
|
const osPermission = await checkPermission();
|
||||||
setIsNotificationEnabled((prev) => {
|
|
||||||
if (prev === false && status === true) {
|
// Jika dari tidak diijinkan sistem kemudian diijinkan (setelah balik dari pengaturan device)
|
||||||
registerToken();
|
if (prevOsPermission.current === false && osPermission === true) {
|
||||||
} else if (prev === true && status === false) {
|
await registerToken();
|
||||||
unregisterToken();
|
}
|
||||||
|
prevOsPermission.current = osPermission;
|
||||||
|
|
||||||
|
if (!osPermission) {
|
||||||
|
setIsNotificationEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await getToken();
|
||||||
|
if (token) {
|
||||||
|
const response = await apiGetCheckToken({ user: entities.id, token });
|
||||||
|
setIsNotificationEnabled(!!response.data);
|
||||||
|
} else {
|
||||||
|
setIsNotificationEnabled(false);
|
||||||
}
|
}
|
||||||
return !!status;
|
} catch (error) {
|
||||||
});
|
console.warn('Error checking token status:', error);
|
||||||
|
setIsNotificationEnabled(false);
|
||||||
|
}
|
||||||
}, [entities.id]);
|
}, [entities.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -78,10 +99,12 @@ export default function ListSetting() {
|
|||||||
}, [checkNotif]);
|
}, [checkNotif]);
|
||||||
|
|
||||||
const handleToggleNotif = async () => {
|
const handleToggleNotif = async () => {
|
||||||
if (isNotificationEnabled) {
|
const osPermission = await checkPermission();
|
||||||
|
|
||||||
|
if (!osPermission) {
|
||||||
setModalConfig({
|
setModalConfig({
|
||||||
title: "Matikan Notifikasi?",
|
title: "Aktifkan Notifikasi?",
|
||||||
message: "Anda akan diarahkan ke pengaturan sistem untuk mematikan notifikasi.",
|
message: "Izin notifikasi tidak diberikan. Buka pengaturan sistem untuk mengaktifkannya?",
|
||||||
confirmText: "Buka Pengaturan",
|
confirmText: "Buka Pengaturan",
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
@@ -90,22 +113,15 @@ export default function ListSetting() {
|
|||||||
});
|
});
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
} else {
|
} else {
|
||||||
const granted = await requestPermission();
|
// OS Permission is granted, perform in-app toggle
|
||||||
if (granted) {
|
const targetState = !isNotificationEnabled;
|
||||||
setIsNotificationEnabled(true);
|
if (targetState) {
|
||||||
registerToken();
|
await registerToken();
|
||||||
} else {
|
} else {
|
||||||
setModalConfig({
|
await unregisterToken();
|
||||||
title: "Aktifkan Notifikasi?",
|
|
||||||
message: "Izin notifikasi tidak diberikan. Buka pengaturan sistem untuk mengaktifkannya?",
|
|
||||||
confirmText: "Buka Pengaturan",
|
|
||||||
onConfirm: () => {
|
|
||||||
setModalVisible(false);
|
|
||||||
openSettings();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setModalVisible(true);
|
|
||||||
}
|
}
|
||||||
|
// UI will be updated by checkNotif (triggered by state change or manually here)
|
||||||
|
setIsNotificationEnabled(targetState);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,6 +197,10 @@ export default function ListSetting() {
|
|||||||
<ThemeOption label="Gelap" value="dark" icon="moon-outline" />
|
<ThemeOption label="Gelap" value="dark" icon="moon-outline" />
|
||||||
<ThemeOption label="Sistem" value="system" icon="phone-portrait-outline" />
|
<ThemeOption label="Sistem" value="system" icon="phone-portrait-outline" />
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
|
<Text style={{ color: colors.icon, textAlign: 'center', marginTop: 'auto', fontSize: 12 }}>
|
||||||
|
Versi {Constants.expoConfig?.version}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import AuthProvider from '@/providers/AuthProvider';
|
import AuthProvider from '@/providers/AuthProvider';
|
||||||
import ThemeProvider, { useTheme } from '@/providers/ThemeProvider';
|
import ThemeProvider, { useTheme } from '@/providers/ThemeProvider';
|
||||||
|
import QueryProvider from '@/providers/QueryProvider';
|
||||||
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||||
|
import { flushErrorQueue } from '@/lib/errorLogger';
|
||||||
import { useFonts } from 'expo-font';
|
import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { AppState } from 'react-native';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { NotifierWrapper } from 'react-native-notifier';
|
import { NotifierWrapper } from 'react-native-notifier';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
@@ -21,7 +25,6 @@ function AppStack() {
|
|||||||
<>
|
<>
|
||||||
<Stack screenOptions={{ contentStyle: { backgroundColor: colors.header } }}>
|
<Stack screenOptions={{ contentStyle: { backgroundColor: colors.header } }}>
|
||||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="verification" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="(application)" options={{ headerShown: false }} />
|
<Stack.Screen name="(application)" options={{ headerShown: false }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
@@ -37,22 +40,34 @@ export default function RootLayout() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
SplashScreen.hideAsync();
|
SplashScreen.hideAsync();
|
||||||
|
flushErrorQueue();
|
||||||
}
|
}
|
||||||
}, [loaded]);
|
}, [loaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = AppState.addEventListener('change', (state) => {
|
||||||
|
if (state === 'active') flushErrorQueue();
|
||||||
|
});
|
||||||
|
return () => sub.remove();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={Styles.flex1}>
|
<GestureHandlerRootView style={Styles.flex1}>
|
||||||
<NotifierWrapper>
|
<ErrorBoundary>
|
||||||
<ThemeProvider>
|
<NotifierWrapper>
|
||||||
<AuthProvider>
|
<ThemeProvider>
|
||||||
<AppStack />
|
<QueryProvider>
|
||||||
</AuthProvider>
|
<AuthProvider>
|
||||||
</ThemeProvider>
|
<AppStack />
|
||||||
</NotifierWrapper>
|
</AuthProvider>
|
||||||
|
</QueryProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</NotifierWrapper>
|
||||||
|
</ErrorBoundary>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import { ButtonForm } from "@/components/buttonForm";
|
|
||||||
import Text from '@/components/Text';
|
|
||||||
import { ConstEnv } from "@/constants/ConstEnv";
|
|
||||||
import Styles from "@/constants/Styles";
|
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
|
||||||
import CryptoES from "crypto-es";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Image, View } from "react-native";
|
|
||||||
import { CodeField, Cursor, useBlurOnFulfill, useClearByFocusCell, } from 'react-native-confirmation-code-field';
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const [value, setValue] = useState('');
|
|
||||||
const ref = useBlurOnFulfill({ value, cellCount: 4 });
|
|
||||||
const [props, getCellOnLayoutHandler] = useClearByFocusCell({
|
|
||||||
value,
|
|
||||||
setValue,
|
|
||||||
});
|
|
||||||
const { colors } = useTheme();
|
|
||||||
|
|
||||||
const { signIn } = useAuthSession();
|
|
||||||
const login = (): void => {
|
|
||||||
// WARNING: This is a hardcoded bypass for development purposes.
|
|
||||||
// It should be removed or secured before production release.
|
|
||||||
if (__DEV__) {
|
|
||||||
const random: string = 'contohLoginMobileDarmasaba';
|
|
||||||
var mytexttoEncryption = "contohLoginMobileDarmasaba"
|
|
||||||
const encrypted = CryptoES.AES.encrypt(mytexttoEncryption, ConstEnv.pass_encrypt).toString();
|
|
||||||
signIn(encrypted);
|
|
||||||
} else {
|
|
||||||
console.warn("Bypass login disabled in production.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View style={[Styles.wrapLogin, { backgroundColor: colors.background }]} >
|
|
||||||
<View style={[Styles.rowItemsCenter, Styles.mv50]}>
|
|
||||||
<Image
|
|
||||||
source={require("../assets/images/logo.png")}
|
|
||||||
style={[{ width: 300, height: 150 }]}
|
|
||||||
width={270}
|
|
||||||
height={110}
|
|
||||||
/>
|
|
||||||
{/* <Text style={[Styles.textSubtitle]}>PERBEKEL DARMASABA</Text> */}
|
|
||||||
</View>
|
|
||||||
<View style={[Styles.mb30]}>
|
|
||||||
<Text style={[Styles.textDefaultSemiBold]}>Verifikasi Nomor Telepon</Text>
|
|
||||||
<Text style={[Styles.textMediumNormal]}>Masukkan kode yang kami kirimkan melalui WhatsApp</Text>
|
|
||||||
<Text style={[Styles.textMediumSemiBold]}>+628980185458</Text>
|
|
||||||
</View>
|
|
||||||
<CodeField
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
value={value}
|
|
||||||
rootStyle={{ width: '80%', alignSelf: 'center' }}
|
|
||||||
onChangeText={setValue}
|
|
||||||
cellCount={4}
|
|
||||||
keyboardType="number-pad"
|
|
||||||
renderCell={({ index, symbol, isFocused }) => (
|
|
||||||
<Text
|
|
||||||
key={index}
|
|
||||||
style={[Styles.verificationCell, isFocused && Styles.verificationFocusCell, { borderColor: isFocused ? colors.tint : colors.icon, color: colors.text }]}
|
|
||||||
onLayout={getCellOnLayoutHandler(index)}>
|
|
||||||
{symbol || (isFocused ? <Cursor /> : null)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ButtonForm
|
|
||||||
text="SUBMIT"
|
|
||||||
// onPress={() => { router.push("/home") }}
|
|
||||||
onPress={login}
|
|
||||||
/>
|
|
||||||
<Text style={[Styles.textInformation, Styles.mt05, Styles.cDefault, { textAlign: 'center', color: colors.tint }]}>
|
|
||||||
Tidak Menerima kode verifikasi? Kirim Ulang
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import Styles from '@/constants/Styles';
|
import Styles from '@/constants/Styles';
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { useRouter } from 'expo-router';
|
|
||||||
import { Platform, Text, View } from 'react-native';
|
import { Platform, Text, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import ButtonBackHeader from './buttonBackHeader';
|
import ButtonBackHeader from './buttonBackHeader';
|
||||||
@@ -15,13 +14,12 @@ type Props = {
|
|||||||
|
|
||||||
export default function AppHeader({ title, right, showBack = true, onPressLeft, left }: Props) {
|
export default function AppHeader({ title, right, showBack = true, onPressLeft, left }: Props) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const router = useRouter();
|
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.headerContainer, Platform.OS === 'ios' ? Styles.pb05 : Styles.pb13, { backgroundColor: colors.header, paddingTop: Platform.OS === 'ios' ? insets.top : 10 }]}>
|
<View style={[Styles.headerContainer, Platform.OS === 'ios' ? Styles.pb05 : Styles.pb13, { backgroundColor: colors.header, paddingTop: Platform.OS === 'ios' ? insets.top : 10 }]}>
|
||||||
<View style={Styles.headerApp}>
|
<View style={Styles.headerApp}>
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter, Styles.flex1]}>
|
||||||
{showBack ? (
|
{showBack ? (
|
||||||
<ButtonBackHeader onPress={onPressLeft} />
|
<ButtonBackHeader onPress={onPressLeft} />
|
||||||
) :
|
) :
|
||||||
@@ -30,7 +28,9 @@ export default function AppHeader({ title, right, showBack = true, onPressLeft,
|
|||||||
<View style={Styles.headerSide} />
|
<View style={Styles.headerSide} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text style={[Styles.headerTitle, Styles.ml05]}>{title}</Text>
|
<Text style={[Styles.headerTitle, Styles.ml05, Styles.flex1, Styles.mr10]} numberOfLines={1} ellipsizeMode="tail">
|
||||||
|
{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={Styles.headerSide}>{right}</View>
|
<View style={Styles.headerSide}>{right}</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
79
components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React, { Component, ReactNode } from 'react';
|
||||||
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends Component<Props, State> {
|
||||||
|
state: State = { hasError: false, error: null };
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error): void {
|
||||||
|
const { logError } = require('@/lib/errorLogger');
|
||||||
|
logError(error.message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReset = () => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) return this.props.fallback;
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text style={styles.title}>Terjadi Kesalahan</Text>
|
||||||
|
<Text style={styles.message}>
|
||||||
|
Silahkan coba lagi beberapa saat lagi atau hubungi admin untuk bantuan.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity style={styles.button} onPress={this.handleReset}>
|
||||||
|
<Text style={styles.buttonText}>Coba Lagi</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 24,
|
||||||
|
backgroundColor: '#f7f7f7',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 8,
|
||||||
|
color: '#11181C',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#707887',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#19345E',
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
42
components/ErrorView.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Text from '@/components/Text';
|
||||||
|
import Styles from '@/constants/Styles';
|
||||||
|
import { useTheme } from '@/providers/ThemeProvider';
|
||||||
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
|
interface ErrorViewProps {
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
icon?: keyof typeof MaterialCommunityIcons.glyphMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ErrorView component to display error or empty states.
|
||||||
|
* Used when data is not found, deleted, or an error occurs during fetching.
|
||||||
|
*/
|
||||||
|
export default function ErrorView({
|
||||||
|
title = "Terjadi Kesalahan",
|
||||||
|
message = "Data tidak ditemukan atau sudah dihapus.",
|
||||||
|
icon = "alert-circle-outline"
|
||||||
|
}: ErrorViewProps) {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[Styles.flex1, Styles.contentItemCenter, Styles.ph20]}>
|
||||||
|
<View style={[Styles.mb10]}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={icon}
|
||||||
|
size={40}
|
||||||
|
color={colors.dimmed}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text, textAlign: 'center' }]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, textAlign: 'center', marginTop: 4 }]}>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
components/ModalUpdateMaintenance.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Modal, View, Image, TouchableOpacity, BackHandler, Platform } from 'react-native';
|
||||||
|
import { useTheme } from '@/providers/ThemeProvider';
|
||||||
|
import Text from './Text';
|
||||||
|
import * as Linking from 'expo-linking';
|
||||||
|
import Styles from '@/constants/Styles';
|
||||||
|
|
||||||
|
interface ModalUpdateMaintenanceProps {
|
||||||
|
visible: boolean;
|
||||||
|
type: 'update' | 'maintenance';
|
||||||
|
isForceUpdate?: boolean;
|
||||||
|
onDismiss?: () => void;
|
||||||
|
appName?: string;
|
||||||
|
customDescription?: string;
|
||||||
|
androidStoreUrl?: string;
|
||||||
|
iosStoreUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalUpdateMaintenance: React.FC<ModalUpdateMaintenanceProps> = ({
|
||||||
|
visible,
|
||||||
|
type,
|
||||||
|
isForceUpdate = false,
|
||||||
|
onDismiss,
|
||||||
|
appName = 'Desa+',
|
||||||
|
customDescription,
|
||||||
|
androidStoreUrl = 'https://play.google.com/store/apps/details?id=mobiledarmasaba.app',
|
||||||
|
iosStoreUrl = 'https://apps.apple.com/id/app/desa-plus-desa/id6752375538'
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const handleUpdate = () => {
|
||||||
|
const storeUrl = Platform.OS === 'ios' ? iosStoreUrl : androidStoreUrl;
|
||||||
|
Linking.openURL(storeUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseApp = () => {
|
||||||
|
// For maintenance mode, we might want to exit the app or just keep the modal.
|
||||||
|
// React Native doesn't have a built-in "exit" for iOS, but for Android:
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
BackHandler.exitApp();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
animationType="fade"
|
||||||
|
transparent={false}
|
||||||
|
onRequestClose={() => {
|
||||||
|
if (!isForceUpdate && type === 'update') {
|
||||||
|
onDismiss?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={[Styles.modalUpdateContainer, { backgroundColor: colors.primary }]}>
|
||||||
|
{/* Background decorative circles could be added here if we had SVGs or images */}
|
||||||
|
<View style={Styles.modalUpdateDecorativeCircle1} />
|
||||||
|
<View style={Styles.modalUpdateDecorativeCircle2} />
|
||||||
|
|
||||||
|
<View style={Styles.modalUpdateContent}>
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/logo-dark.png')}
|
||||||
|
style={Styles.modalUpdateLogo}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View style={Styles.modalUpdateTextContainer}>
|
||||||
|
<Text style={Styles.modalUpdateTitle}>
|
||||||
|
{type === 'update' ? 'Update Tersedia' : 'Perbaikan'}
|
||||||
|
</Text>
|
||||||
|
<Text style={[Styles.modalUpdateDescription, { opacity: 0.8 }]}>
|
||||||
|
{customDescription ? customDescription :
|
||||||
|
(type === 'update'
|
||||||
|
? `Versi terbaru dari ${appName} tersedia di Store. Silakan buka Store untuk menginstalnya.`
|
||||||
|
: 'Aplikasi saat ini sedang dalam pemeliharaan untuk peningkatan sistem. Silakan coba kembali beberapa saat lagi.')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={Styles.modalUpdateButtonContainer}>
|
||||||
|
{type === 'update' ? (
|
||||||
|
<>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[Styles.modalUpdatePrimaryButton, { backgroundColor: 'white' }]}
|
||||||
|
onPress={handleUpdate}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Text style={[Styles.modalUpdatePrimaryButtonText, { color: colors.primary }]}>
|
||||||
|
Update
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{!isForceUpdate && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={Styles.modalUpdateSecondaryButton}
|
||||||
|
onPress={onDismiss}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={Styles.modalUpdateSecondaryButtonText}>Nanti</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
// <TouchableOpacity
|
||||||
|
// style={[Styles.modalUpdatePrimaryButton, { backgroundColor: 'white' }]}
|
||||||
|
// onPress={handleCloseApp}
|
||||||
|
// activeOpacity={0.8}
|
||||||
|
// >
|
||||||
|
// <Text style={[Styles.modalUpdatePrimaryButtonText, { color: colors.primary }]}>
|
||||||
|
// {Platform.OS === 'android' ? 'Close App' : 'Please check back later'}
|
||||||
|
// </Text>
|
||||||
|
// </TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalUpdateMaintenance;
|
||||||
@@ -22,7 +22,7 @@ export default function ViewLogin({ onValidate }: Props) {
|
|||||||
const [disableLogin, setDisableLogin] = useState(true)
|
const [disableLogin, setDisableLogin] = useState(true)
|
||||||
const [phone, setPhone] = useState('')
|
const [phone, setPhone] = useState('')
|
||||||
const { signIn, encryptToken } = useAuthSession();
|
const { signIn, encryptToken } = useAuthSession();
|
||||||
const { colors, theme } = useTheme();
|
const { colors, activeTheme } = useTheme();
|
||||||
|
|
||||||
const handleCheckPhone = async () => {
|
const handleCheckPhone = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -38,13 +38,18 @@ export default function ViewLogin({ onValidate }: Props) {
|
|||||||
if (responseOtp == 200) {
|
if (responseOtp == 200) {
|
||||||
await AsyncStorage.setItem('user', response.id)
|
await AsyncStorage.setItem('user', response.id)
|
||||||
return onValidate({ phone: `62${phone}`, otp })
|
return onValidate({ phone: `62${phone}`, otp })
|
||||||
|
} else {
|
||||||
|
return Toast.show({ type: 'small', text1: 'Gagal mengirim kode verifikasi', position: 'bottom' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Toast.show({ type: 'small', text1: response.message, position: 'bottom' })
|
return Toast.show({ type: 'small', text1: response.message, position: 'bottom' })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
return Toast.show({ type: 'small', text1: `Terjadi kesalahan, coba lagi`, position: 'bottom' })
|
console.error(error);
|
||||||
|
const message = error?.response?.data?.message || "Gagal login"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingLogin(false)
|
setLoadingLogin(false)
|
||||||
}
|
}
|
||||||
@@ -52,11 +57,11 @@ export default function ViewLogin({ onValidate }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<StatusBar style={theme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
|
<StatusBar style={activeTheme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
|
||||||
<View style={[Styles.p20, Styles.h100]}>
|
<View style={[Styles.p20, Styles.h100]}>
|
||||||
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
|
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
|
||||||
<Image
|
<Image
|
||||||
source={theme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
|
source={activeTheme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
|
||||||
style={[{ width: 300, height: 150 }]}
|
style={[{ width: 300, height: 150 }]}
|
||||||
width={270}
|
width={270}
|
||||||
height={110}
|
height={110}
|
||||||
@@ -82,7 +87,7 @@ export default function ViewLogin({ onValidate }: Props) {
|
|||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
<View style={{ alignItems: 'center' }}>
|
<View style={{ alignItems: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
source={theme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
|
source={activeTheme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
|
||||||
style={{ width: 86, height: 27 }}
|
style={{ width: 86, height: 27 }}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function ViewVerification({ phone, otp }: Props) {
|
|||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [otpFix, setOtpFix] = useState(otp)
|
const [otpFix, setOtpFix] = useState(otp)
|
||||||
const { signIn, encryptToken } = useAuthSession();
|
const { signIn, encryptToken } = useAuthSession();
|
||||||
const { colors, theme } = useTheme();
|
const { colors, activeTheme } = useTheme();
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
const valueUser = await AsyncStorage.getItem('user');
|
const valueUser = await AsyncStorage.getItem('user');
|
||||||
@@ -59,11 +59,11 @@ export default function ViewVerification({ phone, otp }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||||
<StatusBar style={theme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
|
<StatusBar style={activeTheme === 'dark' ? 'light' : 'dark'} translucent={false} backgroundColor={colors.background} />
|
||||||
<View style={[Styles.p20, Styles.h100]} >
|
<View style={[Styles.p20, Styles.h100]} >
|
||||||
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
|
<View style={{ alignItems: "center", marginTop: 70, marginBottom: 50 }}>
|
||||||
<Image
|
<Image
|
||||||
source={theme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
|
source={activeTheme === 'dark' ? require("../../assets/images/logo-dark.png") : require("../../assets/images/logo.png")}
|
||||||
style={[{ width: 300, height: 150 }]}
|
style={[{ width: 300, height: 150 }]}
|
||||||
width={270}
|
width={270}
|
||||||
height={110}
|
height={110}
|
||||||
@@ -101,7 +101,7 @@ export default function ViewVerification({ phone, otp }: Props) {
|
|||||||
<View style={{ flex: 1 }} />
|
<View style={{ flex: 1 }} />
|
||||||
<View style={[{ alignItems: 'center' }]}>
|
<View style={[{ alignItems: 'center' }]}>
|
||||||
<Image
|
<Image
|
||||||
source={theme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
|
source={activeTheme === 'dark' ? require("../../assets/images/cogniti-dark.png") : require("../../assets/images/cogniti-light.png")}
|
||||||
style={{ width: 86, height: 27 }}
|
style={{ width: 86, height: 27 }}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function BorderBottomItem({ title, subtitle, icon, desc, onPress,
|
|||||||
{icon}
|
{icon}
|
||||||
<View style={[Styles.rowSpaceBetween, Styles.flex1]}>
|
<View style={[Styles.rowSpaceBetween, Styles.flex1]}>
|
||||||
<View style={[Styles.ml10, Styles.flex1, Styles.mr10]}>
|
<View style={[Styles.ml10, Styles.flex1, Styles.mr10]}>
|
||||||
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title}</Text>
|
<Text style={[titleWeight == 'normal' ? Styles.textDefault : Styles.textDefaultSemiBold, { color: textColorFix }]} numberOfLines={titleShowAll ? 0 : 1} ellipsizeMode='tail'>{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}</Text>
|
||||||
{
|
{
|
||||||
subtitle &&
|
subtitle &&
|
||||||
typeof subtitle == "string"
|
typeof subtitle == "string"
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ export default function HeaderRightCalendarDetail({ id, idReminder }: Props) {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal menghapus data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ export default function HeaderRightDiscussionDetail({ id, status, isActive }: Pr
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
}
|
}
|
||||||
@@ -59,9 +61,11 @@ export default function HeaderRightDiscussionDetail({ id, status, isActive }: Pr
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function DiscussionItem({ title, user, date, onPress }: Props) {
|
|||||||
<View style={[Styles.rowItemsCenter, Styles.mb10]}>
|
<View style={[Styles.rowItemsCenter, Styles.mb10]}>
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={22} color={colors.text} style={Styles.mr10} />
|
<Ionicons name="chatbox-ellipses-outline" size={22} color={colors.text} style={Styles.mr10} />
|
||||||
<View style={[{ flex: 1 }]}>
|
<View style={[{ flex: 1 }]}>
|
||||||
<Text style={{ fontWeight: 'bold' }} numberOfLines={1} ellipsizeMode="tail">{title}</Text>
|
<Text style={{ fontWeight: 'bold' }} numberOfLines={1} ellipsizeMode="tail">{title?.charAt(0).toUpperCase() + title?.slice(1)}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.rowSpaceBetween]}>
|
<View style={[Styles.rowSpaceBetween]}>
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ export default function HeaderRightDivisionInfo({ id, active }: Props) {
|
|||||||
} else {
|
} else {
|
||||||
Toast.show({ type: 'small', text1: response.message, })
|
Toast.show({ type: 'small', text1: response.message, })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error : any ) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
Toast.show({ type: 'small', text1: 'Terjadi kesalahan', })
|
const message = error?.response?.data?.message || "Gagal mengubah status"
|
||||||
|
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
} finally {
|
} finally {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default function HeaderRightDivisionList() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
(entityUser.role == "userRole" || entityUser.role == "developer") &&
|
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
||||||
<MenuItemRow
|
<MenuItemRow
|
||||||
icon={<AntDesign name="filter" color={colors.text} size={25} />}
|
icon={<AntDesign name="filter" color={colors.text} size={25} />}
|
||||||
title="Filter"
|
title="Filter"
|
||||||
|
|||||||