invy/frontend/src/components/ImportContacts.jsx

251 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef } from 'react'
import { importContacts } from '../api/api'
import './ImportContacts.css'
/**
* ImportContacts
*
* Opens a modal that lets the user upload a CSV or JSON file of contacts and
* import them into the current event's guest list.
*
* Props:
* eventId UUID of the current event
* onImportComplete callback called when a real (non-dry-run) import succeeds
*/
function ImportContacts({ eventId, onImportComplete }) {
const [open, setOpen] = useState(false)
const [file, setFile] = useState(null)
const [isDryRun, setIsDryRun] = useState(false)
const [dragging, setDragging] = useState(false)
const [loading, setLoading] = useState(false)
const [result, setResult] = useState(null) // ImportContactsResponse
const [error, setError] = useState('')
const fileInputRef = useRef()
// ── helpers ────────────────────────────────────────────────────────────────
const reset = () => {
setFile(null)
setResult(null)
setError('')
setLoading(false)
setIsDryRun(false)
}
const handleClose = () => {
setOpen(false)
reset()
}
const handleFileChange = (e) => {
const f = e.target.files?.[0]
if (f) { setFile(f); setResult(null); setError('') }
}
const handleDrop = (e) => {
e.preventDefault()
setDragging(false)
const f = e.dataTransfer.files?.[0]
if (f) { setFile(f); setResult(null); setError('') }
}
// ── submit ─────────────────────────────────────────────────────────────────
const handleUpload = async () => {
if (!file) { setError('אנא בחר קובץ לפני ההעלאה.'); return }
setLoading(true)
setError('')
setResult(null)
try {
const res = await importContacts(eventId, file, isDryRun)
setResult(res)
if (!isDryRun && (res.created > 0 || res.updated > 0) && onImportComplete) {
onImportComplete()
}
} catch (err) {
const msg = err?.response?.data?.detail || err.message || 'שגיאה בעת ייבוא הקובץ.'
setError(typeof msg === 'string' ? msg : JSON.stringify(msg))
} finally {
setLoading(false)
}
}
// ── action label helpers ───────────────────────────────────────────────────
const actionLabel = {
created: { text: 'נוצר', cls: 'badge-created' },
updated: { text: 'עודכן', cls: 'badge-updated' },
skipped: { text: 'דולג', cls: 'badge-skipped' },
error: { text: 'שגיאה', cls: 'badge-error' },
would_create: { text: 'ייווצר', cls: 'badge-dry' },
}
// ── modal ──────────────────────────────────────────────────────────────────
if (!open) {
return (
<button
className="btn btn-import"
onClick={() => setOpen(true)}
disabled={!eventId}
title={!eventId ? 'בחר אירוע תחילה' : 'ייבוא אנשי קשר מקובץ CSV / JSON'}
>
📂 ייבוא קובץ
</button>
)
}
return (
<div className="import-overlay" onClick={(e) => e.target === e.currentTarget && handleClose()}>
<div className="import-modal" dir="rtl">
{/* Header */}
<div className="import-header">
<h2>📂 ייבוא אנשי קשר מקובץ</h2>
<button className="import-close" onClick={handleClose}></button>
</div>
{/* Body */}
<div className="import-body">
{/* File drop zone */}
<div
className={`drop-zone ${dragging ? 'dragging' : ''} ${file ? 'has-file' : ''}`}
onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept=".csv,.json,.xlsx"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
{file ? (
<>
<span className="drop-icon"></span>
<p className="drop-filename">{file.name}</p>
<p className="drop-hint">לחץ להחלפת הקובץ</p>
</>
) : (
<>
<span className="drop-icon">📄</span>
<p className="drop-text">גרור קובץ CSV או JSON לכאן</p>
<p className="drop-hint">או לחץ לבחירת קובץ</p>
</>
)}
</div>
{/* Format hint */}
<div className="import-hint">
<details>
<summary>פורמטים נתמכים</summary>
<div className="hint-body">
<p><strong>CSV</strong> כל שורה = אורח. עמודות נתמכות:</p>
<code>First Name, Last Name, Full Name, Phone, Email, RSVP, Meal, Notes, Side, Table</code>
<p><strong>JSON</strong> מערך של אובייקטים עם אותן שדות, או <code>{`{"data":[]}`}</code>.</p>
<p>הייבוא ניתן לביצוע כפעולת מה-Excel שיצאת: אותן עמודות שמופיעות בייצוא לאקסל.</p>
</div>
</details>
</div>
{/* Dry-run toggle */}
<label className="dry-run-toggle">
<input
type="checkbox"
checked={isDryRun}
onChange={(e) => setIsDryRun(e.target.checked)}
/>
<span>בדיקה בלבד (Dry Run) הצג מה היה קורה ללא שמירה</span>
</label>
{/* Error */}
{error && <div className="import-error">{error}</div>}
{/* Upload button */}
{!result && (
<button
className="btn-upload"
onClick={handleUpload}
disabled={loading || !file}
>
{loading ? '⏳ מייבא…' : isDryRun ? '🔍 הרץ בדיקה' : '📥 ייבא אנשי קשר'}
</button>
)}
{/* Results */}
{result && (
<div className="import-results">
<div className={`results-banner ${result.dry_run ? 'dry' : 'live'}`}>
{result.dry_run ? '🔍 תוצאות בדיקה (לא נשמר)' : '✅ הייבוא הושלם!'}
</div>
<div className="results-stats">
<div className="stat"><span>{result.total}</span><small>סה"כ</small></div>
<div className="stat created"><span>{result.created}</span><small>{result.dry_run ? 'ייווצרו' : 'נוצרו'}</small></div>
<div className="stat updated"><span>{result.updated}</span><small>עודכנו</small></div>
<div className="stat skipped"><span>{result.skipped}</span><small>דולגו</small></div>
{result.errors > 0 && (
<div className="stat errors"><span>{result.errors}</span><small>שגיאות</small></div>
)}
</div>
{/* Row-level table */}
{result.rows.length > 0 && (
<div className="results-table-wrap">
<table className="results-table">
<thead>
<tr>
<th>#</th>
<th>שם</th>
<th>טלפון</th>
<th>פעולה</th>
<th>הערה</th>
</tr>
</thead>
<tbody>
{result.rows.map((r) => {
const lbl = actionLabel[r.action] || { text: r.action, cls: '' }
return (
<tr key={r.row} className={`row-${r.action}`}>
<td>{r.row}</td>
<td>{r.name || '—'}</td>
<td dir="ltr">{r.phone || '—'}</td>
<td><span className={`badge ${lbl.cls}`}>{lbl.text}</span></td>
<td>{r.reason || ''}</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
{/* Post-result actions */}
<div className="results-actions">
{result.dry_run && (
<button
className="btn-upload"
onClick={() => { setIsDryRun(false); setResult(null) }}
>
אישור ייבא עכשיו
</button>
)}
<button className="btn-reset" onClick={reset}>
📂 ייבא קובץ חדש
</button>
<button className="btn-close-after" onClick={handleClose}>
סגור
</button>
</div>
</div>
)}
</div>
</div>
</div>
)
}
export default ImportContacts