213 lines
6.5 KiB
TypeScript
213 lines
6.5 KiB
TypeScript
"use client"
|
|
|
|
import { useRef, useState } from "react"
|
|
import { useQueryClient } from "@tanstack/react-query"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Info, Smartphone } from "lucide-react"
|
|
import { parseContactFile, type ContactImportInput } from "@/lib/contacts/import-parsers"
|
|
import { bulkImportContacts, importInputToBulk } from "@/lib/api/contacts-bulk-import"
|
|
import { deviceContactsAvailable, fetchDeviceContacts } from "@/lib/native/contacts"
|
|
import { invalidateContactListCache } from "@/lib/api/contact-list-cache"
|
|
import { useContactsList } from "@/lib/contacts/use-contacts-list"
|
|
import {
|
|
CONTACTS_HEADING_TEXT,
|
|
CONTACTS_MUTED_TEXT,
|
|
CONTACTS_PAGE_LINK_BTN_CLASS,
|
|
CONTACTS_PANEL_MUTED_ICON_CLASS,
|
|
CONTACTS_PRIMARY_BTN_CLASS,
|
|
} from "@/lib/contacts-chrome-classes"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface ImportDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
}
|
|
|
|
export function ImportDialog({ open, onOpenChange }: ImportDialogProps) {
|
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
const queryClient = useQueryClient()
|
|
const { bookId } = useContactsList()
|
|
const [pendingFile, setPendingFile] = useState<File | null>(null)
|
|
const [previewCount, setPreviewCount] = useState(0)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [importing, setImporting] = useState(false)
|
|
const [result, setResult] = useState<string | null>(null)
|
|
|
|
function resetState() {
|
|
setPendingFile(null)
|
|
setPreviewCount(0)
|
|
setError(null)
|
|
setImporting(false)
|
|
setResult(null)
|
|
if (fileRef.current) fileRef.current.value = ""
|
|
}
|
|
|
|
function handleOpenChange(next: boolean) {
|
|
if (!next) resetState()
|
|
onOpenChange(next)
|
|
}
|
|
|
|
function handleFileSelect() {
|
|
fileRef.current?.click()
|
|
}
|
|
|
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
const file = e.target.files?.[0]
|
|
if (!file) return
|
|
|
|
setError(null)
|
|
setPendingFile(file)
|
|
|
|
try {
|
|
const parsed = await parseContactFile(file)
|
|
setPreviewCount(parsed.length)
|
|
if (parsed.length === 0) {
|
|
setError("Aucun contact trouvé dans ce fichier.")
|
|
}
|
|
} catch {
|
|
setError("Impossible de lire ce fichier.")
|
|
setPreviewCount(0)
|
|
}
|
|
}
|
|
|
|
async function runImport(parsed: ContactImportInput[]) {
|
|
if (parsed.length === 0) {
|
|
setError("Aucun contact à importer.")
|
|
return
|
|
}
|
|
setImporting(true)
|
|
setError(null)
|
|
setResult(null)
|
|
try {
|
|
const { created, failed } = await bulkImportContacts(
|
|
bookId,
|
|
parsed.map(importInputToBulk)
|
|
)
|
|
invalidateContactListCache(bookId)
|
|
void queryClient.invalidateQueries({ queryKey: ["contacts", bookId] })
|
|
if (failed.length > 0) {
|
|
setResult(`${created} importé(s), ${failed.length} en échec.`)
|
|
} else {
|
|
handleOpenChange(false)
|
|
}
|
|
} catch {
|
|
setError("L'import a échoué. Vérifiez le format du fichier.")
|
|
} finally {
|
|
setImporting(false)
|
|
}
|
|
}
|
|
|
|
async function handleImport() {
|
|
if (!pendingFile || previewCount === 0) return
|
|
try {
|
|
const parsed = await parseContactFile(pendingFile)
|
|
await runImport(parsed)
|
|
} catch {
|
|
setError("L'import a échoué. Vérifiez le format du fichier.")
|
|
}
|
|
}
|
|
|
|
async function handleDeviceImport() {
|
|
setImporting(true)
|
|
setError(null)
|
|
try {
|
|
const parsed = await fetchDeviceContacts()
|
|
await runImport(parsed)
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : "device_error"
|
|
setError(
|
|
msg === "contacts_unavailable" || msg.includes("denied")
|
|
? "Accès aux contacts du téléphone refusé."
|
|
: "Impossible de lire les contacts du téléphone."
|
|
)
|
|
setImporting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<div className="flex items-center justify-between">
|
|
<DialogTitle>Importer des contacts</DialogTitle>
|
|
<Info className={cn("h-5 w-5", CONTACTS_PANEL_MUTED_ICON_CLASS)} />
|
|
</div>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<p className={cn("text-sm", CONTACTS_HEADING_TEXT)}>
|
|
Pour commencer, sélectionnez un fichier.
|
|
<br />
|
|
Utilisez le format CSV ou vCard (.vcf).
|
|
</p>
|
|
|
|
<Button type="button" onClick={handleFileSelect} className={CONTACTS_PRIMARY_BTN_CLASS}>
|
|
Sélectionner un fichier
|
|
</Button>
|
|
|
|
{deviceContactsAvailable() && (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => void handleDeviceImport()}
|
|
disabled={importing}
|
|
className="w-full gap-2"
|
|
>
|
|
<Smartphone className="h-4 w-4" />
|
|
Importer depuis le téléphone
|
|
</Button>
|
|
)}
|
|
|
|
<input
|
|
ref={fileRef}
|
|
type="file"
|
|
accept=".csv,.vcf,.vcard,text/vcard,text/csv"
|
|
className="hidden"
|
|
onChange={handleFileChange}
|
|
/>
|
|
|
|
{pendingFile && previewCount > 0 && (
|
|
<p className={cn("text-sm", CONTACTS_HEADING_TEXT)}>
|
|
{previewCount} contact{previewCount > 1 ? "s" : ""} prêt
|
|
{previewCount > 1 ? "s" : ""} à importer depuis{" "}
|
|
<span className="font-medium">{pendingFile.name}</span>
|
|
</p>
|
|
)}
|
|
|
|
{result && <p className="text-sm text-amber-600">{result}</p>}
|
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
|
|
<p className={cn("text-xs", CONTACTS_MUTED_TEXT)}>
|
|
Vous essayez de sauvegarder les contacts de votre mobile ?
|
|
<br />
|
|
<span className="cursor-pointer text-primary">Voici comment les synchroniser.</span>
|
|
</p>
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => handleOpenChange(false)}
|
|
className={CONTACTS_PAGE_LINK_BTN_CLASS}
|
|
>
|
|
Non, ne rien faire
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={handleImport}
|
|
disabled={!pendingFile || previewCount === 0 || importing}
|
|
className="text-sm font-medium"
|
|
>
|
|
{importing ? "Importation…" : "Importer"}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|