Fix copy PDF URL and dropdown action conditions

Copy PDF URL now checks window.isSecureContext before using
navigator.clipboard, falls back to textarea+execCommand on HTTP,
and shows a success notification.

Invoice dropdown: Mark as Sent uses its own condition instead of
reusing Send, Resend hidden in detail view.

Estimate dropdown: Mark as Accepted/Rejected hidden when already in
the other terminal state, Convert to Invoice hidden on rejected
estimates. Added Convert to Estimate action for invoices.
This commit is contained in:
Darko Gjorgjijoski
2026-04-06 22:56:49 +02:00
parent 6106ac8208
commit 5c0e761dfa
2 changed files with 77 additions and 20 deletions

View File

@@ -63,7 +63,7 @@
</BaseDropdownItem>
<!-- Convert into Invoice -->
<BaseDropdownItem v-if="canCreateInvoice" @click="convertToInvoice">
<BaseDropdownItem v-if="canCreateInvoice && row.status !== 'REJECTED'" @click="convertToInvoice">
<BaseIcon
name="DocumentTextIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
@@ -106,7 +106,7 @@
<!-- Mark as Accepted -->
<BaseDropdownItem
v-if="row.status !== 'ACCEPTED' && canEdit"
v-if="row.status !== 'ACCEPTED' && row.status !== 'REJECTED' && canEdit"
@click="onMarkAsAccepted"
>
<BaseIcon
@@ -118,7 +118,7 @@
<!-- Mark as Rejected -->
<BaseDropdownItem
v-if="row.status !== 'REJECTED' && canEdit"
v-if="row.status !== 'REJECTED' && row.status !== 'ACCEPTED' && canEdit"
@click="onMarkAsRejected"
>
<BaseIcon
@@ -137,6 +137,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useEstimateStore } from '../store'
import { useDialogStore } from '../../../../stores/dialog.store'
import { useModalStore } from '../../../../stores/modal.store'
import { useNotificationStore } from '../../../../stores/notification.store'
import type { Estimate } from '../../../../types/domain/estimate'
interface TableRef {
@@ -167,6 +168,7 @@ const props = withDefaults(defineProps<Props>(), {
const estimateStore = useEstimateStore()
const dialogStore = useDialogStore()
const modalStore = useModalStore()
const notificationStore = useNotificationStore()
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
@@ -289,16 +291,29 @@ function onMarkAsRejected(): void {
function copyPdfUrl(): void {
const pdfUrl = `${window.location.origin}/estimates/pdf/${props.row.unique_hash}`
navigator.clipboard.writeText(pdfUrl).catch(() => {
const textarea = document.createElement('textarea')
textarea.value = pdfUrl
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
copyToClipboard(pdfUrl)
notificationStore.showNotification({
type: 'success',
message: t('general.copied_pdf_url_clipboard'),
})
}
function copyToClipboard(text: string): void {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text)
return
}
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
function cloneEstimateData(): void {
dialogStore.openDialog({
title: t('general.are_you_sure'),

View File

@@ -54,7 +54,7 @@
</BaseDropdownItem>
<!-- Resend Invoice -->
<BaseDropdownItem v-if="canReSendInvoice" @click="sendInvoice">
<BaseDropdownItem v-if="canReSendInvoice && !isDetailView" @click="sendInvoice">
<BaseIcon
name="PaperAirplaneIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
@@ -76,7 +76,7 @@
</router-link>
<!-- Mark as Sent -->
<BaseDropdownItem v-if="canSendInvoice" @click="onMarkAsSent">
<BaseDropdownItem v-if="row.status === 'DRAFT' && !isDetailView && canSend" @click="onMarkAsSent">
<BaseIcon
name="CheckCircleIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
@@ -93,6 +93,15 @@
{{ $t('invoices.clone_invoice') }}
</BaseDropdownItem>
<!-- Convert to Estimate -->
<BaseDropdownItem v-if="canCreateEstimate" @click="convertToEstimate">
<BaseIcon
name="DocumentIcon"
class="w-5 h-5 mr-3 text-subtle group-hover:text-muted"
/>
{{ $t('invoices.convert_to_estimate') }}
</BaseDropdownItem>
<!-- Delete Invoice -->
<BaseDropdownItem v-if="canDelete" @click="removeInvoice">
<BaseIcon
@@ -111,6 +120,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useInvoiceStore } from '../store'
import { useDialogStore } from '../../../../stores/dialog.store'
import { useModalStore } from '../../../../stores/modal.store'
import { useNotificationStore } from '../../../../stores/notification.store'
import type { Invoice } from '../../../../types/domain/invoice'
interface TableRef {
@@ -127,6 +137,7 @@ interface Props {
canDelete?: boolean
canSend?: boolean
canCreatePayment?: boolean
canCreateEstimate?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@@ -138,11 +149,13 @@ const props = withDefaults(defineProps<Props>(), {
canDelete: false,
canSend: false,
canCreatePayment: false,
canCreateEstimate: false,
})
const invoiceStore = useInvoiceStore()
const dialogStore = useDialogStore()
const modalStore = useModalStore()
const notificationStore = useNotificationStore()
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
@@ -205,6 +218,23 @@ function cloneInvoiceData(): void {
})
}
function convertToEstimate(): void {
dialogStore.openDialog({
title: t('general.are_you_sure'),
message: t('invoices.confirm_convert_to_estimate'),
yesLabel: t('general.ok'),
noLabel: t('general.cancel'),
variant: 'primary',
hideNoButton: false,
size: 'lg',
}).then(async (res: boolean) => {
if (res) {
const response = await invoiceStore.convertToEstimate({ id: props.row.id })
router.push(`/admin/estimates/${response.data.data.id}/edit`)
}
})
}
function onMarkAsSent(): void {
dialogStore.openDialog({
title: t('general.are_you_sure'),
@@ -234,14 +264,26 @@ function sendInvoice(): void {
function copyPdfUrl(): void {
const pdfUrl = `${window.location.origin}/invoices/pdf/${props.row.unique_hash}`
navigator.clipboard.writeText(pdfUrl).catch(() => {
// Fallback
const textarea = document.createElement('textarea')
textarea.value = pdfUrl
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
copyToClipboard(pdfUrl)
notificationStore.showNotification({
type: 'success',
message: t('general.copied_pdf_url_clipboard'),
})
}
function copyToClipboard(text: string): void {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text)
return
}
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
</script>