@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.
@@ -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,kDAqS/B"}
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
- // Extract line items from billing statement and add to wallet
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
- // Create expense report line item from billing statement line item
132
- const expenseLineItem = {
133
- id: generateId(),
134
- label: billingLineItem.description,
135
- group: groupId,
136
- budget: 0, // Could be mapped if available
137
- actuals: billingLineItem.totalPriceCash || 0,
138
- forecast: 0, // Could be mapped if available
139
- payments: 0, // Could be set based on status
140
- comments: null,
141
- };
142
- dispatch(actions.addLineItem({
143
- wallet: walletAddress,
144
- lineItem: expenseLineItem,
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,kDAye9B"}
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 category and aggregate by parent category
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
- // First, aggregate line items by their category (group)
60
- const categoryAggregation = new Map();
65
+ const grouped = new Map();
61
66
  walletLineItems.forEach((item) => {
62
67
  if (!item)
63
68
  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";
69
+ const parentKey = item.parentGroupId || "uncategorized";
93
70
  const items = grouped.get(parentKey) || [];
94
- items.push(aggItem);
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.actuals - item.budget),
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?.actuals || 0) - (item?.budget || 0)),
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 = (groupId, currentComment) => {
132
- setEditingGroupId(groupId);
119
+ const handleStartEdit = (lineItemId, currentComment) => {
120
+ setEditingGroupId(lineItemId);
133
121
  setEditingComment(currentComment);
122
+ setOriginalComment(currentComment);
134
123
  };
135
- // Handle saving comment
136
- const handleSaveComment = (groupId) => {
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
- // 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);
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-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) => {
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.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
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 < 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
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 < 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
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 < 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) })] })] })] }) })] }));
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":"AAUA,wBAAgB,MAAM,4CAiLrB"}
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, useEffect } from "react";
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 { useWalletSync } from "./hooks/useWalletSync.js";
10
- import { useSyncWallet } from "./hooks/useSyncWallet.js";
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 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 }))] }));
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;EA8DhB"}
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
- // 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
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 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
- }));
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,CA2F3D"}
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 line items count and total for this wallet
24
- const currentLineItemsCount = wallet.lineItems?.length || 0;
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
- currentTotal += item?.actuals || 0;
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 what the line items should be based on billing statements
30
- let expectedLineItemsCount = 0;
31
- let expectedTotal = 0;
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
- expectedTotal += item.totalPriceCash || 0;
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
- expectedTags.add(expenseAccountTag.label);
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 current tags in wallet line items
50
- const currentTags = new Set();
51
- wallet.lineItems?.forEach((item) => {
52
- if (item?.group) {
53
- // We need to map the group ID back to label to compare
54
- // This is a simplified check - just checking if tags exist
55
- currentTags.add(item.group);
56
- }
57
- });
58
- // Check if wallet needs sync (count or total mismatch)
59
- const needsQuantitySync = currentLineItemsCount !== expectedLineItemsCount ||
60
- Math.abs(currentTotal - expectedTotal) > 0.01; // Account for floating point precision
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\:opacity-100 {
1250
+ .group-hover\:bg-blue-50 {
1242
1251
  &:is(:where(.group):hover *) {
1243
1252
  @media (hover: hover) {
1244
- opacity: 100%;
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",
4
+ "version": "0.1.5",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
7
7
  "files": [