diff --git a/api.md b/api.md index ea84556..91a60da 100644 --- a/api.md +++ b/api.md @@ -242,40 +242,40 @@ HTTP mapping: Catalog (`GET /privilege-keys`, needs `ADMIN.SETTINGS.USER.PRIVILEGES` `view`). Keys use `Group.Parent.Module` or `Group.Parent.Module.Submodule`: -| code | label | -| ---- | ----- | -| `ADMIN.SETTINGS.USER.PRIVILEGES` | Privileges | -| `ADMIN.SETTINGS.USER.USERS` | Users | -| `ADMIN.SETTINGS.DATA.DIVISION` | Divisions | -| `ADMIN.SETTINGS.DATA.BRANCH` | Branches | -| `ADMIN.SETTINGS.DATA.CUSTOMER` | Customers | -| `ADMIN.SETTINGS.DATA.PRODUCT` | Products | -| `ADMIN.SETTINGS.DATA.SETTING` | Company settings | -| `ADMIN.SALES.DATA.EMPLOYEE` | Employees | -| `ADMIN.SALES.DATA.CYCLE` | Sales cycles | -| `ADMIN.SALES.ACTIVITIES.REQUEST` | Sales requests | -| `ADMIN.SALES.ACTIVITIES.ORDER` | Sales orders | -| `ADMIN.SALES.ACTIVITIES.INVOICE` | Sales invoices | -| `ADMIN.SALES.ACTIVITIES.PAYMENT` | Sales payments | -| `ADMIN.SALES.ACTIVITIES.PLAN` | Sales plans | -| `ADMIN.SALES.ACTIVITIES.TIMELINE` | Sales timeline | -| `ADMIN.SALES.REPORT` | Sales reports | -| `ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP` | Packing slips | -| `ADMIN.LOGISTICS.DATA.CYCLE` | Logistics cycles | -| `ADMIN.LOGISTICS.ACTIVITIES.PLAN` | Logistics plans | -| `ADMIN.LOGISTICS.REPORT` | Logistics reports | -| `MOBILE.SALES.PLAN` | Sales plans (mobile) | -| `MOBILE.SALES.PLAN.ATTENDANCE` | Branch attendance | -| `MOBILE.SALES.VISIT` | Customer visits | -| `MOBILE.SALES.TIMELINE` | Sales timeline (mobile) | +| code | label | +| ----------------------------------------- | ----------------------- | +| `ADMIN.SETTINGS.USER.PRIVILEGES` | Privileges | +| `ADMIN.SETTINGS.USER.USERS` | Users | +| `ADMIN.SETTINGS.DATA.DIVISION` | Divisions | +| `ADMIN.SETTINGS.DATA.BRANCH` | Branches | +| `ADMIN.SETTINGS.DATA.CUSTOMER` | Customers | +| `ADMIN.SETTINGS.DATA.PRODUCT` | Products | +| `ADMIN.SETTINGS.DATA.SETTING` | Company settings | +| `ADMIN.SALES.DATA.EMPLOYEE` | Employees | +| `ADMIN.SALES.DATA.CYCLE` | Sales cycles | +| `ADMIN.SALES.ACTIVITIES.REQUEST` | Sales requests | +| `ADMIN.SALES.ACTIVITIES.ORDER` | Sales orders | +| `ADMIN.SALES.ACTIVITIES.INVOICE` | Sales invoices | +| `ADMIN.SALES.ACTIVITIES.PAYMENT` | Sales payments | +| `ADMIN.SALES.ACTIVITIES.PLAN` | Sales plans | +| `ADMIN.SALES.ACTIVITIES.TIMELINE` | Sales timeline | +| `ADMIN.SALES.REPORT` | Sales reports | +| `ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP` | Packing slips | +| `ADMIN.LOGISTICS.DATA.CYCLE` | Logistics cycles | +| `ADMIN.LOGISTICS.ACTIVITIES.PLAN` | Logistics plans | +| `ADMIN.LOGISTICS.REPORT` | Logistics reports | +| `MOBILE.SALES.PLAN` | Sales plans (mobile) | +| `MOBILE.SALES.PLAN.ATTENDANCE` | Branch attendance | +| `MOBILE.SALES.VISIT` | Customer visits | +| `MOBILE.SALES.TIMELINE` | Sales timeline (mobile) | ### Field purpose Cycles and plans do **not** use a single key. Privilege is resolved from `purpose`: -| purpose | cycle keys | plan keys | -| ----------- | ---------- | --------- | -| `sales` | `ADMIN.SALES.DATA.CYCLE` | `ADMIN.SALES.ACTIVITIES.PLAN`, `MOBILE.SALES.PLAN` | +| purpose | cycle keys | plan keys | +| ----------- | ---------------------------- | ---------------------------------------------------------- | +| `sales` | `ADMIN.SALES.DATA.CYCLE` | `ADMIN.SALES.ACTIVITIES.PLAN`, `MOBILE.SALES.PLAN` | | `logistics` | `ADMIN.LOGISTICS.DATA.CYCLE` | `ADMIN.LOGISTICS.ACTIVITIES.PLAN`, `MOBILE.LOGISTICS.PLAN` | `purpose` is read from **body** (writes) or **query** (lists). If omitted, the user may proceed if they have the action on **either** purpose; list results are filtered to purposes they can view. Superadmin bypasses. diff --git a/apps/web/src/apps/auth/login/components/right-section.tsx b/apps/web/src/apps/auth/login/components/right-section.tsx index 9d8d49e..ec9e355 100644 --- a/apps/web/src/apps/auth/login/components/right-section.tsx +++ b/apps/web/src/apps/auth/login/components/right-section.tsx @@ -104,12 +104,6 @@ export function RightSection() { -

- Compatible with{' '} - Gmail, Outlook Web, LinkedIn and most web editors for a - smooth writing experience anywhere online. -

- {/* Pagination Indicators */}
diff --git a/apps/web/src/apps/main/modules/configuration/company-settings/data/company-settings.remote.service.ts b/apps/web/src/apps/main/modules/configuration/company-settings/data/company-settings.remote.service.ts index 3ece50d..fc31808 100644 --- a/apps/web/src/apps/main/modules/configuration/company-settings/data/company-settings.remote.service.ts +++ b/apps/web/src/apps/main/modules/configuration/company-settings/data/company-settings.remote.service.ts @@ -1,8 +1,5 @@ import type { AxiosInstance } from '@repo/core-api/http-client'; -import type { - CompanySettingsEntity, - UpdateCompanySettingsPayload, -} from '../domain/entities/company-settings.entity'; +import type { CompanySettingsEntity, UpdateCompanySettingsPayload } from '../domain/entities/company-settings.entity'; export class CompanySettingsRemoteService { constructor(private readonly client: AxiosInstance) {} diff --git a/apps/web/src/apps/main/modules/configuration/company-settings/domain/factories/index.ts b/apps/web/src/apps/main/modules/configuration/company-settings/domain/factories/index.ts index fc3d0b1..9489f74 100644 --- a/apps/web/src/apps/main/modules/configuration/company-settings/domain/factories/index.ts +++ b/apps/web/src/apps/main/modules/configuration/company-settings/domain/factories/index.ts @@ -1,10 +1,7 @@ import { BaseDataTransformer } from '@repo/core-api/data-services'; import { apiClient } from '../../../../../../../core/lib/api-client'; import { TrackGoRemoteDataServices } from '../../../../../../../core/lib/trackgo-remote-data-services'; -import { - companySettingsModuleConfig, - type CompanySettingsShellEntity, -} from '../constants/company-settings.constants'; +import { companySettingsModuleConfig, type CompanySettingsShellEntity } from '../constants/company-settings.constants'; import { CompanySettingsRemoteService } from '../../data/company-settings.remote.service'; class CompanySettingsShellTransformer extends BaseDataTransformer { diff --git a/apps/web/src/apps/main/modules/configuration/company-settings/presentation/factory/index.tsx b/apps/web/src/apps/main/modules/configuration/company-settings/presentation/factory/index.tsx index 0662b8c..3348d26 100644 --- a/apps/web/src/apps/main/modules/configuration/company-settings/presentation/factory/index.tsx +++ b/apps/web/src/apps/main/modules/configuration/company-settings/presentation/factory/index.tsx @@ -25,10 +25,7 @@ export default function CompanySettingsModule() { > } /> - } - /> + } /> } /> diff --git a/apps/web/src/apps/main/modules/configuration/company-settings/presentation/pages/company-settings.page.tsx b/apps/web/src/apps/main/modules/configuration/company-settings/presentation/pages/company-settings.page.tsx index f9dbda4..5fb41c7 100644 --- a/apps/web/src/apps/main/modules/configuration/company-settings/presentation/pages/company-settings.page.tsx +++ b/apps/web/src/apps/main/modules/configuration/company-settings/presentation/pages/company-settings.page.tsx @@ -91,11 +91,7 @@ export default function CompanySettingsPage() { - + diff --git a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-activity-panel.tsx b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-activity-panel.tsx index 6c01baa..459b68a 100644 --- a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-activity-panel.tsx +++ b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-activity-panel.tsx @@ -1,12 +1,5 @@ import type { ReactNode } from 'react'; -import { - Paper, - ScrollArea, - SegmentedControl, - Stack, - Text, - TextInput, -} from '@repo/ui/components'; +import { Paper, ScrollArea, SegmentedControl, Stack, Text, TextInput } from '@repo/ui/components'; import { Search } from 'lucide-react'; import { TimelineActivityList } from './timeline-activity-list'; import type { TimelineActivityGroup, TimelineActivityTab } from './timeline-helpers'; diff --git a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.test.ts b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.test.ts index 6794bc9..40b3c62 100644 --- a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.test.ts +++ b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.test.ts @@ -115,7 +115,9 @@ describe('timeline helpers', () => { 'visit-1', '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([ 'Toko Maju', ]); diff --git a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.tsx b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.tsx index e94dd3a..fa7bf43 100644 --- a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.tsx +++ b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-helpers.tsx @@ -1,7 +1,4 @@ -import type { - TimelineActivityEntity, - TimelineFootprintEntity, -} from '../../domain/entities/timeline.entity'; +import type { TimelineActivityEntity, TimelineFootprintEntity } from '../../domain/entities/timeline.entity'; export type TimelineActivityGroup = { key: string; @@ -46,9 +43,7 @@ export function groupActivities(activities: TimelineActivityEntity[]): TimelineA const sorted = sortByRecordedAt(items); const last = sorted[sorted.length - 1]; const first = sorted[0]; - const title = - last?.customer?.name ?? - (key === 'ungrouped' ? 'ungrouped' : (last?.customer?.code ?? key)); + const title = last?.customer?.name ?? (key === 'ungrouped' ? 'ungrouped' : (last?.customer?.code ?? key)); return { key, @@ -95,10 +90,7 @@ export function resolvePlaybackPositions( return []; } - const latestByEmployee = new Map< - string, - { latitude: number; longitude: number; label: string } - >(); + const latestByEmployee = new Map(); for (const footprint of footprints) { if (footprint.recordedAt > playbackTime) { @@ -155,10 +147,6 @@ export function shouldRestartPlayback( return playbackTime === null || playbackTime >= bounds.max; } -export function advancePlaybackTime( - current: number, - bounds: { min: number; max: number }, - step: number, -): number { +export function advancePlaybackTime(current: number, bounds: { min: number; max: number }, step: number): number { return Math.min(bounds.max, current + step); } diff --git a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-playback-overlay.tsx b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-playback-overlay.tsx index b2d54ac..bcb6081 100644 --- a/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-playback-overlay.tsx +++ b/apps/web/src/apps/main/modules/field/timeline/presentation/components/timeline-playback-overlay.tsx @@ -104,10 +104,7 @@ export function TimelinePlaybackOverlay({ value={last ? `${formatClock(last.recordedAt)} · ${activityLabel(last.type)}` : '—'} /> - + diff --git a/apps/web/src/apps/main/modules/field/timeline/presentation/factory/index.tsx b/apps/web/src/apps/main/modules/field/timeline/presentation/factory/index.tsx index 89ee69b..a6c18d8 100644 --- a/apps/web/src/apps/main/modules/field/timeline/presentation/factory/index.tsx +++ b/apps/web/src/apps/main/modules/field/timeline/presentation/factory/index.tsx @@ -25,10 +25,7 @@ export default function SalesTimelineModule() { > } /> - } - /> + } /> } /> diff --git a/apps/web/src/apps/main/modules/field/timeline/presentation/pages/timeline.page.index.tsx b/apps/web/src/apps/main/modules/field/timeline/presentation/pages/timeline.page.index.tsx index 1d2bd3e..e71db0e 100644 --- a/apps/web/src/apps/main/modules/field/timeline/presentation/pages/timeline.page.index.tsx +++ b/apps/web/src/apps/main/modules/field/timeline/presentation/pages/timeline.page.index.tsx @@ -153,10 +153,7 @@ export default function TimelinePage() { [footprints, playbackTime], ); const groups = useMemo(() => groupActivities(visibleActivities), [visibleActivities]); - const filteredGroups = useMemo( - () => filterActivityGroups(groups, { tab, query: search }), - [groups, search, tab], - ); + const filteredGroups = useMemo(() => filterActivityGroups(groups, { tab, query: search }), [groups, search, tab]); useEffect(() => { if (filteredGroups.length === 0) { @@ -179,9 +176,7 @@ export default function TimelinePage() { ); const focusPositions = useMemo(() => { if (selectedGroup && selectedGroup.activities.length > 0) { - return selectedGroup.activities.map( - (activity): [number, number] => [activity.latitude, activity.longitude], - ); + return selectedGroup.activities.map((activity): [number, number] => [activity.latitude, activity.longitude]); } return dayPositions; }, [dayPositions, selectedGroup]); @@ -195,10 +190,7 @@ export default function TimelinePage() { })), [visibleFootprints], ); - const activityLabel = useCallback( - (type: string) => t(`activities.types.${type}`, { defaultValue: type }), - [t], - ); + const activityLabel = useCallback((type: string) => t(`activities.types.${type}`, { defaultValue: type }), [t]); const mapActivities = useMemo( () => visibleActivities.map((activity) => ({ @@ -282,13 +274,7 @@ export default function TimelinePage() { - + { expect(grouped.map((tab) => tab.group)).toEqual(['ADMIN', 'MOBILE']); expect(grouped[0]?.parents[0]?.parent).toBe('SALES'); - expect(grouped[0]?.parents[0]?.modules.map((m) => m.module)).toEqual([ - 'ACTIVITIES', - 'DATA', - ]); + expect(grouped[0]?.parents[0]?.modules.map((m) => m.module)).toEqual(['ACTIVITIES', 'DATA']); expect(grouped[1]?.parents[0]?.modules[0]?.directRow?.code).toBe('MOBILE.SALES.PLAN'); - expect(grouped[1]?.parents[0]?.modules[0]?.submodules[0]?.code).toBe( - 'MOBILE.SALES.PLAN.ATTENDANCE', - ); + expect(grouped[1]?.parents[0]?.modules[0]?.submodules[0]?.code).toBe('MOBILE.SALES.PLAN.ATTENDANCE'); }); }); @@ -72,10 +67,7 @@ describe('flattenParentSectionRows', () => { expect(salesParent).toBeDefined(); const tableRows = flattenParentSectionRows(salesParent!); - expect(tableRows.map((r) => r.code)).toEqual([ - 'ADMIN.SALES.DATA.CYCLE', - 'ADMIN.SALES.ACTIVITIES.PLAN', - ]); + expect(tableRows.map((r) => r.code)).toEqual(['ADMIN.SALES.DATA.CYCLE', 'ADMIN.SALES.ACTIVITIES.PLAN']); expect(tableRows[0]).toMatchObject({ module: 'DATA', submodule: 'CYCLE' }); expect(tableRows[1]).toMatchObject({ module: 'ACTIVITIES', submodule: 'PLAN' }); }); diff --git a/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx index b1af7a0..a6c42d8 100644 --- a/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx +++ b/apps/web/src/apps/main/modules/system/privileges/presentation/components/privilege-matrix-layout.tsx @@ -28,10 +28,7 @@ function segmentLabel(t: (key: string) => string, kind: string, value: string) { return translated === key ? formatPrivilegeSegment(value) : translated; } -function submoduleCellLabel( - t: (key: string) => string, - row: PrivilegeTableRow, -) { +function submoduleCellLabel(t: (key: string) => string, row: PrivilegeTableRow) { if (row.submodule) { return row.label || segmentLabel(t, 'submodule', row.submodule); } @@ -53,11 +50,7 @@ function MatrixActionCells({ {mode === 'edit' && formControl ? ( - + ) : row.cell[action] ? ( ) : ( @@ -137,12 +130,7 @@ function ParentMatrixTable({ ); } -export function PrivilegeMatrixLayout({ - groups, - t, - mode, - formControl, -}: PrivilegeMatrixLayoutProps) { +export function PrivilegeMatrixLayout({ groups, t, mode, formControl }: PrivilegeMatrixLayoutProps) { if (groups.length === 0) { return null; } diff --git a/apps/web/src/core/lib/enterprise-storage-adapter.test.ts b/apps/web/src/core/lib/enterprise-storage-adapter.test.ts index 04570ad..9c4a7fd 100644 --- a/apps/web/src/core/lib/enterprise-storage-adapter.test.ts +++ b/apps/web/src/core/lib/enterprise-storage-adapter.test.ts @@ -27,7 +27,9 @@ describe('enterpriseStorageAdapter', () => { 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); }); diff --git a/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts b/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts index a75d825..c10d6af 100644 --- a/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts +++ b/apps/web/src/core/lib/filter-menu-by-view-privilege.test.ts @@ -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([ 'orders', diff --git a/packages/ui/src/components/map/timeline-map.tsx b/packages/ui/src/components/map/timeline-map.tsx index 124781a..44e5782 100644 --- a/packages/ui/src/components/map/timeline-map.tsx +++ b/packages/ui/src/components/map/timeline-map.tsx @@ -1,13 +1,5 @@ import { useEffect, useMemo } from 'react'; -import { - CircleMarker, - MapContainer, - Polyline, - TileLayer, - Tooltip, - ZoomControl, - useMap, -} from 'react-leaflet'; +import { CircleMarker, MapContainer, Polyline, TileLayer, Tooltip, ZoomControl, useMap } from 'react-leaflet'; import { Box, Text } from '@mantine/core'; import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm'; import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from './location-point'; @@ -76,11 +68,7 @@ function InvalidateSize() { return null; } -function FitTimelineBounds({ - positions, -}: { - positions: Array<[number, number]>; -}) { +function FitTimelineBounds({ positions }: { positions: Array<[number, number]> }) { const map = useMap(); const boundsKey = positions.map(([lat, lng]) => `${lat.toFixed(6)},${lng.toFixed(6)}`).join('|'); useEffect(() => { @@ -97,9 +85,7 @@ function FitTimelineBounds({ return null; } -function groupFootprintsByEmployee( - footprints: readonly TimelineMapFootprint[], -): Map { +function groupFootprintsByEmployee(footprints: readonly TimelineMapFootprint[]): Map { const grouped = new Map(); for (const point of footprints) { const existing = grouped.get(point.employeeId) ?? []; @@ -125,10 +111,7 @@ export function TimelineMap({ fullBleed = false, emptyLabel = 'No timeline data', }: TimelineMapProps) { - const groupedTracks = useMemo( - () => groupFootprintsByEmployee(footprints), - [footprints], - ); + const groupedTracks = useMemo(() => groupFootprintsByEmployee(footprints), [footprints]); const positions = useMemo(() => { const points: Array<[number, number]> = []; @@ -144,18 +127,12 @@ export function TimelineMap({ return points; }, [activities, footprints, playbackPositions]); - const boundsPositions = - focusPositions && focusPositions.length > 0 ? focusPositions : positions; + const boundsPositions = focusPositions && focusPositions.length > 0 ? focusPositions : positions; const employeeIds = [...groupedTracks.keys()]; const hasData = positions.length > 0; return ( - + {!hasData ? ( - + {emptyLabel}