@powerhousedao/contributor-billing 0.1.3 → 0.1.4
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/expense-report/gen/actions.d.ts +4 -0
- package/dist/document-models/expense-report/gen/actions.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/actions.js +1 -0
- package/dist/document-models/expense-report/gen/creators.d.ts +2 -0
- package/dist/document-models/expense-report/gen/creators.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/creators.js +1 -0
- package/dist/document-models/expense-report/gen/document-model.d.ts +3 -0
- package/dist/document-models/expense-report/gen/document-model.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/document-model.js +202 -0
- package/dist/document-models/expense-report/gen/expense-report/actions.d.ts +8 -0
- package/dist/document-models/expense-report/gen/expense-report/actions.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/expense-report/actions.js +1 -0
- package/dist/document-models/expense-report/gen/expense-report/creators.d.ts +4 -0
- package/dist/document-models/expense-report/gen/expense-report/creators.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/expense-report/creators.js +3 -0
- package/dist/document-models/expense-report/gen/expense-report/error.d.ts +2 -0
- package/dist/document-models/expense-report/gen/expense-report/error.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/expense-report/error.js +1 -0
- package/dist/document-models/expense-report/gen/expense-report/object.d.ts +7 -0
- package/dist/document-models/expense-report/gen/expense-report/object.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/expense-report/object.js +7 -0
- package/dist/document-models/expense-report/gen/expense-report/operations.d.ts +7 -0
- package/dist/document-models/expense-report/gen/expense-report/operations.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/expense-report/operations.js +1 -0
- package/dist/document-models/expense-report/gen/index.d.ts +8 -0
- package/dist/document-models/expense-report/gen/index.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/index.js +6 -0
- package/dist/document-models/expense-report/gen/object.d.ts +15 -0
- package/dist/document-models/expense-report/gen/object.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/object.js +25 -0
- package/dist/document-models/expense-report/gen/ph-factories.d.ts +27 -0
- package/dist/document-models/expense-report/gen/ph-factories.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/ph-factories.js +189 -0
- package/dist/document-models/expense-report/gen/reducer.d.ts +5 -0
- package/dist/document-models/expense-report/gen/reducer.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/reducer.js +76 -0
- package/dist/document-models/expense-report/gen/schema/index.d.ts +3 -0
- package/dist/document-models/expense-report/gen/schema/index.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/schema/index.js +2 -0
- package/dist/document-models/expense-report/gen/schema/types.d.ts +254 -0
- package/dist/document-models/expense-report/gen/schema/types.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/schema/types.js +1 -0
- package/dist/document-models/expense-report/gen/schema/zod.d.ts +32 -0
- package/dist/document-models/expense-report/gen/schema/zod.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/schema/zod.js +216 -0
- package/dist/document-models/expense-report/gen/types.d.ts +10 -0
- package/dist/document-models/expense-report/gen/types.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/types.js +1 -0
- package/dist/document-models/expense-report/gen/utils.d.ts +22 -0
- package/dist/document-models/expense-report/gen/utils.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/utils.js +181 -0
- package/dist/document-models/expense-report/gen/wallet/actions.d.ts +64 -0
- package/dist/document-models/expense-report/gen/wallet/actions.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/wallet/actions.js +1 -0
- package/dist/document-models/expense-report/gen/wallet/creators.d.ts +18 -0
- package/dist/document-models/expense-report/gen/wallet/creators.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/wallet/creators.js +17 -0
- package/dist/document-models/expense-report/gen/wallet/error.d.ts +2 -0
- package/dist/document-models/expense-report/gen/wallet/error.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/wallet/error.js +1 -0
- package/dist/document-models/expense-report/gen/wallet/object.d.ts +21 -0
- package/dist/document-models/expense-report/gen/wallet/object.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/wallet/object.js +49 -0
- package/dist/document-models/expense-report/gen/wallet/operations.d.ts +21 -0
- package/dist/document-models/expense-report/gen/wallet/operations.d.ts.map +1 -0
- package/dist/document-models/expense-report/gen/wallet/operations.js +1 -0
- package/dist/document-models/expense-report/index.d.ts +39 -0
- package/dist/document-models/expense-report/index.d.ts.map +1 -0
- package/dist/document-models/expense-report/index.js +21 -0
- package/dist/document-models/expense-report/src/reducers/wallet.d.ts +3 -0
- package/dist/document-models/expense-report/src/reducers/wallet.d.ts.map +1 -0
- package/dist/document-models/expense-report/src/reducers/wallet.js +180 -0
- package/dist/document-models/expense-report/src/tests/document-model.test.d.ts +6 -0
- package/dist/document-models/expense-report/src/tests/document-model.test.d.ts.map +1 -0
- package/dist/document-models/expense-report/src/tests/document-model.test.js +18 -0
- package/dist/document-models/expense-report/src/tests/expense-report.test.d.ts +6 -0
- package/dist/document-models/expense-report/src/tests/expense-report.test.d.ts.map +1 -0
- package/dist/document-models/expense-report/src/tests/expense-report.test.js +24 -0
- package/dist/document-models/expense-report/src/tests/wallet.test.d.ts +6 -0
- package/dist/document-models/expense-report/src/tests/wallet.test.d.ts.map +1 -0
- package/dist/document-models/expense-report/src/tests/wallet.test.js +24 -0
- package/dist/document-models/expense-report/src/utils.d.ts +2 -0
- package/dist/document-models/expense-report/src/utils.d.ts.map +1 -0
- package/dist/document-models/expense-report/src/utils.js +1 -0
- package/dist/document-models/index.d.ts +1 -0
- package/dist/document-models/index.d.ts.map +1 -1
- package/dist/document-models/index.js +1 -0
- package/dist/document-models/integrations/gen/ph-factories.d.ts.map +1 -1
- package/dist/document-models/integrations/gen/ph-factories.js +2 -14
- package/dist/document-models/integrations/gen/utils.d.ts.map +1 -1
- package/dist/document-models/integrations/gen/utils.js +2 -14
- package/dist/document-models/invoice/gen/ph-factories.d.ts.map +1 -1
- package/dist/document-models/invoice/gen/ph-factories.js +2 -5
- package/dist/document-models/invoice/gen/schema/types.d.ts +1 -1
- package/dist/document-models/invoice/gen/schema/types.d.ts.map +1 -1
- package/dist/document-models/invoice/gen/utils.d.ts.map +1 -1
- package/dist/document-models/invoice/gen/utils.js +1 -4
- package/dist/editors/billing-statement/components/lineItemsTable.d.ts.map +1 -1
- package/dist/editors/billing-statement/components/lineItemsTable.js +71 -13
- package/dist/editors/billing-statement/editor.js +1 -1
- package/dist/editors/contributor-billing/components/DriveExplorer.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/DriveExplorer.js +8 -4
- package/dist/editors/contributor-billing/components/InvoiceTable/HeaderControls.d.ts +4 -1
- package/dist/editors/contributor-billing/components/InvoiceTable/HeaderControls.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/InvoiceTable/HeaderControls.js +2 -2
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTable.d.ts.map +1 -1
- package/dist/editors/contributor-billing/components/InvoiceTable/InvoiceTable.js +24 -1
- package/dist/editors/expense-report/components/AddBillingStatementModal.d.ts +11 -0
- package/dist/editors/expense-report/components/AddBillingStatementModal.d.ts.map +1 -0
- package/dist/editors/expense-report/components/AddBillingStatementModal.js +170 -0
- package/dist/editors/expense-report/components/AggregatedExpensesTable.d.ts +11 -0
- package/dist/editors/expense-report/components/AggregatedExpensesTable.d.ts.map +1 -0
- package/dist/editors/expense-report/components/AggregatedExpensesTable.js +232 -0
- package/dist/editors/expense-report/components/WalletsTable.d.ts +10 -0
- package/dist/editors/expense-report/components/WalletsTable.d.ts.map +1 -0
- package/dist/editors/expense-report/components/WalletsTable.js +164 -0
- package/dist/editors/expense-report/editor.d.ts +2 -0
- package/dist/editors/expense-report/editor.d.ts.map +1 -0
- package/dist/editors/expense-report/editor.js +88 -0
- package/dist/editors/expense-report/hooks/useSyncWallet.d.ts +5 -0
- package/dist/editors/expense-report/hooks/useSyncWallet.d.ts.map +1 -0
- package/dist/editors/expense-report/hooks/useSyncWallet.js +55 -0
- package/dist/editors/expense-report/hooks/useWalletSync.d.ts +9 -0
- package/dist/editors/expense-report/hooks/useWalletSync.d.ts.map +1 -0
- package/dist/editors/expense-report/hooks/useWalletSync.js +79 -0
- package/dist/editors/expense-report/index.d.ts +3 -0
- package/dist/editors/expense-report/index.d.ts.map +1 -0
- package/dist/editors/expense-report/index.js +11 -0
- package/dist/editors/hooks/useExpenseReportDocument.d.ts +4 -0
- package/dist/editors/hooks/useExpenseReportDocument.d.ts.map +1 -0
- package/dist/editors/hooks/useExpenseReportDocument.js +8 -0
- package/dist/editors/index.d.ts +1 -0
- package/dist/editors/index.d.ts.map +1 -1
- package/dist/editors/index.js +1 -0
- package/dist/editors/invoice/components/statusModalComponents.d.ts.map +1 -1
- package/dist/editors/invoice/components/statusModalComponents.js +4 -4
- package/dist/editors/invoice/editor.js +1 -1
- package/dist/editors/invoice/ingestPDF.d.ts.map +1 -1
- package/dist/editors/invoice/ingestPDF.js +3 -3
- package/dist/editors/invoice/invoiceToGnosis.js +1 -1
- package/dist/editors/invoice/requestFinance.js +1 -1
- package/dist/editors/invoice/uploadPdfChunked.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/powerhouse.manifest.json +13 -2
- package/dist/style.css +525 -37
- package/dist/subgraphs/expense-report/index.d.ts +11 -0
- package/dist/subgraphs/expense-report/index.d.ts.map +1 -0
- package/dist/subgraphs/expense-report/index.js +11 -0
- package/dist/subgraphs/expense-report/resolvers.d.ts +3 -0
- package/dist/subgraphs/expense-report/resolvers.d.ts.map +1 -0
- package/dist/subgraphs/expense-report/resolvers.js +252 -0
- package/dist/subgraphs/expense-report/schema.d.ts +3 -0
- package/dist/subgraphs/expense-report/schema.d.ts.map +1 -0
- package/dist/subgraphs/expense-report/schema.js +228 -0
- package/dist/subgraphs/index.d.ts +1 -0
- package/dist/subgraphs/index.d.ts.map +1 -1
- package/dist/subgraphs/index.js +1 -0
- package/package.json +13 -13
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { actions } from "../../../document-models/expense-report/index.js";
|
|
5
|
+
import { Textarea } from "@powerhousedao/document-engineering";
|
|
6
|
+
export function AggregatedExpensesTable({ wallets, groups, periodStart, periodEnd, dispatch, }) {
|
|
7
|
+
// State for active tab (selected wallet)
|
|
8
|
+
const [activeWalletIndex, setActiveWalletIndex] = useState(0);
|
|
9
|
+
// State for editing comments
|
|
10
|
+
const [editingGroupId, setEditingGroupId] = useState(null);
|
|
11
|
+
const [editingComment, setEditingComment] = useState("");
|
|
12
|
+
// Format period for title
|
|
13
|
+
const periodTitle = useMemo(() => {
|
|
14
|
+
if (!periodStart)
|
|
15
|
+
return "Breakdown";
|
|
16
|
+
const date = new Date(periodStart);
|
|
17
|
+
const month = date.toLocaleDateString("en-US", { month: "short" });
|
|
18
|
+
const year = date.getFullYear();
|
|
19
|
+
return `${month} ${year} Breakdown`;
|
|
20
|
+
}, [periodStart]);
|
|
21
|
+
// Create a map of groups with their parent info
|
|
22
|
+
const groupsMap = useMemo(() => {
|
|
23
|
+
const map = new Map();
|
|
24
|
+
groups.forEach((group) => {
|
|
25
|
+
map.set(group.id, { group });
|
|
26
|
+
});
|
|
27
|
+
// Add parent references
|
|
28
|
+
groups.forEach((group) => {
|
|
29
|
+
if (group.parentId) {
|
|
30
|
+
const entry = map.get(group.id);
|
|
31
|
+
const parentEntry = map.get(group.parentId);
|
|
32
|
+
if (entry && parentEntry) {
|
|
33
|
+
entry.parent = parentEntry.group;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return map;
|
|
38
|
+
}, [groups]);
|
|
39
|
+
// Get line items for the active wallet with group information
|
|
40
|
+
const walletLineItems = useMemo(() => {
|
|
41
|
+
if (!wallets[activeWalletIndex])
|
|
42
|
+
return [];
|
|
43
|
+
const wallet = wallets[activeWalletIndex];
|
|
44
|
+
const lineItems = wallet.lineItems || [];
|
|
45
|
+
return lineItems
|
|
46
|
+
.filter((item) => item !== null && item !== undefined)
|
|
47
|
+
.map((item) => {
|
|
48
|
+
const groupInfo = item.group ? groupsMap.get(item.group) : undefined;
|
|
49
|
+
return {
|
|
50
|
+
...item,
|
|
51
|
+
groupLabel: groupInfo?.group.label || undefined,
|
|
52
|
+
parentGroupId: groupInfo?.parent?.id || null,
|
|
53
|
+
parentGroupLabel: groupInfo?.parent?.label || undefined,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}, [wallets, activeWalletIndex, groupsMap]);
|
|
57
|
+
// Group line items by category and aggregate by parent category
|
|
58
|
+
const groupedAndAggregatedItems = useMemo(() => {
|
|
59
|
+
// First, aggregate line items by their category (group)
|
|
60
|
+
const categoryAggregation = new Map();
|
|
61
|
+
walletLineItems.forEach((item) => {
|
|
62
|
+
if (!item)
|
|
63
|
+
return;
|
|
64
|
+
const categoryKey = item.group || "uncategorized";
|
|
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";
|
|
93
|
+
const items = grouped.get(parentKey) || [];
|
|
94
|
+
items.push(aggItem);
|
|
95
|
+
grouped.set(parentKey, items);
|
|
96
|
+
});
|
|
97
|
+
return grouped;
|
|
98
|
+
}, [walletLineItems]);
|
|
99
|
+
// Calculate subtotals for each parent group
|
|
100
|
+
const calculateSubtotal = (items) => {
|
|
101
|
+
return items.reduce((acc, item) => ({
|
|
102
|
+
budget: acc.budget + item.budget,
|
|
103
|
+
forecast: acc.forecast + item.forecast,
|
|
104
|
+
actuals: acc.actuals + item.actuals,
|
|
105
|
+
difference: acc.difference + (item.actuals - item.budget),
|
|
106
|
+
payments: acc.payments + item.payments,
|
|
107
|
+
}), { budget: 0, forecast: 0, actuals: 0, difference: 0, payments: 0 });
|
|
108
|
+
};
|
|
109
|
+
// Calculate grand totals
|
|
110
|
+
const grandTotals = useMemo(() => {
|
|
111
|
+
return walletLineItems.reduce((acc, item) => ({
|
|
112
|
+
budget: acc.budget + (item?.budget || 0),
|
|
113
|
+
forecast: acc.forecast + (item?.forecast || 0),
|
|
114
|
+
actuals: acc.actuals + (item?.actuals || 0),
|
|
115
|
+
difference: acc.difference + ((item?.actuals || 0) - (item?.budget || 0)),
|
|
116
|
+
payments: acc.payments + (item?.payments || 0),
|
|
117
|
+
}), { budget: 0, forecast: 0, actuals: 0, difference: 0, payments: 0 });
|
|
118
|
+
}, [walletLineItems]);
|
|
119
|
+
const formatNumber = (value) => {
|
|
120
|
+
return new Intl.NumberFormat("en-US", {
|
|
121
|
+
minimumFractionDigits: 2,
|
|
122
|
+
maximumFractionDigits: 2,
|
|
123
|
+
}).format(value);
|
|
124
|
+
};
|
|
125
|
+
const formatWalletAddress = (address) => {
|
|
126
|
+
if (!address || address.length < 13)
|
|
127
|
+
return address;
|
|
128
|
+
return `${address.substring(0, 6)}...${address.substring(address.length - 6)}`;
|
|
129
|
+
};
|
|
130
|
+
// Handle starting comment edit
|
|
131
|
+
const handleStartEdit = (groupId, currentComment) => {
|
|
132
|
+
setEditingGroupId(groupId);
|
|
133
|
+
setEditingComment(currentComment);
|
|
134
|
+
};
|
|
135
|
+
// Handle saving comment
|
|
136
|
+
const handleSaveComment = (groupId) => {
|
|
137
|
+
const wallet = wallets[activeWalletIndex];
|
|
138
|
+
if (!wallet || !wallet.wallet)
|
|
139
|
+
return;
|
|
140
|
+
// Find all line items with this group ID
|
|
141
|
+
const lineItemsToUpdate = wallet.lineItems?.filter((item) => item?.group === groupId) || [];
|
|
142
|
+
// Create all update actions
|
|
143
|
+
const updateActions = lineItemsToUpdate
|
|
144
|
+
.filter((item) => item?.id)
|
|
145
|
+
.map((item) => actions.updateLineItem({
|
|
146
|
+
wallet: wallet.wallet,
|
|
147
|
+
lineItemId: item.id,
|
|
148
|
+
comments: editingComment,
|
|
149
|
+
}));
|
|
150
|
+
// Dispatch all actions at once
|
|
151
|
+
if (updateActions.length > 0) {
|
|
152
|
+
dispatch(updateActions);
|
|
153
|
+
}
|
|
154
|
+
// Reset editing state
|
|
155
|
+
setEditingGroupId(null);
|
|
156
|
+
setEditingComment("");
|
|
157
|
+
};
|
|
158
|
+
// Handle canceling comment edit
|
|
159
|
+
const handleCancelEdit = () => {
|
|
160
|
+
setEditingGroupId(null);
|
|
161
|
+
setEditingComment("");
|
|
162
|
+
};
|
|
163
|
+
// Sort parent groups: Headcount first, then Non-Headcount, then others, then uncategorized
|
|
164
|
+
const sortedParentKeys = useMemo(() => {
|
|
165
|
+
const keys = Array.from(groupedAndAggregatedItems.keys());
|
|
166
|
+
// Find Headcount and Non-Headcount group IDs
|
|
167
|
+
const headcountGroup = groups.find(g => g.label === "Headcount Expenses");
|
|
168
|
+
const nonHeadcountGroup = groups.find(g => g.label === "Non-Headcount Expenses");
|
|
169
|
+
return keys.sort((a, b) => {
|
|
170
|
+
// Uncategorized always goes last
|
|
171
|
+
if (a === "uncategorized")
|
|
172
|
+
return 1;
|
|
173
|
+
if (b === "uncategorized")
|
|
174
|
+
return -1;
|
|
175
|
+
// Headcount Expenses always first
|
|
176
|
+
if (a === headcountGroup?.id)
|
|
177
|
+
return -1;
|
|
178
|
+
if (b === headcountGroup?.id)
|
|
179
|
+
return 1;
|
|
180
|
+
// Non-Headcount Expenses always second
|
|
181
|
+
if (a === nonHeadcountGroup?.id)
|
|
182
|
+
return -1;
|
|
183
|
+
if (b === nonHeadcountGroup?.id)
|
|
184
|
+
return 1;
|
|
185
|
+
// For other groups, maintain their original order
|
|
186
|
+
return 0;
|
|
187
|
+
});
|
|
188
|
+
}, [groupedAndAggregatedItems, groups]);
|
|
189
|
+
if (wallets.length === 0) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "border-b border-gray-200 dark:border-gray-700", children: _jsx("nav", { className: "-mb-px flex space-x-8", "aria-label": "Tabs", children: wallets.map((wallet, index) => {
|
|
193
|
+
const isActive = index === activeWalletIndex;
|
|
194
|
+
return (_jsx("button", { onClick: () => setActiveWalletIndex(index), className: `
|
|
195
|
+
whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors
|
|
196
|
+
${isActive
|
|
197
|
+
? "border-green-500 text-green-600 dark:text-green-400"
|
|
198
|
+
: "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
|
+
`, 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-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Comments" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Payments" })] }) }), _jsxs("tbody", { className: "divide-y divide-gray-200 dark:divide-gray-700", children: [sortedParentKeys.map((parentKey) => {
|
|
201
|
+
const items = groupedAndAggregatedItems.get(parentKey) || [];
|
|
202
|
+
if (items.length === 0)
|
|
203
|
+
return null;
|
|
204
|
+
const subtotals = calculateSubtotal(items);
|
|
205
|
+
const parentLabel = parentKey === "uncategorized"
|
|
206
|
+
? "Uncategorised"
|
|
207
|
+
: items[0]?.parentGroupLabel || "Other";
|
|
208
|
+
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
|
+
if (!item)
|
|
210
|
+
return null;
|
|
211
|
+
const difference = item.actuals - item.budget;
|
|
212
|
+
const isEditing = editingGroupId === item.groupId;
|
|
213
|
+
return (_jsxs("tr", { className: "bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors", 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: formatNumber(item.budget) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(item.forecast) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(item.actuals) }), _jsx("td", { className: `px-6 py-3 whitespace-nowrap text-right text-sm font-medium ${difference > 0
|
|
214
|
+
? "text-red-600 dark:text-red-400"
|
|
215
|
+
: difference < 0
|
|
216
|
+
? "text-green-600 dark:text-green-400"
|
|
217
|
+
: "text-gray-900 dark:text-white"}`, children: formatNumber(difference) }), _jsx("td", { className: "px-6 py-3 text-sm", children: isEditing ? (_jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "flex-1", children: _jsx(Textarea, { value: editingComment, onChange: (e) => setEditingComment(e.target.value), placeholder: "Add comment...", autoExpand: true, multiline: true, onKeyDown: (e) => {
|
|
218
|
+
if (e.key === "Escape") {
|
|
219
|
+
handleCancelEdit();
|
|
220
|
+
}
|
|
221
|
+
} }) }), _jsxs("div", { className: "flex gap-1 mt-1", children: [_jsx("button", { onClick: () => handleSaveComment(item.groupId), className: "px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700", title: "Save", children: "\u2713" }), _jsx("button", { onClick: handleCancelEdit, className: "px-2 py-1 text-xs bg-gray-400 text-white rounded hover:bg-gray-500", title: "Cancel", children: "\u2715" })] })] })) : (_jsxs("div", { className: "flex items-center gap-2 group", children: [_jsx("span", { className: "text-gray-600 dark:text-gray-400 italic flex-1 whitespace-pre-wrap", children: item.comment || "No comments" }), _jsx("button", { onClick: () => handleStartEdit(item.groupId, item.comment), className: "opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded", title: "Edit comment", children: _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-4 w-4 text-gray-600 dark:text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" }) }) })] })) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(item.payments) })] }, item.groupId));
|
|
222
|
+
}), _jsxs("tr", { className: "bg-gray-50 dark:bg-gray-800/50 font-semibold", 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: formatNumber(subtotals.budget) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(subtotals.forecast) }), _jsx("td", { className: "px-6 py-3 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatNumber(subtotals.actuals) }), _jsx("td", { className: `px-6 py-3 whitespace-nowrap text-right text-sm font-bold ${subtotals.difference > 0
|
|
223
|
+
? "text-red-600 dark:text-red-400"
|
|
224
|
+
: subtotals.difference < 0
|
|
225
|
+
? "text-green-600 dark:text-green-400"
|
|
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
|
|
228
|
+
? "text-red-600 dark:text-red-400"
|
|
229
|
+
: grandTotals.difference < 0
|
|
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) })] })] })] }) })] }));
|
|
232
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Wallet, LineItemGroup } from "../../../document-models/expense-report/gen/types.js";
|
|
2
|
+
interface WalletsTableProps {
|
|
3
|
+
wallets: Wallet[];
|
|
4
|
+
groups: LineItemGroup[];
|
|
5
|
+
onAddBillingStatement: (walletAddress: string) => void;
|
|
6
|
+
dispatch: any;
|
|
7
|
+
}
|
|
8
|
+
export declare function WalletsTable({ wallets, groups, onAddBillingStatement, dispatch, }: WalletsTableProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=WalletsTable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WalletsTable.d.ts","sourceRoot":"","sources":["../../../../editors/expense-report/components/WalletsTable.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,sDAAsD,CAAC;AAKlG,UAAU,iBAAiB;IACzB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,qBAAqB,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,QAAQ,EAAE,GAAG,CAAC;CACf;AAED,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,MAAM,EACN,qBAAqB,EACrB,QAAQ,GACT,EAAE,iBAAiB,2CA+XnB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button, TextInput } from "@powerhousedao/document-engineering";
|
|
4
|
+
import { Plus, Trash2, Pencil, Check, X, Copy, CheckCheck, RefreshCw } from "lucide-react";
|
|
5
|
+
import { actions } from "../../../document-models/expense-report/index.js";
|
|
6
|
+
import { useWalletSync } from "../hooks/useWalletSync.js";
|
|
7
|
+
import { useSyncWallet } from "../hooks/useSyncWallet.js";
|
|
8
|
+
export function WalletsTable({ wallets, groups, onAddBillingStatement, dispatch, }) {
|
|
9
|
+
const [newWalletAddress, setNewWalletAddress] = useState("");
|
|
10
|
+
const [newWalletName, setNewWalletName] = useState("");
|
|
11
|
+
const [walletError, setWalletError] = useState("");
|
|
12
|
+
const [hoveredWallet, setHoveredWallet] = useState(null);
|
|
13
|
+
const [editingWallet, setEditingWallet] = useState(null);
|
|
14
|
+
const [editingName, setEditingName] = useState("");
|
|
15
|
+
const [copiedWallet, setCopiedWallet] = useState(null);
|
|
16
|
+
const [syncingWallet, setSyncingWallet] = useState(null);
|
|
17
|
+
// Check sync status
|
|
18
|
+
const { needsSync, outdatedWallets, tagChangedWallets } = useWalletSync(wallets);
|
|
19
|
+
const { syncWallet } = useSyncWallet();
|
|
20
|
+
const handleAddWallet = () => {
|
|
21
|
+
const trimmedAddress = newWalletAddress.trim();
|
|
22
|
+
if (trimmedAddress) {
|
|
23
|
+
// Check if wallet already exists
|
|
24
|
+
const walletExists = wallets.some(w => w.wallet === trimmedAddress);
|
|
25
|
+
if (walletExists) {
|
|
26
|
+
setWalletError("This wallet already exists");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
dispatch(actions.addWallet({
|
|
30
|
+
wallet: trimmedAddress,
|
|
31
|
+
name: newWalletName.trim() || undefined,
|
|
32
|
+
}));
|
|
33
|
+
setNewWalletAddress("");
|
|
34
|
+
setNewWalletName("");
|
|
35
|
+
setWalletError("");
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const handleStartEditName = (wallet) => {
|
|
39
|
+
setEditingWallet(wallet.wallet || null);
|
|
40
|
+
setEditingName(wallet.name || "");
|
|
41
|
+
};
|
|
42
|
+
const handleSaveEditName = (walletAddress) => {
|
|
43
|
+
const wallet = wallets.find(w => w.wallet === walletAddress);
|
|
44
|
+
const trimmedName = editingName.trim();
|
|
45
|
+
// Only update if the name has changed
|
|
46
|
+
if (trimmedName && wallet && trimmedName !== (wallet.name || "")) {
|
|
47
|
+
dispatch(actions.updateWallet({
|
|
48
|
+
address: walletAddress,
|
|
49
|
+
name: trimmedName,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
setEditingWallet(null);
|
|
53
|
+
setEditingName("");
|
|
54
|
+
};
|
|
55
|
+
const handleCancelEditName = () => {
|
|
56
|
+
setEditingWallet(null);
|
|
57
|
+
setEditingName("");
|
|
58
|
+
};
|
|
59
|
+
const handleCopyAddress = (address) => {
|
|
60
|
+
navigator.clipboard.writeText(address);
|
|
61
|
+
setCopiedWallet(address);
|
|
62
|
+
setTimeout(() => setCopiedWallet(null), 2000);
|
|
63
|
+
};
|
|
64
|
+
const formatAddress = (address) => {
|
|
65
|
+
if (!address || address.length < 11)
|
|
66
|
+
return address;
|
|
67
|
+
return `${address.substring(0, 6)}...${address.substring(address.length - 5)}`;
|
|
68
|
+
};
|
|
69
|
+
const handleSyncWallet = async (wallet) => {
|
|
70
|
+
if (!wallet.wallet || !wallet.billingStatements || wallet.billingStatements.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setSyncingWallet(wallet.wallet);
|
|
74
|
+
try {
|
|
75
|
+
// Remove all existing line items first
|
|
76
|
+
const lineItemsToRemove = [...(wallet.lineItems || [])];
|
|
77
|
+
lineItemsToRemove.forEach((item) => {
|
|
78
|
+
if (item?.id) {
|
|
79
|
+
dispatch(actions.removeLineItem({
|
|
80
|
+
wallet: wallet.wallet,
|
|
81
|
+
lineItemId: item.id,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Re-extract line items from billing statements
|
|
86
|
+
const billingStatementIds = wallet.billingStatements.filter((id) => id !== null && id !== undefined);
|
|
87
|
+
syncWallet(wallet.wallet, billingStatementIds, groups, dispatch);
|
|
88
|
+
// Small delay to show sync animation
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
setSyncingWallet(null);
|
|
91
|
+
}, 500);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error("Error syncing wallet:", error);
|
|
95
|
+
setSyncingWallet(null);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handleRemoveWallet = (walletAddress) => {
|
|
99
|
+
dispatch(actions.removeWallet({
|
|
100
|
+
wallet: walletAddress,
|
|
101
|
+
}));
|
|
102
|
+
};
|
|
103
|
+
// Calculate totals for a wallet
|
|
104
|
+
const calculateWalletTotals = (wallet) => {
|
|
105
|
+
const lineItems = wallet.lineItems || [];
|
|
106
|
+
return {
|
|
107
|
+
budget: lineItems.reduce((sum, item) => sum + (item?.budget || 0), 0),
|
|
108
|
+
forecast: lineItems.reduce((sum, item) => sum + (item?.forecast || 0), 0),
|
|
109
|
+
actuals: lineItems.reduce((sum, item) => sum + (item?.actuals || 0), 0),
|
|
110
|
+
difference: lineItems.reduce((sum, item) => {
|
|
111
|
+
const budget = item?.budget || 0;
|
|
112
|
+
const actuals = item?.actuals || 0;
|
|
113
|
+
return sum + (actuals - budget);
|
|
114
|
+
}, 0),
|
|
115
|
+
payments: lineItems.reduce((sum, item) => sum + (item?.payments || 0), 0),
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
const formatCurrency = (value) => {
|
|
119
|
+
return new Intl.NumberFormat("en-US", {
|
|
120
|
+
style: "currency",
|
|
121
|
+
currency: "USD",
|
|
122
|
+
minimumFractionDigits: 2,
|
|
123
|
+
}).format(value);
|
|
124
|
+
};
|
|
125
|
+
return (_jsxs("div", { className: "space-y-4", children: [wallets.length > 0 ? (_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: "Wallet" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Monthly 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-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Payments" }), _jsx("th", { className: "px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider", children: "Actions" })] }) }), _jsx("tbody", { className: "bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700", children: wallets.map((wallet) => {
|
|
126
|
+
const totals = calculateWalletTotals(wallet);
|
|
127
|
+
const isHovered = hoveredWallet === wallet.wallet;
|
|
128
|
+
return (_jsxs("tr", { onMouseEnter: () => setHoveredWallet(wallet.wallet || null), onMouseLeave: () => setHoveredWallet(null), className: "hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors", children: [_jsx("td", { className: "px-6 py-4 whitespace-nowrap", children: editingWallet === wallet.wallet ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TextInput, { value: editingName, onChange: (e) => setEditingName(e.target.value), placeholder: "Enter wallet name", className: "flex-1", onKeyDown: (e) => {
|
|
129
|
+
if (e.key === "Enter") {
|
|
130
|
+
handleSaveEditName(wallet.wallet || "");
|
|
131
|
+
}
|
|
132
|
+
else if (e.key === "Escape") {
|
|
133
|
+
handleCancelEditName();
|
|
134
|
+
}
|
|
135
|
+
}, autoFocus: true }), _jsx("button", { onClick: () => handleSaveEditName(wallet.wallet || ""), className: "inline-flex items-center justify-center w-7 h-7 text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 rounded-md transition-colors", title: "Save", children: _jsx(Check, { size: 14 }) }), _jsx("button", { onClick: handleCancelEditName, className: "inline-flex items-center justify-center w-7 h-7 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-900/20 rounded-md transition-colors", title: "Cancel", children: _jsx(X, { size: 14 }) })] })) : (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-white", children: wallet.name || "Unnamed Wallet" }), _jsx("button", { onClick: () => handleStartEditName(wallet), className: "inline-flex items-center justify-center w-6 h-6 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors", title: "Edit name", children: _jsx(Pencil, { size: 12 }) }), _jsxs("button", { onClick: () => handleCopyAddress(wallet.wallet || ""), className: "inline-flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 font-mono hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors", title: `Copy address: ${wallet.wallet}`, children: [formatAddress(wallet.wallet || ""), copiedWallet === wallet.wallet ? (_jsx(CheckCheck, { size: 12, className: "text-green-500" })) : (_jsx(Copy, { size: 12 }))] })] })) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatCurrency(totals.budget) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatCurrency(totals.forecast) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatCurrency(totals.actuals) }), _jsx("td", { className: `px-6 py-4 whitespace-nowrap text-right text-sm font-medium ${totals.difference > 0
|
|
136
|
+
? "text-red-600 dark:text-red-400"
|
|
137
|
+
: totals.difference < 0
|
|
138
|
+
? "text-green-600 dark:text-green-400"
|
|
139
|
+
: "text-gray-900 dark:text-white"}`, children: formatCurrency(totals.difference) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm text-gray-900 dark:text-white", children: formatCurrency(totals.payments) }), _jsx("td", { className: "px-6 py-4 whitespace-nowrap text-right text-sm", children: _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsxs("button", { onClick: () => onAddBillingStatement(wallet.wallet || ""), className: "inline-flex items-center gap-1 px-3 py-1 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded-md transition-colors", title: "Add billing statement", children: [_jsx(Plus, { size: 16 }), _jsx("span", { children: "Add Bills" })] }), wallet.billingStatements && wallet.billingStatements.length > 0 && (_jsxs("button", { onClick: () => handleSyncWallet(wallet), disabled: syncingWallet === wallet.wallet, className: `inline-flex items-center gap-1 px-3 py-1 text-sm font-medium rounded-md transition-colors ${tagChangedWallets.includes(wallet.wallet || "")
|
|
140
|
+
? "text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/30 animate-pulse"
|
|
141
|
+
: outdatedWallets.includes(wallet.wallet || "")
|
|
142
|
+
? "text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20 hover:bg-amber-100 dark:hover:bg-amber-900/30 animate-pulse"
|
|
143
|
+
: "text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700"} disabled:opacity-50 disabled:cursor-not-allowed`, title: tagChangedWallets.includes(wallet.wallet || "")
|
|
144
|
+
? "ALERT: Tags have changed in billing statements - sync required!"
|
|
145
|
+
: outdatedWallets.includes(wallet.wallet || "")
|
|
146
|
+
? "Sync needed - billing statements have been updated"
|
|
147
|
+
: "Sync with latest billing statements", children: [_jsx(RefreshCw, { size: 16, className: syncingWallet === wallet.wallet ? "animate-spin" : "" }), _jsx("span", { children: tagChangedWallets.includes(wallet.wallet || "")
|
|
148
|
+
? "Tags Changed!"
|
|
149
|
+
: outdatedWallets.includes(wallet.wallet || "")
|
|
150
|
+
? "Sync"
|
|
151
|
+
: "Synced" })] })), _jsx("button", { onClick: () => handleRemoveWallet(wallet.wallet || ""), className: "inline-flex items-center justify-center w-8 h-8 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-colors", title: "Remove wallet", children: _jsx(Trash2, { size: 16 }) })] }) })] }, wallet.wallet));
|
|
152
|
+
}) })] }) })) : (_jsx("div", { className: "text-center py-12 text-gray-500 dark:text-gray-400", children: _jsx("p", { className: "text-sm", children: "No wallets added yet. Add a wallet to get started." }) })), _jsxs("div", { className: "flex items-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700", children: [_jsxs("div", { className: "flex-1", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1", children: "Wallet Name" }), _jsx(TextInput, { value: newWalletName, onChange: (e) => setNewWalletName(e.target.value), placeholder: "Enter wallet name (optional)", onKeyDown: (e) => {
|
|
153
|
+
if (e.key === "Enter") {
|
|
154
|
+
handleAddWallet();
|
|
155
|
+
}
|
|
156
|
+
} })] }), _jsxs("div", { className: "flex-1 relative", children: [_jsx("label", { className: "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1", children: "Wallet Address" }), _jsx(TextInput, { value: newWalletAddress, onChange: (e) => {
|
|
157
|
+
setNewWalletAddress(e.target.value);
|
|
158
|
+
setWalletError(""); // Clear error when typing
|
|
159
|
+
}, placeholder: "Enter wallet address (e.g., 0x1234...)", onKeyDown: (e) => {
|
|
160
|
+
if (e.key === "Enter") {
|
|
161
|
+
handleAddWallet();
|
|
162
|
+
}
|
|
163
|
+
} }), walletError && (_jsx("div", { className: "absolute left-0 right-0 top-full mt-1 px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md shadow-lg z-10", children: _jsx("p", { className: "text-sm text-red-600 dark:text-red-400", children: walletError }) }))] }), _jsx(Button, { onClick: handleAddWallet, disabled: !newWalletAddress.trim(), children: "Add Wallet" })] })] }));
|
|
164
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../../../editors/expense-report/editor.tsx"],"names":[],"mappings":"AAUA,wBAAgB,MAAM,4CAiLrB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useEffect } from "react";
|
|
3
|
+
import { useSelectedExpenseReportDocument } from "../hooks/useExpenseReportDocument.js";
|
|
4
|
+
import { actions } from "../../document-models/expense-report/index.js";
|
|
5
|
+
import { DatePicker } from "@powerhousedao/document-engineering";
|
|
6
|
+
import { WalletsTable } from "./components/WalletsTable.js";
|
|
7
|
+
import { AggregatedExpensesTable } from "./components/AggregatedExpensesTable.js";
|
|
8
|
+
import { AddBillingStatementModal } from "./components/AddBillingStatementModal.js";
|
|
9
|
+
import { useWalletSync } from "./hooks/useWalletSync.js";
|
|
10
|
+
import { useSyncWallet } from "./hooks/useSyncWallet.js";
|
|
11
|
+
export function Editor() {
|
|
12
|
+
const [document, dispatch] = useSelectedExpenseReportDocument();
|
|
13
|
+
const [selectedWallet, setSelectedWallet] = useState(null);
|
|
14
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
15
|
+
const [periodStart, setPeriodStart] = useState(document.state.global.periodStart || "");
|
|
16
|
+
const [periodEnd, setPeriodEnd] = useState(document.state.global.periodEnd || "");
|
|
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
|
+
// Handle period date changes
|
|
54
|
+
const handlePeriodStartChange = (e) => {
|
|
55
|
+
const value = e.target.value;
|
|
56
|
+
setPeriodStart(value);
|
|
57
|
+
if (value) {
|
|
58
|
+
dispatch(actions.setPeriodStart({ periodStart: value }));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const handlePeriodEndChange = (e) => {
|
|
62
|
+
const value = e.target.value;
|
|
63
|
+
setPeriodEnd(value);
|
|
64
|
+
if (value) {
|
|
65
|
+
dispatch(actions.setPeriodEnd({ periodEnd: value }));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
// Handle wallet selection for adding billing statements
|
|
69
|
+
const handleAddBillingStatement = (walletAddress) => {
|
|
70
|
+
setSelectedWallet(walletAddress);
|
|
71
|
+
setIsModalOpen(true);
|
|
72
|
+
};
|
|
73
|
+
// Handle closing modal
|
|
74
|
+
const handleCloseModal = () => {
|
|
75
|
+
setIsModalOpen(false);
|
|
76
|
+
setSelectedWallet(null);
|
|
77
|
+
};
|
|
78
|
+
// Format period title for the breakdown section
|
|
79
|
+
const breakdownTitle = useMemo(() => {
|
|
80
|
+
if (!periodStart)
|
|
81
|
+
return "Breakdown";
|
|
82
|
+
const date = new Date(periodStart);
|
|
83
|
+
const month = date.toLocaleDateString("en-US", { month: "short" });
|
|
84
|
+
const year = date.getFullYear();
|
|
85
|
+
return `${month} ${year} Breakdown`;
|
|
86
|
+
}, [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 border-b border-gray-200 dark:border-gray-700 px-8 py-6", children: _jsx("div", { className: "max-w-7xl mx-auto", children: _jsxs("div", { className: "text-center mb-6", children: [_jsx("h1", { className: "text-3xl font-bold text-gray-900 dark:text-white mb-2", 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 })] }) })] }) }) }), _jsx("div", { className: "flex-1 overflow-auto px-8 py-6", children: _jsxs("div", { className: "max-w-7xl mx-auto space-y-8", children: [_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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LineItemGroup } from "../../../document-models/expense-report/gen/types.js";
|
|
2
|
+
export declare function useSyncWallet(): {
|
|
3
|
+
syncWallet: (walletAddress: string, billingStatementIds: string[], groups: LineItemGroup[], dispatch: any) => void;
|
|
4
|
+
};
|
|
5
|
+
//# sourceMappingURL=useSyncWallet.d.ts.map
|
|
@@ -0,0 +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;EA8DhB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useSelectedDriveDocuments } from "@powerhousedao/reactor-browser";
|
|
2
|
+
import { actions } from "../../../document-models/expense-report/index.js";
|
|
3
|
+
import { generateId } from "document-model";
|
|
4
|
+
export function useSyncWallet() {
|
|
5
|
+
const documents = useSelectedDriveDocuments();
|
|
6
|
+
const syncWallet = (walletAddress, billingStatementIds, groups, dispatch) => {
|
|
7
|
+
if (!documents)
|
|
8
|
+
return;
|
|
9
|
+
// Get billing statement documents
|
|
10
|
+
const billingStatements = new Map();
|
|
11
|
+
documents
|
|
12
|
+
.filter((doc) => doc.header.documentType === "powerhouse/billing-statement")
|
|
13
|
+
.forEach((doc) => {
|
|
14
|
+
billingStatements.set(doc.header.id, doc);
|
|
15
|
+
});
|
|
16
|
+
// Helper function to map tag to group
|
|
17
|
+
const mapTagToGroup = (billingLineItem) => {
|
|
18
|
+
// Find expense-account tag
|
|
19
|
+
const expenseAccountTag = billingLineItem.lineItemTag?.find((tag) => tag.dimension === "expense-account");
|
|
20
|
+
if (!expenseAccountTag || !expenseAccountTag.label)
|
|
21
|
+
return null;
|
|
22
|
+
// Find matching group by label
|
|
23
|
+
const group = groups.find((g) => g.label === expenseAccountTag.label);
|
|
24
|
+
return group ? group.id : null;
|
|
25
|
+
};
|
|
26
|
+
// Clear existing line items for this wallet first
|
|
27
|
+
// Note: We'll need to add a new action for this, or remove items one by one
|
|
28
|
+
// For now, let's re-add all line items from billing statements
|
|
29
|
+
// Extract and add line items from all billing statements
|
|
30
|
+
billingStatementIds.forEach((statementId) => {
|
|
31
|
+
const statement = billingStatements.get(statementId);
|
|
32
|
+
if (!statement?.state?.global?.lineItems)
|
|
33
|
+
return;
|
|
34
|
+
const lineItems = statement.state.global.lineItems || [];
|
|
35
|
+
lineItems.forEach((billingLineItem) => {
|
|
36
|
+
const groupId = mapTagToGroup(billingLineItem);
|
|
37
|
+
const expenseLineItem = {
|
|
38
|
+
id: generateId(),
|
|
39
|
+
label: billingLineItem.description,
|
|
40
|
+
group: groupId,
|
|
41
|
+
budget: 0,
|
|
42
|
+
actuals: billingLineItem.totalPriceCash || 0,
|
|
43
|
+
forecast: 0,
|
|
44
|
+
payments: 0,
|
|
45
|
+
comments: null,
|
|
46
|
+
};
|
|
47
|
+
dispatch(actions.addLineItem({
|
|
48
|
+
wallet: walletAddress,
|
|
49
|
+
lineItem: expenseLineItem,
|
|
50
|
+
}));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
return { syncWallet };
|
|
55
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Wallet } from "../../../document-models/expense-report/gen/types.js";
|
|
2
|
+
interface SyncStatus {
|
|
3
|
+
needsSync: boolean;
|
|
4
|
+
outdatedWallets: string[];
|
|
5
|
+
tagChangedWallets: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function useWalletSync(wallets: Wallet[]): SyncStatus;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=useWalletSync.d.ts.map
|
|
@@ -0,0 +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,CA2F3D"}
|