diff --git a/app/(application)/(user)/maps/[id]/edit.tsx b/app/(application)/(user)/maps/[id]/edit.tsx index 2688034..75f8894 100644 --- a/app/(application)/(user)/maps/[id]/edit.tsx +++ b/app/(application)/(user)/maps/[id]/edit.tsx @@ -1,54 +1,226 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { - BaseBox, BoxButtonOnFooter, ButtonCenteredOnly, ButtonCustom, InformationBox, LandscapeFrameUploaded, - MapCustom, Spacing, TextInputCustom, - ViewWrapper + ViewWrapper, } from "@/components"; -import { router, useLocalSearchParams } from "expo-router"; +import API_IMAGE from "@/constants/api-storage"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiMapsGetOne, apiMapsUpdate } from "@/service/api-client/api-maps"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import { StyleSheet, View } from "react-native"; +import MapView, { LatLng, Marker } from "react-native-maps"; +import Toast from "react-native-toast-message"; +const defaultRegion = { + latitude: -8.737109, + longitude: 115.1756897, + latitudeDelta: 0.1, + longitudeDelta: 0.1, +}; export default function MapsEdit() { const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + id: "", + namePin: "", + latitude: "", + longitude: "", + imageId: "", + }); + const [selectedLocation, setSelectedLocation] = useState(null); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiMapsGetOne({ id: id as string }); + + if (response.success) { + setData({ + id: response.data.id, + namePin: response.data.namePin, + latitude: response.data.latitude, + longitude: response.data.longitude, + imageId: response.data.imageId, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handleMapPress = (event: any) => { + const { latitude, longitude } = event.nativeEvent.coordinate; + const location = { latitude, longitude }; + setSelectedLocation(location); + }; + + const handleSubmit = async () => { + let newData: any; + if (!data.namePin) { + Toast.show({ + type: "error", + text1: "Nama pin harus diisi", + }); + return; + } + + newData = { + namePin: data?.namePin, + latitude: selectedLocation?.latitude || data?.latitude, + longitude: selectedLocation?.longitude || data?.longitude, + }; + + try { + setLoading(true); + if (image) { + const responseUpload = await uploadFileService({ + dirId: DIRECTORY_ID.map_image, + imageUri: image?.uri, + }); + + if (!responseUpload?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUpload?.data?.id; + + newData = { + namePin: data?.namePin, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + newImageId: imageId, + }; + } + + const responseUpdate = await apiMapsUpdate({ + id: data?.id, + data: newData, + }); + + if (!responseUpdate.success) { + Toast.show({ + type: "error", + text1: "Gagal mengupdate map", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Map berhasil diupdate", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + const buttonFooter = ( { - console.log(`Simpan maps ${id}`); - router.back() - }} + disabled={!data.namePin} + onPress={handleSubmit} + isLoading={isLoading} > - Simpan + Update ); + return ( - - - + + + {selectedLocation ? ( + + ) : ( + + )} + + setData({ ...data, namePin: value })} /> - + { - console.log("Upload foto "); - router.navigate(`/take-picture/${id}`); + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); }} > Upload @@ -57,3 +229,16 @@ export default function MapsEdit() { ); } + +const styles = StyleSheet.create({ + container: { + width: "100%", + backgroundColor: "#f5f5f5", + overflow: "hidden", + borderRadius: 8, + marginBottom: 20, + }, + map: { + flex: 1, + }, +}); diff --git a/app/(application)/(user)/maps/create.tsx b/app/(application)/(user)/maps/create.tsx index f9b7f40..2c8416b 100644 --- a/app/(application)/(user)/maps/create.tsx +++ b/app/(application)/(user)/maps/create.tsx @@ -6,21 +6,96 @@ import { InformationBox, LandscapeFrameUploaded, Spacing, - TextCustom, TextInputCustom, ViewWrapper, } from "@/components"; +import MapSelected from "@/components/Map/MapSelected"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useAuth } from "@/hooks/use-auth"; +import { apiMapsCreate } from "@/service/api-client/api-maps"; +import { uploadFileService } from "@/service/upload-service"; +import pickFile, { IFileData } from "@/utils/pickFile"; import { router, useLocalSearchParams } from "expo-router"; +import { useState } from "react"; +import { LatLng } from "react-native-maps"; +import Toast from "react-native-toast-message"; export default function MapsCreate() { + const { user } = useAuth(); const { id } = useLocalSearchParams(); + const [selectedLocation, setSelectedLocation] = useState(null); + const [name, setName] = useState(""); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + const handleSubmit = async () => { + try { + setLoading(true); + let newData: any; + newData = { + authorId: user?.id, + portofolioId: id, + namePin: name, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + }; + + if (image) { + const responseUpload = await uploadFileService({ + dirId: DIRECTORY_ID.map_image, + imageUri: image?.uri, + }); + + if (!responseUpload?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUpload?.data?.id; + + newData = { + authorId: user?.id, + portofolioId: id, + namePin: name, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + imageId: imageId, + }; + } + + const response = await apiMapsCreate({ + data: newData, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal menambahkan map", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Map berhasil ditambahkan", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + const buttonFooter = ( { - console.log(`Simpan maps ${id}`); - router.replace(`/portofolio/${id}`); - }} + isLoading={isLoading} + disabled={!selectedLocation || name === ""} + onPress={handleSubmit} > Simpan @@ -30,25 +105,34 @@ export default function MapsCreate() { - - Maps Her + + - + { - console.log("Upload foto "); - router.navigate(`/take-picture/${id}`); + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); }} > Upload diff --git a/app/(application)/(user)/maps/index.tsx b/app/(application)/(user)/maps/index.tsx index 87e5c5d..90ba6eb 100644 --- a/app/(application)/(user)/maps/index.tsx +++ b/app/(application)/(user)/maps/index.tsx @@ -1,19 +1,234 @@ -import { MapCustom, ViewWrapper } from "@/components"; +import { + ButtonCustom, + DrawerCustom, + DummyLandscapeImage, + Grid, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import GridTwoView from "@/components/_ShareComponent/GridTwoView"; +import API_IMAGE from "@/constants/api-storage"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; +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 from "react-native-maps"; +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 Maps() { + const [list, setList] = useState(null); + const [loadList, setLoadList] = useState(false); + const [openDrawer, setOpenDrawer] = useState(false); + const [selected, setSelected] = useState({ + id: "", + bidangBisnis: "", + nomorTelepon: "", + alamatBisnis: "", + namePin: "", + imageId: "", + portofolioId: "", + latitude: 0, + longitude: 0, + }); + + useFocusEffect( + useCallback(() => { + handlerLoadList(); + }, []) + ); + + const handlerLoadList = async () => { + try { + setLoadList(true); + const response = await apiMapsGetAll(); + + if (response.success) { + setList(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadList(false); + } + }; + return ( - - {/* */} - - - - + <> + + {/* */} + + {loadList ? ( + + ) : ( + + {list?.map((item: any, index: number) => { + return ( + { + 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 + > + + + + + ); + })} + + )} + + + + setOpenDrawer(false)} + height={"auto"} + > + + + + + } + rightIcon={{selected.namePin}} + /> + + + } + rightIcon={{selected.bidangBisnis}} + /> + + + } + rightIcon={{selected.nomorTelepon}} + /> + + } + rightIcon={{selected.alamatBisnis}} + /> + + + + { + setOpenDrawer(false); + router.push(`/portofolio/${selected.portofolioId}`); + }} + > + Detail + + + + { + openInDeviceMaps({ + latitude: selected.latitude, + longitude: selected.longitude, + title: selected.namePin, + }); + }} + > + Buka Maps + + + + + + ); } diff --git a/app/(application)/(user)/portofolio/[id]/index.tsx b/app/(application)/(user)/portofolio/[id]/index.tsx index adf146c..5e9286f 100644 --- a/app/(application)/(user)/portofolio/[id]/index.tsx +++ b/app/(application)/(user)/portofolio/[id]/index.tsx @@ -1,8 +1,18 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { DrawerCustom, LoaderCustom, Spacing, StackCustom } from "@/components"; +import { + ButtonCustom, + DrawerCustom, + DummyLandscapeImage, + LoaderCustom, + Spacing, + StackCustom, + TextCustom, +} from "@/components"; import LeftButtonCustom from "@/components/Button/BackButton"; +import GridTwoView from "@/components/_ShareComponent/GridTwoView"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { useAuth } from "@/hooks/use-auth"; import Portofolio_BusinessLocation from "@/screens/Portofolio/BusinessLocationSection"; import Portofolio_ButtonDelete from "@/screens/Portofolio/ButtonDelete"; @@ -13,19 +23,20 @@ import Portofolio_SocialMediaSection from "@/screens/Portofolio/SocialMediaSecti import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio"; import { apiUser } from "@/service/api-client/api-user"; import { GStyles } from "@/styles/global-styles"; -import { Ionicons } from "@expo/vector-icons"; +import { openInDeviceMaps } from "@/utils/openInDeviceMaps"; +import { FontAwesome, Ionicons } from "@expo/vector-icons"; import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; import { useCallback, useState } from "react"; import { TouchableOpacity } from "react-native"; export default function Portofolio() { + const { user } = useAuth(); const { id } = useLocalSearchParams(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isLoadingDelete, setIsLoadingDelete] = useState(false); const [data, setData] = useState(); const [profileId, setProfileId] = useState(); - - const { user } = useAuth(); + const [openDrawerLocation, setOpenDrawerLocation] = useState(false); const openDrawer = () => { setIsDrawerOpen(true); @@ -43,19 +54,13 @@ export default function Portofolio() { async function onLoadData(id: string) { const response = await apiGetOnePortofolio({ id: id }); - console.log( - "[PROFILE ID]>>", - JSON.stringify(response.data.Profile.id, null, 2) - ); + setData(response.data); } const onLoadUserByToken = async () => { const response = await apiUser(user?.id as string); - console.log( - "[PROFILE LOGIN]>>", - JSON.stringify(response.data?.Profile.id, null, 2) - ); + setProfileId(response?.data?.Profile?.id); }; @@ -89,7 +94,11 @@ export default function Portofolio() { data={data} listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]} /> - + @@ -110,10 +119,93 @@ export default function Portofolio() { height={"auto"} > + + {/* Drawer Lokasi */} + setOpenDrawerLocation(false)} + height={"auto"} + > + + + + + } + rightIcon={{data?.BusinessMaps?.namePin}} + /> + + + } + rightIcon={ + {data?.MasterBidangBisnis?.name} + } + /> + + + } + rightIcon={{data?.tlpn}} + /> + + } + rightIcon={{data?.alamatKantor}} + /> + + + { + openInDeviceMaps({ + latitude: data?.BusinessMaps?.latitude, + longitude: data?.BusinessMaps?.longitude, + title: data?.BusinessMaps?.namePin, + }); + }} + > + Buka Maps + + + ); } diff --git a/components/Map/MapCustom.tsx b/components/Map/MapCustom.tsx index 318cc69..d4fd1b9 100644 --- a/components/Map/MapCustom.tsx +++ b/components/Map/MapCustom.tsx @@ -1,5 +1,7 @@ // components/MapComponent.js +import API_IMAGE from "@/constants/api-storage"; +import { Image } from "expo-image"; import React from "react"; import { DimensionValue, StyleSheet, View } from "react-native"; import MapView, { Marker } from "react-native-maps"; @@ -10,6 +12,9 @@ interface MapComponentProps { latitudeDelta?: number; longitudeDelta?: number; height?: DimensionValue; + namePin?: string; + imageId?: string; + onPress?: () => void; } const MapCustom = ({ @@ -18,6 +23,9 @@ const MapCustom = ({ latitudeDelta = 0.1, longitudeDelta = 0.1, height = 300, + namePin = "Bali", + imageId, + onPress, }: MapComponentProps) => { const initialRegion = { latitude, @@ -40,9 +48,22 @@ const MapCustom = ({ latitude, longitude, }} - title="Bali" - description="Badung, Bali, Indonesia" - /> + title={namePin} + onPress={onPress} + // Gunakan gambar kustom jika tersedia + > + + + + ); diff --git a/components/Map/MapSelected.tsx b/components/Map/MapSelected.tsx new file mode 100644 index 0000000..2631255 --- /dev/null +++ b/components/Map/MapSelected.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { StyleSheet, View } from "react-native"; +import MapView, { LatLng, Marker } from "react-native-maps"; + +interface MapSelectedProps { + initialRegion?: { + latitude?: number; + longitude?: number; + latitudeDelta?: number; + longitudeDelta?: number; + }; + onLocationSelect?: (location: LatLng) => void; + height?: number; // Opsional: tinggi peta dalam piksel + selectedLocation: LatLng; + setSelectedLocation: (location: LatLng) => void; +} + +const MapSelected: React.FC = ({ + initialRegion, + onLocationSelect, + selectedLocation, + setSelectedLocation, + height = 400, // Default height: 400 +}) => { + const handleMapPress = (event: any) => { + const { latitude, longitude } = event.nativeEvent.coordinate; + const location = { latitude, longitude }; + setSelectedLocation(location); + onLocationSelect?.(location); + }; + + // Default region sesuai permintaan + const defaultRegion = { + latitude: -8.737109, + longitude: 115.1756897, + latitudeDelta: 0.1, + longitudeDelta: 0.1, + }; + + return ( + + + {selectedLocation && ( + + )} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + width: "100%", + backgroundColor: "#f5f5f5", + overflow: "hidden", + borderRadius: 8, + }, + map: { + flex: 1, + }, +}); + +export default MapSelected; diff --git a/components/_ShareComponent/GridTwoView.tsx b/components/_ShareComponent/GridTwoView.tsx new file mode 100644 index 0000000..0aaabb4 --- /dev/null +++ b/components/_ShareComponent/GridTwoView.tsx @@ -0,0 +1,29 @@ +import { StyleProp, ViewStyle } from "react-native"; +import Grid from "../Grid/GridCustom"; + +export default function GridTwoView({ + spanLeft = 6, + spanRight = 6, + leftIcon, + rightIcon, + styleLeft, + styleRight, +}: { + spanLeft?: number; + spanRight?: number; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; + styleLeft?: StyleProp; + styleRight?: StyleProp; +}) { + return ( + + + {leftIcon} + + + {rightIcon} + + + ); +} diff --git a/screens/Portofolio/BusinessLocationSection.tsx b/screens/Portofolio/BusinessLocationSection.tsx index 579e409..445030c 100644 --- a/screens/Portofolio/BusinessLocationSection.tsx +++ b/screens/Portofolio/BusinessLocationSection.tsx @@ -1,12 +1,45 @@ -import { BaseBox, MapCustom, StackCustom, TextCustom } from "@/components"; +import { + BaseBox, + MapCustom, + StackCustom, + TextCustom +} from "@/components"; -export default function Portofolio_BusinessLocation() { +export default function Portofolio_BusinessLocation({ + data, + imageId, + setOpenDrawerLocation, +}: { + data: any; + imageId?: string; + setOpenDrawerLocation: (value: boolean) => void; +}) { return ( <> - + Lokasi Bisnis - + {!data ? ( + + Lokasi bisnis belum ditambahkan + + ) : ( + { + setOpenDrawerLocation(true); + }} + /> + )} diff --git a/screens/Portofolio/DataPortofolio.tsx b/screens/Portofolio/DataPortofolio.tsx index 64b0561..5ccbcfa 100644 --- a/screens/Portofolio/DataPortofolio.tsx +++ b/screens/Portofolio/DataPortofolio.tsx @@ -41,7 +41,11 @@ export default function Portofolio_Data({ }, { icon: ( - + ), label: data && data?.alamatKantor ? data.alamatKantor : "-", }, @@ -89,8 +93,6 @@ export default function Portofolio_Data({ // }, // ]; - // console.log("List Sub Bidang >>", JSON.stringify(listSubBidang, null, 2)); - return ( <> diff --git a/screens/Portofolio/ListPage.tsx b/screens/Portofolio/ListPage.tsx index b2253a7..48bc51e 100644 --- a/screens/Portofolio/ListPage.tsx +++ b/screens/Portofolio/ListPage.tsx @@ -1,9 +1,19 @@ import { IMenuDrawerItem } from "@/components/_Interface/types"; import { AccentColor } from "@/constants/color-palet"; import { ICON_SIZE_MEDIUM } from "@/constants/constans-value"; -import { Ionicons, FontAwesome5, FontAwesome, Fontisto } from "@expo/vector-icons"; +import { + FontAwesome, + Fontisto, + Ionicons +} from "@expo/vector-icons"; -export const drawerItemsPortofolio = ({ id }: { id: string }): IMenuDrawerItem[] => [ +export const drawerItemsPortofolio = ({ + id, + maps, +}: { + id: string; + maps: any; +}): IMenuDrawerItem[] => [ { icon: ( ), - label: "Edit Map", - path: `/(application)/maps/${id}/edit`, - }, - { - icon: ( - - ), - label: "Custom Pin Map", - path: `/(application)/maps/${id}/custom-pin`, + label: `${!maps ? "Tambah" : "Edit"} Map`, + path: !maps + ? `/(application)/maps/create?id=${id}` + : `/(application)/maps/${maps?.id}/edit`, }, + // { + // icon: ( + // + // ), + // label: "Custom Pin Map", + // path: `/(application)/maps/${id}/custom-pin`, + // }, ]; diff --git a/service/api-client/api-maps.ts b/service/api-client/api-maps.ts new file mode 100644 index 0000000..6889d9f --- /dev/null +++ b/service/api-client/api-maps.ts @@ -0,0 +1,41 @@ +import { apiConfig } from "../api-config"; + +export async function apiMapsCreate({ data }: { data: any }) { + try { + const response = await apiConfig.post(`/mobile/maps`, { + data: data, + }); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiMapsGetAll() { + try { + const response = await apiConfig.get("/mobile/maps"); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiMapsGetOne({ id }: { id: string }) { + try { + const response = await apiConfig.get(`/mobile/maps/${id}`); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiMapsUpdate({ id, data }: { id: string; data: any }) { + try { + const response = await apiConfig.put(`/mobile/maps/${id}`, { + data: data, + }); + return response.data; + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/utils/openInDeviceMaps.ts b/utils/openInDeviceMaps.ts new file mode 100644 index 0000000..ad4be3b --- /dev/null +++ b/utils/openInDeviceMaps.ts @@ -0,0 +1,37 @@ +import { Platform, Linking, Alert } from "react-native"; + +export const openInDeviceMaps = async ({ + latitude, + longitude, + title = "Lokasi", +}: { + latitude: number; + longitude: number; + title?: string; +}) => { + let url = ""; + + if (Platform.OS === "ios") { + // Apple Maps + url = `maps://?q=${encodeURIComponent(title)}&ll=${latitude},${longitude}`; + } else { + // Android: Google Maps + url = `geo:${latitude},${longitude}?q=${latitude},${longitude}(${encodeURIComponent( + title + )})`; + } + + try { + const canOpen = await Linking.canOpenURL(url); + if (canOpen) { + await Linking.openURL(url); + } else { + // Fallback ke web + const webUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`; + await Linking.openURL(webUrl); + } + } catch (error) { + console.warn("Gagal membuka Maps:", error); + Alert.alert("Error", "Tidak dapat membuka aplikasi Maps."); + } +};