Replace AppTextField/AppSelect/AppTextarea with native Vuetify equivalents across 22 pages
Vuetify defaults already configure VTextField/VSelect/VTextarea with the correct settings (outlined variant, comfortable density, primary color, auto hideDetails), making the wrapper components unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
|
||||
interface PlanOption {
|
||||
id: number
|
||||
@@ -95,7 +93,7 @@ function submit(): void {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="8">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.code"
|
||||
label="Coupon Code"
|
||||
placeholder="e.g. SAVE20"
|
||||
@@ -114,7 +112,7 @@ function submit(): void {
|
||||
</VBtn>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.type"
|
||||
label="Discount Type"
|
||||
:items="typeOptions"
|
||||
@@ -123,7 +121,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.value"
|
||||
:label="valueLabel"
|
||||
type="number"
|
||||
@@ -143,7 +141,7 @@ function submit(): void {
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Optionally restrict this coupon to specific plans. Leave empty to apply to all plans.
|
||||
</p>
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.applies_to"
|
||||
label="Applicable Plans"
|
||||
:items="planSelectItems"
|
||||
@@ -161,7 +159,7 @@ function submit(): void {
|
||||
<VCol cols="12" lg="4">
|
||||
<VCard title="Limits & Expiry" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.max_uses"
|
||||
label="Max Uses"
|
||||
type="number"
|
||||
@@ -170,7 +168,7 @@ function submit(): void {
|
||||
:error-messages="form.errors.max_uses"
|
||||
class="mb-4"
|
||||
/>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.expires_at"
|
||||
label="Expiry Date"
|
||||
type="datetime-local"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import type { Coupon, CouponRedemption, StatusColor } from '@/types'
|
||||
|
||||
interface PlanOption {
|
||||
@@ -126,7 +124,7 @@ function submit(): void {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.code"
|
||||
label="Coupon Code"
|
||||
placeholder="e.g. SAVE20"
|
||||
@@ -134,7 +132,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.type"
|
||||
label="Discount Type"
|
||||
:items="typeOptions"
|
||||
@@ -143,7 +141,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.value"
|
||||
:label="valueLabel"
|
||||
type="number"
|
||||
@@ -163,7 +161,7 @@ function submit(): void {
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Optionally restrict this coupon to specific plans. Leave empty to apply to all plans.
|
||||
</p>
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.applies_to"
|
||||
label="Applicable Plans"
|
||||
:items="planSelectItems"
|
||||
@@ -234,7 +232,7 @@ function submit(): void {
|
||||
<!-- Limits & Expiry -->
|
||||
<VCard title="Limits & Expiry" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.max_uses"
|
||||
label="Max Uses"
|
||||
type="number"
|
||||
@@ -243,7 +241,7 @@ function submit(): void {
|
||||
:error-messages="form.errors.max_uses"
|
||||
class="mb-4"
|
||||
/>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.expires_at"
|
||||
label="Expiry Date"
|
||||
type="datetime-local"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import type { User } from '@/types'
|
||||
|
||||
defineOptions({ layout: AdminLayout })
|
||||
@@ -59,7 +56,7 @@ function submit(): void {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.name"
|
||||
label="Name"
|
||||
placeholder="Full name"
|
||||
@@ -67,7 +64,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.email"
|
||||
label="Email"
|
||||
type="email"
|
||||
@@ -76,7 +73,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.phone"
|
||||
label="Phone"
|
||||
placeholder="Phone number"
|
||||
@@ -84,7 +81,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.company"
|
||||
label="Company"
|
||||
placeholder="Company name"
|
||||
@@ -92,7 +89,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.status"
|
||||
label="Status"
|
||||
:items="statusOptions"
|
||||
@@ -106,7 +103,7 @@ function submit(): void {
|
||||
<!-- Admin Notes -->
|
||||
<VCard title="Admin Notes" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="form.admin_notes"
|
||||
label="Internal Notes"
|
||||
placeholder="Add internal notes about this customer (only visible to admins)..."
|
||||
|
||||
399
website/resources/ts/Pages/Admin/EmailTemplates/Edit.vue
Normal file
399
website/resources/ts/Pages/Admin/EmailTemplates/Edit.vue
Normal file
@@ -0,0 +1,399 @@
|
||||
<script lang="ts" setup>
|
||||
import { Link, useForm, router } from '@inertiajs/vue3'
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
|
||||
interface EmailTemplate {
|
||||
id: number
|
||||
slug: string
|
||||
name: string
|
||||
subject: string
|
||||
body: string
|
||||
available_variables: string[]
|
||||
is_active: boolean
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
template: EmailTemplate
|
||||
}
|
||||
|
||||
defineOptions({ layout: AdminLayout })
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const form = useForm({
|
||||
subject: props.template.subject,
|
||||
body: props.template.body,
|
||||
is_active: props.template.is_active,
|
||||
})
|
||||
|
||||
const showPreviewDialog = ref<boolean>(false)
|
||||
const previewLoading = ref<boolean>(false)
|
||||
const previewSubject = ref<string>('')
|
||||
const previewBody = ref<string>('')
|
||||
const showResetDialog = ref<boolean>(false)
|
||||
|
||||
const bodyTextareaRef = ref<InstanceType<typeof VTextarea> | null>(null)
|
||||
|
||||
function insertVariable(variable: string): void {
|
||||
const placeholder = `{{${variable}}}`
|
||||
|
||||
// Find the textarea element within the VTextarea component
|
||||
const textareaEl = document.querySelector('.body-editor textarea') as HTMLTextAreaElement | null
|
||||
|
||||
if (textareaEl) {
|
||||
const start = textareaEl.selectionStart
|
||||
const end = textareaEl.selectionEnd
|
||||
const currentValue = form.body
|
||||
|
||||
form.body = currentValue.substring(0, start) + placeholder + currentValue.substring(end)
|
||||
|
||||
// Set cursor position after inserted variable
|
||||
const newPosition = start + placeholder.length
|
||||
requestAnimationFrame(() => {
|
||||
textareaEl.focus()
|
||||
textareaEl.setSelectionRange(newPosition, newPosition)
|
||||
})
|
||||
}
|
||||
else {
|
||||
// Fallback: append to end
|
||||
form.body += placeholder
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreview(): Promise<void> {
|
||||
previewLoading.value = true
|
||||
showPreviewDialog.value = true
|
||||
|
||||
try {
|
||||
const response = await fetch(`/email-templates/${props.template.id}/preview`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
previewSubject.value = data.subject
|
||||
previewBody.value = data.body
|
||||
}
|
||||
}
|
||||
catch {
|
||||
previewSubject.value = 'Preview failed'
|
||||
previewBody.value = 'Unable to load preview.'
|
||||
}
|
||||
finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmReset(): void {
|
||||
showResetDialog.value = true
|
||||
}
|
||||
|
||||
function resetToDefault(): void {
|
||||
router.post(`/email-templates/${props.template.id}/reset`, {}, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
showResetDialog.value = false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function submit(): void {
|
||||
form.put(`/email-templates/${props.template.id}`, {
|
||||
preserveScroll: true,
|
||||
})
|
||||
}
|
||||
|
||||
const formattedUpdatedAt = computed<string>(() => {
|
||||
const date = new Date(props.template.updated_at)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div class="d-flex align-center justify-space-between mb-6">
|
||||
<div>
|
||||
<div class="d-flex align-center gap-2 mb-1">
|
||||
<Link
|
||||
href="/email-templates"
|
||||
class="text-decoration-none"
|
||||
>
|
||||
<VBtn
|
||||
icon="tabler-arrow-left"
|
||||
variant="text"
|
||||
size="small"
|
||||
/>
|
||||
</Link>
|
||||
<span class="text-h4 font-weight-bold">Edit Email Template</span>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis ms-10">
|
||||
Editing "{{ template.name }}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<VRow>
|
||||
<!-- Main Content -->
|
||||
<VCol
|
||||
cols="12"
|
||||
lg="8"
|
||||
>
|
||||
<!-- Template Details -->
|
||||
<VCard
|
||||
title="Template Content"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<!-- Template Name (read-only) -->
|
||||
<VCol cols="12">
|
||||
<div class="d-flex align-center gap-2 mb-4">
|
||||
<span class="text-body-1 font-weight-medium">{{ template.name }}</span>
|
||||
<VChip
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
class="font-monospace"
|
||||
>
|
||||
{{ template.slug }}
|
||||
</VChip>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<!-- Subject -->
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="form.subject"
|
||||
label="Email Subject"
|
||||
placeholder="Enter email subject line..."
|
||||
:error-messages="form.errors.subject"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<!-- Body -->
|
||||
<VCol cols="12">
|
||||
<VTextarea
|
||||
v-model="form.body"
|
||||
label="Email Body (Markdown)"
|
||||
placeholder="Enter email body content..."
|
||||
:rows="16"
|
||||
:error-messages="form.errors.body"
|
||||
class="body-editor"
|
||||
style="font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 13px;"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Available Variables -->
|
||||
<VCard
|
||||
title="Available Variables"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Click a variable to insert it at the cursor position in the body editor. Variables use the format <code v-text="'{{variable_name}}'"></code>.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<VChip
|
||||
v-for="variable in template.available_variables"
|
||||
:key="variable"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="cursor-pointer font-monospace"
|
||||
@click="insertVariable(variable)"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-plus"
|
||||
size="14"
|
||||
start
|
||||
/>
|
||||
<span v-text="'{{' + variable + '}}'"></span>
|
||||
</VChip>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<VCol
|
||||
cols="12"
|
||||
lg="4"
|
||||
>
|
||||
<!-- Template Info -->
|
||||
<VCard
|
||||
title="Template Info"
|
||||
class="mb-6"
|
||||
>
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<span class="text-body-2 text-medium-emphasis">Status</span>
|
||||
<VSwitch
|
||||
v-model="form.is_active"
|
||||
:label="form.is_active ? 'Active' : 'Inactive'"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="text-body-2 text-medium-emphasis">Last Modified</span>
|
||||
<span class="text-body-2">{{ formattedUpdatedAt }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="text-body-2 text-medium-emphasis">Variables</span>
|
||||
<span class="text-body-2 font-weight-medium">{{ template.available_variables.length }}</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Preview -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<VBtn
|
||||
color="info"
|
||||
variant="tonal"
|
||||
block
|
||||
prepend-icon="tabler-eye"
|
||||
@click="loadPreview"
|
||||
>
|
||||
Preview with Sample Data
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Actions -->
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
block
|
||||
:loading="form.processing"
|
||||
:disabled="form.processing"
|
||||
prepend-icon="tabler-check"
|
||||
class="mb-3"
|
||||
>
|
||||
Save Changes
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
block
|
||||
prepend-icon="tabler-restore"
|
||||
class="mb-3"
|
||||
@click="confirmReset"
|
||||
>
|
||||
Reset to Default
|
||||
</VBtn>
|
||||
<Link
|
||||
href="/email-templates"
|
||||
class="text-decoration-none"
|
||||
>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
block
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</Link>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</form>
|
||||
|
||||
<!-- Preview Dialog -->
|
||||
<VDialog
|
||||
v-model="showPreviewDialog"
|
||||
max-width="700"
|
||||
>
|
||||
<VCard>
|
||||
<VCardTitle class="d-flex align-center justify-space-between">
|
||||
<span>Email Preview</span>
|
||||
<VBtn
|
||||
icon="tabler-x"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="showPreviewDialog = false"
|
||||
/>
|
||||
</VCardTitle>
|
||||
<VDivider />
|
||||
<VCardText v-if="previewLoading">
|
||||
<div class="text-center py-8">
|
||||
<VProgressCircular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<div class="text-body-2 text-medium-emphasis mt-2">
|
||||
Loading preview...
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
<VCardText v-else>
|
||||
<div class="mb-4">
|
||||
<span class="text-body-2 text-medium-emphasis">Subject:</span>
|
||||
<div class="text-body-1 font-weight-medium mt-1">
|
||||
{{ previewSubject }}
|
||||
</div>
|
||||
</div>
|
||||
<VDivider class="mb-4" />
|
||||
<div>
|
||||
<span class="text-body-2 text-medium-emphasis">Body:</span>
|
||||
<div
|
||||
class="mt-2 pa-4 rounded-lg"
|
||||
style="background: rgba(var(--v-theme-on-surface), 0.04); white-space: pre-wrap; font-family: inherit; line-height: 1.6;"
|
||||
>
|
||||
{{ previewBody }}
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
|
||||
<!-- Reset Confirmation Dialog -->
|
||||
<VDialog
|
||||
v-model="showResetDialog"
|
||||
max-width="450"
|
||||
>
|
||||
<VCard>
|
||||
<VCardTitle>Reset to Default</VCardTitle>
|
||||
<VCardText>
|
||||
Are you sure you want to reset this template to its default content? This will overwrite your current subject and body text.
|
||||
</VCardText>
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
variant="text"
|
||||
@click="showResetDialog = false"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="warning"
|
||||
@click="resetToDefault"
|
||||
>
|
||||
Reset
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,8 +2,6 @@
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import { formatPrice } from '@/utils/resolvers'
|
||||
|
||||
interface CustomerOption {
|
||||
@@ -135,7 +133,7 @@ function submitAndSend(): void {
|
||||
>
|
||||
<VRow align="center">
|
||||
<VCol cols="12" md="5">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="item.description"
|
||||
placeholder="Item description"
|
||||
density="compact"
|
||||
@@ -143,7 +141,7 @@ function submitAndSend(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="2">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model.number="item.quantity"
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -153,7 +151,7 @@ function submitAndSend(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="item.unit_price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
@@ -209,7 +207,7 @@ function submitAndSend(): void {
|
||||
<!-- Notes -->
|
||||
<VCard title="Notes" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="form.notes"
|
||||
label="Invoice Notes"
|
||||
placeholder="Optional notes to include on the invoice..."
|
||||
@@ -225,7 +223,7 @@ function submitAndSend(): void {
|
||||
<!-- Due Date -->
|
||||
<VCard title="Due Date" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.due_date"
|
||||
label="Due Date"
|
||||
type="date"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import { resolveInvoiceStatusColor, formatPrice } from '@/utils/resolvers'
|
||||
|
||||
interface InvoiceUser {
|
||||
@@ -151,7 +149,7 @@ function submit(): void {
|
||||
>
|
||||
<VRow align="center">
|
||||
<VCol cols="12" md="5">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="item.description"
|
||||
placeholder="Item description"
|
||||
density="compact"
|
||||
@@ -159,7 +157,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="2">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model.number="item.quantity"
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -169,7 +167,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="6" md="3">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="item.unit_price"
|
||||
type="number"
|
||||
step="0.01"
|
||||
@@ -225,7 +223,7 @@ function submit(): void {
|
||||
<!-- Notes -->
|
||||
<VCard title="Notes" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="form.notes"
|
||||
label="Invoice Notes"
|
||||
placeholder="Optional notes to include on the invoice..."
|
||||
@@ -269,7 +267,7 @@ function submit(): void {
|
||||
<!-- Due Date -->
|
||||
<VCard title="Due Date" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.due_date"
|
||||
label="Due Date"
|
||||
type="date"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed, ref } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import { formatPrice } from '@/utils/resolvers'
|
||||
import type { StatusColor } from '@/types'
|
||||
|
||||
@@ -465,7 +464,7 @@ function configurationEntries(): Array<{ key: string; value: string }> {
|
||||
</VCardTitle>
|
||||
|
||||
<VCardText>
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="notesForm.admin_notes"
|
||||
placeholder="Add internal notes about this order..."
|
||||
rows="4"
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { watch } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
|
||||
defineOptions({ layout: AdminLayout })
|
||||
|
||||
@@ -99,7 +96,7 @@ function submit(): void {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.name"
|
||||
label="Plan Name"
|
||||
placeholder="e.g. Basic VPS"
|
||||
@@ -107,7 +104,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.slug"
|
||||
label="Slug"
|
||||
placeholder="e.g. basic-vps"
|
||||
@@ -116,7 +113,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="form.description"
|
||||
label="Description"
|
||||
placeholder="Brief description of the plan..."
|
||||
@@ -133,7 +130,7 @@ function submit(): void {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.service_type"
|
||||
label="Service Type"
|
||||
:items="serviceTypeOptions"
|
||||
@@ -142,7 +139,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.price"
|
||||
label="Price (USD)"
|
||||
type="number"
|
||||
@@ -153,7 +150,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.billing_cycle"
|
||||
label="Billing Cycle"
|
||||
:items="billingCycleOptions"
|
||||
@@ -182,7 +179,7 @@ function submit(): void {
|
||||
>
|
||||
<VRow align="center">
|
||||
<VCol cols="12" md="5">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="feature.key"
|
||||
placeholder="Feature name (e.g. cpu, ram)"
|
||||
density="compact"
|
||||
@@ -190,7 +187,7 @@ function submit(): void {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="5">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="feature.value"
|
||||
placeholder="Value (e.g. 2 vCPU, 4 GB)"
|
||||
density="compact"
|
||||
@@ -226,7 +223,7 @@ function submit(): void {
|
||||
<VCol cols="12" lg="4">
|
||||
<VCard title="Inventory & Ordering" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.stock_quantity"
|
||||
label="Stock Quantity"
|
||||
type="number"
|
||||
@@ -235,7 +232,7 @@ function submit(): void {
|
||||
:error-messages="form.errors.stock_quantity"
|
||||
class="mb-4"
|
||||
/>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.sort_order"
|
||||
label="Sort Order"
|
||||
type="number"
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed, watch } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import type { Plan } from '@/types'
|
||||
|
||||
defineOptions({ layout: AdminLayout })
|
||||
@@ -123,7 +120,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.name"
|
||||
label="Plan Name"
|
||||
placeholder="e.g. Basic VPS"
|
||||
@@ -131,7 +128,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.slug"
|
||||
label="Slug"
|
||||
placeholder="e.g. basic-vps"
|
||||
@@ -140,7 +137,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="form.description"
|
||||
label="Description"
|
||||
placeholder="Brief description of the plan..."
|
||||
@@ -157,7 +154,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.service_type"
|
||||
label="Service Type"
|
||||
:items="serviceTypeOptions"
|
||||
@@ -166,7 +163,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.price"
|
||||
label="Price (USD)"
|
||||
type="number"
|
||||
@@ -177,7 +174,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.billing_cycle"
|
||||
label="Billing Cycle"
|
||||
:items="billingCycleOptions"
|
||||
@@ -206,7 +203,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
>
|
||||
<VRow align="center">
|
||||
<VCol cols="12" md="5">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="feature.key"
|
||||
placeholder="Feature name (e.g. cpu, ram)"
|
||||
density="compact"
|
||||
@@ -214,7 +211,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="5">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="feature.value"
|
||||
placeholder="Value (e.g. 2 vCPU, 4 GB)"
|
||||
density="compact"
|
||||
@@ -275,7 +272,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
<!-- Inventory & Ordering -->
|
||||
<VCard title="Inventory & Ordering" class="mb-6">
|
||||
<VCardText>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.stock_quantity"
|
||||
label="Stock Quantity"
|
||||
type="number"
|
||||
@@ -284,7 +281,7 @@ const formattedCreatedAt = computed<string>(() => {
|
||||
:error-messages="form.errors.stock_quantity"
|
||||
class="mb-4"
|
||||
/>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.sort_order"
|
||||
label="Sort Order"
|
||||
type="number"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
|
||||
interface SettingsGroup {
|
||||
[key: string]: string | boolean | null
|
||||
@@ -284,7 +282,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
<form @submit.prevent="submitGeneral">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="generalForm.company_name"
|
||||
label="Company Name"
|
||||
placeholder="EZSCALE"
|
||||
@@ -293,7 +291,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="generalForm.company_email"
|
||||
label="Company Email"
|
||||
type="email"
|
||||
@@ -303,7 +301,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="generalForm.support_url"
|
||||
label="Support URL"
|
||||
placeholder="https://support.ezscale.cloud"
|
||||
@@ -312,7 +310,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="generalForm.status_page_url"
|
||||
label="Status Page URL"
|
||||
placeholder="https://status.ezscale.cloud"
|
||||
@@ -366,7 +364,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VAlert>
|
||||
<VRow class="mb-4">
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.virtfusion_api_url"
|
||||
label="API URL"
|
||||
placeholder="https://vps.ezscale.cloud/api/v1"
|
||||
@@ -374,7 +372,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.virtfusion_api_token"
|
||||
label="API Token"
|
||||
:type="showVirtfusionToken ? 'text' : 'password'"
|
||||
@@ -388,7 +386,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
@click="showVirtfusionToken = !showVirtfusionToken"
|
||||
/>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -422,7 +420,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VAlert>
|
||||
<VRow class="mb-4">
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.pterodactyl_api_url"
|
||||
label="Panel URL"
|
||||
placeholder="https://game.ezscale.cloud"
|
||||
@@ -430,7 +428,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.pterodactyl_api_token"
|
||||
label="API Key"
|
||||
:type="showPterodactylToken ? 'text' : 'password'"
|
||||
@@ -444,7 +442,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
@click="showPterodactylToken = !showPterodactylToken"
|
||||
/>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -478,7 +476,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VAlert>
|
||||
<VRow class="mb-4">
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.synergycp_api_url"
|
||||
label="API URL"
|
||||
placeholder="https://dedicated.ezscale.cloud/api"
|
||||
@@ -486,7 +484,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.synergycp_api_token"
|
||||
label="API Token"
|
||||
:type="showSynergycpToken ? 'text' : 'password'"
|
||||
@@ -500,7 +498,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
@click="showSynergycpToken = !showSynergycpToken"
|
||||
/>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -534,7 +532,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VAlert>
|
||||
<VRow class="mb-4">
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.enhance_api_url"
|
||||
label="API URL"
|
||||
placeholder="https://hosting.ezscale.cloud/api"
|
||||
@@ -542,7 +540,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.enhance_api_token"
|
||||
label="API Token"
|
||||
:type="showEnhanceToken ? 'text' : 'password'"
|
||||
@@ -556,10 +554,10 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
@click="showEnhanceToken = !showEnhanceToken"
|
||||
/>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="apiForm.enhance_organization_id"
|
||||
label="Organization ID"
|
||||
:type="showEnhanceOrgId ? 'text' : 'password'"
|
||||
@@ -573,7 +571,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
@click="showEnhanceOrgId = !showEnhanceOrgId"
|
||||
/>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
@@ -589,7 +587,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</div>
|
||||
<VRow class="mb-4">
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
:model-value="(props.settings.api.stripe_publishable_key as string) || 'Not configured'"
|
||||
label="Publishable Key"
|
||||
readonly
|
||||
@@ -597,7 +595,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
:model-value="(props.settings.api.stripe_secret_key as string) || 'Not configured'"
|
||||
label="Secret Key"
|
||||
readonly
|
||||
@@ -618,7 +616,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</div>
|
||||
<VRow class="mb-6">
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
:model-value="(props.settings.api.paypal_client_id as string) || 'Not configured'"
|
||||
label="Client ID"
|
||||
readonly
|
||||
@@ -626,7 +624,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
:model-value="(props.settings.api.paypal_client_secret as string) || 'Not configured'"
|
||||
label="Client Secret"
|
||||
readonly
|
||||
@@ -695,7 +693,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
>
|
||||
{{ discordTestResults.payment.message }}
|
||||
</VAlert>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="discordForm.discord_payment_webhook_url"
|
||||
label="Webhook URL"
|
||||
:placeholder="props.settings.discord.discord_payment_webhook_url_set ? '******** (URL is set, leave blank to keep)' : 'https://discord.com/api/webhooks/...'"
|
||||
@@ -704,7 +702,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
<template #prepend-inner>
|
||||
<VIcon icon="tabler-brand-discord" />
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
@@ -748,7 +746,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
>
|
||||
{{ discordTestResults.provisioning.message }}
|
||||
</VAlert>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="discordForm.discord_provisioning_webhook_url"
|
||||
label="Webhook URL"
|
||||
:placeholder="props.settings.discord.discord_provisioning_webhook_url_set ? '******** (URL is set, leave blank to keep)' : 'https://discord.com/api/webhooks/...'"
|
||||
@@ -757,7 +755,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
<template #prepend-inner>
|
||||
<VIcon icon="tabler-brand-discord" />
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
@@ -801,7 +799,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
>
|
||||
{{ discordTestResults.support.message }}
|
||||
</VAlert>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="discordForm.discord_support_webhook_url"
|
||||
label="Webhook URL"
|
||||
:placeholder="props.settings.discord.discord_support_webhook_url_set ? '******** (URL is set, leave blank to keep)' : 'https://discord.com/api/webhooks/...'"
|
||||
@@ -810,7 +808,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
<template #prepend-inner>
|
||||
<VIcon icon="tabler-brand-discord" />
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
@@ -854,7 +852,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
>
|
||||
{{ discordTestResults.system.message }}
|
||||
</VAlert>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="discordForm.discord_system_webhook_url"
|
||||
label="Webhook URL"
|
||||
:placeholder="props.settings.discord.discord_system_webhook_url_set ? '******** (URL is set, leave blank to keep)' : 'https://discord.com/api/webhooks/...'"
|
||||
@@ -863,7 +861,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
<template #prepend-inner>
|
||||
<VIcon icon="tabler-brand-discord" />
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
@@ -890,7 +888,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
|
||||
<VRow class="mb-6">
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="billingForm.default_currency"
|
||||
label="Default Currency"
|
||||
:items="currencyOptions"
|
||||
@@ -899,7 +897,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.grace_period_days"
|
||||
label="Grace Period (days)"
|
||||
type="number"
|
||||
@@ -920,11 +918,11 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
Days after invoice due date before suspension warning is sent
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.suspension_warning_days"
|
||||
label="Suspension Warning (days)"
|
||||
type="number"
|
||||
@@ -945,11 +943,11 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
Days after warning before service is suspended
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.auto_terminate_days"
|
||||
label="Auto-Terminate (days)"
|
||||
type="number"
|
||||
@@ -970,7 +968,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
Days after suspension before service is automatically terminated
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
@@ -994,7 +992,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
|
||||
<VRow class="mb-4">
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.bandwidth_overage_rate"
|
||||
label="Overage Rate ($/GB)"
|
||||
type="number"
|
||||
@@ -1015,11 +1013,11 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
Price charged per GB over the plan's included bandwidth
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.bandwidth_grace_period_days"
|
||||
label="Grace Period Before Billing (days)"
|
||||
type="number"
|
||||
@@ -1040,7 +1038,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
Days after overage detected before billing begins
|
||||
</VTooltip>
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="4">
|
||||
@@ -1194,7 +1192,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
<form @submit.prevent="submitNotifications">
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="notificationsForm.email_from_address"
|
||||
label="Email From Address"
|
||||
type="email"
|
||||
@@ -1204,7 +1202,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="notificationsForm.email_from_name"
|
||||
label="Email From Name"
|
||||
placeholder="EZSCALE"
|
||||
|
||||
196
website/resources/ts/Pages/Admin/TaxRates/Create.vue
Normal file
196
website/resources/ts/Pages/Admin/TaxRates/Create.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts" setup>
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
|
||||
defineOptions({ layout: AdminLayout })
|
||||
|
||||
const countryOptions = [
|
||||
{ title: 'United States (US)', value: 'US' },
|
||||
{ title: 'United Kingdom (GB)', value: 'GB' },
|
||||
{ title: 'Germany (DE)', value: 'DE' },
|
||||
{ title: 'France (FR)', value: 'FR' },
|
||||
{ title: 'Canada (CA)', value: 'CA' },
|
||||
{ title: 'Australia (AU)', value: 'AU' },
|
||||
{ title: 'Netherlands (NL)', value: 'NL' },
|
||||
{ title: 'Sweden (SE)', value: 'SE' },
|
||||
{ title: 'Japan (JP)', value: 'JP' },
|
||||
{ title: 'Brazil (BR)', value: 'BR' },
|
||||
{ title: 'Italy (IT)', value: 'IT' },
|
||||
{ title: 'Spain (ES)', value: 'ES' },
|
||||
{ title: 'Ireland (IE)', value: 'IE' },
|
||||
{ title: 'New Zealand (NZ)', value: 'NZ' },
|
||||
{ title: 'Singapore (SG)', value: 'SG' },
|
||||
{ title: 'India (IN)', value: 'IN' },
|
||||
{ title: 'Switzerland (CH)', value: 'CH' },
|
||||
{ title: 'Austria (AT)', value: 'AT' },
|
||||
{ title: 'Belgium (BE)', value: 'BE' },
|
||||
{ title: 'Portugal (PT)', value: 'PT' },
|
||||
{ title: 'Denmark (DK)', value: 'DK' },
|
||||
{ title: 'Norway (NO)', value: 'NO' },
|
||||
{ title: 'Finland (FI)', value: 'FI' },
|
||||
{ title: 'Poland (PL)', value: 'PL' },
|
||||
{ title: 'Czech Republic (CZ)', value: 'CZ' },
|
||||
{ title: 'Mexico (MX)', value: 'MX' },
|
||||
]
|
||||
|
||||
const typeOptions = [
|
||||
{ title: 'Exclusive (added on top of price)', value: 'exclusive' },
|
||||
{ title: 'Inclusive (included in price)', value: 'inclusive' },
|
||||
]
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
country_code: '' as string,
|
||||
region_code: '' as string,
|
||||
rate: '' as string | number,
|
||||
type: 'exclusive' as string,
|
||||
priority: 0,
|
||||
is_active: true,
|
||||
})
|
||||
|
||||
const typeHint = computed<string>(() => {
|
||||
if (form.type === 'inclusive') {
|
||||
return 'Tax is already included in the displayed price.'
|
||||
}
|
||||
return 'Tax will be added on top of the displayed price at checkout.'
|
||||
})
|
||||
|
||||
function submit(): void {
|
||||
form.post('/tax-rates', {
|
||||
preserveScroll: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div class="d-flex align-center justify-space-between mb-6">
|
||||
<div>
|
||||
<div class="d-flex align-center gap-2 mb-1">
|
||||
<Link href="/tax-rates" class="text-decoration-none">
|
||||
<VBtn icon="tabler-arrow-left" variant="text" size="small" />
|
||||
</Link>
|
||||
<span class="text-h4 font-weight-bold">Create Tax Rate</span>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis ms-10">
|
||||
Add a new tax rate for a country or region
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<VRow>
|
||||
<!-- Tax Rate Details -->
|
||||
<VCol cols="12" lg="8">
|
||||
<VCard title="Tax Rate Details" class="mb-6">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="form.name"
|
||||
label="Name"
|
||||
placeholder="e.g. US Sales Tax - California, EU VAT - Germany"
|
||||
:error-messages="form.errors.name"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="form.country_code"
|
||||
label="Country"
|
||||
:items="countryOptions"
|
||||
placeholder="Select a country"
|
||||
:error-messages="form.errors.country_code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="form.region_code"
|
||||
label="Region / State Code"
|
||||
placeholder="e.g. CA, NY, ON (optional)"
|
||||
:error-messages="form.errors.region_code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="form.rate"
|
||||
label="Tax Rate (%)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="e.g. 21.00"
|
||||
:error-messages="form.errors.rate"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="form.type"
|
||||
label="Tax Type"
|
||||
:items="typeOptions"
|
||||
:error-messages="form.errors.type"
|
||||
/>
|
||||
<p class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ typeHint }}
|
||||
</p>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<VCol cols="12" lg="4">
|
||||
<VCard title="Options" class="mb-6">
|
||||
<VCardText>
|
||||
<VTextField
|
||||
v-model="form.priority"
|
||||
label="Priority"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
:error-messages="form.errors.priority"
|
||||
class="mb-4"
|
||||
/>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Higher priority tax rates are applied first when multiple rates match a location.
|
||||
</p>
|
||||
<VSwitch
|
||||
v-model="form.is_active"
|
||||
label="Active"
|
||||
color="primary"
|
||||
:error-messages="form.errors.is_active"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Actions -->
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
block
|
||||
:loading="form.processing"
|
||||
:disabled="form.processing"
|
||||
prepend-icon="tabler-check"
|
||||
class="mb-3"
|
||||
>
|
||||
Create Tax Rate
|
||||
</VBtn>
|
||||
<Link href="/tax-rates" class="text-decoration-none">
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
block
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</Link>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
232
website/resources/ts/Pages/Admin/TaxRates/Edit.vue
Normal file
232
website/resources/ts/Pages/Admin/TaxRates/Edit.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<script lang="ts" setup>
|
||||
import { Link, useForm } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import type { TaxRate } from '@/types'
|
||||
|
||||
interface Props {
|
||||
taxRate: TaxRate
|
||||
}
|
||||
|
||||
defineOptions({ layout: AdminLayout })
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const countryOptions = [
|
||||
{ title: 'United States (US)', value: 'US' },
|
||||
{ title: 'United Kingdom (GB)', value: 'GB' },
|
||||
{ title: 'Germany (DE)', value: 'DE' },
|
||||
{ title: 'France (FR)', value: 'FR' },
|
||||
{ title: 'Canada (CA)', value: 'CA' },
|
||||
{ title: 'Australia (AU)', value: 'AU' },
|
||||
{ title: 'Netherlands (NL)', value: 'NL' },
|
||||
{ title: 'Sweden (SE)', value: 'SE' },
|
||||
{ title: 'Japan (JP)', value: 'JP' },
|
||||
{ title: 'Brazil (BR)', value: 'BR' },
|
||||
{ title: 'Italy (IT)', value: 'IT' },
|
||||
{ title: 'Spain (ES)', value: 'ES' },
|
||||
{ title: 'Ireland (IE)', value: 'IE' },
|
||||
{ title: 'New Zealand (NZ)', value: 'NZ' },
|
||||
{ title: 'Singapore (SG)', value: 'SG' },
|
||||
{ title: 'India (IN)', value: 'IN' },
|
||||
{ title: 'Switzerland (CH)', value: 'CH' },
|
||||
{ title: 'Austria (AT)', value: 'AT' },
|
||||
{ title: 'Belgium (BE)', value: 'BE' },
|
||||
{ title: 'Portugal (PT)', value: 'PT' },
|
||||
{ title: 'Denmark (DK)', value: 'DK' },
|
||||
{ title: 'Norway (NO)', value: 'NO' },
|
||||
{ title: 'Finland (FI)', value: 'FI' },
|
||||
{ title: 'Poland (PL)', value: 'PL' },
|
||||
{ title: 'Czech Republic (CZ)', value: 'CZ' },
|
||||
{ title: 'Mexico (MX)', value: 'MX' },
|
||||
]
|
||||
|
||||
const typeOptions = [
|
||||
{ title: 'Exclusive (added on top of price)', value: 'exclusive' },
|
||||
{ title: 'Inclusive (included in price)', value: 'inclusive' },
|
||||
]
|
||||
|
||||
const form = useForm({
|
||||
name: props.taxRate.name,
|
||||
country_code: props.taxRate.country_code,
|
||||
region_code: props.taxRate.region_code ?? '',
|
||||
rate: props.taxRate.rate,
|
||||
type: props.taxRate.type,
|
||||
priority: props.taxRate.priority,
|
||||
is_active: props.taxRate.is_active,
|
||||
})
|
||||
|
||||
const typeHint = computed<string>(() => {
|
||||
if (form.type === 'inclusive') {
|
||||
return 'Tax is already included in the displayed price.'
|
||||
}
|
||||
return 'Tax will be added on top of the displayed price at checkout.'
|
||||
})
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
function submit(): void {
|
||||
form.put(`/tax-rates/${props.taxRate.id}`, {
|
||||
preserveScroll: true,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div class="d-flex align-center justify-space-between mb-6">
|
||||
<div>
|
||||
<div class="d-flex align-center gap-2 mb-1">
|
||||
<Link href="/tax-rates" class="text-decoration-none">
|
||||
<VBtn icon="tabler-arrow-left" variant="text" size="small" />
|
||||
</Link>
|
||||
<span class="text-h4 font-weight-bold">Edit Tax Rate</span>
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis ms-10">
|
||||
Update "{{ taxRate.name }}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<VRow>
|
||||
<!-- Tax Rate Details -->
|
||||
<VCol cols="12" lg="8">
|
||||
<VCard title="Tax Rate Details" class="mb-6">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
v-model="form.name"
|
||||
label="Name"
|
||||
placeholder="e.g. US Sales Tax - California, EU VAT - Germany"
|
||||
:error-messages="form.errors.name"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="form.country_code"
|
||||
label="Country"
|
||||
:items="countryOptions"
|
||||
placeholder="Select a country"
|
||||
:error-messages="form.errors.country_code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="form.region_code"
|
||||
label="Region / State Code"
|
||||
placeholder="e.g. CA, NY, ON (optional)"
|
||||
:error-messages="form.errors.region_code"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VTextField
|
||||
v-model="form.rate"
|
||||
label="Tax Rate (%)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="e.g. 21.00"
|
||||
:error-messages="form.errors.rate"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<VSelect
|
||||
v-model="form.type"
|
||||
label="Tax Type"
|
||||
:items="typeOptions"
|
||||
:error-messages="form.errors.type"
|
||||
/>
|
||||
<p class="text-body-2 text-medium-emphasis mt-1">
|
||||
{{ typeHint }}
|
||||
</p>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<VCol cols="12" lg="4">
|
||||
<!-- Info -->
|
||||
<VCard title="Tax Rate Info" class="mb-6">
|
||||
<VCardText>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<span class="text-body-2 text-medium-emphasis">Status</span>
|
||||
<VChip
|
||||
:color="taxRate.is_active ? 'success' : 'error'"
|
||||
size="small"
|
||||
>
|
||||
{{ taxRate.is_active ? 'Active' : 'Inactive' }}
|
||||
</VChip>
|
||||
</div>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="text-body-2 text-medium-emphasis">Created</span>
|
||||
<span class="text-body-2">{{ formatDate(taxRate.created_at) }}</span>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Options -->
|
||||
<VCard title="Options" class="mb-6">
|
||||
<VCardText>
|
||||
<VTextField
|
||||
v-model="form.priority"
|
||||
label="Priority"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
:error-messages="form.errors.priority"
|
||||
class="mb-4"
|
||||
/>
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
Higher priority tax rates are applied first when multiple rates match a location.
|
||||
</p>
|
||||
<VSwitch
|
||||
v-model="form.is_active"
|
||||
label="Active"
|
||||
color="primary"
|
||||
:error-messages="form.errors.is_active"
|
||||
/>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Actions -->
|
||||
<VCard>
|
||||
<VCardText>
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
block
|
||||
:loading="form.processing"
|
||||
:disabled="form.processing"
|
||||
prepend-icon="tabler-check"
|
||||
class="mb-3"
|
||||
>
|
||||
Update Tax Rate
|
||||
</VBtn>
|
||||
<Link href="/tax-rates" class="text-decoration-none">
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
block
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</Link>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
279
website/resources/ts/Pages/Admin/TaxRates/Index.vue
Normal file
279
website/resources/ts/Pages/Admin/TaxRates/Index.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<script lang="ts" setup>
|
||||
import { Link, router } from '@inertiajs/vue3'
|
||||
import { ref, watch } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import type { PaginatedResponse, StatusColor, TaxRate } from '@/types'
|
||||
|
||||
interface Props {
|
||||
taxRates: PaginatedResponse<TaxRate>
|
||||
countries: string[]
|
||||
filters: {
|
||||
search: string
|
||||
country: string
|
||||
status: string
|
||||
}
|
||||
}
|
||||
|
||||
defineOptions({ layout: AdminLayout })
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const search = ref<string>(props.filters.search)
|
||||
const countryFilter = ref<string>(props.filters.country)
|
||||
const statusFilter = ref<string>(props.filters.status)
|
||||
|
||||
const countryNames: Record<string, string> = {
|
||||
US: 'United States',
|
||||
GB: 'United Kingdom',
|
||||
DE: 'Germany',
|
||||
FR: 'France',
|
||||
CA: 'Canada',
|
||||
AU: 'Australia',
|
||||
NL: 'Netherlands',
|
||||
SE: 'Sweden',
|
||||
JP: 'Japan',
|
||||
BR: 'Brazil',
|
||||
IT: 'Italy',
|
||||
ES: 'Spain',
|
||||
IE: 'Ireland',
|
||||
NZ: 'New Zealand',
|
||||
SG: 'Singapore',
|
||||
IN: 'India',
|
||||
CH: 'Switzerland',
|
||||
AT: 'Austria',
|
||||
BE: 'Belgium',
|
||||
PT: 'Portugal',
|
||||
DK: 'Denmark',
|
||||
NO: 'Norway',
|
||||
FI: 'Finland',
|
||||
PL: 'Poland',
|
||||
CZ: 'Czech Republic',
|
||||
MX: 'Mexico',
|
||||
}
|
||||
|
||||
const countryFilterItems = [
|
||||
{ title: 'All Countries', value: 'all' },
|
||||
...props.countries.map(code => ({
|
||||
title: `${countryNames[code] ?? code} (${code})`,
|
||||
value: code,
|
||||
})),
|
||||
]
|
||||
|
||||
const statusFilterItems = [
|
||||
{ title: 'All Statuses', value: 'all' },
|
||||
{ title: 'Active', value: 'active' },
|
||||
{ title: 'Inactive', value: 'inactive' },
|
||||
]
|
||||
|
||||
const tableHeaders = [
|
||||
{ title: 'Name', key: 'name', sortable: true },
|
||||
{ title: 'Country', key: 'country_code', sortable: true },
|
||||
{ title: 'Region', key: 'region_code', sortable: true },
|
||||
{ title: 'Rate', key: 'rate', sortable: true, align: 'end' as const },
|
||||
{ title: 'Type', key: 'type', sortable: true },
|
||||
{ title: 'Priority', key: 'priority', sortable: true, align: 'center' as const },
|
||||
{ title: 'Status', key: 'is_active', sortable: true, align: 'center' as const },
|
||||
{ title: 'Actions', key: 'actions', sortable: false, align: 'center' as const },
|
||||
]
|
||||
|
||||
function getCountryName(code: string): string {
|
||||
return countryNames[code] ?? code
|
||||
}
|
||||
|
||||
function resolveTypeColor(type: string): StatusColor {
|
||||
return type === 'inclusive' ? 'info' : 'warning'
|
||||
}
|
||||
|
||||
function applyFilters(): void {
|
||||
router.get('/tax-rates', {
|
||||
search: search.value || undefined,
|
||||
country: countryFilter.value !== 'all' ? countryFilter.value : undefined,
|
||||
status: statusFilter.value !== 'all' ? statusFilter.value : undefined,
|
||||
}, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
})
|
||||
}
|
||||
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(search, () => {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
searchTimeout = setTimeout(() => applyFilters(), 300)
|
||||
})
|
||||
|
||||
watch([countryFilter, statusFilter], () => {
|
||||
applyFilters()
|
||||
})
|
||||
|
||||
function toggleActive(taxRate: TaxRate): void {
|
||||
router.post(`/tax-rates/${taxRate.id}/toggle-active`, {}, {
|
||||
preserveScroll: true,
|
||||
})
|
||||
}
|
||||
|
||||
function deleteTaxRate(taxRate: TaxRate): void {
|
||||
if (confirm(`Are you sure you want to delete "${taxRate.name}"? This action cannot be undone.`)) {
|
||||
router.delete(`/tax-rates/${taxRate.id}`, {
|
||||
preserveScroll: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header -->
|
||||
<div class="d-flex align-center justify-space-between mb-6">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">
|
||||
Tax Rates
|
||||
</div>
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
Manage tax rates by country and region
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/tax-rates/create">
|
||||
<VBtn color="primary" prepend-icon="tabler-plus">
|
||||
Create Tax Rate
|
||||
</VBtn>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<VCard class="mb-6">
|
||||
<VCardText>
|
||||
<VRow>
|
||||
<VCol cols="12" md="4">
|
||||
<VTextField
|
||||
v-model="search"
|
||||
placeholder="Search by name, country, or region..."
|
||||
prepend-inner-icon="tabler-search"
|
||||
clearable
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="countryFilter"
|
||||
:items="countryFilterItems"
|
||||
placeholder="Filter by country"
|
||||
/>
|
||||
</VCol>
|
||||
<VCol cols="12" md="4">
|
||||
<VSelect
|
||||
v-model="statusFilter"
|
||||
:items="statusFilterItems"
|
||||
placeholder="Filter by status"
|
||||
/>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
<!-- Tax Rates Table -->
|
||||
<VCard>
|
||||
<VDataTable
|
||||
:headers="tableHeaders"
|
||||
:items="taxRates.data"
|
||||
:items-per-page="25"
|
||||
hover
|
||||
class="text-no-wrap"
|
||||
>
|
||||
<!-- Name -->
|
||||
<template #item.name="{ item }">
|
||||
<span class="font-weight-medium">{{ item.name }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Country -->
|
||||
<template #item.country_code="{ item }">
|
||||
<span>{{ getCountryName(item.country_code) }}</span>
|
||||
<span class="text-medium-emphasis ms-1">({{ item.country_code }})</span>
|
||||
</template>
|
||||
|
||||
<!-- Region -->
|
||||
<template #item.region_code="{ item }">
|
||||
<span v-if="item.region_code">{{ item.region_code }}</span>
|
||||
<span v-else class="text-medium-emphasis">--</span>
|
||||
</template>
|
||||
|
||||
<!-- Rate -->
|
||||
<template #item.rate="{ item }">
|
||||
<span class="font-weight-medium">{{ parseFloat(item.rate).toFixed(2) }}%</span>
|
||||
</template>
|
||||
|
||||
<!-- Type -->
|
||||
<template #item.type="{ item }">
|
||||
<VChip
|
||||
:color="resolveTypeColor(item.type)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="text-capitalize"
|
||||
>
|
||||
{{ item.type }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<!-- Priority -->
|
||||
<template #item.priority="{ item }">
|
||||
<span>{{ item.priority }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Status -->
|
||||
<template #item.is_active="{ item }">
|
||||
<VChip
|
||||
:color="item.is_active ? 'success' : 'error'"
|
||||
size="small"
|
||||
>
|
||||
{{ item.is_active ? 'Active' : 'Inactive' }}
|
||||
</VChip>
|
||||
</template>
|
||||
|
||||
<!-- Actions -->
|
||||
<template #item.actions="{ item }">
|
||||
<VMenu>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<VBtn
|
||||
icon="tabler-dots-vertical"
|
||||
variant="text"
|
||||
size="small"
|
||||
v-bind="menuProps"
|
||||
/>
|
||||
</template>
|
||||
<VList density="compact">
|
||||
<Link :href="`/tax-rates/${item.id}/edit`" class="text-decoration-none">
|
||||
<VListItem prepend-icon="tabler-edit">
|
||||
<VListItemTitle>Edit</VListItemTitle>
|
||||
</VListItem>
|
||||
</Link>
|
||||
<VListItem
|
||||
:prepend-icon="item.is_active ? 'tabler-toggle-right' : 'tabler-toggle-left'"
|
||||
@click="toggleActive(item)"
|
||||
>
|
||||
<VListItemTitle>{{ item.is_active ? 'Deactivate' : 'Activate' }}</VListItemTitle>
|
||||
</VListItem>
|
||||
<VListItem
|
||||
prepend-icon="tabler-trash"
|
||||
class="text-error"
|
||||
@click="deleteTaxRate(item)"
|
||||
>
|
||||
<VListItemTitle>Delete</VListItemTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</template>
|
||||
|
||||
<!-- No data -->
|
||||
<template #no-data>
|
||||
<div class="text-center py-8">
|
||||
<VIcon icon="tabler-receipt-tax" size="48" color="disabled" class="mb-2" />
|
||||
<div class="text-medium-emphasis">
|
||||
No tax rates found.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,8 +2,6 @@
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import { resolveTicketStatusColor, resolveTicketPriorityColor } from '@/utils/resolvers'
|
||||
import type { SupportTicket } from '@/types'
|
||||
|
||||
@@ -207,7 +205,7 @@ function getUserInitial(name: string): string {
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="submitReply">
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="replyForm.body"
|
||||
placeholder="Type your reply to the customer..."
|
||||
rows="5"
|
||||
@@ -219,7 +217,7 @@ function getUserInitial(name: string): string {
|
||||
|
||||
<div class="d-flex align-center justify-space-between flex-wrap ga-3">
|
||||
<div style="min-width: 200px;">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="replyForm.status"
|
||||
:items="statusOptions"
|
||||
label="Update Status"
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import AppStepper from '@/@core/components/AppStepper.vue'
|
||||
import { VExpansionPanels, VExpansionPanel, VExpansionPanelTitle, VExpansionPanelText } from 'vuetify/components'
|
||||
import type { Plan, PaymentMethod } from '@/types'
|
||||
@@ -666,7 +663,7 @@ onUnmounted(() => {
|
||||
</VAlert>
|
||||
|
||||
<div class="d-flex align-center gap-4">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model.number="additionalIPv4"
|
||||
type="number"
|
||||
label="Additional IPv4 Count"
|
||||
@@ -680,7 +677,7 @@ onUnmounted(() => {
|
||||
<template #prepend-inner>
|
||||
<VIcon icon="mdi-counter" size="20" />
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
|
||||
<div v-if="additionalIPv4 > 0" class="ip-cost-badge">
|
||||
<VChip color="primary" size="large" variant="tonal">
|
||||
@@ -774,7 +771,7 @@ onUnmounted(() => {
|
||||
</VAlert>
|
||||
|
||||
<!-- SSH Key Textarea -->
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="sshKey"
|
||||
label="SSH Public Key"
|
||||
placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... user@hostname"
|
||||
@@ -820,7 +817,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Saved Payment Methods (Stripe) -->
|
||||
<div v-if="selectedGateway === 'stripe' && paymentMethods.length > 0" class="mt-4">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="selectedPaymentMethod"
|
||||
label="Select Card"
|
||||
:items="paymentMethods.map(pm => ({
|
||||
@@ -846,7 +843,7 @@ onUnmounted(() => {
|
||||
<VCardTitle class="text-h6">Coupon Code</VCardTitle>
|
||||
<VCardText>
|
||||
<div class="d-flex gap-3">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="couponCode"
|
||||
placeholder="Enter coupon code"
|
||||
:disabled="couponApplied"
|
||||
@@ -857,7 +854,7 @@ onUnmounted(() => {
|
||||
<template #prepend-inner>
|
||||
<VIcon icon="mdi-ticket-percent-outline" size="20" />
|
||||
</template>
|
||||
</AppTextField>
|
||||
</VTextField>
|
||||
<VBtn
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import type { User, UserProfile } from '@/types'
|
||||
|
||||
interface Props {
|
||||
@@ -116,7 +114,7 @@ const resetForm = (): void => {
|
||||
md="6"
|
||||
cols="12"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.first_name"
|
||||
label="First Name"
|
||||
placeholder="John"
|
||||
@@ -128,7 +126,7 @@ const resetForm = (): void => {
|
||||
md="6"
|
||||
cols="12"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.last_name"
|
||||
label="Last Name"
|
||||
placeholder="Doe"
|
||||
@@ -140,7 +138,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
:model-value="user.email"
|
||||
label="Email"
|
||||
type="email"
|
||||
@@ -153,7 +151,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.company"
|
||||
label="Organization"
|
||||
placeholder="EZSCALE"
|
||||
@@ -165,7 +163,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.phone"
|
||||
label="Phone Number"
|
||||
placeholder="+1 (917) 543-9876"
|
||||
@@ -177,7 +175,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.address_line1"
|
||||
label="Address"
|
||||
placeholder="123 Main St"
|
||||
@@ -189,7 +187,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.address_line2"
|
||||
label="Address Line 2"
|
||||
placeholder="Apt 4B"
|
||||
@@ -201,7 +199,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.city"
|
||||
label="City"
|
||||
placeholder="New York"
|
||||
@@ -213,7 +211,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.state"
|
||||
label="State"
|
||||
placeholder="New York"
|
||||
@@ -225,7 +223,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.zip"
|
||||
label="Zip Code"
|
||||
placeholder="10001"
|
||||
@@ -237,7 +235,7 @@ const resetForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.country"
|
||||
label="Country"
|
||||
:items="countries"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import type { UserProfile } from '@/types'
|
||||
|
||||
interface Props {
|
||||
@@ -84,7 +82,7 @@ const resetBillingForm = (): void => {
|
||||
<VForm @submit.prevent="submitBilling">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.billing_address_line1"
|
||||
label="Billing Address"
|
||||
placeholder="123 Main St"
|
||||
@@ -93,7 +91,7 @@ const resetBillingForm = (): void => {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.billing_address_line2"
|
||||
label="Address Line 2"
|
||||
placeholder="Suite 100"
|
||||
@@ -105,7 +103,7 @@ const resetBillingForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.billing_city"
|
||||
label="City"
|
||||
placeholder="New York"
|
||||
@@ -117,7 +115,7 @@ const resetBillingForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.billing_state"
|
||||
label="State"
|
||||
placeholder="New York"
|
||||
@@ -129,7 +127,7 @@ const resetBillingForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.billing_zip"
|
||||
label="Zip Code"
|
||||
placeholder="10001"
|
||||
@@ -141,7 +139,7 @@ const resetBillingForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="billingForm.billing_country"
|
||||
label="Country"
|
||||
:items="countries"
|
||||
@@ -154,7 +152,7 @@ const resetBillingForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.tax_id"
|
||||
label="Tax ID"
|
||||
placeholder="123-45-6789"
|
||||
@@ -166,7 +164,7 @@ const resetBillingForm = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="billingForm.company_vat"
|
||||
label="VAT Number"
|
||||
placeholder="GB123456789"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useForm, usePage, router } from '@inertiajs/vue3'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import type { SharedPageProps } from '@/types'
|
||||
|
||||
interface Props {
|
||||
@@ -117,7 +116,7 @@ const disableTwoFactor = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="passwordForm.current_password"
|
||||
:type="isCurrentPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isCurrentPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@@ -135,7 +134,7 @@ const disableTwoFactor = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="passwordForm.password"
|
||||
:type="isNewPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isNewPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@@ -151,7 +150,7 @@ const disableTwoFactor = (): void => {
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="passwordForm.password_confirmation"
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
@@ -249,7 +248,7 @@ const disableTwoFactor = (): void => {
|
||||
@submit.prevent="confirmTwoFactor"
|
||||
style="max-width: 300px;"
|
||||
>
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="confirmationForm.code"
|
||||
label="Confirmation Code"
|
||||
type="text"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useForm, usePage, router } from '@inertiajs/vue3'
|
||||
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import type { SharedPageProps } from '@/types'
|
||||
|
||||
defineOptions({ layout: AccountLayout })
|
||||
@@ -96,7 +95,7 @@ const disableTwoFactor = (): void => {
|
||||
<div v-if="qrCode" v-html="qrCode" class="mb-4" />
|
||||
|
||||
<VForm @submit.prevent="confirmTwoFactor" style="max-width: 300px;">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="confirmationForm.code"
|
||||
label="Confirmation Code"
|
||||
type="text"
|
||||
|
||||
@@ -3,8 +3,6 @@ import { ref, computed } from 'vue'
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||
import { resolveSubscriptionStatusColor, formatPrice } from '@/utils/resolvers'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import type { Subscription, Plan } from '@/types'
|
||||
|
||||
interface Props {
|
||||
@@ -342,7 +340,7 @@ const isCanceled = computed<boolean>(() => props.subscription.stripe_status ===
|
||||
</div>
|
||||
</VAlert>
|
||||
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="cancelReason"
|
||||
:items="cancelReasons"
|
||||
label="Reason for cancelling (optional)"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
|
||||
defineOptions({ layout: AccountLayout })
|
||||
|
||||
@@ -52,7 +49,7 @@ function submitTicket(): void {
|
||||
<VForm @submit.prevent="submitTicket">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
<VTextField
|
||||
v-model="form.subject"
|
||||
label="Subject"
|
||||
placeholder="Brief description of your issue"
|
||||
@@ -61,7 +58,7 @@ function submitTicket(): void {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.department"
|
||||
label="Department"
|
||||
:items="departmentOptions"
|
||||
@@ -70,7 +67,7 @@ function submitTicket(): void {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12" md="6">
|
||||
<AppSelect
|
||||
<VSelect
|
||||
v-model="form.priority"
|
||||
label="Priority"
|
||||
:items="priorityOptions"
|
||||
@@ -79,7 +76,7 @@ function submitTicket(): void {
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="form.message"
|
||||
label="Message"
|
||||
placeholder="Please describe your issue in detail (minimum 10 characters)..."
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { useForm, Link } from '@inertiajs/vue3'
|
||||
import { ref } from 'vue'
|
||||
import AccountLayout from '@/Layouts/AccountLayout.vue'
|
||||
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||
import { resolveTicketStatusColor, resolveTicketPriorityColor } from '@/utils/resolvers'
|
||||
import type { SupportTicket } from '@/types'
|
||||
|
||||
@@ -179,7 +178,7 @@ function getUserInitial(name: string): string {
|
||||
</VCardTitle>
|
||||
<VCardText>
|
||||
<VForm @submit.prevent="submitReply">
|
||||
<AppTextarea
|
||||
<VTextarea
|
||||
v-model="replyForm.body"
|
||||
placeholder="Type your reply..."
|
||||
rows="5"
|
||||
|
||||
Reference in New Issue
Block a user