feat(ui):

This commit is contained in:
Marshal
2026-08-07 23:00:06 +00:00
parent 6b3c055a93
commit d2eb47d14b
6 changed files with 74 additions and 47 deletions

View File

@@ -76,6 +76,12 @@ SEED_EDR_ORG=true
SEED_FREIGHT_STAFF=true SEED_FREIGHT_STAFF=true
SEED_EXPORT_DJIBOUTI_INTERCHANGE_DEMO=false SEED_EXPORT_DJIBOUTI_INTERCHANGE_DEMO=false
# Limits GET /staff/users to employees of this IAM organization (iam.organizations.key).
# Unset = every employee. A key matching no organization returns no users.
# Dev seed key: edr_freight
# Production: ETHIO_DJIBOUTI_STANDARD_GAUGE_RAILWAY_SHARE_COMPANY_001
FREIGHT_ORG_KEY=edr_freight
# MinIO (used by @tria-plc/iamapi-common for file storage) # MinIO (used by @tria-plc/iamapi-common for file storage)
MINIO_ENDPOINT=localhost MINIO_ENDPOINT=localhost
MINIO_PORT=9000 MINIO_PORT=9000

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { PaginatedResponse } from '@edr/types'; import { PaginatedResponse } from '@edr/types';
import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity'; import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity';
@@ -19,6 +20,7 @@ import { paginateQuery } from '../../common/utils/pagination.util';
export class ListUsersService { export class ListUsersService {
constructor( constructor(
@InjectRepository(User) private readonly users: Repository<User>, @InjectRepository(User) private readonly users: Repository<User>,
private readonly config: ConfigService,
) {} ) {}
findAll(query: ListUsersQueryDto): Promise<PaginatedResponse<User>> { findAll(query: ListUsersQueryDto): Promise<PaginatedResponse<User>> {
@@ -40,6 +42,29 @@ export class ListUsersService {
]) ])
.orderBy(`user.${sortBy}`, query.sortOrder ?? 'ASC'); .orderBy(`user.${sortBy}`, query.sortOrder ?? 'ASC');
// Restrict to one IAM organization when configured. The org key differs per
// environment (dev seeds `edr_freight`, production uses the registered
// company key), so this is config rather than a constant. An unset key
// means no restriction; a key matching no organization matches no user —
// failing closed rather than silently widening to every org.
const orgKey = this.config.get<string>('FREIGHT_ORG_KEY');
if (orgKey) {
// EXISTS, not a join: a user with several employee rows would otherwise
// be returned once per row, duplicating them in the list and inflating
// `getManyAndCount`'s total.
qb.andWhere(
`EXISTS (
SELECT 1
FROM iam.employees emp
JOIN iam.organizations org ON org.id = emp.organization_id
WHERE emp.user_id = "user".id
AND org.key = :orgKey
AND org.deleted_at IS NULL
)`,
{ orgKey },
);
}
if (query.userType) { if (query.userType) {
qb.andWhere('user.userType = :userType', { userType: query.userType }); qb.andWhere('user.userType = :userType', { userType: query.userType });
} }

View File

@@ -253,7 +253,7 @@ const RuleEngineCardGrid = ({
config={config} config={config}
layout="compact" layout="compact"
readOnly={readOnly} readOnly={readOnly}
onEdit={onEdit ?? (() => { })} onEdit={onEdit}
onDelete={onDelete ?? (() => { })} onDelete={onDelete ?? (() => { })}
onViewChain={onViewChain} onViewChain={onViewChain}
onSubmitRate={onSubmitRate} onSubmitRate={onSubmitRate}

View File

@@ -14,7 +14,7 @@ import type { RuleEngineRecord } from "@/types/rule-engine";
export interface RuleEngineRecordActionsProps { export interface RuleEngineRecordActionsProps {
record: RuleEngineRecord; record: RuleEngineRecord;
config: RuleEngineResourceConfig; config: RuleEngineResourceConfig;
onEdit: (record: RuleEngineRecord) => void; onEdit?: (record: RuleEngineRecord) => void;
onDelete: (record: RuleEngineRecord) => void; onDelete: (record: RuleEngineRecord) => void;
onViewChain?: () => void; onViewChain?: () => void;
onSubmitRate?: (id: string) => void; onSubmitRate?: (id: string) => void;
@@ -93,17 +93,19 @@ const RuleEngineRecordActions = ({
) : null} ) : null}
<div style={actionGroupStyle}> <div style={actionGroupStyle}>
<Button {onEdit ? (
variant="subtle" <Button
color="gray" variant="subtle"
size="compact-sm" color="gray"
radius="md" size="compact-sm"
onClick={() => onEdit(record)} radius="md"
leftSection={<Pencil size={14} />} onClick={() => onEdit(record)}
styles={{ root: { fontWeight: 600 } }} leftSection={<Pencil size={14} />}
> styles={{ root: { fontWeight: 600 } }}
Edit >
</Button> Edit
</Button>
) : null}
<Button <Button
variant="subtle" variant="subtle"
color="red" color="red"
@@ -151,21 +153,23 @@ const RuleEngineRecordActions = ({
) : null} ) : null}
<div style={actionGroupStyle}> <div style={actionGroupStyle}>
<Tooltip label="Edit"> {onEdit ? (
<ActionIcon <Tooltip label="Edit">
variant="subtle" <ActionIcon
color="gray" variant="subtle"
size="md" color="gray"
radius="md" size="md"
onClick={() => onEdit(record)} radius="md"
aria-label="Edit record" onClick={() => onEdit(record)}
style={{ aria-label="Edit record"
background: "white", style={{
}} background: "white",
> }}
<Pencil size={16} /> >
</ActionIcon> <Pencil size={16} />
</Tooltip> </ActionIcon>
</Tooltip>
) : null}
<Tooltip label="Delete"> <Tooltip label="Delete">
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"

View File

@@ -542,10 +542,14 @@ const RuleEngineResourcePage = () => {
config={config} config={config}
layout="row" layout="row"
readOnly={!canUpdateControls} readOnly={!canUpdateControls}
onEdit={(record) => { onEdit={
setEditing(record); config.slug === "container-types"
setFormOpen(true); ? undefined
}} : (record) => {
setEditing(record);
setFormOpen(true);
}
}
onDelete={setDeleteTarget} onDelete={setDeleteTarget}
onViewChain={ onViewChain={
config.slug === "approval-rules" config.slug === "approval-rules"
@@ -880,7 +884,9 @@ const RuleEngineResourcePage = () => {
totalCount={totalCount} totalCount={totalCount}
onPaginationChange={setPagination} onPaginationChange={setPagination}
readOnly={!canUpdate && !canDelete} readOnly={!canUpdate && !canDelete}
onEdit={canUpdate ? openEdit : undefined} onEdit={
canUpdate && config.slug !== "container-types" ? openEdit : undefined
}
onDelete={canDelete ? setDeleteTarget : undefined} onDelete={canDelete ? setDeleteTarget : undefined}
onViewChain={ onViewChain={
config.slug === "approval-rules" config.slug === "approval-rules"

View File

@@ -18,7 +18,6 @@ import {
History, History,
Inbox, Inbox,
PackageCheck, PackageCheck,
Plus,
RefreshCw, RefreshCw,
Search, Search,
Send, Send,
@@ -311,19 +310,6 @@ export default function WagonTransfersPage() {
> >
Refresh Refresh
</Button> </Button>
{canRequest ? (
<Button
color="edr-green"
radius="md"
leftSection={<Plus size={15} />}
onClick={() => {
setCarryOver(null);
setFormOpen(true);
}}
>
New request
</Button>
) : null}
</Group> </Group>
} }
/> />