From dd1a5d7ffca96c8154204cd949ac2d30524ff70b4bdd042d6112ffe2e38b2ff7 Mon Sep 17 00:00:00 2001
From: Claude Dev
Date: Sat, 14 Mar 2026 17:09:15 -0400
Subject: [PATCH] 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)
---
.../ts/Pages/Admin/Coupons/Create.vue | 14 +-
.../resources/ts/Pages/Admin/Coupons/Edit.vue | 14 +-
.../ts/Pages/Admin/Customers/Edit.vue | 15 +-
.../ts/Pages/Admin/EmailTemplates/Edit.vue | 399 ++++++++++++++++++
.../ts/Pages/Admin/Invoices/Create.vue | 12 +-
.../ts/Pages/Admin/Invoices/Edit.vue | 12 +-
.../resources/ts/Pages/Admin/Orders/Show.vue | 3 +-
.../resources/ts/Pages/Admin/Plans/Create.vue | 23 +-
.../resources/ts/Pages/Admin/Plans/Edit.vue | 23 +-
.../ts/Pages/Admin/Settings/Index.vue | 88 ++--
.../ts/Pages/Admin/TaxRates/Create.vue | 196 +++++++++
.../ts/Pages/Admin/TaxRates/Edit.vue | 232 ++++++++++
.../ts/Pages/Admin/TaxRates/Index.vue | 279 ++++++++++++
.../resources/ts/Pages/Admin/Tickets/Show.vue | 6 +-
website/resources/ts/Pages/Checkout/Show.vue | 15 +-
.../resources/ts/Pages/Profile/AccountTab.vue | 24 +-
.../resources/ts/Pages/Profile/BillingTab.vue | 18 +-
.../ts/Pages/Profile/SecurityTab.vue | 9 +-
.../ts/Pages/Profile/TwoFactorSetup.vue | 3 +-
.../resources/ts/Pages/Subscriptions/Show.vue | 4 +-
website/resources/ts/Pages/Tickets/Create.vue | 11 +-
website/resources/ts/Pages/Tickets/Show.vue | 3 +-
22 files changed, 1236 insertions(+), 167 deletions(-)
create mode 100644 website/resources/ts/Pages/Admin/EmailTemplates/Edit.vue
create mode 100644 website/resources/ts/Pages/Admin/TaxRates/Create.vue
create mode 100644 website/resources/ts/Pages/Admin/TaxRates/Edit.vue
create mode 100644 website/resources/ts/Pages/Admin/TaxRates/Index.vue
diff --git a/website/resources/ts/Pages/Admin/Coupons/Create.vue b/website/resources/ts/Pages/Admin/Coupons/Create.vue
index efe0252..8da9929 100644
--- a/website/resources/ts/Pages/Admin/Coupons/Create.vue
+++ b/website/resources/ts/Pages/Admin/Coupons/Create.vue
@@ -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 {
-
-
-
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.
-
-
-
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 {
-
-
-
-
-
-
+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()
+
+const form = useForm({
+ subject: props.template.subject,
+ body: props.template.body,
+ is_active: props.template.is_active,
+})
+
+const showPreviewDialog = ref(false)
+const previewLoading = ref(false)
+const previewSubject = ref('')
+const previewBody = ref('')
+const showResetDialog = ref(false)
+
+const bodyTextareaRef = ref | 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 {
+ 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(() => {
+ 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',
+ })
+})
+
+
+
+
+
+
+
+
+
+
+
+ Edit Email Template
+
+
+ Editing "{{ template.name }}"
+
+
+
+
+
+
+
+
+
+
+ Email Preview
+
+
+
+
+
+
+
+ Loading preview...
+
+
+
+
+
+
Subject:
+
+ {{ previewSubject }}
+
+
+
+
+
Body:
+
+ {{ previewBody }}
+
+
+
+
+
+
+
+
+
+ Reset to Default
+
+ Are you sure you want to reset this template to its default content? This will overwrite your current subject and body text.
+
+
+
+
+ Cancel
+
+
+ Reset
+
+
+
+
+
+
diff --git a/website/resources/ts/Pages/Admin/Invoices/Create.vue b/website/resources/ts/Pages/Admin/Invoices/Create.vue
index 5ee9c47..4873552 100644
--- a/website/resources/ts/Pages/Admin/Invoices/Create.vue
+++ b/website/resources/ts/Pages/Admin/Invoices/Create.vue
@@ -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 {
>
-
-
-
-
-
-
-
-
-
- {
-
-
-
-
-
-
-
-
-
-
- (() => {
- (() => {
/>
- (() => {
/>
- (() => {
- (() => {
/>
- (() => {
/>
- (() => {
>
- (() => {
/>
- (() => {
- (() => {
:error-messages="form.errors.stock_quantity"
class="mb-4"
/>
- {
@@ -748,7 +746,7 @@ async function testDiscordWebhook(channel: string): Promise {
>
{{ discordTestResults.provisioning.message }}
- {
-
+
@@ -801,7 +799,7 @@ async function testDiscordWebhook(channel: string): Promise {
>
{{ discordTestResults.support.message }}
- {
-
+
@@ -854,7 +852,7 @@ async function testDiscordWebhook(channel: string): Promise {
>
{{ discordTestResults.system.message }}
- {
-
+
@@ -890,7 +888,7 @@ async function testDiscordWebhook(channel: string): Promise {
- {
- {
Days after invoice due date before suspension warning is sent
-
+
- {
Days after warning before service is suspended
-
+
- {
Days after suspension before service is automatically terminated
-
+
@@ -994,7 +992,7 @@ async function testDiscordWebhook(channel: string): Promise {
- {
Price charged per GB over the plan's included bandwidth
-
+
- {
Days after overage detected before billing begins
-
+
@@ -1194,7 +1192,7 @@ async function testDiscordWebhook(channel: string): Promise {