main
tsx 323 lines 10.2 KB
Raw
1 import { useState, useEffect } from "react";
2 import { useNavigate } from "react-router-dom";
3 import { Card } from "@/components/ui/card";
4 import { Button } from "@/components/ui/button";
5 import { Input } from "@/components/ui/input";
6 import { Label } from "@/components/ui/label";
7 import { useLang } from "@/context/LanguageContext";
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 { t, lang } = useLang();
34 const isRTL = lang === "fa";
35 const [recipients, setRecipients] = useState<Recipient[]>([]);
36 const [loading, setLoading] = useState(true);
37 const [dialogOpen, setDialogOpen] = useState(false);
38 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
39 const [editingRecipient, setEditingRecipient] = useState<Recipient | null>(
40 null,
41 );
42 const [deletingRecipient, setDeletingRecipient] = useState<Recipient | null>(
43 null,
44 );
45 const [name, setName] = useState("");
46 const [email, setEmail] = useState("");
47 const [saving, setSaving] = useState(false);
48
49 useEffect(() => {
50 loadRecipients();
51 }, []);
52
53 const loadRecipients = async () => {
54 setLoading(true);
55 const data = await transactionService.getRecipients();
56 setRecipients(data);
57 setLoading(false);
58 };
59
60 const handleOpenDialog = (recipient?: Recipient) => {
61 if (recipient) {
62 setEditingRecipient(recipient);
63 setName(recipient.name);
64 setEmail(recipient.email);
65 } else {
66 setEditingRecipient(null);
67 setName("");
68 setEmail("");
69 }
70 setDialogOpen(true);
71 };
72
73 const handleCloseDialog = () => {
74 setDialogOpen(false);
75 setEditingRecipient(null);
76 setName("");
77 setEmail("");
78 };
79
80 const handleSave = async () => {
81 if (!name.trim() || !email.trim()) {
82 toast({
83 title: t.missingInfo,
84 description: t.fillAllFields,
85 variant: "destructive",
86 });
87 return;
88 }
89
90 // Basic email validation
91 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
92 if (!emailRegex.test(email)) {
93 toast({
94 title: t.invalidEmail,
95 description: t.validEmailAddress,
96 variant: "destructive",
97 });
98 return;
99 }
100
101 setSaving(true);
102 try {
103 if (editingRecipient) {
104 await transactionService.updateRecipient(
105 editingRecipient.id,
106 name,
107 email,
108 );
109 toast({
110 title: t.recipientUpdated,
111 description: t.recipientUpdatedDesc(name),
112 });
113 } else {
114 await transactionService.addRecipient(name, email);
115 toast({
116 title: t.recipientAdded,
117 description: t.recipientAddedDesc(name),
118 });
119 }
120 await loadRecipients();
121 handleCloseDialog();
122 } catch (error) {
123 toast({
124 title: t.error,
125 description: t.failedSaveRecipient,
126 variant: "destructive",
127 });
128 } finally {
129 setSaving(false);
130 }
131 };
132
133 const handleDelete = async () => {
134 if (!deletingRecipient) return;
135
136 try {
137 await transactionService.deleteRecipient(deletingRecipient.id);
138 toast({
139 title: t.recipientDeleted,
140 description: t.recipientDeletedDesc(deletingRecipient.name),
141 });
142 await loadRecipients();
143 setDeleteDialogOpen(false);
144 setDeletingRecipient(null);
145 } catch (error) {
146 toast({
147 title: t.error,
148 description: t.failedDeleteRecipient,
149 variant: "destructive",
150 });
151 }
152 };
153
154 const openDeleteDialog = (recipient: Recipient) => {
155 setDeletingRecipient(recipient);
156 setDeleteDialogOpen(true);
157 };
158
159 return (
160 <>
161 <div className="p-4 space-y-6" dir={isRTL ? "rtl" : "ltr"}>
162 {/* Header */}
163 <div className="flex items-center justify-between">
164 <div>
165 <h2 className="text-2xl font-bold">{t.paymentRecipients}</h2>
166 <p className="text-sm text-muted-foreground font-medium mt-1">
167 {t.manageContacts}
168 </p>
169 </div>
170 <Button onClick={() => handleOpenDialog()}>
171 <Plus className={`h-5 w-5 ${isRTL ? "ml-2" : "mr-2"}`} />
172 {t.add}
173 </Button>
174 </div>
175
176 {/* Recipients List */}
177 {loading ? (
178 <Card className="p-8 border-0 shadow-md text-center">
179 <p className="text-muted-foreground">{t.loadingRecipients}</p>
180 </Card>
181 ) : recipients.length === 0 ? (
182 <Card className="p-12 border-0 shadow-md text-center space-y-4">
183 <div className="h-20 w-20 rounded-full bg-muted mx-auto flex items-center justify-center">
184 <Plus className="h-10 w-10 text-muted-foreground" />
185 </div>
186 <div>
187 <h3 className="text-lg font-bold mb-2">{t.noRecipientsYet}</h3>
188 <p className="text-sm text-muted-foreground font-medium mb-4">
189 {t.addFirstRecipient}
190 </p>
191 <Button onClick={() => handleOpenDialog()}>
192 <Plus className={`h-5 w-5 ${isRTL ? "ml-2" : "mr-2"}`} />
193 {t.addRecipient}
194 </Button>
195 </div>
196 </Card>
197 ) : (
198 <div className="space-y-3">
199 {recipients.map((recipient) => (
200 <Card
201 key={recipient.id}
202 className="p-5 border-0 shadow-md hover:shadow-lg transition-all"
203 >
204 <div className="flex items-center gap-4">
205 <div className="h-14 w-14 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
206 <span className="text-xl font-bold text-primary">
207 {recipient.name.charAt(0).toUpperCase()}
208 </span>
209 </div>
210 <div className="flex-1 min-w-0">
211 <p className="font-semibold text-base truncate">
212 {recipient.name}
213 </p>
214 <p className="text-sm text-muted-foreground font-medium truncate">
215 {recipient.email}
216 </p>
217 </div>
218 <div className="flex items-center gap-2 flex-shrink-0">
219 <Button
220 size="icon"
221 variant="ghost"
222 onClick={() =>
223 navigate("/send", { state: { recipient } })
224 }
225 className="h-10 w-10"
226 >
227 <Send className="h-5 w-5" />
228 </Button>
229 <Button
230 size="icon"
231 variant="ghost"
232 onClick={() => handleOpenDialog(recipient)}
233 className="h-10 w-10"
234 >
235 <Pencil className="h-5 w-5" />
236 </Button>
237 <Button
238 size="icon"
239 variant="ghost"
240 onClick={() => openDeleteDialog(recipient)}
241 className="h-10 w-10 text-destructive hover:text-destructive"
242 >
243 <Trash2 className="h-5 w-5" />
244 </Button>
245 </div>
246 </div>
247 </Card>
248 ))}
249 </div>
250 )}
251 </div>
252
253 {/* Add/Edit Dialog */}
254 <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
255 <DialogContent>
256 <DialogHeader>
257 <DialogTitle>
258 {editingRecipient ? t.editRecipient : t.addRecipient}
259 </DialogTitle>
260 <DialogDescription>
261 {editingRecipient ? t.updateRecipientInfo : t.addNewRecipient}
262 </DialogDescription>
263 </DialogHeader>
264 <div className="space-y-4 py-4">
265 <div className="space-y-2">
266 <Label htmlFor="name">{t.name}</Label>
267 <Input
268 id="name"
269 placeholder="John Doe"
270 value={name}
271 onChange={(e) => setName(e.target.value)}
272 />
273 </div>
274 <div className="space-y-2">
275 <Label htmlFor="email">{t.emailAddress}</Label>
276 <Input
277 id="email"
278 type="email"
279 placeholder="john@example.com"
280 value={email}
281 onChange={(e) => setEmail(e.target.value)}
282 />
283 </div>
284 </div>
285 <DialogFooter>
286 <Button variant="outline" onClick={handleCloseDialog}>
287 {t.cancel}
288 </Button>
289 <Button onClick={handleSave} disabled={saving}>
290 {saving ? t.saving : editingRecipient ? t.update : t.add}
291 </Button>
292 </DialogFooter>
293 </DialogContent>
294 </Dialog>
295
296 {/* Delete Confirmation Dialog */}
297 <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
298 <AlertDialogContent>
299 <AlertDialogHeader>
300 <AlertDialogTitle>{t.deleteRecipientTitle}</AlertDialogTitle>
301 <AlertDialogDescription>
302 {deletingRecipient &&
303 t.deleteRecipientDesc(deletingRecipient.name)}
304 </AlertDialogDescription>
305 </AlertDialogHeader>
306 <AlertDialogFooter>
307 <AlertDialogCancel onClick={() => setDeletingRecipient(null)}>
308 {t.cancel}
309 </AlertDialogCancel>
310 <AlertDialogAction
311 onClick={handleDelete}
312 className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
313 >
314 {t.delete}
315 </AlertDialogAction>
316 </AlertDialogFooter>
317 </AlertDialogContent>
318 </AlertDialog>
319 </>
320 );
321 };
322
323 export default Recipients;