Added E-Mail Sending

This commit is contained in:
2024-09-30 18:34:16 +02:00
parent a75506b183
commit 782f97afcc
6 changed files with 681 additions and 34 deletions

View File

@@ -1,72 +1,83 @@
<template>
<div>
<div class="p-3 mt-3 editor">
<div v-if="editor">
<InputGroup class="mx-3 mt-3">
<InputGroup class="mb-3">
<UButton
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
icon="i-heroicons-bold"
variant="outline"
/>
<UButton
@click="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
icon="i-heroicons-italic"
variant="outline"
/>
<UButton
@click="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
icon="i-mdi-format-strikethrough"
variant="outline"
/>
<UButton
<!--<UButton
@click="editor.chain().focus().toggleCode().run()"
:disabled="!editor.can().chain().focus().toggleCode().run()"
:class="{ 'is-active': editor.isActive('code') }"
icon="i-heroicons-code-bracket"
variant="outline"
/>
<!-- <UButton @click="editor.chain().focus().unsetAllMarks().run()">
<UButton @click="editor.chain().focus().unsetAllMarks().run()">
clear marks
</UButton>
<UButton @click="editor.chain().focus().clearNodes().run()">
clear nodes
</UButton>-->
</UButton>
<UButton
@click="editor.chain().focus().setParagraph().run()"
:class="{ 'is-active': editor.isActive('paragraph') }"
variant="outline"
>
paragraph
</UButton>
</UButton>-->
<UButton
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
variant="outline"
>h1</UButton>
<UButton
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
variant="outline"
>h2</UButton>
<UButton
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
variant="outline"
>
h3
</UButton>
<UButton
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
variant="outline"
>
h4
</UButton>
<UButton
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
variant="outline"
>
h5
</UButton>
<UButton
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
variant="outline"
>
h6
</UButton>
@@ -74,11 +85,13 @@
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
icon="i-heroicons-list-bullet"
variant="outline"
/>
<UButton
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
icon="i-mdi-format-list-numbered"
variant="outline"
/>
<!-- <UButton
@click="editor.chain().focus().toggleCodeBlock().run()"
@@ -120,12 +133,40 @@
</template>
<script setup>
const emit = defineEmits(['updateContent'])
const props = defineProps({
preloadedContent: {
type: String,
required:false
}
})
const {preloadedContent} = props
const editor = useEditor({
content: "<p>I'm running Tiptap with Vue.js. 🎉</p>",
extensions: [TiptapStarterKit],
onUpdate({editor}) {
console.log(editor.getJSON())
console.log(editor.getHTML())
console.log(editor.getText())
emit('updateContent',{json: editor.getJSON(),html: editor.getHTML(), text: editor.getText()})
},
onCreate({editor}) {
editor.commands.setContent(preloadedContent)
emit('updateContent',{json: editor.getJSON(),html: editor.getHTML(), text: editor.getText()})
}
});
onBeforeUnmount(() => {
unref(editor).destroy();
});
</script>
</script>
<style scoped>
.editor {
border: 1px solid #69c350;
border-radius: 10px;
}
</style>

View File

@@ -136,33 +136,65 @@ setupPage()
v-model="openTab"
>
<template #item="{item}">
<UCard class="mt-5">
<div v-if="item.label === 'Informationen'">
<div class="text-wrap mt-3">
<p v-if="itemInfo.customer">Kunde: <nuxt-link :to="`/customers/show/${itemInfo.customer.id}`">{{itemInfo.customer ? itemInfo.customer.name : ""}}</nuxt-link></p>
<p v-if="itemInfo.vendor">Lieferant: <nuxt-link :to="`/vendors/show/${itemInfo.vendor.id}`">{{itemInfo.vendor ? itemInfo.vendor.name : ""}}</nuxt-link></p>
<p>E-Mail: {{itemInfo.email}}</p>
<p>Mobil: {{itemInfo.phoneMobile}}</p>
<p>Festnetz: {{itemInfo.phoneHome}}</p>
<p>Rolle: {{itemInfo.role}}</p>
<p>Geburtstag: {{itemInfo.birthday ? dayjs(itemInfo.birthday).format("DD.MM.YYYY") : ""}}</p>
<p>Notizen:<br> {{itemInfo.notes}}</p>
</div>
<div v-if="item.label === 'Informationen'" class="flex flex-row mt-5">
<div class="w-1/2 mr-5">
<UCard>
<Toolbar>
<UButton
@click="router.push(`/email/new?to=${itemInfo.email}`)"
icon="i-heroicons-envelope"
:disabled="!itemInfo.email"
>
E-Mail
</UButton>
</Toolbar>
<table class="w-full">
<tr>
<td>Kunde: </td>
<td><nuxt-link v-if="itemInfo.customer" :to="`/customers/show/${itemInfo.customer?.id}`">{{itemInfo?.customer?.name}}</nuxt-link></td>
</tr>
<tr>
<td>Lieferant: </td>
<td><nuxt-link v-if="itemInfo.vendor" :to="`/customers/show/${itemInfo.vendor?.id}`">{{itemInfo?.vendor?.name}}</nuxt-link></td>
</tr>
<tr>
<td>E-Mail:</td>
<td>{{itemInfo.email}}</td>
</tr>
<tr>
<td>Mobil:</td>
<td>{{itemInfo.phoneMobile}}</td>
</tr>
<tr>
<td>Festnetz:</td>
<td>{{itemInfo.phoneHome}}</td>
</tr>
<tr>
<td>Rolle:</td>
<td>{{itemInfo.role}}</td>
</tr>
<tr>
<td>Geburtstag:</td>
<td>{{itemInfo.birthday ? dayjs(itemInfo.birthday).format("DD.MM.YYYY") : ""}}</td>
</tr>
<tr>
<td>Notizen:</td>
<td>{{itemInfo.notes}}</td>
</tr>
</table>
</UCard>
</div>
<div v-else-if="item.label === 'Logbuch'">
<HistoryDisplay
type="contact"
v-if="itemInfo"
:element-id="itemInfo.id"
/>
<div class="w-1/2">
<UCard>
<HistoryDisplay
type="contact"
v-if="itemInfo"
:element-id="itemInfo.id"
/>
</UCard>
</div>
</UCard>
</div>
</template>
</UTabs>
<UForm
@@ -289,5 +321,10 @@ setupPage()
</template>
<style scoped>
td {
border-bottom: 1px solid lightgrey;
vertical-align: top;
padding-bottom: 0.15em;
padding-top: 0.15em;
}
</style>

View File

@@ -9,16 +9,19 @@ defineShortcuts({
},
})
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const itemInfo = ref({})
const linkedDocument =ref({})
const setupPage = () => {
const setupPage = async () => {
if(route.params) {
if(route.params.id) itemInfo.value = dataStore.getCreatedDocumentById(Number(route.params.id))
if(route.params.id) itemInfo.value = await useSupabaseSelectSingle("createddocuments",route.params.id,"*")
linkedDocument.value = (await supabase.from("documents").select("id").eq("createdDocument", route.params.id).single()).data
}
}
@@ -47,9 +50,17 @@ setupPage()
>
Übernehmen
</UButton>
<UButton
@click="router.push(`/email/new?loadDocuments=[${linkedDocument.id}]`)"
icon="i-heroicons-envelope"
>
E-Mail
</UButton>
</template>
</UDashboardToolbar>
{{linkedDocument}}
<object
:data="dataStore.documents.find(i => i.createdDocument === itemInfo.id) ? dataStore.documents.find(i => i.createdDocument === itemInfo.id).url : ''"
class="h-full"

279
pages/email/index.vue Normal file
View File

@@ -0,0 +1,279 @@
<script setup>
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const selectedTab = ref(0)
const selectedMail = ref(null)
const selectedMailboxPath = ref("INBOX")
const emails = ref([])
const accountData = ref(null)
const availableAccounts = ref(null)
const selectedAccount = ref(null)
const setupPage = async () => {
availableAccounts.value = (await supabase.from("emailAccounts").select("*").contains("profiles",[dataStore.activeProfile.id])).data
console.log(availableAccounts.value)
if(availableAccounts.value.length > 0) {
selectedAccount.value = availableAccounts.value[0].id
accountData.value = await useSupabaseSelectSingle("emailAccounts", selectedAccount.value)
emails.value = await useSupabaseSelect("emailMessages", "*", "date", false)
}
}
const filteredMails = computed(() => {
let temp = emails.value.filter(i => i.mailboxPath.toLowerCase() === selectedMailboxPath.value.toLowerCase())
if(selectedTab.value === 0){
return temp
} else {
return temp.filter(i => !i.seen)
}
return emails.value
})
const isMailPanelOpen = computed({
get() {
return !!selectedMail.value
},
set(value) {
if (!value) {
selectedMail.value = null
}
}
})
const changeSeen = async (seen) => {
const {data,error} = await supabase.functions.invoke('emailcontrol',{
body: {
method: seen ? "makeSeen" : "makeUnseen",
emailId: selectedMail.value.id
}
})
if(data) {
setupPage()
}
}
setupPage()
</script>
<template>
<!-- <UDashboardNavbar title="E-Mails">
</UDashboardNavbar>
<UDashboardToolbar>
</UDashboardToolbar>-->
<UDashboardPage
v-if="selectedAccount"
>
<UDashboardPanel
id="inbox"
:width="400"
:resizable="{ min: 300, max: 500 }"
>
<UDashboardNavbar>
<template #left>
<USelectMenu
v-if="accountData"
class="w-40"
:options="accountData.mailboxes"
option-attribute="name"
value-attribute="path"
v-model="selectedMailboxPath"
/>
</template>
<template #right>
<UButton
icon="i-heroicons-arrow-path"
variant="ghost"
color="gray"
@click="setupPage"
/>
<UTabs
v-model="selectedTab"
:items="[{label: 'All'}, {label: 'Ungelesen'}]"
:ui="{ wrapper: '', list: { height: 'h-9', tab: { height: 'h-7', size: 'text-[13px]' } } }"
/>
</template>
</UDashboardNavbar>
<!-- ~/components/inbox/InboxList.vue -->
<InboxList
v-model="selectedMail"
:mails="filteredMails"
@emailSelected=" !selectedMail.seen ? changeSeen(true): ''"
/>
</UDashboardPanel>
<UDashboardPanel
v-model="isMailPanelOpen"
collapsible
grow
side="right"
>
<template v-if="selectedMail">
<UDashboardNavbar>
<template #toggle>
<UDashboardNavbarToggle icon="i-heroicons-x-mark" />
<UDivider
orientation="vertical"
class="mx-1.5 lg:hidden"
/>
</template>
<template #left>
<UTooltip text="Ungelesen">
<UButton
icon="i-heroicons-eye-slash"
color="gray"
variant="ghost"
@click="changeSeen(false)"
/>
</UTooltip>
<!-- <UTooltip text="Archive">
<UButton
icon="i-heroicons-archive-box"
color="gray"
variant="ghost"
/>
</UTooltip>
<UTooltip text="Move to junk">
<UButton
icon="i-heroicons-archive-box-x-mark"
color="gray"
variant="ghost"
/>
</UTooltip>
<UDivider
orientation="vertical"
class="mx-1.5"
/>
<UPopover :popper="{ placement: 'bottom-start' }">
<template #default="{ open }">
<UTooltip
text="Snooze"
:prevent="open"
>
<UButton
icon="i-heroicons-clock"
color="gray"
variant="ghost"
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
/>
</UTooltip>
</template>
<template #panel="{ close }">
<DatePicker @close="close" />
</template>
</UPopover>-->
</template>
<template #right>
<UTooltip text="Reply">
<UButton
icon="i-heroicons-arrow-uturn-left"
color="gray"
variant="ghost"
/>
</UTooltip>
<UTooltip text="Forward">
<UButton
icon="i-heroicons-arrow-uturn-right"
color="gray"
variant="ghost"
/>
</UTooltip>
<!-- <UDivider
orientation="vertical"
class="mx-1.5"
/>
<UDropdown :items="dropdownItems">
<UButton
icon="i-heroicons-ellipsis-vertical"
color="gray"
variant="ghost"
/>
</UDropdown>-->
</template>
</UDashboardNavbar>
<!-- ~/components/inbox/InboxMail.vue -->
<InboxMail :mail="selectedMail" />
</template>
<div
v-else
class="flex-1 hidden lg:flex items-center justify-center"
>
<UIcon
name="i-heroicons-inbox"
class="w-32 h-32 text-gray-400 dark:text-gray-500"
/>
</div>
</UDashboardPanel>
</UDashboardPage>
<div
v-else
class="flex-1 flex-col hidden lg:flex items-center justify-center"
>
<UIcon
name="i-heroicons-inbox"
class="w-32 h-32 text-gray-400 dark:text-gray-500"
/>
<span class="font-bold text-2xl">Kein E-Mail Account verfügbar</span>
</div>
<!-- <UInput
placeholder="Empfänger"
variant="ghost"
class="m-2"
/>
<UInput
placeholder="Betreff"
variant="ghost"
class="m-2"
/>
<UInput
placeholder="CC"
variant="ghost"
class="m-2"
/>
<UInput
placeholder="BCC"
variant="ghost"
class="m-2"
/>
<UDivider
class="my-5"
/>
<Tiptap/>-->
</template>
<style scoped>
</style>

279
pages/email/new.vue Normal file
View File

@@ -0,0 +1,279 @@
<script setup>
const supabase = useSupabaseClient()
const dataStore = useDataStore()
const route = useRoute()
const router = useRouter()
const toast = useToast()
const emailData = ref({
to:"",
cc:"",
bcc: "",
subject: "",
html: "",
text: "",
account: "",
})
const emailAccounts = ref([])
const preloadedContent = ref("")
const loadedDocuments = ref([])
const loaded = ref(false)
const setupPage = async () => {
emailAccounts.value = await useSupabaseSelect("emailAccounts")
emailData.value.account = emailAccounts.value[0].id
preloadedContent.value = `<p></p><p></p><p></p>${dataStore.activeProfile.emailSignature}`
//Check Query
if(route.query.to) emailData.value.to = route.query.to
if(route.query.cc) emailData.value.to = route.query.cc
if(route.query.bcc) emailData.value.to = route.query.bcc
if(route.query.subject) emailData.value.to = route.query.subject
if(route.query.loadDocuments) {
console.log(JSON.parse(route.query.loadDocuments))
const {data,error} = await supabase.from("documents").select('*, createdDocument(id,documentNumber,title,contact(email))').in('id',JSON.parse(route.query.loadDocuments))
if(error) console.log(error)
if(data) loadedDocuments.value = data
console.log(loadedDocuments.value)
if(loadedDocuments.value.length > 0) {
emailData.value.subject = loadedDocuments.value[0].createdDocument.title
emailData.value.to = loadedDocuments.value[0].createdDocument.contact.email
}
}
loaded.value = true
}
setupPage()
const contentChanged = (content) => {
emailData.value.html = content.html
emailData.value.text = content.text
}
const selectedAttachments = ref([])
const renderAttachments = () => {
selectedAttachments.value = Array.from(document.getElementById("inputAttachments").files).map(i => {
return {
filename: i.name,
type: i.type
}})
}
const toBase64 = file => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(",")[1]);
reader.onerror = reject;
});
function blobToBase64(blob) {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(",")[1]);
reader.readAsDataURL(blob);
});
}
const sendEmail = async () => {
loaded.value = false
let body = {
...emailData.value,
attachments: []
}
for await (const file of Array.from(document.getElementById("inputAttachments").files)) {
body.attachments.push({
filename: file.name,
content: await toBase64(file),
contentType: file.type,
encoding: "base64",
contentDisposition: "attachment"
})
}
for await (const doc of loadedDocuments.value) {
const {data,error} = await supabase.storage.from("files").download(doc.path)
body.attachments.push({
filename: doc.path.split("/")[doc.path.split("/").length -1],
content: await blobToBase64(data),
contentType: data.type,
encoding: "base64",
contentDisposition: "attachment"
})
}
const { data, error } = await supabase.functions.invoke('send_email', {
body
})
if(error) {
toast.add({title: "Fehler beim Absenden der E-Mail", color: "rose"})
} else if(data) {
router.push("/")
toast.add({title: "E-Mail zum Senden eingereiht"})
}
loaded.value = true
}
</script>
<template>
<UProgress animation="carousel" v-if="!loaded" class="mt-5 w-2/3 mx-auto"/>
<div v-else>
<UDashboardNavbar
title="Neue E-Mail"
>
<template #right>
<UButton
@click="sendEmail"
:disabled="!emailData.to || !emailData.subject"
>
Senden
</UButton>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<div class="flex-col flex w-full">
<UFormGroup
label="Absender"
>
<USelectMenu
:options="emailAccounts"
option-attribute="emailAddress"
value-attribute="id"
v-model="emailData.account"
/>
</UFormGroup>
<UInput
class="w-full my-1"
placeholder="Empfänger"
variant="ghost"
v-model="emailData.to"
/>
<UInput
class="w-full my-1"
placeholder="Kopie"
variant="ghost"
v-model="emailData.cc"
/>
<UInput
class="w-full my-1"
placeholder="Blindkopie"
variant="ghost"
v-model="emailData.bcc"
/>
<UInput
placeholder="Betreff"
class="w-full my-1"
variant="ghost"
v-model="emailData.subject"
/>
</div>
</UDashboardToolbar>
<UDashboardPanelContent>
<div id="parentAttachments" class="flex flex-col justify-center">
<span class="font-medium mb-2 text-xl">Anhänge</span>
<!-- <UIcon
name="i-heroicons-paper-clip"
class="mx-auto w-10 h-10"
/>
<span class="text-center text-2xl">Anhänge hochladen</span>-->
<input
id="inputAttachments"
type="file"
multiple
@change="renderAttachments"
/>
<ul class="mx-5 mt-3">
<li
class="list-disc"
v-for="file in selectedAttachments"
> Datei - {{file.filename}}</li>
<li
class="list-disc"
v-for="doc in loadedDocuments"
>
<span v-if="doc.createdDocument">Dokument - {{doc.createdDocument.documentNumber}}</span>
</li>
</ul>
</div>
<Tiptap
@updateContent="contentChanged"
:preloadedContent="preloadedContent"
/>
</UDashboardPanelContent>
</div>
</template>
<style scoped>
#parentAttachments {
border: 1px dashed #69c350;
border-radius: 10px;
padding: 1em;
}
#inputAttachments {
/*
display: none;
*/
display: inline-block;
cursor: pointer;
opacity: 100;/*
width: 100%;
height: 5%;
position: relative;
top: 0;
bottom: 0;
left: 0;
right: 0;*/
}
#inputAttachments::file-selector-button {
background-color: white;
border: 1px solid #69c350;
border-radius: 5px;
padding: 5px 10px 5px 10px;
}
.fileListItem {
border: 1px solid #69c350;
border-radius: 5px;
padding: .5rem;
}
</style>