neuronix-node 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,38 +1,82 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleExpenseReport = handleExpenseReport;
4
+ /**
5
+ * Format a date string to YYYY-MM-DD for proper Excel sorting.
6
+ */
7
+ function normalizeDate(dateStr) {
8
+ if (!dateStr)
9
+ return "";
10
+ // Already YYYY-MM-DD
11
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr))
12
+ return dateStr;
13
+ // M/D/YYYY or MM/DD/YYYY
14
+ const match = dateStr.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
15
+ if (match)
16
+ return `${match[3]}-${match[1].padStart(2, "0")}-${match[2].padStart(2, "0")}`;
17
+ return dateStr;
18
+ }
19
+ /**
20
+ * Format a number as currency without trailing spaces.
21
+ */
22
+ function fmtCurrency(n) {
23
+ return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
24
+ }
25
+ /**
26
+ * Detect department from filename for merged file processing.
27
+ */
28
+ function detectDepartment(fileName) {
29
+ const lower = fileName.toLowerCase();
30
+ if (/marketing/.test(lower))
31
+ return "Marketing";
32
+ if (/operation/.test(lower))
33
+ return "Operations";
34
+ if (/tech/.test(lower))
35
+ return "Technology";
36
+ if (/hr|payroll|human/.test(lower))
37
+ return "HR & Payroll";
38
+ if (/travel|entertainment/.test(lower))
39
+ return "Travel & Entertainment";
40
+ if (/sales/.test(lower))
41
+ return "Sales";
42
+ if (/admin/.test(lower))
43
+ return "Administration";
44
+ return "General";
45
+ }
4
46
  async function handleExpenseReport(task) {
5
47
  const start = Date.now();
6
48
  const input = task.input_payload;
7
49
  const period = input.period || "Current Period";
50
+ const mergedFrom = input.merged_from || [];
8
51
  const expenses = input.expenses || [
9
52
  { description: "Office Supplies", amount: 245.50, category: "Operations", date: "2026-03-01" },
10
53
  { description: "Software Subscriptions", amount: 890.00, category: "Technology", date: "2026-03-05" },
11
54
  { description: "Team Lunch", amount: 156.75, category: "Meals", date: "2026-03-08" },
12
- { description: "Cloud Hosting", amount: 432.00, category: "Technology", date: "2026-03-10" },
13
- { description: "Marketing Ads", amount: 1200.00, category: "Marketing", date: "2026-03-12" },
14
- { description: "Travel - Client Meeting", amount: 387.25, category: "Travel", date: "2026-03-15" },
15
- { description: "Printer Ink", amount: 89.99, category: "Operations", date: "2026-03-18" },
16
- { description: "Conference Tickets", amount: 599.00, category: "Education", date: "2026-03-20" },
17
55
  ];
56
+ // Assign departments from merged file names if available
57
+ if (mergedFrom.length > 0 && expenses.every((e) => !e.department)) {
58
+ // Try to detect department from category patterns
59
+ for (const exp of expenses) {
60
+ exp.department = detectDepartmentFromCategory(exp.category);
61
+ }
62
+ }
63
+ // Sort by amount descending
64
+ const sorted = [...expenses].sort((a, b) => b.amount - a.amount);
18
65
  // Calculate totals by category
19
66
  const byCategory = {};
20
67
  let total = 0;
21
68
  for (const exp of expenses) {
22
69
  total += exp.amount;
23
70
  if (!byCategory[exp.category]) {
24
- byCategory[exp.category] = { total: 0, count: 0, items: [] };
71
+ byCategory[exp.category] = { total: 0, count: 0 };
25
72
  }
26
73
  byCategory[exp.category].total += exp.amount;
27
74
  byCategory[exp.category].count += 1;
28
- byCategory[exp.category].items.push(exp);
29
75
  }
30
- // Round totals
31
76
  total = Math.round(total * 100) / 100;
32
77
  for (const cat of Object.keys(byCategory)) {
33
78
  byCategory[cat].total = Math.round(byCategory[cat].total * 100) / 100;
34
79
  }
35
- // Sort categories by total descending
36
80
  const sortedCategories = Object.entries(byCategory)
37
81
  .sort((a, b) => b[1].total - a[1].total)
38
82
  .map(([name, data]) => ({
@@ -41,41 +85,110 @@ async function handleExpenseReport(task) {
41
85
  count: data.count,
42
86
  percentage: Math.round((data.total / total) * 1000) / 10,
43
87
  }));
44
- // Generate chart data for the frontend
88
+ // Calculate by department if available
89
+ const hasDepartments = sorted.some((e) => e.department);
90
+ const byDepartment = {};
91
+ if (hasDepartments) {
92
+ for (const exp of expenses) {
93
+ const dept = exp.department || "General";
94
+ if (!byDepartment[dept])
95
+ byDepartment[dept] = { total: 0, count: 0 };
96
+ byDepartment[dept].total += exp.amount;
97
+ byDepartment[dept].count += 1;
98
+ }
99
+ }
100
+ const sortedDepartments = Object.entries(byDepartment)
101
+ .sort((a, b) => b[1].total - a[1].total)
102
+ .map(([name, data]) => ({
103
+ department: name,
104
+ total: Math.round(data.total * 100) / 100,
105
+ count: data.count,
106
+ percentage: Math.round((data.total / total) * 1000) / 10,
107
+ }));
108
+ // Chart data
45
109
  const chartData = {
46
110
  chart_type: "doughnut",
47
111
  title: `Expenses by Category — ${period}`,
48
112
  labels: sortedCategories.map((c) => c.category),
49
- datasets: [{
50
- label: "Expenses",
51
- data: sortedCategories.map((c) => c.total),
52
- }],
113
+ datasets: [{ label: "Expenses", data: sortedCategories.map((c) => c.total) }],
53
114
  };
54
115
  // Summary text
55
116
  const summaryLines = [
56
117
  `Expense Report: ${period}`,
57
- `Total: $${total.toFixed(2)} across ${expenses.length} expenses`,
118
+ `Total: ${fmtCurrency(total)} across ${expenses.length} expenses in ${sortedCategories.length} categories`,
58
119
  ``,
59
- `By Category:`,
60
- ...sortedCategories.map((c) => ` ${c.category}: $${c.total.toFixed(2)} (${c.percentage}%) — ${c.count} item${c.count > 1 ? "s" : ""}`),
120
+ `Top Categories:`,
121
+ ...sortedCategories.slice(0, 10).map((c) => ` ${c.category}: ${fmtCurrency(c.total)} (${c.percentage}%) — ${c.count} items`),
61
122
  ``,
62
- `Largest expense: ${expenses.sort((a, b) => b.amount - a.amount)[0].description} ($${expenses.sort((a, b) => b.amount - a.amount)[0].amount.toFixed(2)})`,
63
- ];
64
- // Generate CSV output
65
- const csvLines = [
66
- "EXPENSE REPORT",
67
- `Period,${period}`,
68
- "",
69
- "EXPENSES BY ITEM",
70
- "Date,Description,Amount,Category",
71
- ...expenses.map((e) => `${e.date || ""},${csvEscape(e.description)},$${e.amount.toFixed(2)},${csvEscape(e.category)}`),
72
- "",
73
- "SUMMARY BY CATEGORY",
74
- "Category,Total,% of Total,Item Count",
75
- ...sortedCategories.map((c) => `${csvEscape(c.category)},$${c.total.toFixed(2)},${c.percentage}%,${c.count}`),
76
- "",
77
- `TOTAL,,$${total.toFixed(2)},${expenses.length} items`,
123
+ `Largest expense: ${sorted[0].description} (${fmtCurrency(sorted[0].amount)})`,
124
+ `Smallest expense: ${sorted[sorted.length - 1].description} (${fmtCurrency(sorted[sorted.length - 1].amount)})`,
78
125
  ];
126
+ // ── Build clean CSV output ────────────────────────────────
127
+ const csvLines = [];
128
+ const hasDept = hasDepartments;
129
+ const itemHeaders = hasDept
130
+ ? "Date,Department,Description,Category,Amount"
131
+ : "Date,Description,Category,Amount";
132
+ // Report header
133
+ csvLines.push("EXPENSE REPORT");
134
+ csvLines.push(`Period: ${period}`);
135
+ csvLines.push(`Generated: ${new Date().toISOString().split("T")[0]}`);
136
+ if (mergedFrom.length > 0) {
137
+ csvLines.push(`Sources: ${mergedFrom.length} files merged`);
138
+ }
139
+ csvLines.push(`Total Items: ${expenses.length}`);
140
+ csvLines.push(`Grand Total: ${fmtCurrency(total)}`);
141
+ csvLines.push("");
142
+ // Section 1: All expenses sorted by amount
143
+ csvLines.push("DETAILED EXPENSES (sorted by amount)");
144
+ csvLines.push(itemHeaders);
145
+ for (const exp of sorted) {
146
+ const date = normalizeDate(exp.date);
147
+ const desc = csvEscape(exp.description);
148
+ const cat = csvEscape(exp.category);
149
+ const amt = fmtCurrency(exp.amount);
150
+ if (hasDept) {
151
+ csvLines.push(`${date},${csvEscape(exp.department || "General")},${desc},${cat},${amt}`);
152
+ }
153
+ else {
154
+ csvLines.push(`${date},${desc},${cat},${amt}`);
155
+ }
156
+ }
157
+ // Subtotal row
158
+ if (hasDept) {
159
+ csvLines.push(`,,,,`);
160
+ csvLines.push(`,,,SUBTOTAL,${fmtCurrency(total)}`);
161
+ }
162
+ else {
163
+ csvLines.push(`,,,`);
164
+ csvLines.push(`,,SUBTOTAL,${fmtCurrency(total)}`);
165
+ }
166
+ csvLines.push("");
167
+ // Section 2: Summary by category
168
+ csvLines.push("SUMMARY BY CATEGORY");
169
+ csvLines.push("Category,Amount,% of Total,Item Count");
170
+ for (const c of sortedCategories) {
171
+ csvLines.push(`${csvEscape(c.category)},${fmtCurrency(c.total)},${c.percentage}%,${c.count}`);
172
+ }
173
+ csvLines.push("");
174
+ // Section 3: Summary by department (if merged)
175
+ if (hasDept && sortedDepartments.length > 0) {
176
+ csvLines.push("SUMMARY BY DEPARTMENT");
177
+ csvLines.push("Department,Amount,% of Total,Item Count");
178
+ for (const d of sortedDepartments) {
179
+ csvLines.push(`${csvEscape(d.department)},${fmtCurrency(d.total)},${d.percentage}%,${d.count}`);
180
+ }
181
+ csvLines.push("");
182
+ }
183
+ // Grand total
184
+ csvLines.push("GRAND TOTAL");
185
+ csvLines.push(`Total Expenses,${fmtCurrency(total)}`);
186
+ csvLines.push(`Total Items,${expenses.length}`);
187
+ csvLines.push(`Total Categories,${sortedCategories.length}`);
188
+ if (hasDept)
189
+ csvLines.push(`Total Departments,${sortedDepartments.length}`);
190
+ csvLines.push(`Largest Expense,${csvEscape(sorted[0].description)} (${fmtCurrency(sorted[0].amount)})`);
191
+ csvLines.push(`Average Expense,${fmtCurrency(Math.round((total / expenses.length) * 100) / 100)}`);
79
192
  const durationMs = Date.now() - start;
80
193
  return {
81
194
  text: summaryLines.join("\n"),
@@ -86,10 +199,12 @@ async function handleExpenseReport(task) {
86
199
  expense_count: expenses.length,
87
200
  category_count: sortedCategories.length,
88
201
  largest_category: sortedCategories[0]?.category,
89
- largest_expense: expenses.sort((a, b) => b.amount - a.amount)[0],
202
+ largest_expense: sorted[0],
203
+ average_expense: Math.round((total / expenses.length) * 100) / 100,
90
204
  },
91
205
  by_category: sortedCategories,
92
- expenses,
206
+ by_department: sortedDepartments.length > 0 ? sortedDepartments : undefined,
207
+ expenses: sorted,
93
208
  chart_data: chartData,
94
209
  duration_ms: durationMs,
95
210
  };
@@ -100,3 +215,19 @@ function csvEscape(value) {
100
215
  }
101
216
  return value;
102
217
  }
218
+ function detectDepartmentFromCategory(category) {
219
+ const lower = category.toLowerCase();
220
+ if (/payroll|benefits|recruiting|onboarding|training|employee|hr|insurance|safety/.test(lower))
221
+ return "HR & Payroll";
222
+ if (/digital|marketing|pr|creative|print|email|influencer|affiliate|content|seo|advertising/.test(lower))
223
+ return "Marketing";
224
+ if (/cloud|software|hardware|ai|infrastructure|security/.test(lower))
225
+ return "Technology";
226
+ if (/rent|utilities|maintenance|office supplies|equipment|furniture/.test(lower))
227
+ return "Operations";
228
+ if (/airfare|lodging|hotel|transport|conference|entertainment|team meal/.test(lower))
229
+ return "Travel & Entertainment";
230
+ if (/consulting/.test(lower))
231
+ return "Consulting";
232
+ return "General";
233
+ }
@@ -1,7 +1,7 @@
1
1
  import type { TaskInput, TaskOutput } from "./index.js";
2
2
  /**
3
3
  * File processor handler.
4
- * Receives a file's content, parses it, determines what to do,
5
- * then routes to the appropriate specialized handler.
4
+ * Receives a file's content, parses it, determines the best handler,
5
+ * converts CSV data into the right format, and routes it.
6
6
  */
7
7
  export declare function handleFileProcess(task: TaskInput): Promise<TaskOutput>;