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:
Claude Dev
2026-03-14 17:09:15 -04:00
parent 66bb180f8f
commit dd1a5d7ffc
22 changed files with 1236 additions and 167 deletions

View File

@@ -2,8 +2,6 @@
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import { computed } from 'vue' import { computed } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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 { interface PlanOption {
id: number id: number
@@ -95,7 +93,7 @@ function submit(): void {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12" md="8"> <VCol cols="12" md="8">
<AppTextField <VTextField
v-model="form.code" v-model="form.code"
label="Coupon Code" label="Coupon Code"
placeholder="e.g. SAVE20" placeholder="e.g. SAVE20"
@@ -114,7 +112,7 @@ function submit(): void {
</VBtn> </VBtn>
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppSelect <VSelect
v-model="form.type" v-model="form.type"
label="Discount Type" label="Discount Type"
:items="typeOptions" :items="typeOptions"
@@ -123,7 +121,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.value" v-model="form.value"
:label="valueLabel" :label="valueLabel"
type="number" type="number"
@@ -143,7 +141,7 @@ function submit(): void {
<p class="text-body-2 text-medium-emphasis mb-4"> <p class="text-body-2 text-medium-emphasis mb-4">
Optionally restrict this coupon to specific plans. Leave empty to apply to all plans. Optionally restrict this coupon to specific plans. Leave empty to apply to all plans.
</p> </p>
<AppSelect <VSelect
v-model="form.applies_to" v-model="form.applies_to"
label="Applicable Plans" label="Applicable Plans"
:items="planSelectItems" :items="planSelectItems"
@@ -161,7 +159,7 @@ function submit(): void {
<VCol cols="12" lg="4"> <VCol cols="12" lg="4">
<VCard title="Limits & Expiry" class="mb-6"> <VCard title="Limits & Expiry" class="mb-6">
<VCardText> <VCardText>
<AppTextField <VTextField
v-model="form.max_uses" v-model="form.max_uses"
label="Max Uses" label="Max Uses"
type="number" type="number"
@@ -170,7 +168,7 @@ function submit(): void {
:error-messages="form.errors.max_uses" :error-messages="form.errors.max_uses"
class="mb-4" class="mb-4"
/> />
<AppTextField <VTextField
v-model="form.expires_at" v-model="form.expires_at"
label="Expiry Date" label="Expiry Date"
type="datetime-local" type="datetime-local"

View File

@@ -2,8 +2,6 @@
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import { computed } from 'vue' import { computed } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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' import type { Coupon, CouponRedemption, StatusColor } from '@/types'
interface PlanOption { interface PlanOption {
@@ -126,7 +124,7 @@ function submit(): void {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<AppTextField <VTextField
v-model="form.code" v-model="form.code"
label="Coupon Code" label="Coupon Code"
placeholder="e.g. SAVE20" placeholder="e.g. SAVE20"
@@ -134,7 +132,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppSelect <VSelect
v-model="form.type" v-model="form.type"
label="Discount Type" label="Discount Type"
:items="typeOptions" :items="typeOptions"
@@ -143,7 +141,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.value" v-model="form.value"
:label="valueLabel" :label="valueLabel"
type="number" type="number"
@@ -163,7 +161,7 @@ function submit(): void {
<p class="text-body-2 text-medium-emphasis mb-4"> <p class="text-body-2 text-medium-emphasis mb-4">
Optionally restrict this coupon to specific plans. Leave empty to apply to all plans. Optionally restrict this coupon to specific plans. Leave empty to apply to all plans.
</p> </p>
<AppSelect <VSelect
v-model="form.applies_to" v-model="form.applies_to"
label="Applicable Plans" label="Applicable Plans"
:items="planSelectItems" :items="planSelectItems"
@@ -234,7 +232,7 @@ function submit(): void {
<!-- Limits & Expiry --> <!-- Limits & Expiry -->
<VCard title="Limits & Expiry" class="mb-6"> <VCard title="Limits & Expiry" class="mb-6">
<VCardText> <VCardText>
<AppTextField <VTextField
v-model="form.max_uses" v-model="form.max_uses"
label="Max Uses" label="Max Uses"
type="number" type="number"
@@ -243,7 +241,7 @@ function submit(): void {
:error-messages="form.errors.max_uses" :error-messages="form.errors.max_uses"
class="mb-4" class="mb-4"
/> />
<AppTextField <VTextField
v-model="form.expires_at" v-model="form.expires_at"
label="Expiry Date" label="Expiry Date"
type="datetime-local" type="datetime-local"

View File

@@ -1,9 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import AdminLayout from '@/Layouts/AdminLayout.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 { User } from '@/types' import type { User } from '@/types'
defineOptions({ layout: AdminLayout }) defineOptions({ layout: AdminLayout })
@@ -59,7 +56,7 @@ function submit(): void {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.name" v-model="form.name"
label="Name" label="Name"
placeholder="Full name" placeholder="Full name"
@@ -67,7 +64,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.email" v-model="form.email"
label="Email" label="Email"
type="email" type="email"
@@ -76,7 +73,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.phone" v-model="form.phone"
label="Phone" label="Phone"
placeholder="Phone number" placeholder="Phone number"
@@ -84,7 +81,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.company" v-model="form.company"
label="Company" label="Company"
placeholder="Company name" placeholder="Company name"
@@ -92,7 +89,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppSelect <VSelect
v-model="form.status" v-model="form.status"
label="Status" label="Status"
:items="statusOptions" :items="statusOptions"
@@ -106,7 +103,7 @@ function submit(): void {
<!-- Admin Notes --> <!-- Admin Notes -->
<VCard title="Admin Notes" class="mb-6"> <VCard title="Admin Notes" class="mb-6">
<VCardText> <VCardText>
<AppTextarea <VTextarea
v-model="form.admin_notes" v-model="form.admin_notes"
label="Internal Notes" label="Internal Notes"
placeholder="Add internal notes about this customer (only visible to admins)..." placeholder="Add internal notes about this customer (only visible to admins)..."

View 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>

View File

@@ -2,8 +2,6 @@
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import { computed } from 'vue' import { computed } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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' import { formatPrice } from '@/utils/resolvers'
interface CustomerOption { interface CustomerOption {
@@ -135,7 +133,7 @@ function submitAndSend(): void {
> >
<VRow align="center"> <VRow align="center">
<VCol cols="12" md="5"> <VCol cols="12" md="5">
<AppTextField <VTextField
v-model="item.description" v-model="item.description"
placeholder="Item description" placeholder="Item description"
density="compact" density="compact"
@@ -143,7 +141,7 @@ function submitAndSend(): void {
/> />
</VCol> </VCol>
<VCol cols="6" md="2"> <VCol cols="6" md="2">
<AppTextField <VTextField
v-model.number="item.quantity" v-model.number="item.quantity"
type="number" type="number"
min="1" min="1"
@@ -153,7 +151,7 @@ function submitAndSend(): void {
/> />
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol cols="6" md="3">
<AppTextField <VTextField
v-model="item.unit_price" v-model="item.unit_price"
type="number" type="number"
step="0.01" step="0.01"
@@ -209,7 +207,7 @@ function submitAndSend(): void {
<!-- Notes --> <!-- Notes -->
<VCard title="Notes" class="mb-6"> <VCard title="Notes" class="mb-6">
<VCardText> <VCardText>
<AppTextarea <VTextarea
v-model="form.notes" v-model="form.notes"
label="Invoice Notes" label="Invoice Notes"
placeholder="Optional notes to include on the invoice..." placeholder="Optional notes to include on the invoice..."
@@ -225,7 +223,7 @@ function submitAndSend(): void {
<!-- Due Date --> <!-- Due Date -->
<VCard title="Due Date" class="mb-6"> <VCard title="Due Date" class="mb-6">
<VCardText> <VCardText>
<AppTextField <VTextField
v-model="form.due_date" v-model="form.due_date"
label="Due Date" label="Due Date"
type="date" type="date"

View File

@@ -2,8 +2,6 @@
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import { computed } from 'vue' import { computed } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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' import { resolveInvoiceStatusColor, formatPrice } from '@/utils/resolvers'
interface InvoiceUser { interface InvoiceUser {
@@ -151,7 +149,7 @@ function submit(): void {
> >
<VRow align="center"> <VRow align="center">
<VCol cols="12" md="5"> <VCol cols="12" md="5">
<AppTextField <VTextField
v-model="item.description" v-model="item.description"
placeholder="Item description" placeholder="Item description"
density="compact" density="compact"
@@ -159,7 +157,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="6" md="2"> <VCol cols="6" md="2">
<AppTextField <VTextField
v-model.number="item.quantity" v-model.number="item.quantity"
type="number" type="number"
min="1" min="1"
@@ -169,7 +167,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="6" md="3"> <VCol cols="6" md="3">
<AppTextField <VTextField
v-model="item.unit_price" v-model="item.unit_price"
type="number" type="number"
step="0.01" step="0.01"
@@ -225,7 +223,7 @@ function submit(): void {
<!-- Notes --> <!-- Notes -->
<VCard title="Notes" class="mb-6"> <VCard title="Notes" class="mb-6">
<VCardText> <VCardText>
<AppTextarea <VTextarea
v-model="form.notes" v-model="form.notes"
label="Invoice Notes" label="Invoice Notes"
placeholder="Optional notes to include on the invoice..." placeholder="Optional notes to include on the invoice..."
@@ -269,7 +267,7 @@ function submit(): void {
<!-- Due Date --> <!-- Due Date -->
<VCard title="Due Date" class="mb-6"> <VCard title="Due Date" class="mb-6">
<VCardText> <VCardText>
<AppTextField <VTextField
v-model="form.due_date" v-model="form.due_date"
label="Due Date" label="Due Date"
type="date" type="date"

View File

@@ -2,7 +2,6 @@
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.vue' import AdminLayout from '@/Layouts/AdminLayout.vue'
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
import { formatPrice } from '@/utils/resolvers' import { formatPrice } from '@/utils/resolvers'
import type { StatusColor } from '@/types' import type { StatusColor } from '@/types'
@@ -465,7 +464,7 @@ function configurationEntries(): Array<{ key: string; value: string }> {
</VCardTitle> </VCardTitle>
<VCardText> <VCardText>
<AppTextarea <VTextarea
v-model="notesForm.admin_notes" v-model="notesForm.admin_notes"
placeholder="Add internal notes about this order..." placeholder="Add internal notes about this order..."
rows="4" rows="4"

View File

@@ -2,9 +2,6 @@
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import { watch } from 'vue' import { watch } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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 }) defineOptions({ layout: AdminLayout })
@@ -99,7 +96,7 @@ function submit(): void {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.name" v-model="form.name"
label="Plan Name" label="Plan Name"
placeholder="e.g. Basic VPS" placeholder="e.g. Basic VPS"
@@ -107,7 +104,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.slug" v-model="form.slug"
label="Slug" label="Slug"
placeholder="e.g. basic-vps" placeholder="e.g. basic-vps"
@@ -116,7 +113,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<AppTextarea <VTextarea
v-model="form.description" v-model="form.description"
label="Description" label="Description"
placeholder="Brief description of the plan..." placeholder="Brief description of the plan..."
@@ -133,7 +130,7 @@ function submit(): void {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppSelect <VSelect
v-model="form.service_type" v-model="form.service_type"
label="Service Type" label="Service Type"
:items="serviceTypeOptions" :items="serviceTypeOptions"
@@ -142,7 +139,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="form.price" v-model="form.price"
label="Price (USD)" label="Price (USD)"
type="number" type="number"
@@ -153,7 +150,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppSelect <VSelect
v-model="form.billing_cycle" v-model="form.billing_cycle"
label="Billing Cycle" label="Billing Cycle"
:items="billingCycleOptions" :items="billingCycleOptions"
@@ -182,7 +179,7 @@ function submit(): void {
> >
<VRow align="center"> <VRow align="center">
<VCol cols="12" md="5"> <VCol cols="12" md="5">
<AppTextField <VTextField
v-model="feature.key" v-model="feature.key"
placeholder="Feature name (e.g. cpu, ram)" placeholder="Feature name (e.g. cpu, ram)"
density="compact" density="compact"
@@ -190,7 +187,7 @@ function submit(): void {
/> />
</VCol> </VCol>
<VCol cols="12" md="5"> <VCol cols="12" md="5">
<AppTextField <VTextField
v-model="feature.value" v-model="feature.value"
placeholder="Value (e.g. 2 vCPU, 4 GB)" placeholder="Value (e.g. 2 vCPU, 4 GB)"
density="compact" density="compact"
@@ -226,7 +223,7 @@ function submit(): void {
<VCol cols="12" lg="4"> <VCol cols="12" lg="4">
<VCard title="Inventory & Ordering" class="mb-6"> <VCard title="Inventory & Ordering" class="mb-6">
<VCardText> <VCardText>
<AppTextField <VTextField
v-model="form.stock_quantity" v-model="form.stock_quantity"
label="Stock Quantity" label="Stock Quantity"
type="number" type="number"
@@ -235,7 +232,7 @@ function submit(): void {
:error-messages="form.errors.stock_quantity" :error-messages="form.errors.stock_quantity"
class="mb-4" class="mb-4"
/> />
<AppTextField <VTextField
v-model="form.sort_order" v-model="form.sort_order"
label="Sort Order" label="Sort Order"
type="number" type="number"

View File

@@ -2,9 +2,6 @@
import { Link, useForm } from '@inertiajs/vue3' import { Link, useForm } from '@inertiajs/vue3'
import { computed, watch } from 'vue' import { computed, watch } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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' import type { Plan } from '@/types'
defineOptions({ layout: AdminLayout }) defineOptions({ layout: AdminLayout })
@@ -123,7 +120,7 @@ const formattedCreatedAt = computed<string>(() => {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.name" v-model="form.name"
label="Plan Name" label="Plan Name"
placeholder="e.g. Basic VPS" placeholder="e.g. Basic VPS"
@@ -131,7 +128,7 @@ const formattedCreatedAt = computed<string>(() => {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="form.slug" v-model="form.slug"
label="Slug" label="Slug"
placeholder="e.g. basic-vps" placeholder="e.g. basic-vps"
@@ -140,7 +137,7 @@ const formattedCreatedAt = computed<string>(() => {
/> />
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<AppTextarea <VTextarea
v-model="form.description" v-model="form.description"
label="Description" label="Description"
placeholder="Brief description of the plan..." placeholder="Brief description of the plan..."
@@ -157,7 +154,7 @@ const formattedCreatedAt = computed<string>(() => {
<VCardText> <VCardText>
<VRow> <VRow>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppSelect <VSelect
v-model="form.service_type" v-model="form.service_type"
label="Service Type" label="Service Type"
:items="serviceTypeOptions" :items="serviceTypeOptions"
@@ -166,7 +163,7 @@ const formattedCreatedAt = computed<string>(() => {
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="form.price" v-model="form.price"
label="Price (USD)" label="Price (USD)"
type="number" type="number"
@@ -177,7 +174,7 @@ const formattedCreatedAt = computed<string>(() => {
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppSelect <VSelect
v-model="form.billing_cycle" v-model="form.billing_cycle"
label="Billing Cycle" label="Billing Cycle"
:items="billingCycleOptions" :items="billingCycleOptions"
@@ -206,7 +203,7 @@ const formattedCreatedAt = computed<string>(() => {
> >
<VRow align="center"> <VRow align="center">
<VCol cols="12" md="5"> <VCol cols="12" md="5">
<AppTextField <VTextField
v-model="feature.key" v-model="feature.key"
placeholder="Feature name (e.g. cpu, ram)" placeholder="Feature name (e.g. cpu, ram)"
density="compact" density="compact"
@@ -214,7 +211,7 @@ const formattedCreatedAt = computed<string>(() => {
/> />
</VCol> </VCol>
<VCol cols="12" md="5"> <VCol cols="12" md="5">
<AppTextField <VTextField
v-model="feature.value" v-model="feature.value"
placeholder="Value (e.g. 2 vCPU, 4 GB)" placeholder="Value (e.g. 2 vCPU, 4 GB)"
density="compact" density="compact"
@@ -275,7 +272,7 @@ const formattedCreatedAt = computed<string>(() => {
<!-- Inventory & Ordering --> <!-- Inventory & Ordering -->
<VCard title="Inventory & Ordering" class="mb-6"> <VCard title="Inventory & Ordering" class="mb-6">
<VCardText> <VCardText>
<AppTextField <VTextField
v-model="form.stock_quantity" v-model="form.stock_quantity"
label="Stock Quantity" label="Stock Quantity"
type="number" type="number"
@@ -284,7 +281,7 @@ const formattedCreatedAt = computed<string>(() => {
:error-messages="form.errors.stock_quantity" :error-messages="form.errors.stock_quantity"
class="mb-4" class="mb-4"
/> />
<AppTextField <VTextField
v-model="form.sort_order" v-model="form.sort_order"
label="Sort Order" label="Sort Order"
type="number" type="number"

View File

@@ -2,8 +2,6 @@
import { useForm } from '@inertiajs/vue3' import { useForm } from '@inertiajs/vue3'
import { ref } from 'vue' import { ref } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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 { interface SettingsGroup {
[key: string]: string | boolean | null [key: string]: string | boolean | null
@@ -284,7 +282,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
<form @submit.prevent="submitGeneral"> <form @submit.prevent="submitGeneral">
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="generalForm.company_name" v-model="generalForm.company_name"
label="Company Name" label="Company Name"
placeholder="EZSCALE" placeholder="EZSCALE"
@@ -293,7 +291,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="generalForm.company_email" v-model="generalForm.company_email"
label="Company Email" label="Company Email"
type="email" type="email"
@@ -303,7 +301,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="generalForm.support_url" v-model="generalForm.support_url"
label="Support URL" label="Support URL"
placeholder="https://support.ezscale.cloud" placeholder="https://support.ezscale.cloud"
@@ -312,7 +310,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="generalForm.status_page_url" v-model="generalForm.status_page_url"
label="Status Page URL" label="Status Page URL"
placeholder="https://status.ezscale.cloud" placeholder="https://status.ezscale.cloud"
@@ -366,7 +364,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VAlert> </VAlert>
<VRow class="mb-4"> <VRow class="mb-4">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="apiForm.virtfusion_api_url" v-model="apiForm.virtfusion_api_url"
label="API URL" label="API URL"
placeholder="https://vps.ezscale.cloud/api/v1" placeholder="https://vps.ezscale.cloud/api/v1"
@@ -374,7 +372,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="apiForm.virtfusion_api_token" v-model="apiForm.virtfusion_api_token"
label="API Token" label="API Token"
:type="showVirtfusionToken ? 'text' : 'password'" :type="showVirtfusionToken ? 'text' : 'password'"
@@ -388,7 +386,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
@click="showVirtfusionToken = !showVirtfusionToken" @click="showVirtfusionToken = !showVirtfusionToken"
/> />
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
</VRow> </VRow>
@@ -422,7 +420,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VAlert> </VAlert>
<VRow class="mb-4"> <VRow class="mb-4">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="apiForm.pterodactyl_api_url" v-model="apiForm.pterodactyl_api_url"
label="Panel URL" label="Panel URL"
placeholder="https://game.ezscale.cloud" placeholder="https://game.ezscale.cloud"
@@ -430,7 +428,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="apiForm.pterodactyl_api_token" v-model="apiForm.pterodactyl_api_token"
label="API Key" label="API Key"
:type="showPterodactylToken ? 'text' : 'password'" :type="showPterodactylToken ? 'text' : 'password'"
@@ -444,7 +442,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
@click="showPterodactylToken = !showPterodactylToken" @click="showPterodactylToken = !showPterodactylToken"
/> />
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
</VRow> </VRow>
@@ -478,7 +476,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VAlert> </VAlert>
<VRow class="mb-4"> <VRow class="mb-4">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="apiForm.synergycp_api_url" v-model="apiForm.synergycp_api_url"
label="API URL" label="API URL"
placeholder="https://dedicated.ezscale.cloud/api" placeholder="https://dedicated.ezscale.cloud/api"
@@ -486,7 +484,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="apiForm.synergycp_api_token" v-model="apiForm.synergycp_api_token"
label="API Token" label="API Token"
:type="showSynergycpToken ? 'text' : 'password'" :type="showSynergycpToken ? 'text' : 'password'"
@@ -500,7 +498,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
@click="showSynergycpToken = !showSynergycpToken" @click="showSynergycpToken = !showSynergycpToken"
/> />
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
</VRow> </VRow>
@@ -534,7 +532,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VAlert> </VAlert>
<VRow class="mb-4"> <VRow class="mb-4">
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="apiForm.enhance_api_url" v-model="apiForm.enhance_api_url"
label="API URL" label="API URL"
placeholder="https://hosting.ezscale.cloud/api" placeholder="https://hosting.ezscale.cloud/api"
@@ -542,7 +540,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
/> />
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="apiForm.enhance_api_token" v-model="apiForm.enhance_api_token"
label="API Token" label="API Token"
:type="showEnhanceToken ? 'text' : 'password'" :type="showEnhanceToken ? 'text' : 'password'"
@@ -556,10 +554,10 @@ async function testDiscordWebhook(channel: string): Promise<void> {
@click="showEnhanceToken = !showEnhanceToken" @click="showEnhanceToken = !showEnhanceToken"
/> />
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="apiForm.enhance_organization_id" v-model="apiForm.enhance_organization_id"
label="Organization ID" label="Organization ID"
:type="showEnhanceOrgId ? 'text' : 'password'" :type="showEnhanceOrgId ? 'text' : 'password'"
@@ -573,7 +571,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
@click="showEnhanceOrgId = !showEnhanceOrgId" @click="showEnhanceOrgId = !showEnhanceOrgId"
/> />
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
</VRow> </VRow>
@@ -589,7 +587,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</div> </div>
<VRow class="mb-4"> <VRow class="mb-4">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
:model-value="(props.settings.api.stripe_publishable_key as string) || 'Not configured'" :model-value="(props.settings.api.stripe_publishable_key as string) || 'Not configured'"
label="Publishable Key" label="Publishable Key"
readonly readonly
@@ -597,7 +595,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
:model-value="(props.settings.api.stripe_secret_key as string) || 'Not configured'" :model-value="(props.settings.api.stripe_secret_key as string) || 'Not configured'"
label="Secret Key" label="Secret Key"
readonly readonly
@@ -618,7 +616,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</div> </div>
<VRow class="mb-6"> <VRow class="mb-6">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
:model-value="(props.settings.api.paypal_client_id as string) || 'Not configured'" :model-value="(props.settings.api.paypal_client_id as string) || 'Not configured'"
label="Client ID" label="Client ID"
readonly readonly
@@ -626,7 +624,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
/> />
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
:model-value="(props.settings.api.paypal_client_secret as string) || 'Not configured'" :model-value="(props.settings.api.paypal_client_secret as string) || 'Not configured'"
label="Client Secret" label="Client Secret"
readonly readonly
@@ -695,7 +693,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
> >
{{ discordTestResults.payment.message }} {{ discordTestResults.payment.message }}
</VAlert> </VAlert>
<AppTextField <VTextField
v-model="discordForm.discord_payment_webhook_url" v-model="discordForm.discord_payment_webhook_url"
label="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/...'" :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> <template #prepend-inner>
<VIcon icon="tabler-brand-discord" /> <VIcon icon="tabler-brand-discord" />
</template> </template>
</AppTextField> </VTextField>
</VCardText> </VCardText>
</VCard> </VCard>
@@ -748,7 +746,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
> >
{{ discordTestResults.provisioning.message }} {{ discordTestResults.provisioning.message }}
</VAlert> </VAlert>
<AppTextField <VTextField
v-model="discordForm.discord_provisioning_webhook_url" v-model="discordForm.discord_provisioning_webhook_url"
label="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/...'" :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> <template #prepend-inner>
<VIcon icon="tabler-brand-discord" /> <VIcon icon="tabler-brand-discord" />
</template> </template>
</AppTextField> </VTextField>
</VCardText> </VCardText>
</VCard> </VCard>
@@ -801,7 +799,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
> >
{{ discordTestResults.support.message }} {{ discordTestResults.support.message }}
</VAlert> </VAlert>
<AppTextField <VTextField
v-model="discordForm.discord_support_webhook_url" v-model="discordForm.discord_support_webhook_url"
label="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/...'" :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> <template #prepend-inner>
<VIcon icon="tabler-brand-discord" /> <VIcon icon="tabler-brand-discord" />
</template> </template>
</AppTextField> </VTextField>
</VCardText> </VCardText>
</VCard> </VCard>
@@ -854,7 +852,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
> >
{{ discordTestResults.system.message }} {{ discordTestResults.system.message }}
</VAlert> </VAlert>
<AppTextField <VTextField
v-model="discordForm.discord_system_webhook_url" v-model="discordForm.discord_system_webhook_url"
label="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/...'" :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> <template #prepend-inner>
<VIcon icon="tabler-brand-discord" /> <VIcon icon="tabler-brand-discord" />
</template> </template>
</AppTextField> </VTextField>
</VCardText> </VCardText>
</VCard> </VCard>
@@ -890,7 +888,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
<VRow class="mb-6"> <VRow class="mb-6">
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppSelect <VSelect
v-model="billingForm.default_currency" v-model="billingForm.default_currency"
label="Default Currency" label="Default Currency"
:items="currencyOptions" :items="currencyOptions"
@@ -899,7 +897,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="billingForm.grace_period_days" v-model="billingForm.grace_period_days"
label="Grace Period (days)" label="Grace Period (days)"
type="number" type="number"
@@ -920,11 +918,11 @@ async function testDiscordWebhook(channel: string): Promise<void> {
Days after invoice due date before suspension warning is sent Days after invoice due date before suspension warning is sent
</VTooltip> </VTooltip>
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="billingForm.suspension_warning_days" v-model="billingForm.suspension_warning_days"
label="Suspension Warning (days)" label="Suspension Warning (days)"
type="number" type="number"
@@ -945,11 +943,11 @@ async function testDiscordWebhook(channel: string): Promise<void> {
Days after warning before service is suspended Days after warning before service is suspended
</VTooltip> </VTooltip>
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="billingForm.auto_terminate_days" v-model="billingForm.auto_terminate_days"
label="Auto-Terminate (days)" label="Auto-Terminate (days)"
type="number" type="number"
@@ -970,7 +968,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
Days after suspension before service is automatically terminated Days after suspension before service is automatically terminated
</VTooltip> </VTooltip>
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
@@ -994,7 +992,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
<VRow class="mb-4"> <VRow class="mb-4">
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="billingForm.bandwidth_overage_rate" v-model="billingForm.bandwidth_overage_rate"
label="Overage Rate ($/GB)" label="Overage Rate ($/GB)"
type="number" type="number"
@@ -1015,11 +1013,11 @@ async function testDiscordWebhook(channel: string): Promise<void> {
Price charged per GB over the plan's included bandwidth Price charged per GB over the plan's included bandwidth
</VTooltip> </VTooltip>
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
<AppTextField <VTextField
v-model="billingForm.bandwidth_grace_period_days" v-model="billingForm.bandwidth_grace_period_days"
label="Grace Period Before Billing (days)" label="Grace Period Before Billing (days)"
type="number" type="number"
@@ -1040,7 +1038,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
Days after overage detected before billing begins Days after overage detected before billing begins
</VTooltip> </VTooltip>
</template> </template>
</AppTextField> </VTextField>
</VCol> </VCol>
<VCol cols="12" md="4"> <VCol cols="12" md="4">
@@ -1194,7 +1192,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
<form @submit.prevent="submitNotifications"> <form @submit.prevent="submitNotifications">
<VRow> <VRow>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="notificationsForm.email_from_address" v-model="notificationsForm.email_from_address"
label="Email From Address" label="Email From Address"
type="email" type="email"
@@ -1204,7 +1202,7 @@ async function testDiscordWebhook(channel: string): Promise<void> {
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppTextField <VTextField
v-model="notificationsForm.email_from_name" v-model="notificationsForm.email_from_name"
label="Email From Name" label="Email From Name"
placeholder="EZSCALE" placeholder="EZSCALE"

View 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>

View 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>

View 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>

View File

@@ -2,8 +2,6 @@
import { useForm, Link } from '@inertiajs/vue3' import { useForm, Link } from '@inertiajs/vue3'
import { ref } from 'vue' import { ref } from 'vue'
import AdminLayout from '@/Layouts/AdminLayout.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 { resolveTicketStatusColor, resolveTicketPriorityColor } from '@/utils/resolvers'
import type { SupportTicket } from '@/types' import type { SupportTicket } from '@/types'
@@ -207,7 +205,7 @@ function getUserInitial(name: string): string {
</VCardTitle> </VCardTitle>
<VCardText> <VCardText>
<VForm @submit.prevent="submitReply"> <VForm @submit.prevent="submitReply">
<AppTextarea <VTextarea
v-model="replyForm.body" v-model="replyForm.body"
placeholder="Type your reply to the customer..." placeholder="Type your reply to the customer..."
rows="5" 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 class="d-flex align-center justify-space-between flex-wrap ga-3">
<div style="min-width: 200px;"> <div style="min-width: 200px;">
<AppSelect <VSelect
v-model="replyForm.status" v-model="replyForm.status"
:items="statusOptions" :items="statusOptions"
label="Update Status" label="Update Status"

View File

@@ -2,9 +2,6 @@
import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useForm, Link } from '@inertiajs/vue3' import { useForm, Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue' 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 AppStepper from '@/@core/components/AppStepper.vue'
import { VExpansionPanels, VExpansionPanel, VExpansionPanelTitle, VExpansionPanelText } from 'vuetify/components' import { VExpansionPanels, VExpansionPanel, VExpansionPanelTitle, VExpansionPanelText } from 'vuetify/components'
import type { Plan, PaymentMethod } from '@/types' import type { Plan, PaymentMethod } from '@/types'
@@ -666,7 +663,7 @@ onUnmounted(() => {
</VAlert> </VAlert>
<div class="d-flex align-center gap-4"> <div class="d-flex align-center gap-4">
<AppTextField <VTextField
v-model.number="additionalIPv4" v-model.number="additionalIPv4"
type="number" type="number"
label="Additional IPv4 Count" label="Additional IPv4 Count"
@@ -680,7 +677,7 @@ onUnmounted(() => {
<template #prepend-inner> <template #prepend-inner>
<VIcon icon="mdi-counter" size="20" /> <VIcon icon="mdi-counter" size="20" />
</template> </template>
</AppTextField> </VTextField>
<div v-if="additionalIPv4 > 0" class="ip-cost-badge"> <div v-if="additionalIPv4 > 0" class="ip-cost-badge">
<VChip color="primary" size="large" variant="tonal"> <VChip color="primary" size="large" variant="tonal">
@@ -774,7 +771,7 @@ onUnmounted(() => {
</VAlert> </VAlert>
<!-- SSH Key Textarea --> <!-- SSH Key Textarea -->
<AppTextarea <VTextarea
v-model="sshKey" v-model="sshKey"
label="SSH Public Key" label="SSH Public Key"
placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... user@hostname" placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... user@hostname"
@@ -820,7 +817,7 @@ onUnmounted(() => {
<!-- Saved Payment Methods (Stripe) --> <!-- Saved Payment Methods (Stripe) -->
<div v-if="selectedGateway === 'stripe' && paymentMethods.length > 0" class="mt-4"> <div v-if="selectedGateway === 'stripe' && paymentMethods.length > 0" class="mt-4">
<AppSelect <VSelect
v-model="selectedPaymentMethod" v-model="selectedPaymentMethod"
label="Select Card" label="Select Card"
:items="paymentMethods.map(pm => ({ :items="paymentMethods.map(pm => ({
@@ -846,7 +843,7 @@ onUnmounted(() => {
<VCardTitle class="text-h6">Coupon Code</VCardTitle> <VCardTitle class="text-h6">Coupon Code</VCardTitle>
<VCardText> <VCardText>
<div class="d-flex gap-3"> <div class="d-flex gap-3">
<AppTextField <VTextField
v-model="couponCode" v-model="couponCode"
placeholder="Enter coupon code" placeholder="Enter coupon code"
:disabled="couponApplied" :disabled="couponApplied"
@@ -857,7 +854,7 @@ onUnmounted(() => {
<template #prepend-inner> <template #prepend-inner>
<VIcon icon="mdi-ticket-percent-outline" size="20" /> <VIcon icon="mdi-ticket-percent-outline" size="20" />
</template> </template>
</AppTextField> </VTextField>
<VBtn <VBtn
variant="outlined" variant="outlined"
color="primary" color="primary"

View File

@@ -1,8 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useForm } 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 { User, UserProfile } from '@/types' import type { User, UserProfile } from '@/types'
interface Props { interface Props {
@@ -116,7 +114,7 @@ const resetForm = (): void => {
md="6" md="6"
cols="12" cols="12"
> >
<AppTextField <VTextField
v-model="form.first_name" v-model="form.first_name"
label="First Name" label="First Name"
placeholder="John" placeholder="John"
@@ -128,7 +126,7 @@ const resetForm = (): void => {
md="6" md="6"
cols="12" cols="12"
> >
<AppTextField <VTextField
v-model="form.last_name" v-model="form.last_name"
label="Last Name" label="Last Name"
placeholder="Doe" placeholder="Doe"
@@ -140,7 +138,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
:model-value="user.email" :model-value="user.email"
label="Email" label="Email"
type="email" type="email"
@@ -153,7 +151,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="form.company" v-model="form.company"
label="Organization" label="Organization"
placeholder="EZSCALE" placeholder="EZSCALE"
@@ -165,7 +163,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="form.phone" v-model="form.phone"
label="Phone Number" label="Phone Number"
placeholder="+1 (917) 543-9876" placeholder="+1 (917) 543-9876"
@@ -177,7 +175,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="form.address_line1" v-model="form.address_line1"
label="Address" label="Address"
placeholder="123 Main St" placeholder="123 Main St"
@@ -189,7 +187,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="form.address_line2" v-model="form.address_line2"
label="Address Line 2" label="Address Line 2"
placeholder="Apt 4B" placeholder="Apt 4B"
@@ -201,7 +199,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="form.city" v-model="form.city"
label="City" label="City"
placeholder="New York" placeholder="New York"
@@ -213,7 +211,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="form.state" v-model="form.state"
label="State" label="State"
placeholder="New York" placeholder="New York"
@@ -225,7 +223,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="form.zip" v-model="form.zip"
label="Zip Code" label="Zip Code"
placeholder="10001" placeholder="10001"
@@ -237,7 +235,7 @@ const resetForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppSelect <VSelect
v-model="form.country" v-model="form.country"
label="Country" label="Country"
:items="countries" :items="countries"

View File

@@ -1,8 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Link } from '@inertiajs/vue3' import { Link } from '@inertiajs/vue3'
import { useForm } 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' import type { UserProfile } from '@/types'
interface Props { interface Props {
@@ -84,7 +82,7 @@ const resetBillingForm = (): void => {
<VForm @submit.prevent="submitBilling"> <VForm @submit.prevent="submitBilling">
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<AppTextField <VTextField
v-model="billingForm.billing_address_line1" v-model="billingForm.billing_address_line1"
label="Billing Address" label="Billing Address"
placeholder="123 Main St" placeholder="123 Main St"
@@ -93,7 +91,7 @@ const resetBillingForm = (): void => {
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<AppTextField <VTextField
v-model="billingForm.billing_address_line2" v-model="billingForm.billing_address_line2"
label="Address Line 2" label="Address Line 2"
placeholder="Suite 100" placeholder="Suite 100"
@@ -105,7 +103,7 @@ const resetBillingForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="billingForm.billing_city" v-model="billingForm.billing_city"
label="City" label="City"
placeholder="New York" placeholder="New York"
@@ -117,7 +115,7 @@ const resetBillingForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="billingForm.billing_state" v-model="billingForm.billing_state"
label="State" label="State"
placeholder="New York" placeholder="New York"
@@ -129,7 +127,7 @@ const resetBillingForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="billingForm.billing_zip" v-model="billingForm.billing_zip"
label="Zip Code" label="Zip Code"
placeholder="10001" placeholder="10001"
@@ -141,7 +139,7 @@ const resetBillingForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppSelect <VSelect
v-model="billingForm.billing_country" v-model="billingForm.billing_country"
label="Country" label="Country"
:items="countries" :items="countries"
@@ -154,7 +152,7 @@ const resetBillingForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="billingForm.tax_id" v-model="billingForm.tax_id"
label="Tax ID" label="Tax ID"
placeholder="123-45-6789" placeholder="123-45-6789"
@@ -166,7 +164,7 @@ const resetBillingForm = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="billingForm.company_vat" v-model="billingForm.company_vat"
label="VAT Number" label="VAT Number"
placeholder="GB123456789" placeholder="GB123456789"

View File

@@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useForm, usePage, router } from '@inertiajs/vue3' import { useForm, usePage, router } from '@inertiajs/vue3'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
import type { SharedPageProps } from '@/types' import type { SharedPageProps } from '@/types'
interface Props { interface Props {
@@ -117,7 +116,7 @@ const disableTwoFactor = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="passwordForm.current_password" v-model="passwordForm.current_password"
:type="isCurrentPasswordVisible ? 'text' : 'password'" :type="isCurrentPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isCurrentPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'" :append-inner-icon="isCurrentPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@@ -135,7 +134,7 @@ const disableTwoFactor = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="passwordForm.password" v-model="passwordForm.password"
:type="isNewPasswordVisible ? 'text' : 'password'" :type="isNewPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isNewPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'" :append-inner-icon="isNewPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@@ -151,7 +150,7 @@ const disableTwoFactor = (): void => {
cols="12" cols="12"
md="6" md="6"
> >
<AppTextField <VTextField
v-model="passwordForm.password_confirmation" v-model="passwordForm.password_confirmation"
:type="isConfirmPasswordVisible ? 'text' : 'password'" :type="isConfirmPasswordVisible ? 'text' : 'password'"
:append-inner-icon="isConfirmPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'" :append-inner-icon="isConfirmPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@@ -249,7 +248,7 @@ const disableTwoFactor = (): void => {
@submit.prevent="confirmTwoFactor" @submit.prevent="confirmTwoFactor"
style="max-width: 300px;" style="max-width: 300px;"
> >
<AppTextField <VTextField
v-model="confirmationForm.code" v-model="confirmationForm.code"
label="Confirmation Code" label="Confirmation Code"
type="text" type="text"

View File

@@ -2,7 +2,6 @@
import { ref } from 'vue' import { ref } from 'vue'
import { useForm, usePage, router } from '@inertiajs/vue3' import { useForm, usePage, router } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue' import AccountLayout from '@/Layouts/AccountLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
import type { SharedPageProps } from '@/types' import type { SharedPageProps } from '@/types'
defineOptions({ layout: AccountLayout }) defineOptions({ layout: AccountLayout })
@@ -96,7 +95,7 @@ const disableTwoFactor = (): void => {
<div v-if="qrCode" v-html="qrCode" class="mb-4" /> <div v-if="qrCode" v-html="qrCode" class="mb-4" />
<VForm @submit.prevent="confirmTwoFactor" style="max-width: 300px;"> <VForm @submit.prevent="confirmTwoFactor" style="max-width: 300px;">
<AppTextField <VTextField
v-model="confirmationForm.code" v-model="confirmationForm.code"
label="Confirmation Code" label="Confirmation Code"
type="text" type="text"

View File

@@ -3,8 +3,6 @@ import { ref, computed } from 'vue'
import { useForm, Link } from '@inertiajs/vue3' import { useForm, Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue' import AccountLayout from '@/Layouts/AccountLayout.vue'
import { resolveSubscriptionStatusColor, formatPrice } from '@/utils/resolvers' 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' import type { Subscription, Plan } from '@/types'
interface Props { interface Props {
@@ -342,7 +340,7 @@ const isCanceled = computed<boolean>(() => props.subscription.stripe_status ===
</div> </div>
</VAlert> </VAlert>
<AppSelect <VSelect
v-model="cancelReason" v-model="cancelReason"
:items="cancelReasons" :items="cancelReasons"
label="Reason for cancelling (optional)" label="Reason for cancelling (optional)"

View File

@@ -1,9 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useForm, Link } from '@inertiajs/vue3' import { useForm, Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue' 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 }) defineOptions({ layout: AccountLayout })
@@ -52,7 +49,7 @@ function submitTicket(): void {
<VForm @submit.prevent="submitTicket"> <VForm @submit.prevent="submitTicket">
<VRow> <VRow>
<VCol cols="12"> <VCol cols="12">
<AppTextField <VTextField
v-model="form.subject" v-model="form.subject"
label="Subject" label="Subject"
placeholder="Brief description of your issue" placeholder="Brief description of your issue"
@@ -61,7 +58,7 @@ function submitTicket(): void {
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppSelect <VSelect
v-model="form.department" v-model="form.department"
label="Department" label="Department"
:items="departmentOptions" :items="departmentOptions"
@@ -70,7 +67,7 @@ function submitTicket(): void {
</VCol> </VCol>
<VCol cols="12" md="6"> <VCol cols="12" md="6">
<AppSelect <VSelect
v-model="form.priority" v-model="form.priority"
label="Priority" label="Priority"
:items="priorityOptions" :items="priorityOptions"
@@ -79,7 +76,7 @@ function submitTicket(): void {
</VCol> </VCol>
<VCol cols="12"> <VCol cols="12">
<AppTextarea <VTextarea
v-model="form.message" v-model="form.message"
label="Message" label="Message"
placeholder="Please describe your issue in detail (minimum 10 characters)..." placeholder="Please describe your issue in detail (minimum 10 characters)..."

View File

@@ -2,7 +2,6 @@
import { useForm, Link } from '@inertiajs/vue3' import { useForm, Link } from '@inertiajs/vue3'
import { ref } from 'vue' import { ref } from 'vue'
import AccountLayout from '@/Layouts/AccountLayout.vue' import AccountLayout from '@/Layouts/AccountLayout.vue'
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
import { resolveTicketStatusColor, resolveTicketPriorityColor } from '@/utils/resolvers' import { resolveTicketStatusColor, resolveTicketPriorityColor } from '@/utils/resolvers'
import type { SupportTicket } from '@/types' import type { SupportTicket } from '@/types'
@@ -179,7 +178,7 @@ function getUserInitial(name: string): string {
</VCardTitle> </VCardTitle>
<VCardText> <VCardText>
<VForm @submit.prevent="submitReply"> <VForm @submit.prevent="submitReply">
<AppTextarea <VTextarea
v-model="replyForm.body" v-model="replyForm.body"
placeholder="Type your reply..." placeholder="Type your reply..."
rows="5" rows="5"