|
| 1 | +<template> |
| 2 | + <Dialog :open="open" @update:open="$emit('update:open', $event)"> |
| 3 | + <DialogContent class="sm:max-w-xl"> |
| 4 | + <DialogHeader> |
| 5 | + <DialogTitle>Merge Conversations</DialogTitle> |
| 6 | + <DialogDescription> |
| 7 | + Search for conversations to merge into this one. This action cannot be undone. |
| 8 | + </DialogDescription> |
| 9 | + </DialogHeader> |
| 10 | + |
| 11 | + <div class="space-y-4"> |
| 12 | + <Input |
| 13 | + v-model="searchQuery" |
| 14 | + placeholder="Search by reference #, subject, or contact..." |
| 15 | + @input="debouncedSearch" |
| 16 | + /> |
| 17 | + |
| 18 | + <div v-if="isSearching" class="flex justify-center py-4"> |
| 19 | + <Spinner /> |
| 20 | + </div> |
| 21 | + |
| 22 | + <div v-else-if="searchResults.length > 0" class="max-h-64 overflow-y-auto space-y-2"> |
| 23 | + <div |
| 24 | + v-for="conv in searchResults" |
| 25 | + :key="conv.uuid" |
| 26 | + class="flex items-center gap-3 p-3 border rounded-lg cursor-pointer hover:bg-muted" |
| 27 | + :class="{ 'border-primary bg-primary/5': selectedUuids.includes(conv.uuid) }" |
| 28 | + @click="toggleSelection(conv.uuid)" |
| 29 | + > |
| 30 | + <Checkbox :checked="selectedUuids.includes(conv.uuid)" /> |
| 31 | + <div class="flex-1 min-w-0"> |
| 32 | + <div class="flex items-center gap-2"> |
| 33 | + <span class="font-mono text-sm text-muted-foreground">#{{ conv.reference_number }}</span> |
| 34 | + <Badge variant="outline" class="text-xs">{{ conv.status }}</Badge> |
| 35 | + </div> |
| 36 | + <div class="text-sm font-medium truncate">{{ conv.subject || 'No subject' }}</div> |
| 37 | + <div class="text-xs text-muted-foreground"> |
| 38 | + {{ conv.contact.first_name }} {{ conv.contact.last_name }} |
| 39 | + </div> |
| 40 | + </div> |
| 41 | + </div> |
| 42 | + </div> |
| 43 | + |
| 44 | + <div v-else-if="searchQuery && !isSearching" class="text-center py-4 text-muted-foreground"> |
| 45 | + No conversations found |
| 46 | + </div> |
| 47 | + </div> |
| 48 | + |
| 49 | + <DialogFooter class="flex gap-2"> |
| 50 | + <Button variant="outline" @click="$emit('update:open', false)">Cancel</Button> |
| 51 | + <Button |
| 52 | + variant="destructive" |
| 53 | + :disabled="selectedUuids.length === 0 || isMerging" |
| 54 | + @click="confirmMerge" |
| 55 | + > |
| 56 | + <Spinner v-if="isMerging" class="mr-2 h-4 w-4" /> |
| 57 | + Merge {{ selectedUuids.length }} conversation{{ selectedUuids.length !== 1 ? 's' : '' }} |
| 58 | + </Button> |
| 59 | + </DialogFooter> |
| 60 | + </DialogContent> |
| 61 | + </Dialog> |
| 62 | + |
| 63 | + <AlertDialog :open="showConfirm" @update:open="showConfirm = $event"> |
| 64 | + <AlertDialogContent> |
| 65 | + <AlertDialogHeader> |
| 66 | + <AlertDialogTitle>Confirm Merge</AlertDialogTitle> |
| 67 | + <AlertDialogDescription> |
| 68 | + You are about to merge {{ selectedUuids.length }} conversation(s) into this one. |
| 69 | + All messages will be combined and the merged conversations will be closed. |
| 70 | + This action cannot be undone. |
| 71 | + </AlertDialogDescription> |
| 72 | + </AlertDialogHeader> |
| 73 | + <AlertDialogFooter> |
| 74 | + <AlertDialogCancel>Cancel</AlertDialogCancel> |
| 75 | + <AlertDialogAction @click="executeMerge" class="bg-destructive text-destructive-foreground hover:bg-destructive/90"> |
| 76 | + Merge |
| 77 | + </AlertDialogAction> |
| 78 | + </AlertDialogFooter> |
| 79 | + </AlertDialogContent> |
| 80 | + </AlertDialog> |
| 81 | +</template> |
| 82 | + |
| 83 | +<script setup> |
| 84 | +import { ref, watch } from 'vue' |
| 85 | +import { useDebounceFn } from '@vueuse/core' |
| 86 | +import api from '@/api' |
| 87 | +import { useEmitter } from '@/composables/useEmitter' |
| 88 | +import { EMITTER_EVENTS } from '@/constants/emitterEvents' |
| 89 | +import { handleHTTPError } from '@/utils/http' |
| 90 | +import { |
| 91 | + Dialog, |
| 92 | + DialogContent, |
| 93 | + DialogDescription, |
| 94 | + DialogFooter, |
| 95 | + DialogHeader, |
| 96 | + DialogTitle |
| 97 | +} from '@/components/ui/dialog' |
| 98 | +import { |
| 99 | + AlertDialog, |
| 100 | + AlertDialogAction, |
| 101 | + AlertDialogCancel, |
| 102 | + AlertDialogContent, |
| 103 | + AlertDialogDescription, |
| 104 | + AlertDialogFooter, |
| 105 | + AlertDialogHeader, |
| 106 | + AlertDialogTitle |
| 107 | +} from '@/components/ui/alert-dialog' |
| 108 | +import { Input } from '@/components/ui/input' |
| 109 | +import { Button } from '@/components/ui/button' |
| 110 | +import { Checkbox } from '@/components/ui/checkbox' |
| 111 | +import { Badge } from '@/components/ui/badge' |
| 112 | +import { Spinner } from '@/components/ui/spinner' |
| 113 | +
|
| 114 | +const props = defineProps({ |
| 115 | + open: Boolean, |
| 116 | + conversationUuid: String |
| 117 | +}) |
| 118 | +
|
| 119 | +const emit = defineEmits(['update:open', 'merged']) |
| 120 | +
|
| 121 | +const emitter = useEmitter() |
| 122 | +const searchQuery = ref('') |
| 123 | +const searchResults = ref([]) |
| 124 | +const selectedUuids = ref([]) |
| 125 | +const isSearching = ref(false) |
| 126 | +const isMerging = ref(false) |
| 127 | +const showConfirm = ref(false) |
| 128 | +
|
| 129 | +const search = async () => { |
| 130 | + if (!searchQuery.value.trim()) { |
| 131 | + searchResults.value = [] |
| 132 | + return |
| 133 | + } |
| 134 | +
|
| 135 | + isSearching.value = true |
| 136 | + try { |
| 137 | + const resp = await api.searchConversationsForMerge(props.conversationUuid, searchQuery.value) |
| 138 | + searchResults.value = resp.data.data || [] |
| 139 | + } catch (err) { |
| 140 | + emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { |
| 141 | + variant: 'destructive', |
| 142 | + description: handleHTTPError(err).message |
| 143 | + }) |
| 144 | + } finally { |
| 145 | + isSearching.value = false |
| 146 | + } |
| 147 | +} |
| 148 | +
|
| 149 | +const debouncedSearch = useDebounceFn(search, 300) |
| 150 | +
|
| 151 | +const toggleSelection = (uuid) => { |
| 152 | + const idx = selectedUuids.value.indexOf(uuid) |
| 153 | + if (idx === -1) { |
| 154 | + selectedUuids.value.push(uuid) |
| 155 | + } else { |
| 156 | + selectedUuids.value.splice(idx, 1) |
| 157 | + } |
| 158 | +} |
| 159 | +
|
| 160 | +const confirmMerge = () => { |
| 161 | + showConfirm.value = true |
| 162 | +} |
| 163 | +
|
| 164 | +const executeMerge = async () => { |
| 165 | + showConfirm.value = false |
| 166 | + isMerging.value = true |
| 167 | +
|
| 168 | + try { |
| 169 | + await api.mergeConversations(props.conversationUuid, selectedUuids.value) |
| 170 | + emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { |
| 171 | + description: `Merged ${selectedUuids.value.length} conversation(s) successfully` |
| 172 | + }) |
| 173 | + emit('merged') |
| 174 | + emit('update:open', false) |
| 175 | + } catch (err) { |
| 176 | + emitter.emit(EMITTER_EVENTS.SHOW_TOAST, { |
| 177 | + variant: 'destructive', |
| 178 | + description: handleHTTPError(err).message |
| 179 | + }) |
| 180 | + } finally { |
| 181 | + isMerging.value = false |
| 182 | + } |
| 183 | +} |
| 184 | +
|
| 185 | +watch(() => props.open, (isOpen) => { |
| 186 | + if (!isOpen) { |
| 187 | + searchQuery.value = '' |
| 188 | + searchResults.value = [] |
| 189 | + selectedUuids.value = [] |
| 190 | + } |
| 191 | +}) |
| 192 | +</script> |
0 commit comments