Add confirm dialog to reminder button.

Add a show function in alertStore so we can use alerts in a defined way everywhere.
#12 #33
This commit is contained in:
2025-10-30 10:15:02 +01:00
parent 9439d14b59
commit f688e3fb17
4 changed files with 100 additions and 39 deletions
@@ -24,13 +24,14 @@ import { Input } from '@/components/ui/input';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
import { StatusBadge, statusBadgeLabels, statusBadgeTextColor, StatusBadgeVariants } from '@/components/ui/status-badge'
import LineItemTable from '@/components/documents/LineItemTable.vue'
import { Eye, FileText, CircleEllipsis, Trash, Trash2, BookUser, User, CodeXml, CalendarIcon, MessageCircleQuestion, X, CircleX, Logs, ListCheck, ClipboardCheck, ClipboardList } 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 { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { exportPdf, exportXml } from "@/routes/invoice"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from '@/components/ui/sheet'
import { GrowingTextarea } from '../ui/growing-textarea'
import { toast } from "vue-sonner"
const props = defineProps<{
invoiceData: Invoice | null,
@@ -47,6 +48,7 @@ const isLoading = ref(false);
const importContact = ref(newContact() as Contact)
const importCustomer = ref(newCustomer() as Customer)
const alert = alertStore()
const reminderLoading = ref(false)
onMounted(async () => {
const response = await axios.get('/api/paymentterms')
@@ -95,6 +97,7 @@ watch(invoice,
postalCode: invoice.value.customer?.billingAddress?.postalCode || "",
countryCode: invoice.value.customer?.billingAddress?.countryCode || "",
},
contactSalutation: invoice.value.customer?.contacts && invoice.value.customer.contacts.length > 0 ? invoice.value.customer.contacts[0].salutation : "",
contactFirstName: invoice.value.customer?.contacts && invoice.value.customer.contacts.length > 0 ? invoice.value.customer.contacts[0].firstName : "",
contactLastName: invoice.value.customer?.contacts && invoice.value.customer.contacts.length > 0 ? invoice.value.customer.contacts[0].lastName : "",
paymentTerms: invoice.value.customer?.paymentTerms || paymentTermsData.value.length > 0 ? paymentTermsData.value[2] : null,
@@ -190,24 +193,23 @@ const saveChanges = () => {
const cancelChanges = (event: Event | null) => {
if (isDirty.value) {
alert.title = "Wirklich schließen?"
alert.message = "Es gibt ungespeicherte Änderungen, die dann verloren gehen."
alert.cancelText = "Abbrechen"
alert.onCancel = () => {
console.log('cancel')
event?.preventDefault()
event.returnValue = true
alert.open = false
}
alert.actionText = "Schließen"
// alert.actionVariant = "destructive"
alert.onAction = () => {
console.log('action')
emit('cancel')
isOpen.value = false
alert.open = false
}
alert.open = true
alert.show(
"Wirklich schließen?",
"Es gibt ungespeicherte Änderungen, die dann verloren gehen.",
{
actionText: "Änderungen verwerfen",
actionVariant: "destructive",
onAction: () => {
emit('cancel')
isOpen.value = false
},
onCancel: () => {
if(!event) return
event.preventDefault()
event.returnValue = true
}
}
)
} else {
emit('cancel')
isOpen.value = false
@@ -230,19 +232,46 @@ const downloadXml = function () {
}
const deleteInvoice = function () {
let confirm = window.confirm('Möchtest Du diese Rechnung wirklich löschen?')
if (!confirm) return
emit('delete', invoice.value?.id)
isOpen.value = false
alert.show(
"Möchtest Du diese Rechnung wirklich löschen?",
(invoice.value?.paymentStatus == "draft") ? null : "Nach GoBD musst Du alle Belege und Daten in unveränderter Form aufbewahren",
{
actionText: "Löschen",
actionVariant: "destructive",
onAction: () => {
emit('delete', invoice.value?.id)
isOpen.value = false
}
}
)
}
const remind = function () {
// await axios call
// make button spin
// success -> set new status and save
// error -> toast
if (!invoice.value) return;
window?.open('/api/invoices/' + invoice.value.id + '/remind', '_blank')?.focus();
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 updateTotalAmount = () => {
@@ -364,7 +393,7 @@ const updateTotalAmount = () => {
<div id="document">
<div id="document-header"
class="block sticky top-0 py-4 bg-slate-100 bg-white dark:bg-neutral-800 z-1 flex items-end gap-12">
class="sticky top-0 py-4 bg-white dark:bg-neutral-800 z-1 flex items-end gap-12">
<div class="grow">
<h1 class="text-xl text-primary-foreground font-bold" v-if="invoice.id > 0">
@@ -483,7 +512,12 @@ const updateTotalAmount = () => {
</Select>
<Button v-if="['due', 'reminded'].includes(invoice.paymentStatus)"
:size="'sm'" :variant="'destructive'" @click="remind">Mahnen</Button>
:size="'sm'" :variant="'destructive'" @click="remind"
:disabled="reminderLoading">
<Loader2 class="h-4 w-4 transition-[width] ease-in-out animate-spin"
:class="{ 'w-0!': !reminderLoading }" />
Mahnen
</Button>
</TableCell>
</TableRow>
+3 -5
View File
@@ -70,13 +70,11 @@ onMounted(() => {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ alert.title }}</AlertDialogTitle>
<AlertDialogDescription>{{ alert.message }}</AlertDialogDescription>
<AlertDialogDescription v-if="alert.message">{{ alert.message }}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<Button v-if="alert.onCancel" @click="alert.onCancel">{{ alert.cancelText }}</Button>
<Button :variant="alert.actionVariant" v-if="alert.onAction" @click="alert.onAction">{{
alert.actionText
}}</Button>
<Button @click="alert.cancel">{{ alert.cancelText }}</Button>
<Button :variant="alert.actionVariant" @click="alert.action">{{ alert.actionText }}</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
+31 -2
View File
@@ -1,11 +1,20 @@
import { options } from '@coders-tm/vue-number-format';
import { defineStore } from 'pinia'
export interface AlertOptions {
cancelText?: string;
onCancel?: () => void;
actionText?: string;
actionVariant?: "action" | "destructive";
onAction?: () => void;
}
export const alertStore = defineStore('alert', {
state: () => {
return {
open: false,
title: "",
message: "",
message: "" as string | null,
cancelText: "Abbrechen",
onCancel: () => { },
actionText: "Ok",
@@ -13,5 +22,25 @@ export const alertStore = defineStore('alert', {
onAction: () => { }
}
},
actions: {}
actions: {
show(title: string, message: string | null, options?: AlertOptions) {
this.title = title;
this.message = message;
this.cancelText = options?.cancelText ? options.cancelText : "Abbrechen"
this.onCancel = options?.onCancel ? options.onCancel : () => { }
this.actionText = options?.actionText ? options.actionText : "Ok"
this.actionVariant = options?.actionVariant ? options.actionVariant : "action"
this.onAction = options?.onAction ? options.onAction : () => { }
this.open = true;
},
cancel() {
this.onCancel();
this.open = false;
},
action() {
this.onAction();
this.open = false;
}
}
})
+2 -2
View File
@@ -28,8 +28,8 @@
Route::get('/invoices/{id}/remind', function ($id) {
$invoice = InvoiceController::single($id);
// Mail::to('daniel@vollstock.de')->send(new Reminder($invoice));
return new Reminder($invoice);
Mail::to('daniel@vollstock.de')->send(new Reminder($invoice));
// return new Reminder($invoice);
});
Route::get('/offers/{id}/confirm', function ($id) {