diff --git a/app/(dashboard)/customers/page.tsx b/app/(dashboard)/customers/page.tsx index b07e51ae..af8abbe0 100644 --- a/app/(dashboard)/customers/page.tsx +++ b/app/(dashboard)/customers/page.tsx @@ -179,7 +179,7 @@ export default function CustomersPage() {
{filteredCustomers.map((customer) => ( - +
diff --git a/app/(dashboard)/deadlines/page.tsx b/app/(dashboard)/deadlines/page.tsx index 4e6797c2..ba21e231 100644 --- a/app/(dashboard)/deadlines/page.tsx +++ b/app/(dashboard)/deadlines/page.tsx @@ -4,9 +4,9 @@ import { useState, useEffect, useCallback } from 'react' import Link from 'next/link' import { createClient } from '@/lib/supabase/client' import { useToast } from '@/components/ui/use-toast' +import { ToastAction } from '@/components/ui/toast' import { DeadlineList } from '@/components/deadlines/DeadlineList' -import { Card, CardContent } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' +import { PageHeader } from '@/components/ui/page-header' import { AlertTriangle, ArrowRight } from 'lucide-react' import type { Deadline } from '@/types' @@ -25,7 +25,6 @@ export default function DeadlinesPage() { try { const today = new Date().toISOString().split('T')[0] - // Fetch all data in parallel const [deadlinesRes, customersRes, overdueRes] = await Promise.all([ supabase.from('deadlines').select('*, customer:customers(name)').order('due_date', { ascending: true }), supabase.from('customers').select('id, name').order('name', { ascending: true }), @@ -92,11 +91,14 @@ export default function DeadlinesPage() { } const handleDeadlineToggle = async (deadline: Deadline) => { + const wasCompleted = deadline.is_completed + const newCompleted = !wasCompleted + try { const response = await fetch(`/api/deadlines/${deadline.id}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ is_completed: !deadline.is_completed }), + body: JSON.stringify({ is_completed: newCompleted }), }) if (!response.ok) { @@ -104,11 +106,33 @@ export default function DeadlinesPage() { throw new Error(result.error || 'Failed to toggle deadline') } - toast({ - title: deadline.is_completed ? 'Markerad som ej klar' : 'Markerad som klar', - }) - fetchData() + + if (newCompleted) { + toast({ + title: `"${deadline.title}" markerad som klar`, + action: ( + { + try { + await fetch(`/api/deadlines/${deadline.id}/complete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ is_completed: false }), + }) + } catch { + toast({ + title: 'Kunde inte ångra', + variant: 'destructive', + }) + } + }}> + Ångra + + ), + }) + } else { + toast({ title: `"${deadline.title}" markerad som ej klar` }) + } } catch (error) { toast({ title: 'Kunde inte uppdatera status', @@ -157,10 +181,7 @@ export default function DeadlinesPage() { throw new Error(result.error || 'Failed to delete deadline') } - toast({ - title: 'Deadline borttagen', - }) - + toast({ title: 'Deadline borttagen' }) fetchData() } catch (error) { toast({ @@ -174,35 +195,21 @@ export default function DeadlinesPage() { if (isLoading) { return (
-
-

Deadlines

-
- {/* Overdue alert skeleton */} -
-
-
-
-
-
-
-
-
-
-
-
- {/* Deadline list skeleton */} -
+ +
{[1, 2, 3, 4].map((i) => (
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
))} @@ -213,32 +220,23 @@ export default function DeadlinesPage() { return (
-
-

Deadlines

-
+ + {/* Overdue invoices alert */} {overdueInvoices.count > 0 && ( - - - -
-
- -
-

Forfallna fakturor

-

- {overdueInvoices.count} st totalt{' '} - {overdueInvoices.total.toLocaleString('sv-SE')} kr -

-
-
-
- {overdueInvoices.count} - -
-
-
-
+ +
+
+ +

+ {overdueInvoices.count} förfallna fakturor + + {overdueInvoices.total.toLocaleString('sv-SE')} kr + +

+
+ +
)} diff --git a/app/(dashboard)/expenses/new/page.tsx b/app/(dashboard)/expenses/new/page.tsx index 42c043df..04427e23 100644 --- a/app/(dashboard)/expenses/new/page.tsx +++ b/app/(dashboard)/expenses/new/page.tsx @@ -81,6 +81,7 @@ export default function NewExpensePage() { const [pendingData, setPendingData] = useState(null) const [showNewSupplier, setShowNewSupplier] = useState(false) const [isCreatingSupplier, setIsCreatingSupplier] = useState(false) + const [pendingSupplierSelect, setPendingSupplierSelect] = useState(null) const [advancedOpen, setAdvancedOpen] = useState(false) const [newSupplier, setNewSupplier] = useState({ name: '', @@ -146,6 +147,14 @@ export default function NewExpensePage() { } }, [watchedSupplierId, suppliers]) + // Auto-select newly created supplier once it's in the list + useEffect(() => { + if (pendingSupplierSelect && suppliers.find((s) => s.id === pendingSupplierSelect)) { + setValue('supplier_id', pendingSupplierSelect, { shouldDirty: true, shouldValidate: true }) + setPendingSupplierSelect(null) + } + }, [suppliers, pendingSupplierSelect, setValue]) + async function fetchSuppliers() { const res = await fetch('/api/suppliers') const { data } = await res.json() @@ -220,7 +229,7 @@ export default function NewExpensePage() { } else { const created = result.data as Supplier setSuppliers((prev) => [...prev, created].sort((a, b) => a.name.localeCompare(b.name))) - setValue('supplier_id', created.id) + setPendingSupplierSelect(created.id) setShowNewSupplier(false) setNewSupplier({ name: '', supplier_type: 'swedish_business', org_number: '', bankgiro: '', plusgiro: '', default_expense_account: '' }) toast({ title: 'Leverantör skapad', description: created.name }) diff --git a/app/(dashboard)/invoices/page.tsx b/app/(dashboard)/invoices/page.tsx index 9894da30..00fd8359 100644 --- a/app/(dashboard)/invoices/page.tsx +++ b/app/(dashboard)/invoices/page.tsx @@ -267,7 +267,7 @@ export default function InvoicesPage() { return ( diff --git a/components/dashboard/SandboxBanner.tsx b/components/dashboard/SandboxBanner.tsx index a25db48f..016f1f56 100644 --- a/components/dashboard/SandboxBanner.tsx +++ b/components/dashboard/SandboxBanner.tsx @@ -18,19 +18,19 @@ export function SandboxBanner() { } return ( -
- - Sandlådemiljö — dina data raderas automatiskt efter 24 timmar +
+ + Sandlådemiljö — data raderas efter 24h - - {/* Compact date */} - + {dayNum} {monthStr} - {/* Title */}

{deadline.title}

- {/* Relative date */} - {!completed && relativeDate && ( + {!completed && relative && ( - {relativeDate} + {relative.text} )} + + {completed && ( + + )}
) } + // -- Full card -- return ( -
-
- {/* Date block — calendar page motif */} -
- - {dayNum} - - - {monthStr} - -
+
+
+ {/* Main row */} +
+ {/* Date block */} +
+ + {dayNum} + + + {monthStr} + +
- {/* Content */} -
- {/* Top: title + hover actions */} -
-
- {/* Toggle */} - + {/* Divider */} +
-
-

- {deadline.title} -

- - {/* Meta line */} -
- {deadline.due_time && ( - - kl. {deadline.due_time.slice(0, 5)} - - )} - {deadline.due_time && (deadline.customer || (!completed && relativeDate)) && ( - · - )} - {!completed && relativeDate && ( - - {relativeDate} - - )} - {relativeDate && deadline.customer && !completed && ( - · - )} - {deadline.customer && ( - - {deadline.customer.name} - - )} -
-
+ {/* Content */} +
+
+

+ {deadline.title} +

+ {deadline.deadline_type !== 'other' && ( + + {DEADLINE_TYPE_LABELS[deadline.deadline_type]} + + )}
- {/* Right column: badges + actions */} -
- {/* Badges */} -
- - {DEADLINE_TYPE_LABELS[deadline.deadline_type]} - - {!completed && deadline.priority !== 'normal' && ( - - {PRIORITY_LABELS[deadline.priority]} - - )} - {overdue && !completed && ( - - Förfallen - - )} -
- - {/* Actions */} - {(onEdit || onDelete) && !completed && ( -
-
- {onEdit && ( - - )} - {onDelete && ( - - )} -
+
+ {deadline.due_time && ( + + kl {deadline.due_time.slice(0, 5)} + + )} + {deadline.due_time && deadline.customer && ( + · + )} + {deadline.customer && ( + + {deadline.customer.name} + )}
- {/* Notes */} - {deadline.notes && !completed && ( -

- {deadline.notes} -

+ {/* Right: relative date + action */} +
+ {!completed && relative && ( + + )} + + {completed ? ( + + + Klar + + ) : ( + + )} + + {/* Edit hint */} + {onEdit && !confirming && ( + + )} +
+
+ + {/* Inline confirmation bar */} +
+
+
+

+ Markera {deadline.title} som klar? +

+
+ + +
+
+
diff --git a/components/deadlines/DeadlineForm.tsx b/components/deadlines/DeadlineForm.tsx index 93b5624b..fc543346 100644 --- a/components/deadlines/DeadlineForm.tsx +++ b/components/deadlines/DeadlineForm.tsx @@ -26,6 +26,7 @@ interface DeadlineFormProps { open: boolean onOpenChange: (open: boolean) => void onSubmit: (data: Omit) => Promise + onDelete?: (deadline: Partial) => void initialData?: Partial initialDate?: Date | null customers: { id: string; name: string }[] @@ -35,10 +36,12 @@ export function DeadlineForm({ open, onOpenChange, onSubmit, + onDelete, initialData, initialDate, customers, }: DeadlineFormProps) { + const [confirmDelete, setConfirmDelete] = useState(false) const [isLoading, setIsLoading] = useState(false) const [formData, setFormData] = useState({ title: '', @@ -53,6 +56,7 @@ export function DeadlineForm({ // Reset form when dialog opens with new data useEffect(() => { if (open) { + setConfirmDelete(false) if (initialData) { setFormData({ title: initialData.title || '', @@ -245,17 +249,61 @@ export function DeadlineForm({ />
- - - + + {/* Delete (only when editing an existing deadline) */} + {initialData?.id && onDelete ? ( +
+ {confirmDelete ? ( + <> + Ta bort? + + + + ) : ( + + )} +
+ ) : ( +
+ )} + +
+ + +
diff --git a/components/deadlines/DeadlineList.tsx b/components/deadlines/DeadlineList.tsx index 3a6fb95b..7b6cf4f7 100644 --- a/components/deadlines/DeadlineList.tsx +++ b/components/deadlines/DeadlineList.tsx @@ -7,7 +7,7 @@ import { DeadlineCard } from './DeadlineCard' import { DeadlineFilters } from './DeadlineFilters' import { DeadlineForm } from './DeadlineForm' import { isDeadlineOverdue } from '@/lib/calendar/utils' -import { Plus, Calendar } from 'lucide-react' +import { Plus } from 'lucide-react' interface DeadlineListProps { deadlines: Deadline[] @@ -33,18 +33,13 @@ export function DeadlineList({ const filteredDeadlines = useMemo(() => { return deadlines.filter((d) => { - // Status filter if (statusFilter === 'pending' && d.is_completed) return false if (statusFilter === 'completed' && !d.is_completed) return false - - // Type filter if (typeFilter !== 'all' && d.deadline_type !== typeFilter) return false - return true }) }, [deadlines, statusFilter, typeFilter]) - // Group by overdue, today, upcoming const groupedDeadlines = useMemo(() => { const today = new Date().toISOString().split('T')[0] const overdue: Deadline[] = [] @@ -92,9 +87,16 @@ export function DeadlineList({ setEditingDeadline(null) } + const sections = [ + { key: 'overdue', label: 'Förfallna', items: groupedDeadlines.overdue }, + { key: 'today', label: 'Idag', items: groupedDeadlines.today }, + { key: 'upcoming', label: 'Kommande', items: groupedDeadlines.upcoming }, + { key: 'completed', label: 'Klara', items: groupedDeadlines.completed }, + ].filter(s => s.items.length > 0) + return ( -
- {/* Header */} +
+ {/* Toolbar */}
- {/* Deadline groups */} + {/* Content */} {filteredDeadlines.length === 0 ? ( -
- -

Inga deadlines

-

+

+

{statusFilter !== 'all' || typeFilter !== 'all' - ? 'Inga deadlines matchar dina filter' - : 'Skapa din första deadline för att komma igång'} + ? 'Inga deadlines matchar filtret.' + : 'Inga deadlines ännu.'}

+
) : (
- {/* Overdue */} - {groupedDeadlines.overdue.length > 0 && ( -
-
+ {sections.map(({ key, label, items }) => ( +
+

- Förfallna + {label}

- - {groupedDeadlines.overdue.length} + + {items.length} -
+
-
- {groupedDeadlines.overdue.map((deadline) => ( +
+ {items.map((deadline) => (
- )} - - {/* Today */} - {groupedDeadlines.today.length > 0 && ( -
-
-

- Idag -

- - {groupedDeadlines.today.length} - -
-
-
- {groupedDeadlines.today.map((deadline) => ( - - ))} -
-
- )} - - {/* Upcoming */} - {groupedDeadlines.upcoming.length > 0 && ( -
-
-

- Kommande -

- - {groupedDeadlines.upcoming.length} - -
-
-
- {groupedDeadlines.upcoming.map((deadline) => ( - - ))} -
-
- )} - - {/* Completed */} - {groupedDeadlines.completed.length > 0 && ( -
-
-

- Klara -

- - {groupedDeadlines.completed.length} - -
-
-
- {groupedDeadlines.completed.map((deadline) => ( - - ))} -
-
- )} + ))}
)} - {/* Deadline form dialog */} { + if (deadline.id) { + const full = deadlines.find(d => d.id === deadline.id) + if (full) onDeadlineDelete(full) + } + }} initialData={editingDeadline || undefined} customers={customers} />