Merge remote-tracking branch 'origin/main' into code-quality-improvements
This commit is contained in:
@@ -1,6 +1,63 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ number: string }> }
|
||||
) {
|
||||
const { number } = await params
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Fetch the account to check if it's a system account
|
||||
const { data: account, error: fetchError } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('id, is_system_account')
|
||||
.eq('user_id', user.id)
|
||||
.eq('account_number', number)
|
||||
.single()
|
||||
|
||||
if (fetchError || !account) {
|
||||
return NextResponse.json({ error: 'Kontot hittades inte' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (account.is_system_account) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Systemkonton kan inte tas bort' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check if account is referenced in posted journal entries
|
||||
const { count } = await supabase
|
||||
.from('journal_entry_lines')
|
||||
.select('id', { count: 'exact', head: true })
|
||||
.eq('account_number', number)
|
||||
|
||||
if (count && count > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Kontot kan inte tas bort eftersom det används i bokförda verifikationer. Inaktivera det istället.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const { error: deleteError } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.delete()
|
||||
.eq('id', account.id)
|
||||
.eq('user_id', user.id)
|
||||
|
||||
if (deleteError) {
|
||||
return NextResponse.json({ error: deleteError.message }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ number: string }> }
|
||||
@@ -15,14 +72,17 @@ export async function PUT(
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
// Build update object with only provided fields
|
||||
const updates: Record<string, unknown> = {}
|
||||
if (body.account_name !== undefined) updates.account_name = body.account_name
|
||||
if (body.is_active !== undefined) updates.is_active = body.is_active
|
||||
if (body.description !== undefined) updates.description = body.description
|
||||
if (body.default_vat_code !== undefined) updates.default_vat_code = body.default_vat_code
|
||||
if (body.sru_code !== undefined) updates.sru_code = body.sru_code
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.update({
|
||||
account_name: body.account_name,
|
||||
is_active: body.is_active,
|
||||
description: body.description,
|
||||
default_vat_code: body.default_vat_code,
|
||||
})
|
||||
.update(updates)
|
||||
.eq('user_id', user.id)
|
||||
.eq('account_number', number)
|
||||
.select()
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
|
||||
|
||||
/**
|
||||
* POST /api/bookkeeping/accounts/activate
|
||||
*
|
||||
* Batch-activate BAS accounts for a user.
|
||||
* Accepts { account_numbers: string[] } and creates chart_of_accounts rows from reference data.
|
||||
* Skips any accounts that already exist for the user.
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const accountNumbers: string[] = body.account_numbers
|
||||
|
||||
if (!Array.isArray(accountNumbers) || accountNumbers.length === 0) {
|
||||
return NextResponse.json({ error: 'account_numbers array required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check which accounts already exist
|
||||
const { data: existing } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('account_number')
|
||||
.eq('user_id', user.id)
|
||||
.in('account_number', accountNumbers)
|
||||
|
||||
const existingNumbers = new Set((existing || []).map((a) => a.account_number))
|
||||
|
||||
// Build rows for accounts that don't already exist
|
||||
const newAccounts = accountNumbers
|
||||
.filter((num) => !existingNumbers.has(num))
|
||||
.map((num) => {
|
||||
const ref = getBASReference(num)
|
||||
if (!ref) return null
|
||||
|
||||
return {
|
||||
user_id: user.id,
|
||||
account_number: ref.account_number,
|
||||
account_name: ref.account_name,
|
||||
account_class: ref.account_class,
|
||||
account_group: ref.account_group,
|
||||
account_type: ref.account_type,
|
||||
normal_balance: ref.normal_balance,
|
||||
plan_type: 'full_bas' as const,
|
||||
is_active: true,
|
||||
is_system_account: false,
|
||||
description: ref.description,
|
||||
sru_code: ref.sru_code,
|
||||
sort_order: parseInt(ref.account_number),
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (newAccounts.length === 0) {
|
||||
return NextResponse.json({
|
||||
data: [],
|
||||
message: 'All accounts already activated',
|
||||
activated: 0,
|
||||
skipped: accountNumbers.length,
|
||||
})
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('chart_of_accounts')
|
||||
.insert(newAccounts)
|
||||
.select()
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
data,
|
||||
activated: data?.length || 0,
|
||||
skipped: accountNumbers.length - (data?.length || 0),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-reference'
|
||||
|
||||
/**
|
||||
* GET /api/bookkeeping/accounts/reference
|
||||
*
|
||||
* Returns the full BAS reference catalog merged with the user's activation status.
|
||||
* Each reference account includes: is_activated (exists in user's chart), is_active, is_system_account, is_custom.
|
||||
*/
|
||||
export async function GET() {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Fetch user's chart of accounts (paginated to avoid 1000-row limit)
|
||||
try {
|
||||
const userAccounts = await fetchAllRows<{ account_number: string; is_active: boolean; is_system_account: boolean }>(({ from, to }) =>
|
||||
supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('account_number, is_active, is_system_account')
|
||||
.eq('user_id', user.id)
|
||||
.range(from, to)
|
||||
)
|
||||
|
||||
// Build lookup map
|
||||
const userAccountMap = new Map(
|
||||
userAccounts.map((a) => [a.account_number, a])
|
||||
)
|
||||
|
||||
// Merge reference with user status
|
||||
const merged = BAS_REFERENCE.map((ref) => {
|
||||
const userAccount = userAccountMap.get(ref.account_number)
|
||||
return {
|
||||
...ref,
|
||||
is_activated: !!userAccount,
|
||||
is_active: userAccount?.is_active ?? false,
|
||||
is_system_account: userAccount?.is_system_account ?? false,
|
||||
}
|
||||
})
|
||||
|
||||
// Also identify custom accounts (in user's chart but not in BAS reference)
|
||||
const basNumbers = new Set(BAS_REFERENCE.map((r) => r.account_number))
|
||||
const customAccounts = userAccounts
|
||||
.filter((a) => !basNumbers.has(a.account_number))
|
||||
.map((a) => ({
|
||||
account_number: a.account_number,
|
||||
is_custom: true,
|
||||
is_activated: true,
|
||||
is_active: a.is_active,
|
||||
is_system_account: a.is_system_account,
|
||||
}))
|
||||
|
||||
return NextResponse.json({ data: merged, customAccounts })
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error instanceof Error ? error.message : 'Failed to fetch accounts' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
@@ -13,27 +14,29 @@ export async function GET(request: Request) {
|
||||
const accountClass = searchParams.get('class')
|
||||
const activeOnly = searchParams.get('active') !== 'false'
|
||||
|
||||
let query = supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.order('sort_order')
|
||||
try {
|
||||
const data = await fetchAllRows(({ from, to }) => {
|
||||
let query = supabase
|
||||
.from('chart_of_accounts')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.order('sort_order')
|
||||
|
||||
if (activeOnly) {
|
||||
query = query.eq('is_active', true)
|
||||
if (activeOnly) {
|
||||
query = query.eq('is_active', true)
|
||||
}
|
||||
|
||||
if (accountClass) {
|
||||
query = query.eq('account_class', parseInt(accountClass))
|
||||
}
|
||||
|
||||
return query.range(from, to)
|
||||
})
|
||||
|
||||
return NextResponse.json({ data })
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error instanceof Error ? error.message : 'Failed to fetch accounts' }, { status: 500 })
|
||||
}
|
||||
|
||||
if (accountClass) {
|
||||
query = query.eq('account_class', parseInt(accountClass))
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ data })
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { CreateFiscalPeriodInput } from '@/types'
|
||||
import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration'
|
||||
|
||||
export async function GET() {
|
||||
const supabase = await createClient()
|
||||
@@ -33,6 +34,12 @@ export async function POST(request: Request) {
|
||||
|
||||
const body = await request.json() as CreateFiscalPeriodInput
|
||||
|
||||
// Validate period duration (max 18 months per BFL 3 kap.)
|
||||
const durationError = validatePeriodDuration(body.period_start, body.period_end)
|
||||
if (durationError) {
|
||||
return NextResponse.json({ error: durationError }, { status: 400 })
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('fiscal_periods')
|
||||
.insert({
|
||||
|
||||
Reference in New Issue
Block a user