Skip to content

Commit 03bd8ea

Browse files
split main panel
1 parent 04afd27 commit 03bd8ea

File tree

3 files changed

+325
-188
lines changed

3 files changed

+325
-188
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<template lang="pug">
2+
.column
3+
.col-10
4+
.row(style="height: 100%")
5+
q-btn(icon="mdi-paperclip" label="Glissez vos fichiers ici"
6+
size="md" :class="isOverDropZone ? 'text-primary' : 'text-grey-5'" flat ref="dropZoneRef"
7+
).col-1.bg-grey-2
8+
q-badge(floating) {{ attachements.length }}
9+
//- client-only
10+
//- tk-tiptap-editor(v-model="message" ref="editorDialog")
11+
q-editor(
12+
v-model="message" placeholder="Votre message ..."
13+
:definitions="editorDefinitions"
14+
:toolbar="editorToolbar" dense style="height: 100%"
15+
).col
16+
template(v-slot:threadTypes)
17+
q-btn-dropdown(
18+
v-model="threadType" :options="threadTypes"
19+
no-wrap unelevated no-caps dense flat
20+
label="Type de thread" :icon="threadType.icon" :color="threadType.color"
21+
)
22+
q-list(dense)
23+
q-item(clickable v-for="threadType in threadTypes" :key="type.value" v-ripple tag="label")
24+
q-item-section
25+
q-item-label {{ type.label }}
26+
q-btn(
27+
icon="mdi-send" size="md" color="primary" flat
28+
@click="sendMessage"
29+
).col-1
30+
q-tooltip.text-body2 Envoyer
31+
32+
q-scroll-area(style="width: 100%").col
33+
q-virtual-scroll(:items="attachements" virtual-scroll-horizontal v-slot="{item}")
34+
q-chip(:key="item.id" icon="mdi-paperclip" dense size='md' :label="item.name" removable @remove="removeAttachment(item.id)")
35+
36+
q-dialog(v-model="isFullscreen")
37+
q-card
38+
q-card-section
39+
q-editor(
40+
min-height="50vh" min-width="50vw"
41+
v-model="message" placeholder="Votre message ..."
42+
:definitions="editorDefinitions"
43+
:toolbar="editorToolbar" class="q-pa-none"
44+
:disable="disabled" ref="dropZoneRef"
45+
)
46+
q-card-section.q-pa-sm
47+
div(ref="dropZoneDialogRef").row.center.bg-grey-3
48+
.col.text-center
49+
q-icon(name="mdi-paperclip" size="md" :class="isOverDropZoneDialog ? 'text-primary' : 'text-grey-5'")
50+
span.q-ml-md(:class="isOverDropZoneDialog ? 'text-primary' : 'text-grey-5'") Déposer un fichier
51+
q-card-section
52+
q-chip(v-for="attachement in attachements" :key="attachement.id" icon="mdi-paperclip" dense size='md' :label="attachement.name" removable @remove="removeAttachment(attachement.id)")
53+
54+
//- .col-1(ref="dropZoneRef").bg-grey-3.items-center.justify-center.q-pa-md
55+
//- q-icon(name="mdi-paperclip" size="md" :class="isOverDropZone ? 'text-primary' : 'text-grey-5'")
56+
//- span.q-ml-md(:class="isOverDropZone ? 'text-primary' : 'text-grey-5'") Déposer un fichier
57+
58+
</template>
59+
60+
<script lang="ts" setup>
61+
import { ref, onMounted, computed } from 'vue'
62+
import { useRoute, useRouter } from 'nuxt/app';
63+
import { useHttpApi } from '~/composables';
64+
import { useDayjs, usePinia } from "#imports";
65+
import { generateMongoId } from '~/utils';
66+
import type { components } from '#build/types/service-api'
67+
import { ThreadType, threadTypes } from '~/utils';
68+
import { useDropZone, useResizeObserver } from '@vueuse/core'
69+
import { FsType } from '~/utils'
70+
import { useQuasar } from 'quasar';
71+
import ObjectID from 'bson-objectid';
72+
73+
type ThreadCreateDto = components['schemas']['ThreadCreateDto']
74+
type FsPart = components["schemas"]["IdfsPartDto"]
75+
76+
const dayjs = useDayjs()
77+
const store = usePinia()
78+
const route = useRoute()
79+
const $q = useQuasar()
80+
const user = store.state.value.auth.user
81+
82+
onMounted(() => {
83+
currentThreadId.value = generateMongoId()
84+
})
85+
86+
const threadType = ref(threadTypes[0])
87+
const isFullscreen = ref(false)
88+
const message = ref('')
89+
const dropZoneRef = ref<HTMLDivElement>()
90+
const dropZoneDialogRef = ref<HTMLDivElement>()
91+
const editorDialog = ref()
92+
const currentThreadId = ref<ObjectID | null>(null)
93+
const attachements = ref<FsPart[]>([])
94+
95+
const onDrop = (files: File[] | null) => {
96+
if (!files) return
97+
for (const file of files) {
98+
uploadFile(file)
99+
}
100+
}
101+
102+
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
103+
const { isOverDropZone: isOverDropZoneDialog } = useDropZone(dropZoneDialogRef, onDrop)
104+
105+
const uploadFile = async (file: File) => {
106+
const formData = new FormData()
107+
formData.append('file', file)
108+
formData.append('namespace', 's3')
109+
formData.append('path', `/ticket/${route.params.id}/attachement/${currentThreadId}/${file.name}`)
110+
formData.append('type', FsType.FILE)
111+
const { data, error } = await useHttpApi(`core/filestorage`, {
112+
method: 'post',
113+
body: formData
114+
})
115+
console.log(error.value)
116+
if (error.value) {
117+
$q.notify({
118+
message: 'Impossible d\'envoyer le fichier',
119+
type: 'negative'
120+
})
121+
return
122+
}
123+
const fsPart: FsPart = {
124+
id: data.value?.data._id,
125+
name: file.name,
126+
namespace: data.value?.data.namespace,
127+
path: data.value?.data.path,
128+
mime: data.value?.data.mime
129+
}
130+
attachements.value.push(fsPart)
131+
$q.notify('Fichier envoyé')
132+
}
133+
134+
const removeAttachment = (id: string) => {
135+
const { data, error } = useHttpApi(`core/filestorage/${id}`, {
136+
method: 'delete'
137+
})
138+
if (error.value) {
139+
$q.notify({
140+
message: 'Impossible de supprimer le fichier',
141+
type: 'negative'
142+
})
143+
return
144+
}
145+
attachements.value = attachements.value.filter(attachement => attachement.id !== id)
146+
$q.notify('Fichier supprimé')
147+
}
148+
149+
const sendMessage = async () => {
150+
const payload: ThreadCreateDto = {
151+
_id: currentThreadId,
152+
attachments: attachements.value,
153+
ticketId: generateMongoId(route.params.id.toString()),
154+
fragments: [
155+
{
156+
id: generateMongoId(),
157+
disposition: 'raw',
158+
message: message.value
159+
}
160+
],
161+
metadata: {
162+
createdBy: user.username,
163+
createdAt: dayjs().toISOString(),
164+
lastUpdatedAt: dayjs().toISOString(),
165+
lastUpdatedBy: user.username
166+
},
167+
type: ThreadType.OUTGOING
168+
}
169+
const { data: thread, error } = await useHttpApi(`tickets/thread`, {
170+
method: 'post',
171+
body: payload
172+
})
173+
if (error.value) {
174+
$q.notify({
175+
message: 'Impossible d\'envoyer le message',
176+
type: 'negative'
177+
})
178+
return
179+
}
180+
threads.value?.data.push(thread.value?.data)
181+
message.value = ''
182+
scroll()
183+
attachements.value = []
184+
$q.notify('Message envoyé')
185+
186+
}
187+
188+
const editorDefinitions = computed(() => (
189+
{
190+
send: {
191+
tip: 'Envoyer',
192+
icon: 'mdi-send',
193+
label: 'Envoyer',
194+
handler: sendMessage
195+
},
196+
attach: {
197+
tip: 'Joindre un fichier',
198+
icon: 'mdi-paperclip',
199+
label: 'Joindre un fichier',
200+
handler: () => {
201+
console.log('joindre')
202+
}
203+
},
204+
fullscreen: {
205+
tip: 'Plein écran',
206+
icon: isFullscreen.value ? 'mdi-fullscreen-exit' : 'mdi-fullscreen',
207+
label: isFullscreen.value ? 'Quitter le plein écran' : 'Plein écran',
208+
handler: () => {
209+
isFullscreen.value = !isFullscreen.value
210+
}
211+
}
212+
}))
213+
214+
const editorToolbar = computed(() => {
215+
return [['left', 'center', 'right', 'justify'], ['bold', 'italic', 'underline', 'strike'], ['undo', 'redo'], ['fullscreen']]
216+
})
217+
218+
</script>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<template lang="pug">
2+
q-scroll-area(ref="chatScroll")
3+
div(v-if="threads.total === 0").flex.items-center.justify-center
4+
span Aucun message
5+
div(v-for='(value, key) in getMessageByDay' :key='key')
6+
q-chat-message
7+
template(v-slot:label)
8+
span {{ key }}
9+
q-separator(inset)
10+
11+
div(v-for="(message, index) in value" :id="message._id").q-mx-md
12+
//- div(v-for="fragment in message.fragments")
13+
q-chat-message(
14+
:sent="message.metadata.createdBy === user.username"
15+
:name="message.metadata.createdBy" size="10"
16+
)
17+
template(v-slot:stamp)
18+
span {{ getTimeFrom(message.metadata.createdAt) }}
19+
q-tooltip.text-body2 {{ getHour(message.metadata.createdAt) }}
20+
template(v-slot:default)
21+
div
22+
q-chip(v-for='attachment in message.attachments' :key='attachment._id' icon="mdi-paperclip" text-color="white" color="primary" dense size='md' :label="attachment.name")
23+
q-separator.q-my-xs(v-if="message.fragments.file")
24+
div(v-for='raw in message.fragments.raw' v-html="raw.message")
25+
</template>
26+
27+
<script lang="ts" setup>
28+
import { ref, onMounted, computed } from 'vue'
29+
import { useRoute, useRouter } from 'nuxt/app';
30+
import { useHttpApi } from '~/composables';
31+
import { useDayjs, usePinia } from "#imports";
32+
import { generateMongoId } from '~/utils';
33+
import type { Fragments, Threads } from '~/types';
34+
import type { components } from '#build/types/service-api'
35+
import { FsType } from '~/utils'
36+
import { useQuasar } from 'quasar';
37+
38+
type ThreadDto = components['schemas']['ThreadDto']
39+
40+
const dayjs = useDayjs()
41+
const store = usePinia()
42+
const route = useRoute()
43+
const $q = useQuasar()
44+
const user = store.state.value.auth.user
45+
46+
onMounted(() => {
47+
scroll()
48+
})
49+
const chatScroll = ref(null)
50+
const baseQuery = ref({
51+
"filters[ticketId]": `${route.params.id}`,
52+
"sort[metadata.createdAt]": 'asc',
53+
"limit": 999,
54+
})
55+
const { data: threads } = await useHttpApi(`tickets/thread`, {
56+
method: 'get',
57+
query: computed(() => {
58+
return {
59+
...baseQuery.value,
60+
...route.query
61+
}
62+
})
63+
})
64+
65+
const getMessageByDay = computed((): Threads => {
66+
return threads.value?.data.reduce((acc: Threads, thread: ThreadDto) => {
67+
const day = dayjs(thread.metadata.createdAt).format('DD-MM-YYYY')
68+
const newTread = {
69+
...thread,
70+
fragments: getThreadFragments(thread)
71+
}
72+
if (!acc[day]) acc[day] = []
73+
acc[day].push(newTread)
74+
return acc
75+
}, {})
76+
})
77+
78+
const getThreadFragments = (thread: ThreadDto): Fragments => {
79+
return thread.fragments.reduce((acc: Fragments, fragment) => {
80+
const disposition = fragment.disposition
81+
if (!acc[disposition]) acc[disposition] = []
82+
acc[disposition]?.push(fragment)
83+
return acc as Fragments
84+
}, {})
85+
}
86+
87+
88+
const scroll = () => {
89+
const target = chatScroll.value?.getScrollTarget()
90+
chatScroll.value?.setScrollPosition('vertical', target.scrollHeight, 0)
91+
}
92+
93+
94+
const getTimeFrom = (time: string) => {
95+
return dayjs().to(dayjs(time))
96+
}
97+
98+
const getHour = (time: string) => {
99+
return dayjs(time).format('DD-MM-YYYY HH:mm')
100+
}
101+
102+
103+
</script>

0 commit comments

Comments
 (0)