Skip to content

Commit 094e16e

Browse files
committed
Decompose recurring and invoice list routes
1 parent 5b013a3 commit 094e16e

11 files changed

Lines changed: 1668 additions & 1986 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
import { fly } from 'svelte/transition';
4+
import Icon from '$lib/components/Icons.svelte';
5+
6+
export let selectedCount = 0;
7+
export let canMarkSent = false;
8+
export let canMarkPaid = false;
9+
10+
const dispatch = createEventDispatcher();
11+
</script>
12+
13+
{#if selectedCount > 0}
14+
<div class="bulk-action-bar" transition:fly={{ y: 50, duration: 200 }}>
15+
<div class="bulk-action-info">
16+
<span class="selection-count">{selectedCount} selected</span>
17+
<button class="btn btn-ghost btn-sm" on:click={() => dispatch('clear')}>
18+
Clear
19+
</button>
20+
</div>
21+
<div class="bulk-action-buttons">
22+
{#if canMarkSent}
23+
<button class="btn btn-secondary btn-sm" on:click={() => dispatch('action', 'mark_sent')} title="Mark selected draft invoices as sent">
24+
<Icon name="send" size="sm" />
25+
<span class="btn-label">Mark Sent</span>
26+
</button>
27+
{/if}
28+
{#if canMarkPaid}
29+
<button class="btn btn-secondary btn-sm" on:click={() => dispatch('action', 'mark_paid')} title="Mark selected sent or overdue invoices as paid">
30+
<Icon name="check" size="sm" />
31+
<span class="btn-label">Mark Paid</span>
32+
</button>
33+
{/if}
34+
<button class="btn btn-danger btn-sm" on:click={() => dispatch('action', 'delete')} title="Delete selected invoices">
35+
<Icon name="trash" size="sm" />
36+
<span class="btn-label">Delete</span>
37+
</button>
38+
</div>
39+
</div>
40+
{/if}
41+
42+
<style>
43+
.bulk-action-bar {
44+
position: fixed;
45+
left: 50%;
46+
bottom: var(--space-6);
47+
transform: translateX(-50%);
48+
display: flex;
49+
align-items: center;
50+
justify-content: space-between;
51+
gap: var(--space-4);
52+
width: min(720px, calc(100% - 2rem));
53+
padding: var(--space-4);
54+
background: color-mix(in srgb, var(--color-bg-elevated) 92%, white);
55+
border: 1px solid var(--color-border);
56+
border-radius: var(--radius-xl);
57+
box-shadow: var(--shadow-lg);
58+
z-index: 30;
59+
backdrop-filter: blur(12px);
60+
}
61+
62+
.bulk-action-info,
63+
.bulk-action-buttons {
64+
display: flex;
65+
align-items: center;
66+
gap: var(--space-3);
67+
flex-wrap: wrap;
68+
}
69+
70+
.selection-count {
71+
font-weight: 600;
72+
color: var(--color-text);
73+
}
74+
75+
@media (max-width: 768px) {
76+
.bulk-action-bar {
77+
flex-direction: column;
78+
align-items: stretch;
79+
}
80+
81+
.bulk-action-buttons {
82+
justify-content: stretch;
83+
}
84+
85+
.bulk-action-buttons :global(.btn) {
86+
flex: 1;
87+
justify-content: center;
88+
}
89+
}
90+
</style>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<script>
2+
import { createEventDispatcher } from 'svelte';
3+
import Icon from '$lib/components/Icons.svelte';
4+
import { formatDate, formatCurrency } from '$lib/stores';
5+
import { getEffectiveStatus, isOverdue } from '$lib/invoices/list';
6+
7+
export let invoices = [];
8+
export let selectedIds = new Set();
9+
export let statusConfig = {};
10+
11+
const dispatch = createEventDispatcher();
12+
</script>
13+
14+
<div class="invoice-cards">
15+
{#each invoices as invoice}
16+
{@const effectiveStatus = getEffectiveStatus(invoice)}
17+
{@const overdue = isOverdue(invoice)}
18+
<div class="invoice-card" class:card-overdue={overdue} class:card-selected={selectedIds.has(invoice.id)}>
19+
<div class="card-checkbox">
20+
<input
21+
type="checkbox"
22+
checked={selectedIds.has(invoice.id)}
23+
on:click|stopPropagation
24+
on:change={() => dispatch('toggleselect', invoice.id)}
25+
aria-label="Select invoice {invoice.invoice_number}"
26+
/>
27+
</div>
28+
<button class="invoice-card-main" on:click={() => dispatch('navigate', invoice.id)}>
29+
<div class="invoice-card-header">
30+
<span class="invoice-card-number font-mono">#{invoice.invoice_number}</span>
31+
<span class="badge {statusConfig[effectiveStatus]?.class || 'badge-draft'}">
32+
{statusConfig[effectiveStatus]?.label || effectiveStatus}
33+
</span>
34+
</div>
35+
<div class="invoice-card-client">{invoice.client_business || invoice.client_name || '---'}</div>
36+
{#if invoice.line_items_count > 0}
37+
<div class="invoice-card-items" title={invoice.line_items_preview}>{invoice.line_items_preview}</div>
38+
{/if}
39+
<div class="invoice-card-footer">
40+
<span class="invoice-card-date" class:text-overdue={overdue}>
41+
{invoice.due_date ? formatDate(invoice.due_date) : formatDate(invoice.issue_date)}
42+
{#if overdue}
43+
<span class="overdue-indicator">overdue</span>
44+
{/if}
45+
</span>
46+
<span class="invoice-card-total">{formatCurrency(invoice.total)}</span>
47+
</div>
48+
</button>
49+
<div class="invoice-card-actions">
50+
{#if invoice.status === 'sent' || invoice.status === 'overdue'}
51+
<button class="btn btn-ghost btn-icon" on:click={() => dispatch('markpaid', invoice.id)} title="Mark as paid">
52+
<Icon name="check" size="sm" />
53+
</button>
54+
{/if}
55+
<button class="btn btn-ghost btn-icon" on:click={() => dispatch('delete', invoice)} title="Delete">
56+
<Icon name="trash" size="sm" />
57+
</button>
58+
</div>
59+
</div>
60+
{/each}
61+
</div>
62+
63+
<style>
64+
.invoice-cards {
65+
display: none;
66+
padding: var(--space-3);
67+
gap: var(--space-3);
68+
}
69+
70+
.invoice-card {
71+
display: grid;
72+
grid-template-columns: auto 1fr auto;
73+
gap: var(--space-3);
74+
align-items: flex-start;
75+
padding: var(--space-4);
76+
background: var(--color-bg-elevated);
77+
border: 1px solid var(--color-border);
78+
border-radius: var(--radius-lg);
79+
}
80+
81+
.card-overdue {
82+
border-color: color-mix(in srgb, var(--color-danger) 35%, var(--color-border));
83+
background: color-mix(in srgb, var(--color-danger-light) 30%, var(--color-bg-elevated));
84+
}
85+
86+
.card-selected {
87+
border-color: var(--color-primary);
88+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 20%, transparent);
89+
}
90+
91+
.invoice-card-main {
92+
border: 0;
93+
background: none;
94+
padding: 0;
95+
text-align: left;
96+
min-width: 0;
97+
}
98+
99+
.invoice-card-header,
100+
.invoice-card-footer {
101+
display: flex;
102+
justify-content: space-between;
103+
gap: var(--space-3);
104+
align-items: center;
105+
}
106+
107+
.invoice-card-header {
108+
margin-bottom: var(--space-2);
109+
}
110+
111+
.invoice-card-number,
112+
.invoice-card-total {
113+
font-weight: 600;
114+
}
115+
116+
.invoice-card-client {
117+
color: var(--color-text);
118+
font-weight: 500;
119+
margin-bottom: var(--space-1);
120+
}
121+
122+
.invoice-card-items {
123+
color: var(--color-text-secondary);
124+
font-size: 0.875rem;
125+
overflow: hidden;
126+
text-overflow: ellipsis;
127+
white-space: nowrap;
128+
margin-bottom: var(--space-2);
129+
}
130+
131+
.invoice-card-date {
132+
color: var(--color-text-secondary);
133+
font-size: 0.875rem;
134+
}
135+
136+
.invoice-card-actions {
137+
display: flex;
138+
flex-direction: column;
139+
gap: var(--space-1);
140+
}
141+
142+
.overdue-indicator {
143+
display: inline-block;
144+
margin-left: var(--space-2);
145+
padding: 0.125rem 0.375rem;
146+
font-size: 0.625rem;
147+
font-weight: 600;
148+
text-transform: uppercase;
149+
letter-spacing: 0.025em;
150+
color: var(--color-danger);
151+
background-color: var(--color-danger-light);
152+
border-radius: var(--radius-sm);
153+
}
154+
155+
.text-overdue {
156+
color: var(--color-danger);
157+
font-weight: 500;
158+
}
159+
160+
@media (max-width: 768px) {
161+
.invoice-cards {
162+
display: grid;
163+
}
164+
}
165+
</style>

0 commit comments

Comments
 (0)