@powerhousedao/contributor-billing 0.1.4 → 0.1.5
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/editors/expense-report/components/AddBillingStatementModal.d.ts.map +1 -1
- package/dist/editors/expense-report/components/AddBillingStatementModal.js +46 -21
- package/dist/editors/expense-report/components/AggregatedExpensesTable.d.ts.map +1 -1
- package/dist/editors/expense-report/components/AggregatedExpensesTable.js +109 -73
- package/dist/editors/expense-report/components/ExpenseReportPDF.d.ts +10 -0
- package/dist/editors/expense-report/components/ExpenseReportPDF.d.ts.map +1 -0
- package/dist/editors/expense-report/components/ExpenseReportPDF.js +287 -0
- package/dist/editors/expense-report/editor.d.ts.map +1 -1
- package/dist/editors/expense-report/editor.js +26 -40
- package/dist/editors/expense-report/hooks/useSyncWallet.d.ts.map +1 -1
- package/dist/editors/expense-report/hooks/useSyncWallet.js +38 -18
- package/dist/editors/expense-report/hooks/useWalletSync.d.ts.map +1 -1
- package/dist/editors/expense-report/hooks/useWalletSync.js +25 -27
- package/dist/style.css +35 -21
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddBillingStatementModal.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/components/AddBillingStatementModal.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sDAAsD,CAAC;AAO1F,UAAU,6BAA6B;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AA+BD,wBAAgB,wBAAwB,CAAC,EACvC,MAAM,EACN,OAAO,EACP,aAAa,EACb,QAAQ,EACR,MAAM,GACP,EAAE,6BAA6B,
|
|
1
|
+
{"version":3,"file":"AddBillingStatementModal.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/components/AddBillingStatementModal.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sDAAsD,CAAC;AAO1F,UAAU,6BAA6B;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AA+BD,wBAAgB,wBAAwB,CAAC,EACvC,MAAM,EACN,OAAO,EACP,aAAa,EACb,QAAQ,EACR,MAAM,GACP,EAAE,6BAA6B,kDAuU/B"}
|
|
@@ -112,39 +112,64 @@ export function AddBillingStatementModal({ isOpen, onClose, walletAddress, dispa
|
|
|
112
112
|
const handleAddStatements = () => {
|
|
113
113
|
if (selectedStatements.size === 0)
|
|
114
114
|
return;
|
|
115
|
+
// First, add all billing statement references
|
|
115
116
|
selectedStatements.forEach((statementId) => {
|
|
116
|
-
const statement = billingStatements.find((s) => s.id === statementId);
|
|
117
|
-
if (!statement || !statement.document)
|
|
118
|
-
return;
|
|
119
|
-
console.log("Statement document:", statement.document);
|
|
120
|
-
// Add billing statement reference to wallet
|
|
121
117
|
dispatch(actions.addBillingStatement({
|
|
122
118
|
wallet: walletAddress,
|
|
123
119
|
billingStatementId: statementId,
|
|
124
120
|
}));
|
|
125
|
-
|
|
121
|
+
});
|
|
122
|
+
// Aggregate line items by category across all selected billing statements
|
|
123
|
+
const categoryAggregation = new Map();
|
|
124
|
+
selectedStatements.forEach((statementId) => {
|
|
125
|
+
const statement = billingStatements.find((s) => s.id === statementId);
|
|
126
|
+
if (!statement || !statement.document)
|
|
127
|
+
return;
|
|
128
|
+
console.log("Statement document:", statement.document);
|
|
129
|
+
// Extract line items from billing statement
|
|
126
130
|
const billingState = statement.document;
|
|
127
131
|
const lineItems = billingState.state?.global?.lineItems || [];
|
|
128
132
|
console.log("Line items found:", lineItems.length, lineItems);
|
|
133
|
+
// Aggregate line items by category
|
|
129
134
|
lineItems.forEach((billingLineItem) => {
|
|
130
135
|
const groupId = mapTagToGroup(billingLineItem);
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
136
|
+
const categoryKey = groupId || "uncategorized";
|
|
137
|
+
const existing = categoryAggregation.get(categoryKey);
|
|
138
|
+
if (existing) {
|
|
139
|
+
// Aggregate values for the same category
|
|
140
|
+
existing.actuals += billingLineItem.totalPriceCash || 0;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Create new category entry
|
|
144
|
+
const group = groups.find((g) => g.id === groupId);
|
|
145
|
+
categoryAggregation.set(categoryKey, {
|
|
146
|
+
groupId: groupId,
|
|
147
|
+
groupLabel: group?.label || "Uncategorised",
|
|
148
|
+
budget: 0,
|
|
149
|
+
actuals: billingLineItem.totalPriceCash || 0,
|
|
150
|
+
forecast: 0,
|
|
151
|
+
payments: 0,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
146
154
|
});
|
|
147
155
|
});
|
|
156
|
+
// Now add aggregated line items to wallet
|
|
157
|
+
categoryAggregation.forEach((aggregatedItem) => {
|
|
158
|
+
const expenseLineItem = {
|
|
159
|
+
id: generateId(),
|
|
160
|
+
label: aggregatedItem.groupLabel,
|
|
161
|
+
group: aggregatedItem.groupId,
|
|
162
|
+
budget: aggregatedItem.budget,
|
|
163
|
+
actuals: aggregatedItem.actuals,
|
|
164
|
+
forecast: aggregatedItem.forecast,
|
|
165
|
+
payments: aggregatedItem.payments,
|
|
166
|
+
comments: null,
|
|
167
|
+
};
|
|
168
|
+
dispatch(actions.addLineItem({
|
|
169
|
+
wallet: walletAddress,
|
|
170
|
+
lineItem: expenseLineItem,
|
|
171
|
+
}));
|
|
172
|
+
});
|
|
148
173
|
onClose();
|
|
149
174
|
};
|
|
150
175
|
if (!isOpen)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AggregatedExpensesTable.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/components/AggregatedExpensesTable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAY,MAAM,sDAAsD,CAAC;AAI5G,UAAU,4BAA4B;IACpC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CACjC;AAQD,wBAAgB,uBAAuB,CAAC,EACtC,OAAO,EACP,MAAM,EACN,WAAW,EACX,SAAS,EACT,QAAQ,GACT,EAAE,4BAA4B,
|
|
1
|
+
{"version":3,"file":"AggregatedExpensesTable.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/components/AggregatedExpensesTable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAY,MAAM,sDAAsD,CAAC;AAI5G,UAAU,4BAA4B;IACpC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CACjC;AAQD,wBAAgB,uBAAuB,CAAC,EACtC,OAAO,EACP,MAAM,EACN,WAAW,EACX,SAAS,EACT,QAAQ,GACT,EAAE,4BAA4B,kDAmjB9B"}
|
|
@@ -9,6 +9,10 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
9
9
|
// State for editing comments
|
|
10
10
|
const [editingGroupId, setEditingGroupId] = useState(null);
|
|
11
11
|
const [editingComment, setEditingComment] = useState("");
|
|
12
|
+
const [originalComment, setOriginalComment] = useState("");
|
|
13
|
+
// State for editing numeric fields
|
|
14
|
+
const [editingField, setEditingField] = useState(null);
|
|
15
|
+
const [editingValue, setEditingValue] = useState("");
|
|
12
16
|
// Format period for title
|
|
13
17
|
const periodTitle = useMemo(() => {
|
|
14
18
|
if (!periodStart)
|
|
@@ -37,6 +41,7 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
37
41
|
return map;
|
|
38
42
|
}, [groups]);
|
|
39
43
|
// Get line items for the active wallet with group information
|
|
44
|
+
// Line items are now already aggregated by category
|
|
40
45
|
const walletLineItems = useMemo(() => {
|
|
41
46
|
if (!wallets[activeWalletIndex])
|
|
42
47
|
return [];
|
|
@@ -48,50 +53,33 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
48
53
|
const groupInfo = item.group ? groupsMap.get(item.group) : undefined;
|
|
49
54
|
return {
|
|
50
55
|
...item,
|
|
51
|
-
groupLabel: groupInfo?.group.label || undefined,
|
|
56
|
+
groupLabel: groupInfo?.group.label || item.label || undefined,
|
|
52
57
|
parentGroupId: groupInfo?.parent?.id || null,
|
|
53
58
|
parentGroupLabel: groupInfo?.parent?.label || undefined,
|
|
54
59
|
};
|
|
55
60
|
});
|
|
56
61
|
}, [wallets, activeWalletIndex, groupsMap]);
|
|
57
|
-
// Group line items by
|
|
62
|
+
// Group line items by parent category
|
|
63
|
+
// Line items are already aggregated by category, so we just need to group them by parent
|
|
58
64
|
const groupedAndAggregatedItems = useMemo(() => {
|
|
59
|
-
|
|
60
|
-
const categoryAggregation = new Map();
|
|
65
|
+
const grouped = new Map();
|
|
61
66
|
walletLineItems.forEach((item) => {
|
|
62
67
|
if (!item)
|
|
63
68
|
return;
|
|
64
|
-
const
|
|
65
|
-
const existing = categoryAggregation.get(categoryKey);
|
|
66
|
-
if (existing) {
|
|
67
|
-
// Aggregate values for the same category
|
|
68
|
-
existing.budget += item.budget || 0;
|
|
69
|
-
existing.forecast += item.forecast || 0;
|
|
70
|
-
existing.actuals += item.actuals || 0;
|
|
71
|
-
existing.payments += item.payments || 0;
|
|
72
|
-
// Comment stays the same (first item's comment is used)
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
// Create new category entry
|
|
76
|
-
categoryAggregation.set(categoryKey, {
|
|
77
|
-
groupId: categoryKey,
|
|
78
|
-
groupLabel: item.groupLabel || "Uncategorised",
|
|
79
|
-
parentGroupId: item.parentGroupId,
|
|
80
|
-
parentGroupLabel: item.parentGroupLabel,
|
|
81
|
-
budget: item.budget || 0,
|
|
82
|
-
forecast: item.forecast || 0,
|
|
83
|
-
actuals: item.actuals || 0,
|
|
84
|
-
payments: item.payments || 0,
|
|
85
|
-
comment: item.comments || "",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
// Then, group aggregated categories by their parent
|
|
90
|
-
const grouped = new Map();
|
|
91
|
-
categoryAggregation.forEach((aggItem) => {
|
|
92
|
-
const parentKey = aggItem.parentGroupId || "uncategorized";
|
|
69
|
+
const parentKey = item.parentGroupId || "uncategorized";
|
|
93
70
|
const items = grouped.get(parentKey) || [];
|
|
94
|
-
items.push(
|
|
71
|
+
items.push({
|
|
72
|
+
lineItemId: item.id || "",
|
|
73
|
+
groupId: item.group || "uncategorized",
|
|
74
|
+
groupLabel: item.groupLabel || "Uncategorised",
|
|
75
|
+
parentGroupId: item.parentGroupId,
|
|
76
|
+
parentGroupLabel: item.parentGroupLabel,
|
|
77
|
+
budget: item.budget || 0,
|
|
78
|
+
forecast: item.forecast || 0,
|
|
79
|
+
actuals: item.actuals || 0,
|
|
80
|
+
payments: item.payments || 0,
|
|
81
|
+
comment: item.comments || "",
|
|
82
|
+
});
|
|
95
83
|
grouped.set(parentKey, items);
|
|
96
84
|
});
|
|
97
85
|
return grouped;
|
|
@@ -102,7 +90,7 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
102
90
|
budget: acc.budget + item.budget,
|
|
103
91
|
forecast: acc.forecast + item.forecast,
|
|
104
92
|
actuals: acc.actuals + item.actuals,
|
|
105
|
-
difference: acc.difference + (item.
|
|
93
|
+
difference: acc.difference + (item.forecast - item.actuals),
|
|
106
94
|
payments: acc.payments + item.payments,
|
|
107
95
|
}), { budget: 0, forecast: 0, actuals: 0, difference: 0, payments: 0 });
|
|
108
96
|
};
|
|
@@ -112,7 +100,7 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
112
100
|
budget: acc.budget + (item?.budget || 0),
|
|
113
101
|
forecast: acc.forecast + (item?.forecast || 0),
|
|
114
102
|
actuals: acc.actuals + (item?.actuals || 0),
|
|
115
|
-
difference: acc.difference + ((item?.
|
|
103
|
+
difference: acc.difference + ((item?.forecast || 0) - (item?.actuals || 0)),
|
|
116
104
|
payments: acc.payments + (item?.payments || 0),
|
|
117
105
|
}), { budget: 0, forecast: 0, actuals: 0, difference: 0, payments: 0 });
|
|
118
106
|
}, [walletLineItems]);
|
|
@@ -128,37 +116,67 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
128
116
|
return `${address.substring(0, 6)}...${address.substring(address.length - 6)}`;
|
|
129
117
|
};
|
|
130
118
|
// Handle starting comment edit
|
|
131
|
-
const handleStartEdit = (
|
|
132
|
-
setEditingGroupId(
|
|
119
|
+
const handleStartEdit = (lineItemId, currentComment) => {
|
|
120
|
+
setEditingGroupId(lineItemId);
|
|
133
121
|
setEditingComment(currentComment);
|
|
122
|
+
setOriginalComment(currentComment);
|
|
134
123
|
};
|
|
135
|
-
// Handle saving comment
|
|
136
|
-
const handleSaveComment = (
|
|
124
|
+
// Handle saving comment for a single line item
|
|
125
|
+
const handleSaveComment = () => {
|
|
137
126
|
const wallet = wallets[activeWalletIndex];
|
|
138
|
-
if (!wallet || !wallet.wallet)
|
|
127
|
+
if (!wallet || !wallet.wallet || !editingGroupId)
|
|
139
128
|
return;
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
lineItemId: item.id,
|
|
148
|
-
comments: editingComment,
|
|
149
|
-
}));
|
|
150
|
-
// Dispatch all actions at once
|
|
151
|
-
if (updateActions.length > 0) {
|
|
152
|
-
dispatch(updateActions);
|
|
129
|
+
// Only dispatch if the comment has actually changed
|
|
130
|
+
if (editingComment !== originalComment) {
|
|
131
|
+
dispatch(actions.updateLineItem({
|
|
132
|
+
wallet: wallet.wallet,
|
|
133
|
+
lineItemId: editingGroupId,
|
|
134
|
+
comments: editingComment,
|
|
135
|
+
}));
|
|
153
136
|
}
|
|
154
137
|
// Reset editing state
|
|
155
138
|
setEditingGroupId(null);
|
|
156
139
|
setEditingComment("");
|
|
140
|
+
setOriginalComment("");
|
|
157
141
|
};
|
|
158
142
|
// Handle canceling comment edit
|
|
159
143
|
const handleCancelEdit = () => {
|
|
160
144
|
setEditingGroupId(null);
|
|
161
145
|
setEditingComment("");
|
|
146
|
+
setOriginalComment("");
|
|
147
|
+
};
|
|
148
|
+
// Handle starting numeric field edit
|
|
149
|
+
const handleStartFieldEdit = (lineItemId, field, currentValue) => {
|
|
150
|
+
setEditingField({ lineItemId, field, originalValue: currentValue });
|
|
151
|
+
setEditingValue(currentValue.toString());
|
|
152
|
+
};
|
|
153
|
+
// Handle saving numeric field
|
|
154
|
+
const handleSaveField = () => {
|
|
155
|
+
const wallet = wallets[activeWalletIndex];
|
|
156
|
+
if (!wallet || !wallet.wallet || !editingField)
|
|
157
|
+
return;
|
|
158
|
+
const numericValue = parseFloat(editingValue);
|
|
159
|
+
if (isNaN(numericValue)) {
|
|
160
|
+
// Invalid number, cancel edit
|
|
161
|
+
handleCancelFieldEdit();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Only dispatch if the value has actually changed
|
|
165
|
+
if (numericValue !== editingField.originalValue) {
|
|
166
|
+
dispatch(actions.updateLineItem({
|
|
167
|
+
wallet: wallet.wallet,
|
|
168
|
+
lineItemId: editingField.lineItemId,
|
|
169
|
+
[editingField.field]: numericValue,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
// Reset editing state
|
|
173
|
+
setEditingField(null);
|
|
174
|
+
setEditingValue("");
|
|
175
|
+
};
|
|
176
|
+
// Handle canceling numeric field edit
|
|
177
|
+
const handleCancelFieldEdit = () => {
|
|
178
|
+
setEditingField(null);
|
|
179
|
+
setEditingValue("");
|
|
162
180
|
};
|
|
163
181
|
// Sort parent groups: Headcount first, then Non-Headcount, then others, then uncategorized
|
|
164
182
|
const sortedParentKeys = useMemo(() => {
|
|
@@ -197,7 +215,7 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
197
215
|
? "border-green-500 text-green-600 dark:text-green-400"
|
|
198
216
|
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"}
|
|
199
217
|
`, children: wallet.name || formatWalletAddress(wallet.wallet || "") }, wallet.wallet || index));
|
|
200
|
-
}) }) }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "min-w-full divide-y divide-gray-200 dark:divide-gray-700", children: [_jsx("thead", { className: "bg-gray-50 dark:bg-gray-800", children: _jsxs("tr", { children: [_jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Expense Category" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Mthly Budget" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Forecast" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Actuals" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Difference" }), _jsx("th", { className: "px-
|
|
218
|
+
}) }) }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "min-w-full divide-y divide-gray-200 dark:divide-gray-700", children: [_jsx("thead", { className: "bg-gray-50 dark:bg-gray-800", children: _jsxs("tr", { children: [_jsx("th", { className: "px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Expense Category" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Mthly Budget" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Forecast" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Actuals" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Difference" }), _jsx("th", { className: "px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-96", children: "Comments" }), _jsx("th", { className: "px-3 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-32", children: "Payments" })] }) }), _jsxs("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: [sortedParentKeys.map((parentKey) => {
|
|
201
219
|
const items = groupedAndAggregatedItems.get(parentKey) || [];
|
|
202
220
|
if (items.length === 0)
|
|
203
221
|
return null;
|
|
@@ -208,25 +226,43 @@ export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEn
|
|
|
208
226
|
return (_jsxs(React.Fragment, { children: [_jsx("tr", { className: "bg-gray-100 dark:bg-gray-800", children: _jsx("td", { colSpan: 7, className: "px-6 py-3 text-sm font-bold text-gray-900 dark:text-white", children: parentLabel }) }), items.map((item) => {
|
|
209
227
|
if (!item)
|
|
210
228
|
return null;
|
|
211
|
-
const difference = item.
|
|
212
|
-
const
|
|
213
|
-
|
|
229
|
+
const difference = item.forecast - item.actuals;
|
|
230
|
+
const isEditingComment = editingGroupId === item.lineItemId;
|
|
231
|
+
// Helper function to render editable numeric cell
|
|
232
|
+
const renderEditableCell = (field, value) => {
|
|
233
|
+
const isEditingThis = editingField?.lineItemId === item.lineItemId &&
|
|
234
|
+
editingField?.field === field;
|
|
235
|
+
if (isEditingThis) {
|
|
236
|
+
return (_jsx("div", { className: "flex items-center gap-1", children: _jsx("input", { type: "number", step: "0.01", value: editingValue, onChange: (e) => setEditingValue(e.target.value), onKeyDown: (e) => {
|
|
237
|
+
if (e.key === "Enter") {
|
|
238
|
+
handleSaveField();
|
|
239
|
+
}
|
|
240
|
+
else if (e.key === "Escape") {
|
|
241
|
+
handleCancelFieldEdit();
|
|
242
|
+
}
|
|
243
|
+
}, onBlur: handleSaveField, autoFocus: true, className: "w-full px-2 py-1 text-right text-sm border border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white" }) }));
|
|
244
|
+
}
|
|
245
|
+
return (_jsx("div", { className: "group cursor-pointer text-right", onClick: () => handleStartFieldEdit(item.lineItemId, field, value), children: _jsx("span", { className: "group-hover:bg-blue-50 dark:group-hover:bg-blue-900/20 inline-block px-1 py-0.5 rounded transition-colors min-w-[4rem]", children: formatNumber(value) }) }));
|
|
246
|
+
};
|
|
247
|
+
return (_jsxs("tr", { className: "bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors align-top", children: [_jsx("td", { className: "px-6 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-white", children: item.groupLabel }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: renderEditableCell("budget", item.budget) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: renderEditableCell("forecast", item.forecast) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: renderEditableCell("actuals", item.actuals) }), _jsx("td", { className: `px-6 py-3 whitespace-nowrap text-right text-sm font-medium ${difference < 0
|
|
214
248
|
? "text-red-600 dark:text-red-400"
|
|
215
|
-
: difference
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
249
|
+
: "text-gray-900 dark:text-white"}`, children: formatNumber(difference) }), _jsx("td", { className: "px-3 py-3 text-sm w-96", children: isEditingComment ? (_jsx(Textarea, { value: editingComment, onChange: (e) => setEditingComment(e.target.value), placeholder: "Add comment...", autoExpand: true, multiline: true, onKeyDown: (e) => {
|
|
250
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
handleSaveComment();
|
|
253
|
+
}
|
|
254
|
+
else if (e.key === "Escape") {
|
|
255
|
+
handleCancelEdit();
|
|
256
|
+
}
|
|
257
|
+
else if (e.key === "Tab") {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
handleSaveComment();
|
|
260
|
+
}
|
|
261
|
+
}, onBlur: handleSaveComment, autoFocus: true, className: "w-full px-2 py-1 text-sm border border-blue-500 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-white max-h-32 overflow-y-auto" })) : (_jsx("div", { className: "group cursor-pointer w-full max-h-20 overflow-hidden", onClick: () => handleStartEdit(item.lineItemId, item.comment), title: item.comment || "No comments", children: _jsx("span", { className: "group-hover:bg-blue-50 dark:group-hover:bg-blue-900/20 px-1 py-0.5 rounded transition-colors block text-gray-600 dark:text-gray-400 break-words", children: item.comment || "No comments" }) })) }), _jsx("td", { className: "px-3 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white w-32", children: renderEditableCell("payments", item.payments) })] }, item.lineItemId));
|
|
262
|
+
}), _jsxs("tr", { className: "bg-gray-50 dark:bg-gray-800/50 font-semibold align-top", children: [_jsx("td", { className: "px-6 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-white", children: "Subtotal" }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(subtotals.budget) }) }) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(subtotals.forecast) }) }) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(subtotals.actuals) }) }) }), _jsx("td", { className: `px-6 py-3 whitespace-nowrap text-right text-sm font-bold ${subtotals.difference < 0
|
|
223
263
|
? "text-red-600 dark:text-red-400"
|
|
224
|
-
: subtotals.difference
|
|
225
|
-
|
|
226
|
-
: "text-gray-900 dark:text-white"}`, children: formatNumber(subtotals.difference) }), _jsx("td", { className: "px-6 py-3" }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(subtotals.payments) })] })] }, parentKey));
|
|
227
|
-
}), _jsxs("tr", { className: "bg-gray-100 dark:bg-gray-800 font-bold", children: [_jsx("td", { className: "px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white", children: "Total" }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(grandTotals.budget) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(grandTotals.forecast) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(grandTotals.actuals) }), _jsx("td", { className: `px-6 py-4 whitespace-nowrap text-right text-sm ${grandTotals.difference > 0
|
|
264
|
+
: "text-gray-900 dark:text-white"}`, children: formatNumber(subtotals.difference) }), _jsx("td", { className: "px-3 py-3" }), _jsx("td", { className: "px-3 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white w-32", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(subtotals.payments) }) }) })] })] }, parentKey));
|
|
265
|
+
}), _jsxs("tr", { className: "bg-gray-100 dark:bg-gray-800 font-bold align-top", children: [_jsx("td", { className: "px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white", children: "Total" }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(grandTotals.budget) }) }) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(grandTotals.forecast) }) }) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(grandTotals.actuals) }) }) }), _jsx("td", { className: `px-6 py-4 whitespace-nowrap text-right text-sm ${grandTotals.difference < 0
|
|
228
266
|
? "text-red-600 dark:text-red-400"
|
|
229
|
-
: grandTotals.difference
|
|
230
|
-
? "text-green-600 dark:text-green-400"
|
|
231
|
-
: "text-gray-900 dark:text-white"}`, children: formatNumber(grandTotals.difference) }), _jsx("td", { className: "px-6 py-4" }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(grandTotals.payments) })] })] })] }) })] }));
|
|
267
|
+
: "text-gray-900 dark:text-white"}`, children: formatNumber(grandTotals.difference) }), _jsx("td", { className: "px-3 py-4" }), _jsx("td", { className: "px-3 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white w-32", children: _jsx("div", { className: "text-right", children: _jsx("span", { className: "inline-block px-1 py-0.5 min-w-[4rem]", children: formatNumber(grandTotals.payments) }) }) })] })] })] }) })] }));
|
|
232
268
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Wallet, LineItemGroup } from "../../../document-models/expense-report/gen/types.js";
|
|
2
|
+
interface ExpenseReportPDFProps {
|
|
3
|
+
periodStart?: string | null;
|
|
4
|
+
periodEnd?: string | null;
|
|
5
|
+
wallets: Wallet[];
|
|
6
|
+
groups: LineItemGroup[];
|
|
7
|
+
}
|
|
8
|
+
export declare function ExpenseReportPDF({ periodStart, periodEnd, wallets, groups, }: ExpenseReportPDFProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=ExpenseReportPDF.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpenseReportPDF.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/components/ExpenseReportPDF.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EACN,aAAa,EAEd,MAAM,sDAAsD,CAAC;AAE9D,UAAU,qBAAqB;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAmKD,wBAAgB,gBAAgB,CAAC,EAC/B,WAAW,EACX,SAAS,EACT,OAAO,EACP,MAAM,GACP,EAAE,qBAAqB,2CAiVvB"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Document, Page, Text, View, StyleSheet } from "@react-pdf/renderer";
|
|
3
|
+
// Tailwind-inspired styles for PDF
|
|
4
|
+
const styles = StyleSheet.create({
|
|
5
|
+
page: {
|
|
6
|
+
padding: 32,
|
|
7
|
+
fontSize: 8,
|
|
8
|
+
fontFamily: "Helvetica",
|
|
9
|
+
backgroundColor: "#ffffff",
|
|
10
|
+
},
|
|
11
|
+
header: {
|
|
12
|
+
marginBottom: 20,
|
|
13
|
+
borderBottom: "1pt solid #e5e7eb",
|
|
14
|
+
paddingBottom: 12,
|
|
15
|
+
},
|
|
16
|
+
title: {
|
|
17
|
+
fontSize: 18,
|
|
18
|
+
fontWeight: "bold",
|
|
19
|
+
marginBottom: 6,
|
|
20
|
+
color: "#111827",
|
|
21
|
+
textAlign: "center",
|
|
22
|
+
},
|
|
23
|
+
period: {
|
|
24
|
+
fontSize: 9,
|
|
25
|
+
color: "#6b7280",
|
|
26
|
+
textAlign: "center",
|
|
27
|
+
marginTop: 2,
|
|
28
|
+
},
|
|
29
|
+
sectionTitle: {
|
|
30
|
+
fontSize: 12,
|
|
31
|
+
fontWeight: "bold",
|
|
32
|
+
marginTop: 16,
|
|
33
|
+
marginBottom: 8,
|
|
34
|
+
color: "#111827",
|
|
35
|
+
},
|
|
36
|
+
walletInfo: {
|
|
37
|
+
fontSize: 8,
|
|
38
|
+
color: "#6b7280",
|
|
39
|
+
marginBottom: 8,
|
|
40
|
+
},
|
|
41
|
+
table: {
|
|
42
|
+
marginBottom: 16,
|
|
43
|
+
},
|
|
44
|
+
tableHeader: {
|
|
45
|
+
flexDirection: "row",
|
|
46
|
+
borderBottom: "1pt solid #e5e7eb",
|
|
47
|
+
paddingBottom: 6,
|
|
48
|
+
marginBottom: 6,
|
|
49
|
+
backgroundColor: "#f9fafb",
|
|
50
|
+
paddingTop: 6,
|
|
51
|
+
paddingHorizontal: 6,
|
|
52
|
+
},
|
|
53
|
+
tableRow: {
|
|
54
|
+
flexDirection: "row",
|
|
55
|
+
paddingVertical: 4,
|
|
56
|
+
paddingHorizontal: 6,
|
|
57
|
+
borderBottom: "0.5pt solid #f3f4f6",
|
|
58
|
+
minHeight: 20,
|
|
59
|
+
},
|
|
60
|
+
tableRowAlt: {
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
paddingVertical: 4,
|
|
63
|
+
paddingHorizontal: 6,
|
|
64
|
+
backgroundColor: "#f9fafb",
|
|
65
|
+
borderBottom: "0.5pt solid #f3f4f6",
|
|
66
|
+
minHeight: 20,
|
|
67
|
+
},
|
|
68
|
+
subtotalRow: {
|
|
69
|
+
flexDirection: "row",
|
|
70
|
+
paddingVertical: 6,
|
|
71
|
+
paddingHorizontal: 6,
|
|
72
|
+
borderTop: "1pt solid #d1d5db",
|
|
73
|
+
marginTop: 3,
|
|
74
|
+
fontWeight: "bold",
|
|
75
|
+
backgroundColor: "#fafafa",
|
|
76
|
+
},
|
|
77
|
+
totalRow: {
|
|
78
|
+
flexDirection: "row",
|
|
79
|
+
paddingVertical: 6,
|
|
80
|
+
paddingHorizontal: 6,
|
|
81
|
+
borderTop: "2pt solid #111827",
|
|
82
|
+
marginTop: 3,
|
|
83
|
+
fontWeight: "bold",
|
|
84
|
+
backgroundColor: "#f3f4f6",
|
|
85
|
+
},
|
|
86
|
+
headerCell: {
|
|
87
|
+
fontSize: 7,
|
|
88
|
+
fontWeight: "bold",
|
|
89
|
+
color: "#374151",
|
|
90
|
+
textTransform: "uppercase",
|
|
91
|
+
letterSpacing: 0.3,
|
|
92
|
+
},
|
|
93
|
+
cell: {
|
|
94
|
+
fontSize: 8,
|
|
95
|
+
color: "#111827",
|
|
96
|
+
},
|
|
97
|
+
cellRight: {
|
|
98
|
+
fontSize: 8,
|
|
99
|
+
color: "#111827",
|
|
100
|
+
textAlign: "right",
|
|
101
|
+
},
|
|
102
|
+
cellBold: {
|
|
103
|
+
fontSize: 8,
|
|
104
|
+
color: "#111827",
|
|
105
|
+
fontWeight: "bold",
|
|
106
|
+
},
|
|
107
|
+
// Breakdown table columns - adjusted to match AggregatedExpensesTable
|
|
108
|
+
categoryCol: { width: "20%" },
|
|
109
|
+
budgetCol: { width: "11%", textAlign: "right" },
|
|
110
|
+
forecastCol: { width: "11%", textAlign: "right" },
|
|
111
|
+
actualsCol: { width: "11%", textAlign: "right" },
|
|
112
|
+
differenceCol: { width: "11%", textAlign: "right" },
|
|
113
|
+
commentsCol: { width: "25%", paddingLeft: 4 },
|
|
114
|
+
paymentsCol: { width: "11%", textAlign: "right" },
|
|
115
|
+
commentsText: {
|
|
116
|
+
fontSize: 7,
|
|
117
|
+
color: "#6b7280",
|
|
118
|
+
lineHeight: 1.4,
|
|
119
|
+
},
|
|
120
|
+
// Color styles for difference column
|
|
121
|
+
differenceNegative: {
|
|
122
|
+
color: "#dc2626", // red-600 for negative values
|
|
123
|
+
},
|
|
124
|
+
differenceNormal: {
|
|
125
|
+
color: "#111827", // gray-900 for positive or zero
|
|
126
|
+
},
|
|
127
|
+
pageNumber: {
|
|
128
|
+
position: "absolute",
|
|
129
|
+
bottom: 20,
|
|
130
|
+
left: 0,
|
|
131
|
+
right: 0,
|
|
132
|
+
textAlign: "center",
|
|
133
|
+
fontSize: 7,
|
|
134
|
+
color: "#6b7280",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
// Format number as currency
|
|
138
|
+
const formatNumber = (value) => {
|
|
139
|
+
if (value === null || value === undefined)
|
|
140
|
+
return "0.00";
|
|
141
|
+
return value.toLocaleString("en-US", {
|
|
142
|
+
minimumFractionDigits: 2,
|
|
143
|
+
maximumFractionDigits: 2,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
// Format date
|
|
147
|
+
const formatDate = (dateString) => {
|
|
148
|
+
if (!dateString)
|
|
149
|
+
return "";
|
|
150
|
+
const date = new Date(dateString);
|
|
151
|
+
return date.toLocaleDateString("en-US", {
|
|
152
|
+
month: "short",
|
|
153
|
+
day: "numeric",
|
|
154
|
+
year: "numeric",
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
export function ExpenseReportPDF({ periodStart, periodEnd, wallets, groups, }) {
|
|
158
|
+
// Create a map of groups with their parent info
|
|
159
|
+
const groupsMap = new Map();
|
|
160
|
+
groups.forEach((group) => {
|
|
161
|
+
groupsMap.set(group.id, { group });
|
|
162
|
+
});
|
|
163
|
+
groups.forEach((group) => {
|
|
164
|
+
if (group.parentId) {
|
|
165
|
+
const entry = groupsMap.get(group.id);
|
|
166
|
+
const parentEntry = groupsMap.get(group.parentId);
|
|
167
|
+
if (entry && parentEntry) {
|
|
168
|
+
entry.parent = parentEntry.group;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// Get line items for a wallet with group information
|
|
173
|
+
const getWalletLineItems = (wallet) => {
|
|
174
|
+
const lineItems = wallet.lineItems || [];
|
|
175
|
+
return lineItems
|
|
176
|
+
.filter((item) => item !== null && item !== undefined)
|
|
177
|
+
.map((item) => {
|
|
178
|
+
const groupInfo = item.group ? groupsMap.get(item.group) : undefined;
|
|
179
|
+
return {
|
|
180
|
+
...item,
|
|
181
|
+
groupLabel: groupInfo?.group.label || item.label || undefined,
|
|
182
|
+
parentGroupId: groupInfo?.parent?.id || null,
|
|
183
|
+
parentGroupLabel: groupInfo?.parent?.label || undefined,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
// Group line items by parent category
|
|
188
|
+
const groupLineItemsByParent = (lineItems) => {
|
|
189
|
+
const grouped = new Map();
|
|
190
|
+
lineItems.forEach((item) => {
|
|
191
|
+
const key = item.parentGroupId || "uncategorized";
|
|
192
|
+
if (!grouped.has(key)) {
|
|
193
|
+
grouped.set(key, []);
|
|
194
|
+
}
|
|
195
|
+
grouped.get(key).push(item);
|
|
196
|
+
});
|
|
197
|
+
// Convert to array and sort by hierarchy: Headcount, Non-Headcount, others, then uncategorized
|
|
198
|
+
const entries = Array.from(grouped.entries());
|
|
199
|
+
// Find Headcount and Non-Headcount group IDs
|
|
200
|
+
const headcountGroup = groups.find((g) => g.label === "Headcount Expenses");
|
|
201
|
+
const nonHeadcountGroup = groups.find((g) => g.label === "Non-Headcount Expenses");
|
|
202
|
+
entries.sort(([keyA], [keyB]) => {
|
|
203
|
+
// Uncategorized always goes last
|
|
204
|
+
if (keyA === "uncategorized")
|
|
205
|
+
return 1;
|
|
206
|
+
if (keyB === "uncategorized")
|
|
207
|
+
return -1;
|
|
208
|
+
// Headcount Expenses always first
|
|
209
|
+
if (keyA === headcountGroup?.id)
|
|
210
|
+
return -1;
|
|
211
|
+
if (keyB === headcountGroup?.id)
|
|
212
|
+
return 1;
|
|
213
|
+
// Non-Headcount Expenses always second
|
|
214
|
+
if (keyA === nonHeadcountGroup?.id)
|
|
215
|
+
return -1;
|
|
216
|
+
if (keyB === nonHeadcountGroup?.id)
|
|
217
|
+
return 1;
|
|
218
|
+
// For other groups, maintain their original order
|
|
219
|
+
return 0;
|
|
220
|
+
});
|
|
221
|
+
return entries.map(([key, items]) => ({
|
|
222
|
+
parentLabel: key === "uncategorized"
|
|
223
|
+
? "Uncategorised"
|
|
224
|
+
: items[0]?.parentGroupLabel || "Unknown",
|
|
225
|
+
items,
|
|
226
|
+
}));
|
|
227
|
+
};
|
|
228
|
+
return (_jsx(Document, { children: _jsxs(Page, { size: "A4", style: styles.page, children: [_jsxs(View, { style: styles.header, children: [_jsx(Text, { style: styles.title, children: "Expense Report" }), periodStart && (_jsxs(Text, { style: styles.period, children: ["Period: ", formatDate(periodStart), " to ", formatDate(periodEnd)] }))] }), wallets.map((wallet, walletIndex) => {
|
|
229
|
+
const lineItems = getWalletLineItems(wallet);
|
|
230
|
+
const groupedItems = groupLineItemsByParent(lineItems);
|
|
231
|
+
// Calculate grand totals
|
|
232
|
+
const grandTotals = lineItems.reduce((acc, item) => ({
|
|
233
|
+
budget: acc.budget + (item.budget || 0),
|
|
234
|
+
forecast: acc.forecast + (item.forecast || 0),
|
|
235
|
+
actuals: acc.actuals + (item.actuals || 0),
|
|
236
|
+
payments: acc.payments + (item.payments || 0),
|
|
237
|
+
}), { budget: 0, forecast: 0, actuals: 0, payments: 0 });
|
|
238
|
+
return (_jsxs(View, { wrap: false, break: walletIndex > 0, children: [_jsxs(Text, { style: styles.sectionTitle, children: [periodStart &&
|
|
239
|
+
new Date(periodStart).toLocaleDateString("en-US", {
|
|
240
|
+
month: "short",
|
|
241
|
+
year: "numeric",
|
|
242
|
+
}), " ", "Breakdown"] }), _jsxs(Text, { style: styles.walletInfo, children: [wallet.name && `${wallet.name} • `, wallet.wallet || "Unknown Wallet"] }), _jsxs(View, { style: styles.table, children: [_jsxs(View, { style: styles.tableHeader, children: [_jsx(Text, { style: [styles.headerCell, styles.categoryCol], children: "Category" }), _jsx(Text, { style: [styles.headerCell, styles.budgetCol], children: "Budget" }), _jsx(Text, { style: [styles.headerCell, styles.forecastCol], children: "Forecast" }), _jsx(Text, { style: [styles.headerCell, styles.actualsCol], children: "Actuals" }), _jsx(Text, { style: [styles.headerCell, styles.differenceCol], children: "Difference" }), _jsx(Text, { style: [styles.headerCell, styles.commentsCol], children: "Comments" }), _jsx(Text, { style: [styles.headerCell, styles.paymentsCol], children: "Payments" })] }), groupedItems.map((group, groupIndex) => {
|
|
243
|
+
const subtotals = group.items.reduce((acc, item) => ({
|
|
244
|
+
budget: acc.budget + (item.budget || 0),
|
|
245
|
+
forecast: acc.forecast + (item.forecast || 0),
|
|
246
|
+
actuals: acc.actuals + (item.actuals || 0),
|
|
247
|
+
payments: acc.payments + (item.payments || 0),
|
|
248
|
+
}), { budget: 0, forecast: 0, actuals: 0, payments: 0 });
|
|
249
|
+
const subtotalDifference = subtotals.forecast - subtotals.actuals;
|
|
250
|
+
return (_jsxs(View, { children: [_jsx(View, { style: {
|
|
251
|
+
paddingVertical: 6,
|
|
252
|
+
paddingHorizontal: 6,
|
|
253
|
+
backgroundColor: "#f9fafb",
|
|
254
|
+
borderBottom: "1pt solid #e5e7eb",
|
|
255
|
+
marginTop: groupIndex > 0 ? 8 : 0,
|
|
256
|
+
}, children: _jsx(Text, { style: {
|
|
257
|
+
fontSize: 9,
|
|
258
|
+
fontWeight: "bold",
|
|
259
|
+
color: "#111827",
|
|
260
|
+
}, children: group.parentLabel }) }), group.items.map((item, itemIndex) => {
|
|
261
|
+
const difference = (item.forecast || 0) - (item.actuals || 0);
|
|
262
|
+
const differenceStyle = difference < 0
|
|
263
|
+
? styles.differenceNegative
|
|
264
|
+
: styles.differenceNormal;
|
|
265
|
+
return (_jsxs(View, { style: itemIndex % 2 === 0
|
|
266
|
+
? styles.tableRow
|
|
267
|
+
: styles.tableRowAlt, children: [_jsx(Text, { style: [styles.cell, styles.categoryCol], children: item.groupLabel || item.label || "Uncategorised" }), _jsx(Text, { style: [styles.cellRight, styles.budgetCol], children: formatNumber(item.budget) }), _jsx(Text, { style: [styles.cellRight, styles.forecastCol], children: formatNumber(item.forecast) }), _jsx(Text, { style: [styles.cellRight, styles.actualsCol], children: formatNumber(item.actuals) }), _jsx(Text, { style: [
|
|
268
|
+
styles.cellRight,
|
|
269
|
+
styles.differenceCol,
|
|
270
|
+
differenceStyle,
|
|
271
|
+
], children: formatNumber(difference) }), _jsx(View, { style: styles.commentsCol, children: item.comments && (_jsx(Text, { style: styles.commentsText, children: item.comments })) }), _jsx(Text, { style: [styles.cellRight, styles.paymentsCol], children: formatNumber(item.payments) })] }, item.id));
|
|
272
|
+
}), _jsxs(View, { style: styles.subtotalRow, children: [_jsx(Text, { style: [styles.cellBold, styles.categoryCol], children: "Subtotal" }), _jsx(Text, { style: [styles.cellRight, styles.budgetCol], children: formatNumber(subtotals.budget) }), _jsx(Text, { style: [styles.cellRight, styles.forecastCol], children: formatNumber(subtotals.forecast) }), _jsx(Text, { style: [styles.cellRight, styles.actualsCol], children: formatNumber(subtotals.actuals) }), _jsx(Text, { style: [
|
|
273
|
+
styles.cellRight,
|
|
274
|
+
styles.differenceCol,
|
|
275
|
+
subtotalDifference < 0
|
|
276
|
+
? styles.differenceNegative
|
|
277
|
+
: styles.differenceNormal,
|
|
278
|
+
], children: formatNumber(subtotalDifference) }), _jsx(View, { style: styles.commentsCol }), _jsx(Text, { style: [styles.cellRight, styles.paymentsCol], children: formatNumber(subtotals.payments) })] })] }, group.parentLabel));
|
|
279
|
+
}), _jsxs(View, { style: styles.totalRow, children: [_jsx(Text, { style: [styles.cellBold, styles.categoryCol], children: "Total" }), _jsx(Text, { style: [styles.cellRight, styles.budgetCol], children: formatNumber(grandTotals.budget) }), _jsx(Text, { style: [styles.cellRight, styles.forecastCol], children: formatNumber(grandTotals.forecast) }), _jsx(Text, { style: [styles.cellRight, styles.actualsCol], children: formatNumber(grandTotals.actuals) }), _jsx(Text, { style: [
|
|
280
|
+
styles.cellRight,
|
|
281
|
+
styles.differenceCol,
|
|
282
|
+
grandTotals.forecast - grandTotals.actuals < 0
|
|
283
|
+
? styles.differenceNegative
|
|
284
|
+
: styles.differenceNormal,
|
|
285
|
+
], children: formatNumber(grandTotals.forecast - grandTotals.actuals) }), _jsx(View, { style: styles.commentsCol }), _jsx(Text, { style: [styles.cellRight, styles.paymentsCol], children: formatNumber(grandTotals.payments) })] })] })] }, wallet.wallet || walletIndex));
|
|
286
|
+
}), _jsx(Text, { style: styles.pageNumber, render: ({ pageNumber, totalPages }) => `Page ${pageNumber} of ${totalPages}`, fixed: true })] }) }));
|
|
287
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../editors/expense-report/editor.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../editors/expense-report/editor.tsx"],"names":[],"mappings":"AAWA,wBAAgB,MAAM,4CAmMrB"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useMemo
|
|
2
|
+
import { useState, useMemo } from "react";
|
|
3
3
|
import { useSelectedExpenseReportDocument } from "../hooks/useExpenseReportDocument.js";
|
|
4
4
|
import { actions } from "../../document-models/expense-report/index.js";
|
|
5
|
-
import { DatePicker } from "@powerhousedao/document-engineering";
|
|
5
|
+
import { DatePicker, Icon, Button } from "@powerhousedao/document-engineering";
|
|
6
6
|
import { WalletsTable } from "./components/WalletsTable.js";
|
|
7
7
|
import { AggregatedExpensesTable } from "./components/AggregatedExpensesTable.js";
|
|
8
8
|
import { AddBillingStatementModal } from "./components/AddBillingStatementModal.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { ExpenseReportPDF } from "./components/ExpenseReportPDF.js";
|
|
10
|
+
import { pdf } from "@react-pdf/renderer";
|
|
11
11
|
export function Editor() {
|
|
12
12
|
const [document, dispatch] = useSelectedExpenseReportDocument();
|
|
13
13
|
const [selectedWallet, setSelectedWallet] = useState(null);
|
|
@@ -15,41 +15,6 @@ export function Editor() {
|
|
|
15
15
|
const [periodStart, setPeriodStart] = useState(document.state.global.periodStart || "");
|
|
16
16
|
const [periodEnd, setPeriodEnd] = useState(document.state.global.periodEnd || "");
|
|
17
17
|
const { wallets, groups } = document.state.global;
|
|
18
|
-
// Check sync status
|
|
19
|
-
const { needsSync, outdatedWallets, tagChangedWallets } = useWalletSync(wallets);
|
|
20
|
-
const { syncWallet } = useSyncWallet();
|
|
21
|
-
// Auto-sync on component mount
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (needsSync && outdatedWallets.length > 0) {
|
|
24
|
-
if (tagChangedWallets.length > 0) {
|
|
25
|
-
console.warn("⚠️ Tag changes detected in wallets:", tagChangedWallets);
|
|
26
|
-
console.log("Auto-syncing wallets with tag changes:", outdatedWallets);
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
console.log("Auto-syncing wallets:", outdatedWallets);
|
|
30
|
-
}
|
|
31
|
-
// Sync each outdated wallet
|
|
32
|
-
outdatedWallets.forEach((walletAddress) => {
|
|
33
|
-
const wallet = wallets.find((w) => w.wallet === walletAddress);
|
|
34
|
-
if (!wallet || !wallet.billingStatements || wallet.billingStatements.length === 0) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
// Remove all existing line items first
|
|
38
|
-
const lineItemsToRemove = [...(wallet.lineItems || [])];
|
|
39
|
-
lineItemsToRemove.forEach((item) => {
|
|
40
|
-
if (item?.id) {
|
|
41
|
-
dispatch(actions.removeLineItem({
|
|
42
|
-
wallet: wallet.wallet,
|
|
43
|
-
lineItemId: item.id,
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
// Re-extract line items from billing statements
|
|
48
|
-
const billingStatementIds = wallet.billingStatements.filter((id) => id !== null && id !== undefined);
|
|
49
|
-
syncWallet(wallet.wallet, billingStatementIds, groups, dispatch);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}, [needsSync, outdatedWallets, wallets, groups, dispatch, syncWallet]);
|
|
53
18
|
// Handle period date changes
|
|
54
19
|
const handlePeriodStartChange = (e) => {
|
|
55
20
|
const value = e.target.value;
|
|
@@ -75,6 +40,27 @@ export function Editor() {
|
|
|
75
40
|
setIsModalOpen(false);
|
|
76
41
|
setSelectedWallet(null);
|
|
77
42
|
};
|
|
43
|
+
// Handle PDF export
|
|
44
|
+
const handleExportPDF = async () => {
|
|
45
|
+
try {
|
|
46
|
+
const blob = await pdf(_jsx(ExpenseReportPDF, { periodStart: periodStart, periodEnd: periodEnd, wallets: wallets, groups: groups })).toBlob();
|
|
47
|
+
// Create download link
|
|
48
|
+
const url = URL.createObjectURL(blob);
|
|
49
|
+
const link = window.document.createElement("a");
|
|
50
|
+
link.href = url;
|
|
51
|
+
// Generate filename with period
|
|
52
|
+
const filename = periodStart
|
|
53
|
+
? `expense-report-${new Date(periodStart).toISOString().split('T')[0]}.pdf`
|
|
54
|
+
: "expense-report.pdf";
|
|
55
|
+
link.download = filename;
|
|
56
|
+
link.click();
|
|
57
|
+
// Cleanup
|
|
58
|
+
URL.revokeObjectURL(url);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error("Error generating PDF:", error);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
78
64
|
// Format period title for the breakdown section
|
|
79
65
|
const breakdownTitle = useMemo(() => {
|
|
80
66
|
if (!periodStart)
|
|
@@ -84,5 +70,5 @@ export function Editor() {
|
|
|
84
70
|
const year = date.getFullYear();
|
|
85
71
|
return `${month} ${year} Breakdown`;
|
|
86
72
|
}, [periodStart]);
|
|
87
|
-
return (_jsxs("div", { className: "ph-default-styles flex flex-col h-full w-full bg-gray-50 dark:bg-gray-900", children: [_jsx("div", { className: "bg-white dark:bg-gray-800
|
|
73
|
+
return (_jsxs("div", { className: "ph-default-styles flex flex-col h-full w-full bg-gray-50 dark:bg-gray-900", children: [_jsx("div", { className: "flex-1 overflow-auto px-8 py-6", children: _jsxs("div", { className: "max-w-7xl mx-auto space-y-8", children: [_jsx("section", { className: "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700", children: _jsx("div", { className: "px-6 py-6", children: _jsxs("div", { className: "relative", children: [_jsxs("div", { className: "text-center", children: [_jsx("h1", { className: "text-3xl font-bold text-gray-900 dark:text-white mb-4", children: "Expense Report" }), _jsx("div", { className: "flex items-center justify-center gap-4 text-sm text-gray-600 dark:text-gray-400", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium", children: "Period:" }), _jsx(DatePicker, { name: "periodStart", value: periodStart, onChange: handlePeriodStartChange }), _jsx("span", { children: "to" }), _jsx(DatePicker, { name: "periodEnd", value: periodEnd, onChange: handlePeriodEndChange })] }) })] }), _jsxs(Button, { variant: "ghost", onClick: handleExportPDF, className: "absolute top-0 right-0 flex items-center gap-2", children: [_jsx(Icon, { name: "ExportPdf", size: 18 }), "Export to PDF"] })] }) }) }), _jsxs("section", { className: "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700", children: [_jsx("div", { className: "px-6 py-4 border-b border-gray-200 dark:border-gray-700", children: _jsx("h2", { className: "text-xl font-semibold text-gray-900 dark:text-white", children: "Wallets" }) }), _jsx("div", { className: "p-6", children: _jsx(WalletsTable, { wallets: wallets, groups: groups, onAddBillingStatement: handleAddBillingStatement, dispatch: dispatch }) })] }), wallets.length > 0 && (_jsxs("section", { className: "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700", children: [_jsx("div", { className: "px-6 py-4 border-b border-gray-200 dark:border-gray-700", children: _jsx("h2", { className: "text-xl font-semibold text-gray-900 dark:text-white", children: breakdownTitle }) }), _jsx("div", { className: "p-6", children: _jsx(AggregatedExpensesTable, { wallets: wallets, groups: groups, periodStart: periodStart, periodEnd: periodEnd, dispatch: dispatch }) })] }))] }) }), isModalOpen && selectedWallet && (_jsx(AddBillingStatementModal, { isOpen: isModalOpen, onClose: handleCloseModal, walletAddress: selectedWallet, dispatch: dispatch, groups: groups }))] }));
|
|
88
74
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSyncWallet.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/hooks/useSyncWallet.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sDAAsD,CAAC;AAkB1F,wBAAgB,aAAa;gCAIV,MAAM,uBACA,MAAM,EAAE,UACrB,aAAa,EAAE,YACb,GAAG;
|
|
1
|
+
{"version":3,"file":"useSyncWallet.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/hooks/useSyncWallet.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sDAAsD,CAAC;AAkB1F,wBAAgB,aAAa;gCAIV,MAAM,uBACA,MAAM,EAAE,UACrB,aAAa,EAAE,YACb,GAAG;EA0FhB"}
|
|
@@ -23,10 +23,9 @@ export function useSyncWallet() {
|
|
|
23
23
|
const group = groups.find((g) => g.label === expenseAccountTag.label);
|
|
24
24
|
return group ? group.id : null;
|
|
25
25
|
};
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
// Extract and add line items from all billing statements
|
|
26
|
+
// Aggregate line items by category
|
|
27
|
+
const categoryAggregation = new Map();
|
|
28
|
+
// Extract and aggregate line items from all billing statements
|
|
30
29
|
billingStatementIds.forEach((statementId) => {
|
|
31
30
|
const statement = billingStatements.get(statementId);
|
|
32
31
|
if (!statement?.state?.global?.lineItems)
|
|
@@ -34,22 +33,43 @@ export function useSyncWallet() {
|
|
|
34
33
|
const lineItems = statement.state.global.lineItems || [];
|
|
35
34
|
lineItems.forEach((billingLineItem) => {
|
|
36
35
|
const groupId = mapTagToGroup(billingLineItem);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
36
|
+
const categoryKey = groupId || "uncategorized";
|
|
37
|
+
const existing = categoryAggregation.get(categoryKey);
|
|
38
|
+
if (existing) {
|
|
39
|
+
// Aggregate values for the same category
|
|
40
|
+
existing.actuals += billingLineItem.totalPriceCash || 0;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Create new category entry
|
|
44
|
+
const group = groups.find((g) => g.id === groupId);
|
|
45
|
+
categoryAggregation.set(categoryKey, {
|
|
46
|
+
groupId: groupId,
|
|
47
|
+
groupLabel: group?.label || "Uncategorised",
|
|
48
|
+
budget: 0,
|
|
49
|
+
actuals: billingLineItem.totalPriceCash || 0,
|
|
50
|
+
forecast: 0,
|
|
51
|
+
payments: 0,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
51
54
|
});
|
|
52
55
|
});
|
|
56
|
+
// Now add aggregated line items to wallet
|
|
57
|
+
categoryAggregation.forEach((aggregatedItem) => {
|
|
58
|
+
const expenseLineItem = {
|
|
59
|
+
id: generateId(),
|
|
60
|
+
label: aggregatedItem.groupLabel,
|
|
61
|
+
group: aggregatedItem.groupId,
|
|
62
|
+
budget: aggregatedItem.budget,
|
|
63
|
+
actuals: aggregatedItem.actuals,
|
|
64
|
+
forecast: aggregatedItem.forecast,
|
|
65
|
+
payments: aggregatedItem.payments,
|
|
66
|
+
comments: null,
|
|
67
|
+
};
|
|
68
|
+
dispatch(actions.addLineItem({
|
|
69
|
+
wallet: walletAddress,
|
|
70
|
+
lineItem: expenseLineItem,
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
53
73
|
};
|
|
54
74
|
return { syncWallet };
|
|
55
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWalletSync.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/hooks/useWalletSync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sDAAsD,CAAC;AAEnF,UAAU,UAAU;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"useWalletSync.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/hooks/useWalletSync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sDAAsD,CAAC;AAEnF,UAAU,UAAU;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,UAAU,CAoG3D"}
|
|
@@ -20,47 +20,45 @@ export function useWalletSync(wallets) {
|
|
|
20
20
|
if (!wallet.billingStatements || wallet.billingStatements.length === 0) {
|
|
21
21
|
return; // No billing statements to sync
|
|
22
22
|
}
|
|
23
|
-
// Get current
|
|
24
|
-
const
|
|
25
|
-
let currentTotal = 0;
|
|
23
|
+
// Get current aggregated totals by category from wallet line items
|
|
24
|
+
const currentCategoryTotals = new Map();
|
|
26
25
|
wallet.lineItems?.forEach((item) => {
|
|
27
|
-
|
|
26
|
+
if (item?.group) {
|
|
27
|
+
const currentTotal = currentCategoryTotals.get(item.group) || 0;
|
|
28
|
+
currentCategoryTotals.set(item.group, currentTotal + (item.actuals || 0));
|
|
29
|
+
}
|
|
28
30
|
});
|
|
29
|
-
// Calculate
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const expectedTags = new Set();
|
|
31
|
+
// Calculate expected aggregated totals by category from billing statements
|
|
32
|
+
const expectedCategoryTotals = new Map();
|
|
33
|
+
const expectedCategoryLabels = new Set();
|
|
33
34
|
wallet.billingStatements.forEach((statementId) => {
|
|
34
35
|
if (!statementId)
|
|
35
36
|
return;
|
|
36
37
|
const statement = billingStatements.get(statementId);
|
|
37
38
|
if (statement?.state?.global?.lineItems) {
|
|
38
|
-
expectedLineItemsCount += statement.state.global.lineItems.length;
|
|
39
39
|
statement.state.global.lineItems.forEach((item) => {
|
|
40
|
-
|
|
41
|
-
// Collect tags from billing statement
|
|
40
|
+
// Find expense-account tag
|
|
42
41
|
const expenseAccountTag = item.lineItemTag?.find((tag) => tag.dimension === "expense-account");
|
|
43
42
|
if (expenseAccountTag?.label) {
|
|
44
|
-
|
|
43
|
+
expectedCategoryLabels.add(expenseAccountTag.label);
|
|
44
|
+
const currentTotal = expectedCategoryTotals.get(expenseAccountTag.label) || 0;
|
|
45
|
+
expectedCategoryTotals.set(expenseAccountTag.label, currentTotal + (item.totalPriceCash || 0));
|
|
45
46
|
}
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
|
-
// Check
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Check if tags have changed (size mismatch suggests tag changes)
|
|
62
|
-
const hasTagChanges = expectedTags.size > 0 && currentTags.size !== expectedTags.size;
|
|
63
|
-
if (needsQuantitySync || hasTagChanges) {
|
|
50
|
+
// Check if categories have changed
|
|
51
|
+
const currentCategories = new Set(currentCategoryTotals.keys());
|
|
52
|
+
const hasTagChanges = currentCategories.size !== expectedCategoryLabels.size;
|
|
53
|
+
// Check if totals per category have changed
|
|
54
|
+
let hasTotalMismatch = false;
|
|
55
|
+
// We need to check if the aggregated totals match
|
|
56
|
+
// Since wallet stores group IDs but billing statements have labels,
|
|
57
|
+
// we need to sum up all line items regardless of category structure
|
|
58
|
+
const currentTotalActuals = Array.from(currentCategoryTotals.values()).reduce((sum, total) => sum + total, 0);
|
|
59
|
+
const expectedTotalActuals = Array.from(expectedCategoryTotals.values()).reduce((sum, total) => sum + total, 0);
|
|
60
|
+
hasTotalMismatch = Math.abs(currentTotalActuals - expectedTotalActuals) > 0.01;
|
|
61
|
+
if (hasTagChanges || hasTotalMismatch) {
|
|
64
62
|
if (wallet.wallet) {
|
|
65
63
|
outdatedWallets.push(wallet.wallet);
|
|
66
64
|
if (hasTagChanges) {
|
package/dist/style.css
CHANGED
|
@@ -310,6 +310,9 @@
|
|
|
310
310
|
.inset-0 {
|
|
311
311
|
inset: calc(var(--spacing) * 0);
|
|
312
312
|
}
|
|
313
|
+
.top-0 {
|
|
314
|
+
top: calc(var(--spacing) * 0);
|
|
315
|
+
}
|
|
313
316
|
.top-1\/2 {
|
|
314
317
|
top: calc(1/2 * 100%);
|
|
315
318
|
}
|
|
@@ -421,6 +424,9 @@
|
|
|
421
424
|
.inline {
|
|
422
425
|
display: inline;
|
|
423
426
|
}
|
|
427
|
+
.inline-block {
|
|
428
|
+
display: inline-block;
|
|
429
|
+
}
|
|
424
430
|
.inline-flex {
|
|
425
431
|
display: inline-flex;
|
|
426
432
|
}
|
|
@@ -473,6 +479,9 @@
|
|
|
473
479
|
.h-full {
|
|
474
480
|
height: 100%;
|
|
475
481
|
}
|
|
482
|
+
.max-h-20 {
|
|
483
|
+
max-height: calc(var(--spacing) * 20);
|
|
484
|
+
}
|
|
476
485
|
.max-h-32 {
|
|
477
486
|
max-height: calc(var(--spacing) * 32);
|
|
478
487
|
}
|
|
@@ -533,6 +542,9 @@
|
|
|
533
542
|
.w-72 {
|
|
534
543
|
width: calc(var(--spacing) * 72);
|
|
535
544
|
}
|
|
545
|
+
.w-96 {
|
|
546
|
+
width: calc(var(--spacing) * 96);
|
|
547
|
+
}
|
|
536
548
|
.w-\[200px\] {
|
|
537
549
|
width: 200px;
|
|
538
550
|
}
|
|
@@ -569,6 +581,9 @@
|
|
|
569
581
|
.min-w-0 {
|
|
570
582
|
min-width: calc(var(--spacing) * 0);
|
|
571
583
|
}
|
|
584
|
+
.min-w-\[4rem\] {
|
|
585
|
+
min-width: 4rem;
|
|
586
|
+
}
|
|
572
587
|
.min-w-\[142px\] {
|
|
573
588
|
min-width: 142px;
|
|
574
589
|
}
|
|
@@ -885,9 +900,6 @@
|
|
|
885
900
|
.bg-gray-300 {
|
|
886
901
|
background-color: var(--color-gray-300);
|
|
887
902
|
}
|
|
888
|
-
.bg-gray-400 {
|
|
889
|
-
background-color: var(--color-gray-400);
|
|
890
|
-
}
|
|
891
903
|
.bg-gray-500 {
|
|
892
904
|
background-color: var(--color-gray-500);
|
|
893
905
|
}
|
|
@@ -1014,6 +1026,9 @@
|
|
|
1014
1026
|
.text-right {
|
|
1015
1027
|
text-align: right;
|
|
1016
1028
|
}
|
|
1029
|
+
.align-top {
|
|
1030
|
+
vertical-align: top;
|
|
1031
|
+
}
|
|
1017
1032
|
.font-mono {
|
|
1018
1033
|
font-family: var(--font-mono);
|
|
1019
1034
|
}
|
|
@@ -1061,15 +1076,15 @@
|
|
|
1061
1076
|
--tw-tracking: var(--tracking-wider);
|
|
1062
1077
|
letter-spacing: var(--tracking-wider);
|
|
1063
1078
|
}
|
|
1079
|
+
.break-words {
|
|
1080
|
+
overflow-wrap: break-word;
|
|
1081
|
+
}
|
|
1064
1082
|
.break-all {
|
|
1065
1083
|
word-break: break-all;
|
|
1066
1084
|
}
|
|
1067
1085
|
.whitespace-nowrap {
|
|
1068
1086
|
white-space: nowrap;
|
|
1069
1087
|
}
|
|
1070
|
-
.whitespace-pre-wrap {
|
|
1071
|
-
white-space: pre-wrap;
|
|
1072
|
-
}
|
|
1073
1088
|
.text-amber-600 {
|
|
1074
1089
|
color: var(--color-amber-600);
|
|
1075
1090
|
}
|
|
@@ -1139,9 +1154,6 @@
|
|
|
1139
1154
|
.uppercase {
|
|
1140
1155
|
text-transform: uppercase;
|
|
1141
1156
|
}
|
|
1142
|
-
.italic {
|
|
1143
|
-
font-style: italic;
|
|
1144
|
-
}
|
|
1145
1157
|
.ordinal {
|
|
1146
1158
|
--tw-ordinal: ordinal;
|
|
1147
1159
|
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
|
@@ -1154,9 +1166,6 @@
|
|
|
1154
1166
|
color: var(--color-gray-500);
|
|
1155
1167
|
}
|
|
1156
1168
|
}
|
|
1157
|
-
.opacity-0 {
|
|
1158
|
-
opacity: 0%;
|
|
1159
|
-
}
|
|
1160
1169
|
.opacity-50 {
|
|
1161
1170
|
opacity: 50%;
|
|
1162
1171
|
}
|
|
@@ -1238,10 +1247,10 @@
|
|
|
1238
1247
|
--tw-outline-style: none;
|
|
1239
1248
|
outline-style: none;
|
|
1240
1249
|
}
|
|
1241
|
-
.group-hover\:
|
|
1250
|
+
.group-hover\:bg-blue-50 {
|
|
1242
1251
|
&:is(:where(.group):hover *) {
|
|
1243
1252
|
@media (hover: hover) {
|
|
1244
|
-
|
|
1253
|
+
background-color: var(--color-blue-50);
|
|
1245
1254
|
}
|
|
1246
1255
|
}
|
|
1247
1256
|
}
|
|
@@ -1325,13 +1334,6 @@
|
|
|
1325
1334
|
}
|
|
1326
1335
|
}
|
|
1327
1336
|
}
|
|
1328
|
-
.hover\:bg-gray-500 {
|
|
1329
|
-
&:hover {
|
|
1330
|
-
@media (hover: hover) {
|
|
1331
|
-
background-color: var(--color-gray-500);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
1337
|
.hover\:bg-gray-600 {
|
|
1336
1338
|
&:hover {
|
|
1337
1339
|
@media (hover: hover) {
|
|
@@ -1710,6 +1712,18 @@
|
|
|
1710
1712
|
}
|
|
1711
1713
|
}
|
|
1712
1714
|
}
|
|
1715
|
+
.dark\:group-hover\:bg-blue-900\/20 {
|
|
1716
|
+
&:where(.dark, .dark *) {
|
|
1717
|
+
&:is(:where(.group):hover *) {
|
|
1718
|
+
@media (hover: hover) {
|
|
1719
|
+
background-color: color-mix(in srgb, oklch(37.9% 0.146 265.522) 20%, transparent);
|
|
1720
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
1721
|
+
background-color: color-mix(in oklab, var(--color-blue-900) 20%, transparent);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1713
1727
|
.dark\:hover\:border-gray-600 {
|
|
1714
1728
|
&:where(.dark, .dark *) {
|
|
1715
1729
|
&:hover {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/contributor-billing",
|
|
3
3
|
"description": "Document models that help contributors of open organisations get paid anonymously for their work on a monthly basis.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|