refactor: clean up code formatting and improve readability
- Standardized table formatting in `api.md` for privilege keys, enhancing clarity. - Removed unnecessary line breaks and improved inline formatting in various components, including `right-section.tsx`, `company-settings.remote.service.ts`, and `timeline-helpers.tsx`, to streamline code readability. - Updated test cases in `privilege-key-hierarchy.test.ts` and `filter-menu-by-view-privilege.test.ts` for better alignment and consistency in assertions. These changes improve the overall code quality and maintainability, making it easier for developers to navigate and understand the codebase.
This commit is contained in:
@@ -243,7 +243,7 @@ HTTP mapping:
|
|||||||
Catalog (`GET /privilege-keys`, needs `ADMIN.SETTINGS.USER.PRIVILEGES` `view`). Keys use `Group.Parent.Module` or `Group.Parent.Module.Submodule`:
|
Catalog (`GET /privilege-keys`, needs `ADMIN.SETTINGS.USER.PRIVILEGES` `view`). Keys use `Group.Parent.Module` or `Group.Parent.Module.Submodule`:
|
||||||
|
|
||||||
| code | label |
|
| code | label |
|
||||||
| ---- | ----- |
|
| ----------------------------------------- | ----------------------- |
|
||||||
| `ADMIN.SETTINGS.USER.PRIVILEGES` | Privileges |
|
| `ADMIN.SETTINGS.USER.PRIVILEGES` | Privileges |
|
||||||
| `ADMIN.SETTINGS.USER.USERS` | Users |
|
| `ADMIN.SETTINGS.USER.USERS` | Users |
|
||||||
| `ADMIN.SETTINGS.DATA.DIVISION` | Divisions |
|
| `ADMIN.SETTINGS.DATA.DIVISION` | Divisions |
|
||||||
@@ -274,7 +274,7 @@ Catalog (`GET /privilege-keys`, needs `ADMIN.SETTINGS.USER.PRIVILEGES` `view`).
|
|||||||
Cycles and plans do **not** use a single key. Privilege is resolved from `purpose`:
|
Cycles and plans do **not** use a single key. Privilege is resolved from `purpose`:
|
||||||
|
|
||||||
| purpose | cycle keys | plan keys |
|
| purpose | cycle keys | plan keys |
|
||||||
| ----------- | ---------- | --------- |
|
| ----------- | ---------------------------- | ---------------------------------------------------------- |
|
||||||
| `sales` | `ADMIN.SALES.DATA.CYCLE` | `ADMIN.SALES.ACTIVITIES.PLAN`, `MOBILE.SALES.PLAN` |
|
| `sales` | `ADMIN.SALES.DATA.CYCLE` | `ADMIN.SALES.ACTIVITIES.PLAN`, `MOBILE.SALES.PLAN` |
|
||||||
| `logistics` | `ADMIN.LOGISTICS.DATA.CYCLE` | `ADMIN.LOGISTICS.ACTIVITIES.PLAN`, `MOBILE.LOGISTICS.PLAN` |
|
| `logistics` | `ADMIN.LOGISTICS.DATA.CYCLE` | `ADMIN.LOGISTICS.ACTIVITIES.PLAN`, `MOBILE.LOGISTICS.PLAN` |
|
||||||
|
|
||||||
|
|||||||
@@ -104,12 +104,6 @@ export function RightSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-[13px] text-center text-gray-600 mt-6 max-w-[85%] z-10 leading-[1.6]">
|
|
||||||
Compatible with{' '}
|
|
||||||
<span className="font-bold italic text-gray-900">Gmail, Outlook Web, LinkedIn and most web editors</span> for a
|
|
||||||
smooth writing experience anywhere online.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Pagination Indicators */}
|
{/* Pagination Indicators */}
|
||||||
<div className="flex gap-1.5 mt-8 z-10 mb-2">
|
<div className="flex gap-1.5 mt-8 z-10 mb-2">
|
||||||
<div className="w-6 h-1 rounded-full bg-brand-500"></div>
|
<div className="w-6 h-1 rounded-full bg-brand-500"></div>
|
||||||
|
|||||||
+1
-4
@@ -1,8 +1,5 @@
|
|||||||
import type { AxiosInstance } from '@repo/core-api/http-client';
|
import type { AxiosInstance } from '@repo/core-api/http-client';
|
||||||
import type {
|
import type { CompanySettingsEntity, UpdateCompanySettingsPayload } from '../domain/entities/company-settings.entity';
|
||||||
CompanySettingsEntity,
|
|
||||||
UpdateCompanySettingsPayload,
|
|
||||||
} from '../domain/entities/company-settings.entity';
|
|
||||||
|
|
||||||
export class CompanySettingsRemoteService {
|
export class CompanySettingsRemoteService {
|
||||||
constructor(private readonly client: AxiosInstance) {}
|
constructor(private readonly client: AxiosInstance) {}
|
||||||
|
|||||||
+1
-4
@@ -1,10 +1,7 @@
|
|||||||
import { BaseDataTransformer } from '@repo/core-api/data-services';
|
import { BaseDataTransformer } from '@repo/core-api/data-services';
|
||||||
import { apiClient } from '../../../../../../../core/lib/api-client';
|
import { apiClient } from '../../../../../../../core/lib/api-client';
|
||||||
import { TrackGoRemoteDataServices } from '../../../../../../../core/lib/trackgo-remote-data-services';
|
import { TrackGoRemoteDataServices } from '../../../../../../../core/lib/trackgo-remote-data-services';
|
||||||
import {
|
import { companySettingsModuleConfig, type CompanySettingsShellEntity } from '../constants/company-settings.constants';
|
||||||
companySettingsModuleConfig,
|
|
||||||
type CompanySettingsShellEntity,
|
|
||||||
} from '../constants/company-settings.constants';
|
|
||||||
import { CompanySettingsRemoteService } from '../../data/company-settings.remote.service';
|
import { CompanySettingsRemoteService } from '../../data/company-settings.remote.service';
|
||||||
|
|
||||||
class CompanySettingsShellTransformer extends BaseDataTransformer<CompanySettingsShellEntity> {
|
class CompanySettingsShellTransformer extends BaseDataTransformer<CompanySettingsShellEntity> {
|
||||||
|
|||||||
+1
-4
@@ -25,10 +25,7 @@ export default function CompanySettingsModule() {
|
|||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/index" element={<IndexPage />} />
|
<Route path="/index" element={<IndexPage />} />
|
||||||
<Route
|
<Route path="/" element={<Navigate to={`${companySettingsModuleConfig.webUrl}/index`} replace />} />
|
||||||
path="/"
|
|
||||||
element={<Navigate to={`${companySettingsModuleConfig.webUrl}/index`} replace />}
|
|
||||||
/>
|
|
||||||
<Route path="*" element={<Navigate to="/404" replace />} />
|
<Route path="*" element={<Navigate to="/404" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</EnterpriseModuleProvider>
|
</EnterpriseModuleProvider>
|
||||||
|
|||||||
+1
-5
@@ -91,11 +91,7 @@ export default function CompanySettingsPage() {
|
|||||||
<Stack gap="md" component="form" onSubmit={onSubmit}>
|
<Stack gap="md" component="form" onSubmit={onSubmit}>
|
||||||
<Grid gutter="md">
|
<Grid gutter="md">
|
||||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||||
<FieldDatePicker
|
<FieldDatePicker control={form.control} name="cycleStartDate" label={t('fields.cycleStartDate')} />
|
||||||
control={form.control}
|
|
||||||
name="cycleStartDate"
|
|
||||||
label={t('fields.cycleStartDate')}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||||
<FieldNumberInput
|
<FieldNumberInput
|
||||||
|
|||||||
+1
-3
@@ -49,9 +49,7 @@ export function TimelineActivityList({
|
|||||||
radius="md"
|
radius="md"
|
||||||
shadow={selected ? 'sm' : undefined}
|
shadow={selected ? 'sm' : undefined}
|
||||||
style={{
|
style={{
|
||||||
borderColor: selected
|
borderColor: selected ? 'var(--mantine-color-blue-filled)' : 'var(--mantine-color-default-border)',
|
||||||
? 'var(--mantine-color-blue-filled)'
|
|
||||||
: 'var(--mantine-color-default-border)',
|
|
||||||
borderWidth: selected ? 2 : 1,
|
borderWidth: selected ? 2 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-8
@@ -1,12 +1,5 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import {
|
import { Paper, ScrollArea, SegmentedControl, Stack, Text, TextInput } from '@repo/ui/components';
|
||||||
Paper,
|
|
||||||
ScrollArea,
|
|
||||||
SegmentedControl,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
TextInput,
|
|
||||||
} from '@repo/ui/components';
|
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { TimelineActivityList } from './timeline-activity-list';
|
import { TimelineActivityList } from './timeline-activity-list';
|
||||||
import type { TimelineActivityGroup, TimelineActivityTab } from './timeline-helpers';
|
import type { TimelineActivityGroup, TimelineActivityTab } from './timeline-helpers';
|
||||||
|
|||||||
+3
-1
@@ -115,7 +115,9 @@ describe('timeline helpers', () => {
|
|||||||
'visit-1',
|
'visit-1',
|
||||||
'ungrouped',
|
'ungrouped',
|
||||||
]);
|
]);
|
||||||
expect(filterActivityGroups(groups, { tab: 'completed', query: '' }).map((group) => group.key)).toEqual(['visit-2']);
|
expect(filterActivityGroups(groups, { tab: 'completed', query: '' }).map((group) => group.key)).toEqual([
|
||||||
|
'visit-2',
|
||||||
|
]);
|
||||||
expect(filterActivityGroups(groups, { tab: 'on_the_way', query: 'maju' }).map((group) => group.title)).toEqual([
|
expect(filterActivityGroups(groups, { tab: 'on_the_way', query: 'maju' }).map((group) => group.title)).toEqual([
|
||||||
'Toko Maju',
|
'Toko Maju',
|
||||||
]);
|
]);
|
||||||
|
|||||||
+4
-16
@@ -1,7 +1,4 @@
|
|||||||
import type {
|
import type { TimelineActivityEntity, TimelineFootprintEntity } from '../../domain/entities/timeline.entity';
|
||||||
TimelineActivityEntity,
|
|
||||||
TimelineFootprintEntity,
|
|
||||||
} from '../../domain/entities/timeline.entity';
|
|
||||||
|
|
||||||
export type TimelineActivityGroup = {
|
export type TimelineActivityGroup = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -46,9 +43,7 @@ export function groupActivities(activities: TimelineActivityEntity[]): TimelineA
|
|||||||
const sorted = sortByRecordedAt(items);
|
const sorted = sortByRecordedAt(items);
|
||||||
const last = sorted[sorted.length - 1];
|
const last = sorted[sorted.length - 1];
|
||||||
const first = sorted[0];
|
const first = sorted[0];
|
||||||
const title =
|
const title = last?.customer?.name ?? (key === 'ungrouped' ? 'ungrouped' : (last?.customer?.code ?? key));
|
||||||
last?.customer?.name ??
|
|
||||||
(key === 'ungrouped' ? 'ungrouped' : (last?.customer?.code ?? key));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
@@ -95,10 +90,7 @@ export function resolvePlaybackPositions(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestByEmployee = new Map<
|
const latestByEmployee = new Map<string, { latitude: number; longitude: number; label: string }>();
|
||||||
string,
|
|
||||||
{ latitude: number; longitude: number; label: string }
|
|
||||||
>();
|
|
||||||
|
|
||||||
for (const footprint of footprints) {
|
for (const footprint of footprints) {
|
||||||
if (footprint.recordedAt > playbackTime) {
|
if (footprint.recordedAt > playbackTime) {
|
||||||
@@ -155,10 +147,6 @@ export function shouldRestartPlayback(
|
|||||||
return playbackTime === null || playbackTime >= bounds.max;
|
return playbackTime === null || playbackTime >= bounds.max;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function advancePlaybackTime(
|
export function advancePlaybackTime(current: number, bounds: { min: number; max: number }, step: number): number {
|
||||||
current: number,
|
|
||||||
bounds: { min: number; max: number },
|
|
||||||
step: number,
|
|
||||||
): number {
|
|
||||||
return Math.min(bounds.max, current + step);
|
return Math.min(bounds.max, current + step);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -104,10 +104,7 @@ export function TimelinePlaybackOverlay({
|
|||||||
value={last ? `${formatClock(last.recordedAt)} · ${activityLabel(last.type)}` : '—'}
|
value={last ? `${formatClock(last.recordedAt)} · ${activityLabel(last.type)}` : '—'}
|
||||||
/>
|
/>
|
||||||
<OverlayMetric label={currentLocationLabel} value={currentLocation || '—'} />
|
<OverlayMetric label={currentLocationLabel} value={currentLocation || '—'} />
|
||||||
<OverlayMetric
|
<OverlayMetric label={activitiesLabel} value={group ? String(group.activities.length) : '—'} />
|
||||||
label={activitiesLabel}
|
|
||||||
value={group ? String(group.activities.length) : '—'}
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ export default function SalesTimelineModule() {
|
|||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/index" element={<IndexPage />} />
|
<Route path="/index" element={<IndexPage />} />
|
||||||
<Route
|
<Route path="/" element={<Navigate to={`${salesTimelineModuleConfig.webUrl}/index`} replace />} />
|
||||||
path="/"
|
|
||||||
element={<Navigate to={`${salesTimelineModuleConfig.webUrl}/index`} replace />}
|
|
||||||
/>
|
|
||||||
<Route path="*" element={<Navigate to="/404" replace />} />
|
<Route path="*" element={<Navigate to="/404" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</EnterpriseModuleProvider>
|
</EnterpriseModuleProvider>
|
||||||
|
|||||||
+4
-18
@@ -153,10 +153,7 @@ export default function TimelinePage() {
|
|||||||
[footprints, playbackTime],
|
[footprints, playbackTime],
|
||||||
);
|
);
|
||||||
const groups = useMemo(() => groupActivities(visibleActivities), [visibleActivities]);
|
const groups = useMemo(() => groupActivities(visibleActivities), [visibleActivities]);
|
||||||
const filteredGroups = useMemo(
|
const filteredGroups = useMemo(() => filterActivityGroups(groups, { tab, query: search }), [groups, search, tab]);
|
||||||
() => filterActivityGroups(groups, { tab, query: search }),
|
|
||||||
[groups, search, tab],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filteredGroups.length === 0) {
|
if (filteredGroups.length === 0) {
|
||||||
@@ -179,9 +176,7 @@ export default function TimelinePage() {
|
|||||||
);
|
);
|
||||||
const focusPositions = useMemo(() => {
|
const focusPositions = useMemo(() => {
|
||||||
if (selectedGroup && selectedGroup.activities.length > 0) {
|
if (selectedGroup && selectedGroup.activities.length > 0) {
|
||||||
return selectedGroup.activities.map(
|
return selectedGroup.activities.map((activity): [number, number] => [activity.latitude, activity.longitude]);
|
||||||
(activity): [number, number] => [activity.latitude, activity.longitude],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return dayPositions;
|
return dayPositions;
|
||||||
}, [dayPositions, selectedGroup]);
|
}, [dayPositions, selectedGroup]);
|
||||||
@@ -195,10 +190,7 @@ export default function TimelinePage() {
|
|||||||
})),
|
})),
|
||||||
[visibleFootprints],
|
[visibleFootprints],
|
||||||
);
|
);
|
||||||
const activityLabel = useCallback(
|
const activityLabel = useCallback((type: string) => t(`activities.types.${type}`, { defaultValue: type }), [t]);
|
||||||
(type: string) => t(`activities.types.${type}`, { defaultValue: type }),
|
|
||||||
[t],
|
|
||||||
);
|
|
||||||
const mapActivities = useMemo(
|
const mapActivities = useMemo(
|
||||||
() =>
|
() =>
|
||||||
visibleActivities.map((activity) => ({
|
visibleActivities.map((activity) => ({
|
||||||
@@ -282,13 +274,7 @@ export default function TimelinePage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<Box
|
<Box pos="absolute" bottom={16} left={{ base: 12, md: 432 }} right={{ base: 12, md: 16 }} style={{ zIndex: 2 }}>
|
||||||
pos="absolute"
|
|
||||||
bottom={16}
|
|
||||||
left={{ base: 12, md: 432 }}
|
|
||||||
right={{ base: 12, md: 16 }}
|
|
||||||
style={{ zIndex: 2 }}
|
|
||||||
>
|
|
||||||
<TimelinePlaybackOverlay
|
<TimelinePlaybackOverlay
|
||||||
group={selectedGroup}
|
group={selectedGroup}
|
||||||
ungroupedLabel={t('activities.ungrouped')}
|
ungroupedLabel={t('activities.ungrouped')}
|
||||||
|
|||||||
+3
-11
@@ -52,14 +52,9 @@ describe('groupPrivilegeKeys', () => {
|
|||||||
|
|
||||||
expect(grouped.map((tab) => tab.group)).toEqual(['ADMIN', 'MOBILE']);
|
expect(grouped.map((tab) => tab.group)).toEqual(['ADMIN', 'MOBILE']);
|
||||||
expect(grouped[0]?.parents[0]?.parent).toBe('SALES');
|
expect(grouped[0]?.parents[0]?.parent).toBe('SALES');
|
||||||
expect(grouped[0]?.parents[0]?.modules.map((m) => m.module)).toEqual([
|
expect(grouped[0]?.parents[0]?.modules.map((m) => m.module)).toEqual(['ACTIVITIES', 'DATA']);
|
||||||
'ACTIVITIES',
|
|
||||||
'DATA',
|
|
||||||
]);
|
|
||||||
expect(grouped[1]?.parents[0]?.modules[0]?.directRow?.code).toBe('MOBILE.SALES.PLAN');
|
expect(grouped[1]?.parents[0]?.modules[0]?.directRow?.code).toBe('MOBILE.SALES.PLAN');
|
||||||
expect(grouped[1]?.parents[0]?.modules[0]?.submodules[0]?.code).toBe(
|
expect(grouped[1]?.parents[0]?.modules[0]?.submodules[0]?.code).toBe('MOBILE.SALES.PLAN.ATTENDANCE');
|
||||||
'MOBILE.SALES.PLAN.ATTENDANCE',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,10 +67,7 @@ describe('flattenParentSectionRows', () => {
|
|||||||
expect(salesParent).toBeDefined();
|
expect(salesParent).toBeDefined();
|
||||||
const tableRows = flattenParentSectionRows(salesParent!);
|
const tableRows = flattenParentSectionRows(salesParent!);
|
||||||
|
|
||||||
expect(tableRows.map((r) => r.code)).toEqual([
|
expect(tableRows.map((r) => r.code)).toEqual(['ADMIN.SALES.DATA.CYCLE', 'ADMIN.SALES.ACTIVITIES.PLAN']);
|
||||||
'ADMIN.SALES.DATA.CYCLE',
|
|
||||||
'ADMIN.SALES.ACTIVITIES.PLAN',
|
|
||||||
]);
|
|
||||||
expect(tableRows[0]).toMatchObject({ module: 'DATA', submodule: 'CYCLE' });
|
expect(tableRows[0]).toMatchObject({ module: 'DATA', submodule: 'CYCLE' });
|
||||||
expect(tableRows[1]).toMatchObject({ module: 'ACTIVITIES', submodule: 'PLAN' });
|
expect(tableRows[1]).toMatchObject({ module: 'ACTIVITIES', submodule: 'PLAN' });
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-15
@@ -28,10 +28,7 @@ function segmentLabel(t: (key: string) => string, kind: string, value: string) {
|
|||||||
return translated === key ? formatPrivilegeSegment(value) : translated;
|
return translated === key ? formatPrivilegeSegment(value) : translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
function submoduleCellLabel(
|
function submoduleCellLabel(t: (key: string) => string, row: PrivilegeTableRow) {
|
||||||
t: (key: string) => string,
|
|
||||||
row: PrivilegeTableRow,
|
|
||||||
) {
|
|
||||||
if (row.submodule) {
|
if (row.submodule) {
|
||||||
return row.label || segmentLabel(t, 'submodule', row.submodule);
|
return row.label || segmentLabel(t, 'submodule', row.submodule);
|
||||||
}
|
}
|
||||||
@@ -53,11 +50,7 @@ function MatrixActionCells({
|
|||||||
<Table.Td key={action} ta="center" w={ACTION_COL_WIDTH} miw={ACTION_COL_WIDTH} maw={ACTION_COL_WIDTH}>
|
<Table.Td key={action} ta="center" w={ACTION_COL_WIDTH} miw={ACTION_COL_WIDTH} maw={ACTION_COL_WIDTH}>
|
||||||
<Box display="flex" style={{ justifyContent: 'center', alignItems: 'center' }}>
|
<Box display="flex" style={{ justifyContent: 'center', alignItems: 'center' }}>
|
||||||
{mode === 'edit' && formControl ? (
|
{mode === 'edit' && formControl ? (
|
||||||
<FieldCheckbox
|
<FieldCheckbox control={formControl.control} name={`matrix.${row.keyId}.${action}`} label="" />
|
||||||
control={formControl.control}
|
|
||||||
name={`matrix.${row.keyId}.${action}`}
|
|
||||||
label=""
|
|
||||||
/>
|
|
||||||
) : row.cell[action] ? (
|
) : row.cell[action] ? (
|
||||||
<Check size={16} />
|
<Check size={16} />
|
||||||
) : (
|
) : (
|
||||||
@@ -137,12 +130,7 @@ function ParentMatrixTable({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrivilegeMatrixLayout({
|
export function PrivilegeMatrixLayout({ groups, t, mode, formControl }: PrivilegeMatrixLayoutProps) {
|
||||||
groups,
|
|
||||||
t,
|
|
||||||
mode,
|
|
||||||
formControl,
|
|
||||||
}: PrivilegeMatrixLayoutProps) {
|
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ describe('enterpriseStorageAdapter', () => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(enterpriseStorageAdapter.getPrivileges('ADMIN.SETTINGS.USER.PRIVILEGES')).resolves.toEqual(defaultPrivileges);
|
await expect(enterpriseStorageAdapter.getPrivileges('ADMIN.SETTINGS.USER.PRIVILEGES')).resolves.toEqual(
|
||||||
|
defaultPrivileges,
|
||||||
|
);
|
||||||
await expect(enterpriseStorageAdapter.getPrivileges('UNKNOWN')).resolves.toEqual(defaultPrivileges);
|
await expect(enterpriseStorageAdapter.getPrivileges('UNKNOWN')).resolves.toEqual(defaultPrivileges);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,11 @@ describe('filterMenuByViewPrivilege', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filtered = filterMenuByViewPrivilege(menu, { 'ADMIN.SALES.ACTIVITIES.ORDER': { ...noPrivileges, ALLOW_VIEW: true } }, false);
|
const filtered = filterMenuByViewPrivilege(
|
||||||
|
menu,
|
||||||
|
{ 'ADMIN.SALES.ACTIVITIES.ORDER': { ...noPrivileges, ALLOW_VIEW: true } },
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
expect(filtered.find((item) => item.key === 'sales')?.children?.map((child) => child.key)).toEqual([
|
expect(filtered.find((item) => item.key === 'sales')?.children?.map((child) => child.key)).toEqual([
|
||||||
'orders',
|
'orders',
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import {
|
import { CircleMarker, MapContainer, Polyline, TileLayer, Tooltip, ZoomControl, useMap } from 'react-leaflet';
|
||||||
CircleMarker,
|
|
||||||
MapContainer,
|
|
||||||
Polyline,
|
|
||||||
TileLayer,
|
|
||||||
Tooltip,
|
|
||||||
ZoomControl,
|
|
||||||
useMap,
|
|
||||||
} from 'react-leaflet';
|
|
||||||
import { Box, Text } from '@mantine/core';
|
import { Box, Text } from '@mantine/core';
|
||||||
import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm';
|
import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm';
|
||||||
import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from './location-point';
|
import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from './location-point';
|
||||||
@@ -76,11 +68,7 @@ function InvalidateSize() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FitTimelineBounds({
|
function FitTimelineBounds({ positions }: { positions: Array<[number, number]> }) {
|
||||||
positions,
|
|
||||||
}: {
|
|
||||||
positions: Array<[number, number]>;
|
|
||||||
}) {
|
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const boundsKey = positions.map(([lat, lng]) => `${lat.toFixed(6)},${lng.toFixed(6)}`).join('|');
|
const boundsKey = positions.map(([lat, lng]) => `${lat.toFixed(6)},${lng.toFixed(6)}`).join('|');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,9 +85,7 @@ function FitTimelineBounds({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupFootprintsByEmployee(
|
function groupFootprintsByEmployee(footprints: readonly TimelineMapFootprint[]): Map<string, TimelineMapFootprint[]> {
|
||||||
footprints: readonly TimelineMapFootprint[],
|
|
||||||
): Map<string, TimelineMapFootprint[]> {
|
|
||||||
const grouped = new Map<string, TimelineMapFootprint[]>();
|
const grouped = new Map<string, TimelineMapFootprint[]>();
|
||||||
for (const point of footprints) {
|
for (const point of footprints) {
|
||||||
const existing = grouped.get(point.employeeId) ?? [];
|
const existing = grouped.get(point.employeeId) ?? [];
|
||||||
@@ -125,10 +111,7 @@ export function TimelineMap({
|
|||||||
fullBleed = false,
|
fullBleed = false,
|
||||||
emptyLabel = 'No timeline data',
|
emptyLabel = 'No timeline data',
|
||||||
}: TimelineMapProps) {
|
}: TimelineMapProps) {
|
||||||
const groupedTracks = useMemo(
|
const groupedTracks = useMemo(() => groupFootprintsByEmployee(footprints), [footprints]);
|
||||||
() => groupFootprintsByEmployee(footprints),
|
|
||||||
[footprints],
|
|
||||||
);
|
|
||||||
|
|
||||||
const positions = useMemo(() => {
|
const positions = useMemo(() => {
|
||||||
const points: Array<[number, number]> = [];
|
const points: Array<[number, number]> = [];
|
||||||
@@ -144,18 +127,12 @@ export function TimelineMap({
|
|||||||
return points;
|
return points;
|
||||||
}, [activities, footprints, playbackPositions]);
|
}, [activities, footprints, playbackPositions]);
|
||||||
|
|
||||||
const boundsPositions =
|
const boundsPositions = focusPositions && focusPositions.length > 0 ? focusPositions : positions;
|
||||||
focusPositions && focusPositions.length > 0 ? focusPositions : positions;
|
|
||||||
const employeeIds = [...groupedTracks.keys()];
|
const employeeIds = [...groupedTracks.keys()];
|
||||||
const hasData = positions.length > 0;
|
const hasData = positions.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box h={height} bdrs={fullBleed ? 0 : radius} className="tg-map-viewport" pos="relative">
|
||||||
h={height}
|
|
||||||
bdrs={fullBleed ? 0 : radius}
|
|
||||||
className="tg-map-viewport"
|
|
||||||
pos="relative"
|
|
||||||
>
|
|
||||||
<MapContainer
|
<MapContainer
|
||||||
center={boundsPositions[0] ?? DEFAULT_MAP_CENTER}
|
center={boundsPositions[0] ?? DEFAULT_MAP_CENTER}
|
||||||
zoom={hasData ? 12 : DEFAULT_MAP_ZOOM}
|
zoom={hasData ? 12 : DEFAULT_MAP_ZOOM}
|
||||||
@@ -221,13 +198,7 @@ export function TimelineMap({
|
|||||||
<InvalidateSize />
|
<InvalidateSize />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
{!hasData ? (
|
{!hasData ? (
|
||||||
<Box
|
<Box pos="absolute" top="50%" left="50%" style={{ zIndex: 1, transform: 'translate(-50%, -50%)' }} p="sm">
|
||||||
pos="absolute"
|
|
||||||
top="50%"
|
|
||||||
left="50%"
|
|
||||||
style={{ zIndex: 1, transform: 'translate(-50%, -50%)' }}
|
|
||||||
p="sm"
|
|
||||||
>
|
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{emptyLabel}
|
{emptyLabel}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user