UX optimization
This commit is contained in:
@@ -8,6 +8,7 @@ import { Label } from '@/components/ui/label'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Loader2, Mail, Sparkles } from 'lucide-react'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
@@ -30,8 +31,8 @@ export default function LoginPage() {
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error.message,
|
||||
title: 'Inloggning misslyckades',
|
||||
description: getErrorMessage(error, { context: 'auth' }),
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
@@ -42,10 +43,10 @@ export default function LoginPage() {
|
||||
title: 'E-post skickad!',
|
||||
description: 'Kolla din inkorg för att logga in.',
|
||||
})
|
||||
} catch {
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Något gick fel. Försök igen.',
|
||||
title: 'Inloggning misslyckades',
|
||||
description: getErrorMessage(error, { context: 'auth' }),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import CustomerForm from '@/components/customers/CustomerForm'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Building,
|
||||
@@ -66,6 +67,7 @@ export default function CustomerDetailPage({
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isEditOpen, setIsEditOpen] = useState(false)
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
const { dialogProps: confirmDialogProps, confirm: confirmAction } = useDestructiveConfirm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchCustomer()
|
||||
@@ -124,7 +126,13 @@ export default function CustomerDetailPage({
|
||||
|
||||
async function handleDelete() {
|
||||
if (!customer) return
|
||||
if (!confirm(`Ta bort "${customer.name}"? Detta kan inte angras.`)) return
|
||||
const ok = await confirmAction({
|
||||
title: `Ta bort ${customer.name}`,
|
||||
description: 'Kunden och tillhörande data tas bort permanent. Denna åtgärd kan inte ångras.',
|
||||
confirmLabel: 'Ta bort',
|
||||
variant: 'destructive',
|
||||
})
|
||||
if (!ok) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/customers/${id}`, {
|
||||
@@ -352,6 +360,8 @@ export default function CustomerDetailPage({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DestructiveConfirmDialog {...confirmDialogProps} />
|
||||
|
||||
{/* Edit dialog */}
|
||||
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
|
||||
@@ -21,6 +21,8 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { Loader2, Plus, Trash2, ArrowLeft, Send, Eye } from 'lucide-react'
|
||||
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
|
||||
import { InvoiceReviewContent } from '@/components/invoices/InvoiceReviewContent'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { useUnsavedChanges } from '@/lib/hooks/use-unsaved-changes'
|
||||
import type { Customer, Currency, CreateInvoiceInput, InvoiceDocumentType } from '@/types'
|
||||
|
||||
const itemSchema = z.object({
|
||||
@@ -73,7 +75,7 @@ export default function NewInvoicePage() {
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
formState: { errors, isDirty },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
@@ -86,6 +88,8 @@ export default function NewInvoicePage() {
|
||||
},
|
||||
})
|
||||
|
||||
useUnsavedChanges(isDirty)
|
||||
|
||||
// Set date defaults on client only to avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
setValue('invoice_date', format(new Date(), 'yyyy-MM-dd'))
|
||||
@@ -209,7 +213,7 @@ export default function NewInvoicePage() {
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || 'Kunde inte skapa faktura')
|
||||
throw new Error(getErrorMessage(result, { context: 'invoice', statusCode: response.status }))
|
||||
}
|
||||
|
||||
const docLabel = watchDocumentType === 'proforma' ? 'Proformafaktura' : watchDocumentType === 'delivery_note' ? 'Följesedel' : 'Faktura'
|
||||
@@ -229,8 +233,8 @@ export default function NewInvoicePage() {
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Något gick fel',
|
||||
title: 'Kunde inte skapa faktura',
|
||||
description: getErrorMessage(error, { context: 'invoice' }),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -249,7 +253,7 @@ export default function NewInvoicePage() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json()
|
||||
throw new Error(result.error || 'Kunde inte skicka faktura')
|
||||
throw new Error(getErrorMessage(result, { context: 'invoice', statusCode: response.status }))
|
||||
}
|
||||
|
||||
toast({
|
||||
@@ -258,8 +262,8 @@ export default function NewInvoicePage() {
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel vid skickning',
|
||||
description: error instanceof Error ? error.message : 'Något gick fel',
|
||||
title: 'Kunde inte skicka faktura',
|
||||
description: getErrorMessage(error, { context: 'invoice' }),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -292,7 +296,7 @@ export default function NewInvoicePage() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json()
|
||||
throw new Error(result.error || 'Kunde inte generera förhandsgranskning')
|
||||
throw new Error(getErrorMessage(result, { context: 'invoice', statusCode: response.status }))
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
@@ -300,8 +304,8 @@ export default function NewInvoicePage() {
|
||||
window.open(url, '_blank')
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte generera PDF',
|
||||
title: 'Kunde inte generera PDF',
|
||||
description: getErrorMessage(error, { context: 'invoice' }),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
|
||||
@@ -170,6 +170,37 @@ export default async function DashboardPage() {
|
||||
.lt('amount', 0)
|
||||
.is('receipt_id', null)
|
||||
|
||||
// Count journal entries missing underlag (documents)
|
||||
// Source types that require supporting documents
|
||||
const needsDocSourceTypes = [
|
||||
'manual',
|
||||
'bank_transaction',
|
||||
'supplier_invoice_registered',
|
||||
'supplier_invoice_paid',
|
||||
'supplier_invoice_cash_payment',
|
||||
'import',
|
||||
]
|
||||
|
||||
const { count: postedEntriesCount } = await supabase
|
||||
.from('journal_entries')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('user_id', user.id)
|
||||
.eq('status', 'posted')
|
||||
.in('source_type', needsDocSourceTypes)
|
||||
|
||||
const { data: entriesWithDocs } = await supabase
|
||||
.from('document_attachments')
|
||||
.select('journal_entry_id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('is_current_version', true)
|
||||
.not('journal_entry_id', 'is', null)
|
||||
|
||||
const uniqueEntriesWithDocs = new Set(
|
||||
(entriesWithDocs || []).map((d) => d.journal_entry_id)
|
||||
).size
|
||||
|
||||
const missingUnderlagCount = Math.max(0, (postedEntriesCount || 0) - uniqueEntriesWithDocs)
|
||||
|
||||
// Calculate receipt streak
|
||||
const { data: recentReceiptActivity } = await supabase
|
||||
.from('receipts')
|
||||
@@ -219,6 +250,7 @@ export default async function DashboardPage() {
|
||||
bankBalance,
|
||||
deadlines: (deadlines || []) as Deadline[],
|
||||
receiptQueue,
|
||||
missingUnderlagCount,
|
||||
}}
|
||||
onboardingProgress={onboardingProgress}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useToast } from '@/components/ui/use-toast'
|
||||
import { ArrowLeft, CheckCircle, CreditCard, FileText, Trash2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { AccountNumber } from '@/components/ui/account-number'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import type { SupplierInvoice, SupplierInvoiceItem, SupplierInvoicePayment } from '@/types'
|
||||
|
||||
function formatAmount(amount: number): string {
|
||||
@@ -47,6 +48,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
const [isPayDialogOpen, setIsPayDialogOpen] = useState(false)
|
||||
const [payAmount, setPayAmount] = useState('')
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const { dialogProps: confirmDialogProps, confirm: confirmAction } = useDestructiveConfirm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchInvoice()
|
||||
@@ -100,7 +102,13 @@ export default function SupplierInvoiceDetailPage() {
|
||||
}
|
||||
|
||||
async function handleCredit() {
|
||||
if (!confirm('Vill du registrera en kreditfaktura för denna faktura?')) return
|
||||
const ok = await confirmAction({
|
||||
title: 'Registrera kreditfaktura',
|
||||
description: 'En kreditfaktura skapas som reverserar den ursprungliga fakturan. Denna åtgärd kan inte ångras.',
|
||||
confirmLabel: 'Registrera kreditfaktura',
|
||||
variant: 'warning',
|
||||
})
|
||||
if (!ok) return
|
||||
setIsProcessing(true)
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}/credit`, { method: 'POST' })
|
||||
const result = await res.json()
|
||||
@@ -114,7 +122,13 @@ export default function SupplierInvoiceDetailPage() {
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('Vill du ta bort denna faktura?')) return
|
||||
const ok = await confirmAction({
|
||||
title: 'Ta bort faktura',
|
||||
description: 'Fakturan och tillhörande data tas bort permanent. Denna åtgärd kan inte ångras.',
|
||||
confirmLabel: 'Ta bort',
|
||||
variant: 'destructive',
|
||||
})
|
||||
if (!ok) return
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}`, { method: 'DELETE' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
@@ -410,6 +424,8 @@ export default function SupplierInvoiceDetailPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<DestructiveConfirmDialog {...confirmDialogProps} />
|
||||
|
||||
{/* Pay Dialog */}
|
||||
<Dialog open={isPayDialogOpen} onOpenChange={setIsPayDialogOpen}>
|
||||
<DialogContent>
|
||||
|
||||
@@ -16,6 +16,8 @@ import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
|
||||
import { SupplierInvoiceReviewContent } from '@/components/suppliers/SupplierInvoiceReviewContent'
|
||||
import AccountCombobox from '@/components/bookkeeping/AccountCombobox'
|
||||
import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions'
|
||||
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
||||
import { useUnsavedChanges } from '@/lib/hooks/use-unsaved-changes'
|
||||
import type { Supplier, BASAccount, VatTreatment } from '@/types'
|
||||
|
||||
interface LineItem {
|
||||
@@ -67,7 +69,7 @@ export default function NewSupplierInvoicePage() {
|
||||
const [showReview, setShowReview] = useState(false)
|
||||
const [pendingData, setPendingData] = useState<FormData | null>(null)
|
||||
|
||||
const { register, control, handleSubmit, watch, setValue } = useForm<FormData>({
|
||||
const { register, control, handleSubmit, watch, setValue, formState: { isDirty } } = useForm<FormData>({
|
||||
defaultValues: {
|
||||
supplier_id: '',
|
||||
supplier_invoice_number: '',
|
||||
@@ -90,6 +92,8 @@ export default function NewSupplierInvoicePage() {
|
||||
},
|
||||
})
|
||||
|
||||
useUnsavedChanges(isDirty)
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'items' })
|
||||
const watchedItems = watch('items')
|
||||
const watchedSupplierId = watch('supplier_id')
|
||||
@@ -208,7 +212,7 @@ export default function NewSupplierInvoicePage() {
|
||||
const result = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error || 'Kunde inte registrera faktura', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte registrera faktura', description: getErrorMessage(result, { context: 'supplier_invoice', statusCode: res.status }), variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Faktura registrerad', description: `Ankomstnummer: ${result.data.arrival_number}` })
|
||||
setShowReview(false)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useToast } from '@/components/ui/use-toast'
|
||||
import { ArrowLeft, Edit, Trash2, FileText } from 'lucide-react'
|
||||
import SupplierForm from '@/components/suppliers/SupplierForm'
|
||||
import Link from 'next/link'
|
||||
import { DestructiveConfirmDialog, useDestructiveConfirm } from '@/components/ui/destructive-confirm-dialog'
|
||||
import type { Supplier, SupplierType, CreateSupplierInput, SupplierInvoice } from '@/types'
|
||||
|
||||
const supplierTypeLabels: Record<SupplierType, string> = {
|
||||
@@ -31,6 +32,7 @@ export default function SupplierDetailPage() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isEditOpen, setIsEditOpen] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const { dialogProps: confirmDialogProps, confirm: confirmAction } = useDestructiveConfirm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchSupplier()
|
||||
@@ -76,7 +78,13 @@ export default function SupplierDetailPage() {
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('Är du säker på att du vill ta bort denna leverantör?')) return
|
||||
const ok = await confirmAction({
|
||||
title: 'Ta bort leverantör',
|
||||
description: `"${supplier?.name}" och tillhörande data tas bort permanent. Denna åtgärd kan inte ångras.`,
|
||||
confirmLabel: 'Ta bort',
|
||||
variant: 'destructive',
|
||||
})
|
||||
if (!ok) return
|
||||
|
||||
const res = await fetch(`/api/suppliers/${params.id}`, { method: 'DELETE' })
|
||||
const result = await res.json()
|
||||
@@ -267,6 +275,8 @@ export default function SupplierDetailPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DestructiveConfirmDialog {...confirmDialogProps} />
|
||||
|
||||
{/* Edit Dialog */}
|
||||
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createClient } from '@/lib/supabase/client'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration'
|
||||
import type { CompanySettings, EntityType, MomsPeriod } from '@/types'
|
||||
|
||||
import Step1EntityType from '@/components/onboarding/Step1EntityType'
|
||||
@@ -26,6 +27,14 @@ const STEP_TITLES = [
|
||||
'Tillägg',
|
||||
]
|
||||
|
||||
function translatePeriodError(msg: string): string {
|
||||
if (msg.includes('end must be after')) return 'Slutdatumet måste vara efter startdatumet.'
|
||||
if (msg.includes('start must be the 1st')) return 'Startdatumet måste vara den 1:a i en månad.'
|
||||
if (msg.includes('end must be the last day')) return 'Slutdatumet måste vara sista dagen i en månad.'
|
||||
if (msg.includes('exceeds maximum 18 months')) return 'Räkenskapsåret får inte överstiga 18 månader (BFL 3 kap.).'
|
||||
return 'Ogiltigt räkenskapsår. Kontrollera datumen och försök igen.'
|
||||
}
|
||||
|
||||
export default function OnboardingPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="flex items-center justify-center h-64"><Loader2 className="h-8 w-8 animate-spin text-primary" /></div>}>
|
||||
@@ -210,26 +219,33 @@ function OnboardingPageContent() {
|
||||
: `Räkenskapsår ${currentYear}/${currentYear + 1}`
|
||||
}
|
||||
|
||||
// Validate period duration (max 18 months)
|
||||
const startDate = new Date(startStr)
|
||||
const endDate = new Date(endStr)
|
||||
const months = (endDate.getFullYear() - startDate.getFullYear()) * 12 +
|
||||
(endDate.getMonth() - startDate.getMonth()) + 1
|
||||
if (months > 18) {
|
||||
console.error(`Period duration ${months} months exceeds 18-month maximum`)
|
||||
} else {
|
||||
await supabase.from('fiscal_periods').upsert({
|
||||
user_id: user.id,
|
||||
name: periodName,
|
||||
period_start: startStr,
|
||||
period_end: endStr,
|
||||
}, {
|
||||
onConflict: 'user_id,period_start,period_end',
|
||||
// Validate period duration
|
||||
const validationError = validatePeriodDuration(startStr, endStr)
|
||||
if (validationError) {
|
||||
toast({
|
||||
title: 'Ogiltigt räkenskapsår',
|
||||
description: translatePeriodError(validationError),
|
||||
variant: 'destructive',
|
||||
})
|
||||
setCurrentStep(3)
|
||||
return
|
||||
}
|
||||
|
||||
await supabase.from('fiscal_periods').upsert({
|
||||
user_id: user.id,
|
||||
name: periodName,
|
||||
period_start: startStr,
|
||||
period_end: endStr,
|
||||
}, {
|
||||
onConflict: 'user_id,period_start,period_end',
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create fiscal period:', err)
|
||||
toast({
|
||||
title: 'Kunde inte skapa räkenskapsår',
|
||||
description: 'Ett fel uppstod när räkenskapsåret skulle skapas. Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { loadExtensions } from '@/lib/extensions/loader'
|
||||
import {
|
||||
sendTaxDeadlineNotifications,
|
||||
sendInvoiceNotifications,
|
||||
sendMissingUnderlagNotifications,
|
||||
} from '@/extensions/general/push-notifications/notification-scheduler'
|
||||
|
||||
/**
|
||||
@@ -40,13 +41,14 @@ export async function GET(request: Request) {
|
||||
|
||||
try {
|
||||
// Send all notification types in parallel
|
||||
const [taxResult, invoiceResult] = await Promise.all([
|
||||
const [taxResult, invoiceResult, underlagResult] = await Promise.all([
|
||||
sendTaxDeadlineNotifications(supabase),
|
||||
sendInvoiceNotifications(supabase),
|
||||
sendMissingUnderlagNotifications(supabase),
|
||||
])
|
||||
|
||||
const totalSent = taxResult.sent + invoiceResult.sent
|
||||
const totalSkipped = taxResult.skipped + invoiceResult.skipped
|
||||
const totalSent = taxResult.sent + invoiceResult.sent + underlagResult.sent
|
||||
const totalSkipped = taxResult.skipped + invoiceResult.skipped + underlagResult.skipped
|
||||
|
||||
console.log(
|
||||
`Push notification cron completed: ${totalSent} sent, ${totalSkipped} skipped`
|
||||
@@ -57,6 +59,9 @@ export async function GET(request: Request) {
|
||||
console.log(
|
||||
` Invoice: ${invoiceResult.sent} sent, ${invoiceResult.skipped} skipped`
|
||||
)
|
||||
console.log(
|
||||
` Missing underlag: ${underlagResult.sent} sent, ${underlagResult.skipped} skipped`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -65,6 +70,7 @@ export async function GET(request: Request) {
|
||||
details: {
|
||||
taxDeadlines: taxResult,
|
||||
invoices: invoiceResult,
|
||||
missingUnderlag: underlagResult,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
generateInvoiceEmailSubject
|
||||
} from '@/lib/email/invoice-templates'
|
||||
import { createInvoiceJournalEntry } from '@/lib/bookkeeping/invoice-entries'
|
||||
import { uploadDocument } from '@/lib/core/documents/document-service'
|
||||
import type { Invoice, InvoiceItem, Customer, CompanySettings } from '@/types'
|
||||
|
||||
ensureInitialized()
|
||||
@@ -166,6 +167,7 @@ export async function POST(
|
||||
|
||||
// Only create journal entries for real invoices (not proformas or delivery notes)
|
||||
const isRealInvoice = !invoice.document_type || invoice.document_type === 'invoice'
|
||||
let createdJournalEntryId: string | undefined
|
||||
if (isRealInvoice && ((company as Record<string, unknown>).accounting_method === 'accrual' || !(company as Record<string, unknown>).accounting_method)) {
|
||||
try {
|
||||
const journalEntry = await createInvoiceJournalEntry(
|
||||
@@ -174,6 +176,7 @@ export async function POST(
|
||||
(company as CompanySettings).entity_type
|
||||
)
|
||||
if (journalEntry) {
|
||||
createdJournalEntryId = journalEntry.id
|
||||
await supabase
|
||||
.from('invoices')
|
||||
.update({ journal_entry_id: journalEntry.id })
|
||||
@@ -185,6 +188,24 @@ export async function POST(
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-store invoice PDF as underlag and link to journal entry
|
||||
if (isRealInvoice) {
|
||||
try {
|
||||
const pdfArrayBuffer = new Uint8Array(pdfBuffer).buffer as ArrayBuffer
|
||||
await uploadDocument(user.id, {
|
||||
name: filename,
|
||||
buffer: pdfArrayBuffer,
|
||||
type: 'application/pdf',
|
||||
}, {
|
||||
upload_source: 'system',
|
||||
journal_entry_id: createdJournalEntryId,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to store invoice PDF as underlag:', err)
|
||||
// Non-blocking — don't fail the send
|
||||
}
|
||||
}
|
||||
|
||||
await eventBus.emit({
|
||||
type: 'invoice.sent',
|
||||
payload: { invoice: invoice as Invoice, userId: user.id },
|
||||
|
||||
@@ -136,6 +136,36 @@ export default async function RootPage() {
|
||||
.lt('amount', 0)
|
||||
.is('receipt_id', null)
|
||||
|
||||
// Count journal entries missing underlag (documents)
|
||||
const needsDocSourceTypes = [
|
||||
'manual',
|
||||
'bank_transaction',
|
||||
'supplier_invoice_registered',
|
||||
'supplier_invoice_paid',
|
||||
'supplier_invoice_cash_payment',
|
||||
'import',
|
||||
]
|
||||
|
||||
const { count: postedEntriesCount } = await supabase
|
||||
.from('journal_entries')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('user_id', user.id)
|
||||
.eq('status', 'posted')
|
||||
.in('source_type', needsDocSourceTypes)
|
||||
|
||||
const { data: entriesWithDocs } = await supabase
|
||||
.from('document_attachments')
|
||||
.select('journal_entry_id')
|
||||
.eq('user_id', user.id)
|
||||
.eq('is_current_version', true)
|
||||
.not('journal_entry_id', 'is', null)
|
||||
|
||||
const uniqueEntriesWithDocs = new Set(
|
||||
(entriesWithDocs || []).map((d) => d.journal_entry_id)
|
||||
).size
|
||||
|
||||
const missingUnderlagCount = Math.max(0, (postedEntriesCount || 0) - uniqueEntriesWithDocs)
|
||||
|
||||
// Calculate receipt streak
|
||||
const { data: recentReceiptActivity } = await supabase
|
||||
.from('receipts')
|
||||
@@ -188,6 +218,7 @@ export default async function RootPage() {
|
||||
bankBalance,
|
||||
deadlines: (deadlines || []) as Deadline[],
|
||||
receiptQueue,
|
||||
missingUnderlagCount,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user