refactor: improve code formatting and organization across multiple components
- Enhanced readability by restructuring code formatting in various components, including API documentation, user management, and sales modules. - Standardized table and object definitions for better clarity in API documentation. - Improved layout and indentation in React components for better maintainability. - Updated unit tests to reflect changes in formatting and ensure consistency. These changes enhance the overall code quality and maintainability of the project, making it easier for developers to navigate and understand the codebase.
This commit is contained in:
@@ -45,7 +45,11 @@ Do **not** redeclare:
|
|||||||
// GOOD
|
// GOOD
|
||||||
const columnDefs = [
|
const columnDefs = [
|
||||||
{ field: 'username', headerName: t('common:fields.username'), minWidth: 160 },
|
{ field: 'username', headerName: t('common:fields.username'), minWidth: 160 },
|
||||||
{ field: 'privilege', headerName: t('common:fields.privilege'), valueGetter: ({ data }) => relationLabel(data?.privilege) },
|
{
|
||||||
|
field: 'privilege',
|
||||||
|
headerName: t('common:fields.privilege'),
|
||||||
|
valueGetter: ({ data }) => relationLabel(data?.privilege),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// BAD — duplicates shared chrome
|
// BAD — duplicates shared chrome
|
||||||
@@ -60,7 +64,7 @@ const columnDefs = [
|
|||||||
API and transformers map:
|
API and transformers map:
|
||||||
|
|
||||||
| Entity field | Meaning |
|
| Entity field | Meaning |
|
||||||
|---|---|
|
| ------------ | ------------------------------------- |
|
||||||
| `createdBy` | actor string, `{ username }`, or uuid |
|
| `createdBy` | actor string, `{ username }`, or uuid |
|
||||||
| `createdAt` | unix ms |
|
| `createdAt` | unix ms |
|
||||||
| `updatedBy` | actor string, `{ username }`, or uuid |
|
| `updatedBy` | actor string, `{ username }`, or uuid |
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Base URL: `http://localhost:{PORT}` (default **3000**). There is **no** global p
|
|||||||
### Tokens
|
### Tokens
|
||||||
|
|
||||||
| Token | Type | Default lifetime | Transport |
|
| Token | Type | Default lifetime | Transport |
|
||||||
| ----- | ---- | ---------------- | --------- |
|
| ------- | --------------------- | -------------------------------------- | ------------------------- |
|
||||||
| Access | JWT HS256 | `15m` (`JWT_ACCESS_EXPIRES_IN`) | `Authorization: Bearer …` |
|
| Access | JWT HS256 | `15m` (`JWT_ACCESS_EXPIRES_IN`) | `Authorization: Bearer …` |
|
||||||
| Refresh | Opaque 64-char string | 7 days (`REFRESH_TOKEN_EXPIRES_IN_MS`) | JSON body `refreshToken` |
|
| Refresh | Opaque 64-char string | 7 days (`REFRESH_TOKEN_EXPIRES_IN_MS`) | JSON body `refreshToken` |
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ CSV import uses `multipart/form-data` with field name **`file`** (max 1 MiB).
|
|||||||
Query (all optional):
|
Query (all optional):
|
||||||
|
|
||||||
| Param | Rules | Default |
|
| Param | Rules | Default |
|
||||||
| ----- | ----- | ------- |
|
| -------- | ----------- | ------- |
|
||||||
| `page` | integer ≥ 1 | `1` |
|
| `page` | integer ≥ 1 | `1` |
|
||||||
| `limit` | 1–**200** | `10` |
|
| `limit` | 1–**200** | `10` |
|
||||||
| `offset` | integer ≥ 0 | — |
|
| `offset` | integer ≥ 0 | — |
|
||||||
@@ -123,7 +123,9 @@ Public response:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": [ /* items */ ],
|
"data": [
|
||||||
|
/* items */
|
||||||
|
],
|
||||||
"meta": {
|
"meta": {
|
||||||
"currentPage": 1,
|
"currentPage": 1,
|
||||||
"itemCount": 10,
|
"itemCount": 10,
|
||||||
@@ -141,7 +143,7 @@ Public response:
|
|||||||
Unwrapped resource object, or:
|
Unwrapped resource object, or:
|
||||||
|
|
||||||
| Operation | Status | Body |
|
| Operation | Status | Body |
|
||||||
| --------- | ------ | ---- |
|
| --------------- | ------ | ------------------------------------------ |
|
||||||
| Create | `201` | resource DTO (detail shape) |
|
| Create | `201` | resource DTO (detail shape) |
|
||||||
| Update / status | `200` | resource DTO |
|
| Update / status | `200` | resource DTO |
|
||||||
| Delete | `204` | empty |
|
| Delete | `204` | empty |
|
||||||
@@ -163,7 +165,7 @@ Unwrapped resource object, or:
|
|||||||
`message` is a string or an array of validation strings.
|
`message` is a string or an array of validation strings.
|
||||||
|
|
||||||
| Status | Typical cause |
|
| Status | Typical cause |
|
||||||
| ------ | ------------- |
|
| ------ | --------------------------------------------------------------------------- |
|
||||||
| `400` | Validation, extra fields, invalid VO (phone/date/status), `status` on PATCH |
|
| `400` | Validation, extra fields, invalid VO (phone/date/status), `status` on PATCH |
|
||||||
| `401` | Missing/expired/revoked JWT, bad credentials, invalid refresh |
|
| `401` | Missing/expired/revoked JWT, bad credentials, invalid refresh |
|
||||||
| `403` | `Insufficient privilege` |
|
| `403` | `Insufficient privilege` |
|
||||||
@@ -190,7 +192,7 @@ Core (configuration, privileges, cycles, plans, settings): `draft` | `active` |
|
|||||||
Sales statuses (use **only** these on that resource):
|
Sales statuses (use **only** these on that resource):
|
||||||
|
|
||||||
| Resource | Allowed |
|
| Resource | Allowed |
|
||||||
| -------- | ------- |
|
| -------------------------- | --------------------------------------------------------- |
|
||||||
| Sales request | `draft`, `pending`, `approved`, `rejected` |
|
| Sales request | `draft`, `pending`, `approved`, `rejected` |
|
||||||
| Sales order / packing slip | `draft`, `processed`, `completed`, `cancelled` |
|
| Sales order / packing slip | `draft`, `processed`, `completed`, `cancelled` |
|
||||||
| Sales invoice | `draft`, `processed`, `partial`, `completed`, `cancelled` |
|
| Sales invoice | `draft`, `processed`, `partial`, `completed`, `cancelled` |
|
||||||
@@ -231,7 +233,7 @@ Actions: `view` | `create` | `update` | `delete` | `import`.
|
|||||||
HTTP mapping:
|
HTTP mapping:
|
||||||
|
|
||||||
| Handler | Action |
|
| Handler | Action |
|
||||||
| ------- | ------ |
|
| --------------------------------------------------------------------------------------------------- | -------- |
|
||||||
| `GET` list / detail | `view` |
|
| `GET` list / detail | `view` |
|
||||||
| `POST /` create, `POST /plans/generate` | `create` |
|
| `POST /` create, `POST /plans/generate` | `create` |
|
||||||
| `PATCH /:id`, `PATCH /:id/status`, `POST /bulk-status`, nested customer contacts, plan destinations | `update` |
|
| `PATCH /:id`, `PATCH /:id/status`, `POST /bulk-status`, nested customer contacts, plan destinations | `update` |
|
||||||
@@ -241,7 +243,7 @@ HTTP mapping:
|
|||||||
Catalog (`GET /privilege-keys`, needs `PRIVILEGES` `view`):
|
Catalog (`GET /privilege-keys`, needs `PRIVILEGES` `view`):
|
||||||
|
|
||||||
| code | label |
|
| code | label |
|
||||||
| ---- | ----- |
|
| ------------------------ | ---------------- |
|
||||||
| `PRIVILEGES` | Privileges |
|
| `PRIVILEGES` | Privileges |
|
||||||
| `USERS` | Users |
|
| `USERS` | Users |
|
||||||
| `CONFIGURATION.DIVISION` | Divisions |
|
| `CONFIGURATION.DIVISION` | Divisions |
|
||||||
@@ -265,7 +267,7 @@ Catalog (`GET /privilege-keys`, needs `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 key | plan key |
|
| purpose | cycle key | plan key |
|
||||||
| ------- | --------- | -------- |
|
| ----------- | ----------------- | ---------------- |
|
||||||
| `sales` | `SALES.CYCLE` | `SALES.PLAN` |
|
| `sales` | `SALES.CYCLE` | `SALES.PLAN` |
|
||||||
| `logistics` | `LOGISTICS.CYCLE` | `LOGISTICS.PLAN` |
|
| `logistics` | `LOGISTICS.CYCLE` | `LOGISTICS.PLAN` |
|
||||||
|
|
||||||
@@ -280,7 +282,7 @@ Sales plans may attach `invoiceIds` only. Logistics plans may attach `packingSli
|
|||||||
Unless a section says otherwise, each resource below implements:
|
Unless a section says otherwise, each resource below implements:
|
||||||
|
|
||||||
| Method | Path | Status | Notes |
|
| Method | Path | Status | Notes |
|
||||||
| ------ | ---- | ------ | ----- |
|
| -------- | ------------------------- | ------ | --------------------------------------------- |
|
||||||
| `GET` | `/{resource}` | `200` | Paginated `{ data, meta }` |
|
| `GET` | `/{resource}` | `200` | Paginated `{ data, meta }` |
|
||||||
| `GET` | `/{resource}/:id` | `200` | Detail (may include nested arrays list omits) |
|
| `GET` | `/{resource}/:id` | `200` | Detail (may include nested arrays list omits) |
|
||||||
| `POST` | `/{resource}` | `201` | Create |
|
| `POST` | `/{resource}` | `201` | Create |
|
||||||
@@ -352,9 +354,7 @@ List filters: `name`, `code`, `status`, `search` (name/code).
|
|||||||
"name": "Sales Staff",
|
"name": "Sales Staff",
|
||||||
"code": "SALES_STAFF",
|
"code": "SALES_STAFF",
|
||||||
"status": "draft",
|
"status": "draft",
|
||||||
"details": [
|
"details": [{ "privilegeKeyId": "uuid", "action": "view", "value": true }]
|
||||||
{ "privilegeKeyId": "uuid", "action": "view", "value": true }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ Contact create: `name` required; `jobTitle?`, `phone?`, `mobilePhone?`, `notes?`
|
|||||||
### Nested contacts (privilege = customer **update**)
|
### Nested contacts (privilege = customer **update**)
|
||||||
|
|
||||||
| Method | Path | Status | Body | Response |
|
| Method | Path | Status | Body | Response |
|
||||||
| ------ | ---- | ------ | ---- | -------- |
|
| -------- | ------------------------------------ | ------ | --------------- | ------------------ |
|
||||||
| `POST` | `/customers/:id/contacts` | `200` | create contact | full `CustomerDto` |
|
| `POST` | `/customers/:id/contacts` | `200` | create contact | full `CustomerDto` |
|
||||||
| `PATCH` | `/customers/:id/contacts/:contactId` | `200` | partial contact | full `CustomerDto` |
|
| `PATCH` | `/customers/:id/contacts/:contactId` | `200` | partial contact | full `CustomerDto` |
|
||||||
| `DELETE` | `/customers/:id/contacts/:contactId` | `204` | — | empty |
|
| `DELETE` | `/customers/:id/contacts/:contactId` | `204` | — | empty |
|
||||||
@@ -574,12 +574,8 @@ Detail adds:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"products": [
|
"products": [{ "id": "uuid", "productId": "uuid", "quantity": "2.0000", "price": "12500.0000" }],
|
||||||
{ "id": "uuid", "productId": "uuid", "quantity": "2.0000", "price": "12500.0000" }
|
"images": [{ "id": "uuid", "url": "https://…", "description": null }]
|
||||||
],
|
|
||||||
"images": [
|
|
||||||
{ "id": "uuid", "url": "https://…", "description": null }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -660,7 +656,7 @@ CSV required: `date` (allocations empty on import).
|
|||||||
Key: `CONFIGURATION.SETTING`. Singleton — no list/CRUD.
|
Key: `CONFIGURATION.SETTING`. Singleton — no list/CRUD.
|
||||||
|
|
||||||
| Method | Path | Action | Notes |
|
| Method | Path | Action | Notes |
|
||||||
| ------ | ---- | ------ | ----- |
|
| ------- | ----------- | ------ | ----------------------------------------------------------------- |
|
||||||
| `GET` | `/settings` | view | `404` `{ "message": "Settings not configured" }` if never patched |
|
| `GET` | `/settings` | view | `404` `{ "message": "Settings not configured" }` if never patched |
|
||||||
| `PATCH` | `/settings` | update | upserts |
|
| `PATCH` | `/settings` | update | upserts |
|
||||||
|
|
||||||
@@ -730,11 +726,12 @@ No hard delete: `DELETE` / `bulk-delete` archive. **Has CSV import.** Standard l
|
|||||||
"endBranchId": "uuid",
|
"endBranchId": "uuid",
|
||||||
"routeGeometry": {
|
"routeGeometry": {
|
||||||
"type": "LineString",
|
"type": "LineString",
|
||||||
"coordinates": [[106.8456, -6.2088], [107.0, -6.3]]
|
"coordinates": [
|
||||||
},
|
[106.8456, -6.2088],
|
||||||
"destinations": [
|
[107.0, -6.3]
|
||||||
{ "id": "uuid", "customerId": "uuid", "sortOrder": 0 }
|
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"destinations": [{ "id": "uuid", "customerId": "uuid", "sortOrder": 0 }]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"status": "draft",
|
"status": "draft",
|
||||||
@@ -760,7 +757,7 @@ Privilege: `RequireFieldPrivilege('plan', action)` → `SALES.PLAN` or `LOGISTIC
|
|||||||
**No CSV import.** Delete archives. Extra routes: generate, add/remove destinations.
|
**No CSV import.** Delete archives. Extra routes: generate, add/remove destinations.
|
||||||
|
|
||||||
| Method | Path | Action | Status | Body / response |
|
| Method | Path | Action | Status | Body / response |
|
||||||
| ------ | ---- | ------ | ------ | --------------- |
|
| -------- | ---------------------------------------- | ------ | ------- | ------------------------------------------------------------ |
|
||||||
| `GET` | `/plans` | view | 200 | paginated |
|
| `GET` | `/plans` | view | 200 | paginated |
|
||||||
| `GET` | `/plans/:id` | view | 200 | `PlanDto` |
|
| `GET` | `/plans/:id` | view | 200 | `PlanDto` |
|
||||||
| `POST` | `/plans/generate` | create | 200 | `{ employeeId, purpose, from, to }` → `{ created, skipped }` |
|
| `POST` | `/plans/generate` | create | 200 | `{ employeeId, purpose, from, to }` → `{ created, skipped }` |
|
||||||
@@ -898,14 +895,7 @@ type UnixMs = number;
|
|||||||
type DecimalString = string;
|
type DecimalString = string;
|
||||||
type CoreStatus = 'draft' | 'active' | 'archived';
|
type CoreStatus = 'draft' | 'active' | 'archived';
|
||||||
type FieldPurpose = 'sales' | 'logistics';
|
type FieldPurpose = 'sales' | 'logistics';
|
||||||
type Weekday =
|
type Weekday = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
|
||||||
| 'monday'
|
|
||||||
| 'tuesday'
|
|
||||||
| 'wednesday'
|
|
||||||
| 'thursday'
|
|
||||||
| 'friday'
|
|
||||||
| 'saturday'
|
|
||||||
| 'sunday';
|
|
||||||
type PrivilegeAction = 'view' | 'create' | 'update' | 'delete' | 'import';
|
type PrivilegeAction = 'view' | 'create' | 'update' | 'delete' | 'import';
|
||||||
|
|
||||||
type PaginationMeta = {
|
type PaginationMeta = {
|
||||||
@@ -932,10 +922,7 @@ type Me = {
|
|||||||
username: string;
|
username: string;
|
||||||
isSuperadmin: boolean;
|
isSuperadmin: boolean;
|
||||||
privilege: { id: Uuid; name: string; code: string } | null;
|
privilege: { id: Uuid; name: string; code: string } | null;
|
||||||
permissions: Record<
|
permissions: Record<string, Record<PrivilegeAction, boolean>>;
|
||||||
string,
|
|
||||||
Record<PrivilegeAction, boolean>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type RouteGeometry = {
|
type RouteGeometry = {
|
||||||
|
|||||||
+1
-3
@@ -29,9 +29,7 @@ describe('CustomersRemoteDataTransformer', () => {
|
|||||||
|
|
||||||
it('includes named contacts on create and omits status', () => {
|
it('includes named contacts on create and omits status', () => {
|
||||||
const payload = transformer.transformCreatePayload(dto);
|
const payload = transformer.transformCreatePayload(dto);
|
||||||
expect(payload.contacts).toEqual([
|
expect(payload.contacts).toEqual([{ name: 'Andi Pratama', jobTitle: 'Manager', phone: '+6281111111111' }]);
|
||||||
{ name: 'Andi Pratama', jobTitle: 'Manager', phone: '+6281111111111' },
|
|
||||||
]);
|
|
||||||
expect(payload).not.toHaveProperty('status');
|
expect(payload).not.toHaveProperty('status');
|
||||||
expect(payload).not.toHaveProperty('id');
|
expect(payload).not.toHaveProperty('id');
|
||||||
});
|
});
|
||||||
|
|||||||
+11
-2
@@ -13,7 +13,11 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
notifications,
|
notifications,
|
||||||
} from '@repo/ui/components';
|
} from '@repo/ui/components';
|
||||||
import { useDetailPageContext, useEnterpriseModuleConfigContext, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
import {
|
||||||
|
useDetailPageContext,
|
||||||
|
useEnterpriseModuleConfigContext,
|
||||||
|
useEnterpriseModuleTranslationContext,
|
||||||
|
} from '@repo/ui/foundations';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Pencil, Plus, Trash2 } from 'lucide-react';
|
import { Pencil, Plus, Trash2 } from 'lucide-react';
|
||||||
@@ -131,7 +135,12 @@ export function DetailContacts() {
|
|||||||
<ActionIcon variant="subtle" onClick={() => openEdit(contact)} aria-label={t('edit_contact')}>
|
<ActionIcon variant="subtle" onClick={() => openEdit(contact)} aria-label={t('edit_contact')}>
|
||||||
<Pencil size={16} />
|
<Pencil size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
<ActionIcon variant="subtle" color="red" onClick={() => handleDelete(contact)} aria-label={t('delete_contact')}>
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
onClick={() => handleDelete(contact)}
|
||||||
|
aria-label={t('delete_contact')}
|
||||||
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
+12
-1
@@ -1,4 +1,15 @@
|
|||||||
import { ActionIcon, Box, Button, FieldTextInput, FieldTextarea, Group, Paper, SimpleGrid, Stack, Text } from '@repo/ui/components';
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FieldTextInput,
|
||||||
|
FieldTextarea,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
} from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
||||||
import { useFieldArray } from '@repo/ui/form';
|
import { useFieldArray } from '@repo/ui/form';
|
||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
|
|||||||
+2
-8
@@ -28,14 +28,8 @@ export const createProductSchema = (t: (key: string) => string) => {
|
|||||||
return z.object({
|
return z.object({
|
||||||
code: configCodeSchema(t, PRODUCT_CODE_MAX),
|
code: configCodeSchema(t, PRODUCT_CODE_MAX),
|
||||||
name: productNameSchema(t),
|
name: productNameSchema(t),
|
||||||
unit: z.preprocess(
|
unit: z.preprocess(emptyToUndefined, compose(z.string(), maxLength(16, t('common:fields.unit'))).optional()),
|
||||||
emptyToUndefined,
|
|
||||||
compose(z.string(), maxLength(16, t('common:fields.unit'))).optional(),
|
|
||||||
),
|
|
||||||
price: optionalDecimalStringSchema(t, 'common:fields.price'),
|
price: optionalDecimalStringSchema(t, 'common:fields.price'),
|
||||||
brand: z.preprocess(
|
brand: z.preprocess(emptyToUndefined, compose(z.string(), maxLength(64, t('common:fields.brand'))).optional()),
|
||||||
emptyToUndefined,
|
|
||||||
compose(z.string(), maxLength(64, t('common:fields.brand'))).optional(),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ describe('CyclesRemoteDataServices', () => {
|
|||||||
|
|
||||||
it('uses PATCH when editing a cycle', async () => {
|
it('uses PATCH when editing a cycle', async () => {
|
||||||
await service.edit('cyc-1', { cycleNumber: 2 } as any);
|
await service.edit('cyc-1', { cycleNumber: 2 } as any);
|
||||||
expect(httpClient.request).toHaveBeenCalledWith(
|
expect(httpClient.request).toHaveBeenCalledWith(expect.objectContaining({ url: '/cycles/cyc-1', method: 'PATCH' }));
|
||||||
expect.objectContaining({ url: '/cycles/cyc-1', method: 'PATCH' }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('bulk-deletes via POST /cycles/bulk-delete', async () => {
|
it('bulk-deletes via POST /cycles/bulk-delete', async () => {
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ export interface CycleDto {
|
|||||||
employeeId: string;
|
employeeId: string;
|
||||||
purpose: FieldPurpose;
|
purpose: FieldPurpose;
|
||||||
cycleNumber: number;
|
cycleNumber: number;
|
||||||
weekdays?: CycleWeekdayEntity[] | Record<string, { startBranchId: string; endBranchId: string; customerIds: string[] }>;
|
weekdays?:
|
||||||
|
| CycleWeekdayEntity[]
|
||||||
|
| Record<string, { startBranchId: string; endBranchId: string; customerIds: string[] }>;
|
||||||
status?: ConfigurationStatus;
|
status?: ConfigurationStatus;
|
||||||
createdAt?: number;
|
createdAt?: number;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
|
|||||||
+1
-6
@@ -2,12 +2,7 @@ import { BaseDataTransformer } from '@repo/core-api/data-services';
|
|||||||
import { omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators';
|
import { omitEmptyFields } from '../../../../../../../core/domain/configuration-field-validators';
|
||||||
import { WEEKDAYS } from '../../../../../../../core/domain/configuration-field-validators';
|
import { WEEKDAYS } from '../../../../../../../core/domain/configuration-field-validators';
|
||||||
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
|
import type { FieldPurpose } from '../../../../../../../core/domain/field-purpose';
|
||||||
import type {
|
import type { CycleEntity, CycleWeekdayEntity, CycleWeekdayRow, CycleWeekdayWrite } from '../entities';
|
||||||
CycleEntity,
|
|
||||||
CycleWeekdayEntity,
|
|
||||||
CycleWeekdayRow,
|
|
||||||
CycleWeekdayWrite,
|
|
||||||
} from '../entities';
|
|
||||||
|
|
||||||
function relationId(value: unknown): string | undefined {
|
function relationId(value: unknown): string | undefined {
|
||||||
if (value && typeof value === 'object' && 'id' in value) {
|
if (value && typeof value === 'object' && 'id' in value) {
|
||||||
|
|||||||
@@ -44,7 +44,18 @@ export default function CyclePageForm({ formPageType }: { formPageType: FormPage
|
|||||||
formControl={formControl}
|
formControl={formControl}
|
||||||
formPageType={formPageType}
|
formPageType={formPageType}
|
||||||
ignoreKeyDuplicate={['id']}
|
ignoreKeyDuplicate={['id']}
|
||||||
ignoreKeyUpdate={['status', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'id', 'purpose', 'employeeId', 'weekdays', 'routeGeometry']}
|
ignoreKeyUpdate={[
|
||||||
|
'status',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'createdBy',
|
||||||
|
'updatedBy',
|
||||||
|
'id',
|
||||||
|
'purpose',
|
||||||
|
'employeeId',
|
||||||
|
'weekdays',
|
||||||
|
'routeGeometry',
|
||||||
|
]}
|
||||||
highlightDataKey="cycleNumber"
|
highlightDataKey="cycleNumber"
|
||||||
pageHeaderProps={{
|
pageHeaderProps={{
|
||||||
title: title?.title,
|
title: title?.title,
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export default function CyclePageIndex() {
|
|||||||
field: 'weekdayRows',
|
field: 'weekdayRows',
|
||||||
headerName: t('section_weekdays'),
|
headerName: t('section_weekdays'),
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
valueGetter: ({ data }) => data?.weekdayRows?.filter((row) => row.enabled).length ?? data?.weekdays?.length ?? 0,
|
valueGetter: ({ data }) =>
|
||||||
|
data?.weekdayRows?.filter((row) => row.enabled).length ?? data?.weekdays?.length ?? 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|||||||
+3
-1
@@ -65,7 +65,9 @@ export class PlansRemoteDataTransformer extends BaseDataTransformer<PlanEntity>
|
|||||||
if (this.purpose === 'sales') {
|
if (this.purpose === 'sales') {
|
||||||
payload.invoiceIds = relationIds(entity.invoices).length ? relationIds(entity.invoices) : entity.invoiceIds;
|
payload.invoiceIds = relationIds(entity.invoices).length ? relationIds(entity.invoices) : entity.invoiceIds;
|
||||||
} else {
|
} else {
|
||||||
payload.packingSlipIds = relationIds(entity.packingSlips).length ? relationIds(entity.packingSlips) : entity.packingSlipIds;
|
payload.packingSlipIds = relationIds(entity.packingSlips).length
|
||||||
|
? relationIds(entity.packingSlips)
|
||||||
|
: entity.packingSlipIds;
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-6
@@ -1,6 +1,24 @@
|
|||||||
import { ActionIcon, Box, Button, FieldAsyncSelect, FieldValue, Group, Paper, RenderDate, SimpleGrid, Stack, StatusBadge, Text, notifications } from '@repo/ui/components';
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FieldAsyncSelect,
|
||||||
|
FieldValue,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
RenderDate,
|
||||||
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
|
StatusBadge,
|
||||||
|
Text,
|
||||||
|
notifications,
|
||||||
|
} from '@repo/ui/components';
|
||||||
import { RouteMap } from '@repo/ui/map';
|
import { RouteMap } from '@repo/ui/map';
|
||||||
import { useDetailPageContext, useEnterpriseModuleTranslationContext, useEnterpriseModuleDataServiceContext } from '@repo/ui/foundations';
|
import {
|
||||||
|
useDetailPageContext,
|
||||||
|
useEnterpriseModuleTranslationContext,
|
||||||
|
useEnterpriseModuleDataServiceContext,
|
||||||
|
} from '@repo/ui/foundations';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
import { relationLabel } from '../../../../shared/relation-label';
|
import { relationLabel } from '../../../../shared/relation-label';
|
||||||
@@ -39,10 +57,24 @@ export function DetailGeneral() {
|
|||||||
</Text>
|
</Text>
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" verticalSpacing="xl">
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" verticalSpacing="xl">
|
||||||
<FieldValue label={t('common:fields.employee')} value={relationLabel(data?.employee) || data?.employeeId} />
|
<FieldValue label={t('common:fields.employee')} value={relationLabel(data?.employee) || data?.employeeId} />
|
||||||
<FieldValue label={t('common:fields.date')} value={data?.date} render={(val) => <RenderDate value={val as any} />} />
|
<FieldValue
|
||||||
<FieldValue label={t('common:fields.startBranch')} value={relationLabel(data?.startBranch) || data?.startBranchId} />
|
label={t('common:fields.date')}
|
||||||
<FieldValue label={t('common:fields.endBranch')} value={relationLabel(data?.endBranch) || data?.endBranchId} />
|
value={data?.date}
|
||||||
<FieldValue label={t('common:fields.status')} value={data?.status} render={(val) => <StatusBadge status={String(val ?? '')} />} />
|
render={(val) => <RenderDate value={val as any} />}
|
||||||
|
/>
|
||||||
|
<FieldValue
|
||||||
|
label={t('common:fields.startBranch')}
|
||||||
|
value={relationLabel(data?.startBranch) || data?.startBranchId}
|
||||||
|
/>
|
||||||
|
<FieldValue
|
||||||
|
label={t('common:fields.endBranch')}
|
||||||
|
value={relationLabel(data?.endBranch) || data?.endBranchId}
|
||||||
|
/>
|
||||||
|
<FieldValue
|
||||||
|
label={t('common:fields.status')}
|
||||||
|
value={data?.status}
|
||||||
|
render={(val) => <StatusBadge status={String(val ?? '')} />}
|
||||||
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -1,5 +1,9 @@
|
|||||||
import { Box, FieldAsyncSelect, FieldDatePicker, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
import { Box, FieldAsyncSelect, FieldDatePicker, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useFormPageContext, useEnterpriseModuleConfigContext } from '@repo/ui/foundations';
|
import {
|
||||||
|
useEnterpriseModuleTranslationContext,
|
||||||
|
useFormPageContext,
|
||||||
|
useEnterpriseModuleConfigContext,
|
||||||
|
} from '@repo/ui/foundations';
|
||||||
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
|
import { purposeFromModuleKey } from '../../../../../../../../core/domain/field-purpose';
|
||||||
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
|
import { loadEmployeeOptions } from '../../../../shared/load-employee-options';
|
||||||
import { loadBranchOptions } from '../../../../shared/load-branch-options';
|
import { loadBranchOptions } from '../../../../shared/load-branch-options';
|
||||||
|
|||||||
@@ -39,7 +39,20 @@ export default function PlanPageForm({ formPageType }: { formPageType: FormPageT
|
|||||||
formControl={formControl}
|
formControl={formControl}
|
||||||
formPageType={formPageType}
|
formPageType={formPageType}
|
||||||
ignoreKeyDuplicate={['id']}
|
ignoreKeyDuplicate={['id']}
|
||||||
ignoreKeyUpdate={['status', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'id', 'purpose', 'employeeId', 'destinations', 'routeGeometry', 'invoiceIds', 'packingSlipIds']}
|
ignoreKeyUpdate={[
|
||||||
|
'status',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'createdBy',
|
||||||
|
'updatedBy',
|
||||||
|
'id',
|
||||||
|
'purpose',
|
||||||
|
'employeeId',
|
||||||
|
'destinations',
|
||||||
|
'routeGeometry',
|
||||||
|
'invoiceIds',
|
||||||
|
'packingSlipIds',
|
||||||
|
]}
|
||||||
highlightDataKey="date"
|
highlightDataKey="date"
|
||||||
pageHeaderProps={{
|
pageHeaderProps={{
|
||||||
title: title?.title,
|
title: title?.title,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export function relationLabel(
|
export function relationLabel(item: { code?: string | null; name?: string; id?: string | number } | null | undefined) {
|
||||||
item: { code?: string | null; name?: string; id?: string | number } | null | undefined,
|
|
||||||
) {
|
|
||||||
if (!item) return '';
|
if (!item) return '';
|
||||||
if (item.code && item.name) return `${item.code} - ${item.name}`;
|
if (item.code && item.name) return `${item.code} - ${item.name}`;
|
||||||
return item.name || item.code || (item.id == null ? '' : String(item.id));
|
return item.name || item.code || (item.id == null ? '' : String(item.id));
|
||||||
|
|||||||
+3
-1
@@ -24,7 +24,9 @@ export default function SalesOrderPageDetail() {
|
|||||||
{ label: t('nav:sales-orders'), type: 'link', href: `${salesOrdersModuleConfig.webUrl}/index` },
|
{ label: t('nav:sales-orders'), type: 'link', href: `${salesOrdersModuleConfig.webUrl}/index` },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
customPageActions={(data, pageActions) => actions.detailStatusActions(data as SalesOrderEntity, pageActions ?? [])}
|
customPageActions={(data, pageActions) =>
|
||||||
|
actions.detailStatusActions(data as SalesOrderEntity, pageActions ?? [])
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<DetailGeneral salesRequestHref={(id) => `${salesRequestsModuleConfig.webUrl}/detail/${id}`} />
|
<DetailGeneral salesRequestHref={(id) => `${salesRequestsModuleConfig.webUrl}/detail/${id}`} />
|
||||||
|
|||||||
+10
-1
@@ -62,7 +62,16 @@ export default function SalesOrderPageForm({ formPageType }: { formPageType: For
|
|||||||
formControl={formControl}
|
formControl={formControl}
|
||||||
formPageType={formPageType}
|
formPageType={formPageType}
|
||||||
ignoreKeyDuplicate={['code']}
|
ignoreKeyDuplicate={['code']}
|
||||||
ignoreKeyUpdate={['status', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy', 'id', 'salesRequestId', 'salesRequest']}
|
ignoreKeyUpdate={[
|
||||||
|
'status',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'createdBy',
|
||||||
|
'updatedBy',
|
||||||
|
'id',
|
||||||
|
'salesRequestId',
|
||||||
|
'salesRequest',
|
||||||
|
]}
|
||||||
highlightDataKey="code"
|
highlightDataKey="code"
|
||||||
pageHeaderProps={{
|
pageHeaderProps={{
|
||||||
title: title?.title,
|
title: title?.title,
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ export function DetailGeneral({
|
|||||||
}: {
|
}: {
|
||||||
salesRequestHref?: (id: string) => string;
|
salesRequestHref?: (id: string) => string;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { detailData } = useDetailPageContext<SalesDocumentEntity & { salesRequestId?: string | null; salesRequest?: { id?: string; code?: string } | null }>();
|
const { detailData } = useDetailPageContext<
|
||||||
|
SalesDocumentEntity & { salesRequestId?: string | null; salesRequest?: { id?: string; code?: string } | null }
|
||||||
|
>();
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
const data = detailData;
|
const data = detailData;
|
||||||
const salesRequestId = data?.salesRequestId ?? data?.salesRequest?.id;
|
const salesRequestId = data?.salesRequestId ?? data?.salesRequest?.id;
|
||||||
@@ -26,7 +28,10 @@ export function DetailGeneral({
|
|||||||
value={data?.date}
|
value={data?.date}
|
||||||
render={(val) => <RenderDate value={val as any} />}
|
render={(val) => <RenderDate value={val as any} />}
|
||||||
/>
|
/>
|
||||||
<FieldValue label={t('common:fields.salesPerson')} value={relationLabel(data?.salesPerson) || data?.salesPersonId} />
|
<FieldValue
|
||||||
|
label={t('common:fields.salesPerson')}
|
||||||
|
value={relationLabel(data?.salesPerson) || data?.salesPersonId}
|
||||||
|
/>
|
||||||
<FieldValue label={t('common:fields.branch')} value={relationLabel(data?.branch) || data?.branchId} />
|
<FieldValue label={t('common:fields.branch')} value={relationLabel(data?.branch) || data?.branchId} />
|
||||||
<FieldValue label={t('common:fields.division')} value={relationLabel(data?.division) || data?.divisionId} />
|
<FieldValue label={t('common:fields.division')} value={relationLabel(data?.division) || data?.divisionId} />
|
||||||
<FieldValue label={t('common:fields.customer')} value={relationLabel(data?.customer) || data?.customerId} />
|
<FieldValue label={t('common:fields.customer')} value={relationLabel(data?.customer) || data?.customerId} />
|
||||||
@@ -36,9 +41,7 @@ export function DetailGeneral({
|
|||||||
value={salesRequestId}
|
value={salesRequestId}
|
||||||
render={() =>
|
render={() =>
|
||||||
salesRequestId ? (
|
salesRequestId ? (
|
||||||
<Anchor href={salesRequestHref(salesRequestId)}>
|
<Anchor href={salesRequestHref(salesRequestId)}>{data?.salesRequest?.code || salesRequestId}</Anchor>
|
||||||
{data?.salesRequest?.code || salesRequestId}
|
|
||||||
</Anchor>
|
|
||||||
) : (
|
) : (
|
||||||
'-'
|
'-'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ export function DetailLocation() {
|
|||||||
{t('section_location')}
|
{t('section_location')}
|
||||||
</Text>
|
</Text>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<LocationMap
|
<LocationMap latitude={data?.latitude} longitude={data?.longitude} emptyLabel={t('common:map.noLocation')} />
|
||||||
latitude={data?.latitude}
|
|
||||||
longitude={data?.longitude}
|
|
||||||
emptyLabel={t('common:map.noLocation')}
|
|
||||||
/>
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" verticalSpacing="xl">
|
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} spacing="md" verticalSpacing="xl">
|
||||||
<FieldValue label={t('common:fields.address')} value={data?.address} />
|
<FieldValue label={t('common:fields.address')} value={data?.address} />
|
||||||
<FieldValue label={t('common:fields.latitude')} value={data?.latitude} />
|
<FieldValue label={t('common:fields.latitude')} value={data?.latitude} />
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ export function FormGeneral() {
|
|||||||
placeholder="e.g. SR-20260826-0001"
|
placeholder="e.g. SR-20260826-0001"
|
||||||
radius="md"
|
radius="md"
|
||||||
/>
|
/>
|
||||||
<FieldDatePicker control={formControl.control} name="date" label={t('common:fields.date')} required radius="md" />
|
<FieldDatePicker
|
||||||
|
control={formControl.control}
|
||||||
|
name="date"
|
||||||
|
label={t('common:fields.date')}
|
||||||
|
required
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
<FieldAsyncSelect<EmployeeEntity>
|
<FieldAsyncSelect<EmployeeEntity>
|
||||||
control={formControl.control}
|
control={formControl.control}
|
||||||
name="salesPerson"
|
name="salesPerson"
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ export function FormImages() {
|
|||||||
<Paper withBorder shadow="sm" radius="md" p="xl">
|
<Paper withBorder shadow="sm" radius="md" p="xl">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Text fw={600}>{t('section_images')}</Text>
|
<Text fw={600}>{t('section_images')}</Text>
|
||||||
<Button variant="light" size="xs" leftSection={<Plus size={14} />} onClick={() => append({ url: '', description: '' })}>
|
<Button
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
leftSection={<Plus size={14} />}
|
||||||
|
onClick={() => append({ url: '', description: '' })}
|
||||||
|
>
|
||||||
{t('add_image')}
|
{t('add_image')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import { ActionIcon, Box, Button, FieldAsyncSelect, FieldTextInput, Group, Paper, Table, Text } from '@repo/ui/components';
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FieldAsyncSelect,
|
||||||
|
FieldTextInput,
|
||||||
|
Group,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
} from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
||||||
import { useFieldArray, useWatch } from '@repo/ui/form';
|
import { useFieldArray, useWatch } from '@repo/ui/form';
|
||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
@@ -70,7 +80,12 @@ export function FormProducts() {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta="right">{currency.format(lineTotal(line?.quantity, line?.price))}</Table.Td>
|
<Table.Td ta="right">{currency.format(lineTotal(line?.quantity, line?.price))}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<ActionIcon variant="subtle" color="red" onClick={() => remove(index)} aria-label={t('remove_line')}>
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
aria-label={t('remove_line')}
|
||||||
|
>
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ export function toSalesWritePayload(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options?.includeSalesRequestId) {
|
if (options?.includeSalesRequestId) {
|
||||||
payload.salesRequestId = relationId((entity as { salesRequest?: unknown }).salesRequest) ?? (entity as { salesRequestId?: string }).salesRequestId;
|
payload.salesRequestId =
|
||||||
|
relationId((entity as { salesRequest?: unknown }).salesRequest) ??
|
||||||
|
(entity as { salesRequestId?: string }).salesRequestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { compose, maxLength, required } from '@repo/ui/validators';
|
import { compose, maxLength, required } from '@repo/ui/validators';
|
||||||
import { configAddressSchema, optionalLatitudeSchema, optionalLongitudeSchema } from '../../../../../core/domain/configuration-field-validators';
|
import {
|
||||||
|
configAddressSchema,
|
||||||
|
optionalLatitudeSchema,
|
||||||
|
optionalLongitudeSchema,
|
||||||
|
} from '../../../../../core/domain/configuration-field-validators';
|
||||||
import { decimalStringSchema } from '../../../../../core/domain/decimal-string.schema';
|
import { decimalStringSchema } from '../../../../../core/domain/decimal-string.schema';
|
||||||
|
|
||||||
const NOTES_MAX = 1024;
|
const NOTES_MAX = 1024;
|
||||||
|
|||||||
@@ -82,7 +82,10 @@ export function useSalesDocumentActions(documentType: SalesDocumentType) {
|
|||||||
icon: <Icon size={16} />,
|
icon: <Icon size={16} />,
|
||||||
variant: 'light' as const,
|
variant: 'light' as const,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
void applyStatus(selectedRows.map((row) => String(row.id)), action.target);
|
void applyStatus(
|
||||||
|
selectedRows.map((row) => String(row.id)),
|
||||||
|
action.target,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-3
@@ -95,9 +95,7 @@ describe('PrivilegesRemoteDataTransformer', () => {
|
|||||||
|
|
||||||
expect(payload).not.toHaveProperty('status');
|
expect(payload).not.toHaveProperty('status');
|
||||||
expect(payload).not.toHaveProperty('id');
|
expect(payload).not.toHaveProperty('id');
|
||||||
expect(payload.details).toEqual(
|
expect(payload.details).toEqual(expect.arrayContaining([{ privilegeKeyId: 'key-1', action: 'view', value: true }]));
|
||||||
expect.arrayContaining([{ privilegeKeyId: 'key-1', action: 'view', value: true }]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps an empty details list to an empty matrix', () => {
|
it('maps an empty details list to an empty matrix', () => {
|
||||||
|
|||||||
+1
-5
@@ -56,11 +56,7 @@ export function FormPermissions() {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
{PRIVILEGE_ACTIONS.map((action) => (
|
{PRIVILEGE_ACTIONS.map((action) => (
|
||||||
<Table.Td key={action} ta="center">
|
<Table.Td key={action} ta="center">
|
||||||
<FieldCheckbox
|
<FieldCheckbox control={formControl.control} name={`matrix.${key.id}.${action}`} label="" />
|
||||||
control={formControl.control}
|
|
||||||
name={`matrix.${key.id}.${action}`}
|
|
||||||
label=""
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
))}
|
))}
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ describe('UsersRemoteDataServices', () => {
|
|||||||
|
|
||||||
it('uses PATCH when editing a user', async () => {
|
it('uses PATCH when editing a user', async () => {
|
||||||
await service.edit('user-1', { username: 'alice' } as any);
|
await service.edit('user-1', { username: 'alice' } as any);
|
||||||
expect(httpClient.request).toHaveBeenCalledWith(
|
expect(httpClient.request).toHaveBeenCalledWith(expect.objectContaining({ url: '/users/user-1', method: 'PATCH' }));
|
||||||
expect.objectContaining({ url: '/users/user-1', method: 'PATCH' }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('bulk-deletes via POST /users/bulk-delete', async () => {
|
it('bulk-deletes via POST /users/bulk-delete', async () => {
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ export const createUserSchema = (t: (key: string) => string, options?: { require
|
|||||||
: z.union([z.literal(''), compose(z.string(), rangeLength(8, 72, passwordLabel))]).optional();
|
: z.union([z.literal(''), compose(z.string(), rangeLength(8, 72, passwordLabel))]).optional();
|
||||||
|
|
||||||
return z.object({
|
return z.object({
|
||||||
username: compose(
|
username: compose(z.string(), required(usernameLabel), rangeLength(3, 32, usernameLabel), (schema: z.ZodString) =>
|
||||||
z.string(),
|
|
||||||
required(usernameLabel),
|
|
||||||
rangeLength(3, 32, usernameLabel),
|
|
||||||
(schema: z.ZodString) =>
|
|
||||||
schema.regex(USERNAME_PATTERN, {
|
schema.regex(USERNAME_PATTERN, {
|
||||||
message: JSON.stringify({ key: 'validation:invalid_format', values: { field: usernameLabel } }),
|
message: JSON.stringify({ key: 'validation:invalid_format', values: { field: usernameLabel } }),
|
||||||
}),
|
}),
|
||||||
|
|||||||
+11
-7
@@ -1,5 +1,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Box, FieldTextInput, FieldPasswordInput, FieldAsyncSelect, Paper, SimpleGrid, Text } from '@repo/ui/components';
|
import {
|
||||||
|
Box,
|
||||||
|
FieldTextInput,
|
||||||
|
FieldPasswordInput,
|
||||||
|
FieldAsyncSelect,
|
||||||
|
Paper,
|
||||||
|
SimpleGrid,
|
||||||
|
Text,
|
||||||
|
} from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
import { useEnterpriseModuleTranslationContext, useFormPageContext } from '@repo/ui/foundations';
|
||||||
import { loadPrivilegeOptions } from '../../load-privilege-options';
|
import { loadPrivilegeOptions } from '../../load-privilege-options';
|
||||||
import { loadEmployeeOptions } from '../../load-employee-options';
|
import { loadEmployeeOptions } from '../../load-employee-options';
|
||||||
@@ -34,15 +42,11 @@ export function FormGeneral({ requirePassword }: { requirePassword: boolean }) {
|
|||||||
const employee = formControl.watch('employee');
|
const employee = formControl.watch('employee');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hydrateRelation(formControl, 'privilege', 'privilegeId', privilege, (id) =>
|
hydrateRelation(formControl, 'privilege', 'privilegeId', privilege, (id) => privilegesDataService.getOne(id));
|
||||||
privilegesDataService.getOne(id),
|
|
||||||
);
|
|
||||||
}, [privilege, formControl]);
|
}, [privilege, formControl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hydrateRelation(formControl, 'employee', 'employeeId', employee, (id) =>
|
hydrateRelation(formControl, 'employee', 'employeeId', employee, (id) => employeesDataService.getOne(id));
|
||||||
employeesDataService.getOne(id),
|
|
||||||
);
|
|
||||||
}, [employee, formControl]);
|
}, [employee, formControl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,11 +21,7 @@ registerModuleNamespace(usersModuleConfig.translationNamespace, {
|
|||||||
|
|
||||||
export default function UsersModule() {
|
export default function UsersModule() {
|
||||||
return (
|
return (
|
||||||
<EnterpriseModuleProvider<UserEntity>
|
<EnterpriseModuleProvider<UserEntity> config={usersModuleConfig} dataServices={usersDataService} store={usersStore}>
|
||||||
config={usersModuleConfig}
|
|
||||||
dataServices={usersDataService}
|
|
||||||
store={usersStore}
|
|
||||||
>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/index" element={<IndexPage />} />
|
<Route path="/index" element={<IndexPage />} />
|
||||||
<Route path="/detail/:dataId" element={<DetailPage />} />
|
<Route path="/detail/:dataId" element={<DetailPage />} />
|
||||||
|
|||||||
@@ -33,13 +33,12 @@ function emptyToUndefinedNumber(value: unknown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function configCodeSchema(t: (key: string) => string, max = CONFIG_CODE_MAX) {
|
export function configCodeSchema(t: (key: string) => string, max = CONFIG_CODE_MAX) {
|
||||||
return compose(
|
return compose(z.string(), required(t('common:fields.code')), maxLength(max, t('common:fields.code'))).regex(
|
||||||
z.string(),
|
CODE_PATTERN,
|
||||||
required(t('common:fields.code')),
|
{
|
||||||
maxLength(max, t('common:fields.code')),
|
|
||||||
).regex(CODE_PATTERN, {
|
|
||||||
message: JSON.stringify({ key: 'validation:invalid_format', values: { field: t('common:fields.code') } }),
|
message: JSON.stringify({ key: 'validation:invalid_format', values: { field: t('common:fields.code') } }),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configNameSchema(t: (key: string) => string) {
|
export function configNameSchema(t: (key: string) => string) {
|
||||||
@@ -69,13 +68,20 @@ export function optionalPhoneSchema() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function configAddressSchema(t: (key: string) => string) {
|
export function configAddressSchema(t: (key: string) => string) {
|
||||||
return compose(z.string(), required(t('common:fields.address')), maxLength(CONFIG_ADDRESS_MAX, t('common:fields.address')));
|
return compose(
|
||||||
|
z.string(),
|
||||||
|
required(t('common:fields.address')),
|
||||||
|
maxLength(CONFIG_ADDRESS_MAX, t('common:fields.address')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function weekdaySchema(t: (key: string) => string) {
|
export function weekdaySchema(t: (key: string) => string) {
|
||||||
return z.enum(WEEKDAYS, {
|
return z.enum(WEEKDAYS, {
|
||||||
required_error: JSON.stringify({ key: 'validation:required', values: { field: t('common:fields.weekday') } }),
|
required_error: JSON.stringify({ key: 'validation:required', values: { field: t('common:fields.weekday') } }),
|
||||||
invalid_type_error: JSON.stringify({ key: 'validation:invalid_format', values: { field: t('common:fields.weekday') } }),
|
invalid_type_error: JSON.stringify({
|
||||||
|
key: 'validation:invalid_format',
|
||||||
|
values: { field: t('common:fields.weekday') },
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +92,19 @@ export function workingHoursSchema(t: (key: string) => string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function optionalNfcIdSchema(t: (key: string) => string) {
|
export function optionalNfcIdSchema(t: (key: string) => string) {
|
||||||
return z.preprocess(emptyToUndefined, compose(z.string(), maxLength(CONFIG_NFC_ID_MAX, t('common:fields.nfcId'))).optional());
|
return z.preprocess(
|
||||||
|
emptyToUndefined,
|
||||||
|
compose(z.string(), maxLength(CONFIG_NFC_ID_MAX, t('common:fields.nfcId'))).optional(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function optionalLatitudeSchema() {
|
export function optionalLatitudeSchema() {
|
||||||
return z.preprocess(
|
return z.preprocess(
|
||||||
emptyToUndefinedNumber,
|
emptyToUndefinedNumber,
|
||||||
compose(
|
compose(
|
||||||
z.number({ invalid_type_error: JSON.stringify({ key: 'validation:invalid_format', values: { field: 'latitude' } }) }),
|
z.number({
|
||||||
|
invalid_type_error: JSON.stringify({ key: 'validation:invalid_format', values: { field: 'latitude' } }),
|
||||||
|
}),
|
||||||
minValue(-90, 'latitude'),
|
minValue(-90, 'latitude'),
|
||||||
maxValue(90, 'latitude'),
|
maxValue(90, 'latitude'),
|
||||||
).optional(),
|
).optional(),
|
||||||
@@ -104,7 +115,9 @@ export function optionalLongitudeSchema() {
|
|||||||
return z.preprocess(
|
return z.preprocess(
|
||||||
emptyToUndefinedNumber,
|
emptyToUndefinedNumber,
|
||||||
compose(
|
compose(
|
||||||
z.number({ invalid_type_error: JSON.stringify({ key: 'validation:invalid_format', values: { field: 'longitude' } }) }),
|
z.number({
|
||||||
|
invalid_type_error: JSON.stringify({ key: 'validation:invalid_format', values: { field: 'longitude' } }),
|
||||||
|
}),
|
||||||
minValue(-180, 'longitude'),
|
minValue(-180, 'longitude'),
|
||||||
maxValue(180, 'longitude'),
|
maxValue(180, 'longitude'),
|
||||||
).optional(),
|
).optional(),
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ export function filterMenuByViewPrivilege<T extends { moduleKey?: string; childr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items.flatMap((item) => {
|
return items.flatMap((item) => {
|
||||||
const children = item.children
|
const children = item.children ? filterMenuByViewPrivilege(item.children, privileges, false) : undefined;
|
||||||
? filterMenuByViewPrivilege(item.children, privileges, false)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (item.moduleKey && privileges[item.moduleKey]?.ALLOW_VIEW !== true) {
|
if (item.moduleKey && privileges[item.moduleKey]?.ALLOW_VIEW !== true) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ const { setView, panTo, invalidateSize } = vi.hoisted(() => ({
|
|||||||
vi.mock('react-leaflet', () => ({
|
vi.mock('react-leaflet', () => ({
|
||||||
MapContainer: ({ children }: { children: React.ReactNode }) => <div data-testid="osm-map">{children}</div>,
|
MapContainer: ({ children }: { children: React.ReactNode }) => <div data-testid="osm-map">{children}</div>,
|
||||||
TileLayer: () => <div data-testid="osm-tiles" />,
|
TileLayer: () => <div data-testid="osm-tiles" />,
|
||||||
CircleMarker: ({ center }: { center: [number, number] }) => <div data-testid="location-marker">{`${center[0]},${center[1]}`}</div>,
|
CircleMarker: ({ center }: { center: [number, number] }) => (
|
||||||
|
<div data-testid="location-marker">{`${center[0]},${center[1]}`}</div>
|
||||||
|
),
|
||||||
useMap: () => ({ setView, panTo, invalidateSize }),
|
useMap: () => ({ setView, panTo, invalidateSize }),
|
||||||
useMapEvents: (handlers: { click?: (event: LeafletClick) => void }) => {
|
useMapEvents: (handlers: { click?: (event: LeafletClick) => void }) => {
|
||||||
mapClick = handlers.click;
|
mapClick = handlers.click;
|
||||||
|
|||||||
@@ -123,10 +123,19 @@ export function LocationMap({
|
|||||||
className="tg-map-viewport"
|
className="tg-map-viewport"
|
||||||
style={{ cursor: interactive ? 'crosshair' : undefined }}
|
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} />
|
||||||
{position ? (
|
{position ? (
|
||||||
<CircleMarker center={position} radius={10} pathOptions={{ color: 'var(--mantine-color-blue-8)', fillOpacity: 0.9 }} />
|
<CircleMarker
|
||||||
|
center={position}
|
||||||
|
radius={10}
|
||||||
|
pathOptions={{ color: 'var(--mantine-color-blue-8)', fillOpacity: 0.9 }}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<SyncMapView latitude={latitude} longitude={longitude} lastPickedRef={lastPickedRef} />
|
<SyncMapView latitude={latitude} longitude={longitude} lastPickedRef={lastPickedRef} />
|
||||||
{onChange ? <MapClickPicker onPick={handlePick} /> : null}
|
{onChange ? <MapClickPicker onPick={handlePick} /> : null}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function clampLatitude(value: number): number {
|
|||||||
// which the form schemas reject. Keep 180 and -180 as authored instead of collapsing them.
|
// which the form schemas reject. Keep 180 and -180 as authored instead of collapsing them.
|
||||||
export function wrapLongitude(value: number): number {
|
export function wrapLongitude(value: number): number {
|
||||||
if (value >= -180 && value <= 180) return value;
|
if (value >= -180 && value <= 180) return value;
|
||||||
const wrapped = ((value + 180) % 360 + 360) % 360 - 180;
|
const wrapped = ((((value + 180) % 360) + 360) % 360) - 180;
|
||||||
return wrapped;
|
return wrapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +54,7 @@ export function locationFromLatLng(lat: number, lng: number): { latitude: number
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapViewForLocation(
|
export function mapViewForLocation(latitude: unknown, longitude: unknown): { center: [number, number]; zoom: number } {
|
||||||
latitude: unknown,
|
|
||||||
longitude: unknown,
|
|
||||||
): { center: [number, number]; zoom: number } {
|
|
||||||
const point = toLocationLatLng(latitude, longitude);
|
const point = toLocationLatLng(latitude, longitude);
|
||||||
if (!point) {
|
if (!point) {
|
||||||
return { center: DEFAULT_MAP_CENTER, zoom: DEFAULT_MAP_ZOOM };
|
return { center: DEFAULT_MAP_CENTER, zoom: DEFAULT_MAP_ZOOM };
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export const OSM_TILE_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
export const OSM_TILE_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||||
export const OSM_ATTRIBUTION = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
export const OSM_ATTRIBUTION =
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||||
|
|||||||
@@ -7,7 +7,5 @@ export function toLeafletLatLngs(geometry?: RouteGeometry | null): Array<[number
|
|||||||
if (!geometry?.coordinates?.length) {
|
if (!geometry?.coordinates?.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return geometry.coordinates
|
return geometry.coordinates.filter((pair) => Array.isArray(pair) && pair.length >= 2).map(([lng, lat]) => [lat, lng]);
|
||||||
.filter((pair) => Array.isArray(pair) && pair.length >= 2)
|
|
||||||
.map(([lng, lat]) => [lat, lng]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ export function RouteMap({ geometry, height = 280 }: RouteMapProps) {
|
|||||||
<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 }} />
|
||||||
{positions.map((position, index) => (
|
{positions.map((position, index) => (
|
||||||
<CircleMarker key={`${position[0]}-${position[1]}-${index}`} center={position} radius={8} pathOptions={{ color: 'var(--mantine-color-blue-8)' }}>
|
<CircleMarker
|
||||||
|
key={`${position[0]}-${position[1]}-${index}`}
|
||||||
|
center={position}
|
||||||
|
radius={8}
|
||||||
|
pathOptions={{ color: 'var(--mantine-color-blue-8)' }}
|
||||||
|
>
|
||||||
<Tooltip permanent>{index + 1}</Tooltip>
|
<Tooltip permanent>{index + 1}</Tooltip>
|
||||||
</CircleMarker>
|
</CircleMarker>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+5
-12
@@ -1,24 +1,17 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { DateUtils } from '@repo/utils';
|
import { DateUtils } from '@repo/utils';
|
||||||
import {
|
import { EMPTY_AUDIT_DISPLAY, formatAuditActor, formatAuditTimestamp, resolveAuditValue } from './audit-column.utils';
|
||||||
EMPTY_AUDIT_DISPLAY,
|
|
||||||
formatAuditActor,
|
|
||||||
formatAuditTimestamp,
|
|
||||||
resolveAuditValue,
|
|
||||||
} from './audit-column.utils';
|
|
||||||
|
|
||||||
describe('resolveAuditValue', () => {
|
describe('resolveAuditValue', () => {
|
||||||
it('prefers camelCase over snake_case', () => {
|
it('prefers camelCase over snake_case', () => {
|
||||||
expect(
|
expect(resolveAuditValue({ createdBy: 'alice', creator_name: 'legacy' }, 'createdBy', 'creator_name')).toBe(
|
||||||
resolveAuditValue({ createdBy: 'alice', creator_name: 'legacy' }, 'createdBy', 'creator_name'),
|
'alice',
|
||||||
).toBe('alice');
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to snake_case when camelCase is missing', () => {
|
it('falls back to snake_case when camelCase is missing', () => {
|
||||||
expect(resolveAuditValue({ creator_name: 'legacy' }, 'createdBy', 'creator_name')).toBe('legacy');
|
expect(resolveAuditValue({ creator_name: 'legacy' }, 'createdBy', 'creator_name')).toBe('legacy');
|
||||||
expect(resolveAuditValue({ created_at: 1_700_000_000_000 }, 'createdAt', 'created_at')).toBe(
|
expect(resolveAuditValue({ created_at: 1_700_000_000_000 }, 'createdAt', 'created_at')).toBe(1_700_000_000_000);
|
||||||
1_700_000_000_000,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined when neither field is present', () => {
|
it('returns undefined when neither field is present', () => {
|
||||||
|
|||||||
+4
-1
@@ -63,7 +63,10 @@ export function BulkActionMenu({
|
|||||||
|
|
||||||
const hasActive = selectedRows.some((row) => row[statusKey]?.toLowerCase() === 'active');
|
const hasActive = selectedRows.some((row) => row[statusKey]?.toLowerCase() === 'active');
|
||||||
const hasInactive = selectedRows.some(
|
const hasInactive = selectedRows.some(
|
||||||
(row) => row[statusKey]?.toLowerCase() === 'inactive' || row[statusKey]?.toLowerCase() === 'draft' || row[statusKey]?.toLowerCase() === 'archived',
|
(row) =>
|
||||||
|
row[statusKey]?.toLowerCase() === 'inactive' ||
|
||||||
|
row[statusKey]?.toLowerCase() === 'draft' ||
|
||||||
|
row[statusKey]?.toLowerCase() === 'archived',
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultActions: PageActionProps[] = [];
|
const defaultActions: PageActionProps[] = [];
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
const filterKeys = filterConfig?.defaultValues
|
const filterKeys = filterConfig?.defaultValues
|
||||||
? Object.keys(filterConfig.defaultValues)
|
? Object.keys(filterConfig.defaultValues)
|
||||||
: Object.keys(filterData).filter(
|
: Object.keys(filterData).filter(
|
||||||
(key) => key !== searchKey && !['page', 'limit', 'order_by', 'order_type'].includes(key),
|
(key) => key !== searchKey && !['page', 'limit', 'orderBy', 'orderType'].includes(key),
|
||||||
);
|
);
|
||||||
|
|
||||||
return filterKeys.filter((key) => {
|
return filterKeys.filter((key) => {
|
||||||
@@ -773,8 +773,8 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
const requestParams: Record<string, any> = {
|
const requestParams: Record<string, any> = {
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
order_by: orderBy,
|
orderBy: orderBy,
|
||||||
order_type: orderType,
|
orderType: orderType,
|
||||||
...filterRef.current,
|
...filterRef.current,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -504,7 +504,8 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
|||||||
const isMasterData = moduleType === 'MASTER_DATA';
|
const isMasterData = moduleType === 'MASTER_DATA';
|
||||||
|
|
||||||
const isDataActive = detailData && ['active'].includes(detailData[statusKey]?.toLowerCase());
|
const isDataActive = detailData && ['active'].includes(detailData[statusKey]?.toLowerCase());
|
||||||
const isDataInActive = detailData && ['inactive', 'draft', 'archived'].includes(detailData[statusKey]?.toLowerCase());
|
const isDataInActive =
|
||||||
|
detailData && ['inactive', 'draft', 'archived'].includes(detailData[statusKey]?.toLowerCase());
|
||||||
|
|
||||||
// 1. Declare action with Privilege & Module Type conditions directly
|
// 1. Declare action with Privilege & Module Type conditions directly
|
||||||
const rawActions = [
|
const rawActions = [
|
||||||
|
|||||||
Reference in New Issue
Block a user