Files
Teren-app/resources/js/Pages/Admin/MailProfiles/Index.vue
T

576 lines
20 KiB
Vue

<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import { Head, Link, useForm } from "@inertiajs/vue3";
import { ref, computed } from "vue";
import {
PlusIcon,
FlaskConicalIcon,
MailIcon,
PencilIcon,
SendIcon,
MoreVerticalIcon,
Trash2Icon,
} from "lucide-vue-next";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Badge } from "@/Components/ui/badge";
import { Switch } from "@/Components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
const props = defineProps({
profiles: { type: Array, default: () => [] },
});
const createOpen = ref(false); // create modal
const editOpen = ref(false); // edit modal
const editTarget = ref(null); // profile being edited
// Signature items — array of {key, value} pairs edited in the dialog
const signatureItems = ref([{ key: "", value: "" }]);
function addSignatureItem() {
signatureItems.value.push({ key: "", value: "" });
}
function removeSignatureItem(index) {
signatureItems.value.splice(index, 1);
if (signatureItems.value.length === 0)
signatureItems.value.push({ key: "", value: "" });
}
function signatureToObject() {
const obj = {};
signatureItems.value.forEach(({ key, value }) => {
const k = (key || "").trim();
if (k) obj[k] = value ?? "";
});
return Object.keys(obj).length ? obj : null;
}
function signatureFromObject(sig) {
const entries = Object.entries(sig || {});
return entries.length
? entries.map(([key, value]) => ({ key, value }))
: [{ key: "", value: "" }];
}
const form = useForm({
name: "",
host: "",
port: 587,
encryption: "tls",
username: "",
password: "",
from_address: "",
from_name: "",
priority: 10,
auto_mailer: false,
});
function openCreate() {
form.reset();
signatureItems.value = [{ key: "", value: "" }];
createOpen.value = true;
editTarget.value = null;
}
function openEdit(p) {
// populate form with existing profile data (exclude password which is write-only)
form.reset();
form.name = p.name || "";
form.host = p.host || "";
form.port = p.port || 587;
form.encryption = p.encryption || "";
form.username = p.username || "";
form.password = ""; // empty -> keep existing unless user fills
form.from_address = p.from_address || "";
form.from_name = p.from_name || "";
form.priority = p.priority ?? 10;
form.auto_mailer = p.auto_mailer ?? false;
editTarget.value = p;
signatureItems.value = signatureFromObject(p.signature);
editOpen.value = true;
}
function closeCreate() {
if (form.processing) return;
createOpen.value = false;
}
function submitCreate() {
form
.transform((data) => ({ ...data, signature: signatureToObject() }))
.post(route("admin.mail-profiles.store"), {
preserveScroll: true,
onSuccess: () => {
createOpen.value = false;
},
});
}
function closeEdit() {
if (form.processing) return;
editOpen.value = false;
editTarget.value = null;
}
function submitEdit() {
if (!editTarget.value) return;
// Build payload excluding empty password
const payload = {
name: form.name,
host: form.host,
port: form.port,
encryption: form.encryption || null,
username: form.username || null,
from_address: form.from_address,
from_name: form.from_name || null,
priority: form.priority,
auto_mailer: form.auto_mailer,
signature: signatureToObject(),
};
if (form.password && form.password.trim() !== "") {
payload.password = form.password.trim();
}
form
.transform(() => payload)
.put(route("admin.mail-profiles.update", editTarget.value.id), {
preserveScroll: true,
onSuccess: () => {
editOpen.value = false;
editTarget.value = null;
},
});
}
function toggleActive(p) {
window.axios
.post(route("admin.mail-profiles.toggle", p.id))
.then(() => window.location.reload());
}
function toggleAutoMailer(p) {
window.axios
.post(route("admin.mail-profiles.toggle-auto-mailer", p.id))
.then(() => window.location.reload());
}
function testConnection(p) {
window.axios
.post(route("admin.mail-profiles.test", p.id))
.then(() => window.location.reload());
}
function sendTestEmail(p) {
window.axios
.post(route("admin.mail-profiles.send-test", p.id))
.then(() => window.location.reload());
}
const statusClass = (p) => {
if (p.test_status === "success") return "text-emerald-600";
if (p.test_status === "failed") return "text-rose-600";
if (p.test_status === "queued") return "text-amber-500";
return "text-gray-400";
};
</script>
<template>
<AdminLayout title="Mail profili">
<Head title="Mail profili" />
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<div class="flex items-start gap-3">
<div
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
>
<MailIcon class="h-5 w-5" />
</div>
<div>
<CardTitle>Mail profili</CardTitle>
<CardDescription
>Upravljajte SMTP profile za pošiljanje e-pošte ({{
profiles.length
}})</CardDescription
>
</div>
</div>
<Button @click="openCreate">
<PlusIcon class="h-4 w-4 mr-2" />
Nov profil
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Ime</TableHead>
<TableHead>Host</TableHead>
<TableHead class="text-center">Port</TableHead>
<TableHead class="text-center">Enc</TableHead>
<TableHead class="text-center">Aktivno</TableHead>
<TableHead class="text-center">Auto-mailer</TableHead>
<TableHead class="text-center">Status</TableHead>
<TableHead>Zadnji uspeh</TableHead>
<TableHead>Napaka</TableHead>
<TableHead class="text-right">Akcije</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="p in profiles" :key="p.id">
<TableCell class="font-medium">{{ p.name }}</TableCell>
<TableCell>{{ p.host }}</TableCell>
<TableCell class="text-center">{{ p.port }}</TableCell>
<TableCell class="text-center">
<Badge v-if="p.encryption" variant="outline">{{
p.encryption.toUpperCase()
}}</Badge>
<span v-else class="text-muted-foreground"></span>
</TableCell>
<TableCell class="text-center">
<Switch
:default-value="p.active"
@update:model-value="() => toggleActive(p)"
/>
</TableCell>
<TableCell class="text-center">
<Switch
:default-value="p.auto_mailer"
@update:model-value="() => toggleAutoMailer(p)"
/>
</TableCell>
<TableCell class="text-center">
<Badge
v-if="p.test_status === 'success'"
variant="default"
class="bg-green-100 text-green-800 hover:bg-green-100"
>{{ p.test_status }}</Badge
>
<Badge v-else-if="p.test_status === 'failed'" variant="destructive">{{
p.test_status
}}</Badge>
<Badge
v-else-if="p.test_status === 'queued'"
variant="secondary"
class="bg-amber-100 text-amber-800 hover:bg-amber-100"
>{{ p.test_status }}</Badge
>
<span v-else class="text-muted-foreground">—</span>
</TableCell>
<TableCell class="text-sm text-muted-foreground">
{{
p.last_success_at ? new Date(p.last_success_at).toLocaleString() : ""
}}
</TableCell>
<TableCell
class="text-sm text-destructive max-w-[200px] truncate"
:title="p.last_error_message"
>
{{ p.last_error_message || "" }}
</TableCell>
<TableCell class="text-right">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="sm">
<MoreVerticalIcon class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Akcije</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem @click="testConnection(p)">
<FlaskConicalIcon class="h-4 w-4 mr-2" />
Test povezavo
</DropdownMenuItem>
<DropdownMenuItem @click="sendTestEmail(p)">
<SendIcon class="h-4 w-4 mr-2" />
Pošlji testni email
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="openEdit(p)">
<PencilIcon class="h-4 w-4 mr-2" />
Uredi profil
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
<Dialog :open="createOpen" @update:open="(val) => (createOpen = val)">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Nov Mail profil</DialogTitle>
<DialogDescription
>Ustvarite nov SMTP profil za pošiljanje e-pošte</DialogDescription
>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="grid gap-4 grid-cols-2">
<div class="space-y-2">
<Label for="create-name">Ime</Label>
<Input id="create-name" v-model="form.name" type="text" />
</div>
<div class="space-y-2">
<Label for="create-host">Host</Label>
<Input id="create-host" v-model="form.host" type="text" />
</div>
<div class="space-y-2">
<Label for="create-port">Port</Label>
<Input id="create-port" v-model.number="form.port" type="number" />
</div>
<div class="space-y-2">
<Label for="create-encryption">Encryption</Label>
<Select v-model="form.encryption">
<SelectTrigger id="create-encryption">
<SelectValue placeholder="Izberi..." />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">None</SelectItem>
<SelectItem value="tls">TLS</SelectItem>
<SelectItem value="ssl">SSL</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="create-username">Username</Label>
<Input id="create-username" v-model="form.username" type="text" />
</div>
<div class="space-y-2">
<Label for="create-password">Password</Label>
<Input
id="create-password"
v-model="form.password"
type="password"
autocomplete="new-password"
/>
</div>
<div class="space-y-2">
<Label for="create-from-address">From naslov</Label>
<Input id="create-from-address" v-model="form.from_address" type="email" />
</div>
<div class="space-y-2">
<Label for="create-from-name">From ime</Label>
<Input id="create-from-name" v-model="form.from_name" type="text" />
</div>
<div class="space-y-2">
<Label for="create-priority">Prioriteta</Label>
<Input id="create-priority" v-model.number="form.priority" type="number" />
</div>
<div class="flex items-center gap-3">
<Switch
id="create-auto-mailer"
:model-value="form.auto_mailer"
@update:model-value="(val) => (form.auto_mailer = val)"
/>
<Label for="create-auto-mailer">Auto-mailer</Label>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<Label>Podpis (signature)</Label>
<Button type="button" size="sm" variant="outline" @click="addSignatureItem">
<PlusIcon class="h-3 w-3 mr-1" />
Dodaj vrstico
</Button>
</div>
<p class="text-xs text-muted-foreground">
Vrednosti so dostopne v predlogah kot
<code class="font-mono" v-pre>{{ profile.signature.ključ }}</code
>.
</p>
<div class="space-y-2">
<div
v-for="(item, i) in signatureItems"
:key="i"
class="flex gap-2 items-start"
>
<Input
v-model="item.key"
placeholder="Ključ (npr. ime)"
class="w-36 shrink-0 font-mono text-xs"
/>
<Input v-model="item.value" placeholder="Vrednost" class="flex-1" />
<Button
type="button"
variant="ghost"
size="sm"
@click="removeSignatureItem(i)"
>
<Trash2Icon class="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</div>
</form>
<DialogFooter>
<Button variant="outline" @click="closeCreate" :disabled="form.processing"
>Prekliči</Button
>
<Button @click="submitCreate" :disabled="form.processing">Shrani</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog :open="editOpen" @update:open="(val) => (editOpen = val)">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Uredi Mail profil</DialogTitle>
<DialogDescription>Posodobite nastavitve SMTP profila</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="grid gap-4 grid-cols-2">
<div class="space-y-2">
<Label for="edit-name">Ime</Label>
<Input id="edit-name" v-model="form.name" type="text" />
</div>
<div class="space-y-2">
<Label for="edit-host">Host</Label>
<Input id="edit-host" v-model="form.host" type="text" />
</div>
<div class="space-y-2">
<Label for="edit-port">Port</Label>
<Input id="edit-port" v-model.number="form.port" type="number" />
</div>
<div class="space-y-2">
<Label for="edit-encryption">Encryption</Label>
<Select v-model="form.encryption">
<SelectTrigger id="edit-encryption">
<SelectValue placeholder="Izberi..." />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">None</SelectItem>
<SelectItem value="tls">TLS</SelectItem>
<SelectItem value="ssl">SSL</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="edit-username">Username</Label>
<Input id="edit-username" v-model="form.username" type="text" />
</div>
<div class="space-y-2">
<Label for="edit-password">Password (nova, če spreminjaš)</Label>
<Input
id="edit-password"
v-model="form.password"
type="password"
autocomplete="new-password"
/>
</div>
<div class="space-y-2">
<Label for="edit-from-address">From naslov</Label>
<Input id="edit-from-address" v-model="form.from_address" type="email" />
</div>
<div class="space-y-2">
<Label for="edit-from-name">From ime</Label>
<Input id="edit-from-name" v-model="form.from_name" type="text" />
</div>
<div class="space-y-2">
<Label for="edit-priority">Prioriteta</Label>
<Input id="edit-priority" v-model.number="form.priority" type="number" />
</div>
<div class="flex items-center gap-3">
<Switch
id="edit-auto-mailer"
:model-value="form.auto_mailer"
@update:model-value="(val) => (form.auto_mailer = val)"
/>
<Label for="edit-auto-mailer">Auto-mailer</Label>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<Label>Podpis (signature)</Label>
<Button type="button" size="sm" variant="outline" @click="addSignatureItem">
<PlusIcon class="h-3 w-3 mr-1" />
Dodaj vrstico
</Button>
</div>
<p class="text-xs text-muted-foreground">
Vrednosti so dostopne v predlogah kot
<code class="font-mono" v-pre>{{ profile.signature.ključ }}</code
>.
</p>
<div class="space-y-2">
<div
v-for="(item, i) in signatureItems"
:key="i"
class="flex gap-2 items-start"
>
<Input
v-model="item.key"
placeholder="Ključ (npr. ime)"
class="w-36 shrink-0 font-mono text-xs"
/>
<Input v-model="item.value" placeholder="Vrednost" class="flex-1" />
<Button
type="button"
variant="ghost"
size="sm"
@click="removeSignatureItem(i)"
>
<Trash2Icon class="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</div>
<p class="text-sm text-muted-foreground">
Pusti geslo prazno, če želiš obdržati obstoječe.
</p>
</form>
<DialogFooter>
<Button variant="outline" @click="closeEdit" :disabled="form.processing"
>Prekliči</Button
>
<Button @click="submitEdit" :disabled="form.processing">Shrani</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</AdminLayout>
</template>
<style scoped></style>