import { useState } from "react"; import { trpc } from "@/lib/trpc"; import DashboardLayout from "@/components/DashboardLayout"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { isValidWebPortalUrl, normalizeWebPortalUrl } from "@shared/webPortalUrl"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Globe, Plus, Pencil, Trash2, Key, CheckCircle, XCircle, Clock, RefreshCw, Play } from "lucide-react"; import { toast } from "sonner"; const CONNECTOR_TYPES = [ { value: "sfr", label: "SFR Pro", url: "https://www.sfr.fr/mon-espace-client/" }, { value: "orange", label: "Orange Pro", url: "https://espaceclient.orange.fr/" }, { value: "bouygues", label: "Bouygues Telecom", url: "https://www.bouyguestelecom.fr/mon-compte/" }, { value: "free", label: "Free Pro", url: "https://pro.free.fr/" }, { value: "starlink", label: "Starlink", url: "https://www.starlink.com/account/" }, { value: "custom", label: "Autre (personnalisé)", url: "" }, ]; const FREQUENCY_LABELS: Record = { manual: "Manuel", daily: "Quotidien", weekly: "Hebdomadaire", monthly: "Mensuel", }; type Source = { id: number; name: string; connectorType: string; portalUrl: string; loginEmail: string; loginPassword: string; frequency: "manual" | "daily" | "weekly" | "monthly"; autoEnabled: number; lastSuccessAt: Date | null; lastStatus: string | null; lastImportCount: number | null; apiToken: string; createdAt: Date; updatedAt: Date; }; type FormData = { name: string; connectorType: string; portalUrl: string; loginEmail: string; loginPassword: string; frequency: "manual" | "daily" | "weekly" | "monthly"; autoEnabled: number; }; const emptyForm: FormData = { name: "", connectorType: "sfr", portalUrl: "https://www.sfr.fr/mon-espace-client/", loginEmail: "", loginPassword: "", frequency: "monthly", autoEnabled: 0, }; export default function WebImportSources() { const [showForm, setShowForm] = useState(false); const [editSource, setEditSource] = useState(null); const [deleteId, setDeleteId] = useState(null); const [manualImportId, setManualImportId] = useState(null); const [showToken, setShowToken] = useState(null); const [form, setForm] = useState(emptyForm); const { data: sources = [], refetch } = trpc.webImportSources.list.useQuery(); const { data: tokenData } = trpc.webImportSources.getToken.useQuery( { id: showToken! }, { enabled: showToken !== null } ); const createMutation = trpc.webImportSources.create.useMutation({ onSuccess: () => { toast.success("Source créée", { description: "Le connecteur web a été ajouté." }); setShowForm(false); setForm(emptyForm); refetch(); }, onError: (e) => toast.error("Erreur", { description: e.message }), }); const updateMutation = trpc.webImportSources.update.useMutation({ onSuccess: () => { toast.success("Source mise à jour"); setEditSource(null); setForm(emptyForm); refetch(); }, onError: (e) => toast.error("Erreur", { description: e.message }), }); const deleteMutation = trpc.webImportSources.delete.useMutation({ onSuccess: () => { toast.success("Source supprimée"); setDeleteId(null); refetch(); }, onError: (e) => toast.error("Erreur", { description: e.message }), }); const runManualMutation = trpc.webImportSources.runManual.useMutation({ onSuccess: (result) => { toast.success("Import manuel terminé", { description: `${result.imported} facture(s) importée(s), ${result.duplicates} doublon(s) ignoré(s).`, }); setManualImportId(null); refetch(); }, onError: (e) => { toast.error("Import manuel en erreur", { description: e.message }); setManualImportId(null); refetch(); }, }); function openCreate() { setForm(emptyForm); setEditSource(null); setShowForm(true); } function openEdit(source: Source) { setForm({ name: source.name, connectorType: source.connectorType, portalUrl: source.portalUrl, loginEmail: source.loginEmail, loginPassword: "", // Ne pas pré-remplir le mot de passe frequency: source.frequency, autoEnabled: source.autoEnabled, }); setEditSource(source); setShowForm(true); } function handleConnectorTypeChange(value: string) { const connector = CONNECTOR_TYPES.find(c => c.value === value); setForm(f => ({ ...f, connectorType: value, name: f.name || connector?.label || "", portalUrl: connector?.url || f.portalUrl, })); } function handleSubmit() { if (!form.name || !form.loginEmail || (!editSource && !form.loginPassword)) { toast.error("Champs requis", { description: "Nom, identifiant et mot de passe sont obligatoires." }); return; } const portalUrl = normalizeWebPortalUrl(form.portalUrl); if (!isValidWebPortalUrl(portalUrl)) { toast.error("URL de portail invalide", { description: "Saisissez une adresse HTTP(S), par exemple https://espaceclient.exemple.fr.", }); return; } if (editSource) { const updateData: any = { id: editSource.id, ...form, portalUrl }; if (!form.loginPassword) delete updateData.loginPassword; // Ne pas écraser si vide updateMutation.mutate(updateData); } else { createMutation.mutate({ ...form, portalUrl }); } } return (
{/* En-tête */}

Connecteurs web

Import automatique de factures depuis des espaces clients

{/* Info technique */}
Fonctionnement : Un script cron tourne sur le serveur LWS et se connecte automatiquement aux espaces clients configurés pour télécharger les nouvelles factures. Le token API affiché ci-dessous est utilisé par ce script pour s'authentifier auprès de l'application.
{/* Liste des sources */} {sources.length === 0 ? (

Aucun connecteur configuré

Ajoutez un connecteur pour importer automatiquement des factures depuis un espace client web.

) : (
{(sources as Source[]).map((source) => (
{source.name} {source.connectorType} {source.autoEnabled ? "Auto activé" : "Manuel"} {FREQUENCY_LABELS[source.frequency]}

{source.portalUrl}

Identifiant : {source.loginEmail}

{source.lastSuccessAt ? ( Dernier import : {new Date(source.lastSuccessAt).toLocaleDateString("fr-FR")} {source.lastImportCount !== null && ` (${source.lastImportCount} facture(s))`} ) : ( Jamais importé )} {source.lastStatus && !source.lastSuccessAt && ( {source.lastStatus} )}
{/* Token API affiché inline */} {showToken === source.id && tokenData && (

Token API (à configurer dans le script cron) :

{tokenData.apiToken}
)}
))}
)} {/* Dialog création / édition */} { if (!open) { setShowForm(false); setEditSource(null); } }}> {editSource ? "Modifier le connecteur" : "Nouveau connecteur web"}
setForm(f => ({ ...f, name: e.target.value }))} placeholder="Ex : SFR Pro - Itinova" />
setForm(f => ({ ...f, portalUrl: e.target.value }))} placeholder="https://..." />

Le préfixe https:// est ajouté automatiquement si nécessaire.

setForm(f => ({ ...f, loginEmail: e.target.value }))} placeholder="votre@email.com" />
setForm(f => ({ ...f, loginPassword: e.target.value }))} placeholder={editSource ? "••••••••" : "Mot de passe"} />
setForm(f => ({ ...f, autoEnabled: v ? 1 : 0 }))} />
{ if (!open && !runManualMutation.isPending) setManualImportId(null); }}> Lancer l’import manuel ? L’application se connectera ponctuellement à l’espace client SFR, téléchargera les nouvelles factures puis les analysera via le flux normal. Cette action ne crée aucune planification automatique. Annuler manualImportId !== null && runManualMutation.mutate({ id: manualImportId })} > {runManualMutation.isPending ? "Import en cours…" : "Lancer l’import"} {/* Confirmation suppression */} { if (!open) setDeleteId(null); }}> Supprimer ce connecteur ? Cette action est irréversible. Le connecteur et son token API seront supprimés. Annuler deleteId !== null && deleteMutation.mutate({ id: deleteId })} > Supprimer
); }