Skip to content

Commit b0cd9da

Browse files
committed
ImportSubscribers
1 parent 36560f4 commit b0cd9da

File tree

2 files changed

+137
-4
lines changed

2 files changed

+137
-4
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<template>
2+
<div
3+
v-if="isImportResultOpen"
4+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
5+
@click.self="$emit('close')"
6+
>
7+
<div class="w-full max-w-md rounded-xl bg-white shadow-xl border border-slate-200">
8+
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-200">
9+
<h3 class="text-base font-semibold text-slate-900">Import Result</h3>
10+
<button
11+
class="text-slate-400 hover:text-slate-600 transition-colors"
12+
@click="$emit('close')"
13+
>
14+
<BaseIcon name="x" class="w-5 h-5" />
15+
</button>
16+
</div>
17+
18+
<div class="p-4 space-y-4">
19+
<div class="grid grid-cols-2 gap-3">
20+
<div class="rounded-lg bg-slate-50 border border-slate-200 p-3">
21+
<div class="text-xs text-slate-500">Imported</div>
22+
<div class="text-lg font-semibold text-slate-900">
23+
{{ importResult.imported }}
24+
</div>
25+
</div>
26+
27+
<div class="rounded-lg bg-slate-50 border border-slate-200 p-3">
28+
<div class="text-xs text-slate-500">Skipped</div>
29+
<div class="text-lg font-semibold text-slate-900">
30+
{{ importResult.skipped }}
31+
</div>
32+
</div>
33+
</div>
34+
35+
<div v-if="importResult.errors && importResult.errors.length">
36+
<div class="text-sm font-medium text-slate-900 mb-2">Errors</div>
37+
<div class="max-h-48 overflow-y-auto rounded-lg border border-red-200 bg-red-50 p-3">
38+
<ul class="space-y-1 text-sm text-red-700">
39+
<li v-for="(error, index) in importResult.errors" :key="index">
40+
{{ error }}
41+
</li>
42+
</ul>
43+
</div>
44+
</div>
45+
46+
<div v-else class="text-sm text-green-700 rounded-lg border border-green-200 bg-green-50 p-3">
47+
Import completed without errors.
48+
</div>
49+
</div>
50+
51+
<div class="px-4 py-3 border-t border-slate-200 flex justify-end">
52+
<button
53+
class="px-4 py-2 bg-ext-wf1 hover:bg-ext-wf3 text-white text-sm font-medium rounded-lg transition-colors"
54+
@click="$emit('close')"
55+
>
56+
Close
57+
</button>
58+
</div>
59+
</div>
60+
</div>
61+
</template>
62+
63+
<script setup>
64+
import BaseIcon from '../base/BaseIcon.vue'
65+
66+
defineProps({
67+
isImportResultOpen: {
68+
type: Boolean,
69+
required: true
70+
},
71+
importResult: {
72+
type: Object,
73+
required: true,
74+
default: () => ({
75+
imported: 0,
76+
skipped: 0,
77+
errors: []
78+
})
79+
}
80+
})
81+
82+
defineEmits(['close'])
83+
</script>

assets/vue/components/subscribers/SubscriberDirectory.vue

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@
2626
</select>
2727
</div>
2828
<div class="flex gap-2">
29+
<input
30+
ref="fileInput"
31+
type="file"
32+
class="hidden"
33+
accept=".csv"
34+
@change="handleFileChange"
35+
>
2936
<button
30-
class="flex-[3] sm:flex-none px-4 py-2 bg-ext-wf1 hover:bg-ext-wf3 text-white text-sm font-medium rounded-lg flex items-center justify-center gap-2 transition-colors"
31-
@click="exportSubscribers"
37+
class="flex-[3] sm:flex-none px-4 py-2 bg-ext-wf1 hover:bg-ext-wf3 text-white text-sm font-medium rounded-lg flex items-center justify-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
38+
:disabled="isImporting"
39+
@click="importSubscribers"
3240
>
33-
<BaseIcon name="download" class="w-4 h-4" />
34-
<span class="flex items-center whitespace-nowrap">Export CSV</span>
41+
<BaseIcon v-if="!isImporting" name="upload" class="w-4 h-4" />
42+
<div v-else class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
43+
<span class="flex items-center whitespace-nowrap">{{ isImporting ? 'Importing...' : 'Import CSV' }}</span>
3544
</button>
3645
</div>
3746
</div>
@@ -47,6 +56,11 @@
4756
@close="closeSubscriberModal"
4857
@updated="handleSubscriberUpdated"
4958
/>
59+
<ImportResult
60+
:is-import-result-open="isImportResultOpen"
61+
:import-result="importResult"
62+
@close="isImportResultOpen = false"
63+
/>
5064
<div class="p-4 sm:p-6 border-t border-slate-200 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
5165
<div class="text-center sm:text-left">
5266
Showing <span class="font-medium text-slate-900">{{ subscribers.length }}</span> of <span class="font-medium text-slate-900">{{ pagination.total }}</span> subscribers
@@ -76,6 +90,7 @@ import BaseIcon from '../base/BaseIcon.vue'
7690
import SubscriberFilters from './SubscriberFilters.vue'
7791
import SubscriberTable from './SubscriberTable.vue'
7892
import SubscriberModal from './SubscriberModal.vue'
93+
import ImportResult from './ImportResult.vue'
7994
import { inject, ref, onMounted, watch } from 'vue'
8095
import { subscriberFilters } from './subscriberFilters'
8196
import { subscribersClient } from '../../api'
@@ -96,6 +111,14 @@ const searchQuery = ref('')
96111
const searchColumn = ref('email')
97112
const isModalOpen = ref(false)
98113
const selectedSubscriberId = ref(null)
114+
const fileInput = ref(null)
115+
const isImporting = ref(false)
116+
const isImportResultOpen = ref(false)
117+
const importResult = ref({
118+
imported: 0,
119+
skipped: 0,
120+
errors: []
121+
})
99122
let searchTimeout = null
100123
101124
const searchColumns = [
@@ -242,6 +265,33 @@ const handleFilterChange = (filterId) => {
242265
fetchSubscribers()
243266
}
244267
268+
const importSubscribers = () => {
269+
fileInput.value.click()
270+
}
271+
272+
const handleFileChange = async (event) => {
273+
const file = event.target.files[0]
274+
if (!file) return
275+
276+
isImporting.value = true
277+
try {
278+
const result = await subscribersClient.importSubscribers({
279+
file,
280+
listId: 0,
281+
updateExisting: true
282+
})
283+
importResult.value = result
284+
isImportResultOpen.value = true
285+
fetchSubscribers()
286+
} catch (error) {
287+
console.error('Failed to import subscribers:', error)
288+
alert('Failed to import subscribers. Please check the file and try again.')
289+
} finally {
290+
isImporting.value = false
291+
event.target.value = ''
292+
}
293+
}
294+
245295
const exportSubscribers = () => {
246296
const params = new URLSearchParams()
247297
if (currentFilter.value && currentFilter.value !== 'all') {

0 commit comments

Comments
 (0)