main
tsx 311 lines 10.1 KB
Raw
1 import { useState, useEffect } from "react";
2 import { useNavigate } from "react-router-dom";
3 import Layout from "@/components/Layout";
4 import { Card } from "@/components/ui/card";
5 import { Button } from "@/components/ui/button";
6 import { Input } from "@/components/ui/input";
7 import { Label } from "@/components/ui/label";
8 import {
9 Dialog,
10 DialogContent,
11 DialogDescription,
12 DialogFooter,
13 DialogHeader,
14 DialogTitle,
15 } from "@/components/ui/dialog";
16 import {
17 AlertDialog,
18 AlertDialogAction,
19 AlertDialogCancel,
20 AlertDialogContent,
21 AlertDialogDescription,
22 AlertDialogFooter,
23 AlertDialogHeader,
24 AlertDialogTitle,
25 } from "@/components/ui/alert-dialog";
26 import { Plus, Pencil, Trash2, Send } from "lucide-react";
27 import { transactionService } from "@/services/transactionService";
28 import { Recipient } from "@/types/transaction";
29 import { toast } from "@/hooks/use-toast";
30
31 const Recipients = () => {
32 const navigate = useNavigate();
33 const [recipients, setRecipients] = useState<Recipient[]>([]);
34 const [loading, setLoading] = useState(true);
35 const [dialogOpen, setDialogOpen] = useState(false);
36 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
37 const [editingRecipient, setEditingRecipient] = useState<Recipient | null>(null);
38 const [deletingRecipient, setDeletingRecipient] = useState<Recipient | null>(null);
39 const [name, setName] = useState("");
40 const [email, setEmail] = useState("");
41 const [saving, setSaving] = useState(false);
42
43 useEffect(() => {
44 loadRecipients();
45 }, []);
46
47 const loadRecipients = async () => {
48 setLoading(true);
49 const data = await transactionService.getRecipients();
50 setRecipients(data);
51 setLoading(false);
52 };
53
54 const handleOpenDialog = (recipient?: Recipient) => {
55 if (recipient) {
56 setEditingRecipient(recipient);
57 setName(recipient.name);
58 setEmail(recipient.email);
59 } else {
60 setEditingRecipient(null);
61 setName("");
62 setEmail("");
63 }
64 setDialogOpen(true);
65 };
66
67 const handleCloseDialog = () => {
68 setDialogOpen(false);
69 setEditingRecipient(null);
70 setName("");
71 setEmail("");
72 };
73
74 const handleSave = async () => {
75 if (!name.trim() || !email.trim()) {
76 toast({
77 title: "Missing Information",
78 description: "Please fill in all fields",
79 variant: "destructive",
80 });
81 return;
82 }
83
84 // Basic email validation
85 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
86 if (!emailRegex.test(email)) {
87 toast({
88 title: "Invalid Email",
89 description: "Please enter a valid email address",
90 variant: "destructive",
91 });
92 return;
93 }
94
95 setSaving(true);
96 try {
97 if (editingRecipient) {
98 await transactionService.updateRecipient(editingRecipient.id, name, email);
99 toast({
100 title: "Recipient Updated",
101 description: `${name} has been updated successfully`,
102 });
103 } else {
104 await transactionService.addRecipient(name, email);
105 toast({
106 title: "Recipient Added",
107 description: `${name} has been added to your recipients`,
108 });
109 }
110 await loadRecipients();
111 handleCloseDialog();
112 } catch (error) {
113 toast({
114 title: "Error",
115 description: "Failed to save recipient. Please try again.",
116 variant: "destructive",
117 });
118 } finally {
119 setSaving(false);
120 }
121 };
122
123 const handleDelete = async () => {
124 if (!deletingRecipient) return;
125
126 try {
127 await transactionService.deleteRecipient(deletingRecipient.id);
128 toast({
129 title: "Recipient Deleted",
130 description: `${deletingRecipient.name} has been removed`,
131 });
132 await loadRecipients();
133 setDeleteDialogOpen(false);
134 setDeletingRecipient(null);
135 } catch (error) {
136 toast({
137 title: "Error",
138 description: "Failed to delete recipient. Please try again.",
139 variant: "destructive",
140 });
141 }
142 };
143
144 const openDeleteDialog = (recipient: Recipient) => {
145 setDeletingRecipient(recipient);
146 setDeleteDialogOpen(true);
147 };
148
149 return (
150 <Layout title="Recipients">
151 <div className="p-4 space-y-6">
152 {/* Header */}
153 <div className="flex items-center justify-between">
154 <div>
155 <h2 className="text-2xl font-bold">Payment Recipients</h2>
156 <p className="text-sm text-muted-foreground font-medium mt-1">
157 Manage your frequent payment contacts
158 </p>
159 </div>
160 <Button onClick={() => handleOpenDialog()}>
161 <Plus className="h-5 w-5 mr-2" />
162 Add
163 </Button>
164 </div>
165
166 {/* Recipients List */}
167 {loading ? (
168 <Card className="p-8 border-0 shadow-md text-center">
169 <p className="text-muted-foreground">Loading recipients...</p>
170 </Card>
171 ) : recipients.length === 0 ? (
172 <Card className="p-12 border-0 shadow-md text-center space-y-4">
173 <div className="h-20 w-20 rounded-full bg-muted mx-auto flex items-center justify-center">
174 <Plus className="h-10 w-10 text-muted-foreground" />
175 </div>
176 <div>
177 <h3 className="text-lg font-bold mb-2">No Recipients Yet</h3>
178 <p className="text-sm text-muted-foreground font-medium mb-4">
179 Add your first recipient to get started
180 </p>
181 <Button onClick={() => handleOpenDialog()}>
182 <Plus className="h-5 w-5 mr-2" />
183 Add Recipient
184 </Button>
185 </div>
186 </Card>
187 ) : (
188 <div className="space-y-3">
189 {recipients.map((recipient) => (
190 <Card
191 key={recipient.id}
192 className="p-5 border-0 shadow-md hover:shadow-lg transition-all"
193 >
194 <div className="flex items-center gap-4">
195 <div className="h-14 w-14 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
196 <span className="text-xl font-bold text-primary">
197 {recipient.name.charAt(0).toUpperCase()}
198 </span>
199 </div>
200 <div className="flex-1 min-w-0">
201 <p className="font-semibold text-base truncate">{recipient.name}</p>
202 <p className="text-sm text-muted-foreground font-medium truncate">
203 {recipient.email}
204 </p>
205 </div>
206 <div className="flex items-center gap-2 flex-shrink-0">
207 <Button
208 size="icon"
209 variant="ghost"
210 onClick={() => navigate("/send", { state: { recipient } })}
211 className="h-10 w-10"
212 >
213 <Send className="h-5 w-5" />
214 </Button>
215 <Button
216 size="icon"
217 variant="ghost"
218 onClick={() => handleOpenDialog(recipient)}
219 className="h-10 w-10"
220 >
221 <Pencil className="h-5 w-5" />
222 </Button>
223 <Button
224 size="icon"
225 variant="ghost"
226 onClick={() => openDeleteDialog(recipient)}
227 className="h-10 w-10 text-destructive hover:text-destructive"
228 >
229 <Trash2 className="h-5 w-5" />
230 </Button>
231 </div>
232 </div>
233 </Card>
234 ))}
235 </div>
236 )}
237 </div>
238
239 {/* Add/Edit Dialog */}
240 <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
241 <DialogContent>
242 <DialogHeader>
243 <DialogTitle>
244 {editingRecipient ? "Edit Recipient" : "Add Recipient"}
245 </DialogTitle>
246 <DialogDescription>
247 {editingRecipient
248 ? "Update the recipient's information"
249 : "Add a new recipient to your contacts"}
250 </DialogDescription>
251 </DialogHeader>
252 <div className="space-y-4 py-4">
253 <div className="space-y-2">
254 <Label htmlFor="name">Name</Label>
255 <Input
256 id="name"
257 placeholder="John Doe"
258 value={name}
259 onChange={(e) => setName(e.target.value)}
260 />
261 </div>
262 <div className="space-y-2">
263 <Label htmlFor="email">Email Address</Label>
264 <Input
265 id="email"
266 type="email"
267 placeholder="john@example.com"
268 value={email}
269 onChange={(e) => setEmail(e.target.value)}
270 />
271 </div>
272 </div>
273 <DialogFooter>
274 <Button variant="outline" onClick={handleCloseDialog}>
275 Cancel
276 </Button>
277 <Button onClick={handleSave} disabled={saving}>
278 {saving ? "Saving..." : editingRecipient ? "Update" : "Add"}
279 </Button>
280 </DialogFooter>
281 </DialogContent>
282 </Dialog>
283
284 {/* Delete Confirmation Dialog */}
285 <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
286 <AlertDialogContent>
287 <AlertDialogHeader>
288 <AlertDialogTitle>Delete Recipient?</AlertDialogTitle>
289 <AlertDialogDescription>
290 Are you sure you want to delete {deletingRecipient?.name}? This action
291 cannot be undone.
292 </AlertDialogDescription>
293 </AlertDialogHeader>
294 <AlertDialogFooter>
295 <AlertDialogCancel onClick={() => setDeletingRecipient(null)}>
296 Cancel
297 </AlertDialogCancel>
298 <AlertDialogAction
299 onClick={handleDelete}
300 className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
301 >
302 Delete
303 </AlertDialogAction>
304 </AlertDialogFooter>
305 </AlertDialogContent>
306 </AlertDialog>
307 </Layout>
308 );
309 };
310
311 export default Recipients;