Skip to content

Commit 73b1eaa

Browse files
committed
feat: add Clients tab to /invoices with inline CRUD
- Invoices page now has Invoices/Clients tabs - Clients tab supports create, edit, delete inline - /clients and /clients/create redirect to /invoices?tab=clients - URL updates with ?tab=clients for direct linking
1 parent 7b75c06 commit 73b1eaa

File tree

3 files changed

+340
-353
lines changed

3 files changed

+340
-353
lines changed

src/app/clients/create/page.tsx

Lines changed: 3 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,5 @@
1-
'use client';
1+
import { redirect } from 'next/navigation';
22

3-
import { useState, useEffect } from 'react';
4-
import { useRouter } from 'next/navigation';
5-
import Link from 'next/link';
6-
import { authFetch } from '@/lib/auth/client';
7-
8-
interface Business {
9-
id: string;
10-
name: string;
11-
}
12-
13-
export default function CreateClientPage() {
14-
const router = useRouter();
15-
const [businesses, setBusinesses] = useState<Business[]>([]);
16-
const [loading, setLoading] = useState(true);
17-
const [saving, setSaving] = useState(false);
18-
const [error, setError] = useState('');
19-
const [form, setForm] = useState({
20-
business_id: '',
21-
email: '',
22-
name: '',
23-
company_name: '',
24-
phone: '',
25-
address: '',
26-
website: '',
27-
});
28-
29-
useEffect(() => {
30-
const fetch = async () => {
31-
const result = await authFetch('/api/businesses', {}, router);
32-
if (result?.data.success) {
33-
setBusinesses(result.data.businesses);
34-
if (result.data.businesses.length === 1) {
35-
setForm(f => ({ ...f, business_id: result.data.businesses[0].id }));
36-
}
37-
}
38-
setLoading(false);
39-
};
40-
fetch();
41-
}, []);
42-
43-
const handleSubmit = async (e: React.FormEvent) => {
44-
e.preventDefault();
45-
setSaving(true);
46-
setError('');
47-
48-
const result = await authFetch('/api/clients', {
49-
method: 'POST',
50-
headers: { 'Content-Type': 'application/json' },
51-
body: JSON.stringify(form),
52-
}, router);
53-
54-
if (result?.data.success) {
55-
router.push('/clients');
56-
} else {
57-
setError(result?.data.error || 'Failed to create client');
58-
setSaving(false);
59-
}
60-
};
61-
62-
if (loading) {
63-
return (
64-
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
65-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-400"></div>
66-
</div>
67-
);
68-
}
69-
70-
return (
71-
<div className="min-h-screen bg-gray-900 py-8 px-4 sm:px-6 lg:px-8">
72-
<div className="max-w-lg mx-auto">
73-
<Link href="/clients" className="text-purple-400 hover:text-purple-300 text-sm mb-4 inline-block">← Back to Clients</Link>
74-
<h1 className="text-3xl font-bold text-white mb-8">Add Client</h1>
75-
76-
{error && (
77-
<div className="mb-6 bg-red-500/10 border border-red-500/30 text-red-400 px-4 py-3 rounded-lg">{error}</div>
78-
)}
79-
80-
<form onSubmit={handleSubmit} className="bg-gray-800/50 rounded-2xl border border-gray-700 p-6 space-y-4">
81-
<div>
82-
<label className="block text-sm font-medium text-gray-300 mb-2">Business *</label>
83-
<select
84-
required
85-
value={form.business_id}
86-
onChange={e => setForm({ ...form, business_id: e.target.value })}
87-
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white"
88-
>
89-
<option value="">Select business</option>
90-
{businesses.map(b => <option key={b.id} value={b.id}>{b.name}</option>)}
91-
</select>
92-
</div>
93-
<div>
94-
<label className="block text-sm font-medium text-gray-300 mb-2">Email *</label>
95-
<input type="email" required value={form.email} onChange={e => setForm({ ...form, email: e.target.value })}
96-
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white" placeholder="client@example.com" />
97-
</div>
98-
<div>
99-
<label className="block text-sm font-medium text-gray-300 mb-2">Name</label>
100-
<input type="text" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })}
101-
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white" placeholder="John Doe" />
102-
</div>
103-
<div>
104-
<label className="block text-sm font-medium text-gray-300 mb-2">Company</label>
105-
<input type="text" value={form.company_name} onChange={e => setForm({ ...form, company_name: e.target.value })}
106-
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white" placeholder="Acme Corp" />
107-
</div>
108-
<div>
109-
<label className="block text-sm font-medium text-gray-300 mb-2">Phone</label>
110-
<input type="tel" value={form.phone} onChange={e => setForm({ ...form, phone: e.target.value })}
111-
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white" />
112-
</div>
113-
<div>
114-
<label className="block text-sm font-medium text-gray-300 mb-2">Address</label>
115-
<textarea value={form.address} onChange={e => setForm({ ...form, address: e.target.value })}
116-
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white" rows={2} />
117-
</div>
118-
<div>
119-
<label className="block text-sm font-medium text-gray-300 mb-2">Website</label>
120-
<input type="url" value={form.website} onChange={e => setForm({ ...form, website: e.target.value })}
121-
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white" placeholder="https://example.com" />
122-
</div>
123-
124-
<div className="flex justify-end gap-3 pt-4">
125-
<Link href="/clients" className="px-4 py-2 text-gray-400 hover:text-white">Cancel</Link>
126-
<button type="submit" disabled={saving}
127-
className="px-6 py-2 bg-purple-600 hover:bg-purple-500 text-white font-medium rounded-lg disabled:opacity-50">
128-
{saving ? 'Saving...' : 'Add Client'}
129-
</button>
130-
</div>
131-
</form>
132-
</div>
133-
</div>
134-
);
3+
export default function CreateClientRedirect() {
4+
redirect('/invoices?tab=clients');
1355
}

src/app/clients/page.tsx

Lines changed: 3 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,5 @@
1-
'use client';
1+
import { redirect } from 'next/navigation';
22

3-
import { useState, useEffect } from 'react';
4-
import { useRouter } from 'next/navigation';
5-
import Link from 'next/link';
6-
import { authFetch } from '@/lib/auth/client';
7-
8-
interface Client {
9-
id: string;
10-
name: string | null;
11-
email: string;
12-
phone: string | null;
13-
company_name: string | null;
14-
business_id: string;
15-
created_at: string;
16-
}
17-
18-
export default function ClientsPage() {
19-
const router = useRouter();
20-
const [clients, setClients] = useState<Client[]>([]);
21-
const [loading, setLoading] = useState(true);
22-
const [error, setError] = useState('');
23-
const [deleting, setDeleting] = useState<string | null>(null);
24-
25-
useEffect(() => { fetchClients(); }, []);
26-
27-
const fetchClients = async () => {
28-
const result = await authFetch('/api/clients', {}, router);
29-
if (!result) return;
30-
if (result.data.success) setClients(result.data.clients);
31-
else setError(result.data.error || 'Failed to load clients');
32-
setLoading(false);
33-
};
34-
35-
const handleDelete = async (id: string) => {
36-
if (!confirm('Delete this client?')) return;
37-
setDeleting(id);
38-
const result = await authFetch(`/api/clients/${id}`, { method: 'DELETE' }, router);
39-
if (result?.data.success) {
40-
setClients(c => c.filter(cl => cl.id !== id));
41-
} else {
42-
setError(result?.data.error || 'Failed to delete');
43-
}
44-
setDeleting(null);
45-
};
46-
47-
if (loading) {
48-
return (
49-
<div className="min-h-screen bg-gray-900 flex items-center justify-center">
50-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-400"></div>
51-
</div>
52-
);
53-
}
54-
55-
return (
56-
<div className="min-h-screen bg-gray-900 py-8 px-4 sm:px-6 lg:px-8">
57-
<div className="max-w-7xl mx-auto">
58-
<div className="mb-8 flex items-center justify-between">
59-
<div>
60-
<h1 className="text-3xl font-bold text-white">Clients</h1>
61-
<p className="mt-2 text-gray-400">Manage your invoice clients</p>
62-
</div>
63-
<Link
64-
href="/clients/create"
65-
className="inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white text-sm font-medium rounded-lg transition-colors"
66-
>
67-
+ Add Client
68-
</Link>
69-
</div>
70-
71-
{error && (
72-
<div className="mb-6 bg-red-500/10 border border-red-500/30 text-red-400 px-4 py-3 rounded-lg">{error}</div>
73-
)}
74-
75-
{clients.length === 0 ? (
76-
<div className="bg-gray-800/50 rounded-2xl p-12 text-center border border-gray-700">
77-
<h3 className="text-lg font-medium text-white">No clients yet</h3>
78-
<p className="mt-2 text-gray-400">Add your first client to start invoicing.</p>
79-
<Link href="/clients/create" className="mt-4 inline-block px-4 py-2 bg-purple-600 text-white rounded-lg">
80-
Add Client
81-
</Link>
82-
</div>
83-
) : (
84-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
85-
{clients.map(client => (
86-
<div key={client.id} className="bg-gray-800/50 rounded-xl border border-gray-700 p-5 hover:border-gray-600 transition-colors">
87-
<div className="flex items-start justify-between">
88-
<div>
89-
<h3 className="text-white font-medium">{client.company_name || client.name || 'Unnamed'}</h3>
90-
<p className="text-gray-400 text-sm">{client.email}</p>
91-
{client.phone && <p className="text-gray-500 text-xs mt-1">{client.phone}</p>}
92-
</div>
93-
<button
94-
onClick={() => handleDelete(client.id)}
95-
disabled={deleting === client.id}
96-
className="text-gray-500 hover:text-red-400 text-sm"
97-
>
98-
{deleting === client.id ? '...' : '×'}
99-
</button>
100-
</div>
101-
<p className="text-xs text-gray-600 mt-3">Added {new Date(client.created_at).toLocaleDateString()}</p>
102-
</div>
103-
))}
104-
</div>
105-
)}
106-
</div>
107-
</div>
108-
);
3+
export default function ClientsRedirect() {
4+
redirect('/invoices?tab=clients');
1095
}

0 commit comments

Comments
 (0)