@powerhousedao/contributor-billing 0.0.98 → 0.1.0
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/document-models/billing-statement/src/reducers/general.d.ts.map +1 -1
- package/dist/document-models/billing-statement/src/reducers/general.js +8 -26
- package/dist/document-models/billing-statement/src/reducers/line-items.d.ts.map +1 -1
- package/dist/document-models/billing-statement/src/reducers/line-items.js +19 -29
- package/dist/document-models/billing-statement/src/reducers/tags.d.ts.map +1 -1
- package/dist/document-models/billing-statement/src/reducers/tags.js +20 -25
- package/dist/document-models/integrations/src/reducers/integrations.d.ts.map +1 -1
- package/dist/document-models/integrations/src/reducers/integrations.js +29 -44
- package/dist/document-models/invoice/src/reducers/general.d.ts.map +1 -1
- package/dist/document-models/invoice/src/reducers/general.js +31 -56
- package/dist/document-models/invoice/src/reducers/items.d.ts.map +1 -1
- package/dist/document-models/invoice/src/reducers/items.js +63 -88
- package/dist/document-models/invoice/src/reducers/parties.d.ts.map +1 -1
- package/dist/document-models/invoice/src/reducers/parties.js +199 -229
- package/dist/editors/billing-statement/components/lineItemsTable.d.ts +3 -2
- package/dist/editors/billing-statement/components/lineItemsTable.d.ts.map +1 -1
- package/dist/editors/billing-statement/components/lineItemsTable.js +12 -8
- package/dist/editors/billing-statement/components/objectSetTable.d.ts +3 -2
- package/dist/editors/billing-statement/components/objectSetTable.d.ts.map +1 -1
- package/dist/editors/billing-statement/editor.d.ts +3 -1
- package/dist/editors/billing-statement/editor.d.ts.map +1 -1
- package/dist/editors/billing-statement/editor.js +2 -2
- package/dist/editors/billing-statement/lineItemTags/lineItemTags.d.ts +2 -2
- package/dist/editors/billing-statement/lineItemTags/lineItemTags.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/DriveExplorer.d.ts +8 -1
- package/dist/editors/contributor-billing/components/DriveExplorer.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/DriveExplorer.js +18 -64
- package/dist/editors/contributor-billing/components/InvoiceTable/HeaderControls.d.ts +2 -8
- package/dist/editors/contributor-billing/components/InvoiceTable/HeaderControls.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/InvoiceTable/HeaderControls.js +21 -7
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTable.d.ts +8 -10
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTable.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTable.js +27 -24
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTableRow.d.ts +6 -8
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTableRow.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTableRow.js +8 -17
- package/dist/editors/contributor-billing/editor.d.ts +1 -10
- package/dist/editors/contributor-billing/editor.d.ts.map +1 -1
- package/dist/editors/contributor-billing/editor.js +2 -23
- package/dist/editors/contributor-billing/hooks/useTransformedNodes.d.ts +2 -1
- package/dist/editors/contributor-billing/hooks/useTransformedNodes.d.ts.map +1 -1
- package/dist/editors/contributor-billing/index.js +1 -1
- package/dist/editors/hooks/useBillingStatementDocument.d.ts +4 -0
- package/dist/editors/hooks/useBillingStatementDocument.d.ts.map +1 -0
- package/dist/editors/hooks/useBillingStatementDocument.js +8 -0
- package/dist/editors/hooks/useIntegrationsDocument.d.ts +4 -0
- package/dist/editors/hooks/useIntegrationsDocument.d.ts.map +1 -0
- package/dist/editors/hooks/useIntegrationsDocument.js +8 -0
- package/dist/editors/hooks/useInvoiceDocument.d.ts +4 -0
- package/dist/editors/hooks/useInvoiceDocument.d.ts.map +1 -0
- package/dist/editors/hooks/useInvoiceDocument.js +8 -0
- package/dist/editors/integrations/editor.d.ts +4 -1
- package/dist/editors/integrations/editor.d.ts.map +1 -1
- package/dist/editors/integrations/editor.js +7 -12
- package/dist/editors/invoice/components/lineItemCard.d.ts +21 -0
- package/dist/editors/invoice/components/lineItemCard.d.ts.map +1 -0
- package/dist/editors/invoice/components/lineItemCard.js +23 -0
- package/dist/editors/invoice/components/lineItemMobileModal.d.ts +33 -0
- package/dist/editors/invoice/components/lineItemMobileModal.d.ts.map +1 -0
- package/dist/editors/invoice/components/lineItemMobileModal.js +73 -0
- package/dist/editors/invoice/components/lineItemsEmptyState.d.ts +6 -0
- package/dist/editors/invoice/components/lineItemsEmptyState.d.ts.map +1 -0
- package/dist/editors/invoice/components/lineItemsEmptyState.js +5 -0
- package/dist/editors/invoice/editor.d.ts +4 -1
- package/dist/editors/invoice/editor.d.ts.map +1 -1
- package/dist/editors/invoice/editor.js +41 -22
- package/dist/editors/invoice/ingestPDF.js +1 -1
- package/dist/editors/invoice/invoiceToGnosis.d.ts +3 -2
- package/dist/editors/invoice/invoiceToGnosis.d.ts.map +1 -1
- package/dist/editors/invoice/invoiceToGnosis.js +11 -11
- package/dist/editors/invoice/legalEntity/bankSection.d.ts +1 -0
- package/dist/editors/invoice/legalEntity/bankSection.d.ts.map +1 -1
- package/dist/editors/invoice/legalEntity/bankSection.js +36 -12
- package/dist/editors/invoice/legalEntity/legalEntity.d.ts +2 -1
- package/dist/editors/invoice/legalEntity/legalEntity.d.ts.map +1 -1
- package/dist/editors/invoice/legalEntity/legalEntity.js +2 -2
- package/dist/editors/invoice/lineItemTags/lineItemTags.d.ts.map +1 -1
- package/dist/editors/invoice/lineItemTags/lineItemTags.js +25 -3
- package/dist/editors/invoice/lineItemTags/tagCard.d.ts +15 -0
- package/dist/editors/invoice/lineItemTags/tagCard.d.ts.map +1 -0
- package/dist/editors/invoice/lineItemTags/tagCard.js +13 -0
- package/dist/editors/invoice/lineItemTags/tagMobileModal.d.ts +18 -0
- package/dist/editors/invoice/lineItemTags/tagMobileModal.d.ts.map +1 -0
- package/dist/editors/invoice/lineItemTags/tagMobileModal.js +71 -0
- package/dist/editors/invoice/lineItems.d.ts.map +1 -1
- package/dist/editors/invoice/lineItems.js +76 -38
- package/dist/editors/invoice/requestFinance.js +2 -2
- package/dist/editors/invoice/uploadPdfChunked.js +1 -1
- package/dist/editors/invoice/validation/validationHandler.d.ts +1 -1
- package/dist/editors/invoice/validation/validationHandler.d.ts.map +1 -1
- package/dist/editors/invoice/validation/validationHandler.js +25 -9
- package/dist/editors/invoice/validation/validationManager.d.ts.map +1 -1
- package/dist/editors/invoice/validation/validationManager.js +3 -2
- package/dist/editors/invoice/validation/validationRules.d.ts +2 -0
- package/dist/editors/invoice/validation/validationRules.d.ts.map +1 -1
- package/dist/editors/invoice/validation/validationRules.js +37 -7
- package/dist/scripts/contributor-billing/createExpenseReportCsv.d.ts +5 -0
- package/dist/scripts/contributor-billing/createExpenseReportCsv.d.ts.map +1 -0
- package/dist/scripts/contributor-billing/createExpenseReportCsv.js +122 -0
- package/dist/style.css +180 -12
- package/dist/tailwind.config.js +1 -0
- package/package.json +13 -12
- package/dist/reducers/general.d.ts +0 -8
- package/dist/reducers/general.d.ts.map +0 -1
- package/dist/reducers/general.js +0 -73
- package/dist/reducers/items.d.ts +0 -8
- package/dist/reducers/items.d.ts.map +0 -1
- package/dist/reducers/items.js +0 -195
- package/dist/reducers/parties.d.ts +0 -8
- package/dist/reducers/parties.d.ts.map +0 -1
- package/dist/reducers/parties.js +0 -266
- package/dist/reducers/transitions.d.ts +0 -8
- package/dist/reducers/transitions.d.ts.map +0 -1
- package/dist/reducers/transitions.js +0 -162
|
@@ -6,6 +6,9 @@ import { Tag } from "lucide-react";
|
|
|
6
6
|
import { NumberForm } from "./components/numberForm.js";
|
|
7
7
|
import { InputField } from "./components/inputField.js";
|
|
8
8
|
import { LineItemTagsTable } from "./lineItemTags/lineItemTags.js";
|
|
9
|
+
import { LineItemCard } from "./components/lineItemCard.js";
|
|
10
|
+
import { LineItemMobileModal } from "./components/lineItemMobileModal.js";
|
|
11
|
+
import { LineItemsEmptyState } from "./components/lineItemsEmptyState.js";
|
|
9
12
|
// Helper function to get precision based on currency
|
|
10
13
|
function getCurrencyPrecision(currency) {
|
|
11
14
|
return currency === "USDS" || currency === "DAI" ? 6 : 2;
|
|
@@ -286,6 +289,8 @@ export function LineItemsTable({ lineItems, currency, onAddItem, onUpdateItem, o
|
|
|
286
289
|
const [editingId, setEditingId] = useState(null);
|
|
287
290
|
const [isAddingNew, setIsAddingNew] = useState(false);
|
|
288
291
|
const [showTagTable, setShowTagTable] = useState(false);
|
|
292
|
+
const [mobileEditItem, setMobileEditItem] = useState(null);
|
|
293
|
+
const [showMobileModal, setShowMobileModal] = useState(false);
|
|
289
294
|
const containerRef = useRef(null);
|
|
290
295
|
const tableContainerRef = useRef(null);
|
|
291
296
|
const tableRef = useRef(null);
|
|
@@ -340,59 +345,92 @@ export function LineItemsTable({ lineItems, currency, onAddItem, onUpdateItem, o
|
|
|
340
345
|
item: item.description,
|
|
341
346
|
period: "", // Default value
|
|
342
347
|
expenseAccount: "", // Default value
|
|
343
|
-
total:
|
|
348
|
+
total: `${currency} ${formatNumber(item.totalPriceTaxIncl)}`,
|
|
344
349
|
lineItemTag: item.lineItemTag,
|
|
345
350
|
}));
|
|
346
351
|
if (showTagTable) {
|
|
347
352
|
return (_jsx(LineItemTagsTable, { lineItems: tagAssignmentRows, onClose: () => setShowTagTable(false), dispatch: dispatch, paymentAccounts: paymentAccounts }));
|
|
348
353
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
354
|
+
// Calculate totals for mobile footer
|
|
355
|
+
const totalPriceTaxExcl = lineItems.reduce((sum, item) => sum + item.totalPriceTaxExcl, 0);
|
|
356
|
+
const totalPriceTaxIncl = lineItems.reduce((sum, item) => sum + item.totalPriceTaxIncl, 0);
|
|
357
|
+
return (_jsxs("div", { ref: containerRef, className: "relative w-full", children: [showMobileModal && (_jsx(LineItemMobileModal, { item: mobileEditItem || {}, currency: currency, isNew: !mobileEditItem?.id || mobileEditItem.id === "", onSave: (item) => {
|
|
358
|
+
try {
|
|
359
|
+
// If editing an item with empty ID, delete it first, then add new one
|
|
360
|
+
if (mobileEditItem?.id === "") {
|
|
361
|
+
onDeleteItem({ id: "" });
|
|
362
|
+
onAddItem(item);
|
|
363
|
+
}
|
|
364
|
+
else if (mobileEditItem?.id) {
|
|
365
|
+
onUpdateItem(item);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
onAddItem(item);
|
|
369
|
+
}
|
|
370
|
+
setShowMobileModal(false);
|
|
371
|
+
setMobileEditItem(null);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
toast(error.message || "Failed to save line item", {
|
|
375
|
+
type: "error",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}, onCancel: () => {
|
|
379
|
+
setShowMobileModal(false);
|
|
380
|
+
setMobileEditItem(null);
|
|
381
|
+
} }, mobileEditItem?.id || "new")), _jsxs("div", { className: "mt-4", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between", children: [_jsx("h4", { className: "text-xl font-semibold text-gray-900", children: "Line Items" }), _jsxs("div", { className: "hidden md:flex items-center gap-3", children: [_jsxs("button", { onClick: () => setShowTagTable(true), className: "flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors text-sm font-medium text-gray-700", title: "Manage Tags for All Line Items", children: [_jsx(Tag, { className: "w-4 h-4" }), _jsx("span", { className: "hidden md:inline", children: "Manage Tags" })] }), _jsx(RWAButton, { className: "hidden md:block", disabled: isAddingNew, onClick: handleAddClick, children: "Add Line Item" })] })] }), lineItems.length === 0 && !isAddingNew && (_jsx("div", { className: "md:hidden", children: _jsx(LineItemsEmptyState, { onAddItem: () => {
|
|
382
|
+
setMobileEditItem({});
|
|
383
|
+
setShowMobileModal(true);
|
|
384
|
+
} }) })), lineItems.length === 0 && !isAddingNew && (_jsx("div", { className: "hidden md:block", children: _jsx(LineItemsEmptyState, { onAddItem: handleAddClick }) })), lineItems.length > 0 && (_jsxs("div", { className: "md:hidden space-y-3", children: [_jsxs("div", { className: "flex gap-2 mb-4", children: [_jsxs("button", { onClick: () => setShowTagTable(true), className: "flex items-center justify-center gap-2 px-4 py-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors font-medium text-gray-700", title: "Manage Tags for All Line Items", children: [_jsx(Tag, { className: "w-4 h-4" }), _jsx("span", { children: "Tags" })] }), _jsx("button", { onClick: () => {
|
|
385
|
+
setMobileEditItem({});
|
|
386
|
+
setShowMobileModal(true);
|
|
387
|
+
}, className: "flex-1 py-3 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium", children: "Add Line Item" })] }), lineItems.map((item) => (_jsx(LineItemCard, { item: item, currency: currency, onEdit: () => {
|
|
388
|
+
setMobileEditItem(item);
|
|
389
|
+
setShowMobileModal(true);
|
|
390
|
+
}, onDelete: () => {
|
|
391
|
+
const input = { id: item.id };
|
|
392
|
+
onDeleteItem(input);
|
|
393
|
+
} }, item.id)))] })), (lineItems.length > 0 || isAddingNew) && (_jsx("div", { ref: tableContainerRef, className: "hidden md:block overflow-x-auto rounded-lg border border-gray-200", children: _jsxs("table", { ref: tableRef, className: "w-full table-fixed border-collapse bg-white", children: [_jsxs("colgroup", { children: [_jsx("col", { style: { width: "30%" } }), _jsx("col", { style: { width: "10%" } }), _jsx("col", { style: { width: "12%" } }), _jsx("col", { style: { width: "8%" } }), _jsx("col", {}), _jsx("col", {}), _jsx("col", {})] }), _jsx("thead", { children: _jsxs("tr", { className: "bg-gray-50", children: [_jsx("th", { className: "border-b border-gray-200 p-3 text-left", children: "Description" }), _jsx("th", { className: "border-b border-gray-200 p-3 text-right", children: "Quantity" }), _jsx("th", { className: "border-b border-gray-200 p-3 text-right", children: "Unit Price (excl. tax)" }), _jsx("th", { className: "border-b border-gray-200 p-3 text-right", children: "Tax %" }), _jsx("th", { className: "border-b border-gray-200 p-3 text-right", children: "Total (excl. tax)" }), _jsx("th", { className: "border-b border-gray-200 p-3 text-right", children: "Total (incl. tax)" }), _jsx("th", { className: "border-b border-gray-200 p-3 text-center", children: "Actions" })] }) }), _jsxs("tbody", { children: [lineItems.map((item) => editingId === item.id ? (_jsx(EditableLineItem, { currency: currency, item: item, onCancel: () => setEditingId(null), onSave: (updatedItem) => {
|
|
394
|
+
try {
|
|
395
|
+
onUpdateItem(updatedItem);
|
|
396
|
+
setEditingId(null);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.error(error);
|
|
400
|
+
if (error?.message?.includes("Invalid action input:")) {
|
|
401
|
+
try {
|
|
402
|
+
const zodError = JSON.parse(error.message.split("Invalid action input: ")[1]);
|
|
403
|
+
if (Array.isArray(zodError) &&
|
|
404
|
+
zodError.length > 0) {
|
|
405
|
+
const firstError = zodError[0];
|
|
406
|
+
const errorJSX = (_jsxs("div", { children: [_jsx("p", { className: "font-semibold", children: "Failed to update line item" }), _jsxs("p", { children: [firstError.message, ": "] }), zodError.map((err, index) => (_jsx("ul", { children: _jsxs("li", { className: "text-red-500 font-semibold", children: ["- ", err.path.join(".")] }) }, index)))] }));
|
|
407
|
+
toast(errorJSX, {
|
|
408
|
+
type: "error",
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (parseError) {
|
|
414
|
+
console.error("Failed to parse Zod error:", parseError);
|
|
415
|
+
toast("Invalid input data", {
|
|
370
416
|
type: "error",
|
|
371
417
|
});
|
|
372
418
|
return;
|
|
373
419
|
}
|
|
374
420
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
toast("Invalid input data", {
|
|
421
|
+
else if (error?.message) {
|
|
422
|
+
toast(error.message, {
|
|
378
423
|
type: "error",
|
|
379
424
|
});
|
|
380
425
|
return;
|
|
381
426
|
}
|
|
382
|
-
|
|
383
|
-
else if (error?.message) {
|
|
384
|
-
toast(error.message, {
|
|
427
|
+
toast("Failed to update line item", {
|
|
385
428
|
type: "error",
|
|
386
429
|
});
|
|
387
|
-
return;
|
|
388
430
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
? item.quantity.toString()
|
|
395
|
-
: item.quantity.toFixed(2) }), _jsx("td", { className: "border-b border-gray-200 p-3 text-right table-cell", children: formatNumber(item.unitPriceTaxExcl) }), _jsxs("td", { className: "border-b border-gray-200 p-3 text-right table-cell", children: [typeof item.taxPercent === "number"
|
|
396
|
-
? Math.round(item.taxPercent)
|
|
397
|
-
: 0, "%"] }), _jsx("td", { className: "border-b border-gray-200 p-3 text-right font-medium table-cell", children: formatNumber(item.totalPriceTaxExcl) }), _jsx("td", { className: "border-b border-gray-200 p-3 text-right font-medium table-cell", children: formatNumber(item.totalPriceTaxIncl) }), _jsx("td", { className: "border-b border-gray-200 p-3 table-cell", children: _jsxs("div", { className: "flex justify-center space-x-2", children: [_jsx("button", { className: "rounded bg-blue-500 px-3 py-1 text-white hover:bg-blue-200", onClick: () => setEditingId(item.id), children: "Edit" }), _jsx("button", { className: "rounded bg-red-600 px-3 py-1 text-white hover:bg-red-700", onClick: () => onDeleteItem({ id: item.id }), children: "Delete" })] }) })] }, item.id))), isAddingNew ? (_jsx(EditableLineItem, { currency: currency, item: {}, onCancel: handleCancelNewItem, onSave: handleSaveNewItem, onEditingItemChange: onEditingItemChange })) : null] })] }) })] }) }));
|
|
431
|
+
}, onEditingItemChange: onEditingItemChange }, item.id)) : (_jsxs("tr", { className: "hover:bg-gray-50 table-row", children: [_jsx("td", { className: "border-b border-gray-200 p-3 table-cell", children: item.description }), _jsx("td", { className: "border-b border-gray-200 p-3 text-right table-cell", children: item.quantity % 1 === 0
|
|
432
|
+
? item.quantity.toString()
|
|
433
|
+
: item.quantity.toFixed(2) }), _jsx("td", { className: "border-b border-gray-200 p-3 text-right table-cell", children: formatNumber(item.unitPriceTaxExcl) }), _jsxs("td", { className: "border-b border-gray-200 p-3 text-right table-cell", children: [typeof item.taxPercent === "number"
|
|
434
|
+
? Math.round(item.taxPercent)
|
|
435
|
+
: 0, "%"] }), _jsx("td", { className: "border-b border-gray-200 p-3 text-right font-medium table-cell", children: formatNumber(item.totalPriceTaxExcl) }), _jsx("td", { className: "border-b border-gray-200 p-3 text-right font-medium table-cell", children: formatNumber(item.totalPriceTaxIncl) }), _jsx("td", { className: "border-b border-gray-200 p-3 table-cell", children: _jsxs("div", { className: "flex justify-center space-x-2", children: [_jsx("button", { className: "rounded bg-blue-500 px-3 py-1 text-white hover:bg-blue-200", onClick: () => setEditingId(item.id), children: "Edit" }), _jsx("button", { className: "rounded bg-red-600 px-3 py-1 text-white hover:bg-red-700", onClick: () => onDeleteItem({ id: item.id }), children: "Delete" })] }) })] }, item.id))), isAddingNew ? (_jsx(EditableLineItem, { currency: currency, item: {}, onCancel: handleCancelNewItem, onSave: handleSaveNewItem, onEditingItemChange: onEditingItemChange })) : null] })] }) }))] }), lineItems.length > 0 && (_jsxs("div", { className: "md:hidden mt-4 bg-white border border-gray-200 rounded-lg p-4 space-y-2", children: [_jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { className: "text-gray-600", children: "Subtotal (excl. tax):" }), _jsxs("span", { className: "font-medium text-gray-900", children: [currency, " ", formatNumber(totalPriceTaxExcl)] })] }), _jsxs("div", { className: "flex justify-between text-sm", children: [_jsx("span", { className: "text-gray-600", children: "Total tax:" }), _jsxs("span", { className: "font-medium text-gray-900", children: [currency, " ", formatNumber(totalPriceTaxIncl - totalPriceTaxExcl)] })] }), _jsxs("div", { className: "flex justify-between text-base pt-2 border-t border-gray-200", children: [_jsx("span", { className: "font-semibold text-gray-900", children: "Total (incl. tax):" }), _jsxs("span", { className: "font-bold text-gray-900", children: [currency, " ", formatNumber(totalPriceTaxIncl)] })] })] }))] }));
|
|
398
436
|
}
|
|
@@ -3,8 +3,8 @@ import { useState } from "react";
|
|
|
3
3
|
import { actions } from "../../document-models/invoice/index.js";
|
|
4
4
|
import { generateId } from "document-model";
|
|
5
5
|
let GRAPHQL_URL = "http://localhost:4001/graphql/invoice";
|
|
6
|
-
if (!window.document.baseURI.includes(
|
|
7
|
-
GRAPHQL_URL =
|
|
6
|
+
if (!window.document.baseURI.includes("localhost")) {
|
|
7
|
+
GRAPHQL_URL = "https://switchboard-dev.powerhouse.xyz/graphql/invoice";
|
|
8
8
|
}
|
|
9
9
|
const RequestFinance = ({ docState, dispatch, }) => {
|
|
10
10
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
let GRAPHQL_URL = 'http://localhost:4001/graphql/invoice';
|
|
9
9
|
if (!window.document.baseURI.includes('localhost')) {
|
|
10
|
-
GRAPHQL_URL = 'https://
|
|
10
|
+
GRAPHQL_URL = 'https://switchboard-dev.powerhouse.xyz/graphql/invoice';
|
|
11
11
|
}
|
|
12
12
|
export async function uploadPdfChunked(pdfData, endpoint = GRAPHQL_URL, chunkSize = 500 * 1024, // 500KB chunks
|
|
13
13
|
onProgress) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { ValidationResult } from "./validationManager.js";
|
|
2
|
-
declare const validateStatusBeforeContinue: (newStatus: string, state: any, setInvoiceValidation: (validation: ValidationResult) => void, setWalletValidation: (validation: ValidationResult) => void, setCurrencyValidation: (validation: ValidationResult) => void, setMainCountryValidation: (validation: ValidationResult) => void, setBankCountryValidation: (validation: ValidationResult) => void, setIbanValidation: (validation: ValidationResult) => void, setBicValidation: (validation: ValidationResult) => void, setBankNameValidation: (validation: ValidationResult) => void, setStreetAddressValidation: (validation: ValidationResult) => void, setCityValidation: (validation: ValidationResult) => void, setPostalCodeValidation: (validation: ValidationResult) => void, setPayerEmailValidation: (validation: ValidationResult) => void, setLineItemValidation: (validation: ValidationResult) => void, setRoutingNumberValidation: (validation: ValidationResult) => void, isFiatCurrency: (currency: string) => boolean) => boolean | undefined;
|
|
2
|
+
declare const validateStatusBeforeContinue: (newStatus: string, state: any, setInvoiceValidation: (validation: ValidationResult) => void, setWalletValidation: (validation: ValidationResult) => void, setCurrencyValidation: (validation: ValidationResult) => void, setMainCountryValidation: (validation: ValidationResult) => void, setBankCountryValidation: (validation: ValidationResult) => void, setIbanValidation: (validation: ValidationResult) => void, setBicValidation: (validation: ValidationResult) => void, setAccountNumberValidation: (validation: ValidationResult) => void, setBankNameValidation: (validation: ValidationResult) => void, setStreetAddressValidation: (validation: ValidationResult) => void, setCityValidation: (validation: ValidationResult) => void, setPostalCodeValidation: (validation: ValidationResult) => void, setPayerEmailValidation: (validation: ValidationResult) => void, setLineItemValidation: (validation: ValidationResult) => void, setRoutingNumberValidation: (validation: ValidationResult) => void, isFiatCurrency: (currency: string) => boolean) => boolean | undefined;
|
|
3
3
|
export default validateStatusBeforeContinue;
|
|
4
4
|
//# sourceMappingURL=validationHandler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validationHandler.d.ts","sourceRoot":"","sources":["../../../../editors/invoice/validation/validationHandler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validationHandler.d.ts","sourceRoot":"","sources":["../../../../editors/invoice/validation/validationHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAoC,MAAM,wBAAwB,CAAC;AAI5F,QAAA,MAAM,4BAA4B,GAC9B,WAAW,MAAM,EACjB,OAAO,GAAG,EACV,sBAAsB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAC5D,qBAAqB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAC3D,uBAAuB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAC7D,0BAA0B,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAChE,0BAA0B,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAChE,mBAAmB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EACzD,kBAAkB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EACxD,4BAA4B,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAClE,uBAAuB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAC7D,4BAA4B,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAClE,mBAAmB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EACzD,yBAAyB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAC/D,yBAAyB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAC/D,uBAAuB,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAC7D,4BAA4B,CAAC,UAAU,EAAE,gBAAgB,KAAK,IAAI,EAClE,gBAAgB,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,wBAqNhD,CAAA;AAED,eAAe,4BAA4B,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateField } from "./validationManager.js";
|
|
2
2
|
import { toast } from "@powerhousedao/design-system";
|
|
3
|
-
|
|
3
|
+
import { isValidIBAN } from "./validationRules.js";
|
|
4
|
+
const validateStatusBeforeContinue = (newStatus, state, setInvoiceValidation, setWalletValidation, setCurrencyValidation, setMainCountryValidation, setBankCountryValidation, setIbanValidation, setBicValidation, setAccountNumberValidation, setBankNameValidation, setStreetAddressValidation, setCityValidation, setPostalCodeValidation, setPayerEmailValidation, setLineItemValidation, setRoutingNumberValidation, isFiatCurrency) => {
|
|
4
5
|
if (newStatus === "PAYMENTSCHEDULED" || newStatus === "ISSUED") {
|
|
5
6
|
const context = {
|
|
6
7
|
currency: state.currency,
|
|
@@ -43,11 +44,25 @@ const validateStatusBeforeContinue = (newStatus, state, setInvoiceValidation, se
|
|
|
43
44
|
if (bankCountryValidation && !bankCountryValidation.isValid) {
|
|
44
45
|
validationErrors.push(bankCountryValidation);
|
|
45
46
|
}
|
|
46
|
-
// Validate
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
// Validate account number or IBAN depending on currency to avoid duplicate validation of the same field
|
|
48
|
+
const IBAN_CURRENCIES = ["EUR", "GBP", "DKK"];
|
|
49
|
+
if (IBAN_CURRENCIES.includes(state.currency)) {
|
|
50
|
+
// Only IBAN applies
|
|
51
|
+
const ibanValidation = validateField("accountNum", state.issuer.paymentRouting?.bank?.accountNum, context);
|
|
52
|
+
setIbanValidation(ibanValidation);
|
|
53
|
+
setAccountNumberValidation(null);
|
|
54
|
+
if (ibanValidation && !ibanValidation.isValid) {
|
|
55
|
+
validationErrors.push(ibanValidation);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Generic account number applies
|
|
60
|
+
const accountNumberValidation = validateField("accountNum", state.issuer.paymentRouting?.bank?.accountNum, context);
|
|
61
|
+
setAccountNumberValidation(accountNumberValidation);
|
|
62
|
+
setIbanValidation(null);
|
|
63
|
+
if (accountNumberValidation && !accountNumberValidation.isValid) {
|
|
64
|
+
validationErrors.push(accountNumberValidation);
|
|
65
|
+
}
|
|
51
66
|
}
|
|
52
67
|
// Validate BIC/SWIFT number
|
|
53
68
|
const bicValidation = validateField("bicNumber", state.issuer.paymentRouting?.bank?.BIC || state.issuer.paymentRouting?.bank?.SWIFT, context);
|
|
@@ -57,9 +72,10 @@ const validateStatusBeforeContinue = (newStatus, state, setInvoiceValidation, se
|
|
|
57
72
|
}
|
|
58
73
|
// Validate routing number
|
|
59
74
|
const routingNumberValidation = validateField("routingNumber", state.issuer.paymentRouting?.bank?.ABA, context);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
const usdIbanPayment = isValidIBAN(state.issuer.paymentRouting?.bank?.accountNum ?? "") && state.currency === "USD";
|
|
76
|
+
setRoutingNumberValidation(usdIbanPayment ? null : routingNumberValidation);
|
|
77
|
+
if (usdIbanPayment ? null : routingNumberValidation && !routingNumberValidation.isValid) {
|
|
78
|
+
validationErrors.push(usdIbanPayment ? { isValid: true, message: '', severity: 'none' } : routingNumberValidation);
|
|
63
79
|
}
|
|
64
80
|
// Validate bank name
|
|
65
81
|
const bankNameValidation = validateField("bankName", state.issuer.paymentRouting?.bank?.name, context);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validationManager.d.ts","sourceRoot":"","sources":["../../../../editors/invoice/validation/validationManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"validationManager.d.ts","sourceRoot":"","sources":["../../../../editors/invoice/validation/validationManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAoBnE,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAE9D,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,kBAAkB,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,gBAAgB,CAAC;IAC1D,SAAS,EAAE;QACP,UAAU,EAAE,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;QAClC,iBAAiB,EAAE;YACf,IAAI,EAAE,MAAM,EAAE,CAAC;YACf,EAAE,EAAE,MAAM,EAAE,CAAC;SAChB,CAAC;KACL,CAAC;CACL,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;CACxB,CAAC;AAiCF,wBAAgB,aAAa,CACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,iBAAiB,GAC3B,gBAAgB,GAAG,IAAI,CAqBzB;AAGD,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,OAAO,EAAE,iBAAiB,GAC3B,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAalC;AAGD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAE5D;AAGD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAKxD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { accountIbanRule, bicNumberRule, bankNameRule, currencyRule, ethereumAddressRule, invoiceNumberRule, issuerPostalCodeRule, issuerStreetAddressRule, issuerCityRule, payerEmailRule, lineItemRule, mainCountryRule, bankCountryRule, routingNumberRule, accountNumberRule } from "./validationRules.js";
|
|
2
2
|
// Validation rules registry
|
|
3
3
|
const validationRules = [];
|
|
4
4
|
// Register rules
|
|
@@ -7,7 +7,7 @@ validationRules.push(ethereumAddressRule);
|
|
|
7
7
|
validationRules.push(currencyRule);
|
|
8
8
|
validationRules.push(mainCountryRule);
|
|
9
9
|
validationRules.push(bankCountryRule);
|
|
10
|
-
validationRules.push(
|
|
10
|
+
validationRules.push(accountIbanRule);
|
|
11
11
|
validationRules.push(bicNumberRule);
|
|
12
12
|
validationRules.push(bankNameRule);
|
|
13
13
|
validationRules.push(issuerStreetAddressRule);
|
|
@@ -16,6 +16,7 @@ validationRules.push(issuerPostalCodeRule);
|
|
|
16
16
|
validationRules.push(payerEmailRule);
|
|
17
17
|
validationRules.push(lineItemRule);
|
|
18
18
|
validationRules.push(routingNumberRule);
|
|
19
|
+
validationRules.push(accountNumberRule);
|
|
19
20
|
// Helper to check if a rule applies to the current context
|
|
20
21
|
function ruleAppliesToContext(rule, context) {
|
|
21
22
|
const { currencies, statusTransitions } = rule.appliesTo;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { ValidationRule } from "./validationManager.js";
|
|
2
|
+
export declare function isValidIBAN(iban: string): boolean;
|
|
2
3
|
export declare const invoiceNumberRule: ValidationRule;
|
|
3
4
|
export declare const ethereumAddressRule: ValidationRule;
|
|
4
5
|
export declare const currencyRule: ValidationRule;
|
|
5
6
|
export declare const mainCountryRule: ValidationRule;
|
|
6
7
|
export declare const bankCountryRule: ValidationRule;
|
|
8
|
+
export declare const accountIbanRule: ValidationRule;
|
|
7
9
|
export declare const accountNumberRule: ValidationRule;
|
|
8
10
|
export declare const bicNumberRule: ValidationRule;
|
|
9
11
|
export declare const bankNameRule: ValidationRule;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validationRules.d.ts","sourceRoot":"","sources":["../../../../editors/invoice/validation/validationRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"validationRules.d.ts","sourceRoot":"","sources":["../../../../editors/invoice/validation/validationRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAQxD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAYjD;AAQD,eAAO,MAAM,iBAAiB,EAAE,cAuB/B,CAAC;AAGF,eAAO,MAAM,mBAAmB,EAAE,cA8BjC,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,cAuB1B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,cAuB7B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,cAuB7B,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,cA8B7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,cA+B/B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,cAkC3B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,cAuB1B,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,cAuBrC,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,cAuB5B,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,cAuBlC,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,cA8B5B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,cAuB1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,cAqC/B,CAAC"}
|
|
@@ -3,17 +3,15 @@ function isValidEthereumAddress(address) {
|
|
|
3
3
|
const ethereumAddressRegex = /^0x[a-fA-F0-9]{40}$/;
|
|
4
4
|
return ethereumAddressRegex.test(address);
|
|
5
5
|
}
|
|
6
|
-
function isValidIBAN(iban) {
|
|
7
|
-
const ibanRegex = /^([A-Z]{2}[0-9]{2})(?=(?:[A-Z0-9]){9,30}$)((?:[A-Z0-9]{3,5}){2,7})([A-Z0-9]{1,3})?$/;
|
|
8
|
-
const hasNumbers = /\d/.test(iban);
|
|
6
|
+
export function isValidIBAN(iban) {
|
|
7
|
+
const ibanRegex = /^([A-Z]{2}[-]?[0-9]{2})(?=(?:[ -]?[A-Z0-9]){9,30}$)((?:[ -]?[A-Z0-9]{3,5}){2,7})([-]?[A-Z0-9]{1,3})?$/;
|
|
9
8
|
// Extract country code from IBAN (first 2 letters)
|
|
10
9
|
const countryCode = iban.substring(0, 2).toUpperCase();
|
|
11
10
|
// If IBAN starts with a valid country code (2 letters), validate full IBAN format
|
|
12
11
|
if (/^[A-Z]{2}$/.test(countryCode)) {
|
|
13
12
|
return ibanRegex.test(iban);
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
return iban.trim() !== '' && hasNumbers;
|
|
14
|
+
return false;
|
|
17
15
|
}
|
|
18
16
|
function isValidEmail(email) {
|
|
19
17
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -148,7 +146,7 @@ export const bankCountryRule = {
|
|
|
148
146
|
}
|
|
149
147
|
}
|
|
150
148
|
};
|
|
151
|
-
export const
|
|
149
|
+
export const accountIbanRule = {
|
|
152
150
|
field: 'accountNum',
|
|
153
151
|
validate: (value, document) => {
|
|
154
152
|
if (!value || value.trim() === '') {
|
|
@@ -172,7 +170,39 @@ export const accountNumberRule = {
|
|
|
172
170
|
};
|
|
173
171
|
},
|
|
174
172
|
appliesTo: {
|
|
175
|
-
currencies: ['EUR', 'GBP'],
|
|
173
|
+
currencies: ['EUR', 'GBP', 'DKK'],
|
|
174
|
+
statusTransitions: {
|
|
175
|
+
from: ['DRAFT'],
|
|
176
|
+
to: ['ISSUED']
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
export const accountNumberRule = {
|
|
181
|
+
field: 'accountNum',
|
|
182
|
+
validate: (value) => {
|
|
183
|
+
if (!value || value.trim() === '') {
|
|
184
|
+
return {
|
|
185
|
+
isValid: false,
|
|
186
|
+
message: 'Account number is required',
|
|
187
|
+
severity: 'warning'
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Valid account numbers are 6-25 alphanumeric characters. If it DOES NOT match, it's invalid.
|
|
191
|
+
if (!/^[\da-zA-Z]{6,25}$/.test(value)) {
|
|
192
|
+
return {
|
|
193
|
+
isValid: false,
|
|
194
|
+
message: 'Invalid account number format - For account number, ensure it is 6-25 characters long',
|
|
195
|
+
severity: 'warning'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
isValid: true,
|
|
200
|
+
message: '',
|
|
201
|
+
severity: 'none'
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
appliesTo: {
|
|
205
|
+
currencies: ['USD', 'JPY', 'CNY', 'CHF'],
|
|
176
206
|
statusTransitions: {
|
|
177
207
|
from: ['DRAFT'],
|
|
178
208
|
to: ['ISSUED']
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createExpenseReportCsv.d.ts","sourceRoot":"","sources":["../../../scripts/contributor-billing/createExpenseReportCsv.ts"],"names":[],"mappings":"AA+CA;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgGtG"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Simple in-memory cache for exchange rates
|
|
2
|
+
const exchangeRateCache = {};
|
|
3
|
+
/**
|
|
4
|
+
* Fetches the exchange rate from `from` currency to `to` currency for a given date using Frankfurter API.
|
|
5
|
+
* Returns 1 for unsupported currencies or errors.
|
|
6
|
+
*/
|
|
7
|
+
async function getExchangeRate(date, from, to) {
|
|
8
|
+
if (!date || !from || !to || from === to)
|
|
9
|
+
return 1;
|
|
10
|
+
// Convert crypto currencies to USD for API compatibility
|
|
11
|
+
let effectiveFrom = from;
|
|
12
|
+
if (from === 'DAI' || from === 'USDS') {
|
|
13
|
+
effectiveFrom = 'USD';
|
|
14
|
+
}
|
|
15
|
+
let effectiveTo = to;
|
|
16
|
+
if (to === 'DAI' || to === 'USDS') {
|
|
17
|
+
effectiveTo = 'USD';
|
|
18
|
+
}
|
|
19
|
+
const formattedDate = date.split('T')[0];
|
|
20
|
+
const cacheKey = `${formattedDate}|${effectiveFrom}|${effectiveTo}`;
|
|
21
|
+
if (exchangeRateCache[cacheKey] !== undefined) {
|
|
22
|
+
return exchangeRateCache[cacheKey];
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const url = `https://api.frankfurter.dev/v1/${formattedDate}?base=${effectiveFrom}&symbols=${effectiveTo}`;
|
|
26
|
+
console.log('Fetching from Frankfurter URL:', url);
|
|
27
|
+
const res = await fetch(url);
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
throw new Error(`Failed to fetch Frankfurter exchange rate: ${res.status} ${res.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
const rate = data?.rates?.[effectiveTo];
|
|
33
|
+
if (typeof rate !== 'number') {
|
|
34
|
+
throw new Error(`Frankfurter exchange rate for ${effectiveFrom} to ${effectiveTo} on ${formattedDate} not found in response`);
|
|
35
|
+
}
|
|
36
|
+
exchangeRateCache[cacheKey] = rate;
|
|
37
|
+
return rate;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
console.error('Frankfurter ForEx API error:', err);
|
|
41
|
+
return 1; // Fallback to 1:1 on error
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates an expense report CSV that categorizes line items by their tags
|
|
46
|
+
*/
|
|
47
|
+
export async function exportExpenseReportCSV(invoiceStates, baseCurrency) {
|
|
48
|
+
// Track invoices missing tags
|
|
49
|
+
const missingTagInvoices = [];
|
|
50
|
+
// Data structure to aggregate expenses by tag label
|
|
51
|
+
const expensesByTag = {};
|
|
52
|
+
// Process each selected invoice
|
|
53
|
+
for (const invoiceState of invoiceStates) {
|
|
54
|
+
const state = invoiceState.state.global;
|
|
55
|
+
const invoiceId = invoiceState.header.id;
|
|
56
|
+
const invoiceName = state.name || invoiceId;
|
|
57
|
+
const items = state.lineItems || [];
|
|
58
|
+
const dateIssued = state.dateIssued;
|
|
59
|
+
const currency = state.currency || 'USD';
|
|
60
|
+
// Check if any line item is missing tags
|
|
61
|
+
const hasMissingTags = items.some((item) => {
|
|
62
|
+
return !item.lineItemTag || item.lineItemTag.length === 0;
|
|
63
|
+
});
|
|
64
|
+
if (hasMissingTags) {
|
|
65
|
+
missingTagInvoices.push(invoiceName);
|
|
66
|
+
continue; // Skip this invoice
|
|
67
|
+
}
|
|
68
|
+
// Get exchange rate for this invoice
|
|
69
|
+
let effectiveCurrency = currency;
|
|
70
|
+
if (currency === 'DAI' || currency === 'USDS') {
|
|
71
|
+
effectiveCurrency = 'USD';
|
|
72
|
+
}
|
|
73
|
+
const exchangeRate = await getExchangeRate(dateIssued, effectiveCurrency, baseCurrency);
|
|
74
|
+
// Aggregate line items by tag
|
|
75
|
+
items.forEach((item) => {
|
|
76
|
+
const lineItemTags = item.lineItemTag || [];
|
|
77
|
+
const lineItemTotal = item.totalPriceTaxIncl || 0;
|
|
78
|
+
const convertedAmount = lineItemTotal * exchangeRate;
|
|
79
|
+
lineItemTags.forEach((tag) => {
|
|
80
|
+
const dimension = tag.dimension || '';
|
|
81
|
+
const label = tag.label || tag.value || 'Unknown';
|
|
82
|
+
// Skip accounting-period dimension
|
|
83
|
+
if (dimension === 'accounting-period') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Aggregate by tag label in base currency
|
|
87
|
+
if (!expensesByTag[label]) {
|
|
88
|
+
expensesByTag[label] = 0;
|
|
89
|
+
}
|
|
90
|
+
expensesByTag[label] += convertedAmount;
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// If any invoices are missing tags, throw an error
|
|
95
|
+
if (missingTagInvoices.length > 0) {
|
|
96
|
+
throw {
|
|
97
|
+
message: `The following invoices have line items missing tags: ${[...new Set(missingTagInvoices)].join(', ')}`,
|
|
98
|
+
missingTagInvoices: missingTagInvoices
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Create CSV headers
|
|
102
|
+
const headers = ['Tag Label', 'Currency', 'Total Amount'];
|
|
103
|
+
// Convert aggregated data to rows
|
|
104
|
+
const expenseRows = Object.entries(expensesByTag)
|
|
105
|
+
.sort(([labelA], [labelB]) => labelA.localeCompare(labelB))
|
|
106
|
+
.map(([label, total]) => [label, baseCurrency, total.toFixed(2)]);
|
|
107
|
+
// Combine headers and data rows
|
|
108
|
+
const allRows = [headers, ...expenseRows];
|
|
109
|
+
// Convert to CSV format
|
|
110
|
+
const csvLines = allRows.map((row) => row.map((value) => `"${value}"`).join(','));
|
|
111
|
+
const csvData = csvLines.join('\n');
|
|
112
|
+
// Download CSV file (browser)
|
|
113
|
+
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
|
|
114
|
+
const link = document.createElement('a');
|
|
115
|
+
const url = URL.createObjectURL(blob);
|
|
116
|
+
link.setAttribute('href', url);
|
|
117
|
+
link.setAttribute('download', `expense-report-by-tag-${new Date().toISOString().split('T')[0]}.csv`);
|
|
118
|
+
link.style.visibility = 'hidden';
|
|
119
|
+
document.body.appendChild(link);
|
|
120
|
+
link.click();
|
|
121
|
+
document.body.removeChild(link);
|
|
122
|
+
}
|