@burdenoff/microfe-movethewheels 2026.1102.1 → 2026.1102.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/freight-cost/FreightCommercialFactEditor.js +46 -46
- package/dist/components/freight-cost/FreightCommercialHistory.js +7 -7
- package/dist/components/freight-cost/FreightDraftForm.js +20 -20
- package/dist/components/freight-invoice/InvoiceReviewPanel.js +255 -0
- package/dist/components/freight-invoice/InvoiceReviewQueue.js +93 -0
- package/dist/generated/freight-invoice-operations.d.ts +461 -0
- package/dist/hooks/useInvoiceComparisonRecovery.js +26 -26
- package/dist/hooks/useInvoiceReviewPermissions.js +14 -0
- package/dist/movethewheels/components/freight-invoice/InvoiceReviewPanel.d.ts +11 -0
- package/dist/movethewheels/components/freight-invoice/InvoiceReviewQueue.d.ts +22 -0
- package/dist/movethewheels/hooks/useInvoiceReviewPermissions.d.ts +5 -0
- package/dist/movethewheels/pages/FinancePage.d.ts +3 -1
- package/dist/movethewheels/utils/invoiceReview.d.ts +26 -0
- package/dist/pages/FinancePage.js +109 -67
- package/dist/pages/FreightEstimatePage.js +89 -89
- package/dist/pages/FreightInvoicePage.js +138 -120
- package/dist/src/generated/freight-invoice-operations.js +2100 -1
- package/dist/utils/invoiceReview.js +84 -0
- package/package.json +2 -2
- package/src/generated/freight-invoice-operations.ts +245 -1
- package/src/movethewheels/components/freight-invoice/InvoiceReviewPanel.tsx +319 -0
- package/src/movethewheels/components/freight-invoice/InvoiceReviewQueue.tsx +96 -0
- package/src/movethewheels/hooks/useInvoiceReviewPermissions.ts +25 -0
- package/src/movethewheels/pages/FinancePage.tsx +34 -2
- package/src/movethewheels/pages/FreightInvoicePage.tsx +44 -3
- package/src/movethewheels/utils/invoiceReview.ts +146 -0
- package/src/operations/freight-invoice/review.graphql +26 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freight invoice review helpers (BOFF-10588): labels, the steps a viewer can
|
|
3
|
+
* take, and the exact mutation inputs. The server enforces every rule again;
|
|
4
|
+
* these only keep the page from offering a step that would be refused.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
DecideMtwFreightInvoiceInput,
|
|
8
|
+
HandOffMtwFreightInvoiceSettlementInput,
|
|
9
|
+
InvoiceReviewFragment,
|
|
10
|
+
MtwFreightInvoiceDecision,
|
|
11
|
+
MtwFreightInvoiceResolution,
|
|
12
|
+
MtwFreightInvoiceReviewAction,
|
|
13
|
+
MtwFreightInvoiceReviewStatus,
|
|
14
|
+
RecordMtwFreightInvoiceSupplierResponseInput,
|
|
15
|
+
ResolveMtwFreightInvoiceDisputeInput,
|
|
16
|
+
} from '../../generated/freight-invoice-operations';
|
|
17
|
+
|
|
18
|
+
export const REVIEW_STATUS_LABELS: Record<MtwFreightInvoiceReviewStatus, string> = {
|
|
19
|
+
AWAITING_REVIEW: 'Awaiting review',
|
|
20
|
+
ACCEPTED: 'Accepted',
|
|
21
|
+
ACCEPTED_WITH_VARIANCE: 'Accepted with variance',
|
|
22
|
+
DISPUTED: 'Disputed',
|
|
23
|
+
SUPPLIER_RESPONDED: 'Supplier responded',
|
|
24
|
+
SETTLEMENT_HANDED_OFF: 'Handed to Billing',
|
|
25
|
+
SUPERSEDED: 'Superseded by a correction',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const REVIEW_EVENT_LABELS: Record<InvoiceReviewFragment['events'][number]['kind'], string> =
|
|
29
|
+
{
|
|
30
|
+
DECISION: 'Review decision',
|
|
31
|
+
SUPPLIER_RESPONSE: 'Supplier response',
|
|
32
|
+
DISPUTE_RESOLUTION: 'Dispute resolved',
|
|
33
|
+
SETTLEMENT_HANDOFF: 'Handed to Billing for settlement',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Statuses a reviewer can filter the queue by; SUPERSEDED never appears in the queue. */
|
|
37
|
+
export const REVIEW_QUEUE_STATUSES: MtwFreightInvoiceReviewStatus[] = [
|
|
38
|
+
'AWAITING_REVIEW',
|
|
39
|
+
'DISPUTED',
|
|
40
|
+
'SUPPLIER_RESPONDED',
|
|
41
|
+
'ACCEPTED',
|
|
42
|
+
'ACCEPTED_WITH_VARIANCE',
|
|
43
|
+
'SETTLEMENT_HANDED_OFF',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export interface ReviewPermissions {
|
|
47
|
+
canRead: boolean;
|
|
48
|
+
canUpdate: boolean;
|
|
49
|
+
canApprove: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PERMISSION_FOR_ACTION: Record<MtwFreightInvoiceReviewAction, keyof ReviewPermissions> = {
|
|
53
|
+
DECIDE: 'canApprove',
|
|
54
|
+
RECORD_SUPPLIER_RESPONSE: 'canUpdate',
|
|
55
|
+
RESOLVE_DISPUTE: 'canApprove',
|
|
56
|
+
HAND_OFF_SETTLEMENT: 'canApprove',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Steps the server says are open (author separation included) that this viewer is also permitted to take. */
|
|
60
|
+
export function availableReviewActions(
|
|
61
|
+
review: Pick<InvoiceReviewFragment, 'allowedActions'>,
|
|
62
|
+
permissions: ReviewPermissions
|
|
63
|
+
): MtwFreightInvoiceReviewAction[] {
|
|
64
|
+
return review.allowedActions.filter((action) => permissions[PERMISSION_FOR_ACTION[action]]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Why the author sees no decision: the server withholds it, so say so instead of showing nothing. */
|
|
68
|
+
export function authorNotice(
|
|
69
|
+
review: Pick<InvoiceReviewFragment, 'viewerIsAuthor' | 'status'>
|
|
70
|
+
): string | null {
|
|
71
|
+
if (!review.viewerIsAuthor) return null;
|
|
72
|
+
if (
|
|
73
|
+
review.status === 'AWAITING_REVIEW' ||
|
|
74
|
+
review.status === 'DISPUTED' ||
|
|
75
|
+
review.status === 'SUPPLIER_RESPONDED'
|
|
76
|
+
)
|
|
77
|
+
return 'You recorded this comparison, so another reviewer must accept, dispute or resolve it.';
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function text(value: string): string | null {
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
return trimmed ? trimmed : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Common {
|
|
87
|
+
review: Pick<InvoiceReviewFragment, 'shipmentId' | 'comparisonId' | 'sequence'>;
|
|
88
|
+
idempotencyKey: string;
|
|
89
|
+
}
|
|
90
|
+
function common({ review, idempotencyKey }: Common) {
|
|
91
|
+
return {
|
|
92
|
+
shipmentId: review.shipmentId,
|
|
93
|
+
comparisonId: review.comparisonId,
|
|
94
|
+
expectedSequence: review.sequence,
|
|
95
|
+
idempotencyKey,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type DraftProblem = 'REASON_REQUIRED' | 'LINES_REQUIRED' | 'RESPONSE_REQUIRED';
|
|
100
|
+
export const DRAFT_PROBLEM_LABELS: Record<DraftProblem, string> = {
|
|
101
|
+
REASON_REQUIRED: 'Give a reason for a dispute, a variance or a resolution.',
|
|
102
|
+
LINES_REQUIRED: 'Choose at least one invoice line you dispute.',
|
|
103
|
+
RESPONSE_REQUIRED: "Record the supplier's response.",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function decisionInput(
|
|
107
|
+
base: Common,
|
|
108
|
+
decision: MtwFreightInvoiceDecision,
|
|
109
|
+
reason: string,
|
|
110
|
+
disputedLineIds: readonly string[]
|
|
111
|
+
): DecideMtwFreightInvoiceInput | DraftProblem {
|
|
112
|
+
const why = text(reason);
|
|
113
|
+
if (decision !== 'ACCEPTED' && !why) return 'REASON_REQUIRED';
|
|
114
|
+
if (decision === 'DISPUTED' && disputedLineIds.length === 0) return 'LINES_REQUIRED';
|
|
115
|
+
return {
|
|
116
|
+
...common(base),
|
|
117
|
+
decision,
|
|
118
|
+
reason: why,
|
|
119
|
+
disputedLineIds: decision === 'DISPUTED' ? [...new Set(disputedLineIds)] : [],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function supplierResponseInput(
|
|
124
|
+
base: Common,
|
|
125
|
+
response: string
|
|
126
|
+
): RecordMtwFreightInvoiceSupplierResponseInput | DraftProblem {
|
|
127
|
+
const said = text(response);
|
|
128
|
+
return said ? { ...common(base), response: said } : 'RESPONSE_REQUIRED';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolutionInput(
|
|
132
|
+
base: Common,
|
|
133
|
+
resolution: MtwFreightInvoiceResolution,
|
|
134
|
+
reason: string
|
|
135
|
+
): ResolveMtwFreightInvoiceDisputeInput | DraftProblem {
|
|
136
|
+
const why = text(reason);
|
|
137
|
+
return why ? { ...common(base), resolution, reason: why } : 'REASON_REQUIRED';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function handOffInput(base: Common, note: string): HandOffMtwFreightInvoiceSettlementInput {
|
|
141
|
+
return { ...common(base), note: text(note) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function isDraftProblem(value: unknown): value is DraftProblem {
|
|
145
|
+
return typeof value === 'string' && value in DRAFT_PROBLEM_LABELS;
|
|
146
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
fragment InvoiceReview on MtwFreightInvoiceReview {
|
|
2
|
+
comparisonId shipmentId invoiceIdentityId revision latestRevision supersededByComparisonId
|
|
3
|
+
supplierReference supplierLabel invoiceReference currency { code minorUnitDigits }
|
|
4
|
+
knownInvoiceAmountMinor invoiceAmountComplete comparisonRecordedAt comparisonCreatedBy
|
|
5
|
+
status resolvedByCorrection sequence allowedActions viewerIsAuthor settlementReference
|
|
6
|
+
events { id sequence kind decision statusAfter reason disputedLineIds settlementReference recordedAt createdBy }
|
|
7
|
+
}
|
|
8
|
+
query InvoiceReviewExact($shipmentId: ID!, $comparisonId: ID!) {
|
|
9
|
+
mtwFreightInvoiceReview(shipmentId: $shipmentId, comparisonId: $comparisonId) { ...InvoiceReview }
|
|
10
|
+
}
|
|
11
|
+
query InvoiceReviewQueue($status: MtwFreightInvoiceReviewStatus, $pagination: MtwContainerPagination) {
|
|
12
|
+
mtwFreightInvoiceReviewPage(status: $status, pagination: $pagination) {
|
|
13
|
+
items { ...InvoiceReview } total limit offset hasMore nextOffset
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
query InvoiceReviewSummary {
|
|
17
|
+
mtwFreightInvoiceReviewSummary { total awaitingReview accepted acceptedWithVariance disputed supplierResponded settlementHandedOff }
|
|
18
|
+
}
|
|
19
|
+
mutation DecideInvoiceReview($input: DecideMtwFreightInvoiceInput!) { decideMtwFreightInvoice(input: $input) { ...InvoiceReview } }
|
|
20
|
+
mutation RecordInvoiceSupplierResponse($input: RecordMtwFreightInvoiceSupplierResponseInput!) {
|
|
21
|
+
recordMtwFreightInvoiceSupplierResponse(input: $input) { ...InvoiceReview }
|
|
22
|
+
}
|
|
23
|
+
mutation ResolveInvoiceDispute($input: ResolveMtwFreightInvoiceDisputeInput!) { resolveMtwFreightInvoiceDispute(input: $input) { ...InvoiceReview } }
|
|
24
|
+
mutation HandOffInvoiceSettlement($input: HandOffMtwFreightInvoiceSettlementInput!) {
|
|
25
|
+
handOffMtwFreightInvoiceSettlement(input: $input) { ...InvoiceReview }
|
|
26
|
+
}
|