Add mail dialog component, work on #12

This commit is contained in:
2025-11-11 11:29:17 +01:00
parent 67a0ef2180
commit ec482cea6c
3 changed files with 126 additions and 28 deletions
@@ -22,7 +22,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrig
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import { StatusBadge, statusBadgeLabels, statusBadgeTextColor, StatusBadgeVariants } from '@/components/ui/status-badge' import { StatusBadge, statusBadgeLabels, statusTextStyle, StatusBadgeVariants } from '@/components/ui/status-badge'
import LineItemTable from '@/components/documents/LineItemTable.vue' import LineItemTable from '@/components/documents/LineItemTable.vue'
import { Eye, FileText, CircleEllipsis, Trash, Trash2, BookUser, User, CodeXml, CalendarIcon, MessageCircleQuestion, X, CircleX, Logs, ListCheck, ClipboardCheck, ClipboardList, Loader, Loader2 } from "lucide-vue-next" import { Eye, FileText, CircleEllipsis, Trash, Trash2, BookUser, User, CodeXml, CalendarIcon, MessageCircleQuestion, X, CircleX, Logs, ListCheck, ClipboardCheck, ClipboardList, Loader, Loader2 } from "lucide-vue-next"
import { alertStore } from "@/stores/alertStore" import { alertStore } from "@/stores/alertStore"
@@ -32,6 +32,7 @@ import { exportPdf, exportXml } from "@/routes/invoice"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet'
import { GrowingTextarea } from '../ui/growing-textarea' import { GrowingTextarea } from '../ui/growing-textarea'
import { toast } from "vue-sonner" import { toast } from "vue-sonner"
import { SendMailDialog } from "../ui/send-mail-dialog"
const props = defineProps<{ const props = defineProps<{
invoiceData: Invoice | null, invoiceData: Invoice | null,
@@ -48,6 +49,7 @@ const isLoading = ref(false);
const importContact = ref(newContact() as Contact) const importContact = ref(newContact() as Contact)
const importCustomer = ref(newCustomer() as Customer) const importCustomer = ref(newCustomer() as Customer)
const alert = alertStore() const alert = alertStore()
const reminderDialogOpen = ref(false)
const reminderLoading = ref(false) const reminderLoading = ref(false)
onMounted(async () => { onMounted(async () => {
@@ -247,33 +249,56 @@ const deleteInvoice = function () {
) )
} }
const remind = function () { const openReminderDialog = function () {
if (!invoice.value) return; if (!invoice.value) return
reminderDialogOpen.value = true
// alert.show(
// "Zahlungserinnerung senden?",
// "E-mail an " + invoice.value.customer?.contacts[0].email,
// {
// actionText: "Senden",
// onAction: async () => {
// // make button spin and disable button
// reminderLoading.value = true
// // await axios call
// await axios.get('/api/invoices/' + invoice.value.id + '/remind')
// .then(function (response) {
// toast.success("Zahlungserinnerung gesendet", { description: "daniel@vollstock.de" })
// })
// .catch(function (error) {
// toast.error(error.title, { description: error.message })
// })
// .finally(() => {
// reminderLoading.value = false
// })
// }
// }
// )
}
const sendReminder = async function (recipient: string | undefined) {
if (!recipient) return
if (!invoice.value || !invoice.value.id) return
// Close dialog
reminderDialogOpen.value = false
alert.show(
"Zahlungserinnerung senden?",
"E-mail an " + invoice.value.customer?.contacts[0].email,
{
actionText: "Senden",
onAction: async () => {
// make button spin and disable button // make button spin and disable button
reminderLoading.value = true reminderLoading.value = true
try {
// await axios call // await axios call
await axios.get('/api/invoices/' + invoice.value.id + '/remind') await axios.get('/api/invoices/' + invoice.value.id + '/remind/' + recipient)
.then(function (response) { toast.success("Zahlungserinnerung gesendet", { description: recipient })
toast.success("Zahlungserinnerung gesendet", { description: "daniel@vollstock.de" }) } catch (error: any) {
}) toast.error(error?.title || 'Fehler', { description: error?.message || String(error) })
.catch(function (error) { } finally {
toast.error(error.title, { description: error.message })
})
.finally(() => {
reminderLoading.value = false reminderLoading.value = false
})
} }
} }
)
}
const updateTotalAmount = () => { const updateTotalAmount = () => {
if (!invoice.value) return; if (!invoice.value) return;
@@ -516,7 +541,7 @@ const updateTotalAmount = () => {
</Select> </Select>
<Button v-if="['due', 'reminded'].includes(invoice.paymentStatus)" <Button v-if="['due', 'reminded'].includes(invoice.paymentStatus)"
:size="'sm'" :variant="'destructive'" @click="remind" :size="'sm'" :variant="'destructive'" @click="openReminderDialog"
:disabled="reminderLoading"> :disabled="reminderLoading">
<Loader2 class="h-4 w-4 transition-[width] ease-in-out animate-spin" <Loader2 class="h-4 w-4 transition-[width] ease-in-out animate-spin"
:class="{ 'w-0!': !reminderLoading }" /> :class="{ 'w-0!': !reminderLoading }" />
@@ -622,6 +647,8 @@ const updateTotalAmount = () => {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<SendMailDialog v-model:open="reminderDialogOpen" title="Zahlungserinnerung senden?" description=""
:recipient="invoice?.customer?.contacts[0].email" @send="(recipient) => sendReminder(recipient)" />
</template> </template>
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { ref, watch } from "vue"
import type { PrimitiveProps } from "reka-ui"
import { type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'reka-ui'
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogClose, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
const props = defineProps<DialogRootProps & {
title: string,
description?: string,
recipient?: string
}>()
const recipient = ref<string | undefined>(props.recipient)
const emits = defineEmits<DialogRootEmits & {
send: [recipient: string | undefined]
}>()
const forwarded = useForwardPropsEmits(props, emits)
watch(() => props.open,
(newValue, oldValue) => {
recipient.value = props.recipient
}
)
</script>
<template>
<Dialog v-bind="forwarded" :open="open">
<DialogContent class="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{{ props.title }}</DialogTitle>
<DialogDescription v-if="props.description" v-html="props.description" />
</DialogHeader>
<div class="grid grid-cols-[min-content_1fr] gap-4 py-4">
<Label for="email" class="text-right text-muted-foreground">
An
</Label>
<Input id="email" required v-model="recipient" type="email" placeholder="E-Mail" />
<Label for="email" class="text-right text-muted-foreground">
Kopie
</Label>
<Input id="email" required :value="'buchhaltung@tooloop.de'" type="email" placeholder="E-Mail" />
</div>
<DialogFooter>
<DialogClose as-child>
<Button>
Abbrechen
</Button>
</DialogClose>
<Button variant="destructive" @click="$emit('send', recipient)">
E-Mail senden
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<style>
/* Remove close X */
[data-slot=dialog-content] button.ring-offset-background {
display: none;
}
</style>
+8 -2
View File
@@ -26,9 +26,15 @@
Route::put('/invoices/{id}', [InvoiceController::class, 'update']); Route::put('/invoices/{id}', [InvoiceController::class, 'update']);
Route::delete('/invoices/{id}', [InvoiceController::class, 'delete']); Route::delete('/invoices/{id}', [InvoiceController::class, 'delete']);
Route::get('/invoices/{id}/remind', function ($id) { Route::get('/invoices/{id}/remind/{recipient}', function ($id, $recipient) {
if (empty($recipient) || !filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
return response()->json([
'error' => 'Keine gültige E_Mail-Adresse'
], 400);
}
$invoice = InvoiceController::single($id); $invoice = InvoiceController::single($id);
Mail::to('daniel@vollstock.de')->send(new Reminder($invoice)); Mail::to($recipient)->send(new Reminder($invoice));
// return new Reminder($invoice); // return new Reminder($invoice);
}); });