fix-bug #59

Merged
bagasbanuna merged 2 commits from fix-bug/4-mar-26 into staging 2026-03-04 16:46:32 +08:00
13 changed files with 336 additions and 190 deletions

View File

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

View File

@@ -1,66 +1,100 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
import HeaderBell from "@/screens/Home/HeaderBell";
import { stylesHome } from "@/screens/Home/homeViewStyle";
import Home_ImageSection from "@/screens/Home/imageSection";
import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList";
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
import { RefreshControl, View } from "react-native";
export default function Application() {
const { token, user, userData } = useAuth();
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore();
const [listData, setListData] = useState<any[] | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataJob();
checkVersion();
userData(token as string);
userData(token as string).catch((error) => {
console.log("[ERROR userData]", error?.message);
console.log("[ERROR userData Response]", error?.response?.data);
});
syncUnreadCount();
}, [user?.id, token]),
);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log(
"[Profile ID]>>",
JSON.stringify(response?.data?.Profile?.id, null, 2),
);
setData(response.data);
try {
const response = await apiUser(user?.id as string);
setData(response.data);
} catch (error: any) {
console.log("[ERROR onLoadData]", error?.message);
console.log("[ERROR Response]", error?.response?.data);
// Set data tetap agar UI tidak stuck di loading
setData(null);
}
}
const onLoadDataJob = async () => {
try {
const response = await apiJobGetAll({
category: "beranda",
});
const result = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
.slice(0, 2);
setListData(result);
} catch (error) {
console.log("[ERROR]", error);
}
};
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const checkVersion = async () => {
const response = await apiVersion();
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
try {
const response = await apiVersion();
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
} catch (error: any) {
console.log("[ERROR checkVersion]", error?.message);
}
};
const onRefresh = useCallback(() => {
setRefreshing(true);
onLoadData();
onLoadDataJob();
checkVersion();
setRefreshing(false);
}, []);
// if (user && user?.termsOfServiceAccepted === false) {
// console.log("User is not accept term service");
// return <Redirect href={`/terms-agreement`} />;
// }
if (data && data?.active === false) {
console.log("User is not active");
console.warn("User is not active");
return (
<BasicWrapper>
<Redirect href={`/waiting-room`} />
@@ -69,7 +103,7 @@ export default function Application() {
}
if (data && data?.Profile === null) {
console.log("Profile is null");
console.warn("Profile is null");
return (
<BasicWrapper>
<Redirect href={`/profile/create`} />
@@ -91,17 +125,25 @@ export default function Application() {
<Stack.Screen
options={{
title: `HIPMI`,
headerLeft: () => (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
),
headerRight: () => <HeaderBell />,
headerLeft: () =>
data ? (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
headerRight: () =>
data ? (
<HeaderBell />
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
}}
/>
<ViewWrapper
@@ -114,25 +156,51 @@ export default function Application() {
/>
}
footerComponent={
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
/>
data && data ? (
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
/>
) : (
<View style={GStyles.tabBar}>
<View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
{Array.from({ length: 4 }).map((e, index) => (
<CustomSkeleton
key={index}
height={40}
width={40}
radius={100}
/>
))}
</View>
</View>
)
}
>
<StackCustom>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
<Home_ImageSection />
<Home_FeatureSection />
{data && data ? (
<Home_FeatureSection />
) : (
<View style={stylesHome.gridContainer}>
{Array.from({ length: 4 }).map((item, index) => (
<CustomSkeleton
key={index}
style={stylesHome.gridItem}
radius={50}
/>
))}
</View>
)}
<Home_BottomFeatureSection />
{data ? (
<Home_BottomFeatureSection listData={listData} />
) : (
<CustomSkeleton height={200} />
)}
</StackCustom>
</ViewWrapper>
</>

View File

@@ -10,6 +10,7 @@ import {
} from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
@@ -64,6 +65,8 @@ export default function Portofolio() {
setProfileId(response?.data?.Profile?.id);
};
return (
<>
{/* Header */}
@@ -87,7 +90,10 @@ export default function Portofolio() {
/>
<ViewWrapper>
{!data || !profileId ? (
<LoaderCustom />
<StackCustom>
<CustomSkeleton height={400} />
<CustomSkeleton height={300} />
</StackCustom>
) : (
<StackCustom>
<Portofolio_Data

View File

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

View File

@@ -9,32 +9,14 @@ import {
ViewWrapper,
} from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import API_IMAGE from "@/constants/api-storage";
import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { View } from "react-native";
import MapView, { Marker } from "react-native-maps";
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
height: 300,
};
export interface LocationItem {
id: string | number;
latitude: number;
longitude: number;
name: string;
imageId?: string;
}
export default function AdminMaps() {
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
@@ -72,74 +54,30 @@ export default function AdminMaps() {
}
};
const markers: MapMarker[] = list?.map((item) => ({
id: item.id,
coordinate: [item.longitude, item.latitude] as [number, number],
imageId: item.Portofolio?.logoId,
onSelected: () => {
setOpenDrawer(true);
setSelected({
id: item?.id,
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
nomorTelepon: item?.Portofolio?.tlpn,
alamatBisnis: item?.Portofolio?.alamatKantor,
namePin: item?.namePin,
imageId: item?.imageId,
portofolioId: item?.Portofolio?.id,
latitude: item?.latitude,
longitude: item?.longitude,
});
},
})) || [];
return (
<>
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
{/* <MapCustom height={"100%"} /> */}
<View style={{ flex: 1 }}>
{loadList ? (
<MapView
initialRegion={defaultRegion}
style={{
width: "100%",
height: "100%",
}}
/>
) : (
<MapView
initialRegion={defaultRegion}
style={{
width: "100%",
height: "100%",
}}
>
{list?.map((item: any, index: number) => {
return (
<Marker
key={item?.id}
coordinate={{
latitude: item?.latitude,
longitude: item?.longitude,
}}
title={item?.namePin}
onPress={() => {
setOpenDrawer(true);
setSelected({
id: item?.id,
bidangBisnis:
item?.Portofolio?.MasterBidangBisnis?.name,
nomorTelepon: item?.Portofolio?.tlpn,
alamatBisnis: item?.Portofolio?.alamatKantor,
namePin: item?.namePin,
imageId: item?.imageId,
portofolioId: item?.Portofolio?.id,
latitude: item?.latitude,
longitude: item?.longitude,
});
}}
// Gunakan gambar kustom jika tersedia
>
<View>
<Image
source={{
uri: API_IMAGE.GET({
fileId: item?.Portofolio?.logoId,
}),
}}
style={{
width: 30,
height: 30,
borderRadius: 100,
borderWidth: 1,
}}
/>
</View>
</Marker>
);
})}
</MapView>
)}
</View>
<MapsV2Custom markers={markers} />
</ViewWrapper>
<DrawerCustom
@@ -147,7 +85,9 @@ export default function AdminMaps() {
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<DummyLandscapeImage height={200} imageId={selected.imageId} />
{selected.imageId && (
<DummyLandscapeImage height={200} imageId={selected.imageId} />
)}
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView

View File

@@ -77,8 +77,12 @@ export default function NotificationInitializer() {
});
console.log("✅ Device token berhasil didaftarkan ke backend");
} catch (error) {
console.error("❌ Gagal mendaftarkan device token:", error);
} catch (error: any) {
// Log error detail tapi jangan crash aplikasi
console.error("❌ Gagal mendaftarkan device token:", error?.message);
console.error("Response status:", error?.response?.status);
console.error("Response data:", error?.response?.data);
// Skip logout - biarkan user tetap bisa pakai app meski notif gagal
}
};

View File

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

View File

@@ -24,7 +24,7 @@ export {
// OS Height
const OS_ANDROID_HEIGHT = 115
const OS_IOS_HEIGHT = 90
const OS_IOS_HEIGHT = 80
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
// Text Size

View File

@@ -171,6 +171,13 @@
EEC6AC8AF9C04E91AA81C190 /* Remove signature files (Xcode workaround) */,
D2BED766D85C4781B154BD69 /* Remove signature files (Xcode workaround) */,
E01278D305D540D5B29ED50A /* Remove signature files (Xcode workaround) */,
72EDC26CA2144B90BEFE947F /* Remove signature files (Xcode workaround) */,
0A09E19272A94BEBAAF5A27A /* Remove signature files (Xcode workaround) */,
9B007D2599C64C7F8F525B86 /* Remove signature files (Xcode workaround) */,
1393AE9C86924FA8B1F8D11E /* Remove signature files (Xcode workaround) */,
E1F9AE3DCABE4A088A05E180 /* Remove signature files (Xcode workaround) */,
211F6E22A1B24524B67693F8 /* Remove signature files (Xcode workaround) */,
469F2CAA8928481CA86EB0F4 /* Remove signature files (Xcode workaround) */,
);
buildRules = (
);
@@ -779,6 +786,125 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
72EDC26CA2144B90BEFE947F /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
0A09E19272A94BEBAAF5A27A /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
9B007D2599C64C7F8F525B86 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
1393AE9C86924FA8B1F8D11E /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
E1F9AE3DCABE4A088A05E180 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
211F6E22A1B24524B67693F8 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
469F2CAA8928481CA86EB0F4 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

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

View File

@@ -1,45 +1,15 @@
import { ClickableCustom, TextCustom } from "@/components";
import Spacing from "@/components/_ShareComponent/Spacing";
import React, { useCallback, useState } from "react";
import { router } from "expo-router";
import { View } from "react-native";
import Icon from "react-native-vector-icons/FontAwesome";
import { stylesHome } from "./homeViewStyle";
import { router, useFocusEffect } from "expo-router";
import { apiJobGetAll } from "@/service/api-client/api-job";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
export default function Home_BottomFeatureSection() {
const [listData, setListData] = useState<any[] | null>(null);
const onLoadData = async () => {
try {
const response = await apiJobGetAll({
category: "beranda",
});
console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
const result = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
.slice(0, 2);
setListData(result);
} catch (error) {
console.log("[ERROR]", error);
}
};
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
if (listData === null) {
return <CustomSkeleton height={200}/>
}
export default function Home_BottomFeatureSection({
listData,
}: {
listData: any[] | null;
}) {
return (
<>
<ClickableCustom onPress={() => router.push("/job")}>

View File

@@ -139,8 +139,8 @@ export default function UserSearchMainView_V2() {
searchQuery: search,
emptyMessage: "Tidak ada pengguna ditemukan",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: 5,
skeletonHeight: 150,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
loadingFooterText: "Memuat lebih banyak pengguna...",
isInitialLoad,
});

View File

@@ -15,13 +15,14 @@ export async function apiDeviceRegisterToken({
data: DeviceTokenData;
}) {
try {
const response = await apiConfig.post(`/mobile/auth/device-tokens`, {
data: data,
});
const response = await apiConfig.post(`/mobile/auth/device-tokens`, data);
return response.data;
} catch (error) {
} catch (error: any) {
console.error("Failed to register device token:", error);
console.error("Response data:", error?.response?.data);
console.error("Response status:", error?.response?.status);
console.error("Request payload:", data);
throw error;
}
}