feat: implement location management with interactive maps

- Introduced a new `LocationMap` component for handling latitude and longitude inputs in forms and detail views.
- Updated form and detail components to utilize the `LocationMap`, enhancing user experience by allowing map-based location selection.
- Added internationalization support for map-related labels in English and Indonesian.
- Developed unit tests for the `LocationMap` to ensure functionality and reliability.

This commit enhances the application by providing a user-friendly interface for managing geographical locations, improving data accuracy and user interaction.
This commit is contained in:
shancheas
2026-08-26 07:56:04 +07:00
parent 5287da918c
commit 27dfc167d2
15 changed files with 578 additions and 62 deletions
@@ -1,4 +1,5 @@
import { Box, Paper, SimpleGrid, FieldValue, Text } from '@repo/ui/components';
import { Paper, SimpleGrid, FieldValue, Stack, Text } from '@repo/ui/components';
import { LocationMap } from '@repo/ui/map';
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import type { BranchEntity } from '../../../domain/entities';
@@ -13,13 +14,14 @@ export function DetailLocation() {
<Text fw={600} mb="md">
{t('section_location')}
</Text>
<Box>
<Stack gap="md">
<LocationMap latitude={data?.latitude} longitude={data?.longitude} emptyLabel={t('common:map.noLocation')} />
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" verticalSpacing="xl">
<FieldValue label={t('common:fields.latitude')} value={data?.latitude} />
<FieldValue label={t('common:fields.longitude')} value={data?.longitude} />
<FieldValue label={t('common:fields.division')} value={divisionLabel} />
</SimpleGrid>
</Box>
</Stack>
</Paper>
);
}
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { Box, FieldNumberInput, FieldAsyncSelect, Paper, SimpleGrid, Text } from '@repo/ui/components';
import { Box, FieldNumberInput, FieldAsyncSelect, Paper, SimpleGrid, Stack, Text } from '@repo/ui/components';
import { LocationMap } from '@repo/ui/map';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { loadDivisionOptions } from '../../../../shared/load-division-options';
import { divisionsDataService } from '../../../../divisions/domain/factories';
@@ -9,6 +10,8 @@ export function FormLocation() {
const { formControl } = useFormPageContext();
const { t } = useEnterpriseModuleTranslationContext();
const division = formControl.watch('division');
const latitude = formControl.watch('latitude');
const longitude = formControl.watch('longitude');
useEffect(() => {
const current = formControl.getValues('division') as DivisionEntity | null | undefined;
@@ -26,39 +29,50 @@ export function FormLocation() {
<Text fw={600} mb="md">
{t('section_location')}
</Text>
<Box>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<FieldNumberInput
control={formControl.control}
name="latitude"
label={t('common:fields.latitude')}
placeholder="-6.2"
decimalScale={6}
radius="md"
/>
<FieldNumberInput
control={formControl.control}
name="longitude"
label={t('common:fields.longitude')}
placeholder="106.8"
decimalScale={6}
radius="md"
/>
<FieldAsyncSelect<DivisionEntity>
control={formControl.control}
name="division"
label={t('common:fields.division')}
placeholder={t('common:fields.division')}
valueKey="id"
labelKey="name"
clearable
searchable
loadOptions={loadDivisionOptions}
defaultOptions={division ? [division] : []}
renderLabel={(item) => `${item.code} - ${item.name}`}
/>
</SimpleGrid>
</Box>
<Stack gap="md">
<LocationMap
latitude={latitude}
longitude={longitude}
helperLabel={t('common:map.pickLocation')}
onChange={(point) => {
formControl.setValue('latitude', point.latitude, { shouldDirty: true, shouldValidate: true });
formControl.setValue('longitude', point.longitude, { shouldDirty: true, shouldValidate: true });
}}
/>
<Box>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<FieldNumberInput
control={formControl.control}
name="latitude"
label={t('common:fields.latitude')}
placeholder="-6.2"
decimalScale={6}
radius="md"
/>
<FieldNumberInput
control={formControl.control}
name="longitude"
label={t('common:fields.longitude')}
placeholder="106.8"
decimalScale={6}
radius="md"
/>
<FieldAsyncSelect<DivisionEntity>
control={formControl.control}
name="division"
label={t('common:fields.division')}
placeholder={t('common:fields.division')}
valueKey="id"
labelKey="name"
clearable
searchable
loadOptions={loadDivisionOptions}
defaultOptions={division ? [division] : []}
renderLabel={(item) => `${item.code} - ${item.name}`}
/>
</SimpleGrid>
</Box>
</Stack>
</Paper>
);
}
@@ -1,4 +1,5 @@
import { Box, Paper, SimpleGrid, FieldValue, Text } from '@repo/ui/components';
import { Paper, SimpleGrid, FieldValue, Stack, Text } from '@repo/ui/components';
import { LocationMap } from '@repo/ui/map';
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import type { CustomerEntity } from '../../../domain/entities';
@@ -12,12 +13,13 @@ export function DetailLocation() {
<Text fw={600} mb="md">
{t('section_location')}
</Text>
<Box>
<Stack gap="md">
<LocationMap latitude={data?.latitude} longitude={data?.longitude} emptyLabel={t('common:map.noLocation')} />
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" verticalSpacing="xl">
<FieldValue label={t('common:fields.latitude')} value={data?.latitude} />
<FieldValue label={t('common:fields.longitude')} value={data?.longitude} />
</SimpleGrid>
</Box>
</Stack>
</Paper>
);
}
@@ -1,35 +1,49 @@
import { Box, FieldNumberInput, Paper, SimpleGrid, Text } from '@repo/ui/components';
import { Box, FieldNumberInput, Paper, SimpleGrid, Stack, Text } from '@repo/ui/components';
import { LocationMap } from '@repo/ui/map';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
export function FormLocation() {
const { formControl } = useFormPageContext();
const { t } = useEnterpriseModuleTranslationContext();
const latitude = formControl.watch('latitude');
const longitude = formControl.watch('longitude');
return (
<Paper withBorder shadow="sm" radius="md" p="xl">
<Text fw={600} mb="md">
{t('section_location')}
</Text>
<Box>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<FieldNumberInput
control={formControl.control}
name="latitude"
label={t('common:fields.latitude')}
placeholder="-6.2"
decimalScale={6}
radius="md"
/>
<FieldNumberInput
control={formControl.control}
name="longitude"
label={t('common:fields.longitude')}
placeholder="106.8"
decimalScale={6}
radius="md"
/>
</SimpleGrid>
</Box>
<Stack gap="md">
<LocationMap
latitude={latitude}
longitude={longitude}
helperLabel={t('common:map.pickLocation')}
onChange={(point) => {
formControl.setValue('latitude', point.latitude, { shouldDirty: true, shouldValidate: true });
formControl.setValue('longitude', point.longitude, { shouldDirty: true, shouldValidate: true });
}}
/>
<Box>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<FieldNumberInput
control={formControl.control}
name="latitude"
label={t('common:fields.latitude')}
placeholder="-6.2"
decimalScale={6}
radius="md"
/>
<FieldNumberInput
control={formControl.control}
name="longitude"
label={t('common:fields.longitude')}
placeholder="106.8"
decimalScale={6}
radius="md"
/>
</SimpleGrid>
</Box>
</Stack>
</Paper>
);
}