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:
+35
-16
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-62
@@ -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 400–1000 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} />
|
||||||
|
|||||||
@@ -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 }} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user