11<template >
2- <div class =" flex flex-col grow" >
3- <div
4- class =" flex flex-col grow border border-surface-200 rounded-md px-2 pt-6"
2+ <div
3+ class =" flex flex-col grow border border-surface-200 rounded-md p-4 gap-4"
4+ >
5+ <div class =" flex items-center justify-between" >
6+ <h1 class =" text-xl font-semibold" >{{ t('sources') }}</h1 >
7+ </div >
8+
9+ <DataView
10+ :value =" $leadminer.miningSources"
11+ data-key =" email"
12+ :paginator =" true"
13+ :rows =" 10"
514 >
6- <DataTable :value =" $leadminer.miningSources" >
7- <Column field =" email" :header =" t('email')" > </Column >
8- <Column field =" type" :header =" t('type')" >
9- <template #body =" slotProps " >
10- <i
11- :class =" getIcon(slotProps.data.type)"
12- class =" text-secondary text-sm"
13- ></i >
14- <span class =" ml-2" >{{ slotProps.data.type }}</span >
15- </template >
16- </Column >
17- <Column field =" passive_mining" :header =" t('passive_mining')" >
18- <template #body =" slotProps " >
19- <div v-if =" slotProps.data.passive_mining" >
20- <span class =" relative flex h-3 w-3" >
21- <span
22- class =" animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
23- ></span >
24- <span
25- class =" relative inline-flex h-3 w-3 rounded-full bg-green-500"
26- ></span >
27- </span >
15+ <template #empty >
16+ <div class =" text-center py-8 text-surface-500" >
17+ {{ t('no_sources') }}
18+ </div >
19+ </template >
20+ <template #list =" slotProps " >
21+ <div class =" grid gap-2" >
22+ <div
23+ v-for =" source in slotProps.items"
24+ :key =" source.email"
25+ class =" border border-surface-200 rounded-md p-4"
26+ >
27+ <div class =" flex items-center justify-between gap-2" >
28+ <div class =" flex items-center gap-2" >
29+ <div class =" font-medium" >{{ source.email }}</div >
30+ <Tag
31+ :value ="
32+ source.isValid ? t('connected') : t('credential_expired')
33+ "
34+ :severity =" source.isValid ? 'success' : 'warn'"
35+ />
36+ </div >
37+ <div class =" flex items-center gap-2" >
38+ <Button
39+ v-if =" isActiveMiningSource(source)"
40+ size =" small"
41+ outlined
42+ severity =" warning"
43+ icon =" pi pi-stop"
44+ :label =" t('stop_mining')"
45+ :loading =" isStoppingMining"
46+ @click =" confirmStopMining"
47+ />
48+ <Button
49+ size =" small"
50+ outlined
51+ severity =" danger"
52+ icon =" pi pi-trash"
53+ :label =" t('delete_source')"
54+ :loading ="
55+ isDeleting && deletingSource?.email === source.email
56+ "
57+ @click =" openDeleteDialog(source)"
58+ />
59+ </div >
2860 </div >
29- <div v-else >
30- <span class =" inline-flex h-3 w-3 rounded-full bg-gray-500" ></span >
61+
62+ <div class =" grid grid-cols-2 lg:grid-cols-4 gap-2 mt-4 text-sm" >
63+ <div class =" p-2 rounded bg-surface-50" >
64+ <div class =" text-surface-500" >{{ t('type') }}</div >
65+ <div class =" flex items-center gap-2 font-medium" >
66+ <i
67+ :class =" getIcon(source.type)"
68+ class =" text-secondary size-xs"
69+ ></i >
70+ <span >{{ source.type }}</span >
71+ </div >
72+ </div >
73+
74+ <div class =" p-2 rounded bg-surface-50" >
75+ <div class =" text-surface-500" >{{ t('passive_mining') }}</div >
76+ <div class =" flex items-center gap-2" >
77+ <span
78+ v-if =" source.passive_mining"
79+ class =" relative flex h-3 w-3"
80+ >
81+ <span
82+ class =" animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
83+ ></span >
84+ <span
85+ class =" relative inline-flex h-3 w-3 rounded-full bg-green-500"
86+ ></span >
87+ </span >
88+ <span
89+ v-else
90+ class =" inline-flex h-3 w-3 rounded-full bg-gray-500"
91+ ></span >
92+ <span >{{
93+ source.passive_mining ? t('enabled') : t('disabled')
94+ }}</span >
95+ </div >
96+ </div >
97+
98+ <div class =" p-2 rounded bg-surface-50" >
99+ <div class =" text-surface-500" >{{ t('total_contacts') }}</div >
100+ <div class =" font-medium" >{{ source.totalContacts || 0 }}</div >
101+ </div >
102+
103+ <div class =" p-2 rounded bg-surface-50" >
104+ <div class =" text-surface-500" >{{ t('last_mining') }}</div >
105+ <div class =" font-medium" >
106+ {{
107+ source.lastMiningDate
108+ ? formatDate(source.lastMiningDate)
109+ : '-'
110+ }}
111+ </div >
112+ <div
113+ v-if =" source.totalFromLastMining"
114+ class =" text-xs text-surface-500"
115+ >
116+ {{ source.totalFromLastMining }} {{ t('contacts') }}
117+ </div >
118+ </div >
31119 </div >
32- </template >
33- </Column >
34- </DataTable >
35- </div >
120+
121+ <div
122+ v-if =" isActiveMiningSource(source)"
123+ class =" mt-4 p-3 rounded bg-surface-50 border border-primary/20"
124+ >
125+ <div class =" flex items-center justify-between flex-wrap gap-2" >
126+ <div class =" flex items-center gap-2" >
127+ <span class =" relative flex h-2 w-2" >
128+ <span
129+ class =" animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"
130+ ></span >
131+ <span
132+ class =" relative inline-flex h-2 w-2 rounded-full bg-primary"
133+ ></span >
134+ </span >
135+ <span class =" text-sm font-medium text-primary" >{{
136+ t('mining_in_progress')
137+ }}</span >
138+ </div >
139+ <div class =" flex items-center gap-2 text-sm text-surface-600" >
140+ <span
141+ >{{ t('emails_scanned') }}:
142+ {{ $leadminer.scannedEmails }}</span
143+ >
144+ <span class =" text-surface-400" >|</span >
145+ <span
146+ >{{ t('emails_extracted') }}:
147+ {{ $leadminer.extractedEmails }}</span
148+ >
149+ <span class =" text-surface-400" >|</span >
150+ <span
151+ >{{ t('emails_cleaned') }}:
152+ {{ $leadminer.verifiedContacts }}</span
153+ >
154+ <Button
155+ size =" small"
156+ severity =" secondary"
157+ :label =" t('view_mining')"
158+ class =" ml-2"
159+ @click =" navigateTo('/mine')"
160+ />
161+ </div >
162+ </div >
163+ </div >
164+ </div >
165+ </div >
166+ </template >
167+ </DataView >
168+
169+ <Dialog
170+ v-model:visible =" deleteDialogVisible"
171+ modal
172+ :header =" t('delete_source')"
173+ :style =" { width: '28rem', maxWidth: '95vw' }"
174+ >
175+ <div class =" text-sm text-surface-700" >
176+ {{ t('delete_source_confirm') }}
177+ </div >
178+
179+ <template #footer >
180+ <div class =" flex justify-end gap-2 w-full" >
181+ <Button outlined :label =" t('cancel')" @click =" closeDeleteDialog" />
182+ <Button
183+ severity =" danger"
184+ :label =" t('delete_source')"
185+ :loading =" isDeleting"
186+ @click =" confirmDelete"
187+ />
188+ </div >
189+ </template >
190+ </Dialog >
36191 </div >
37192</template >
38193
39194<script setup lang="ts">
195+ import type { MiningSource } from ' ~/types/mining' ;
196+
40197const $leadminer = useLeadminerStore ();
41198const { t } = useI18n ({
42199 useScope: ' local' ,
43200});
201+ const { $saasEdgeFunctions } = useNuxtApp ();
202+ const $toast = useToast ();
203+
204+ const deleteDialogVisible = ref (false );
205+ const deletingSource = ref <MiningSource | null >(null );
206+ const isDeleting = ref (false );
207+ const isStoppingMining = ref (false );
44208
45209function getIcon(type : string ) {
46210 switch (type ) {
@@ -52,22 +216,151 @@ function getIcon(type: string) {
52216 return ' pi pi-inbox' ;
53217 }
54218}
219+
220+ function isActiveMiningSource(source : MiningSource ): boolean {
221+ return Boolean (
222+ $leadminer .activeMiningSource ?.email === source .email &&
223+ $leadminer .miningTask ,
224+ );
225+ }
226+
227+ function formatDate(dateString : string ) {
228+ return new Date (dateString ).toLocaleDateString ();
229+ }
230+
231+ function openDeleteDialog(source : MiningSource ) {
232+ deletingSource .value = source ;
233+ deleteDialogVisible .value = true ;
234+ }
235+
236+ function closeDeleteDialog() {
237+ deleteDialogVisible .value = false ;
238+ deletingSource .value = null ;
239+ }
240+
241+ async function confirmDelete() {
242+ if (! deletingSource .value ) return ;
243+
244+ isDeleting .value = true ;
245+
246+ try {
247+ await $saasEdgeFunctions (' delete-mining-source' , {
248+ method: ' DELETE' ,
249+ body: { email: deletingSource .value .email },
250+ });
251+
252+ $toast .add ({
253+ severity: ' success' ,
254+ summary: t (' source_deleted' ),
255+ detail: t (' source_deleted_detail' ),
256+ life: 3500 ,
257+ });
258+
259+ closeDeleteDialog ();
260+ await $leadminer .fetchMiningSources ();
261+ } catch (error ) {
262+ $toast .add ({
263+ severity: ' error' ,
264+ summary: t (' delete_source_failed' ),
265+ detail: (error as Error ).message ,
266+ life: 4500 ,
267+ });
268+ } finally {
269+ isDeleting .value = false ;
270+ }
271+ }
272+
273+ async function confirmStopMining() {
274+ isStoppingMining .value = true ;
275+ try {
276+ await $leadminer .stopMining (true , null );
277+ $toast .add ({
278+ severity: ' success' ,
279+ summary: t (' mining_stopped' ),
280+ detail: t (' mining_stopped_detail' ),
281+ life: 3500 ,
282+ });
283+ } catch (error ) {
284+ $toast .add ({
285+ severity: ' error' ,
286+ summary: t (' stop_mining_failed' ),
287+ detail: (error as Error ).message ,
288+ life: 4500 ,
289+ });
290+ } finally {
291+ isStoppingMining .value = false ;
292+ }
293+ }
294+
55295onMounted (async () => {
56296 await $leadminer .fetchMiningSources ();
297+ await $leadminer .getCurrentRunningMining ();
57298});
58299 </script >
59300
60301<i18n lang="json">
61302{
62303 "en" : {
304+ "sources" : " Sources" ,
305+ "no_sources" : " No sources yet" ,
63306 "email" : " Email" ,
64307 "type" : " Type" ,
65- "passive_mining" : " Passive mining"
308+ "passive_mining" : " Passive mining" ,
309+ "credentials" : " Credentials" ,
310+ "status" : " Status" ,
311+ "connected" : " Connected" ,
312+ "credential_expired" : " Credential expired" ,
313+ "enabled" : " Enabled" ,
314+ "disabled" : " Disabled" ,
315+ "delete_source" : " Delete" ,
316+ "delete_source_confirm" : " Delete this mining source permanently? This action cannot be undone." ,
317+ "cancel" : " Cancel" ,
318+ "source_deleted" : " Source deleted" ,
319+ "source_deleted_detail" : " The mining source has been permanently deleted." ,
320+ "delete_source_failed" : " Unable to delete source" ,
321+ "stop_mining" : " Stop mining" ,
322+ "view_mining" : " View mining" ,
323+ "mining_in_progress" : " Mining in progress" ,
324+ "emails_scanned" : " Scanned" ,
325+ "emails_extracted" : " Extracted" ,
326+ "emails_cleaned" : " Cleaned" ,
327+ "mining_stopped" : " Mining stopped" ,
328+ "mining_stopped_detail" : " The mining process has been stopped." ,
329+ "stop_mining_failed" : " Unable to stop mining" ,
330+ "total_contacts" : " Total contacts" ,
331+ "last_mining" : " Last mining" ,
332+ "contacts" : " contacts"
66333 },
67334 "fr" : {
335+ "sources" : " Sources" ,
336+ "no_sources" : " Aucune source" ,
68337 "email" : " Email" ,
69338 "type" : " Type" ,
70- "passive_mining" : " Extraction passive"
339+ "passive_mining" : " Extraction passive" ,
340+ "credentials" : " Identifiants" ,
341+ "status" : " Statut" ,
342+ "connected" : " Connecté" ,
343+ "credential_expired" : " Identifiant expiré" ,
344+ "enabled" : " Activé" ,
345+ "disabled" : " Désactivé" ,
346+ "delete_source" : " Supprimer" ,
347+ "delete_source_confirm" : " Supprimer définitivement cette source de minage ? Cette action est irréversible." ,
348+ "cancel" : " Annuler" ,
349+ "source_deleted" : " Source supprimée" ,
350+ "source_deleted_detail" : " La source de minage a été supprimée définitivement." ,
351+ "delete_source_failed" : " Impossible de supprimer la source" ,
352+ "stop_mining" : " Arrêter le minage" ,
353+ "view_mining" : " Voir le minage" ,
354+ "mining_in_progress" : " Extraction en cours" ,
355+ "emails_scanned" : " Scannés" ,
356+ "emails_extracted" : " Extracts" ,
357+ "emails_cleaned" : " Nettoyés" ,
358+ "mining_stopped" : " Extraction stoppée" ,
359+ "mining_stopped_detail" : " Le processus d'extraction a été stoppé." ,
360+ "stop_mining_failed" : " Impossible de stopper le minage" ,
361+ "total_contacts" : " Total contacts" ,
362+ "last_mining" : " Dernier minage" ,
363+ "contacts" : " contacts"
71364 }
72365}
73366 </i18n >
0 commit comments