add reports module with controller, service, and repository

This commit is contained in:
Marshal
2026-08-03 21:58:35 +00:00
parent c3979b08b6
commit 5e8eec9539
4 changed files with 392 additions and 33 deletions

View File

@@ -1,10 +1,10 @@
import { DataSource } from 'typeorm';
export interface ReportFilters {
/** ISO timestamp, inclusive lower bound. */
dateFrom: string;
/** ISO timestamp, exclusive upper bound. */
dateTo: string;
/** ISO timestamp, inclusive lower bound. null = no lower bound (all time). */
dateFrom: string | null;
/** ISO timestamp, exclusive upper bound. null = no upper bound. */
dateTo: string | null;
granularity: 'day' | 'week' | 'month';
companyIds: string[] | null;
routeIds: string[] | null;
@@ -52,7 +52,8 @@ function bookingWhere(f: ReportFilters): { where: string; params: unknown[] } {
where: `
b.deleted_at IS NULL
AND ${NOT_UMBRELLA}
AND b.created_at >= $1::timestamptz AND b.created_at < $2::timestamptz
AND ($1::timestamptz IS NULL OR b.created_at >= $1)
AND ($2::timestamptz IS NULL OR b.created_at < $2)
AND ($3::uuid[] IS NULL OR b.company_id = ANY($3))
AND ($4::uuid[] IS NULL OR b.cargo_type_id = ANY($4))
AND ($5::text[] IS NULL OR b.trade_direction = ANY($5))
@@ -182,8 +183,9 @@ const contractUtilization: ReportQuery = async (ds, f) => {
AND b.status NOT IN (${DEAD_STATUSES})) booked ON true
WHERE ct.deleted_at IS NULL
AND ct.status NOT IN ('DRAFT')
AND ct.contract_valid_from < $2::timestamptz
AND (ct.contract_valid_until IS NULL OR ct.contract_valid_until >= $1::timestamptz)
AND ct.contract_valid_from < COALESCE($2::timestamptz, 'infinity')
AND (ct.contract_valid_until IS NULL
OR ct.contract_valid_until >= COALESCE($1::timestamptz, '-infinity'))
AND ($3::uuid[] IS NULL OR ct.company_id = ANY($3))
AND ($4::text[] IS NULL OR ct.trade_direction = ANY($4))
AND ($5::text[] IS NULL OR ct.status = ANY($5))
@@ -227,8 +229,8 @@ const trainOnTime: ReportQuery = async (ds, f) => {
JOIN freight.yards d ON d.id = ts.destination_station_id
WHERE ts.deleted_at IS NULL
AND ts.status IN ('DISPATCHED', 'ARRIVED')
AND ts.scheduled_departure_date >= $1::timestamptz
AND ts.scheduled_departure_date < $2::timestamptz
AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1)
AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2)
AND ($3::uuid[] IS NULL OR ts.route_id = ANY($3))
AND ($4::text[] IS NULL OR ts.direction = ANY($4))
AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5))
@@ -280,8 +282,8 @@ const scheduleFillRate: ReportQuery = async (ds, f) => {
WHERE tw.train_set_id = ts.train_set_id AND tw.deleted_at IS NULL) w ON true
WHERE ts.deleted_at IS NULL
AND ts.status <> 'CANCELLED'
AND ts.scheduled_departure_date >= $1::timestamptz
AND ts.scheduled_departure_date < $2::timestamptz
AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1)
AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2)
AND ($3::uuid[] IS NULL OR ts.route_id = ANY($3))
AND ($4::text[] IS NULL OR ts.direction = ANY($4))
AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5))
@@ -319,8 +321,8 @@ const tripsPerRoute: ReportQuery = async (ds, f) => {
WHERE tw.train_set_id = ts.train_set_id AND tw.deleted_at IS NULL) w ON true
WHERE ts.deleted_at IS NULL
AND ts.status IN ('DISPATCHED', 'ARRIVED')
AND ts.scheduled_departure_date >= $1::timestamptz
AND ts.scheduled_departure_date < $2::timestamptz
AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1)
AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2)
AND ($3::uuid[] IS NULL OR ts.route_id = ANY($3))
AND ($4::text[] IS NULL OR ts.direction = ANY($4))
AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5))
@@ -347,8 +349,8 @@ const invoicedVsCollected: ReportQuery = async (ds, f) => {
FROM freight.invoices i
WHERE i.deleted_at IS NULL
AND i.status NOT IN ('DRAFT', 'CANCELLED')
AND COALESCE(i.issued_at, i.created_at) >= $1::timestamptz
AND COALESCE(i.issued_at, i.created_at) < $2::timestamptz
AND ($1::timestamptz IS NULL OR COALESCE(i.issued_at, i.created_at) >= $1)
AND ($2::timestamptz IS NULL OR COALESCE(i.issued_at, i.created_at) < $2)
AND ($3::uuid[] IS NULL OR i.company_id = ANY($3))
AND ${refDirScope('i.source_id', '$4')}
GROUP BY 1 ORDER BY 1`,
@@ -371,26 +373,27 @@ const invoicedVsCollected: ReportQuery = async (ds, f) => {
};
};
// Aging is an as-of snapshot: dateTo is the as-of moment, dateFrom is ignored.
// Aging is an as-of snapshot: dateTo is the as-of moment (default now),
// dateFrom is ignored.
const agingReceivables: ReportQuery = async (ds, f) => {
const rows = await ds.query(
`SELECT c.name AS customer,
COUNT(*)::int AS invoices,
ROUND(SUM(i.balance_amount))::float8 AS outstanding,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at >= $1::timestamptz), 0))::float8 AS current,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < $1::timestamptz
AND i.due_at >= $1::timestamptz - interval '30 days'), 0))::float8 AS overdue_0_30,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < $1::timestamptz - interval '30 days'
AND i.due_at >= $1::timestamptz - interval '60 days'), 0))::float8 AS overdue_31_60,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < $1::timestamptz - interval '60 days'
AND i.due_at >= $1::timestamptz - interval '90 days'), 0))::float8 AS overdue_61_90,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < $1::timestamptz - interval '90 days'), 0))::float8 AS overdue_90_plus
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at >= COALESCE($1::timestamptz, now())), 0))::float8 AS current,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now())
AND i.due_at >= COALESCE($1::timestamptz, now()) - interval '30 days'), 0))::float8 AS overdue_0_30,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now()) - interval '30 days'
AND i.due_at >= COALESCE($1::timestamptz, now()) - interval '60 days'), 0))::float8 AS overdue_31_60,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now()) - interval '60 days'
AND i.due_at >= COALESCE($1::timestamptz, now()) - interval '90 days'), 0))::float8 AS overdue_61_90,
ROUND(COALESCE(SUM(i.balance_amount) FILTER (WHERE i.due_at < COALESCE($1::timestamptz, now()) - interval '90 days'), 0))::float8 AS overdue_90_plus
FROM freight.invoices i
JOIN freight.companies c ON c.id = i.company_id
WHERE i.deleted_at IS NULL
AND i.status IN ('ISSUED', 'PENDING', 'PARTIALLY_PAID', 'OVERDUE')
AND i.balance_amount > 0
AND i.created_at < $1::timestamptz
AND ($1::timestamptz IS NULL OR i.created_at < $1)
AND ($2::uuid[] IS NULL OR i.company_id = ANY($2))
AND ${refDirScope('i.source_id', '$3')}
GROUP BY 1 ORDER BY outstanding DESC LIMIT 200`,
@@ -416,7 +419,8 @@ const revenueByPaymentMethod: ReportQuery = async (ds, f) => {
ROUND(SUM(p.amount))::float8 AS amount
FROM freight.payments p
WHERE p.status = 'success'
AND p.created_at >= $1::timestamptz AND p.created_at < $2::timestamptz
AND ($1::timestamptz IS NULL OR p.created_at >= $1)
AND ($2::timestamptz IS NULL OR p.created_at < $2)
AND ${refDirScope('p.ref_id', '$3')}
GROUP BY 1 ORDER BY amount DESC`,
[f.dateFrom, f.dateTo, f.directions],
@@ -436,7 +440,222 @@ const revenueByPaymentMethod: ReportQuery = async (ds, f) => {
};
};
// ---------------------------------------------------------------------------
// Record-level list exports. Same engine, raw rows instead of aggregates.
// ponytail: flat LIMIT 5000 per list — stream/paginate the export if a table
// ever outgrows that.
const LIST_LIMIT = 5000;
const bookingsList: ReportQuery = async (ds, f) => {
const { where, params } = bookingWhere(f);
const rows = await ds.query(
`SELECT b.reference,
to_char(b.created_at, 'YYYY-MM-DD') AS created,
c.name AS customer, b.status, b.freight_type,
b.trade_direction AS direction,
o.label AS origin, d.label AS destination,
COALESCE(cty.cargo_type_name, b.cargo_free_text) AS cargo,
ROUND(${TONS})::float8 AS tons,
ROUND(${REVENUE})::float8 AS amount,
b.payment_status, b.scheduling_status
FROM freight.bookings b
JOIN freight.companies c ON c.id = b.company_id
JOIN freight.yards o ON o.id = b.origin_yard_id
JOIN freight.yards d ON d.id = b.destination_yard_id
LEFT JOIN freight.cargo_types cty ON cty.id = b.cargo_type_id
WHERE ${where}
ORDER BY b.created_at DESC LIMIT ${LIST_LIMIT}`,
params,
);
return {
kpis: [
{ label: 'Bookings', value: rows.length },
{ label: 'Tonnage', value: sum(rows, 'tons'), unit: 't' },
{ label: 'Amount', value: sum(rows, 'amount'), unit: 'ETB' },
],
rows,
};
};
const contractsList: ReportQuery = async (ds, f) => {
const rows = await ds.query(
`SELECT ct.reference, c.name AS customer, ct.contract_kind AS kind,
ct.status, ct.trade_direction AS direction, ct.freight_type,
to_char(ct.contract_valid_from, 'YYYY-MM-DD') AS valid_from,
to_char(ct.contract_valid_until, 'YYYY-MM-DD') AS valid_until,
to_char(ct.created_at, 'YYYY-MM-DD') AS created
FROM freight.contracts ct
LEFT JOIN freight.companies c ON c.id = ct.company_id
WHERE ct.deleted_at IS NULL
AND ($1::timestamptz IS NULL OR ct.created_at >= $1)
AND ($2::timestamptz IS NULL OR ct.created_at < $2)
AND ($3::uuid[] IS NULL OR ct.company_id = ANY($3))
AND ($4::text[] IS NULL OR ct.trade_direction = ANY($4))
AND ($5::text[] IS NULL OR ct.status = ANY($5))
ORDER BY ct.created_at DESC LIMIT ${LIST_LIMIT}`,
[f.dateFrom, f.dateTo, f.companyIds, f.directions, f.statuses],
);
const active = rows.filter((r: Record<string, unknown>) =>
['CONTRACT_ACTIVE', 'ACTIVE_SHIPMENT_IN_PROGRESS'].includes(String(r.status)),
).length;
return {
kpis: [
{ label: 'Contracts', value: rows.length },
{ label: 'Active', value: active },
],
rows,
};
};
const schedulesList: ReportQuery = async (ds, f) => {
const rows = await ds.query(
`SELECT ts.train_number, ts.reference, ts.direction, ts.status,
o.label AS origin, d.label AS destination,
to_char(ts.scheduled_departure_date, 'YYYY-MM-DD HH24:MI') AS scheduled_departure,
to_char(ts.actual_departure_at, 'YYYY-MM-DD HH24:MI') AS actual_departure,
to_char(ts.scheduled_arrival_date, 'YYYY-MM-DD HH24:MI') AS scheduled_arrival,
to_char(ts.actual_arrival_at, 'YYYY-MM-DD HH24:MI') AS actual_arrival,
ts.max_wagons, tset.wagon_count
FROM freight.train_schedules ts
JOIN freight.yards o ON o.id = ts.origin_station_id
JOIN freight.yards d ON d.id = ts.destination_station_id
LEFT JOIN freight.train_sets tset ON tset.id = ts.train_set_id
WHERE ts.deleted_at IS NULL
AND ($1::timestamptz IS NULL OR ts.scheduled_departure_date >= $1)
AND ($2::timestamptz IS NULL OR ts.scheduled_departure_date < $2)
AND ($3::text[] IS NULL OR ts.direction = ANY($3))
AND ($4::text[] IS NULL OR ts.status = ANY($4))
AND ($5::uuid[] IS NULL OR ts.origin_station_id = ANY($5) OR ts.destination_station_id = ANY($5))
ORDER BY ts.scheduled_departure_date DESC LIMIT ${LIST_LIMIT}`,
[f.dateFrom, f.dateTo, f.directions, f.statuses, f.yardIds],
);
const count = (s: string) =>
rows.filter((r: Record<string, unknown>) => r.status === s).length;
return {
kpis: [
{ label: 'Schedules', value: rows.length },
{ label: 'Dispatched', value: count('DISPATCHED') },
{ label: 'Arrived', value: count('ARRIVED') },
],
rows,
};
};
const fleetWagons: ReportQuery = async (ds, f) => {
const rows = await ds.query(
`SELECT w.wagon_number, wt.name AS type,
wt.capacity_tons::float8 AS capacity_tons,
w.status, y.label AS current_yard
FROM freight.wagons w
JOIN freight.wagon_types wt ON wt.id = w.wagon_type_id
LEFT JOIN freight.yards y ON y.id = w.current_yard_id
WHERE w.deleted_at IS NULL
AND ($1::text[] IS NULL OR w.status = ANY($1))
AND ($2::uuid[] IS NULL OR w.current_yard_id = ANY($2))
ORDER BY w.wagon_number LIMIT ${LIST_LIMIT}`,
[f.statuses, f.yardIds],
);
const count = (s: string) =>
rows.filter((r: Record<string, unknown>) => r.status === s).length;
return {
kpis: [
{ label: 'Wagons', value: rows.length },
{ label: 'Available', value: count('AVAILABLE') },
{ label: 'Assigned', value: count('ASSIGNED') },
{ label: 'Maintenance', value: count('MAINTENANCE') },
],
rows,
};
};
const fleetLocomotives: ReportQuery = async (ds, f) => {
const rows = await ds.query(
`SELECT l.code, l.name, l.locomotive_type,
l.max_pull_weight_tons::float8 AS max_pull_tons,
l.status, y.label AS current_yard
FROM freight.locomotives l
LEFT JOIN freight.yards y ON y.id = l.current_yard_id
WHERE l.deleted_at IS NULL
AND ($1::text[] IS NULL OR l.status = ANY($1))
AND ($2::uuid[] IS NULL OR l.current_yard_id = ANY($2))
ORDER BY l.code LIMIT ${LIST_LIMIT}`,
[f.statuses, f.yardIds],
);
const available = rows.filter(
(r: Record<string, unknown>) => r.status === 'AVAILABLE',
).length;
return {
kpis: [
{ label: 'Locomotives', value: rows.length },
{ label: 'Available', value: available },
],
rows,
};
};
const customersList: ReportQuery = async (ds, f) => {
const rows = await ds.query(
`SELECT c.name, c.type, c.kind, c.status, c.tin,
to_char(c.approved_at, 'YYYY-MM-DD') AS approved,
to_char(c.created_at, 'YYYY-MM-DD') AS created
FROM freight.companies c
WHERE c.deleted_at IS NULL
AND ($1::timestamptz IS NULL OR c.created_at >= $1)
AND ($2::timestamptz IS NULL OR c.created_at < $2)
AND ($3::text[] IS NULL OR c.status = ANY($3))
ORDER BY c.created_at DESC LIMIT ${LIST_LIMIT}`,
[f.dateFrom, f.dateTo, f.statuses],
);
const active = rows.filter(
(r: Record<string, unknown>) => r.status === 'active',
).length;
return {
kpis: [
{ label: 'Customers', value: rows.length },
{ label: 'Active', value: active },
],
rows,
};
};
const paymentsList: ReportQuery = async (ds, f) => {
// No deleted_at on freight.payments; statuses are lowercase-hyphenated.
const rows = await ds.query(
`SELECT to_char(p.created_at, 'YYYY-MM-DD HH24:MI') AS created,
p.method::text AS method, p.status::text AS status,
p.currency::text AS currency,
ROUND(p.amount)::float8 AS amount,
p.transaction_id, p.merchant_order_id,
to_char(p.paid_at, 'YYYY-MM-DD') AS paid
FROM freight.payments p
WHERE ($1::timestamptz IS NULL OR p.created_at >= $1)
AND ($2::timestamptz IS NULL OR p.created_at < $2)
AND ($3::text[] IS NULL OR p.status::text = ANY($3))
AND ${refDirScope('p.ref_id', '$4')}
ORDER BY p.created_at DESC LIMIT ${LIST_LIMIT}`,
[f.dateFrom, f.dateTo, f.statuses, f.directions],
);
const success = rows.filter(
(r: Record<string, unknown>) => r.status === 'success',
);
return {
kpis: [
{ label: 'Payments', value: rows.length },
{ label: 'Successful', value: success.length },
{ label: 'Collected', value: sum(success, 'amount'), unit: 'ETB' },
],
rows,
};
};
export const REPORT_QUERIES: Record<string, ReportQuery> = {
'bookings-list': bookingsList,
'contracts-list': contractsList,
'schedules-list': schedulesList,
'fleet-wagons': fleetWagons,
'fleet-locomotives': fleetLocomotives,
'customers-list': customersList,
'payments-list': paymentsList,
'bookings-trend': bookingsTrend,
'revenue-by-customer': revenueByCustomer,
'revenue-by-lane': revenueByLane,

View File

@@ -25,12 +25,13 @@ export class ReportsService {
if (!(key in REPORT_QUERIES)) {
throw new NotFoundException(`Unknown report: ${key}`);
}
const to = dto.dateTo ? new Date(dto.dateTo) : new Date();
const from = dto.dateFrom ? new Date(dto.dateFrom) : new Date(to.getTime() - 30 * DAY_MS);
// No default range: absent dates mean all time, so exports cover everything.
const to = dto.dateTo ? new Date(dto.dateTo) : null;
const from = dto.dateFrom ? new Date(dto.dateFrom) : null;
const filters: ReportFilters = {
dateFrom: from.toISOString(),
dateFrom: from ? from.toISOString() : null,
// dateTo is inclusive in the API; queries treat the bound as exclusive.
dateTo: new Date(to.getTime() + DAY_MS).toISOString(),
dateTo: to ? new Date(to.getTime() + DAY_MS).toISOString() : null,
granularity: dto.granularity ?? 'day',
companyIds: list(dto.companyIds),
routeIds: list(dto.routeIds),

View File

@@ -305,7 +305,7 @@ export default function ReportPage() {
value={toDate(params.get("dateFrom"))}
maxDate={toDate(params.get("dateTo")) ?? undefined}
onChange={(d) => setParam("dateFrom", toParam(d))}
placeholder="30 days ago"
placeholder="All time"
/>
<DateInput
label="To"
@@ -314,7 +314,7 @@ export default function ReportPage() {
value={toDate(params.get("dateTo"))}
minDate={toDate(params.get("dateFrom")) ?? undefined}
onChange={(d) => setParam("dateTo", toParam(d))}
placeholder="Today"
placeholder="All time"
/>
{config.filters.includes("granularity") ? (
<Select

View File

@@ -1,4 +1,4 @@
export type ReportDomain = "Commercial" | "Operations" | "Finance";
export type ReportDomain = "Commercial" | "Operations" | "Finance" | "Data";
export type ReportColumnUnit = "ETB" | "t" | "%" | "min";
@@ -282,6 +282,144 @@ export const REPORT_CONFIGS: ReportConfig[] = [
{ key: "amount", label: "Amount", unit: "ETB" },
],
},
// --- Record-level list exports (Data domain) — filtered or full dumps ---
{
key: "bookings-list",
title: "Bookings Export",
description: "Booking records with customer, lane, cargo, amounts",
domain: "Data",
filters: ["yards", "direction", "freightType", "statuses"],
statusOptions: BOOKING_STATUSES,
columns: [
{ key: "reference", label: "Reference" },
{ key: "created", label: "Created" },
{ key: "customer", label: "Customer" },
{ key: "status", label: "Status" },
{ key: "freight_type", label: "Freight" },
{ key: "direction", label: "Direction" },
{ key: "origin", label: "Origin" },
{ key: "destination", label: "Destination" },
{ key: "cargo", label: "Cargo" },
{ key: "tons", label: "Tonnage", unit: "t" },
{ key: "amount", label: "Amount", unit: "ETB" },
{ key: "payment_status", label: "Payment" },
{ key: "scheduling_status", label: "Scheduling" },
],
},
{
key: "contracts-list",
title: "Contracts Export",
description: "Contract records with validity, status, customer",
domain: "Data",
filters: ["direction", "statuses"],
statusOptions: CONTRACT_STATUSES,
columns: [
{ key: "reference", label: "Reference" },
{ key: "customer", label: "Customer" },
{ key: "kind", label: "Kind" },
{ key: "status", label: "Status" },
{ key: "direction", label: "Direction" },
{ key: "freight_type", label: "Freight" },
{ key: "valid_from", label: "Valid from" },
{ key: "valid_until", label: "Valid until" },
{ key: "created", label: "Created" },
],
},
{
key: "schedules-list",
title: "Train Schedules Export",
description: "Schedule records with planned vs actual times",
domain: "Data",
filters: ["yards", "direction", "statuses"],
statusOptions: ["DRAFT", "SCHEDULED", "DISPATCHED", "ARRIVED", "CANCELLED"],
columns: [
{ key: "train_number", label: "Train" },
{ key: "reference", label: "Reference" },
{ key: "direction", label: "Direction" },
{ key: "status", label: "Status" },
{ key: "origin", label: "Origin" },
{ key: "destination", label: "Destination" },
{ key: "scheduled_departure", label: "Sched. departure" },
{ key: "actual_departure", label: "Actual departure" },
{ key: "scheduled_arrival", label: "Sched. arrival" },
{ key: "actual_arrival", label: "Actual arrival" },
{ key: "max_wagons", label: "Max wagons", numeric: true },
{ key: "wagon_count", label: "Wagons", numeric: true },
],
},
{
key: "fleet-wagons",
title: "Wagons Export",
description: "Wagon fleet with type, capacity, status, location",
domain: "Data",
filters: ["yards", "statuses"],
statusOptions: ["AVAILABLE", "ASSIGNED", "MAINTENANCE"],
columns: [
{ key: "wagon_number", label: "Wagon" },
{ key: "type", label: "Type" },
{ key: "capacity_tons", label: "Capacity", unit: "t" },
{ key: "status", label: "Status" },
{ key: "current_yard", label: "Current yard" },
],
},
{
key: "fleet-locomotives",
title: "Locomotives Export",
description: "Locomotive fleet with type, pull capacity, status",
domain: "Data",
filters: ["yards", "statuses"],
statusOptions: ["AVAILABLE", "OUT_OF_SERVICE"],
columns: [
{ key: "code", label: "Code" },
{ key: "name", label: "Name" },
{ key: "locomotive_type", label: "Type" },
{ key: "max_pull_tons", label: "Max pull", unit: "t" },
{ key: "status", label: "Status" },
{ key: "current_yard", label: "Current yard" },
],
},
{
key: "customers-list",
title: "Customers Export",
description: "Company records with type, status, TIN",
domain: "Data",
filters: ["statuses"],
statusOptions: ["pending", "active"],
columns: [
{ key: "name", label: "Name" },
{ key: "type", label: "Type" },
{ key: "kind", label: "Kind" },
{ key: "status", label: "Status" },
{ key: "tin", label: "TIN" },
{ key: "approved", label: "Approved" },
{ key: "created", label: "Created" },
],
},
{
key: "payments-list",
title: "Payments Export",
description: "Payment transactions with method, status, references",
domain: "Data",
filters: ["direction", "statuses"],
statusOptions: [
"action-required",
"processing",
"success",
"failed",
"canceled",
"refunded",
],
columns: [
{ key: "created", label: "Created" },
{ key: "method", label: "Method" },
{ key: "status", label: "Status" },
{ key: "currency", label: "Currency" },
{ key: "amount", label: "Amount", unit: "ETB" },
{ key: "transaction_id", label: "Transaction" },
{ key: "merchant_order_id", label: "Merchant order" },
{ key: "paid", label: "Paid" },
],
},
];
export const REPORT_CONFIG_BY_KEY = new Map(
@@ -292,4 +430,5 @@ export const REPORT_DOMAINS: ReportDomain[] = [
"Commercial",
"Operations",
"Finance",
"Data",
];