e3bc5da7e3
Co-authored-by: Copilot <copilot@github.com>
484 lines
14 KiB
Vue
484 lines
14 KiB
Vue
<script setup>
|
|
import { ref, watch, computed, nextTick } from "vue";
|
|
import axios from "axios";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/Components/ui/dialog";
|
|
import { router, usePage } from "@inertiajs/vue3";
|
|
import { useForm, Field as FormField } from "vee-validate";
|
|
import { toTypedSchema } from "@vee-validate/zod";
|
|
import * as z from "zod";
|
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@/Components/ui/form";
|
|
import { Input } from "@/Components/ui/input";
|
|
import { Textarea } from "@/Components/ui/textarea";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/Components/ui/select";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { ScrollArea } from "@/Components/ui/scroll-area";
|
|
|
|
const props = defineProps({
|
|
show: { type: Boolean, default: false },
|
|
email: { type: Object, default: null },
|
|
clientCaseUuid: { type: String, default: null },
|
|
emailTemplates: { type: Array, default: () => [] },
|
|
mailProfiles: { type: Array, default: () => [] },
|
|
});
|
|
|
|
const emit = defineEmits(["close"]);
|
|
|
|
const page = usePage();
|
|
const pageProps = computed(() => page?.props ?? {});
|
|
|
|
const pageEmailTemplates = computed(() => {
|
|
const fromProps =
|
|
Array.isArray(props.emailTemplates) && props.emailTemplates.length
|
|
? props.emailTemplates
|
|
: null;
|
|
return fromProps ?? pageProps.value?.email_templates ?? [];
|
|
});
|
|
|
|
const pageMailProfiles = computed(() => {
|
|
const fromProps =
|
|
Array.isArray(props.mailProfiles) && props.mailProfiles.length
|
|
? props.mailProfiles
|
|
: null;
|
|
return fromProps ?? pageProps.value?.mail_profiles ?? [];
|
|
});
|
|
|
|
// Form schema
|
|
const formSchema = toTypedSchema(
|
|
z.object({
|
|
subject: z.string().min(1, "Zadeva je obvezna.").max(255),
|
|
html_body: z.string().nullable().optional(),
|
|
body_text: z.string().max(10000).nullable().optional(),
|
|
template_id: z.number().nullable().optional(),
|
|
mail_profile_id: z.number().nullable().optional(),
|
|
contract_uuid: z.string().nullable().optional(),
|
|
})
|
|
);
|
|
|
|
const form = useForm({
|
|
validationSchema: formSchema,
|
|
initialValues: {
|
|
subject: "",
|
|
html_body: "",
|
|
body_text: "",
|
|
template_id: null,
|
|
mail_profile_id: null,
|
|
contract_uuid: null,
|
|
},
|
|
});
|
|
|
|
const processing = ref(false);
|
|
const contractsForCase = ref([]);
|
|
const hasBodyText = ref(false); // whether selected template uses {{body_text}}
|
|
|
|
// WYSIWYG iframe
|
|
const iframeRef = ref(null);
|
|
let iframeSyncing = false;
|
|
|
|
function ensureFullDoc(html) {
|
|
if (!html) {
|
|
return '<!doctype html><html><head><meta charset="utf-8" /></head><body></body></html>';
|
|
}
|
|
if (/<html[\s\S]*<\/html>/i.test(html)) return html;
|
|
return `<!doctype html><html><head><meta charset="utf-8" /></head><body>${html}</body></html>`;
|
|
}
|
|
|
|
function writeIframeDocument(html) {
|
|
const iframe = iframeRef.value;
|
|
if (!iframe) return;
|
|
const doc = iframe.contentDocument;
|
|
if (!doc) return;
|
|
const full = ensureFullDoc(html ?? form.values.html_body ?? "");
|
|
doc.open();
|
|
doc.write(full);
|
|
doc.close();
|
|
try {
|
|
doc.body.setAttribute("spellcheck", "false");
|
|
} catch {}
|
|
}
|
|
|
|
function initIframeEditor(html) {
|
|
writeIframeDocument(html);
|
|
const iframe = iframeRef.value;
|
|
if (!iframe) return;
|
|
const doc = iframe.contentDocument;
|
|
if (!doc) return;
|
|
try {
|
|
doc.designMode = "on";
|
|
} catch {}
|
|
|
|
const syncHandler = () => {
|
|
if (iframeSyncing) return;
|
|
try {
|
|
iframeSyncing = true;
|
|
const full = doc.documentElement.outerHTML;
|
|
form.setFieldValue("html_body", full);
|
|
} finally {
|
|
iframeSyncing = false;
|
|
}
|
|
};
|
|
|
|
doc.removeEventListener("input", syncHandler);
|
|
doc.removeEventListener("keyup", syncHandler);
|
|
doc.addEventListener("input", syncHandler);
|
|
doc.addEventListener("keyup", syncHandler);
|
|
}
|
|
|
|
function iframeExec(command) {
|
|
const iframe = iframeRef.value;
|
|
if (!iframe) return;
|
|
const doc = iframe.contentDocument;
|
|
if (!doc) return;
|
|
try {
|
|
doc.body.focus();
|
|
} catch {}
|
|
try {
|
|
doc.execCommand(command, false, null);
|
|
} catch (e) {
|
|
console.warn("execCommand failed", command, e);
|
|
}
|
|
}
|
|
|
|
// Load template preview from server
|
|
const loadingPreview = ref(false);
|
|
|
|
const updateFromTemplate = async () => {
|
|
if (!form.values.template_id || !props.clientCaseUuid) return;
|
|
loadingPreview.value = true;
|
|
try {
|
|
const url = route("clientCase.email.preview", {
|
|
client_case: props.clientCaseUuid,
|
|
email_id: props.email?.id,
|
|
});
|
|
const { data } = await axios.post(url, {
|
|
template_id: form.values.template_id,
|
|
contract_uuid: form.values.contract_uuid || null,
|
|
body_text: form.values.body_text || "",
|
|
});
|
|
const hadBodyText = hasBodyText.value;
|
|
hasBodyText.value = !!data?.has_body_text;
|
|
// Pre-fill body_text from text_template when the placeholder is present and field is empty
|
|
if (data?.has_body_text && !hadBodyText) {
|
|
const tpl = pageEmailTemplates.value.find((t) => t.id === form.values.template_id);
|
|
if (tpl?.text_template && !form.values.body_text) {
|
|
form.setFieldValue("body_text", tpl.text_template);
|
|
}
|
|
}
|
|
if (data?.subject) {
|
|
form.setFieldValue("subject", data.subject);
|
|
}
|
|
const html = data?.html ?? "";
|
|
form.setFieldValue("html_body", html);
|
|
await nextTick();
|
|
initIframeEditor(html);
|
|
} catch (e) {
|
|
// ignore
|
|
} finally {
|
|
loadingPreview.value = false;
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => form.values.template_id,
|
|
() => {
|
|
updateFromTemplate();
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => form.values.contract_uuid,
|
|
() => {
|
|
if (form.values.template_id) {
|
|
updateFromTemplate();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Re-preview when body_text changes (debounce-like: only when a template is active)
|
|
watch(
|
|
() => form.values.body_text,
|
|
() => {
|
|
if (form.values.template_id && hasBodyText.value) {
|
|
updateFromTemplate();
|
|
}
|
|
}
|
|
);
|
|
|
|
const loadContractsForCase = async () => {
|
|
try {
|
|
const url = route("clientCase.contracts.list", {
|
|
client_case: props.clientCaseUuid,
|
|
});
|
|
const res = await fetch(url, {
|
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
|
credentials: "same-origin",
|
|
});
|
|
const json = await res.json();
|
|
contractsForCase.value = Array.isArray(json?.data) ? json.data : [];
|
|
} catch (e) {
|
|
contractsForCase.value = [];
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => props.show,
|
|
async (newVal) => {
|
|
if (newVal) {
|
|
form.resetForm({
|
|
values: {
|
|
subject: "",
|
|
html_body: "",
|
|
body_text: "",
|
|
template_id: null,
|
|
mail_profile_id: pageMailProfiles.value?.[0]?.id ?? null,
|
|
contract_uuid: null,
|
|
},
|
|
});
|
|
hasBodyText.value = false;
|
|
contractsForCase.value = [];
|
|
await loadContractsForCase();
|
|
// Init empty iframe
|
|
await nextTick();
|
|
initIframeEditor("");
|
|
}
|
|
}
|
|
);
|
|
|
|
const closeDialog = () => {
|
|
emit("close");
|
|
};
|
|
|
|
const onSubmit = form.handleSubmit((values) => {
|
|
if (!props.email || !props.clientCaseUuid) return;
|
|
processing.value = true;
|
|
router.post(
|
|
route("clientCase.email.send", {
|
|
client_case: props.clientCaseUuid,
|
|
email_id: props.email.id,
|
|
}),
|
|
values,
|
|
{
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
processing.value = false;
|
|
closeDialog();
|
|
},
|
|
onError: () => {
|
|
processing.value = false;
|
|
},
|
|
onFinish: () => {
|
|
processing.value = false;
|
|
},
|
|
}
|
|
);
|
|
});
|
|
|
|
const open = computed({
|
|
get: () => props.show,
|
|
set: (value) => {
|
|
if (!value) closeDialog();
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog v-model:open="open">
|
|
<DialogContent class="sm:max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Pošlji e-pošto</DialogTitle>
|
|
<DialogDescription>
|
|
<p class="text-sm text-gray-600">
|
|
Prejemnik:
|
|
<span class="font-mono">{{ email?.value || email?.email || email?.address }}</span>
|
|
</p>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<ScrollArea class="max-h-[70vh] pr-1">
|
|
<form @submit.prevent="onSubmit" class="space-y-4 pr-3">
|
|
<!-- Mail profile -->
|
|
<FormField v-slot="{ value, handleChange }" name="mail_profile_id">
|
|
<FormItem>
|
|
<FormLabel>E-poštni profil</FormLabel>
|
|
<Select :model-value="value" @update:model-value="handleChange">
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="—" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem :value="null">—</SelectItem>
|
|
<SelectItem
|
|
v-for="p in pageMailProfiles"
|
|
:key="p.id"
|
|
:value="p.id"
|
|
>
|
|
{{ p.name || "Profil #" + p.id }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- Contract -->
|
|
<FormField v-slot="{ value, handleChange }" name="contract_uuid">
|
|
<FormItem>
|
|
<FormLabel>Pogodba</FormLabel>
|
|
<Select :model-value="value" @update:model-value="handleChange">
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="—" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem :value="null">—</SelectItem>
|
|
<SelectItem
|
|
v-for="c in contractsForCase"
|
|
:key="c.uuid"
|
|
:value="c.uuid"
|
|
>
|
|
{{ c.reference || c.uuid }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
Izberite pogodbo za zapolnitev spremenljivk v predlogi.
|
|
</p>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- Template -->
|
|
<FormField v-slot="{ value, handleChange }" name="template_id">
|
|
<FormItem>
|
|
<FormLabel>Predloga</FormLabel>
|
|
<Select
|
|
:model-value="value"
|
|
@update:model-value="handleChange"
|
|
:disabled="loadingPreview"
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="—" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem :value="null">—</SelectItem>
|
|
<SelectItem
|
|
v-for="t in pageEmailTemplates"
|
|
:key="t.id"
|
|
:value="t.id"
|
|
>
|
|
{{ t.name || "Predloga #" + t.id }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- Subject -->
|
|
<FormField v-slot="{ componentField }" name="subject">
|
|
<FormItem>
|
|
<FormLabel>Zadeva</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="text"
|
|
placeholder="Zadeva e-poštnega sporočila..."
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- body_text textarea — shown only when the template uses {{body_text}} -->
|
|
<FormField v-if="hasBodyText" v-slot="{ componentField }" name="body_text">
|
|
<FormItem>
|
|
<FormLabel>Besedilo sporočila</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Vnesite besedilo, ki se vstavi na mesto {{body_text}} v predlogi..."
|
|
class="min-h-[120px] resize-y"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
Besedilo se vstavi na oznako <code>{{body_text}}</code> v predlogi. Besedilo ne podpira spremenljivk.
|
|
</p>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- WYSIWYG body editor -->
|
|
<div>
|
|
<label class="text-sm font-medium leading-none">Vsebina</label>
|
|
<!-- Toolbar -->
|
|
<div class="flex gap-1 mt-2 mb-1 border rounded-t-md bg-gray-50 p-1">
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="ghost"
|
|
class="font-bold px-2 py-1 h-7"
|
|
title="Krepko (Ctrl+B)"
|
|
@click="iframeExec('bold')"
|
|
>B</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="ghost"
|
|
class="italic px-2 py-1 h-7"
|
|
title="Poševno (Ctrl+I)"
|
|
@click="iframeExec('italic')"
|
|
>I</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="ghost"
|
|
class="underline px-2 py-1 h-7"
|
|
title="Podčrtano (Ctrl+U)"
|
|
@click="iframeExec('underline')"
|
|
>U</Button>
|
|
</div>
|
|
<iframe
|
|
ref="iframeRef"
|
|
class="w-full border rounded-b-md bg-white"
|
|
style="min-height: 240px; max-height: 360px"
|
|
frameborder="0"
|
|
sandbox="allow-same-origin allow-scripts"
|
|
/>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
Kliknite v vsebino in začnite pisati. Izberite predlogo za samodejno zapolnitev.
|
|
</p>
|
|
</div>
|
|
</form>
|
|
</ScrollArea>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="closeDialog" :disabled="processing">
|
|
Prekliči
|
|
</Button>
|
|
<Button
|
|
@click="onSubmit"
|
|
:disabled="processing || !form.values.subject"
|
|
>
|
|
{{ processing ? "Pošiljanje..." : "Pošlji" }}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|