feat: overhaul all 12 sector extensions with full CRUD, validation, and tests
Add shared components (ConfirmDeleteDialog, EditEntryDialog, validation utils), enhance all 12 extension workspaces with edit/delete dialogs, input validation, period comparisons, and new analytics features. Fix critical bugs in ProjectBilling margin calculation and EarningsPerLiter revenue allocation. Add pure calculation modules with 183 new tests across all extensions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const from = searchParams.get('from')
|
||||
const to = searchParams.get('to')
|
||||
const dateFrom = searchParams.get('date_from')
|
||||
const dateTo = searchParams.get('date_to')
|
||||
const groupBy = searchParams.get('group_by')
|
||||
|
||||
if (!from || !to) {
|
||||
return NextResponse.json(
|
||||
{ error: 'from and to account numbers are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Get posted journal entries within date range
|
||||
let entriesQuery = supabase
|
||||
.from('journal_entries')
|
||||
.select('id, entry_date')
|
||||
.eq('user_id', user.id)
|
||||
.eq('status', 'posted')
|
||||
|
||||
if (dateFrom) {
|
||||
entriesQuery = entriesQuery.gte('entry_date', dateFrom)
|
||||
}
|
||||
if (dateTo) {
|
||||
entriesQuery = entriesQuery.lte('entry_date', dateTo)
|
||||
}
|
||||
|
||||
const { data: entries, error: entriesError } = await entriesQuery
|
||||
|
||||
if (entriesError) {
|
||||
return NextResponse.json({ error: entriesError.message }, { status: 500 })
|
||||
}
|
||||
|
||||
if (!entries || entries.length === 0) {
|
||||
return NextResponse.json({ totals: [], monthly: groupBy === 'month' ? [] : undefined })
|
||||
}
|
||||
|
||||
const entryIds = entries.map((e) => e.id)
|
||||
const entryDateMap = new Map(entries.map((e) => [e.id, e.entry_date]))
|
||||
|
||||
// Fetch lines in batches to avoid URL length limits
|
||||
const batchSize = 200
|
||||
const allLines: Array<{
|
||||
journal_entry_id: string
|
||||
account_number: string
|
||||
debit_amount: number
|
||||
credit_amount: number
|
||||
}> = []
|
||||
|
||||
for (let i = 0; i < entryIds.length; i += batchSize) {
|
||||
const batch = entryIds.slice(i, i + batchSize)
|
||||
const { data: lines, error: linesError } = await supabase
|
||||
.from('journal_entry_lines')
|
||||
.select('journal_entry_id, account_number, debit_amount, credit_amount')
|
||||
.in('journal_entry_id', batch)
|
||||
.gte('account_number', from)
|
||||
.lte('account_number', to)
|
||||
|
||||
if (linesError) {
|
||||
return NextResponse.json({ error: linesError.message }, { status: 500 })
|
||||
}
|
||||
if (lines) {
|
||||
allLines.push(...lines)
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate by account
|
||||
const accountTotals = new Map<string, { debit: number; credit: number }>()
|
||||
|
||||
for (const line of allLines) {
|
||||
const existing = accountTotals.get(line.account_number) || { debit: 0, credit: 0 }
|
||||
existing.debit += Number(line.debit_amount) || 0
|
||||
existing.credit += Number(line.credit_amount) || 0
|
||||
accountTotals.set(line.account_number, existing)
|
||||
}
|
||||
|
||||
const totals = Array.from(accountTotals.entries())
|
||||
.map(([account_number, bal]) => ({
|
||||
account_number,
|
||||
debit: Math.round(bal.debit * 100) / 100,
|
||||
credit: Math.round(bal.credit * 100) / 100,
|
||||
net: Math.round((bal.debit - bal.credit) * 100) / 100,
|
||||
}))
|
||||
.sort((a, b) => a.account_number.localeCompare(b.account_number))
|
||||
|
||||
// Monthly grouping
|
||||
if (groupBy === 'month') {
|
||||
const monthlyMap = new Map<string, Map<string, { debit: number; credit: number }>>()
|
||||
|
||||
for (const line of allLines) {
|
||||
const entryDate = entryDateMap.get(line.journal_entry_id)
|
||||
if (!entryDate) continue
|
||||
const month = entryDate.slice(0, 7)
|
||||
if (!monthlyMap.has(month)) {
|
||||
monthlyMap.set(month, new Map())
|
||||
}
|
||||
const monthAccounts = monthlyMap.get(month)!
|
||||
const existing = monthAccounts.get(line.account_number) || { debit: 0, credit: 0 }
|
||||
existing.debit += Number(line.debit_amount) || 0
|
||||
existing.credit += Number(line.credit_amount) || 0
|
||||
monthAccounts.set(line.account_number, existing)
|
||||
}
|
||||
|
||||
const monthlyFlat = Array.from(monthlyMap.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.flatMap(([month, accounts]) =>
|
||||
Array.from(accounts.entries()).map(([account_number, bal]) => ({
|
||||
month,
|
||||
account_number,
|
||||
debit: Math.round(bal.debit * 100) / 100,
|
||||
credit: Math.round(bal.credit * 100) / 100,
|
||||
net: Math.round((bal.debit - bal.credit) * 100) / 100,
|
||||
}))
|
||||
)
|
||||
|
||||
return NextResponse.json({ totals, monthly: monthlyFlat })
|
||||
}
|
||||
|
||||
return NextResponse.json({ totals })
|
||||
}
|
||||
@@ -24,8 +24,12 @@ export async function GET(
|
||||
.eq('user_id', user.id)
|
||||
.eq('extension_id', extensionId)
|
||||
|
||||
const prefix = searchParams.get('prefix')
|
||||
|
||||
if (key) {
|
||||
query = query.eq('key', key)
|
||||
} else if (prefix) {
|
||||
query = query.ilike('key', `${prefix}%`)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
Reference in New Issue
Block a user