2 Commits
Author SHA1 Message Date
shancheas f5c1b7430a refactor: simplify login component imports and remove unused dependencies
- Removed the `Divider` import and unnecessary icons (`GoogleIcon`, `MicrosoftIcon`) from the login component, streamlining the code.
- Improved code readability by consolidating imports, enhancing maintainability.

These changes contribute to a cleaner and more efficient login component, making it easier for developers to work with the codebase.
2026-09-02 08:40:08 +07:00
shancheas e316afa51d 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.
2026-09-02 08:14:37 +07:00
19 changed files with 70 additions and 174 deletions
+2 -2
View File
@@ -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`:
| code | label |
| ---- | ----- |
| ----------------------------------------- | ----------------------- |
| `ADMIN.SETTINGS.USER.PRIVILEGES` | Privileges |
| `ADMIN.SETTINGS.USER.USERS` | Users |
| `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`:
| 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` |
@@ -104,12 +104,6 @@ export function RightSection() {
</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 */}
<div className="flex gap-1.5 mt-8 z-10 mb-2">
<div className="w-6 h-1 rounded-full bg-brand-500"></div>
+1 -4
View File
@@ -3,14 +3,11 @@ import {
FieldPasswordInput,
Checkbox,
Button,
Divider,
Select,
Image,
notifications,
} from '@repo/ui/components';
import { Lock, Apple, LogInIcon, UserRound, Globe } from 'lucide-react';
import { GoogleIcon } from './components/google-icon';
import { MicrosoftIcon } from './components/microsoft-icon';
import { Lock, LogInIcon, UserRound, Globe } from 'lucide-react';
import { RightSection } from './components/right-section';
import { useTranslation, registerModuleNamespace } from '@repo/core-i18n';
import { AppStorageKey, appStorage } from '../../../core/storage/local';
@@ -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) {}
@@ -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<CompanySettingsShellEntity> {
@@ -25,10 +25,7 @@ export default function CompanySettingsModule() {
>
<Routes>
<Route path="/index" element={<IndexPage />} />
<Route
path="/"
element={<Navigate to={`${companySettingsModuleConfig.webUrl}/index`} replace />}
/>
<Route path="/" element={<Navigate to={`${companySettingsModuleConfig.webUrl}/index`} replace />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
</EnterpriseModuleProvider>
@@ -91,11 +91,7 @@ export default function CompanySettingsPage() {
<Stack gap="md" component="form" onSubmit={onSubmit}>
<Grid gutter="md">
<Grid.Col span={{ base: 12, md: 6 }}>
<FieldDatePicker
control={form.control}
name="cycleStartDate"
label={t('fields.cycleStartDate')}
/>
<FieldDatePicker control={form.control} name="cycleStartDate" label={t('fields.cycleStartDate')} />
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<FieldNumberInput
@@ -49,9 +49,7 @@ export function TimelineActivityList({
radius="md"
shadow={selected ? 'sm' : undefined}
style={{
borderColor: selected
? 'var(--mantine-color-blue-filled)'
: 'var(--mantine-color-default-border)',
borderColor: selected ? 'var(--mantine-color-blue-filled)' : 'var(--mantine-color-default-border)',
borderWidth: selected ? 2 : 1,
}}
>
@@ -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';
@@ -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',
]);
@@ -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<string, { latitude: number; longitude: number; label: string }>();
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);
}
@@ -104,10 +104,7 @@ export function TimelinePlaybackOverlay({
value={last ? `${formatClock(last.recordedAt)} · ${activityLabel(last.type)}` : '—'}
/>
<OverlayMetric label={currentLocationLabel} value={currentLocation || '—'} />
<OverlayMetric
label={activitiesLabel}
value={group ? String(group.activities.length) : '—'}
/>
<OverlayMetric label={activitiesLabel} value={group ? String(group.activities.length) : '—'} />
</SimpleGrid>
</Stack>
@@ -25,10 +25,7 @@ export default function SalesTimelineModule() {
>
<Routes>
<Route path="/index" element={<IndexPage />} />
<Route
path="/"
element={<Navigate to={`${salesTimelineModuleConfig.webUrl}/index`} replace />}
/>
<Route path="/" element={<Navigate to={`${salesTimelineModuleConfig.webUrl}/index`} replace />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
</EnterpriseModuleProvider>
@@ -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() {
</Box>
</FormProvider>
<Box
pos="absolute"
bottom={16}
left={{ base: 12, md: 432 }}
right={{ base: 12, md: 16 }}
style={{ zIndex: 2 }}
>
<Box pos="absolute" bottom={16} left={{ base: 12, md: 432 }} right={{ base: 12, md: 16 }} style={{ zIndex: 2 }}>
<TimelinePlaybackOverlay
group={selectedGroup}
ungroupedLabel={t('activities.ungrouped')}
@@ -52,14 +52,9 @@ describe('groupPrivilegeKeys', () => {
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' });
});
@@ -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({
<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' }}>
{mode === 'edit' && formControl ? (
<FieldCheckbox
control={formControl.control}
name={`matrix.${row.keyId}.${action}`}
label=""
/>
<FieldCheckbox control={formControl.control} name={`matrix.${row.keyId}.${action}`} label="" />
) : row.cell[action] ? (
<Check size={16} />
) : (
@@ -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;
}
@@ -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);
});
@@ -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',
@@ -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<string, TimelineMapFootprint[]> {
function groupFootprintsByEmployee(footprints: readonly TimelineMapFootprint[]): Map<string, TimelineMapFootprint[]> {
const grouped = new Map<string, TimelineMapFootprint[]>();
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 (
<Box
h={height}
bdrs={fullBleed ? 0 : radius}
className="tg-map-viewport"
pos="relative"
>
<Box h={height} bdrs={fullBleed ? 0 : radius} className="tg-map-viewport" pos="relative">
<MapContainer
center={boundsPositions[0] ?? DEFAULT_MAP_CENTER}
zoom={hasData ? 12 : DEFAULT_MAP_ZOOM}
@@ -221,13 +198,7 @@ export function TimelineMap({
<InvalidateSize />
</MapContainer>
{!hasData ? (
<Box
pos="absolute"
top="50%"
left="50%"
style={{ zIndex: 1, transform: 'translate(-50%, -50%)' }}
p="sm"
>
<Box pos="absolute" top="50%" left="50%" style={{ zIndex: 1, transform: 'translate(-50%, -50%)' }} p="sm">
<Text size="sm" c="dimmed">
{emptyLabel}
</Text>