# Detail Page Patterns

Complete guide to building create, edit, and view pages using AwPageSingle with forms and validation.

## When to Use AwPageSingle

Use `AwPageSingle` for:
- **Create pages** - Forms for creating new records
- **Edit pages** - Forms for updating existing records
- **Detail views** - Display single item details
- **Single-focus workflows** - Wizards, settings, profiles

**Key characteristics:**
- Can hide aside menu with `hide-menu` prop
- Full-width content area
- Action button in header (save, submit, etc.)
- Best for focused tasks and forms

**⚠️ Essential Requirements:**

1. **Always use `layout: 'empty'`**

Every page using `AwPageSingle` must include `layout: 'empty'` in the component export:

```javascript
export default {
    layout: 'empty',  // Required for AwPageSingle pages
    // ... rest of component
}
```

2. **Always use vue-mc models for data fetching and creation**

Items fetching, creation, and updates MUST use vue-mc models (`BaseModel` from `@awes-io/vue-mc`). Do not use custom `fetch()` API calls or manual HTTP requests.

```javascript
// ✅ GOOD - Use vue-mc model
import { BaseModel } from '@awes-io/vue-mc'

class Service extends BaseModel {
    defaults() {
        return {
            id: null,
            name: '',
            description: ''
        }
    }

    routes() {
        return {
            fetch: '/api/services/{id}',
            save: '/api/services',
            update: '/api/services/{id}',
            delete: '/api/services/{id}'
        }
    }
}

export default {
    data() {
        return {
            service: new Service()
        }
    },
    methods: {
        async save() {
            await this.service.save()  // Uses vue-mc
        }
    }
}

// ❌ BAD - Don't use custom fetch
async save() {
    const res = await fetch('/api/services', { ... })  // Wrong!
}
```

3. **CTA button MUST use `:action` prop, not `#buttons` slot**

The primary action button (Save, Create, etc.) MUST use the `:action` prop and `@action` event on `AwPageSingle`. The `#buttons` slot is ONLY for secondary actions (Delete, Disable, etc.) wrapped in `AwPageMenuButtons`.

```javascript
// ✅ GOOD - CTA button via :action prop
<AwPageSingle
    :action="{ text: 'Save', loading: model.saving }"
    @action="save"
>
    <template #buttons>
        <AwPageMenuButtons
            v-if="!model.isNew()"
            :items="[{ text: 'Delete', color: 'error', listeners: { click: delete } }]"
        />
    </template>
</AwPageSingle>

// ❌ BAD - CTA button in #buttons slot
<AwPageSingle>
    <template #buttons>
        <AwButton @click="save">Save</AwButton>  // Wrong!
    </template>
</AwPageSingle>
```

## Basic Create/Edit Page

### Minimal Example

```markup
<template>
    <AwPageSingle
        hide-menu
        :title="pageTitle"
        :action="saveButton"
        @action="save"
    >
        <AwCard title="Customer Details">
            <AwGrid>
                <AwInput
                    v-model="customer.name"
                    label="Name"
                    :error="customer.errors.name"
                    required
                />

                <AwInput
                    v-model="customer.email"
                    label="Email"
                    type="email"
                    :error="customer.errors.email"
                    required
                />

                <AwTel
                    v-model="customer.phone"
                    label="Phone"
                    :error="customer.errors.phone"
                />
            </AwGrid>
        </AwCard>

        <AwCard title="Profile picture">
            <AwImageUpload
                v-model="customer.avatar"
                @loading="customer.saving = $event"
            />
        </AwCard>
    </AwPageSingle>
</template>

<script>
import Customer from '~/models/Customer'

export default {
    layout: 'empty',

    middleware: 'auth',

    data() {
        return {
            customer: new Customer(
                { id: this.$route.params.id },
                null,
                { shop_uuid: this.$route.params.shop_uuid }
            )
        }
    },

    computed: {
        pageTitle() {
            return this.customer.isNew() ? 'Create Customer' : 'Edit Customer'
        },

        saveButton() {
            return {
                text: 'Save',
                loading: this.customer.saving
            }
        }
    },

    async mounted() {
        if (!this.customer.isNew()) {
            try {
                await this.customer.fetch()
            } catch (error) {
                this.$notify({
                    title: 'Customer not found',
                    type: 'error'
                })
                this.$router.push(`/${this.$route.params.shop_uuid}/customers`)
            }
        }
    },

    methods: {
        async save() {
            try {
                await this.customer.save()

                this.$notify({
                    title: `Customer ${this.customer.isNew() ? 'created' : 'updated'} successfully`,
                    type: 'success'
                })

                this.$router.push(`/${this.$route.params.shop_uuid}/customers`)
            } catch (error) {
                this.$notify({
                    title: 'Failed to save customer',
                    type: 'error'
                })
            }
        }
    }
}
</script>
```

## Preview Slot Patterns

We have two standard patterns for the `#preview` slot in `AwPageSingle` to maintain visual consistency across the application.

### Human Entities (Users, Customers)

Used for people. Displays a large avatar and the person's name.

```markup
<template #preview>
    <AwCard class="text-center">
        <AwAvatar
            :src="user.avatar"
            :name="userName"
            size="240"
            class="mx-auto"
        />
        <AwHeadline class="mt-4">
            {{ userName }}
        </AwHeadline>
    </AwCard>
</template>
```

### Non-Human Entities (Services, Products, Locations)

Used for items with a representative image. Displays a large image with rounded corners.

```markup
<template #preview>
    <AwCard style="--card-padding-x: 0.5rem; --card-padding-y: 0.5rem;">
        <AwActionIcon
            :size="380"
            :image="item.image ? { src: item.image, alt: item.name } : null"
            icon="awesio/image"
            icon-color="mono-400"
            color="mono-800"
            class="w-full"
            style="--icon-size: 48px; --radius: 0.5rem;"
        />
        <AwHeadline class="p-4 mt-2">
            {{ item.name }}
        </AwHeadline>
    </AwCard>
</template>
```

## Image Uploads

For entities with images (avatars or item photos), provide an `AwImageUpload` component in its own `AwCard`.

```markup
<AwCard title="Profile picture">
    <AwImageUpload
        v-model="user.avatar"
        @loading="user.saving = $event"
    />
</AwCard>
```

**Key Points:**
- ✅ Use a separate `AwCard` with an appropriate title.
- ✅ Bind `v-model` to the model's image/avatar property.
- ✅ Listen to `@loading` to set the model's `saving` state, preventing form submission during upload.

## Model Initialization

### Constructor Pattern

**Model constructor signature:**
```javascript
new Model(attributes, collection, options)
```

**Three arguments:**
1. `attributes` - Initial data (object)
2. `collection` - Parent collection or `null`
3. `options` - Additional options (shop_uuid, etc.)

### Create Page (New Model)

```javascript
data() {
    return {
        customer: new Customer(
            {},  // Empty attributes for new record
            null,  // No parent collection
            { shop_uuid: this.$route.params.shop_uuid }
        )
    }
}
```

### Edit Page (Existing Model)

```javascript
data() {
    return {
        customer: new Customer(
            { id: this.$route.params.id },  // ID from route
            null,
            { shop_uuid: this.$route.params.shop_uuid }
        )
    }
},

async mounted() {
    // Fetch existing data
    if (!this.customer.isNew()) {
        await this.customer.fetch()
    }
}
```

### Handling UUID Routes

For routes using 'new' as placeholder:

```javascript
data() {
    const uuid = this.$route.params.uuid
    return {
        template: new Template(
            uuid === 'new' ? {} : { uuid },
            null,
            { shop_uuid: this.$route.params.shop_uuid }
        )
    }
},

async mounted() {
    if (this.$route.params.uuid !== 'new') {
        await this.template.fetch()
    }
}
```

### Using Same Template for Create/Edit

**Best Practice:** Use the same template for both create and edit pages. Write the template in `create.vue` and extend it in `_id.vue`:

**`create.vue`** (main template):
```javascript
export default {
    layout: 'empty',
    
    data() {
        return {
            service: new Service({ id: this.$route.params.id })
        }
    },
    
    computed: {
        pageTitle() {
            return this.service.isNew() ? 'Create service' : 'Edit service'
        },
        
        actionButton() {
            return {
                text: this.service.isNew() ? 'Create' : 'Save',
                loading: this.service.saving
            }
        }
    },
    
    async mounted() {
        if (!this.service.isNew()) {
            try {
                await this.service.fetch()
            } catch (error) {
                this.$notify({
                    title: 'Service not found',
                    type: 'error'
                })
                this.$router.push('/services')
            }
        }
    },
    
    watch: {
        '$route.params.id': {
            handler(newId) {
                this.service = new Service({ id: newId })
                if (newId) {
                    this.service.fetch()
                }
            }
        }
    }
}
```

**`_id.vue`** (extends create.vue):
```javascript
<script>
import CreatePage from './create.vue'

export default {
    extends: CreatePage
}
</script>
```

**Benefits:**
- ✅ Single source of truth for form structure
- ✅ Automatic create/edit mode detection via `model.isNew()`
- ✅ Computed props handle dynamic titles and button texts
- ✅ Less code duplication

## Page Title Patterns

### Dynamic Title Based on State

```javascript
computed: {
    pageTitle() {
        return this.model.isNew()
            ? 'Create Product'
            : 'Edit Product'
    }
}
```

### Title with Item Name

```javascript
computed: {
    pageTitle() {
        if (this.model.isNew()) {
            return 'Create Template'
        }
        return this.model.name || 'Edit Template'
    }
}
```

### Title with ID

```javascript
computed: {
    pageTitle() {
        return this.model.isNew()
            ? 'New Invoice'
            : `Invoice #${this.model.invoice_number}`
    }
}
```

## Person selectors with avatars (users/customers/instructors)

For detail/create/edit pages that pick a person (user, customer, instructor), use `AwSelectObject` with `icon` and `option-label` slots to show avatars and a fallback icon.

```markup
<AwSelectObject
    v-model="selectedPerson"
    :options="searchPeople"
    :option-label="person => `${person.first_name} ${person.last_name}`"
    track-by="id"
    clearable
>
    <template #icon="{ option }">
        <AwAvatar
            v-if="option"
            class="mx-3"
            :src="option.avatar"
            :name="`${option.first_name} ${option.last_name}`"
            size="24"
            icon="awesio/user"
        />
        <AwActionIcon
            v-else
            class="mx-3 rounded-full"
            icon="awesio/user"
            size="xs"
        />
    </template>

    <template #option-label="{ option, highlightSearch }">
        <div class="flex items-center gap-2">
            <AwAvatar
                :src="option.avatar"
                :name="`${option.first_name} ${option.last_name}`"
                size="24"
                class="-ml-1"
                icon="awesio/user"
            />
            <div class="leading-tight">
                <div v-html="highlightSearch(`${option.first_name} ${option.last_name}`)" />
                <div v-if="option.email" class="text-sm text-mono-500">
                    {{ option.email }}
                </div>
            </div>
        </div>
    </template>
</AwSelectObject>

<script>
export default {
    methods: {
        searchPeople(search, page) {
            return {
                url: '/api/users',
                params: { search, page }
            }
        }
    }
}
</script>
```

## Form Layouts

### Single Column Layout

Simple forms with basic fields:

```markup
<template>
    <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
        <AwCard title="Settings">
            <AwGrid>
                <AwInput v-model="settings.company_name" label="Company Name" />
                <AwInput v-model="settings.email" label="Email" />
                <AwTel v-model="settings.phone" label="Phone" />
                <AwTextarea v-model="settings.address" label="Address" />
            </AwGrid>
        </AwCard>
    </AwPageSingle>
</template>
```

### Two-Column Layout

Complex forms or forms with sidebar:

```markup
<template>
    <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
        <AwGrid :col="{ lg: 3 }">
            <!-- Main content (2/3 width) -->
            <div span="{ lg: 2 }" class="space-y-6">
                <AwCard title="Product Details">
                    <AwGrid>
                        <AwInput v-model="product.name" label="Name" />
                        <AwInput v-model="product.sku" label="SKU" />
                        <AwTextarea v-model="product.description" label="Description" />
                    </AwGrid>
                </AwCard>

                <AwCard title="Pricing">
                    <AwGrid>
                        <AwMoney v-model="product.price" label="Price" />
                        <AwMoney v-model="product.cost" label="Cost" />
                    </AwGrid>
                </AwCard>
```

**⚠️ Important: Use AwMoney for all money fields**

For all money-related fields (price, cost, discount_amount, tax_amount, etc.), always use `AwMoney` instead of `AwInput` with `type="number"`:

```markup
<!-- ✅ GOOD - Use AwMoney for money fields -->
<AwMoney v-model="product.price" label="Price" :error="product.errors.price" />
<AwMoney v-model="product.cost" label="Cost" :error="product.errors.cost" />
<AwMoney v-model="order.discount_amount" label="Discount" :error="order.errors.discount_amount" />

<!-- ❌ BAD - Don't use AwInput with type="number" for money -->
<AwInput v-model="product.price" label="Price" type="number" />
            </div>

            <!-- Sidebar (1/3 width) -->
            <div class="space-y-6">
                <AwCard title="Status">
                    <AwSwitcher v-model="product.is_active" label="Active" />
                    <AwSwitcher v-model="product.is_featured" label="Featured" />
                </AwCard>

                <AwCard title="Category">
                    <AwSelect
                        v-model="product.category_id"
                        :options="categories"
                        label="Category"
                    />
                </AwCard>
            </div>
        </AwGrid>
    </AwPageSingle>
</template>
```

### Multiple Sections

Large forms organized by sections:

```markup
<template>
    <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
        <div class="space-y-6">
            <!-- Basic Information -->
            <AwCard title="Basic Information">
                <AwGrid>
                    <AwInput v-model="user.first_name" label="First Name" />
                    <AwInput v-model="user.last_name" label="Last Name" />
                    <AwInput v-model="user.email" label="Email" />
                </AwGrid>
            </AwCard>

            <!-- Contact Details -->
            <AwCard title="Contact Details">
                <AwGrid>
                    <AwTel v-model="user.phone" label="Phone" />
                    <AwAddress v-model="user.address" label="Address" />
                </AwGrid>
            </AwCard>

            <!-- Permissions -->
            <AwCard title="Permissions">
                <AwSelect
                    v-model="user.role"
                    :options="['user', 'manager', 'admin']"
                    label="Role"
                />
                <AwCheckbox v-model="user.can_export" label="Can export data" />
                <AwCheckbox v-model="user.can_import" label="Can import data" />
            </AwCard>
        </div>
    </AwPageSingle>
</template>
```

## Validation & Error Handling

### Field-Level Errors

```markup
<AwInput
    v-model="model.name"
    label="Name"
    :error="model.errors.name"
    required
/>
```

**How it works:**
1. Model's `save()` sends request to Laravel
2. Laravel returns 422 with validation errors
3. Model populates `errors` object automatically
4. Error displays below field

### Check for Errors After Save

```javascript
async save() {
    await this.model.save()

    // Check if validation failed
    if (Object.keys(this.model.errors).length > 0) {
        this.$notify({
            title: 'Please fix validation errors',
            type: 'error'
        })
        return
    }

    // Success path
    this.$notify({
        title: 'Saved successfully',
        type: 'success'
    })
    this.$router.push('/list')
}
```

### Handle Fetch Errors

```javascript
async mounted() {
    if (!this.model.isNew()) {
        try {
            await this.model.fetch()
        } catch (error) {
            // 404 - Record not found
            if (error.response?.status === 404) {
                this.$notify({
                    title: 'Record not found',
                    type: 'error'
                })
                this.$router.push('/list')
                return
            }

            // Other errors
            this.$notify({
                title: 'Failed to load data',
                type: 'error'
            })
        }
    }
}
```

### Custom Validation Messages

```markup
<template>
    <AwInput
        v-model="model.email"
        label="Email"
        :error="emailError"
        required
    />
</template>

<script>
export default {
    computed: {
        emailError() {
            if (this.model.errors.email) {
                return this.model.errors.email
            }
            if (this.model.email && !this.isValidEmail(this.model.email)) {
                return 'Please enter a valid email address'
            }
            return null
        }
    },

    methods: {
        isValidEmail(email) {
            return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
        }
    }
}
</script>
```

## Loading & Saving States

### Save Button Loading

```javascript
computed: {
    saveButton() {
        return {
            text: 'Save',
            loading: this.model.saving  // Automatically set by vue-mc
        }
    }
}
```

### Fetch Loading State

When loading data without a card wrapper:

```markup
<template>
    <AwPageSingle hide-menu :title="pageTitle">
        <AwContentPlaceholder v-if="loading" type="form" :lines="6" />

        <div v-else class="space-y-6">
            <!-- Form content -->
        </div>
    </AwPageSingle>
</template>

<script>
export default {
    data() {
        return {
            loading: true
        }
    },

    async mounted() {
        if (!this.model.isNew()) {
            try {
                await this.model.fetch()
            } finally {
                this.loading = false
            }
        } else {
            this.loading = false
        }
    }
}
</script>
```

When using `AwCard`, place the placeholder inside the card to replace the content:

```markup
<template>
    <AwPageSingle hide-menu :title="pageTitle">
        <AwCard title="Details">
            <AwContentPlaceholder v-if="model.loading && !model.isNew()" type="form" :lines="4" />

            <AwGrid v-else>
                <AwInput v-model="model.name" label="Name" />
                <AwInput v-model="model.email" label="Email" />
            </AwGrid>
        </AwCard>
    </AwPageSingle>
</template>
```

### Complex Loading State

For pages with multiple sections, use multiple placeholders:

```markup
<template>
    <AwPageSingle hide-menu :title="pageTitle">
        <AwGrid v-if="loading" :col="{ lg: 3 }">
            <div span="{ lg: 2 }">
                <AwContentPlaceholder type="form" :lines="8" />
            </div>
            <div>
                <AwContentPlaceholder type="form" :lines="4" />
            </div>
        </AwGrid>

        <div v-else class="space-y-6">
            <!-- Form content -->
        </div>
    </AwPageSingle>
</template>
```

### Progress Indicators in Header

```markup
<template>
    <AwPageSingle hide-menu :title="pageTitle" :action="saveButton" @action="save">
        <!-- Custom header indicators -->
        <template #buttons>
            <div v-if="autoSaving" class="flex items-center gap-2 text-sm text-secondary">
                <AwProgress size="sm" indeterminate />
                <span>Saving...</span>
            </div>
            <div v-else-if="lastSaved" class="text-sm text-success">
                Saved {{ $dayjs(lastSaved).fromNow() }}
            </div>
        </template>

        <!-- Form content -->
    </AwPageSingle>
</template>

<script>
export default {
    data() {
        return {
            autoSaving: false,
            lastSaved: null
        }
    },

    watch: {
        // Auto-save on changes
        'model.$attributes': {
            handler() {
                this.debouncedSave()
            },
            deep: true
        }
    },

    methods: {
        debouncedSave: _.debounce(function() {
            this.autoSave()
        }, 2000),

        async autoSave() {
            if (this.model.isNew()) return

            this.autoSaving = true
            try {
                await this.model.save()
                this.lastSaved = new Date()
            } catch (error) {
                // Silently fail auto-save
            } finally {
                this.autoSaving = false
            }
        }
    }
}
</script>
```

## Action Button Patterns

**⚠️ Important: CTA button MUST use `:action` prop**

The primary action button (Save, Create, etc.) MUST use the `:action` prop and `@action` event on `AwPageSingle`. Do NOT put the CTA button in the `#buttons` slot.

The `#buttons` slot is ONLY for secondary actions (Delete, Disable, etc.) and they MUST be wrapped in `AwPageMenuButtons` without `cta` prop.

### Delete Button

Use the `#buttons` slot with `AwPageMenuButtons` for delete and other secondary actions:

```markup
<template>
    <AwPageSingle
        hide-menu
        :title="pageTitle"
        :action="saveButton"
        @action="save"
    >
        <template #buttons>
            <AwPageMenuButtons
                v-if="!model.isNew()"
                :items="[
                    {
                        text: 'Delete',
                        icon: 'awesio/delete',
                        color: 'error',
                        listeners: { click: deleteItem }
                    }
                ]"
            />
        </template>

        <!-- Form content -->
    </AwPageSingle>
</template>

<script>
export default {
    computed: {
        saveButton() {
            return {
                text: this.model.isNew() ? 'Create' : 'Save',
                loading: this.model.saving
            }
        }
    },

    methods: {
        async save() {
            try {
                await this.model.save()
                this.$notify({
                    title: `Item ${this.model.isNew() ? 'created' : 'updated'} successfully`,
                    type: 'success'
                })
                this.$router.push('/list')
            } catch (error) {
                this.$notify({
                    title: 'Failed to save item',
                    type: 'error'
                })
            }
        },

        async deleteItem() {
            try {
                await this.$confirm({
                    title: 'Delete Item',
                    message: `Are you sure you want to delete "${this.model.name}"?`
                })

                await this.model.delete()
                this.$notify({
                    title: 'Item deleted successfully',
                    type: 'success'
                })
                this.$router.push('/list')
            } catch (error) {
                if (this.$confirm.isCancel(error)) {
                    return
                }

                this.$notify({
                    title: 'Failed to delete item',
                    type: 'error'
                })
            }
        }
    }
}
</script>
```

**Key points:**
- ✅ Delete button wrapped in `AwPageMenuButtons`
- ✅ Delete button has `color: 'error'` prop
- ✅ Delete button only shows when editing (`!model.isNew()`)
- ✅ `$confirm` wrapped in try-catch with `$confirm.isCancel()` check

### Multiple Secondary Actions

For multiple secondary actions, use `#buttons` slot with `AwPageMenuButtons`:

```markup
<template>
    <AwPageSingle
        hide-menu
        :title="pageTitle"
        :action="publishButton"
        @action="publish"
    >
        <template #buttons>
            <AwPageMenuButtons
                :items="[
                    {
                        text: 'Preview',
                        icon: 'awesio/eye',
                        listeners: { click: preview }
                    },
                    {
                        text: 'Save as Draft',
                        icon: 'awesio/save',
                        listeners: { click: saveAsDraft }
                    }
                ]"
            />
        </template>

        <!-- Form content -->
    </AwPageSingle>
</template>

<script>
export default {
    computed: {
        publishButton() {
            return {
                key: 'publish',
                label: 'Publish',
                loading: this.model.saving,
                color: 'accent'
            }
        }
    },

    methods: {
        async publish() {
            // Publish logic
        },

        preview() {
            // Preview logic
        },

        async saveAsDraft() {
            // Save as draft logic
        }
    }
}
</script>
```

### Dropdown Menu for More Actions

For many secondary actions, use the `#buttons` slot with a dropdown:

```markup
<template>
    <AwPageSingle
        hide-menu
        :title="pageTitle"
        :action="saveButton"
        @action="save"
    >
        <template #buttons>
            <AwDropdown>
                <template #trigger>
                    <AwButton text="More" icon="dots-vertical" />
                </template>

                <AwButton @click="duplicate" theme="text" text="Duplicate" />
                <AwButton @click="archive" theme="text" text="Archive" />
                <AwButton @click="deleteItem" theme="text" color="error" text="Delete" />
            </AwDropdown>
        </template>

        <!-- Form content -->
    </AwPageSingle>
</template>
```

## Complete Examples

### Simple Create/Edit Page

```markup
<template>
    <AwPageSingle
        hide-menu
        :title="pageTitle"
        :action="saveButton"
        @action="save"
    >
        <AwCard title="Template Details">
            <AwGrid>
                <AwInput
                    v-model="template.name"
                    label="Template Name"
                    :error="template.errors.name"
                    required
                />

                <AwSelect
                    v-model="template.channel"
                    :options="['email', 'sms', 'push']"
                    label="Channel"
                    :error="template.errors.channel"
                    required
                />

                <AwInput
                    v-model="template.subject"
                    label="Subject"
                    :error="template.errors.subject"
                    class="col-span-2"
                />

                <AwTextarea
                    v-model="template.body"
                    label="Message Body"
                    :error="template.errors.body"
                    :rows="8"
                    class="col-span-2"
                    required
                />

                <AwSwitcher
                    v-model="template.is_active"
                    label="Active"
                />
            </AwGrid>
        </AwCard>
    </AwPageSingle>
</template>

<script>
import Template from '~/models/Template'

export default {
    middleware: 'auth',

    data() {
        const uuid = this.$route.params.uuid
        return {
            template: new Template(
                uuid === 'new' ? {} : { uuid },
                null,
                { shop_uuid: this.$route.params.shop_uuid }
            )
        }
    },

    computed: {
        pageTitle() {
            return this.template.isNew() ? 'Create Template' : 'Edit Template'
        },

        saveButton() {
            return {
                text: this.template.isNew() ? 'Create' : 'Save',
                loading: this.template.saving
            }
        }
    },

    async mounted() {
        if (this.$route.params.uuid !== 'new') {
            try {
                await this.template.fetch()
            } catch (error) {
                this.$notify({
                    title: 'Template not found',
                    type: 'error'
                })
                this.$router.push(`/${this.$route.params.shop_uuid}/templates`)
            }
        }
    },

    methods: {
        async save() {
            try {
                await this.template.save()

                this.$notify({
                    title: `Template ${this.template.isNew() ? 'created' : 'updated'} successfully`,
                    type: 'success'
                })

                this.$router.push(`/${this.$route.params.shop_uuid}/templates`)
            } catch (error) {
                this.$notify({
                    title: 'Failed to save template',
                    type: 'error'
                })
            }
        }
    }
}
</script>
```

### Complex Multi-Section Page

```markup
<template>
    <AwPageSingle
        hide-menu
        :title="pageTitle"
        :action="saveButton"
        @action="save"
    >
        <template #buttons>
            <AwPageMenuButtons
                v-if="!product.isNew()"
                :items="[
                    {
                        text: 'Delete',
                        icon: 'awesio/delete',
                        color: 'error',
                        listeners: { click: deleteProduct }
                    }
                ]"
            />
        </template>

        <AwGrid :col="{ lg: 3 }">
            <!-- Main content -->
            <div span="{ lg: 2 }" class="space-y-6">
                <!-- Basic Details -->
                <AwCard title="Product Details">
                    <AwGrid>
                        <AwInput
                            v-model="product.name"
                            label="Product Name"
                            :error="product.errors.name"
                            required
                            class="col-span-2"
                        />

                        <AwInput
                            v-model="product.sku"
                            label="SKU"
                            :error="product.errors.sku"
                        />

                        <AwSelect
                            v-model="product.category_id"
                            :options="loadCategories"
                            option-label="name"
                            track-by="id"
                            label="Category"
                            :error="product.errors.category_id"
                        />

                        <AwTextarea
                            v-model="product.description"
                            label="Description"
                            :error="product.errors.description"
                            :rows="6"
                            class="col-span-2"
                        />
                    </AwGrid>
                </AwCard>

                <!-- Pricing -->
                <AwCard title="Pricing">
                    <AwGrid>
                        <AwMoney
                            v-model="product.price"
                            label="Retail Price"
                            :error="product.errors.price"
                            required
                        />

                        <AwMoney
                            v-model="product.cost"
                            label="Cost"
                            :error="product.errors.cost"
                        />

                        <AwInput
                            v-model="product.compare_at_price"
                            label="Compare at Price"
                            type="number"
                            :error="product.errors.compare_at_price"
                        />
                    </AwGrid>
                </AwCard>

                <!-- Images -->
                <AwCard title="Images">
                    <AwUploader
                        v-model="product.images"
                        label="Product Images"
                        :error="product.errors.images"
                        accept="image/*"
                        multiple
                    />
                </AwCard>
            </div>

            <!-- Sidebar -->
            <div class="space-y-6">
                <!-- Status -->
                <AwCard title="Status">
                    <div class="space-y-4">
                        <AwSwitcher
                            v-model="product.is_active"
                            label="Active"
                        />
                        <AwSwitcher
                            v-model="product.is_featured"
                            label="Featured"
                        />
                    </div>
                </AwCard>

                <!-- Inventory -->
                <AwCard title="Inventory">
                    <AwGrid>
                        <AwInput
                            v-model="product.stock_quantity"
                            label="Stock Quantity"
                            type="number"
                            :error="product.errors.stock_quantity"
                        />

                        <AwSwitcher
                            v-model="product.track_inventory"
                            label="Track Inventory"
                        />
                    </AwGrid>
                </AwCard>

                <!-- Shipping -->
                <AwCard title="Shipping">
                    <AwGrid>
                        <AwInput
                            v-model="product.weight"
                            label="Weight (kg)"
                            type="number"
                            :error="product.errors.weight"
                        />

                        <AwSwitcher
                            v-model="product.requires_shipping"
                            label="Requires Shipping"
                        />
                    </AwGrid>
                </AwCard>
            </div>
        </AwGrid>
    </AwPageSingle>
</template>

<script>
import Product from '~/models/Product'

export default {
    middleware: 'auth',

    data() {
        return {
            product: new Product(
                { id: this.$route.params.id },
                null,
                { shop_uuid: this.$route.params.shop_uuid }
            )
        }
    },

    computed: {
        pageTitle() {
            return this.product.isNew() ? 'Create Product' : 'Edit Product'
        },

        saveButton() {
            return {
                text: 'Save',
                loading: this.product.saving,
                color: 'accent'
            }
        }
    },

    async mounted() {
        if (!this.product.isNew()) {
            try {
                await this.product.fetch()
            } catch (error) {
                this.$notify({
                    title: 'Product not found',
                    type: 'error'
                })
                this.$router.push(`/${this.$route.params.shop_uuid}/products`)
            }
        }
    },

    methods: {
        loadCategories(search) {
            const shopUuid = this.$route.params.shop_uuid
            return `/api/shops/${shopUuid}/categories?search=${search}`
        },

        async save() {
            try {
                await this.product.save()

                this.$notify({
                    title: `Product ${this.product.isNew() ? 'created' : 'updated'} successfully`,
                    type: 'success'
                })

                this.$router.push(`/${this.$route.params.shop_uuid}/products`)
            } catch (error) {
                this.$notify({
                    title: 'Failed to save product',
                    type: 'error'
                })
            }
        },

        async deleteProduct() {
            try {
                await this.$confirm({
                    title: 'Delete Product',
                    message: `Are you sure you want to delete "${this.product.name}"?`
                })

                await this.product.delete()
                this.$notify({
                    message: 'Product deleted successfully',
                    type: 'success'
                })
                this.$router.push(`/${this.$route.params.shop_uuid}/products`)
            } catch (error) {
                if (this.$confirm.isCancel(error)) {
                    return
                }

                this.$notify({
                    message: 'Failed to delete product',
                    type: 'error'
                })
            }
        }
    }
}
</script>

## Best Practices

### 1. Always Check isNew()

```javascript
computed: {
    pageTitle() {
        return this.model.isNew() ? 'Create' : 'Edit'
    }
}
```

### 2. Handle Fetch Errors

```javascript
async mounted() {
    if (!this.model.isNew()) {
        try {
            await this.model.fetch()
        } catch (error) {
            // Redirect on error
            this.$router.push('/list')
        }
    }
}
```

### 3. Handle Save Errors

The `save()` method throws on validation errors, so no need to check `errors` manually:

```javascript
try {
    await this.model.save()
    // Success path - only reached if save succeeds
    this.$notify({ title: 'Saved successfully', type: 'success' })
} catch (error) {
    // Error path - handles both validation and network errors
    this.$notify({ title: 'Failed to save', type: 'error' })
}
```

### 4. Confirm Destructive Actions

Always wrap `$confirm` in try-catch and check for cancellation:

```javascript
async deleteItem() {
    try {
        await this.$confirm({
            title: 'Delete Item',
            message: `Are you sure you want to delete "${this.model.name}"?`
        })

        await this.model.delete()
        this.$notify({ title: 'Deleted successfully', type: 'success' })
        this.$router.push('/list')
    } catch (error) {
        if (this.$confirm.isCancel(error)) {
            return  // User cancelled, do nothing
        }

        this.$notify({ title: 'Failed to delete', type: 'error' })
    }
}
```

### 5. Provide User Feedback

```javascript
this.$notify({
    message: 'Saved successfully',
    type: 'success'
})
```

## See Also

- [List Pages](./list-pages.md) - Table-based list pages
- [Dashboard Pages](./dashboard-pages.md) - Metrics and overview pages
- [AwPageSingle](../../components/pages/aw-page-single.md) - Page component reference
- [Forms Guide](../forms-guide.md) - Form patterns and validation
- [Error Handling Guide](../error-handling.md) - Error handling patterns
