Fix/user feedback (#210)
* Add delete policies for provider consent tokens and provider OTC * Add trade name support for companies in settings and documents * Resolved currency selection issue * Enhance invoice line display with foreign currency support and update delivery date schema to allow empty values * Add currency display for journal entries and include currency metadata in transaction creation * Add trade_name column to company_settings for external display
This commit is contained in:
@@ -211,22 +211,39 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lines.map((line) => (
|
||||
<tr key={line.id} className="border-b last:border-0">
|
||||
<td className="py-2"><AccountNumber number={line.account_number} showName /></td>
|
||||
<td className="py-2 text-muted-foreground">{line.line_description || ''}</td>
|
||||
<td className="py-2 text-right tabular-nums">
|
||||
{Number(line.debit_amount) > 0
|
||||
? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: ''}
|
||||
</td>
|
||||
<td className="py-2 text-right tabular-nums">
|
||||
{Number(line.credit_amount) > 0
|
||||
? Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{lines.map((line) => {
|
||||
const hasForeignCurrency = line.currency && line.currency !== 'SEK' && line.amount_in_currency != null
|
||||
return (
|
||||
<tr key={line.id} className="border-b last:border-0">
|
||||
<td className="py-2"><AccountNumber number={line.account_number} showName /></td>
|
||||
<td className="py-2 text-muted-foreground">{line.line_description || ''}</td>
|
||||
<td className="py-2 text-right tabular-nums">
|
||||
{Number(line.debit_amount) > 0 && (
|
||||
<>
|
||||
{Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
{hasForeignCurrency && Number(line.debit_amount) > 0 && (
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{Number(line.amount_in_currency).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} {line.currency}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 text-right tabular-nums">
|
||||
{Number(line.credit_amount) > 0 && (
|
||||
<>
|
||||
{Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
{hasForeignCurrency && Number(line.credit_amount) > 0 && (
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{Number(line.amount_in_currency).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} {line.currency}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="font-semibold">
|
||||
@@ -244,24 +261,41 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
|
||||
|
||||
{/* Mobile cards */}
|
||||
<div className="sm:hidden space-y-2">
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} className="flex items-center justify-between py-2 border-b last:border-0 gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm"><AccountNumber number={line.account_number} showName /></div>
|
||||
{line.line_description && (
|
||||
<p className="text-xs text-muted-foreground truncate">{line.line_description}</p>
|
||||
)}
|
||||
{lines.map((line) => {
|
||||
const hasForeignCurrency = line.currency && line.currency !== 'SEK' && line.amount_in_currency != null
|
||||
return (
|
||||
<div key={line.id} className="flex items-center justify-between py-2 border-b last:border-0 gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm"><AccountNumber number={line.account_number} showName /></div>
|
||||
{line.line_description && (
|
||||
<p className="text-xs text-muted-foreground truncate">{line.line_description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right shrink-0 text-sm tabular-nums">
|
||||
{Number(line.debit_amount) > 0 && (
|
||||
<p>
|
||||
{Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} D
|
||||
{hasForeignCurrency && (
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{Number(line.amount_in_currency).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} {line.currency}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{Number(line.credit_amount) > 0 && (
|
||||
<p>
|
||||
{Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} K
|
||||
{hasForeignCurrency && (
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{Number(line.amount_in_currency).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} {line.currency}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0 text-sm tabular-nums">
|
||||
{Number(line.debit_amount) > 0 && (
|
||||
<p>{Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} D</p>
|
||||
)}
|
||||
{Number(line.credit_amount) > 0 && (
|
||||
<p>{Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} K</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
<div className="flex justify-between font-semibold text-sm pt-1">
|
||||
<span>Summa</span>
|
||||
<div className="flex gap-3 tabular-nums">
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function CompanySettingsPage() {
|
||||
function handleSave(formData: FormData) {
|
||||
const updates: Record<string, unknown> = {
|
||||
...(formData.has('company_name') && { company_name: formData.get('company_name') as string }),
|
||||
trade_name: (formData.get('trade_name') as string) || null,
|
||||
...(formData.has('org_number') && { org_number: formData.get('org_number') as string }),
|
||||
address_line1: formData.get('address_line1') as string,
|
||||
postal_code: formData.get('postal_code') as string,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import { X } from 'lucide-react'
|
||||
import TransactionForm from '@/components/transactions/TransactionForm'
|
||||
import SwipeCategorizationView from '@/components/transactions/SwipeCategorizationView'
|
||||
@@ -88,6 +89,7 @@ export default function TransactionsPage() {
|
||||
const [exitingIds, setExitingIds] = useState<Set<string>>(new Set())
|
||||
|
||||
const { toast } = useToast()
|
||||
const { dialogProps: deleteDialogProps, confirm: confirmDelete } = useDestructiveConfirm()
|
||||
const supabase = createClient()
|
||||
|
||||
// Computed lists
|
||||
@@ -508,6 +510,40 @@ export default function TransactionsPage() {
|
||||
setIsCreating(false)
|
||||
}
|
||||
|
||||
async function handleDeleteTransaction(id: string) {
|
||||
const transaction = transactions.find((t) => t.id === id)
|
||||
if (!transaction) return
|
||||
|
||||
const ok = await confirmDelete({
|
||||
title: 'Ta bort transaktion',
|
||||
description: `Är du säker på att du vill ta bort "${transaction.description}"? Åtgärden kan inte ångras.`,
|
||||
confirmLabel: 'Ta bort',
|
||||
variant: 'destructive',
|
||||
})
|
||||
if (!ok) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/transactions/${id}`, { method: 'DELETE' })
|
||||
if (!response.ok) {
|
||||
const result = await response.json()
|
||||
toast({
|
||||
title: 'Kunde inte ta bort',
|
||||
description: result.error || 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
}
|
||||
setTransactions((prev) => prev.filter((t) => t.id !== id))
|
||||
toast({ title: 'Borttagen', description: 'Transaktionen har tagits bort' })
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Kunde inte ta bort',
|
||||
description: 'Transaktionen kunde inte tas bort. Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleTransactionBooked(transactionId: string, journalEntryId: string) {
|
||||
setExitingIds((prev) => new Set(prev).add(transactionId))
|
||||
setTimeout(() => {
|
||||
@@ -786,7 +822,7 @@ export default function TransactionsPage() {
|
||||
onMarkPrivate={handleMarkPrivate}
|
||||
onOpenMatchDialog={openMatchDialog}
|
||||
onOpenCategoryDialog={openCategoryDialog}
|
||||
|
||||
onDelete={handleDeleteTransaction}
|
||||
onOpenQuickReview={handleOpenQuickReview}
|
||||
onOpenTemplateReview={handleOpenTemplateReview}
|
||||
onToggleSelect={toggleBatchSelect}
|
||||
@@ -917,6 +953,8 @@ export default function TransactionsPage() {
|
||||
<TransactionForm onSubmit={handleCreateTransaction} isLoading={isCreating} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DestructiveConfirmDialog {...deleteDialogProps} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export async function POST(
|
||||
html: generateInvoiceEmailHtml(emailData),
|
||||
text: generateInvoiceEmailText(emailData),
|
||||
replyTo: company.email || undefined,
|
||||
fromName: company.company_name,
|
||||
fromName: company.trade_name || company.company_name,
|
||||
attachments: [
|
||||
{
|
||||
filename,
|
||||
|
||||
@@ -211,6 +211,7 @@ export async function POST(request: Request) {
|
||||
.single()
|
||||
|
||||
if (invoiceError) {
|
||||
console.error('Invoice insert error:', invoiceError)
|
||||
return NextResponse.json({ error: invoiceError.message }, { status: 500 })
|
||||
}
|
||||
|
||||
|
||||
@@ -94,11 +94,11 @@ export async function GET(request: Request) {
|
||||
// Get company name for the consent page
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('company_name')
|
||||
.select('company_name, trade_name')
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
const companyName = settings?.company_name || user.email
|
||||
const companyName = settings?.trade_name || settings?.company_name || user.email
|
||||
|
||||
// Render consent page
|
||||
const html = `<!DOCTYPE html>
|
||||
|
||||
@@ -559,7 +559,7 @@ async function commitSendInvoice(
|
||||
html: generateInvoiceEmailHtml(emailData),
|
||||
text: generateInvoiceEmailText(emailData),
|
||||
replyTo: company.email || undefined,
|
||||
fromName: company.company_name,
|
||||
fromName: company.trade_name || company.company_name,
|
||||
attachments: [{ filename, content: pdfBuffer, contentType: 'application/pdf' }],
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
parseJsonResponse,
|
||||
createMockRouteParams,
|
||||
createQueuedMockSupabase,
|
||||
makeTransaction,
|
||||
} from '@/tests/helpers'
|
||||
|
||||
const { supabase: mockSupabase, enqueue, reset } = createQueuedMockSupabase()
|
||||
vi.mock('@/lib/supabase/server', () => ({
|
||||
createClient: () => Promise.resolve(mockSupabase),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/company/context', () => ({
|
||||
requireCompanyId: vi.fn().mockResolvedValue('company-1'),
|
||||
}))
|
||||
|
||||
import { DELETE } from '../route'
|
||||
|
||||
describe('DELETE /api/transactions/[id]', () => {
|
||||
const mockUser = { id: 'user-1', email: 'test@test.se' }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
reset()
|
||||
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
|
||||
})
|
||||
|
||||
it('returns 401 when not authenticated', async () => {
|
||||
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } })
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(401)
|
||||
expect(body).toEqual({ error: 'Unauthorized' })
|
||||
})
|
||||
|
||||
it('returns 404 when transaction not found', async () => {
|
||||
enqueue({ data: null, error: { message: 'Not found' } })
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(404)
|
||||
expect(body).toEqual({ error: 'Transaction not found' })
|
||||
})
|
||||
|
||||
it('returns 409 when transaction has a journal entry', async () => {
|
||||
const tx = makeTransaction({ journal_entry_id: 'je-1', bank_connection_id: null, import_source: null })
|
||||
enqueue({ data: tx, error: null })
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(409)
|
||||
expect(body.error).toContain('booked')
|
||||
})
|
||||
|
||||
it('returns 409 when transaction is bank-synced', async () => {
|
||||
const tx = makeTransaction({ bank_connection_id: 'bc-1', journal_entry_id: null, import_source: null })
|
||||
enqueue({ data: tx, error: null })
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(409)
|
||||
expect(body.error).toContain('bank-synced')
|
||||
})
|
||||
|
||||
it('returns 409 when transaction was imported', async () => {
|
||||
const tx = makeTransaction({ import_source: 'csv_nordea', journal_entry_id: null, bank_connection_id: null })
|
||||
enqueue({ data: tx, error: null })
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse<{ error: string }>(response)
|
||||
|
||||
expect(status).toBe(409)
|
||||
expect(body.error).toContain('imported')
|
||||
})
|
||||
|
||||
it('deletes a manually added unbooked transaction', async () => {
|
||||
const tx = makeTransaction({ journal_entry_id: null, bank_connection_id: null, import_source: null })
|
||||
enqueue({ data: tx, error: null }) // fetch
|
||||
enqueue({ data: null, error: null }) // delete
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(200)
|
||||
expect(body).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('returns 500 when deletion fails', async () => {
|
||||
const tx = makeTransaction({ journal_entry_id: null, bank_connection_id: null, import_source: null })
|
||||
enqueue({ data: tx, error: null }) // fetch
|
||||
enqueue({ data: null, error: { message: 'DB error' } }) // delete fails
|
||||
|
||||
const request = new Request('http://localhost/api/transactions/tx-1', { method: 'DELETE' })
|
||||
const response = await DELETE(request, createMockRouteParams({ id: 'tx-1' }))
|
||||
const { status, body } = await parseJsonResponse(response)
|
||||
|
||||
expect(status).toBe(500)
|
||||
expect(body).toEqual({ error: 'Failed to delete transaction' })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { requireCompanyId } from '@/lib/company/context'
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const supabase = await createClient()
|
||||
const { id } = await params
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const companyId = await requireCompanyId(supabase, user.id)
|
||||
|
||||
// Fetch the transaction with ownership check
|
||||
const { data: transaction, error: fetchError } = await supabase
|
||||
.from('transactions')
|
||||
.select('id, journal_entry_id, bank_connection_id, import_source')
|
||||
.eq('id', id)
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
if (fetchError || !transaction) {
|
||||
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Guard: only manually added, unbooked transactions can be deleted
|
||||
if (transaction.journal_entry_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot delete a booked transaction. Use reversal (storno) instead.' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
if (transaction.bank_connection_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot delete a bank-synced transaction' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
if (transaction.import_source) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Cannot delete an imported transaction' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
const { error: deleteError } = await supabase
|
||||
.from('transactions')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.eq('company_id', companyId)
|
||||
|
||||
if (deleteError) {
|
||||
return NextResponse.json({ error: 'Failed to delete transaction' }, { status: 500 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
@@ -23,6 +23,9 @@ export interface FormLine {
|
||||
debit_amount: string
|
||||
credit_amount: string
|
||||
line_description: string
|
||||
currency?: string
|
||||
amount_in_currency?: number
|
||||
exchange_rate?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -159,6 +162,9 @@ export default function JournalEntryForm({
|
||||
debit_amount: parseFloat(l.debit_amount) || 0,
|
||||
credit_amount: parseFloat(l.credit_amount) || 0,
|
||||
line_description: l.line_description || undefined,
|
||||
...(l.currency ? { currency: l.currency } : {}),
|
||||
...(l.amount_in_currency != null ? { amount_in_currency: l.amount_in_currency } : {}),
|
||||
...(l.exchange_rate != null ? { exchange_rate: l.exchange_rate } : {}),
|
||||
}))
|
||||
|
||||
const url = submitUrl ?? '/api/bookkeeping/journal-entries'
|
||||
|
||||
@@ -390,11 +390,18 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
<span className="text-muted-foreground">
|
||||
{Number(line.debit_amount) > 0 ? 'Debet' : 'Kredit'}
|
||||
</span>
|
||||
<span className="font-mono tabular-nums font-medium">
|
||||
{Number(line.debit_amount) > 0
|
||||
? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="font-mono tabular-nums font-medium">
|
||||
{Number(line.debit_amount) > 0
|
||||
? Number(line.debit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })
|
||||
: Number(line.credit_amount).toLocaleString('sv-SE', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
{line.currency && line.currency !== 'SEK' && line.amount_in_currency != null && (
|
||||
<span className="block text-xs text-muted-foreground font-mono tabular-nums">
|
||||
{Number(line.amount_in_currency).toLocaleString('sv-SE', { minimumFractionDigits: 2 })} {line.currency}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -28,6 +28,18 @@ export function CompanyInfoForm({ settings }: CompanyInfoFormProps) {
|
||||
<p className="text-xs text-muted-foreground">Kan inte ändras efter att kontot skapats</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="trade_name">Handelsnamn</Label>
|
||||
<Input
|
||||
id="trade_name"
|
||||
name="trade_name"
|
||||
defaultValue={settings.trade_name || ''}
|
||||
placeholder="Visas på fakturor istället för företagsnamnet"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Valfritt. Visas som huvudnamn på fakturor och e-post, med det juridiska namnet i parentes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="org_number">Organisationsnummer</Label>
|
||||
<Input
|
||||
|
||||
@@ -11,6 +11,7 @@ import JournalEntryForm from '@/components/bookkeeping/JournalEntryForm'
|
||||
import DocumentUploadZone from '@/components/bookkeeping/DocumentUploadZone'
|
||||
import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone'
|
||||
import type { FormLine } from '@/components/bookkeeping/JournalEntryForm'
|
||||
import { resolveSekAmount, buildCurrencyMetadata } from '@/lib/bookkeeping/currency-utils'
|
||||
import type { TransactionWithInvoice } from './transaction-types'
|
||||
|
||||
interface TransactionBookingDialogProps {
|
||||
@@ -21,21 +22,40 @@ interface TransactionBookingDialogProps {
|
||||
}
|
||||
|
||||
function buildInitialLines(transaction: TransactionWithInvoice): FormLine[] {
|
||||
const amount = Math.round(Math.abs(transaction.amount_sek ?? transaction.amount) * 100) / 100
|
||||
const amountStr = amount.toFixed(2)
|
||||
const sekAmount = Math.round(Math.abs(resolveSekAmount(
|
||||
transaction.amount,
|
||||
transaction.amount_sek,
|
||||
transaction.currency,
|
||||
transaction.exchange_rate
|
||||
)) * 100) / 100
|
||||
const amountStr = sekAmount.toFixed(2)
|
||||
const isExpense = transaction.amount < 0
|
||||
|
||||
if (isExpense) {
|
||||
return [
|
||||
{ account_number: '1930', debit_amount: '', credit_amount: amountStr, line_description: 'Företagskonto' },
|
||||
{ account_number: '', debit_amount: amountStr, credit_amount: '', line_description: '' },
|
||||
]
|
||||
const isForeign = !!transaction.currency && transaction.currency !== 'SEK'
|
||||
const currencyMeta = isForeign
|
||||
? buildCurrencyMetadata(
|
||||
transaction.currency,
|
||||
Math.abs(transaction.amount),
|
||||
transaction.exchange_rate
|
||||
)
|
||||
: {}
|
||||
|
||||
const bankLine: FormLine = {
|
||||
account_number: '1930',
|
||||
debit_amount: isExpense ? '' : amountStr,
|
||||
credit_amount: isExpense ? amountStr : '',
|
||||
line_description: 'Företagskonto',
|
||||
...currencyMeta,
|
||||
}
|
||||
|
||||
return [
|
||||
{ account_number: '1930', debit_amount: amountStr, credit_amount: '', line_description: 'Företagskonto' },
|
||||
{ account_number: '', debit_amount: '', credit_amount: amountStr, line_description: '' },
|
||||
]
|
||||
const counterLine: FormLine = {
|
||||
account_number: '',
|
||||
debit_amount: isExpense ? amountStr : '',
|
||||
credit_amount: isExpense ? '' : amountStr,
|
||||
line_description: '',
|
||||
}
|
||||
|
||||
return isExpense ? [bankLine, counterLine] : [bankLine, counterLine]
|
||||
}
|
||||
|
||||
export default function TransactionBookingDialog({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { ArrowUpRight, ArrowDownRight, FileText, Loader2, Paperclip } from 'lucide-react'
|
||||
import { ArrowUpRight, ArrowDownRight, FileText, Loader2, Paperclip, Trash2 } from 'lucide-react'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/info-tooltip'
|
||||
import { getAccountName, formatAccountWithName } from '@/lib/bookkeeping/client-account-names'
|
||||
import { getTemplateById } from '@/lib/bookkeeping/booking-templates'
|
||||
@@ -26,7 +26,7 @@ interface TransactionInboxCardProps {
|
||||
onMarkPrivate: (id: string) => void
|
||||
onOpenMatchDialog: (transaction: TransactionWithInvoice) => void
|
||||
onOpenCategoryDialog: (transaction: TransactionWithInvoice) => void
|
||||
|
||||
onDelete?: (id: string) => void
|
||||
onOpenQuickReview?: (transaction: TransactionWithInvoice, suggestion: SuggestedCategory) => void
|
||||
onOpenTemplateReview?: (transaction: TransactionWithInvoice, templateId: string) => void
|
||||
onToggleSelect: (id: string) => void
|
||||
@@ -45,7 +45,7 @@ export default function TransactionInboxCard({
|
||||
onMarkPrivate,
|
||||
onOpenMatchDialog,
|
||||
onOpenCategoryDialog,
|
||||
|
||||
onDelete,
|
||||
onOpenQuickReview,
|
||||
onOpenTemplateReview,
|
||||
onToggleSelect,
|
||||
@@ -60,6 +60,7 @@ export default function TransactionInboxCard({
|
||||
const isUncategorized = transaction.is_business === null && !transaction.journal_entry_id
|
||||
const showCheckbox = isBatchMode && isUncategorized
|
||||
const hasDocumentMatch = !!transaction.matched_inbox_item
|
||||
const isManualTransaction = !transaction.bank_connection_id && !transaction.import_source && !transaction.journal_entry_id
|
||||
|
||||
function handleSuggestionClick(suggestion: SuggestedCategory) {
|
||||
if (onOpenQuickReview) {
|
||||
@@ -249,6 +250,20 @@ export default function TransactionInboxCard({
|
||||
>
|
||||
Välj mall...
|
||||
</Button>
|
||||
|
||||
{/* Delete button — only for manually added, unbooked transactions */}
|
||||
{isManualTransaction && onDelete && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-9 w-9 p-0 ml-auto text-muted-foreground hover:text-destructive"
|
||||
onClick={() => onDelete(transaction.id)}
|
||||
disabled={isProcessing || isDisabled}
|
||||
aria-label="Ta bort transaktion"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Invoice, Customer, CompanySettings, InvoiceDocumentType } from '@/types'
|
||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils'
|
||||
|
||||
function getDocumentLabel(invoice: Invoice): string {
|
||||
if (invoice.credited_invoice_id) return 'Kreditfaktura'
|
||||
@@ -41,7 +41,7 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
|
||||
<!-- Header -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h1 style="margin: 0 0 10px 0; font-size: 24px; font-weight: 600; color: #111;">
|
||||
${documentType} från ${company.company_name}
|
||||
${documentType} från ${getCompanyPrimaryName(company)}
|
||||
</h1>
|
||||
<p style="margin: 0; color: #666; font-size: 14px;">
|
||||
${documentType}nummer: ${invoice.invoice_number}
|
||||
@@ -136,7 +136,8 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
|
||||
</p>
|
||||
<p style="margin: 0; color: #666; font-size: 14px;">
|
||||
Med vänliga hälsningar,<br>
|
||||
<strong>${company.company_name}</strong>
|
||||
<strong>${getCompanyPrimaryName(company)}</strong>
|
||||
${company.trade_name && company.company_name ? `<br><span style="font-weight: normal; font-size: 12px; color: #999;">(${company.company_name})</span>` : ''}
|
||||
</p>
|
||||
${company.org_number ? `
|
||||
<p style="margin: 10px 0 0 0; color: #999; font-size: 12px;">
|
||||
@@ -165,7 +166,7 @@ export function generateInvoiceEmailText(data: InvoiceEmailData): string {
|
||||
const isProforma = docType === 'proforma'
|
||||
const hidePayment = isCreditNote || isDeliveryNote || isProforma
|
||||
|
||||
let text = `${documentType} från ${company.company_name}\n`
|
||||
let text = `${documentType} från ${getCompanyPrimaryName(company)}\n`
|
||||
text += `${documentType}nummer: ${invoice.invoice_number}\n\n`
|
||||
|
||||
text += `Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},\n\n`
|
||||
@@ -197,7 +198,7 @@ export function generateInvoiceEmailText(data: InvoiceEmailData): string {
|
||||
|
||||
text += `Har du frågor om fakturan? Svara direkt på detta mejl så hjälper vi dig.\n\n`
|
||||
text += `Med vänliga hälsningar,\n`
|
||||
text += `${company.company_name}\n`
|
||||
text += `${getCompanyDisplayName(company)}\n`
|
||||
|
||||
if (company.org_number) {
|
||||
text += `\nOrg.nr: ${company.org_number}`
|
||||
@@ -216,5 +217,5 @@ export function generateInvoiceEmailSubject(data: InvoiceEmailData): string {
|
||||
const { invoice, company } = data
|
||||
const documentType = getDocumentLabel(invoice)
|
||||
|
||||
return `${documentType} ${invoice.invoice_number} från ${company.company_name}`
|
||||
return `${documentType} ${invoice.invoice_number} från ${getCompanyPrimaryName(company)}`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Invoice, Customer, CompanySettings } from '@/types'
|
||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils'
|
||||
|
||||
export interface ReminderEmailData {
|
||||
invoice: Invoice
|
||||
@@ -179,7 +179,8 @@ export function generateReminderEmailHtml(data: ReminderEmailData): string {
|
||||
</p>
|
||||
<p style="margin: 0; color: #666; font-size: 14px;">
|
||||
Med vänliga hälsningar,<br>
|
||||
<strong>${company.company_name}</strong>
|
||||
<strong>${getCompanyPrimaryName(company)}</strong>
|
||||
${company.trade_name && company.company_name ? `<br><span style="font-weight: normal; font-size: 12px; color: #999;">(${company.company_name})</span>` : ''}
|
||||
</p>
|
||||
${company.org_number ? `
|
||||
<p style="margin: 10px 0 0 0; color: #999; font-size: 12px;">
|
||||
@@ -247,7 +248,7 @@ export function generateReminderEmailText(data: ReminderEmailData): string {
|
||||
|
||||
text += `Har du frågor? Svara direkt på detta mejl så hjälper vi dig.\n\n`
|
||||
text += `Med vänliga hälsningar,\n`
|
||||
text += `${company.company_name}\n`
|
||||
text += `${getCompanyDisplayName(company)}\n`
|
||||
|
||||
if (company.org_number) {
|
||||
text += `\nOrg.nr: ${company.org_number}`
|
||||
|
||||
+6
-3
@@ -153,11 +153,13 @@ export const CreateInvoiceItemSchema = z.object({
|
||||
vat_rate: z.number().min(0).max(100).optional(),
|
||||
})
|
||||
|
||||
const optionalIsoDate = isoDate.or(z.literal('')).transform(v => v || undefined).optional()
|
||||
|
||||
export const CreateInvoiceSchema = z.object({
|
||||
customer_id: uuid,
|
||||
invoice_date: isoDate,
|
||||
due_date: isoDate,
|
||||
delivery_date: isoDate.optional(),
|
||||
delivery_date: optionalIsoDate,
|
||||
currency: CurrencySchema,
|
||||
document_type: InvoiceDocumentTypeSchema.optional(),
|
||||
your_reference: z.string().optional(),
|
||||
@@ -254,7 +256,7 @@ export const CreateSupplierInvoiceSchema = z.object({
|
||||
supplier_invoice_number: z.string().min(1, 'Supplier invoice number is required'),
|
||||
invoice_date: isoDate,
|
||||
due_date: isoDate,
|
||||
delivery_date: isoDate.optional(),
|
||||
delivery_date: optionalIsoDate,
|
||||
currency: CurrencySchema.optional(),
|
||||
exchange_rate: z.number().positive().optional(),
|
||||
vat_treatment: VatTreatmentSchema.optional(),
|
||||
@@ -275,7 +277,7 @@ export const UpdateSupplierInvoiceSchema = z.object({
|
||||
supplier_invoice_number: z.string().min(1).optional(),
|
||||
invoice_date: isoDate.optional(),
|
||||
due_date: isoDate.optional(),
|
||||
delivery_date: isoDate.optional(),
|
||||
delivery_date: optionalIsoDate,
|
||||
payment_reference: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
})
|
||||
@@ -349,6 +351,7 @@ export const MatchSupplierInvoiceSchema = z.object({
|
||||
export const UpdateSettingsSchema = z.object({
|
||||
entity_type: EntityTypeSchema.optional(),
|
||||
company_name: z.string().optional(),
|
||||
trade_name: z.string().nullable().optional(),
|
||||
org_number: z.string().optional(),
|
||||
address_line1: z.string().optional(),
|
||||
address_line2: z.string().optional(),
|
||||
|
||||
@@ -196,6 +196,7 @@ export async function createTransactionJournalEntry(
|
||||
debit_amount: absAmount,
|
||||
credit_amount: 0,
|
||||
line_description: transaction.description,
|
||||
...(debitAccount === '1930' ? currencyMeta : {}),
|
||||
})
|
||||
// Credit revenue for net amount
|
||||
lines.push({
|
||||
@@ -221,6 +222,7 @@ export async function createTransactionJournalEntry(
|
||||
debit_amount: absAmount,
|
||||
credit_amount: 0,
|
||||
line_description: transaction.description,
|
||||
...(debitAccount === '1930' ? currencyMeta : {}),
|
||||
},
|
||||
{
|
||||
account_number: creditAccount,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Invoice, Customer, CompanySettings, InvoiceDocumentType } from '@/types'
|
||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils'
|
||||
|
||||
function getDocumentLabel(invoice: Invoice): string {
|
||||
if (invoice.credited_invoice_id) return 'Kreditfaktura'
|
||||
@@ -41,7 +41,7 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
|
||||
<!-- Header -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h1 style="margin: 0 0 10px 0; font-size: 24px; font-weight: 600; color: #111;">
|
||||
${documentType} från ${company.company_name}
|
||||
${documentType} från ${getCompanyPrimaryName(company)}
|
||||
</h1>
|
||||
<p style="margin: 0; color: #666; font-size: 14px;">
|
||||
${documentType}nummer: ${invoice.invoice_number}
|
||||
@@ -136,7 +136,8 @@ export function generateInvoiceEmailHtml(data: InvoiceEmailData): string {
|
||||
</p>
|
||||
<p style="margin: 0; color: #666; font-size: 14px;">
|
||||
Med vänliga hälsningar,<br>
|
||||
<strong>${company.company_name}</strong>
|
||||
<strong>${getCompanyPrimaryName(company)}</strong>
|
||||
${company.trade_name && company.company_name ? `<br><span style="font-weight: normal; font-size: 12px; color: #999;">(${company.company_name})</span>` : ''}
|
||||
</p>
|
||||
${company.org_number ? `
|
||||
<p style="margin: 10px 0 0 0; color: #999; font-size: 12px;">
|
||||
@@ -165,7 +166,7 @@ export function generateInvoiceEmailText(data: InvoiceEmailData): string {
|
||||
const isProforma = docType === 'proforma'
|
||||
const hidePayment = isCreditNote || isDeliveryNote || isProforma
|
||||
|
||||
let text = `${documentType} från ${company.company_name}\n`
|
||||
let text = `${documentType} från ${getCompanyPrimaryName(company)}\n`
|
||||
text += `${documentType}nummer: ${invoice.invoice_number}\n\n`
|
||||
|
||||
text += `Hej${customer.name ? ` ${customer.name.split(' ')[0]}` : ''},\n\n`
|
||||
@@ -197,7 +198,7 @@ export function generateInvoiceEmailText(data: InvoiceEmailData): string {
|
||||
|
||||
text += `Har du frågor om fakturan? Svara direkt på detta mejl så hjälper vi dig.\n\n`
|
||||
text += `Med vänliga hälsningar,\n`
|
||||
text += `${company.company_name}\n`
|
||||
text += `${getCompanyDisplayName(company)}\n`
|
||||
|
||||
if (company.org_number) {
|
||||
text += `\nOrg.nr: ${company.org_number}`
|
||||
@@ -216,5 +217,5 @@ export function generateInvoiceEmailSubject(data: InvoiceEmailData): string {
|
||||
const { invoice, company } = data
|
||||
const documentType = getDocumentLabel(invoice)
|
||||
|
||||
return `${documentType} ${invoice.invoice_number} från ${company.company_name}`
|
||||
return `${documentType} ${invoice.invoice_number} från ${getCompanyPrimaryName(company)}`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Invoice, Customer, CompanySettings } from '@/types'
|
||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { formatCurrency, formatDate, getCompanyDisplayName, getCompanyPrimaryName } from '@/lib/utils'
|
||||
|
||||
export interface ReminderEmailData {
|
||||
invoice: Invoice
|
||||
@@ -179,7 +179,8 @@ export function generateReminderEmailHtml(data: ReminderEmailData): string {
|
||||
</p>
|
||||
<p style="margin: 0; color: #666; font-size: 14px;">
|
||||
Med vänliga hälsningar,<br>
|
||||
<strong>${company.company_name}</strong>
|
||||
<strong>${getCompanyPrimaryName(company)}</strong>
|
||||
${company.trade_name && company.company_name ? `<br><span style="font-weight: normal; font-size: 12px; color: #999;">(${company.company_name})</span>` : ''}
|
||||
</p>
|
||||
${company.org_number ? `
|
||||
<p style="margin: 10px 0 0 0; color: #999; font-size: 12px;">
|
||||
@@ -247,7 +248,7 @@ export function generateReminderEmailText(data: ReminderEmailData): string {
|
||||
|
||||
text += `Har du frågor? Svara direkt på detta mejl så hjälper vi dig.\n\n`
|
||||
text += `Med vänliga hälsningar,\n`
|
||||
text += `${company.company_name}\n`
|
||||
text += `${getCompanyDisplayName(company)}\n`
|
||||
|
||||
if (company.org_number) {
|
||||
text += `\nOrg.nr: ${company.org_number}`
|
||||
|
||||
@@ -317,7 +317,10 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
|
||||
{company.logo_url && (
|
||||
<Image src={company.logo_url} style={{ maxHeight: 40, maxWidth: 150, marginBottom: 6, alignSelf: 'flex-end' }} />
|
||||
)}
|
||||
<Text style={styles.companyName}>{company.company_name}</Text>
|
||||
<Text style={styles.companyName}>{company.trade_name || company.company_name}</Text>
|
||||
{company.trade_name && company.company_name && (
|
||||
<Text style={{ fontSize: 8, color: '#666' }}>({company.company_name})</Text>
|
||||
)}
|
||||
{company.address_line1 && <Text>{company.address_line1}</Text>}
|
||||
{(company.postal_code || company.city) && (
|
||||
<Text>{company.postal_code} {company.city}</Text>
|
||||
@@ -604,7 +607,8 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>
|
||||
{company.company_name}
|
||||
{company.trade_name || company.company_name}
|
||||
{company.trade_name && company.company_name ? ` (${company.company_name})` : ''}
|
||||
{company.org_number ? ` | Org.nr: ${formatOrgNumber(company.org_number)}` : ''}
|
||||
{company.f_skatt ? ' | Godkänd för F-skatt' : ''}
|
||||
{company.vat_number ? ` | Momsreg.nr: ${company.vat_number}` : ''}
|
||||
|
||||
@@ -116,7 +116,7 @@ export async function sendReminder(
|
||||
html: generateReminderEmailHtml(emailData),
|
||||
text: generateReminderEmailText(emailData),
|
||||
replyTo: company.email || undefined,
|
||||
fromName: company.company_name || undefined
|
||||
fromName: company.trade_name || company.company_name || undefined
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function generateFullArchive(
|
||||
// Fetch company settings
|
||||
const { data: company } = await supabase
|
||||
.from('company_settings')
|
||||
.select('company_name, org_number, moms_period')
|
||||
.select('company_name, trade_name, org_number, moms_period')
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
@@ -72,6 +72,7 @@ export async function generateFullArchive(
|
||||
const sieContent = await generateSIEExport(supabase, companyId, {
|
||||
fiscal_period_id: period_id,
|
||||
company_name: company.company_name || 'Unknown',
|
||||
trade_name: company.trade_name,
|
||||
org_number: company.org_number,
|
||||
program_name: 'ERPBase',
|
||||
})
|
||||
|
||||
@@ -710,7 +710,7 @@ export async function generateINK2Declaration(
|
||||
// Fetch company settings
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('company_name, org_number, entity_type, address_line1, postal_code, city, email')
|
||||
.select('company_name, trade_name, org_number, entity_type, address_line1, postal_code, city, email')
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
@@ -931,7 +931,7 @@ export async function generateINK2Declaration(
|
||||
resultAfterFinancial,
|
||||
},
|
||||
companyInfo: {
|
||||
companyName: settings?.company_name || 'Okänt företag',
|
||||
companyName: settings?.trade_name || settings?.company_name || 'Okänt företag',
|
||||
orgNumber: settings?.org_number || null,
|
||||
addressLine1: settings?.address_line1 || null,
|
||||
postalCode: settings?.postal_code || null,
|
||||
|
||||
@@ -175,7 +175,7 @@ export async function generateNEDeclaration(
|
||||
// Fetch company settings
|
||||
const { data: settings } = await supabase
|
||||
.from('company_settings')
|
||||
.select('company_name, org_number, entity_type')
|
||||
.select('company_name, trade_name, org_number, entity_type')
|
||||
.eq('company_id', companyId)
|
||||
.single()
|
||||
|
||||
@@ -320,7 +320,7 @@ export async function generateNEDeclaration(
|
||||
rutor,
|
||||
breakdown,
|
||||
companyInfo: {
|
||||
companyName: settings?.company_name || 'Okänt företag',
|
||||
companyName: settings?.trade_name || settings?.company_name || 'Okänt företag',
|
||||
orgNumber: settings?.org_number || null,
|
||||
},
|
||||
warnings,
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function generateSIEExport(
|
||||
lines.push(`#ORGNR ${options.org_number}`)
|
||||
}
|
||||
|
||||
lines.push(`#FNAMN "${escapeQuotes(options.company_name)}"`)
|
||||
lines.push(`#FNAMN "${escapeQuotes(options.trade_name || options.company_name)}"`)
|
||||
|
||||
// === Fiscal year ===
|
||||
// #RAR 0 = current year, #RAR -1 = previous year (both should be present per spec)
|
||||
|
||||
@@ -29,6 +29,27 @@ export function formatOrgNumber(orgNumber: string): string {
|
||||
return orgNumber
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name for a company, using trade name as primary
|
||||
* with legal name in parentheses if both exist.
|
||||
*/
|
||||
export function getCompanyDisplayName(settings: { trade_name?: string | null; company_name?: string | null }): string {
|
||||
const tradeName = settings.trade_name?.trim()
|
||||
const legalName = settings.company_name?.trim()
|
||||
if (tradeName && legalName) {
|
||||
return `${tradeName} (${legalName})`
|
||||
}
|
||||
return legalName || tradeName || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns just the primary name for contexts where a short name is needed
|
||||
* (e.g. email from name). Uses trade name if set, otherwise legal name.
|
||||
*/
|
||||
export function getCompanyPrimaryName(settings: { trade_name?: string | null; company_name?: string | null }): string {
|
||||
return settings.trade_name?.trim() || settings.company_name?.trim() || ''
|
||||
}
|
||||
|
||||
export function generateInvoiceNumber(): string {
|
||||
const year = new Date().getFullYear()
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0')
|
||||
|
||||
@@ -73,6 +73,13 @@ CREATE POLICY provider_consent_tokens_update ON provider_consent_tokens
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY provider_consent_tokens_delete ON provider_consent_tokens
|
||||
FOR DELETE USING (consent_id IN (
|
||||
SELECT id FROM provider_consents WHERE company_id IN (
|
||||
SELECT company_id FROM team_members WHERE user_id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
CREATE TRIGGER update_provider_consent_tokens_updated_at
|
||||
BEFORE UPDATE ON provider_consent_tokens
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
@@ -109,3 +116,10 @@ CREATE POLICY provider_otc_update ON provider_otc
|
||||
SELECT company_id FROM team_members WHERE user_id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
CREATE POLICY provider_otc_delete ON provider_otc
|
||||
FOR DELETE USING (consent_id IN (
|
||||
SELECT id FROM provider_consents WHERE company_id IN (
|
||||
SELECT company_id FROM team_members WHERE user_id = auth.uid()
|
||||
)
|
||||
));
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Add trade_name column to company_settings
|
||||
-- Allows companies to display a trade name (handelsnamn) on invoices
|
||||
-- and other external-facing documents instead of the legal company name.
|
||||
ALTER TABLE public.company_settings
|
||||
ADD COLUMN trade_name text;
|
||||
@@ -488,6 +488,7 @@ export function makeCompanySettings(
|
||||
company_id: 'company-1',
|
||||
entity_type: 'enskild_firma',
|
||||
company_name: 'Test Firma',
|
||||
trade_name: null,
|
||||
org_number: '199001011234',
|
||||
address_line1: 'Testgatan 1',
|
||||
address_line2: null,
|
||||
|
||||
@@ -136,6 +136,7 @@ export interface CompanySettings {
|
||||
// Entity info
|
||||
entity_type: EntityType
|
||||
company_name: string | null
|
||||
trade_name: string | null
|
||||
org_number: string | null
|
||||
|
||||
// Address
|
||||
@@ -1119,6 +1120,7 @@ export interface BalanceSheetReport {
|
||||
export interface SIEExportOptions {
|
||||
fiscal_period_id: string
|
||||
company_name: string
|
||||
trade_name?: string | null
|
||||
org_number: string | null
|
||||
program_name?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user