PageSourceSearch

https://cornerstone-ccb8cf1b.base44.app/assets/SeedFromActualsModal-Cb5MMq9D.js

js cornerstone-ccb8cf1b.base44.app collected 2026-10-03 03:13:16 UTC 11,902 bytes, 277 lines download raw bytes

1const t=`import React, { useState, useEffect } from 'react';
2import { base44 } from '@/api/base44Client';
3import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
4import { Button } from '@/components/ui/button';
5import { Loader2 } from 'lucide-react';
6import moment from 'moment';
7
8const DEBIT_TYPES = ['Withdrawal', 'Transfer Out', 'Check', 'ACH', 'Electronic Payment'];
9
10function fmtCurrency(val) {
11  return '$' + (parseFloat(val) || 0).toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
12}
13
14export default function SeedFromActualsModal({ budget, tenantId, bankCategories, existingLines, onSeeded, onClose, lineType = 'Expense' }) {
15  const [seedSource, setSeedSource] = useState('last_year');
16  const [loading, setLoading] = useState(true);
17  const [categoryData, setCategoryData] = useState([]);
18  const [checked, setChecked] = useState({});
19  const [creating, setCreating] = useState(false);
20  const [progress, setProgress] = useState('');
21
22  // Fiscal year date bounds (current fiscal year)
23  const fiscalStart = \`\${budget.fiscal_year}-\${String(budget.fiscal_start_month || 1).padStart(2, '0')}-01\`;
24  const fiscalEnd = moment(fiscalStart).add(11, 'months').endOf('month').format('YYYY-MM-DD');
25
26  // Prior fiscal year bounds
27  const priorStart = moment(fiscalStart).subtract(1, 'year').format('YYYY-MM-DD');
28  const priorEnd = moment(fiscalEnd).subtract(1, 'year').format('YYYY-MM-DD');
29  const priorYear = budget.fiscal_year - 1;
30
31  // YTD bounds
32  const today = moment().format('YYYY-MM-DD');
33  const ytdEnd = today < fiscalEnd ? today : fiscalEnd;
34
35  // Months elapsed in current fiscal year (minimum 1)
36  const monthsElapsed = Math.max(1, moment(ytdEnd).diff(moment(fiscalStart), 'months') + 1);
37
38  // Which fiscal month index (1-based) maps to which calendar key
39  const fiscalMonthMeta = Array.from({ length: 12 }, (_, i) => {
40    const m = moment(fiscalStart).add(i, 'months');
41    return {
42      key: \`m\${String(i + 1).padStart(2, '0')}\`,
43      ym: m.format('YYYY-MM'),
44      fiscalIndex: i + 1, // 1-based position in fiscal year
45    };
46  });
47
48  useEffect(() => {
49    loadActuals();
50  }, [seedSource]); // eslint-disable-line react-hooks/exhaustive-deps
51
52  const loadActuals = async () => {
53    setLoading(true);
54    setCategoryData([]);
55    setChecked({});
56    try {
57      const txns = await base44.entities.BankTransaction.filter({ tenant_id: tenantId });
58
59      const rangeStart = seedSource === 'last_year' ? priorStart : fiscalStart;
60      const rangeEnd = seedSource === 'last_year' ? priorEnd : ytdEnd;
61
62      const isDebit = (t) => DEBIT_TYPES.includes(t.transaction_type);
63      const filtered = txns.filter(t => {
64        if (t.transaction_date < rangeStart || t.transaction_date > rangeEnd) return false;
65        if (lineType === 'Expense') return isDebit(t) && t.category;
66        // Income: non-debit transactions; allow blank category (will group as Uncategorized Income)
67        return !isDebit(t);
68      }).map(t => ({
69        ...t,
70        category: t.category || 'Uncategorized Income'
71      }));
72
73      // Group by category — store amounts per fiscal-year-month key (m01..m12)
74      const byCategory = {};
75      filtered.forEach(t => {
76        const cat = t.category;
77        const txnYM = moment(t.transaction_date).format('YYYY-MM');
78        // Find which fiscal month key this maps to
79        const meta = fiscalMonthMeta.find(m =>
80          seedSource === 'last_year'
81            ? moment(t.transaction_date).format('MM') === m.ym.slice(5) // match calendar month position for prior year
82            : m.ym === txnYM
83        );
84        if (!byCategory[cat]) {
85          byCategory[cat] = { monthly: {}, total: 0 };
86          fiscalMonthMeta.forEach(m => { byCategory[cat].monthly[m.key] = 0; });
87        }
88        if (meta) {
89          byCategory[cat].monthly[meta.key] = (byCategory[cat].monthly[meta.key] || 0) + (t.amount || 0);
90        }
91        byCategory[cat].total += (t.amount || 0);
92      });
93
94      // For YTD: annualize future months using average monthly spend
95      if (seedSource === 'ytd') {
96        Object.keys(byCategory).forEach(cat => {
97          const catData = byCategory[cat];
98          const avgMonthly = catData.total / monthsElapsed;
99          let projectedTotal = 0;
100          fiscalMonthMeta.forEach(m => {
101            if (m.fiscalIndex > monthsElapsed) {
102              // Future month — use average
103              catData.monthly[m.key] = Math.round(avgMonthly * 100) / 100;
104            }
105            projectedTotal += catData.monthly[m.key] || 0;
106          });
107          catData.projectedAnnual = projectedTotal;
108        });
109      }
110
111      const existingCatNames = new Set(existingLines.filter(l => l.line_type === lineType).map(l => l.category_name));
112      const data = Object.entries(byCategory)
113        .filter(([cat]) => !existingCatNames.has(cat))
114        .map(([cat, info]) => ({
115          category: cat,
116          monthly: info.monthly,
117          displayAmount: seedSource === 'ytd' ? info.projectedAnnual : info.total,
118        }))
119        .sort((a, b) => b.displayAmount - a.displayAmount);
120
121      setCategoryData(data);
122      const initChecked = {};
123      data.forEach(d => { initChecked[d.category] = true; });
124      setChecked(initChecked);
125    } finally {
126      setLoading(false);
127    }
128  };
129
130  const handleCreate = async () => {
131    const toCreate = categoryData.filter(d => checked[d.category]);
132    setCreating(true);
133    const created = [];
134    const existingExpenseCount = existingLines.filter(l => l.line_type === lineType).length;
135    for (let i = 0; i < toCreate.length; i++) {
136      const d = toCreate[i];
137      setProgress(\`Creating \${i + 1} of \${toCreate.length} lines...\`);
138      const annualAmount = fiscalMonthMeta.reduce((s, m) => s + (d.monthly[m.key] || 0), 0);
139      const line = await base44.entities.BudgetLine.create({
140        tenant_id: tenantId,
141        budget_id: budget.id,
142        budget_year: budget.fiscal_year,
143        line_type: lineType,
144        category_name: d.category,
145        annual_amount: annualAmount,
146        m01: d.monthly['m01'] || 0,
147        m02: d.monthly['m02'] || 0,
148        m03: d.monthly['m03'] || 0,
149        m04: d.monthly['m04'] || 0,
150        m05: d.monthly['m05'] || 0,
151        m06: d.monthly['m06'] || 0,
152        m07: d.monthly['m07'] || 0,
153        m08: d.monthly['m08'] || 0,
154        m09: d.monthly['m09'] || 0,
155        m10: d.monthly['m10'] || 0,
156        m11: d.monthly['m11'] || 0,
157        m12: d.monthly['m12'] || 0,
158        sort_order: existingExpenseCount + i
159      });
160      created.push(line);
161    }
162    setCreating(false);
163    onSeeded(created);
164  };
165
166  const allChecked = categoryData.length > 0 && categoryData.every(d => checked[d.category]);
167  const checkedCount = Object.values(checked).filter(Boolean).length;
168
169  const amountLabel = seedSource === 'ytd'
170    ? 'YTD Projected Annual'
171    : lineType === 'Income' ? 'Income Amount' : 'Prior Year Total';
172
173  const summaryText = seedSource === 'last_year'
174    ? \`Found \${categoryData.length} \${lineType === 'Income' ? 'income' : 'spending'} categories from FY \${priorYear}\`
175    : \`Found \${categoryData.length} \${lineType === 'Income' ? 'income' : 'spending'} categories — YTD actuals (\${moment(fiscalStart).format('MMM D, YYYY')} through \${moment(ytdEnd).format('MMM D, YYYY')})\`;
176
177  return (
178    <Dialog open onOpenChange={onClose}>
179      <DialogContent className="max-w-lg max-h-[85vh] overflow-hidden flex flex-col">
180        <DialogHeader>
181          <DialogTitle>{lineType === 'Income' ? 'Seed Income Lines from Actuals' : 'Seed Expense Lines from Actuals'}</DialogTitle>
182        </DialogHeader>
183
184        {/* Source Toggle */}
185        <div className="flex rounded-lg border overflow-hidden text-sm">
186          <button
187            onClick={() => setSeedSource('last_year')}
188            className={\`flex-1 px-3 py-2 font-medium transition-colors \${seedSource === 'last_year' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'}\`}
189          >
190            Last Year's Actuals
191          </button>
192          <button
193            onClick={() => setSeedSource('ytd')}
194            className={\`flex-1 px-3 py-2 font-medium transition-colors border-l \${seedSource === 'ytd' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'}\`}
195          >
196            This Year's YTD Actuals
197          </button>
198        </div>
199
200        {/* YTD note */}
201        {seedSource === 'ytd' && (
202          <div className="bg-amber-50 border border-amber-200 rounded-lg px-3 py-2 text-xs text-amber-800">
203            Future months are estimated based on your average monthly spend so far this year.
204          </div>
205        )}
206
207        {loading ? (
208          <div className="flex items-center justify-center py-12 text-sm text-gray-500">
209            <Loader2 className="w-5 h-5 animate-spin text-blue-600 mr-2" />
210            Loading transactions...
211          </div>
212        ) : (
213          <>
214            <p className="text-sm text-gray-500 px-1">
215              {categoryData.length >
215 0 ? summaryText : \`No new spending categories found.\`}
216              {categoryData.length > 0 ? '. Select which to import as budget lines.' : ''}
217            </p>
218
219            {categoryData.length > 0 && (
220              <div className="overflow-y-auto flex-1 border rounded-lg">
221                <table className="w-full text-sm">
222                  <thead className="bg-gray-50 sticky top-0 z-10">
223                    <tr className="border-b text-xs text-gray-500 uppercase tracking-wide">
224                      <th className="px-3 py-2 text-left w-8">
225                        <input
226                          type="checkbox"
227                          checked={allChecked}
228                          onChange={e => {
229                            const all = {};
230                            categoryData.forEach(d => { all[d.category] = e.target.checked; });
231                            setChecked(all);
232                          }}
233                        />
234                      </th>
235                      <th className="px-3 py-2 text-left">Category</th>
236                      <th className="px-3 py-2 text-right">{amountLabel}</th>
237                    </tr>
238                  </thead>
239                  <tbody>
240                    {categoryData.map(d => (
241                      <tr key={d.category} className="border-b hover:bg-gray-50">
242                        <td className="px-3 py-2">
243                          <input
244                            type="checkbox"
245                            checked={!!checked[d.category]}
246                            onChange={e => setChecked(prev => ({ ...prev, [d.category]: e.target.checked }))}
247                          />
248                        </td>
249                        <td className="px-3 py-2 font-medium text-gray-800">{d.category}</td>
250                        <td className="px-3 py-2 text-right font-mono text-gray-700">{fmtCurrency(d.displayAmount)}</td>
251                      </tr>
252                    ))}
253                  </tbody>
254                </table>
255              </div>
256            )}
257
258            {progress && (
259              <p className="text-sm text-blue-600 flex items-center gap-1.5 mt-1">
260                <Loader2 className="w-3.5 h-3.5 animate-spin" />
261                {progress}
262              </p>
263            )}
264          </>
265        )}
266
267        <DialogFooter className="mt-2">
268          <Button variant="outline" onClick={onClose} disabled={creating}>Cancel</Button>
269          <Button onClick={handleCreate} disabled={creating || loading || checkedCount === 0}>
270            {creating && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
271            Create {checkedCount > 0 ? \`\${checkedCount} \` : ''}Budget Lines
272          </Button>
273        </DialogFooter>
274      </DialogContent>
275    </Dialog>
276  );
277}`;export{t as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.