Part A: Fix duplicate Service creation on provisioning retry - All 4 provisioning services use Service::firstOrCreate() keyed on subscription_id+service_type to prevent duplicates on queue retries - HandleSubscriptionCreated sends notification before provisioning, no longer re-throws on failure - RetryProvisioningCommand simplified to reuse existing Service records Part B: Plans/Pricing page complete redesign - Service type tabs (VPS, Dedicated, Web Hosting, MySQL) - Billing cycle segmented toggle (monthly/quarterly/semi-annual/annual) - Feature icons per service type, Popular/Best Value badges - Stock indicators, effective monthly price calculations Part C: Admin service soft-delete/archive - Service model uses SoftDeletes trait - Admin can archive and restore services - Show archived toggle on services list - Migration adds deleted_at column Docs: Updated TASKS.md, CLAUDE.md, PROJECT_DEVELOPMENT.md, MEMORY.md - Phase 3 marked complete, test counts updated (252 passing) - SupportPal references replaced with standalone ticket system - Frontend design skill background rule added - Closed GitHub issues #3, #6, #7, #8, #9 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
680 lines
21 KiB
Vue
680 lines
21 KiB
Vue
<script lang="ts" setup>
|
|
import { Link, useForm } from '@inertiajs/vue3'
|
|
import { computed, ref } from 'vue'
|
|
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
|
import type { StatusColor } from '@/types'
|
|
|
|
interface ServiceUser {
|
|
id: number
|
|
name: string
|
|
email: string
|
|
status: string
|
|
}
|
|
|
|
interface ServicePlan {
|
|
id: number
|
|
name: string
|
|
service_type: string
|
|
price: string
|
|
billing_cycle: string
|
|
}
|
|
|
|
interface AvailablePlan {
|
|
id: number
|
|
name: string
|
|
price: string
|
|
billing_cycle: string
|
|
}
|
|
|
|
interface ProvisioningLogItem {
|
|
id: number
|
|
action: string
|
|
platform: string | null
|
|
platform_response: Record<string, unknown> | null
|
|
status: string
|
|
error_message: string | null
|
|
created_at: string
|
|
}
|
|
|
|
interface ServiceDetail {
|
|
id: number
|
|
user_id: number
|
|
service_type: string
|
|
platform: string | null
|
|
platform_service_id: string | null
|
|
status: string
|
|
hostname: string | null
|
|
domain: string | null
|
|
ipv4_address: string | null
|
|
ipv6_address: string | null
|
|
auto_renew: boolean
|
|
provisioned_at: string | null
|
|
suspended_at: string | null
|
|
terminated_at: string | null
|
|
deleted_at: string | null
|
|
created_at: string
|
|
updated_at: string
|
|
user: ServiceUser | null
|
|
plan: ServicePlan | null
|
|
provisioning_logs: ProvisioningLogItem[]
|
|
}
|
|
|
|
interface Props {
|
|
service: ServiceDetail
|
|
availablePlans: AvailablePlan[]
|
|
}
|
|
|
|
defineOptions({ layout: AdminLayout })
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const confirmDialog = ref<boolean>(false)
|
|
const confirmAction = ref<'suspend' | 'unsuspend' | 'terminate' | 'provision' | 'archive' | 'restore'>('suspend')
|
|
const confirmTitle = ref<string>('')
|
|
const confirmMessage = ref<string>('')
|
|
const confirmColor = ref<string>('warning')
|
|
|
|
const modifyDialog = ref<boolean>(false)
|
|
|
|
const suspendForm = useForm({})
|
|
const unsuspendForm = useForm({})
|
|
const terminateForm = useForm({})
|
|
const provisionForm = useForm({})
|
|
const archiveForm = useForm({})
|
|
const restoreForm = useForm({})
|
|
const modifyForm = useForm({
|
|
plan_id: props.service.plan?.id ?? null,
|
|
notes: '',
|
|
})
|
|
|
|
const isProcessing = computed<boolean>(() =>
|
|
suspendForm.processing || unsuspendForm.processing || terminateForm.processing || provisionForm.processing || modifyForm.processing || archiveForm.processing || restoreForm.processing,
|
|
)
|
|
|
|
function openConfirmDialog(action: 'suspend' | 'unsuspend' | 'terminate' | 'provision' | 'archive' | 'restore'): void {
|
|
confirmAction.value = action
|
|
|
|
const actions: Record<string, { title: string; message: string; color: string }> = {
|
|
suspend: {
|
|
title: 'Suspend Service',
|
|
message: `Are you sure you want to suspend service #${props.service.id}? The customer will lose access to their service.`,
|
|
color: 'warning',
|
|
},
|
|
unsuspend: {
|
|
title: 'Unsuspend Service',
|
|
message: `Are you sure you want to unsuspend service #${props.service.id}? The customer will regain access to their service.`,
|
|
color: 'success',
|
|
},
|
|
provision: {
|
|
title: 'Provision Service',
|
|
message: `Are you sure you want to manually provision service #${props.service.id}? This will trigger provisioning on the configured platform.`,
|
|
color: 'info',
|
|
},
|
|
terminate: {
|
|
title: 'Terminate Service',
|
|
message: `Are you sure you want to terminate service #${props.service.id}? This action may be irreversible. The service will be permanently deactivated.`,
|
|
color: 'error',
|
|
},
|
|
archive: {
|
|
title: 'Archive Service',
|
|
message: `Are you sure you want to archive service #${props.service.id}? It will be hidden from the default list but can be restored later.`,
|
|
color: 'error',
|
|
},
|
|
restore: {
|
|
title: 'Restore Service',
|
|
message: `Are you sure you want to restore service #${props.service.id}? It will be visible in the services list again.`,
|
|
color: 'success',
|
|
},
|
|
}
|
|
|
|
const config = actions[action]
|
|
confirmTitle.value = config.title
|
|
confirmMessage.value = config.message
|
|
confirmColor.value = config.color
|
|
confirmDialog.value = true
|
|
}
|
|
|
|
function executeAction(): void {
|
|
const action = confirmAction.value
|
|
const opts = {
|
|
preserveScroll: true,
|
|
onSuccess: () => { confirmDialog.value = false },
|
|
}
|
|
|
|
if (action === 'suspend') {
|
|
suspendForm.post(`/services/${props.service.id}/suspend`, opts)
|
|
} else if (action === 'unsuspend') {
|
|
unsuspendForm.post(`/services/${props.service.id}/unsuspend`, opts)
|
|
} else if (action === 'provision') {
|
|
provisionForm.post(`/services/${props.service.id}/provision`, opts)
|
|
} else if (action === 'archive') {
|
|
archiveForm.delete(`/services/${props.service.id}`, opts)
|
|
} else if (action === 'restore') {
|
|
restoreForm.post(`/services/${props.service.id}/restore`, opts)
|
|
} else {
|
|
terminateForm.post(`/services/${props.service.id}/terminate`, opts)
|
|
}
|
|
}
|
|
|
|
function openModifyDialog(): void {
|
|
modifyForm.plan_id = props.service.plan?.id ?? null
|
|
modifyForm.notes = ''
|
|
modifyDialog.value = true
|
|
}
|
|
|
|
function submitModify(): void {
|
|
modifyForm.put(`/services/${props.service.id}`, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
modifyDialog.value = false
|
|
},
|
|
})
|
|
}
|
|
|
|
function resolveServiceStatusColor(statusVal: string): StatusColor {
|
|
const map: Record<string, StatusColor> = {
|
|
active: 'success',
|
|
suspended: 'warning',
|
|
terminated: 'error',
|
|
pending: 'info',
|
|
}
|
|
return map[statusVal] ?? 'secondary'
|
|
}
|
|
|
|
function resolveServiceTypeColor(type: string): string {
|
|
const map: Record<string, string> = {
|
|
vps: 'primary',
|
|
dedicated: 'info',
|
|
web_hosting: 'success',
|
|
game: 'warning',
|
|
}
|
|
return map[type] ?? 'secondary'
|
|
}
|
|
|
|
function formatServiceType(type: string): string {
|
|
const map: Record<string, string> = {
|
|
vps: 'VPS',
|
|
dedicated: 'Dedicated',
|
|
web_hosting: 'Web Hosting',
|
|
game: 'Game Hosting',
|
|
}
|
|
return map[type] ?? type
|
|
}
|
|
|
|
function resolveLogStatusColor(statusVal: string): StatusColor {
|
|
const map: Record<string, StatusColor> = {
|
|
success: 'success',
|
|
failed: 'error',
|
|
pending: 'warning',
|
|
in_progress: 'info',
|
|
}
|
|
return map[statusVal] ?? 'secondary'
|
|
}
|
|
|
|
function formatDate(dateStr: string | null): string {
|
|
if (!dateStr) return '---'
|
|
const date = new Date(dateStr)
|
|
return date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
|
|
}
|
|
|
|
function formatDateTime(dateStr: string | null): string {
|
|
if (!dateStr) return '---'
|
|
const date = new Date(dateStr)
|
|
return date.toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
function formatPrice(price: string | number, cycle?: string): string {
|
|
const amount = parseFloat(String(price)).toFixed(2)
|
|
return cycle ? `$${amount}/${cycle}` : `$${amount}`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- Header -->
|
|
<div class="d-flex align-center justify-space-between mb-6">
|
|
<div class="d-flex align-center gap-4">
|
|
<Link href="/services">
|
|
<VBtn variant="text" icon="tabler-arrow-left" size="small" />
|
|
</Link>
|
|
<div>
|
|
<div class="d-flex align-center gap-2">
|
|
<span class="text-h4 font-weight-bold">Service #{{ service.id }}</span>
|
|
<VChip
|
|
:color="resolveServiceStatusColor(service.status)"
|
|
size="small"
|
|
class="text-capitalize"
|
|
>
|
|
{{ service.status }}
|
|
</VChip>
|
|
<VChip
|
|
:color="resolveServiceTypeColor(service.service_type)"
|
|
size="small"
|
|
variant="tonal"
|
|
>
|
|
{{ formatServiceType(service.service_type) }}
|
|
</VChip>
|
|
<VChip
|
|
v-if="service.deleted_at"
|
|
color="secondary"
|
|
size="small"
|
|
variant="outlined"
|
|
>
|
|
Archived
|
|
</VChip>
|
|
</div>
|
|
<div class="text-body-2 text-medium-emphasis mt-1">
|
|
{{ service.user?.name ?? 'Unknown Customer' }} · {{ service.user?.email ?? '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<VBtn
|
|
v-if="!service.provisioned_at && service.status !== 'terminated'"
|
|
color="info"
|
|
variant="tonal"
|
|
:disabled="isProcessing"
|
|
@click="openConfirmDialog('provision')"
|
|
>
|
|
<VIcon icon="tabler-rocket" start />
|
|
Provision
|
|
</VBtn>
|
|
|
|
<VBtn
|
|
v-if="service.status !== 'terminated'"
|
|
color="primary"
|
|
variant="tonal"
|
|
:disabled="isProcessing"
|
|
@click="openModifyDialog"
|
|
>
|
|
<VIcon icon="tabler-edit" start />
|
|
Modify Service
|
|
</VBtn>
|
|
|
|
<VBtn
|
|
v-if="service.status === 'active'"
|
|
color="warning"
|
|
variant="tonal"
|
|
:disabled="isProcessing"
|
|
@click="openConfirmDialog('suspend')"
|
|
>
|
|
<VIcon icon="tabler-player-pause" start />
|
|
Suspend
|
|
</VBtn>
|
|
|
|
<VBtn
|
|
v-if="service.status === 'suspended'"
|
|
color="success"
|
|
variant="tonal"
|
|
:disabled="isProcessing"
|
|
@click="openConfirmDialog('unsuspend')"
|
|
>
|
|
<VIcon icon="tabler-player-play" start />
|
|
Unsuspend
|
|
</VBtn>
|
|
|
|
<VBtn
|
|
v-if="service.status !== 'terminated' && !service.deleted_at"
|
|
color="error"
|
|
variant="tonal"
|
|
:disabled="isProcessing"
|
|
@click="openConfirmDialog('terminate')"
|
|
>
|
|
<VIcon icon="tabler-trash" start />
|
|
Terminate
|
|
</VBtn>
|
|
|
|
<VBtn
|
|
v-if="!service.deleted_at"
|
|
color="error"
|
|
variant="outlined"
|
|
:disabled="isProcessing"
|
|
@click="openConfirmDialog('archive')"
|
|
>
|
|
<VIcon icon="tabler-archive" start />
|
|
Archive
|
|
</VBtn>
|
|
|
|
<VBtn
|
|
v-if="service.deleted_at"
|
|
color="success"
|
|
variant="flat"
|
|
:disabled="isProcessing"
|
|
@click="openConfirmDialog('restore')"
|
|
>
|
|
<VIcon icon="tabler-refresh" start />
|
|
Restore
|
|
</VBtn>
|
|
</div>
|
|
</div>
|
|
|
|
<VRow>
|
|
<!-- Service Details -->
|
|
<VCol cols="12" lg="6">
|
|
<VCard>
|
|
<VCardTitle class="d-flex align-center gap-2">
|
|
<VIcon icon="tabler-server" size="22" />
|
|
<span>Service Details</span>
|
|
</VCardTitle>
|
|
|
|
<VCardText>
|
|
<VList density="compact" class="pa-0">
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Plan</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2 font-weight-medium">
|
|
{{ service.plan?.name ?? 'N/A' }}
|
|
<template v-if="service.plan">
|
|
· {{ formatPrice(service.plan.price, service.plan.billing_cycle) }}
|
|
</template>
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Platform</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ service.platform ?? 'Not assigned' }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Platform ID</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ service.platform_service_id ?? '---' }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Hostname</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ service.hostname ?? '---' }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Domain</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ service.domain ?? '---' }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">IPv4 Address</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ service.ipv4_address ?? '---' }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">IPv6 Address</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ service.ipv6_address ?? '---' }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Auto Renew</span>
|
|
</template>
|
|
<VListItemTitle>
|
|
<VChip :color="service.auto_renew ? 'success' : 'secondary'" size="small">
|
|
{{ service.auto_renew ? 'Yes' : 'No' }}
|
|
</VChip>
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
</VList>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
|
|
<!-- Dates & Customer -->
|
|
<VCol cols="12" lg="6">
|
|
<VCard class="mb-4">
|
|
<VCardTitle class="d-flex align-center gap-2">
|
|
<VIcon icon="tabler-calendar" size="22" />
|
|
<span>Dates</span>
|
|
</VCardTitle>
|
|
|
|
<VCardText>
|
|
<VList density="compact" class="pa-0">
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Created</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ formatDateTime(service.created_at) }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem>
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Provisioned</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2">
|
|
{{ formatDateTime(service.provisioned_at) }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem v-if="service.suspended_at">
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Suspended</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2 text-warning">
|
|
{{ formatDateTime(service.suspended_at) }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
|
|
<VListItem v-if="service.terminated_at">
|
|
<template #prepend>
|
|
<span class="text-body-2 text-medium-emphasis" style="min-width: 140px;">Terminated</span>
|
|
</template>
|
|
<VListItemTitle class="text-body-2 text-error">
|
|
{{ formatDateTime(service.terminated_at) }}
|
|
</VListItemTitle>
|
|
</VListItem>
|
|
</VList>
|
|
</VCardText>
|
|
</VCard>
|
|
|
|
<VCard>
|
|
<VCardTitle class="d-flex align-center gap-2">
|
|
<VIcon icon="tabler-user" size="22" />
|
|
<span>Customer</span>
|
|
</VCardTitle>
|
|
|
|
<VCardText v-if="service.user">
|
|
<div class="d-flex align-center gap-3">
|
|
<VAvatar color="primary" variant="tonal" size="40">
|
|
<span class="text-body-1 font-weight-semibold">
|
|
{{ service.user.name.charAt(0).toUpperCase() }}
|
|
</span>
|
|
</VAvatar>
|
|
<div>
|
|
<div class="text-body-1 font-weight-medium">
|
|
{{ service.user.name }}
|
|
</div>
|
|
<div class="text-body-2 text-medium-emphasis">
|
|
{{ service.user.email }}
|
|
</div>
|
|
</div>
|
|
<VSpacer />
|
|
<Link :href="`/customers/${service.user.id}`">
|
|
<VBtn variant="tonal" size="small" color="primary">
|
|
View Customer
|
|
</VBtn>
|
|
</Link>
|
|
</div>
|
|
</VCardText>
|
|
</VCard>
|
|
</VCol>
|
|
</VRow>
|
|
|
|
<!-- Provisioning Logs -->
|
|
<VCard class="mt-6">
|
|
<VCardTitle class="d-flex align-center gap-2">
|
|
<VIcon icon="tabler-list-details" size="22" />
|
|
<span>Provisioning Logs</span>
|
|
<VSpacer />
|
|
<VChip size="small" color="secondary" variant="tonal">
|
|
{{ service.provisioning_logs.length }} {{ service.provisioning_logs.length === 1 ? 'entry' : 'entries' }}
|
|
</VChip>
|
|
</VCardTitle>
|
|
|
|
<VCardText v-if="service.provisioning_logs.length === 0" class="text-center py-8">
|
|
<VIcon icon="tabler-inbox" size="48" color="disabled" class="mb-2" />
|
|
<div class="text-medium-emphasis">
|
|
No provisioning logs recorded for this service.
|
|
</div>
|
|
</VCardText>
|
|
|
|
<VTable v-else density="comfortable" hover>
|
|
<thead>
|
|
<tr>
|
|
<th>Action</th>
|
|
<th>Status</th>
|
|
<th>Platform</th>
|
|
<th>Message</th>
|
|
<th>Timestamp</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="log in service.provisioning_logs" :key="log.id">
|
|
<td class="text-body-2 font-weight-medium text-capitalize">
|
|
{{ log.action }}
|
|
</td>
|
|
<td>
|
|
<VChip
|
|
:color="resolveLogStatusColor(log.status)"
|
|
size="small"
|
|
class="text-capitalize"
|
|
>
|
|
{{ log.status }}
|
|
</VChip>
|
|
</td>
|
|
<td class="text-body-2">
|
|
{{ log.platform ?? '---' }}
|
|
</td>
|
|
<td class="text-body-2 text-medium-emphasis" style="max-width: 400px;">
|
|
<span class="d-inline-block text-truncate" style="max-width: 400px;">
|
|
{{ log.error_message ?? 'OK' }}
|
|
</span>
|
|
</td>
|
|
<td class="text-body-2">
|
|
{{ formatDateTime(log.created_at) }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</VTable>
|
|
</VCard>
|
|
|
|
<!-- Modify Service Dialog -->
|
|
<VDialog v-model="modifyDialog" max-width="600" persistent>
|
|
<VCard>
|
|
<VCardTitle class="text-h5 pa-5">
|
|
Modify Service #{{ service.id }}
|
|
</VCardTitle>
|
|
<VCardText class="px-5 pb-2">
|
|
<VRow>
|
|
<VCol cols="12">
|
|
<label class="text-body-2 text-medium-emphasis mb-1 d-block">Change Plan</label>
|
|
<VSelect
|
|
v-model="modifyForm.plan_id"
|
|
:items="availablePlans"
|
|
item-title="name"
|
|
item-value="id"
|
|
:error-messages="modifyForm.errors.plan_id"
|
|
placeholder="Select a plan"
|
|
variant="outlined"
|
|
density="comfortable"
|
|
>
|
|
<template #item="{ props: itemProps, item }">
|
|
<VListItem v-bind="itemProps">
|
|
<template #subtitle>
|
|
{{ formatPrice(item.raw.price, item.raw.billing_cycle) }}
|
|
</template>
|
|
</VListItem>
|
|
</template>
|
|
</VSelect>
|
|
<div class="text-caption text-medium-emphasis mt-1">
|
|
Current plan: {{ service.plan?.name ?? 'N/A' }}
|
|
</div>
|
|
</VCol>
|
|
|
|
<VCol cols="12">
|
|
<label class="text-body-2 text-medium-emphasis mb-1 d-block">Admin Notes (Optional)</label>
|
|
<VTextarea
|
|
v-model="modifyForm.notes"
|
|
:error-messages="modifyForm.errors.notes"
|
|
placeholder="Add internal notes about this modification..."
|
|
variant="outlined"
|
|
density="comfortable"
|
|
rows="3"
|
|
/>
|
|
</VCol>
|
|
</VRow>
|
|
</VCardText>
|
|
<VCardActions class="pa-5">
|
|
<VSpacer />
|
|
<VBtn variant="text" :disabled="modifyForm.processing" @click="modifyDialog = false">
|
|
Cancel
|
|
</VBtn>
|
|
<VBtn
|
|
color="primary"
|
|
variant="flat"
|
|
:loading="modifyForm.processing"
|
|
@click="submitModify"
|
|
>
|
|
Update Service
|
|
</VBtn>
|
|
</VCardActions>
|
|
</VCard>
|
|
</VDialog>
|
|
|
|
<!-- Confirmation Dialog -->
|
|
<VDialog v-model="confirmDialog" max-width="500" persistent>
|
|
<VCard>
|
|
<VCardTitle class="text-h5 pa-5">
|
|
{{ confirmTitle }}
|
|
</VCardTitle>
|
|
<VCardText class="px-5 pb-2">
|
|
{{ confirmMessage }}
|
|
</VCardText>
|
|
<VCardActions class="pa-5">
|
|
<VSpacer />
|
|
<VBtn variant="text" :disabled="isProcessing" @click="confirmDialog = false">
|
|
Cancel
|
|
</VBtn>
|
|
<VBtn
|
|
:color="confirmColor"
|
|
variant="flat"
|
|
:loading="isProcessing"
|
|
@click="executeAction"
|
|
>
|
|
Confirm
|
|
</VBtn>
|
|
</VCardActions>
|
|
</VCard>
|
|
</VDialog>
|
|
</div>
|
|
</template>
|