EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
CustomerMergeModal.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { apiFetch } from '../lib/api';
3import { notifySuccess, notifyError } from '../utils/notifications';
4
5export default function CustomerMergeModal({ isOpen, onClose, onMergeComplete, customers }) {
6 const [customer1Id, setCustomer1Id] = useState('');
7 const [customer2Id, setCustomer2Id] = useState('');
8 const [comparisonData, setComparisonData] = useState(null);
9 const [primaryCustomerId, setPrimaryCustomerId] = useState(null);
10 const [loading, setLoading] = useState(false);
11 const [error, setError] = useState('');
12
13 useEffect(() => {
14 // Reset state when modal opens/closes
15 if (isOpen) {
16 setCustomer1Id('');
17 setCustomer2Id('');
18 setComparisonData(null);
19 setPrimaryCustomerId(null);
20 setError('');
21 }
22 }, [isOpen]);
23
24 const handleCompare = async () => {
25 if (!customer1Id || !customer2Id) {
26 setError('Please select both customers to compare');
27 return;
28 }
29
30 if (customer1Id === customer2Id) {
31 setError('Please select two different customers');
32 return;
33 }
34
35 setLoading(true);
36 setError('');
37
38 try {
39 const token = localStorage.getItem('token');
40 const response = await apiFetch(`/customers/merge/compare/${customer1Id}/${customer2Id}`, {
41 headers: { Authorization: `Bearer ${token}` }
42 });
43 const data = await response.json();
44 setComparisonData(data);
45 // Default to customer with more data as primary
46 const c1Total = data.customer1.tickets_count +
47 data.customer1.invoices_count +
48 data.customer1.contracts_count;
49 const c2Total = data.customer2.tickets_count +
50 data.customer2.invoices_count +
51 data.customer2.contracts_count;
52 setPrimaryCustomerId(c1Total >= c2Total ? data.customer1.customer_id : data.customer2.customer_id);
53 } catch (err) {
54 const errorData = await err.json?.() || {};
55 setError(errorData.error || 'Failed to compare customers');
56 console.error('Error comparing customers:', err);
57 } finally {
58 setLoading(false);
59 }
60 };
61
62 const handleMerge = async () => {
63 if (!primaryCustomerId || !comparisonData) {
64 setError('Please select which customer to keep');
65 return;
66 }
67
68 const secondaryCustomerId = primaryCustomerId === comparisonData.customer1.customer_id
69 ? comparisonData.customer2.customer_id
70 : comparisonData.customer1.customer_id;
71
72 const confirmMessage = `Are you sure you want to merge these customers?\n\n` +
73 `This will:\n` +
74 `- Keep customer: ${comparisonData[primaryCustomerId === comparisonData.customer1.customer_id ? 'customer1' : 'customer2'].name}\n` +
75 `- Move all data from the other customer\n` +
76 `- Delete the other customer\n\n` +
77 `This action cannot be undone.`;
78
79 if (!confirm(confirmMessage)) {
80 return;
81 }
82
83 setLoading(true);
84 setError('');
85
86 try {
87 const token = localStorage.getItem('token');
88 await apiFetch('/customers/merge', {
89 method: 'POST',
90 headers: {
91 Authorization: `Bearer ${token}`,
92 'Content-Type': 'application/json'
93 },
94 body: JSON.stringify({
95 primary_customer_id: primaryCustomerId,
96 secondary_customer_id: secondaryCustomerId
97 })
98 });
99
100 await notifySuccess('Merge Successful', 'Customers have been merged successfully');
101 onMergeComplete();
102 onClose();
103 } catch (err) {
104 const errorData = await err.json?.() || {};
105 const errorMsg = errorData.error || 'Failed to merge customers';
106 await notifyError('Merge Failed', errorMsg);
107 setError(errorMsg);
108 console.error('Error merging customers:', err);
109 } finally {
110 setLoading(false);
111 }
112 };
113
114 if (!isOpen) return null;
115
116 return (
117 <div style={{
118 position: 'fixed',
119 top: 0,
120 left: 0,
121 right: 0,
122 bottom: 0,
123 backgroundColor: 'rgba(0, 0, 0, 0.5)',
124 display: 'flex',
125 alignItems: 'center',
126 justifyContent: 'center',
127 zIndex: 1000
128 }}>
129 <div style={{
130 backgroundColor: 'var(--surface)',
131 borderRadius: '8px',
132 padding: '24px',
133 maxWidth: '900px',
134 width: '90%',
135 maxHeight: '90vh',
136 overflow: 'auto',
137 border: '1px solid var(--border)'
138 }}>
139 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
140 <h2 style={{ margin: 0, color: 'var(--text)' }}>Merge Customers</h2>
141 <button
142 onClick={onClose}
143 style={{
144 background: 'none',
145 border: 'none',
146 fontSize: '24px',
147 cursor: 'pointer',
148 color: 'var(--text-secondary)',
149 padding: '0 8px'
150 }}
151 >
152 ×
153 </button>
154 </div>
155
156 {/* Customer Selection */}
157 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '20px' }}>
158 <div>
159 <label style={{ display: 'block', marginBottom: '8px', color: 'var(--text)', fontWeight: 500 }}>
160 First Customer
161 </label>
162 <select
163 value={customer1Id}
164 onChange={(e) => setCustomer1Id(e.target.value)}
165 disabled={loading}
166 style={{
167 width: '100%',
168 padding: '10px',
169 borderRadius: '4px',
170 border: '1px solid var(--border)',
171 backgroundColor: 'var(--background)',
172 color: 'var(--text)',
173 fontSize: '14px'
174 }}
175 >
176 <option value="">Select customer...</option>
177 {customers.map(c => (
178 <option key={c.customer_id} value={c.customer_id}>
179 {c.name}
180 </option>
181 ))}
182 </select>
183 </div>
184
185 <div>
186 <label style={{ display: 'block', marginBottom: '8px', color: 'var(--text)', fontWeight: 500 }}>
187 Second Customer
188 </label>
189 <select
190 value={customer2Id}
191 onChange={(e) => setCustomer2Id(e.target.value)}
192 disabled={loading}
193 style={{
194 width: '100%',
195 padding: '10px',
196 borderRadius: '4px',
197 border: '1px solid var(--border)',
198 backgroundColor: 'var(--background)',
199 color: 'var(--text)',
200 fontSize: '14px'
201 }}
202 >
203 <option value="">Select customer...</option>
204 {customers.map(c => (
205 <option key={c.customer_id} value={c.customer_id}>
206 {c.name}
207 </option>
208 ))}
209 </select>
210 </div>
211 </div>
212
213 <button
214 onClick={handleCompare}
215 disabled={loading || !customer1Id || !customer2Id}
216 style={{
217 padding: '10px 20px',
218 backgroundColor: 'var(--primary)',
219 color: 'white',
220 border: 'none',
221 borderRadius: '4px',
222 cursor: loading ? 'wait' : 'pointer',
223 fontSize: '14px',
224 marginBottom: '20px',
225 opacity: (!customer1Id || !customer2Id) ? 0.5 : 1
226 }}
227 >
228 {loading ? 'Comparing...' : 'Compare Customers'}
229 </button>
230
231 {error && (
232 <div style={{
233 padding: '12px',
234 backgroundColor: 'var(--error-bg)',
235 color: 'var(--error)',
236 borderRadius: '4px',
237 marginBottom: '20px',
238 border: '1px solid var(--error)'
239 }}>
240 {error}
241 </div>
242 )}
243
244 {/* Comparison Results */}
245 {comparisonData && (
246 <>
247 <div style={{
248 display: 'grid',
249 gridTemplateColumns: '1fr 1fr',
250 gap: '16px',
251 marginBottom: '24px'
252 }}>
253 {/* Customer 1 Card */}
254 <div style={{
255 border: primaryCustomerId === comparisonData.customer1.customer_id
256 ? '2px solid var(--primary)'
257 : '1px solid var(--border)',
258 borderRadius: '8px',
259 padding: '16px',
260 backgroundColor: primaryCustomerId === comparisonData.customer1.customer_id
261 ? 'var(--info-bg)'
262 : 'var(--surface)',
263 cursor: 'pointer'
264 }}
265 onClick={() => setPrimaryCustomerId(comparisonData.customer1.customer_id)}
266 >
267 <div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
268 <input
269 type="radio"
270 checked={primaryCustomerId === comparisonData.customer1.customer_id}
271 onChange={() => setPrimaryCustomerId(comparisonData.customer1.customer_id)}
272 style={{ marginRight: '8px' }}
273 />
274 <h3 style={{ margin: 0, color: 'var(--text)' }}>
275 {comparisonData.customer1.name}
276 </h3>
277 </div>
278 <div style={{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: '1.8' }}>
279 <div><strong>Email:</strong> {comparisonData.customer1.email || 'N/A'}</div>
280 <div><strong>Phone:</strong> {comparisonData.customer1.phone || 'N/A'}</div>
281 <div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid var(--border)' }}>
282 <strong>Associated Data:</strong>
283 </div>
284 <div>• Tickets: {comparisonData.customer1.tickets_count}</div>
285 <div>• Invoices: {comparisonData.customer1.invoices_count}</div>
286 <div>• Contracts: {comparisonData.customer1.contracts_count}</div>
287 <div>• Agents: {comparisonData.customer1.agents_count}</div>
288 <div>• Hosting Apps: {comparisonData.customer1.hosting_apps_count}</div>
289 </div>
290 </div>
291
292 {/* Customer 2 Card */}
293 <div style={{
294 border: primaryCustomerId === comparisonData.customer2.customer_id
295 ? '2px solid var(--primary)'
296 : '1px solid var(--border)',
297 borderRadius: '8px',
298 padding: '16px',
299 backgroundColor: primaryCustomerId === comparisonData.customer2.customer_id
300 ? 'var(--info-bg)'
301 : 'var(--surface)',
302 cursor: 'pointer'
303 }}
304 onClick={() => setPrimaryCustomerId(comparisonData.customer2.customer_id)}
305 >
306 <div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
307 <input
308 type="radio"
309 checked={primaryCustomerId === comparisonData.customer2.customer_id}
310 onChange={() => setPrimaryCustomerId(comparisonData.customer2.customer_id)}
311 style={{ marginRight: '8px' }}
312 />
313 <h3 style={{ margin: 0, color: 'var(--text)' }}>
314 {comparisonData.customer2.name}
315 </h3>
316 </div>
317 <div style={{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: '1.8' }}>
318 <div><strong>Email:</strong> {comparisonData.customer2.email || 'N/A'}</div>
319 <div><strong>Phone:</strong> {comparisonData.customer2.phone || 'N/A'}</div>
320 <div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid var(--border)' }}>
321 <strong>Associated Data:</strong>
322 </div>
323 <div>• Tickets: {comparisonData.customer2.tickets_count}</div>
324 <div>• Invoices: {comparisonData.customer2.invoices_count}</div>
325 <div>• Contracts: {comparisonData.customer2.contracts_count}</div>
326 <div>• Agents: {comparisonData.customer2.agents_count}</div>
327 <div>• Hosting Apps: {comparisonData.customer2.hosting_apps_count}</div>
328 </div>
329 </div>
330 </div>
331
332 <div style={{
333 padding: '12px',
334 backgroundColor: 'var(--warning-bg)',
335 color: 'var(--warning)',
336 borderRadius: '4px',
337 marginBottom: '20px',
338 border: '1px solid var(--warning)',
339 fontSize: '13px'
340 }}>
341 <strong>⚠️ Warning:</strong> The selected customer will be kept. All data from the other customer will be moved to it, and the other customer will be permanently deleted.
342 </div>
343
344 {/* Action Buttons */}
345 <div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
346 <button
347 onClick={onClose}
348 disabled={loading}
349 style={{
350 padding: '10px 20px',
351 backgroundColor: 'transparent',
352 color: 'var(--text)',
353 border: '1px solid var(--border)',
354 borderRadius: '4px',
355 cursor: loading ? 'not-allowed' : 'pointer',
356 fontSize: '14px'
357 }}
358 >
359 Cancel
360 </button>
361 <button
362 onClick={handleMerge}
363 disabled={loading || !primaryCustomerId}
364 style={{
365 padding: '10px 20px',
366 backgroundColor: 'var(--error)',
367 color: 'white',
368 border: 'none',
369 borderRadius: '4px',
370 cursor: loading || !primaryCustomerId ? 'not-allowed' : 'pointer',
371 fontSize: '14px',
372 opacity: !primaryCustomerId ? 0.5 : 1
373 }}
374 >
375 {loading ? 'Merging...' : 'Merge Customers'}
376 </button>
377 </div>
378 </>
379 )}
380 </div>
381 </div>
382 );
383}