feat: enhance weekdays management with tabbed interface and improved layout

- Refactored `DetailWeekdays` and `FormWeekdays` components to utilize a tabbed interface for better organization of weekday-related information.
- Introduced `FieldValue` components for displaying start and end branches, and customer details within each tab.
- Added a new CSS file for managing z-index stacking context to ensure Leaflet maps render correctly beneath modals.
- Updated `LocationMap` and `RouteMap` components to include the new CSS for proper layering.
- Enhanced unit tests for `LocationMap` to verify the correct stacking context.

These changes improve the user experience by providing a more structured and visually appealing way to manage weekdays and their associated data.
This commit is contained in:
shancheas
2026-08-26 12:09:12 +07:00
parent 27dfc167d2
commit ee6c3273d9
8 changed files with 133 additions and 83 deletions
@@ -1,4 +1,4 @@
import { Box, Paper, Stack, Text } from '@repo/ui/components'; import { Box, FieldValue, Paper, SimpleGrid, Tabs, Text } from '@repo/ui/components';
import { RouteMap } from '@repo/ui/map'; import { RouteMap } from '@repo/ui/map';
import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations'; import { useDetailPageContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import type { CycleEntity } from '../../../domain/entities'; import type { CycleEntity } from '../../../domain/entities';
@@ -7,6 +7,7 @@ export function DetailWeekdays() {
const { detailData } = useDetailPageContext<CycleEntity>(); const { detailData } = useDetailPageContext<CycleEntity>();
const { t } = useEnterpriseModuleTranslationContext(); const { t } = useEnterpriseModuleTranslationContext();
const weekdays = detailData?.weekdays ?? []; const weekdays = detailData?.weekdays ?? [];
const activeWeekday = weekdays[0]?.weekday;
if (weekdays.length === 0) { if (weekdays.length === 0) {
return ( return (
@@ -20,20 +21,38 @@ export function DetailWeekdays() {
} }
return ( return (
<Stack gap="md"> <Paper withBorder shadow="sm" radius="md" p="xl">
{weekdays.map((row) => ( <Text fw={600} mb="md">
<Paper key={row.id ?? row.weekday} withBorder shadow="sm" radius="md" p="xl"> {t('section_weekdays')}
<Text fw={600} mb="md"> </Text>
{t(`weekday_${row.weekday}`)} <Tabs key={String(detailData?.id ?? activeWeekday)} defaultValue={activeWeekday} variant="outline" radius="md">
</Text> <Tabs.List mb="md">
<Text size="sm" mb="md"> {weekdays.map((row) => (
{(row.destinations ?? []).map((destination, index) => `${index + 1}. ${destination.customerId}`).join(' | ') || t('empty_route')} <Tabs.Tab key={row.id ?? row.weekday} value={row.weekday}>
</Text> {t(`weekday_${row.weekday}`)}
<Box> </Tabs.Tab>
<RouteMap geometry={row.routeGeometry} /> ))}
</Box> </Tabs.List>
</Paper> {weekdays.map((row) => (
))} <Tabs.Panel key={row.id ?? row.weekday} value={row.weekday} pt="md">
</Stack> <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md" verticalSpacing="xl" mb="md">
<FieldValue label={t('common:fields.startBranch')} value={row.startBranchId} />
<FieldValue label={t('common:fields.endBranch')} value={row.endBranchId} />
</SimpleGrid>
<FieldValue
label={t('common:fields.customers')}
value={
(row.destinations ?? [])
.map((destination, index) => `${index + 1}. ${destination.customerId}`)
.join(' | ') || t('empty_route')
}
/>
<Box mt="md">
<RouteMap geometry={row.routeGeometry} />
</Box>
</Tabs.Panel>
))}
</Tabs>
</Paper>
); );
} }
@@ -1,4 +1,4 @@
import { Box, FieldAsyncSelect, FieldSwitch, Paper, SimpleGrid, Stack, Text } from '@repo/ui/components'; import { Box, FieldAsyncSelect, FieldSwitch, Paper, SimpleGrid, Tabs, Text } from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations'; import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
import { loadBranchOptions } from '../../../../shared/load-branch-options'; import { loadBranchOptions } from '../../../../shared/load-branch-options';
import { loadCustomerOptions } from '../../../../shared/load-customer-options'; import { loadCustomerOptions } from '../../../../shared/load-customer-options';
@@ -14,69 +14,77 @@ export function FormWeekdays() {
const weekdayRows = (formControl.watch('weekdayRows') as CycleWeekdayRow[] | undefined) ?? []; const weekdayRows = (formControl.watch('weekdayRows') as CycleWeekdayRow[] | undefined) ?? [];
return ( return (
<Stack gap="md"> <Paper withBorder shadow="sm" radius="md" p="xl">
<Text fw={600}>{t('section_weekdays')}</Text> <Text fw={600} mb="md">
{WEEKDAYS.map((weekday, index) => { {t('section_weekdays')}
const row = weekdayRows[index]; </Text>
const enabled = Boolean(row?.enabled); <Tabs defaultValue={WEEKDAYS[0]} variant="outline" radius="md">
return ( <Tabs.List mb="md">
<Paper key={weekday} withBorder shadow="sm" radius="md" p="xl"> {WEEKDAYS.map((weekday) => (
<Text fw={600} mb="md"> <Tabs.Tab key={weekday} value={weekday}>
{t(`weekday_${weekday}`)} {t(`weekday_${weekday}`)}
</Text> </Tabs.Tab>
<FieldSwitch ))}
control={formControl.control} </Tabs.List>
name={`weekdayRows.${index}.enabled`} {WEEKDAYS.map((weekday, index) => {
label={t('weekday_enabled')} const row = weekdayRows[index];
/> const enabled = Boolean(row?.enabled);
{enabled ? ( return (
<Box mt="md"> <Tabs.Panel key={weekday} value={weekday} pt="md">
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md"> <FieldSwitch
<FieldAsyncSelect<BranchEntity> control={formControl.control}
control={formControl.control} name={`weekdayRows.${index}.enabled`}
name={`weekdayRows.${index}.startBranch`} label={t('weekday_enabled')}
label={t('common:fields.startBranch')} />
valueKey="id" {enabled ? (
labelKey="name"
required
searchable
loadOptions={loadBranchOptions}
defaultOptions={(row?.startBranch ? [row.startBranch] : []) as any}
renderLabel={relationLabel}
/>
<FieldAsyncSelect<BranchEntity>
control={formControl.control}
name={`weekdayRows.${index}.endBranch`}
label={t('common:fields.endBranch')}
valueKey="id"
labelKey="name"
required
searchable
loadOptions={loadBranchOptions}
defaultOptions={(row?.endBranch ? [row.endBranch] : []) as any}
renderLabel={relationLabel}
/>
</SimpleGrid>
<Box mt="md"> <Box mt="md">
<FieldAsyncSelect<CustomerEntity> <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
control={formControl.control} <FieldAsyncSelect<BranchEntity>
name={`weekdayRows.${index}.customers`} control={formControl.control}
label={t('common:fields.customers')} name={`weekdayRows.${index}.startBranch`}
valueKey="id" label={t('common:fields.startBranch')}
labelKey="name" valueKey="id"
required labelKey="name"
searchable required
multiple searchable
loadOptions={loadCustomerOptions} loadOptions={loadBranchOptions}
defaultOptions={(row?.customers ?? []) as any} defaultOptions={(row?.startBranch ? [row.startBranch] : []) as any}
renderLabel={relationLabel} renderLabel={relationLabel}
/> />
<FieldAsyncSelect<BranchEntity>
control={formControl.control}
name={`weekdayRows.${index}.endBranch`}
label={t('common:fields.endBranch')}
valueKey="id"
labelKey="name"
required
searchable
loadOptions={loadBranchOptions}
defaultOptions={(row?.endBranch ? [row.endBranch] : []) as any}
renderLabel={relationLabel}
/>
</SimpleGrid>
<Box mt="md">
<FieldAsyncSelect<CustomerEntity>
control={formControl.control}
name={`weekdayRows.${index}.customers`}
label={t('common:fields.customers')}
valueKey="id"
labelKey="name"
required
searchable
multiple
loadOptions={loadCustomerOptions}
defaultOptions={(row?.customers ?? []) as any}
renderLabel={relationLabel}
/>
</Box>
</Box> </Box>
</Box> ) : null}
) : null} </Tabs.Panel>
</Paper> );
); })}
})} </Tabs>
</Stack> </Paper>
); );
} }
@@ -0,0 +1,12 @@
/* Leaflet panes/controls use z-index 4001000 and would cover Mantine modals (200) unless contained. */
.tg-map-viewport {
position: relative;
z-index: 0;
isolation: isolate;
overflow: hidden;
}
.leaflet-container {
isolation: isolate;
z-index: 0;
}
@@ -112,4 +112,9 @@ describe('LocationMap', () => {
expect(setView).not.toHaveBeenCalled(); expect(setView).not.toHaveBeenCalled();
expect(panTo).not.toHaveBeenCalled(); expect(panTo).not.toHaveBeenCalled();
}); });
it('keeps the map in a local stacking context so Leaflet panes stay below modals', () => {
renderMap(<LocationMap latitude={-6.2} longitude={106.8} />);
expect(screen.getByTestId('osm-map').closest('.tg-map-viewport')).not.toBeNull();
});
}); });
@@ -10,6 +10,7 @@ import {
toLocationLatLng, toLocationLatLng,
} from './location-point'; } from './location-point';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import './leaflet-stacking.css';
export interface LocationMapProps { export interface LocationMapProps {
latitude?: unknown; latitude?: unknown;
@@ -119,7 +120,8 @@ export function LocationMap({
h={height} h={height}
bdrs="md" bdrs="md"
bd="1px solid var(--mantine-color-default-border)" bd="1px solid var(--mantine-color-default-border)"
style={{ overflow: 'hidden', cursor: interactive ? 'crosshair' : undefined }} className="tg-map-viewport"
style={{ cursor: interactive ? 'crosshair' : undefined }}
> >
<MapContainer center={center} zoom={zoom} style={{ height: '100%', width: '100%' }} scrollWheelZoom={interactive}> <MapContainer center={center} zoom={zoom} style={{ height: '100%', width: '100%' }} scrollWheelZoom={interactive}>
<TileLayer attribution={attribution} url={tileUrl} /> <TileLayer attribution={attribution} url={tileUrl} />
+2 -1
View File
@@ -5,6 +5,7 @@ import type { RouteGeometry } from './route-geometry';
import { toLeafletLatLngs } from './route-geometry'; import { toLeafletLatLngs } from './route-geometry';
import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm'; import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import './leaflet-stacking.css';
function FitRouteBounds({ positions }: { positions: Array<[number, number]> }) { function FitRouteBounds({ positions }: { positions: Array<[number, number]> }) {
const map = useMap(); const map = useMap();
@@ -38,7 +39,7 @@ export function RouteMap({ geometry, height = 280 }: RouteMapProps) {
} }
return ( return (
<Box h={height} bdrs="md" style={{ overflow: 'hidden' }}> <Box h={height} bdrs="md" className="tg-map-viewport">
<MapContainer center={positions[0]} zoom={12} style={{ height: '100%', width: '100%' }} scrollWheelZoom> <MapContainer center={positions[0]} zoom={12} style={{ height: '100%', width: '100%' }} scrollWheelZoom>
<TileLayer attribution={OSM_ATTRIBUTION} url={OSM_TILE_URL} /> <TileLayer attribution={OSM_ATTRIBUTION} url={OSM_TILE_URL} />
<Polyline positions={positions} pathOptions={{ color: 'var(--mantine-color-blue-6)', weight: 4 }} /> <Polyline positions={positions} pathOptions={{ color: 'var(--mantine-color-blue-6)', weight: 4 }} />
+4 -1
View File
@@ -52,6 +52,9 @@ export function ThemeProvider({ children, colorScheme = 'light', density = 'comp
fontFamilyMonospace: typography.fontFamilyMonospace, fontFamilyMonospace: typography.fontFamilyMonospace,
headings: typography.headings, headings: typography.headings,
radius: radius, radius: radius,
components: {
Drawer: { defaultProps: { zIndex: 1100 } },
},
}); });
const selectedDensity = densityMap[density]; const selectedDensity = densityMap[density];
@@ -59,7 +62,7 @@ export function ThemeProvider({ children, colorScheme = 'light', density = 'comp
return ( return (
<MantineProvider theme={mergedTheme} forceColorScheme={colorScheme} defaultColorScheme={colorScheme}> <MantineProvider theme={mergedTheme} forceColorScheme={colorScheme} defaultColorScheme={colorScheme}>
<ModalsProvider> <ModalsProvider modalProps={{ zIndex: 1100 }}>
{children} {children}
<Notifications position="top-right" autoClose={4000} zIndex={1000} /> <Notifications position="top-right" autoClose={4000} zIndex={1000} />
</ModalsProvider> </ModalsProvider>
+2 -2
View File
@@ -36,7 +36,7 @@ export const standardDensity: MantineThemeOverride = {
Pagination: { defaultProps: { size: 'md' } }, Pagination: { defaultProps: { size: 'md' } },
Table: { defaultProps: { verticalSpacing: 'sm', horizontalSpacing: 'md' } }, Table: { defaultProps: { verticalSpacing: 'sm', horizontalSpacing: 'md' } },
Card: { defaultProps: { padding: 'md' } }, Card: { defaultProps: { padding: 'md' } },
Modal: { defaultProps: { padding: 'md' } }, Modal: { defaultProps: { padding: 'md', zIndex: 1100 } },
}, },
}; };
@@ -99,7 +99,7 @@ export const compactDensity: MantineThemeOverride = {
}, },
}, },
Card: { defaultProps: { padding: 'sm' } }, // Card padding shrinks Card: { defaultProps: { padding: 'sm' } }, // Card padding shrinks
Modal: { defaultProps: { padding: 'sm' } }, // Modal padding shrinks Modal: { defaultProps: { padding: 'sm', zIndex: 1100 } }, // Modal padding shrinks; above Leaflet panes (1000)
// 5. Force 13px ('sm') text inside 'sm' physical inputs // 5. Force 13px ('sm') text inside 'sm' physical inputs
Input: { Input: {