Merge branch 'freight_feature/usermanagement' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement

This commit is contained in:
marshalyordanos
2026-09-06 10:32:09 +03:00
217 changed files with 8668 additions and 804 deletions

View File

@@ -1,4 +1,4 @@
import type { Freight, PaginatedResponse } from "@edr/types";
import type { Freight, PaginatedResponse, Publication } from "@edr/types";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import type { FleetResourceSlug } from "@/pages/fleet/config/resources";
@@ -191,6 +191,7 @@ import type { EimsInvoiceStatusView, EimsModeOfPayment, EimsReceiptView, EimsVer
import { invoicesService } from "./invoices.service";
import { dropdownSettingsService } from "./dropdownSettings.service";
import { fileUploadSettingsService } from "./fileUploadSettings.service";
import { publicationsService, type UpdatePublicationPayload } from "./publications.service";
import {
fleetService,
type FleetListFilters,
@@ -1532,7 +1533,7 @@ export const api = {
),
feePreview: endpoint<
{ inventoryId: string; billingCurrency?: "ETB" | "USD" },
{ inventoryId: string; billingCurrency?: "ETB" | "USD" | "DJF" },
FeePreview[]
>(
"warehouse-inventory",
@@ -1884,7 +1885,7 @@ export const api = {
{
inventoryId: string;
confirmZero?: boolean;
billingCurrency?: "ETB" | "USD";
billingCurrency?: "ETB" | "USD" | "DJF";
},
WarehouseFeeInvoice
>(
@@ -2934,6 +2935,52 @@ export const api = {
),
},
publications: {
list: endpoint<void, Publication[]>(
"publications",
"list",
publicationsService.list,
),
create: endpoint<
{ file: File; meta: UpdatePublicationPayload & { title: string }; onProgress?: (percent: number | null) => void },
Publication
>(
"publications",
"create",
({ file, meta, onProgress }) => publicationsService.create(file, meta, onProgress),
undefined,
() => [["publications"]],
),
update: endpoint<{ id: string; dto: UpdatePublicationPayload }, Publication>(
"publications",
"update",
({ id, dto }) => publicationsService.update(id, dto),
undefined,
() => [["publications"]],
),
replaceFile: endpoint<
{ id: string; file: File; onProgress?: (percent: number | null) => void },
Publication
>(
"publications",
"replaceFile",
({ id, file, onProgress }) => publicationsService.replaceFile(id, file, onProgress),
undefined,
() => [["publications"]],
),
remove: endpoint<{ id: string }, void>(
"publications",
"remove",
({ id }) => publicationsService.remove(id),
undefined,
() => [["publications"]],
),
},
dropdownSettings: {
list: endpoint<void, DropdownSetting[]>(
"dropdown-settings",

View File

@@ -11,7 +11,7 @@ const BASE = URL_CONSTANTS.EXCHANGE_SETTINGS.BASE;
*/
export type ExchangeRateSource = "live" | "stored";
/** Health of the CBE exchange-rate feed. */
/** Health of the CBE exchange-rate feed for one currency. */
export interface ExchangeFeedStatus {
rate: number | null;
source: ExchangeRateSource | null;
@@ -19,25 +19,31 @@ export interface ExchangeFeedStatus {
lastError: string | null;
}
export interface ExchangeSettings {
fallbackRate: number;
/** `AUTO` when synced from CBE, `MANUAL` when set here. */
fallbackSource: "AUTO" | "MANUAL";
/** One currency's X→ETB fallback settings — the API returns one per foreign currency. */
export interface ExchangeSetting {
currency: string;
fallbackRate: number | null;
/** `AUTO` when synced from CBE, `MANUAL` when set here. `null` before the row exists. */
fallbackSource: "AUTO" | "MANUAL" | null;
lastSyncedAt: string | null;
updatedById: string | null;
feed?: ExchangeFeedStatus;
}
export const exchangeSettingsService = {
get: async (): Promise<ExchangeSettings> => {
const response = await client.get<ApiResponse<ExchangeSettings>>(BASE);
list: async (): Promise<ExchangeSetting[]> => {
const response = await client.get<ApiResponse<ExchangeSetting[]>>(BASE);
return unwrap(response.data);
},
setFallbackRate: async (fallbackRate: number): Promise<ExchangeSettings> => {
const response = await client.patch<ApiResponse<ExchangeSettings>>(BASE, {
fallbackRate,
});
setFallbackRate: async (
currency: string,
fallbackRate: number,
): Promise<ExchangeSetting> => {
const response = await client.patch<ApiResponse<ExchangeSetting>>(
`${BASE}/${currency}`,
{ fallbackRate },
);
return unwrap(response.data);
},
};

View File

@@ -13,6 +13,7 @@ const BASE = URL_CONSTANTS.MANUAL_PAYMENT_SETTINGS.BASE;
export interface ManualPaymentSettings {
etbEnabled: boolean;
usdEnabled: boolean;
djfEnabled: boolean;
updatedById: string | null;
updatedAt?: string;
}
@@ -25,7 +26,9 @@ export const manualPaymentSettingsService = {
/** Partial: an omitted currency keeps its current setting. */
update: async (
patch: Partial<Pick<ManualPaymentSettings, "etbEnabled" | "usdEnabled">>,
patch: Partial<
Pick<ManualPaymentSettings, "etbEnabled" | "usdEnabled" | "djfEnabled">
>,
): Promise<ManualPaymentSettings> => {
const response = await client.patch<ApiResponse<ManualPaymentSettings>>(
BASE,

View File

@@ -0,0 +1,74 @@
import type { Publication } from "@edr/types";
import { api as client } from "../auth/http";
const BASE = "/publications";
export interface UpdatePublicationPayload {
title?: string;
description?: string;
category?: string;
sortOrder?: number;
published?: boolean;
}
/**
* The freight portal's public document library (/publications), managed here.
* Every write is multipart because create/replaceFile carry a real file — the
* client's response interceptor already unwraps the `{ success, data }`
* envelope, so each method stays a one-liner.
*/
export const publicationsService = {
async list(): Promise<Publication[]> {
const { data } = await client.get<Publication[]>(`${BASE}/admin`);
return data;
},
async create(
file: File,
meta: UpdatePublicationPayload & { title: string },
onProgress?: (percent: number | null) => void,
): Promise<Publication> {
const form = new FormData();
form.append("file", file);
Object.entries(meta).forEach(([key, value]) => {
if (value !== undefined) form.append(key, String(value));
});
const { data } = await client.post<Publication>(BASE, form, {
timeout: 2 * 60 * 1000,
onUploadProgress: (event) =>
onProgress?.(
event.total ? Math.round((event.loaded / event.total) * 100) : null,
),
});
return data;
},
async update(id: string, dto: UpdatePublicationPayload): Promise<Publication> {
const { data } = await client.patch<Publication>(`${BASE}/${id}`, dto);
return data;
},
async replaceFile(
id: string,
file: File,
onProgress?: (percent: number | null) => void,
): Promise<Publication> {
const form = new FormData();
form.append("file", file);
const { data } = await client.post<Publication>(`${BASE}/${id}/file`, form, {
timeout: 2 * 60 * 1000,
onUploadProgress: (event) =>
onProgress?.(
event.total ? Math.round((event.loaded / event.total) * 100) : null,
),
});
return data;
},
async remove(id: string): Promise<void> {
await client.delete(`${BASE}/${id}`);
},
};

View File

@@ -558,11 +558,11 @@ export const warehouseService = {
updateFeeRule: (id: string, payload: Partial<SaveFeeRulePayload>) =>
apiClient.patch<FeeRule>(URL_CONSTANTS.WAREHOUSE_RULES.FEES_BY_ID(id), payload),
deleteFeeRule: (id: string) => apiClient.delete(URL_CONSTANTS.WAREHOUSE_RULES.FEES_BY_ID(id)),
feePreview: (inventoryId: string, billingCurrency?: 'ETB' | 'USD') =>
feePreview: (inventoryId: string, billingCurrency?: 'ETB' | 'USD' | 'DJF') =>
apiClient.get<FeePreview[]>(URL_CONSTANTS.WAREHOUSE_RULES.FEE_PREVIEW(inventoryId), {
params: cleanParams({ billingCurrency }),
}),
accrualDashboard: (billingCurrency?: 'ETB' | 'USD') =>
accrualDashboard: (billingCurrency?: 'ETB' | 'USD' | 'DJF') =>
apiClient.get<AccrualDashboardRow[]>(URL_CONSTANTS.WAREHOUSE_RULES.ACCRUAL_DASHBOARD, {
params: cleanParams({ billingCurrency }),
}),
@@ -592,7 +592,7 @@ export const warehouseService = {
apiClient.get<WarehouseFeeInvoice[]>(URL_CONSTANTS.WAREHOUSE_INVOICES.FOR_INVENTORY(inventoryId)),
invoicesForBooking: (bookingId: string) =>
apiClient.get<WarehouseFeeInvoice[]>(URL_CONSTANTS.WAREHOUSE_INVOICES.FOR_BOOKING(bookingId)),
generateInvoice: (inventoryId: string, confirmZero = false, billingCurrency?: 'ETB' | 'USD') =>
generateInvoice: (inventoryId: string, confirmZero = false, billingCurrency?: 'ETB' | 'USD' | 'DJF') =>
apiClient.post<WarehouseFeeInvoice>(URL_CONSTANTS.WAREHOUSE_INVOICES.GENERATE(inventoryId), {
confirmZero,
billingCurrency,