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.