@awes-io/ui 2.142.0 → 2.143.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/assets/css/components/_index.css +7 -1
- package/assets/css/components/animation.css +38 -32
- package/assets/css/components/content-placeholder.css +103 -0
- package/assets/css/components/empty-container.css +69 -1
- package/assets/css/components/filter-chosen.css +6 -0
- package/assets/css/components/filter-date-range.css +17 -1
- package/assets/css/components/filter-month.css +23 -17
- package/assets/css/components/filter-select.css +11 -0
- package/assets/css/components/layout.css +1 -32
- package/assets/css/components/modal.css +1 -1
- package/assets/css/components/number.css +12 -0
- package/assets/css/components/page-aside.css +54 -0
- package/assets/js/css.js +1 -1
- package/assets/js/icons/mono.js +59 -91
- package/assets/js/icons/multicolor.js +1 -31
- package/components/1_atoms/AwContentPlaceholder.vue +60 -0
- package/components/1_atoms/AwFlow.vue +21 -48
- package/components/1_atoms/AwLabel.vue +1 -1
- package/components/2_molecules/AwButton.vue +1 -1
- package/components/2_molecules/AwEmptyContainer.vue +74 -72
- package/components/2_molecules/AwNumber.vue +180 -0
- package/components/2_molecules/AwSelect.vue +11 -4
- package/components/3_organisms/AwFilterChosen.vue +73 -0
- package/components/3_organisms/AwFilterDateRange.vue +177 -0
- package/components/3_organisms/AwFilterMonth.vue +37 -40
- package/components/3_organisms/AwFilterSelect.vue +368 -0
- package/components/3_organisms/AwImageUpload.vue +1 -1
- package/components/3_organisms/AwMarkdownEditor.vue +0 -0
- package/components/3_organisms/AwMultiBlockBuilder.vue +1 -1
- package/components/3_organisms/AwTable/AwTableBuilder.vue +12 -60
- package/components/4_pages/AwPageAside.vue +108 -0
- package/components/5_layouts/AwLayoutCenter.vue +3 -8
- package/components/5_layouts/_AwUserMenu.vue +1 -1
- package/dist/css/aw-icons.css +26 -0
- package/dist/fonts/aw-icons.svg +18 -0
- package/dist/fonts/aw-icons.ttf +0 -0
- package/dist/fonts/aw-icons.woff +0 -0
- package/dist/fonts/aw-icons.woff2 +0 -0
- package/docs/_template.md +80 -0
- package/docs/components/atoms/aw-accordion-fold.md +91 -0
- package/docs/components/atoms/aw-action-card-body.md +67 -0
- package/docs/components/atoms/aw-action-card.md +94 -0
- package/docs/components/atoms/aw-action-icon.md +88 -0
- package/docs/components/atoms/aw-avatar.md +106 -0
- package/docs/components/atoms/aw-card.md +112 -0
- package/docs/components/atoms/aw-checkbox.md +112 -0
- package/docs/components/atoms/aw-content-placeholder.md +116 -0
- package/docs/components/atoms/aw-description.md +83 -0
- package/docs/components/atoms/aw-dock.md +84 -0
- package/docs/components/atoms/aw-dropdown-button.md +94 -0
- package/docs/components/atoms/aw-dropdown.md +128 -0
- package/docs/components/atoms/aw-file.md +73 -0
- package/docs/components/atoms/aw-flow.md +92 -0
- package/docs/components/atoms/aw-grid.md +91 -0
- package/docs/components/atoms/aw-headline.md +71 -0
- package/docs/components/atoms/aw-icon-system-color.md +121 -0
- package/docs/components/atoms/aw-icon-system-mono.md +205 -0
- package/docs/components/atoms/aw-icon.md +235 -0
- package/docs/components/atoms/aw-info.md +85 -0
- package/docs/components/atoms/aw-input.md +120 -0
- package/docs/components/atoms/aw-label.md +83 -0
- package/docs/components/atoms/aw-link.md +99 -0
- package/docs/components/atoms/aw-list.md +88 -0
- package/docs/components/atoms/aw-progress.md +70 -0
- package/docs/components/atoms/aw-radio.md +109 -0
- package/docs/components/atoms/aw-refresh-wrapper.md +81 -0
- package/docs/components/atoms/aw-select-native.md +106 -0
- package/docs/components/atoms/aw-slider.md +82 -0
- package/docs/components/atoms/aw-sub-headline.md +73 -0
- package/docs/components/atoms/aw-switcher.md +115 -0
- package/docs/components/atoms/aw-tag.md +80 -0
- package/docs/components/atoms/aw-title.md +70 -0
- package/docs/components/atoms/aw-toggler.md +69 -0
- package/docs/components/layouts/aw-layout-center.md +168 -0
- package/docs/components/layouts/aw-layout-error.md +153 -0
- package/docs/components/layouts/aw-layout-provider.md +238 -0
- package/docs/components/layouts/aw-layout.md +88 -0
- package/docs/components/molecules/aw-action-button.md +91 -0
- package/docs/components/molecules/aw-alert.md +96 -0
- package/docs/components/molecules/aw-badge.md +108 -0
- package/docs/components/molecules/aw-banner-text.md +90 -0
- package/docs/components/molecules/aw-button-nav.md +46 -0
- package/docs/components/molecules/aw-button.md +123 -0
- package/docs/components/molecules/aw-description-input.md +67 -0
- package/docs/components/molecules/aw-empty-container.md +86 -0
- package/docs/components/molecules/aw-island.md +234 -0
- package/docs/components/molecules/aw-number.md +138 -0
- package/docs/components/molecules/aw-select-object.md +401 -0
- package/docs/components/molecules/aw-select.md +215 -0
- package/docs/components/molecules/aw-tab-nav.md +108 -0
- package/docs/components/molecules/aw-tel.md +129 -0
- package/docs/components/molecules/aw-textarea.md +83 -0
- package/docs/components/molecules/aw-userpic.md +115 -0
- package/docs/components/organisms/aw-address-block.md +64 -0
- package/docs/components/organisms/aw-address.md +132 -0
- package/docs/components/organisms/aw-birthday-picker.md +73 -0
- package/docs/components/organisms/aw-bottom-bar.md +66 -0
- package/docs/components/organisms/aw-calendar-days.md +115 -0
- package/docs/components/organisms/aw-calendar-nav.md +98 -0
- package/docs/components/organisms/aw-calendar-view.md +98 -0
- package/docs/components/organisms/aw-calendar.md +166 -0
- package/docs/components/organisms/aw-chart.md +154 -0
- package/docs/components/organisms/aw-chip-select.md +164 -0
- package/docs/components/organisms/aw-chip.md +126 -0
- package/docs/components/organisms/aw-code-snippet.md +94 -0
- package/docs/components/organisms/aw-code.md +132 -0
- package/docs/components/organisms/aw-context-menu.md +117 -0
- package/docs/components/organisms/aw-cropper.md +151 -0
- package/docs/components/organisms/aw-date.md +161 -0
- package/docs/components/organisms/aw-display-date.md +33 -0
- package/docs/components/organisms/aw-download-link.md +46 -0
- package/docs/components/organisms/aw-fetch-data.md +161 -0
- package/docs/components/organisms/aw-filter-chosen.md +226 -0
- package/docs/components/organisms/aw-filter-date-range.md +205 -0
- package/docs/components/organisms/aw-filter-month.md +43 -0
- package/docs/components/organisms/aw-filter-select.md +225 -0
- package/docs/components/organisms/aw-form.md +174 -0
- package/docs/components/organisms/aw-gmap-marker.md +86 -0
- package/docs/components/organisms/aw-gmap.md +90 -0
- package/docs/components/organisms/aw-image-upload.md +56 -0
- package/docs/components/organisms/aw-island-avatar.md +87 -0
- package/docs/components/organisms/aw-markdown-editor.md +104 -0
- package/docs/components/organisms/aw-modal-buttons.md +57 -0
- package/docs/components/organisms/aw-modal.md +246 -0
- package/docs/components/organisms/aw-model-edit.md +74 -0
- package/docs/components/organisms/aw-money.md +53 -0
- package/docs/components/organisms/aw-multi-block-builder.md +165 -0
- package/docs/components/organisms/aw-pagination.md +121 -0
- package/docs/components/organisms/aw-password.md +103 -0
- package/docs/components/organisms/aw-preview-card.md +45 -0
- package/docs/components/organisms/aw-search.md +116 -0
- package/docs/components/organisms/aw-subnav.md +122 -0
- package/docs/components/organisms/aw-table-builder.md +165 -0
- package/docs/components/organisms/aw-table-col.md +123 -0
- package/docs/components/organisms/aw-table-head.md +92 -0
- package/docs/components/organisms/aw-table-row.md +91 -0
- package/docs/components/organisms/aw-table.md +172 -0
- package/docs/components/organisms/aw-tags.md +54 -0
- package/docs/components/organisms/aw-toggle-show-aside.md +43 -0
- package/docs/components/organisms/aw-uploader-files.md +125 -0
- package/docs/components/organisms/aw-uploader.md +163 -0
- package/docs/components/organisms/aw-user-menu.md +87 -0
- package/docs/components/pages/aw-page-aside.md +296 -0
- package/docs/components/pages/aw-page-menu-buttons.md +172 -0
- package/docs/components/pages/aw-page-modal.md +198 -0
- package/docs/components/pages/aw-page-single.md +253 -0
- package/docs/components/pages/aw-page.md +194 -0
- package/docs/configuration.md +493 -0
- package/docs/cookbook/advanced-patterns.md +1388 -0
- package/docs/cookbook/common-patterns.md +965 -0
- package/docs/cookbook/index.md +786 -0
- package/docs/getting-started.md +596 -0
- package/docs/guides/best-practices.md +1106 -0
- package/docs/guides/data-fetching.md +852 -0
- package/docs/guides/error-handling.md +1172 -0
- package/docs/guides/forms-guide.md +1329 -0
- package/docs/guides/mobile-subnavigation.md +359 -0
- package/docs/guides/page-patterns/aside-pages.md +1418 -0
- package/docs/guides/page-patterns/dashboard-pages.md +990 -0
- package/docs/guides/page-patterns/detail-pages.md +1493 -0
- package/docs/guides/page-patterns/list-pages.md +1094 -0
- package/docs/index.md +263 -1
- package/docs/integrations.md +870 -0
- package/docs/reference/menu.md +462 -0
- package/docs/reference/plugins.md +970 -0
- package/docs/reference/troubleshooting.md +945 -0
- package/nuxt/awes.config.js +9 -8
- package/nuxt/icons.css +26 -0
- package/nuxt/index.js +2 -2
- package/nuxt/pages/more.vue +1 -1
- package/package.json +5 -3
- package/readme.md +171 -1
- package/docs/aw-accordion-fold.md +0 -46
- package/docs/aw-address.md +0 -44
- package/docs/aw-avatar.md +0 -51
- package/docs/aw-badge.md +0 -32
- package/docs/aw-button-nav.md +0 -44
- package/docs/aw-button.md +0 -50
- package/docs/aw-calendar-days.md +0 -46
- package/docs/aw-calendar-nav.md +0 -25
- package/docs/aw-calendar-view.md +0 -12
- package/docs/aw-calendar.md +0 -59
- package/docs/aw-card.md +0 -48
- package/docs/aw-chart.md +0 -51
- package/docs/aw-checkbox.md +0 -56
- package/docs/aw-chip-select.md +0 -46
- package/docs/aw-chip.md +0 -53
- package/docs/aw-code-snippet.md +0 -18
- package/docs/aw-code.md +0 -56
- package/docs/aw-content-wrapper.md +0 -40
- package/docs/aw-context-menu.md +0 -31
- package/docs/aw-cropper.md +0 -60
- package/docs/aw-dashboard-card.md +0 -37
- package/docs/aw-dashboard-donut.md +0 -30
- package/docs/aw-dashboard-line.md +0 -20
- package/docs/aw-dashboard-progress.md +0 -33
- package/docs/aw-dashboard-section.md +0 -32
- package/docs/aw-dashboard-speed.md +0 -30
- package/docs/aw-date.md +0 -52
- package/docs/aw-dropdown-button.md +0 -31
- package/docs/aw-dropdown.md +0 -69
- package/docs/aw-fetch-data.md +0 -45
- package/docs/aw-form.md +0 -52
- package/docs/aw-grid.md +0 -48
- package/docs/aw-icon.md +0 -50
- package/docs/aw-info.md +0 -53
- package/docs/aw-input.md +0 -55
- package/docs/aw-layout-default.md +0 -30
- package/docs/aw-layout-frame-center.md +0 -29
- package/docs/aw-layout-simple.md +0 -49
- package/docs/aw-link.md +0 -54
- package/docs/aw-markdown-editor.md +0 -51
- package/docs/aw-modal.md +0 -63
- package/docs/aw-multi-block-builder.md +0 -66
- package/docs/aw-page.md +0 -36
- package/docs/aw-pagination.md +0 -54
- package/docs/aw-password.md +0 -48
- package/docs/aw-radio.md +0 -54
- package/docs/aw-search.md +0 -49
- package/docs/aw-select.md +0 -93
- package/docs/aw-slider.md +0 -40
- package/docs/aw-svg-image.md +0 -19
- package/docs/aw-switcher.md +0 -51
- package/docs/aw-tab-nav.md +0 -55
- package/docs/aw-table-builder.md +0 -58
- package/docs/aw-table-col.md +0 -33
- package/docs/aw-table-head.md +0 -28
- package/docs/aw-table-row.md +0 -33
- package/docs/aw-table.md +0 -59
- package/docs/aw-tel.md +0 -47
- package/docs/aw-textarea.md +0 -47
- package/docs/aw-timeline-builder.md +0 -50
- package/docs/aw-toggler.md +0 -41
- package/docs/aw-uploader-files.md +0 -20
- package/docs/aw-uploader.md +0 -60
- package/docs/aw-user-menu.md +0 -34
- package/docs/aw-userpic.md +0 -34
- /package/components/{3_organisms → 2_molecules}/AwTel.vue +0 -0
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
# Pages with Aside Sidebar Pattern
|
|
2
|
+
|
|
3
|
+
Complete guide to building pages with persistent sidebar using AwPageAside for forms, configuration, and detail pages with contextual information.
|
|
4
|
+
|
|
5
|
+
## When to Use AwPageAside
|
|
6
|
+
|
|
7
|
+
Use `AwPageAside` for:
|
|
8
|
+
- **Edit pages with summary** - Forms with pricing, totals, or status sidebar
|
|
9
|
+
- **Configuration pages** - Settings with marketing/info in sidebar
|
|
10
|
+
- **Booking/order pages** - Main content with summary and actions in sidebar
|
|
11
|
+
- **Detail pages with actions** - View details with related info and quick actions
|
|
12
|
+
- **Complex workflows** - Multi-step processes with progress or help in sidebar
|
|
13
|
+
|
|
14
|
+
**Key characteristics:**
|
|
15
|
+
- Fixed sidebar on desktop (right side)
|
|
16
|
+
- Responsive layout (sidebar becomes card on mobile)
|
|
17
|
+
- Sticky action buttons at bottom of sidebar
|
|
18
|
+
- Pass-through support for all AwPage props
|
|
19
|
+
- `isDesktop` prop available in slots for responsive content
|
|
20
|
+
|
|
21
|
+
## Basic Page with Aside
|
|
22
|
+
|
|
23
|
+
### Minimal Example
|
|
24
|
+
|
|
25
|
+
```markup
|
|
26
|
+
<template>
|
|
27
|
+
<AwPageAside title="Edit Booking">
|
|
28
|
+
<template #default>
|
|
29
|
+
<AwCard title="Booking Details">
|
|
30
|
+
<AwGrid>
|
|
31
|
+
<AwDate
|
|
32
|
+
v-model="booking.date"
|
|
33
|
+
label="Date"
|
|
34
|
+
:error="booking.errors.date"
|
|
35
|
+
required
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<AwInput
|
|
39
|
+
v-model="booking.client_name"
|
|
40
|
+
label="Client Name"
|
|
41
|
+
:error="booking.errors.client_name"
|
|
42
|
+
required
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<AwSelect
|
|
46
|
+
v-model="booking.service_id"
|
|
47
|
+
:options="services"
|
|
48
|
+
track-by="id"
|
|
49
|
+
option-text="name"
|
|
50
|
+
label="Service"
|
|
51
|
+
:error="booking.errors.service_id"
|
|
52
|
+
required
|
|
53
|
+
/>
|
|
54
|
+
</AwGrid>
|
|
55
|
+
</AwCard>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #aside>
|
|
59
|
+
<h3 class="text-lg font-semibold mb-4">Summary</h3>
|
|
60
|
+
<div class="space-y-4">
|
|
61
|
+
<div class="flex justify-between">
|
|
62
|
+
<span class="text-secondary">Service</span>
|
|
63
|
+
<span class="font-semibold">{{ selectedService?.name }}</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="flex justify-between">
|
|
66
|
+
<span class="text-secondary">Price</span>
|
|
67
|
+
<span class="font-semibold">{{ formatPrice(selectedService?.price) }}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<hr />
|
|
70
|
+
<div class="flex justify-between text-lg">
|
|
71
|
+
<span class="font-semibold">Total</span>
|
|
72
|
+
<span class="font-bold text-accent">{{ formatPrice(total) }}</span>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<template #aside-buttons>
|
|
78
|
+
<AwButton
|
|
79
|
+
@click="save"
|
|
80
|
+
:loading="booking.saving"
|
|
81
|
+
cta
|
|
82
|
+
block
|
|
83
|
+
>
|
|
84
|
+
Save Booking
|
|
85
|
+
</AwButton>
|
|
86
|
+
</template>
|
|
87
|
+
</AwPageAside>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<script>
|
|
91
|
+
import Booking from '~/models/Booking'
|
|
92
|
+
|
|
93
|
+
export default {
|
|
94
|
+
middleware: 'auth',
|
|
95
|
+
|
|
96
|
+
data() {
|
|
97
|
+
return {
|
|
98
|
+
booking: new Booking(
|
|
99
|
+
{ id: this.$route.params.id },
|
|
100
|
+
null,
|
|
101
|
+
{ shop_uuid: this.$route.params.shop_uuid }
|
|
102
|
+
),
|
|
103
|
+
services: []
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
computed: {
|
|
108
|
+
selectedService() {
|
|
109
|
+
return this.services.find(s => s.id === this.booking.service_id)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
total() {
|
|
113
|
+
return this.selectedService?.price || 0
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async mounted() {
|
|
118
|
+
await this.loadServices()
|
|
119
|
+
|
|
120
|
+
if (!this.booking.isNew()) {
|
|
121
|
+
await this.booking.fetch()
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
methods: {
|
|
126
|
+
async loadServices() {
|
|
127
|
+
const shopUuid = this.$route.params.shop_uuid
|
|
128
|
+
const response = await this.$axios.get(`/api/shops/${shopUuid}/services`)
|
|
129
|
+
this.services = response.data.data
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
formatPrice(price) {
|
|
133
|
+
return new Intl.NumberFormat('en-US', {
|
|
134
|
+
style: 'currency',
|
|
135
|
+
currency: 'USD'
|
|
136
|
+
}).format(price || 0)
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
async save() {
|
|
140
|
+
try {
|
|
141
|
+
await this.booking.save()
|
|
142
|
+
|
|
143
|
+
if (Object.keys(this.booking.errors).length > 0) {
|
|
144
|
+
this.$notify({
|
|
145
|
+
message: 'Please fix validation errors',
|
|
146
|
+
type: 'error'
|
|
147
|
+
})
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.$notify({
|
|
152
|
+
message: 'Booking saved successfully',
|
|
153
|
+
type: 'success'
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
this.$router.push(`/${this.$route.params.shop_uuid}/bookings`)
|
|
157
|
+
} catch (error) {
|
|
158
|
+
this.$notify({
|
|
159
|
+
message: 'Failed to save booking',
|
|
160
|
+
type: 'error'
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
cancel() {
|
|
166
|
+
this.$router.back()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
</script>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**What happens:**
|
|
174
|
+
1. ✅ Main content area shows booking form
|
|
175
|
+
2. ✅ Aside shows pricing summary
|
|
176
|
+
3. ✅ Action buttons fixed at bottom of aside
|
|
177
|
+
4. ✅ Responsive: sidebar becomes card below content on mobile
|
|
178
|
+
5. ✅ Total updates when service changes
|
|
179
|
+
|
|
180
|
+
## Common Patterns
|
|
181
|
+
|
|
182
|
+
### 1. Booking/Order Page with Dynamic Summary
|
|
183
|
+
|
|
184
|
+
Form with services, client info, and total in sidebar:
|
|
185
|
+
|
|
186
|
+
```markup
|
|
187
|
+
<template>
|
|
188
|
+
<AwPageAside
|
|
189
|
+
title="New Booking"
|
|
190
|
+
:breadcrumb="{ href: `/bookings`, title: 'Bookings' }"
|
|
191
|
+
>
|
|
192
|
+
<template #default>
|
|
193
|
+
<!-- Service Selection -->
|
|
194
|
+
<AwCard title="Services">
|
|
195
|
+
<div class="space-y-4">
|
|
196
|
+
<div
|
|
197
|
+
v-for="service in services"
|
|
198
|
+
:key="service.id"
|
|
199
|
+
class="border rounded-lg p-4 cursor-pointer hover:border-accent"
|
|
200
|
+
:class="{ 'border-accent bg-accent-50': isSelected(service) }"
|
|
201
|
+
@click="toggleService(service)"
|
|
202
|
+
>
|
|
203
|
+
<div class="flex justify-between items-start">
|
|
204
|
+
<div>
|
|
205
|
+
<h3 class="font-semibold">{{ service.name }}</h3>
|
|
206
|
+
<p class="text-sm text-secondary">{{ service.description }}</p>
|
|
207
|
+
<p class="text-sm text-secondary mt-1">{{ service.duration }} min</p>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="text-right">
|
|
210
|
+
<p class="font-semibold text-accent">{{ formatPrice(service.price) }}</p>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</AwCard>
|
|
216
|
+
|
|
217
|
+
<!-- Client Information -->
|
|
218
|
+
<AwCard title="Client Information">
|
|
219
|
+
<AwGrid>
|
|
220
|
+
<AwInput
|
|
221
|
+
v-model="booking.client_name"
|
|
222
|
+
label="Name"
|
|
223
|
+
:error="booking.errors.client_name"
|
|
224
|
+
required
|
|
225
|
+
/>
|
|
226
|
+
|
|
227
|
+
<AwInput
|
|
228
|
+
v-model="booking.client_email"
|
|
229
|
+
label="Email"
|
|
230
|
+
type="email"
|
|
231
|
+
:error="booking.errors.client_email"
|
|
232
|
+
required
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
<AwTel
|
|
236
|
+
v-model="booking.client_phone"
|
|
237
|
+
label="Phone"
|
|
238
|
+
:error="booking.errors.client_phone"
|
|
239
|
+
/>
|
|
240
|
+
</AwGrid>
|
|
241
|
+
</AwCard>
|
|
242
|
+
|
|
243
|
+
<!-- Date & Time -->
|
|
244
|
+
<AwCard title="Date & Time">
|
|
245
|
+
<AwGrid>
|
|
246
|
+
<AwDate
|
|
247
|
+
v-model="booking.date"
|
|
248
|
+
label="Date"
|
|
249
|
+
:error="booking.errors.date"
|
|
250
|
+
required
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
<AwSelect
|
|
254
|
+
v-model="booking.time_slot"
|
|
255
|
+
:options="availableSlots"
|
|
256
|
+
label="Time"
|
|
257
|
+
:error="booking.errors.time_slot"
|
|
258
|
+
required
|
|
259
|
+
/>
|
|
260
|
+
</AwGrid>
|
|
261
|
+
</AwCard>
|
|
262
|
+
|
|
263
|
+
<!-- Notes -->
|
|
264
|
+
<AwCard title="Additional Notes">
|
|
265
|
+
<AwTextarea
|
|
266
|
+
v-model="booking.notes"
|
|
267
|
+
label="Notes"
|
|
268
|
+
:error="booking.errors.notes"
|
|
269
|
+
:rows="4"
|
|
270
|
+
placeholder="Any special requests or notes..."
|
|
271
|
+
/>
|
|
272
|
+
</AwCard>
|
|
273
|
+
</template>
|
|
274
|
+
|
|
275
|
+
<template #aside>
|
|
276
|
+
<h3 class="text-lg font-semibold mb-4">Booking Summary</h3>
|
|
277
|
+
|
|
278
|
+
<!-- Client Info -->
|
|
279
|
+
<div class="mb-6">
|
|
280
|
+
<h4 class="text-sm font-semibold text-secondary mb-2">Client</h4>
|
|
281
|
+
<p class="font-medium">{{ booking.client_name || 'Not specified' }}</p>
|
|
282
|
+
<p class="text-sm text-secondary">{{ booking.client_email || '-' }}</p>
|
|
283
|
+
<p class="text-sm text-secondary">{{ booking.client_phone || '-' }}</p>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<!-- Date & Time -->
|
|
287
|
+
<div class="mb-6" v-if="booking.date">
|
|
288
|
+
<h4 class="text-sm font-semibold text-secondary mb-2">Date & Time</h4>
|
|
289
|
+
<p class="font-medium">{{ formatDate(booking.date) }}</p>
|
|
290
|
+
<p class="text-sm text-secondary">{{ booking.time_slot || '-' }}</p>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<!-- Selected Services -->
|
|
294
|
+
<div class="mb-6">
|
|
295
|
+
<h4 class="text-sm font-semibold text-secondary mb-2">Services</h4>
|
|
296
|
+
<div v-if="selectedServices.length === 0" class="text-sm text-secondary">
|
|
297
|
+
No services selected
|
|
298
|
+
</div>
|
|
299
|
+
<div v-else class="space-y-3">
|
|
300
|
+
<div
|
|
301
|
+
v-for="service in selectedServices"
|
|
302
|
+
:key="service.id"
|
|
303
|
+
class="flex justify-between items-start"
|
|
304
|
+
>
|
|
305
|
+
<div class="flex-1">
|
|
306
|
+
<p class="font-medium">{{ service.name }}</p>
|
|
307
|
+
<p class="text-xs text-secondary">{{ service.duration }} min</p>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="text-right">
|
|
310
|
+
<p class="font-medium">{{ formatPrice(service.price) }}</p>
|
|
311
|
+
<button
|
|
312
|
+
@click="removeService(service)"
|
|
313
|
+
class="text-xs text-error hover:underline"
|
|
314
|
+
>
|
|
315
|
+
Remove
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<!-- Totals -->
|
|
323
|
+
<hr class="my-4" />
|
|
324
|
+
<div class="space-y-2">
|
|
325
|
+
<div class="flex justify-between text-sm">
|
|
326
|
+
<span class="text-secondary">Subtotal</span>
|
|
327
|
+
<span>{{ formatPrice(subtotal) }}</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="flex justify-between text-sm">
|
|
330
|
+
<span class="text-secondary">Duration</span>
|
|
331
|
+
<span>{{ totalDuration }} min</span>
|
|
332
|
+
</div>
|
|
333
|
+
<hr class="my-2" />
|
|
334
|
+
<div class="flex justify-between items-center">
|
|
335
|
+
<span class="text-lg font-semibold">Total</span>
|
|
336
|
+
<span class="text-2xl font-bold text-accent">{{ formatPrice(total) }}</span>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</template>
|
|
340
|
+
|
|
341
|
+
<template #aside-buttons>
|
|
342
|
+
<AwButton
|
|
343
|
+
@click="save"
|
|
344
|
+
:loading="booking.saving"
|
|
345
|
+
:disabled="!canSave"
|
|
346
|
+
cta
|
|
347
|
+
block
|
|
348
|
+
>
|
|
349
|
+
Confirm Booking
|
|
350
|
+
</AwButton>
|
|
351
|
+
<AwButton
|
|
352
|
+
@click="cancel"
|
|
353
|
+
theme="outline"
|
|
354
|
+
block
|
|
355
|
+
>
|
|
356
|
+
Cancel
|
|
357
|
+
</AwButton>
|
|
358
|
+
</template>
|
|
359
|
+
</AwPageAside>
|
|
360
|
+
</template>
|
|
361
|
+
|
|
362
|
+
<script>
|
|
363
|
+
import Booking from '~/models/Booking'
|
|
364
|
+
|
|
365
|
+
export default {
|
|
366
|
+
middleware: 'auth',
|
|
367
|
+
|
|
368
|
+
data() {
|
|
369
|
+
return {
|
|
370
|
+
booking: new Booking({}, null, {
|
|
371
|
+
shop_uuid: this.$route.params.shop_uuid
|
|
372
|
+
}),
|
|
373
|
+
services: [],
|
|
374
|
+
selectedServiceIds: [],
|
|
375
|
+
availableSlots: []
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
computed: {
|
|
380
|
+
selectedServices() {
|
|
381
|
+
return this.services.filter(s => this.selectedServiceIds.includes(s.id))
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
subtotal() {
|
|
385
|
+
return this.selectedServices.reduce((sum, s) => sum + s.price, 0)
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
total() {
|
|
389
|
+
return this.subtotal
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
totalDuration() {
|
|
393
|
+
return this.selectedServices.reduce((sum, s) => sum + s.duration, 0)
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
canSave() {
|
|
397
|
+
return this.selectedServices.length > 0 &&
|
|
398
|
+
this.booking.client_name &&
|
|
399
|
+
this.booking.client_email &&
|
|
400
|
+
this.booking.date &&
|
|
401
|
+
this.booking.time_slot
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
async mounted() {
|
|
406
|
+
await this.loadServices()
|
|
407
|
+
await this.loadAvailableSlots()
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
methods: {
|
|
411
|
+
async loadServices() {
|
|
412
|
+
const shopUuid = this.$route.params.shop_uuid
|
|
413
|
+
const response = await this.$axios.get(`/api/shops/${shopUuid}/services`)
|
|
414
|
+
this.services = response.data.data
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
async loadAvailableSlots() {
|
|
418
|
+
const shopUuid = this.$route.params.shop_uuid
|
|
419
|
+
const response = await this.$axios.get(`/api/shops/${shopUuid}/time-slots`)
|
|
420
|
+
this.availableSlots = response.data.data
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
isSelected(service) {
|
|
424
|
+
return this.selectedServiceIds.includes(service.id)
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
toggleService(service) {
|
|
428
|
+
const index = this.selectedServiceIds.indexOf(service.id)
|
|
429
|
+
if (index > -1) {
|
|
430
|
+
this.selectedServiceIds.splice(index, 1)
|
|
431
|
+
} else {
|
|
432
|
+
this.selectedServiceIds.push(service.id)
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
removeService(service) {
|
|
437
|
+
const index = this.selectedServiceIds.indexOf(service.id)
|
|
438
|
+
if (index > -1) {
|
|
439
|
+
this.selectedServiceIds.splice(index, 1)
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
formatPrice(price) {
|
|
444
|
+
return new Intl.NumberFormat('en-US', {
|
|
445
|
+
style: 'currency',
|
|
446
|
+
currency: 'USD'
|
|
447
|
+
}).format(price || 0)
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
formatDate(date) {
|
|
451
|
+
return this.$dayjs(date).format('MMMM D, YYYY')
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
async save() {
|
|
455
|
+
this.booking.service_ids = this.selectedServiceIds
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
await this.booking.save()
|
|
459
|
+
|
|
460
|
+
if (Object.keys(this.booking.errors).length > 0) {
|
|
461
|
+
this.$notify({
|
|
462
|
+
message: 'Please fix validation errors',
|
|
463
|
+
type: 'error'
|
|
464
|
+
})
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.$notify({
|
|
469
|
+
message: 'Booking created successfully',
|
|
470
|
+
type: 'success'
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
this.$router.push(`/${this.$route.params.shop_uuid}/bookings`)
|
|
474
|
+
} catch (error) {
|
|
475
|
+
this.$notify({
|
|
476
|
+
message: 'Failed to create booking',
|
|
477
|
+
type: 'error'
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
cancel() {
|
|
483
|
+
this.$router.back()
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
</script>
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 2. Widget Configuration with Marketing Info
|
|
491
|
+
|
|
492
|
+
Configuration form with promotional content in sidebar:
|
|
493
|
+
|
|
494
|
+
```markup
|
|
495
|
+
<template>
|
|
496
|
+
<AwPageAside
|
|
497
|
+
title="Configure Widget"
|
|
498
|
+
:breadcrumb="{ href: `/widgets`, title: 'Widgets' }"
|
|
499
|
+
>
|
|
500
|
+
<template #default>
|
|
501
|
+
<!-- Widget Preview -->
|
|
502
|
+
<AwCard title="Widget Preview">
|
|
503
|
+
<div class="border rounded-lg p-6 bg-gray-50">
|
|
504
|
+
<div
|
|
505
|
+
class="widget-preview"
|
|
506
|
+
:style="{
|
|
507
|
+
backgroundColor: widget.background_color,
|
|
508
|
+
color: widget.text_color,
|
|
509
|
+
fontSize: `${widget.font_size}px`
|
|
510
|
+
}"
|
|
511
|
+
>
|
|
512
|
+
<h3>{{ widget.title || 'Widget Title' }}</h3>
|
|
513
|
+
<p>{{ widget.message || 'Widget message will appear here' }}</p>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
</AwCard>
|
|
517
|
+
|
|
518
|
+
<!-- Appearance Settings -->
|
|
519
|
+
<AwCard title="Appearance">
|
|
520
|
+
<AwGrid>
|
|
521
|
+
<AwInput
|
|
522
|
+
v-model="widget.title"
|
|
523
|
+
label="Title"
|
|
524
|
+
:error="widget.errors.title"
|
|
525
|
+
required
|
|
526
|
+
/>
|
|
527
|
+
|
|
528
|
+
<AwTextarea
|
|
529
|
+
v-model="widget.message"
|
|
530
|
+
label="Message"
|
|
531
|
+
:error="widget.errors.message"
|
|
532
|
+
:rows="3"
|
|
533
|
+
required
|
|
534
|
+
/>
|
|
535
|
+
|
|
536
|
+
<AwInput
|
|
537
|
+
v-model="widget.background_color"
|
|
538
|
+
label="Background Color"
|
|
539
|
+
type="color"
|
|
540
|
+
:error="widget.errors.background_color"
|
|
541
|
+
/>
|
|
542
|
+
|
|
543
|
+
<AwInput
|
|
544
|
+
v-model="widget.text_color"
|
|
545
|
+
label="Text Color"
|
|
546
|
+
type="color"
|
|
547
|
+
:error="widget.errors.text_color"
|
|
548
|
+
/>
|
|
549
|
+
|
|
550
|
+
<AwSlider
|
|
551
|
+
v-model="widget.font_size"
|
|
552
|
+
label="Font Size"
|
|
553
|
+
:min="12"
|
|
554
|
+
:max="24"
|
|
555
|
+
:error="widget.errors.font_size"
|
|
556
|
+
/>
|
|
557
|
+
</AwGrid>
|
|
558
|
+
</AwCard>
|
|
559
|
+
|
|
560
|
+
<!-- Behavior Settings -->
|
|
561
|
+
<AwCard title="Behavior">
|
|
562
|
+
<AwGrid>
|
|
563
|
+
<AwSelect
|
|
564
|
+
v-model="widget.position"
|
|
565
|
+
:options="['top-left', 'top-right', 'bottom-left', 'bottom-right']"
|
|
566
|
+
label="Position"
|
|
567
|
+
:error="widget.errors.position"
|
|
568
|
+
/>
|
|
569
|
+
|
|
570
|
+
<AwNumber
|
|
571
|
+
v-model="widget.delay"
|
|
572
|
+
label="Delay (seconds)"
|
|
573
|
+
:min="0"
|
|
574
|
+
:max="60"
|
|
575
|
+
:error="widget.errors.delay"
|
|
576
|
+
/>
|
|
577
|
+
|
|
578
|
+
<AwSwitcher
|
|
579
|
+
v-model="widget.show_close_button"
|
|
580
|
+
label="Show Close Button"
|
|
581
|
+
/>
|
|
582
|
+
|
|
583
|
+
<AwSwitcher
|
|
584
|
+
v-model="widget.auto_hide"
|
|
585
|
+
label="Auto Hide"
|
|
586
|
+
/>
|
|
587
|
+
|
|
588
|
+
<AwNumber
|
|
589
|
+
v-if="widget.auto_hide"
|
|
590
|
+
v-model="widget.auto_hide_delay"
|
|
591
|
+
label="Auto Hide Delay (seconds)"
|
|
592
|
+
:min="1"
|
|
593
|
+
:max="60"
|
|
594
|
+
:error="widget.errors.auto_hide_delay"
|
|
595
|
+
/>
|
|
596
|
+
</AwGrid>
|
|
597
|
+
</AwCard>
|
|
598
|
+
|
|
599
|
+
<!-- Targeting -->
|
|
600
|
+
<AwCard title="Targeting">
|
|
601
|
+
<AwGrid>
|
|
602
|
+
<AwSelect
|
|
603
|
+
v-model="widget.pages"
|
|
604
|
+
:options="pageOptions"
|
|
605
|
+
label="Show on Pages"
|
|
606
|
+
multiple
|
|
607
|
+
:error="widget.errors.pages"
|
|
608
|
+
/>
|
|
609
|
+
|
|
610
|
+
<AwSelect
|
|
611
|
+
v-model="widget.devices"
|
|
612
|
+
:options="['desktop', 'tablet', 'mobile']"
|
|
613
|
+
label="Show on Devices"
|
|
614
|
+
multiple
|
|
615
|
+
:error="widget.errors.devices"
|
|
616
|
+
/>
|
|
617
|
+
</AwGrid>
|
|
618
|
+
</AwCard>
|
|
619
|
+
</template>
|
|
620
|
+
|
|
621
|
+
<template #aside>
|
|
622
|
+
<!-- Marketing Info -->
|
|
623
|
+
<div class="mb-6">
|
|
624
|
+
<h3 class="text-lg font-semibold mb-4">Why Use Widgets?</h3>
|
|
625
|
+
<div class="space-y-4 text-sm">
|
|
626
|
+
<div class="flex items-start gap-3">
|
|
627
|
+
<AwIcon name="awesio/check-circle" class="text-success mt-0.5" />
|
|
628
|
+
<div>
|
|
629
|
+
<h4 class="font-semibold mb-1">Increase Engagement</h4>
|
|
630
|
+
<p class="text-secondary">
|
|
631
|
+
Capture visitor attention with timely, personalized messages
|
|
632
|
+
</p>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div class="flex items-start gap-3">
|
|
637
|
+
<AwIcon name="awesio/check-circle" class="text-success mt-0.5" />
|
|
638
|
+
<div>
|
|
639
|
+
<h4 class="font-semibold mb-1">Boost Conversions</h4>
|
|
640
|
+
<p class="text-secondary">
|
|
641
|
+
Drive actions with targeted calls-to-action and special offers
|
|
642
|
+
</p>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<div class="flex items-start gap-3">
|
|
647
|
+
<AwIcon name="awesio/check-circle" class="text-success mt-0.5" />
|
|
648
|
+
<div>
|
|
649
|
+
<h4 class="font-semibold mb-1">Easy to Customize</h4>
|
|
650
|
+
<p class="text-secondary">
|
|
651
|
+
Match your brand with flexible design options
|
|
652
|
+
</p>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
<hr class="my-4" />
|
|
657
|
+
|
|
658
|
+
<div class="bg-accent-50 rounded-lg p-4">
|
|
659
|
+
<h4 class="font-semibold text-accent mb-2">Pro Tip</h4>
|
|
660
|
+
<p class="text-secondary text-xs">
|
|
661
|
+
Widgets with a delay of 3-5 seconds have 40% higher engagement
|
|
662
|
+
than immediate popups.
|
|
663
|
+
</p>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
</div>
|
|
667
|
+
|
|
668
|
+
<!-- Stats (if editing) -->
|
|
669
|
+
<div v-if="!widget.isNew()">
|
|
670
|
+
<h3 class="text-lg font-semibold mb-4">Performance</h3>
|
|
671
|
+
<div class="space-y-3">
|
|
672
|
+
<div class="flex justify-between items-center">
|
|
673
|
+
<span class="text-sm text-secondary">Impressions</span>
|
|
674
|
+
<span class="font-semibold">{{ widget.stats?.impressions || 0 }}</span>
|
|
675
|
+
</div>
|
|
676
|
+
<div class="flex justify-between items-center">
|
|
677
|
+
<span class="text-sm text-secondary">Clicks</span>
|
|
678
|
+
<span class="font-semibold">{{ widget.stats?.clicks || 0 }}</span>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="flex justify-between items-center">
|
|
681
|
+
<span class="text-sm text-secondary">Conversion Rate</span>
|
|
682
|
+
<span class="font-semibold text-accent">
|
|
683
|
+
{{ formatPercentage(widget.stats?.conversion_rate) }}
|
|
684
|
+
</span>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
</div>
|
|
688
|
+
</template>
|
|
689
|
+
|
|
690
|
+
<template #aside-buttons>
|
|
691
|
+
<AwButton
|
|
692
|
+
@click="save"
|
|
693
|
+
:loading="widget.saving"
|
|
694
|
+
cta
|
|
695
|
+
block
|
|
696
|
+
>
|
|
697
|
+
{{ widget.isNew() ? 'Create Widget' : 'Update Widget' }}
|
|
698
|
+
</AwButton>
|
|
699
|
+
<AwButton
|
|
700
|
+
v-if="!widget.isNew()"
|
|
701
|
+
@click="testWidget"
|
|
702
|
+
theme="outline"
|
|
703
|
+
block
|
|
704
|
+
>
|
|
705
|
+
Test Widget
|
|
706
|
+
</AwButton>
|
|
707
|
+
</template>
|
|
708
|
+
</AwPageAside>
|
|
709
|
+
</template>
|
|
710
|
+
|
|
711
|
+
<script>
|
|
712
|
+
import Widget from '~/models/Widget'
|
|
713
|
+
|
|
714
|
+
export default {
|
|
715
|
+
middleware: 'auth',
|
|
716
|
+
|
|
717
|
+
data() {
|
|
718
|
+
return {
|
|
719
|
+
widget: new Widget(
|
|
720
|
+
{ id: this.$route.params.id },
|
|
721
|
+
null,
|
|
722
|
+
{ shop_uuid: this.$route.params.shop_uuid }
|
|
723
|
+
),
|
|
724
|
+
pageOptions: [
|
|
725
|
+
'home',
|
|
726
|
+
'products',
|
|
727
|
+
'checkout',
|
|
728
|
+
'cart',
|
|
729
|
+
'account'
|
|
730
|
+
]
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
async mounted() {
|
|
735
|
+
if (!this.widget.isNew()) {
|
|
736
|
+
await this.widget.fetch()
|
|
737
|
+
} else {
|
|
738
|
+
// Set defaults for new widget
|
|
739
|
+
this.widget.background_color = '#3B82F6'
|
|
740
|
+
this.widget.text_color = '#FFFFFF'
|
|
741
|
+
this.widget.font_size = 16
|
|
742
|
+
this.widget.position = 'bottom-right'
|
|
743
|
+
this.widget.delay = 3
|
|
744
|
+
this.widget.show_close_button = true
|
|
745
|
+
this.widget.devices = ['desktop', 'tablet', 'mobile']
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
methods: {
|
|
750
|
+
async save() {
|
|
751
|
+
try {
|
|
752
|
+
await this.widget.save()
|
|
753
|
+
|
|
754
|
+
if (Object.keys(this.widget.errors).length > 0) {
|
|
755
|
+
this.$notify({
|
|
756
|
+
message: 'Please fix validation errors',
|
|
757
|
+
type: 'error'
|
|
758
|
+
})
|
|
759
|
+
return
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
this.$notify({
|
|
763
|
+
message: `Widget ${this.widget.isNew() ? 'created' : 'updated'} successfully`,
|
|
764
|
+
type: 'success'
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
this.$router.push(`/${this.$route.params.shop_uuid}/widgets`)
|
|
768
|
+
} catch (error) {
|
|
769
|
+
this.$notify({
|
|
770
|
+
message: 'Failed to save widget',
|
|
771
|
+
type: 'error'
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
testWidget() {
|
|
777
|
+
// Open test preview in new window
|
|
778
|
+
const url = `/${this.$route.params.shop_uuid}/widgets/${this.widget.id}/preview`
|
|
779
|
+
window.open(url, '_blank')
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
formatPercentage(value) {
|
|
783
|
+
return `${(value || 0).toFixed(1)}%`
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
</script>
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### 3. Edit Form with Dynamic Aside Content
|
|
791
|
+
|
|
792
|
+
Form where aside content changes based on interaction:
|
|
793
|
+
|
|
794
|
+
```markup
|
|
795
|
+
<template>
|
|
796
|
+
<AwPageAside title="Edit Product">
|
|
797
|
+
<template #default>
|
|
798
|
+
<AwCard title="Product Details">
|
|
799
|
+
<AwGrid>
|
|
800
|
+
<AwInput
|
|
801
|
+
v-model="product.name"
|
|
802
|
+
label="Name"
|
|
803
|
+
:error="product.errors.name"
|
|
804
|
+
required
|
|
805
|
+
/>
|
|
806
|
+
|
|
807
|
+
<AwInput
|
|
808
|
+
v-model="product.sku"
|
|
809
|
+
label="SKU"
|
|
810
|
+
:error="product.errors.sku"
|
|
811
|
+
/>
|
|
812
|
+
|
|
813
|
+
<AwSelect
|
|
814
|
+
v-model="product.category_id"
|
|
815
|
+
:options="categories"
|
|
816
|
+
track-by="id"
|
|
817
|
+
option-text="name"
|
|
818
|
+
label="Category"
|
|
819
|
+
:error="product.errors.category_id"
|
|
820
|
+
@input="onCategoryChange"
|
|
821
|
+
/>
|
|
822
|
+
|
|
823
|
+
<AwTextarea
|
|
824
|
+
v-model="product.description"
|
|
825
|
+
label="Description"
|
|
826
|
+
:error="product.errors.description"
|
|
827
|
+
:rows="4"
|
|
828
|
+
class="col-span-2"
|
|
829
|
+
/>
|
|
830
|
+
</AwGrid>
|
|
831
|
+
</AwCard>
|
|
832
|
+
|
|
833
|
+
<AwCard title="Pricing">
|
|
834
|
+
<AwGrid>
|
|
835
|
+
<AwMoney
|
|
836
|
+
v-model="product.price"
|
|
837
|
+
label="Price"
|
|
838
|
+
:error="product.errors.price"
|
|
839
|
+
required
|
|
840
|
+
@input="onPriceChange"
|
|
841
|
+
/>
|
|
842
|
+
|
|
843
|
+
<AwMoney
|
|
844
|
+
v-model="product.cost"
|
|
845
|
+
label="Cost"
|
|
846
|
+
:error="product.errors.cost"
|
|
847
|
+
@input="onPriceChange"
|
|
848
|
+
/>
|
|
849
|
+
</AwGrid>
|
|
850
|
+
</AwCard>
|
|
851
|
+
</template>
|
|
852
|
+
|
|
853
|
+
<template #aside>
|
|
854
|
+
<!-- Dynamic aside based on editMode -->
|
|
855
|
+
<div v-if="editMode === null">
|
|
856
|
+
<h3 class="text-lg font-semibold mb-4">Product Info</h3>
|
|
857
|
+
<div class="space-y-4">
|
|
858
|
+
<div>
|
|
859
|
+
<span class="text-sm text-secondary">Category</span>
|
|
860
|
+
<p class="font-medium">{{ selectedCategory?.name || '-' }}</p>
|
|
861
|
+
<AwButton
|
|
862
|
+
@click="editMode = 'category'"
|
|
863
|
+
theme="text"
|
|
864
|
+
size="sm"
|
|
865
|
+
class="mt-1"
|
|
866
|
+
>
|
|
867
|
+
Change Category
|
|
868
|
+
</AwButton>
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
<hr />
|
|
872
|
+
|
|
873
|
+
<div>
|
|
874
|
+
<span class="text-sm text-secondary">Pricing</span>
|
|
875
|
+
<div class="mt-2 space-y-2">
|
|
876
|
+
<div class="flex justify-between">
|
|
877
|
+
<span class="text-sm">Price</span>
|
|
878
|
+
<span class="font-medium">{{ formatPrice(product.price) }}</span>
|
|
879
|
+
</div>
|
|
880
|
+
<div class="flex justify-between">
|
|
881
|
+
<span class="text-sm">Cost</span>
|
|
882
|
+
<span class="font-medium">{{ formatPrice(product.cost) }}</span>
|
|
883
|
+
</div>
|
|
884
|
+
<div class="flex justify-between text-accent">
|
|
885
|
+
<span class="text-sm font-semibold">Profit</span>
|
|
886
|
+
<span class="font-semibold">{{ formatPrice(profit) }}</span>
|
|
887
|
+
</div>
|
|
888
|
+
<div class="flex justify-between">
|
|
889
|
+
<span class="text-sm">Margin</span>
|
|
890
|
+
<span class="font-medium">{{ marginPercentage }}%</span>
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
893
|
+
</div>
|
|
894
|
+
|
|
895
|
+
<hr />
|
|
896
|
+
|
|
897
|
+
<div>
|
|
898
|
+
<span class="text-sm text-secondary">Status</span>
|
|
899
|
+
<div class="mt-2">
|
|
900
|
+
<AwLabel :color="product.is_active ? 'success' : 'mono'">
|
|
901
|
+
{{ product.is_active ? 'Active' : 'Inactive' }}
|
|
902
|
+
</AwLabel>
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<!-- Category Edit Mode -->
|
|
909
|
+
<div v-else-if="editMode === 'category'">
|
|
910
|
+
<h3 class="text-lg font-semibold mb-4">Select Category</h3>
|
|
911
|
+
<div class="space-y-2">
|
|
912
|
+
<div
|
|
913
|
+
v-for="category in categories"
|
|
914
|
+
:key="category.id"
|
|
915
|
+
class="p-3 border rounded-lg cursor-pointer hover:border-accent"
|
|
916
|
+
:class="{
|
|
917
|
+
'border-accent bg-accent-50': product.category_id === category.id
|
|
918
|
+
}"
|
|
919
|
+
@click="selectCategory(category)"
|
|
920
|
+
>
|
|
921
|
+
<div class="font-medium">{{ category.name }}</div>
|
|
922
|
+
<div class="text-xs text-secondary">{{ category.description }}</div>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
</template>
|
|
927
|
+
|
|
928
|
+
<template #aside-buttons>
|
|
929
|
+
<AwButton
|
|
930
|
+
v-if="editMode === null"
|
|
931
|
+
@click="save"
|
|
932
|
+
:loading="product.saving"
|
|
933
|
+
cta
|
|
934
|
+
block
|
|
935
|
+
>
|
|
936
|
+
Save Changes
|
|
937
|
+
</AwButton>
|
|
938
|
+
<AwButton
|
|
939
|
+
v-else
|
|
940
|
+
@click="editMode = null"
|
|
941
|
+
theme="outline"
|
|
942
|
+
block
|
|
943
|
+
>
|
|
944
|
+
Done
|
|
945
|
+
</AwButton>
|
|
946
|
+
</template>
|
|
947
|
+
</AwPageAside>
|
|
948
|
+
</template>
|
|
949
|
+
|
|
950
|
+
<script>
|
|
951
|
+
import Product from '~/models/Product'
|
|
952
|
+
|
|
953
|
+
export default {
|
|
954
|
+
middleware: 'auth',
|
|
955
|
+
|
|
956
|
+
data() {
|
|
957
|
+
return {
|
|
958
|
+
product: new Product(
|
|
959
|
+
{ id: this.$route.params.id },
|
|
960
|
+
null,
|
|
961
|
+
{ shop_uuid: this.$route.params.shop_uuid }
|
|
962
|
+
),
|
|
963
|
+
categories: [],
|
|
964
|
+
editMode: null // null, 'category', etc.
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
computed: {
|
|
969
|
+
selectedCategory() {
|
|
970
|
+
return this.categories.find(c => c.id === this.product.category_id)
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
profit() {
|
|
974
|
+
return (this.product.price || 0) - (this.product.cost || 0)
|
|
975
|
+
},
|
|
976
|
+
|
|
977
|
+
marginPercentage() {
|
|
978
|
+
if (!this.product.price || this.product.price === 0) return 0
|
|
979
|
+
return ((this.profit / this.product.price) * 100).toFixed(1)
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
|
|
983
|
+
async mounted() {
|
|
984
|
+
await this.loadCategories()
|
|
985
|
+
if (!this.product.isNew()) {
|
|
986
|
+
await this.product.fetch()
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
methods: {
|
|
991
|
+
async loadCategories() {
|
|
992
|
+
const shopUuid = this.$route.params.shop_uuid
|
|
993
|
+
const response = await this.$axios.get(`/api/shops/${shopUuid}/categories`)
|
|
994
|
+
this.categories = response.data.data
|
|
995
|
+
},
|
|
996
|
+
|
|
997
|
+
selectCategory(category) {
|
|
998
|
+
this.product.category_id = category.id
|
|
999
|
+
this.editMode = null
|
|
1000
|
+
},
|
|
1001
|
+
|
|
1002
|
+
onCategoryChange() {
|
|
1003
|
+
// Category changed in main form
|
|
1004
|
+
},
|
|
1005
|
+
|
|
1006
|
+
onPriceChange() {
|
|
1007
|
+
// Prices changed, profit/margin will auto-update via computed
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
formatPrice(price) {
|
|
1011
|
+
return new Intl.NumberFormat('en-US', {
|
|
1012
|
+
style: 'currency',
|
|
1013
|
+
currency: 'USD'
|
|
1014
|
+
}).format(price || 0)
|
|
1015
|
+
},
|
|
1016
|
+
|
|
1017
|
+
async save() {
|
|
1018
|
+
try {
|
|
1019
|
+
await this.product.save()
|
|
1020
|
+
|
|
1021
|
+
if (Object.keys(this.product.errors).length > 0) {
|
|
1022
|
+
this.$notify({
|
|
1023
|
+
message: 'Please fix validation errors',
|
|
1024
|
+
type: 'error'
|
|
1025
|
+
})
|
|
1026
|
+
return
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
this.$notify({
|
|
1030
|
+
message: 'Product updated successfully',
|
|
1031
|
+
type: 'success'
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
this.$router.push(`/${this.$route.params.shop_uuid}/products`)
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
this.$notify({
|
|
1037
|
+
message: 'Failed to save product',
|
|
1038
|
+
type: 'error'
|
|
1039
|
+
})
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
</script>
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
## Responsive Content
|
|
1048
|
+
|
|
1049
|
+
### Using isDesktop Prop
|
|
1050
|
+
|
|
1051
|
+
All slots receive `isDesktop` prop for responsive rendering:
|
|
1052
|
+
|
|
1053
|
+
```markup
|
|
1054
|
+
<template>
|
|
1055
|
+
<AwPageAside title="Responsive Page">
|
|
1056
|
+
<template #aside="{ isDesktop }">
|
|
1057
|
+
<!-- Desktop: Full details -->
|
|
1058
|
+
<div v-if="isDesktop">
|
|
1059
|
+
<h3 class="text-lg font-semibold mb-4">Details</h3>
|
|
1060
|
+
<div class="space-y-4">
|
|
1061
|
+
<!-- Full details here -->
|
|
1062
|
+
</div>
|
|
1063
|
+
</div>
|
|
1064
|
+
|
|
1065
|
+
<!-- Mobile: Compact accordion -->
|
|
1066
|
+
<AwAccordionFold v-else title="Details">
|
|
1067
|
+
<div class="space-y-2">
|
|
1068
|
+
<!-- Compact details here -->
|
|
1069
|
+
</div>
|
|
1070
|
+
</AwAccordionFold>
|
|
1071
|
+
</template>
|
|
1072
|
+
</AwPageAside>
|
|
1073
|
+
</template>
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
### Custom Mobile Aside Wrapper
|
|
1077
|
+
|
|
1078
|
+
```markup
|
|
1079
|
+
<template>
|
|
1080
|
+
<AwPageAside title="Custom Mobile">
|
|
1081
|
+
<template #aside>
|
|
1082
|
+
<h3 class="text-lg font-semibold mb-4">Info</h3>
|
|
1083
|
+
<p>Content here</p>
|
|
1084
|
+
</template>
|
|
1085
|
+
|
|
1086
|
+
<!-- Custom wrapper for mobile aside -->
|
|
1087
|
+
<template #mobile-aside="{ isDesktop }">
|
|
1088
|
+
<div v-if="!isDesktop" class="bg-gray-50 rounded-lg p-4">
|
|
1089
|
+
<slot name="aside" />
|
|
1090
|
+
</div>
|
|
1091
|
+
</template>
|
|
1092
|
+
</AwPageAside>
|
|
1093
|
+
</template>
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
### Custom Desktop Breakpoint
|
|
1097
|
+
|
|
1098
|
+
```markup
|
|
1099
|
+
<template>
|
|
1100
|
+
<AwPageAside
|
|
1101
|
+
title="Custom Breakpoint"
|
|
1102
|
+
desktop-from="xl"
|
|
1103
|
+
>
|
|
1104
|
+
<!-- Switches to desktop layout at xl breakpoint instead of lg -->
|
|
1105
|
+
</AwPageAside>
|
|
1106
|
+
</template>
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
## Best Practices
|
|
1110
|
+
|
|
1111
|
+
### 1. Use Aside for Contextual Information
|
|
1112
|
+
|
|
1113
|
+
**Good:**
|
|
1114
|
+
```markup
|
|
1115
|
+
<!-- Summary, totals, status, quick actions -->
|
|
1116
|
+
<template #aside>
|
|
1117
|
+
<h3 class="text-lg font-semibold mb-4">Order Summary</h3>
|
|
1118
|
+
<div>Total: {{ total }}</div>
|
|
1119
|
+
</template>
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
**Avoid:**
|
|
1123
|
+
```markup
|
|
1124
|
+
<!-- Don't put primary form fields in aside -->
|
|
1125
|
+
<template #aside>
|
|
1126
|
+
<AwInput v-model="product.name" label="Name" />
|
|
1127
|
+
</template>
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
### 2. Sticky Buttons for Primary Actions
|
|
1131
|
+
|
|
1132
|
+
```markup
|
|
1133
|
+
<template #aside-buttons>
|
|
1134
|
+
<AwButton @click="save" cta block>Save</AwButton>
|
|
1135
|
+
<AwButton @click="cancel" theme="outline" block>Cancel</AwButton>
|
|
1136
|
+
</template>
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
### 3. Update Aside Reactively
|
|
1140
|
+
|
|
1141
|
+
```markup
|
|
1142
|
+
<script>
|
|
1143
|
+
export default {
|
|
1144
|
+
computed: {
|
|
1145
|
+
total() {
|
|
1146
|
+
// Reactive calculation
|
|
1147
|
+
return this.items.reduce((sum, item) => sum + item.price, 0)
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
</script>
|
|
1152
|
+
|
|
1153
|
+
<template>
|
|
1154
|
+
<template #aside>
|
|
1155
|
+
<div>Total: {{ total }}</div> <!-- Updates automatically -->
|
|
1156
|
+
</template>
|
|
1157
|
+
</template>
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
### 4. Hide Mobile Aside When Not Needed
|
|
1161
|
+
|
|
1162
|
+
```markup
|
|
1163
|
+
<AwPageAside
|
|
1164
|
+
title="Desktop Only Sidebar"
|
|
1165
|
+
hide-mobile-aside
|
|
1166
|
+
>
|
|
1167
|
+
<!-- Aside only shows on desktop -->
|
|
1168
|
+
</AwPageAside>
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
### 5. Visual Separator on Desktop
|
|
1172
|
+
|
|
1173
|
+
```markup
|
|
1174
|
+
<AwPageAside
|
|
1175
|
+
title="Page Title"
|
|
1176
|
+
modifiers="line"
|
|
1177
|
+
>
|
|
1178
|
+
<!-- Adds vertical line between main and aside on desktop -->
|
|
1179
|
+
</AwPageAside>
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
## Complete Example: Order Edit Page
|
|
1183
|
+
|
|
1184
|
+
```markup
|
|
1185
|
+
<template>
|
|
1186
|
+
<AwPageAside
|
|
1187
|
+
:title="`Order #${order.order_number || 'New'}`"
|
|
1188
|
+
:breadcrumb="{ href: '/orders', title: 'Orders' }"
|
|
1189
|
+
modifiers="line"
|
|
1190
|
+
>
|
|
1191
|
+
<template #default>
|
|
1192
|
+
<!-- Order Items -->
|
|
1193
|
+
<AwCard title="Order Items">
|
|
1194
|
+
<AwTableBuilder
|
|
1195
|
+
:collection="order.items"
|
|
1196
|
+
:fields="itemFields"
|
|
1197
|
+
/>
|
|
1198
|
+
<AwButton
|
|
1199
|
+
@click="addItem"
|
|
1200
|
+
theme="text"
|
|
1201
|
+
icon="awesio/plus"
|
|
1202
|
+
class="mt-4"
|
|
1203
|
+
>
|
|
1204
|
+
Add Item
|
|
1205
|
+
</AwButton>
|
|
1206
|
+
</AwCard>
|
|
1207
|
+
|
|
1208
|
+
<!-- Customer Information -->
|
|
1209
|
+
<AwCard title="Customer">
|
|
1210
|
+
<AwGrid>
|
|
1211
|
+
<AwInput
|
|
1212
|
+
v-model="order.customer_name"
|
|
1213
|
+
label="Name"
|
|
1214
|
+
:error="order.errors.customer_name"
|
|
1215
|
+
required
|
|
1216
|
+
/>
|
|
1217
|
+
<AwInput
|
|
1218
|
+
v-model="order.customer_email"
|
|
1219
|
+
label="Email"
|
|
1220
|
+
:error="order.errors.customer_email"
|
|
1221
|
+
required
|
|
1222
|
+
/>
|
|
1223
|
+
<AwTel
|
|
1224
|
+
v-model="order.customer_phone"
|
|
1225
|
+
label="Phone"
|
|
1226
|
+
:error="order.errors.customer_phone"
|
|
1227
|
+
/>
|
|
1228
|
+
</AwGrid>
|
|
1229
|
+
</AwCard>
|
|
1230
|
+
|
|
1231
|
+
<!-- Shipping Address -->
|
|
1232
|
+
<AwCard title="Shipping Address">
|
|
1233
|
+
<AwAddress
|
|
1234
|
+
v-model="order.shipping_address"
|
|
1235
|
+
:error="order.errors.shipping_address"
|
|
1236
|
+
/>
|
|
1237
|
+
</AwCard>
|
|
1238
|
+
</template>
|
|
1239
|
+
|
|
1240
|
+
<template #aside>
|
|
1241
|
+
<div class="mb-6">
|
|
1242
|
+
<h3 class="text-lg font-semibold mb-4">Order Summary</h3>
|
|
1243
|
+
|
|
1244
|
+
<!-- Items -->
|
|
1245
|
+
<div class="space-y-3 mb-4">
|
|
1246
|
+
<div
|
|
1247
|
+
v-for="item in order.items"
|
|
1248
|
+
:key="item.id"
|
|
1249
|
+
class="flex justify-between text-sm"
|
|
1250
|
+
>
|
|
1251
|
+
<div>
|
|
1252
|
+
<div class="font-medium">{{ item.name }}</div>
|
|
1253
|
+
<div class="text-secondary">Qty: {{ item.quantity }}</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
<div class="text-right">
|
|
1256
|
+
{{ formatPrice(item.price * item.quantity) }}
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
</div>
|
|
1260
|
+
|
|
1261
|
+
<!-- Totals -->
|
|
1262
|
+
<hr class="my-4" />
|
|
1263
|
+
<div class="space-y-2">
|
|
1264
|
+
<div class="flex justify-between text-sm">
|
|
1265
|
+
<span class="text-secondary">Subtotal</span>
|
|
1266
|
+
<span>{{ formatPrice(subtotal) }}</span>
|
|
1267
|
+
</div>
|
|
1268
|
+
<div class="flex justify-between text-sm">
|
|
1269
|
+
<span class="text-secondary">Tax</span>
|
|
1270
|
+
<span>{{ formatPrice(tax) }}</span>
|
|
1271
|
+
</div>
|
|
1272
|
+
<div class="flex justify-between text-sm">
|
|
1273
|
+
<span class="text-secondary">Shipping</span>
|
|
1274
|
+
<span>{{ formatPrice(shipping) }}</span>
|
|
1275
|
+
</div>
|
|
1276
|
+
<hr class="my-2" />
|
|
1277
|
+
<div class="flex justify-between">
|
|
1278
|
+
<span class="font-semibold">Total</span>
|
|
1279
|
+
<span class="text-xl font-bold text-accent">
|
|
1280
|
+
{{ formatPrice(total) }}
|
|
1281
|
+
</span>
|
|
1282
|
+
</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
|
|
1286
|
+
<!-- Status -->
|
|
1287
|
+
<div>
|
|
1288
|
+
<h3 class="text-lg font-semibold mb-4">Status</h3>
|
|
1289
|
+
<AwSelect
|
|
1290
|
+
v-model="order.status"
|
|
1291
|
+
:options="['pending', 'processing', 'shipped', 'delivered', 'cancelled']"
|
|
1292
|
+
label="Order Status"
|
|
1293
|
+
:error="order.errors.status"
|
|
1294
|
+
/>
|
|
1295
|
+
</div>
|
|
1296
|
+
</template>
|
|
1297
|
+
|
|
1298
|
+
<template #aside-buttons>
|
|
1299
|
+
<AwButton
|
|
1300
|
+
@click="save"
|
|
1301
|
+
:loading="order.saving"
|
|
1302
|
+
cta
|
|
1303
|
+
block
|
|
1304
|
+
>
|
|
1305
|
+
{{ order.isNew() ? 'Create Order' : 'Update Order' }}
|
|
1306
|
+
</AwButton>
|
|
1307
|
+
<AwButton
|
|
1308
|
+
v-if="!order.isNew()"
|
|
1309
|
+
@click="printInvoice"
|
|
1310
|
+
theme="outline"
|
|
1311
|
+
block
|
|
1312
|
+
>
|
|
1313
|
+
Print Invoice
|
|
1314
|
+
</AwButton>
|
|
1315
|
+
</template>
|
|
1316
|
+
</AwPageAside>
|
|
1317
|
+
</template>
|
|
1318
|
+
|
|
1319
|
+
<script>
|
|
1320
|
+
import Order from '~/models/Order'
|
|
1321
|
+
|
|
1322
|
+
export default {
|
|
1323
|
+
middleware: 'auth',
|
|
1324
|
+
|
|
1325
|
+
data() {
|
|
1326
|
+
return {
|
|
1327
|
+
order: new Order(
|
|
1328
|
+
{ id: this.$route.params.id },
|
|
1329
|
+
null,
|
|
1330
|
+
{ shop_uuid: this.$route.params.shop_uuid }
|
|
1331
|
+
),
|
|
1332
|
+
itemFields: [
|
|
1333
|
+
{ key: 'name', label: 'Product' },
|
|
1334
|
+
{ key: 'quantity', label: 'Qty' },
|
|
1335
|
+
{ key: 'price', label: 'Price', format: this.formatPrice }
|
|
1336
|
+
]
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1340
|
+
computed: {
|
|
1341
|
+
subtotal() {
|
|
1342
|
+
return this.order.items?.reduce((sum, item) =>
|
|
1343
|
+
sum + (item.price * item.quantity), 0
|
|
1344
|
+
) || 0
|
|
1345
|
+
},
|
|
1346
|
+
|
|
1347
|
+
tax() {
|
|
1348
|
+
return this.subtotal * 0.1 // 10% tax
|
|
1349
|
+
},
|
|
1350
|
+
|
|
1351
|
+
shipping() {
|
|
1352
|
+
return 10 // Fixed shipping
|
|
1353
|
+
},
|
|
1354
|
+
|
|
1355
|
+
total() {
|
|
1356
|
+
return this.subtotal + this.tax + this.shipping
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
|
|
1360
|
+
async mounted() {
|
|
1361
|
+
if (!this.order.isNew()) {
|
|
1362
|
+
await this.order.fetch()
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
|
|
1366
|
+
methods: {
|
|
1367
|
+
formatPrice(price) {
|
|
1368
|
+
return new Intl.NumberFormat('en-US', {
|
|
1369
|
+
style: 'currency',
|
|
1370
|
+
currency: 'USD'
|
|
1371
|
+
}).format(price || 0)
|
|
1372
|
+
},
|
|
1373
|
+
|
|
1374
|
+
addItem() {
|
|
1375
|
+
// Open modal to add item
|
|
1376
|
+
},
|
|
1377
|
+
|
|
1378
|
+
async save() {
|
|
1379
|
+
try {
|
|
1380
|
+
await this.order.save()
|
|
1381
|
+
|
|
1382
|
+
if (Object.keys(this.order.errors).length > 0) {
|
|
1383
|
+
this.$notify({
|
|
1384
|
+
message: 'Please fix validation errors',
|
|
1385
|
+
type: 'error'
|
|
1386
|
+
})
|
|
1387
|
+
return
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
this.$notify({
|
|
1391
|
+
message: `Order ${this.order.isNew() ? 'created' : 'updated'} successfully`,
|
|
1392
|
+
type: 'success'
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
this.$router.push(`/${this.$route.params.shop_uuid}/orders`)
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
this.$notify({
|
|
1398
|
+
message: 'Failed to save order',
|
|
1399
|
+
type: 'error'
|
|
1400
|
+
})
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1403
|
+
|
|
1404
|
+
printInvoice() {
|
|
1405
|
+
window.print()
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
</script>
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
## See Also
|
|
1413
|
+
|
|
1414
|
+
- [Detail Pages](./detail-pages.md) - Single-focus edit pages with AwPageSingle
|
|
1415
|
+
- [List Pages](./list-pages.md) - Table-based list pages
|
|
1416
|
+
- [AwPageAside](../../components/pages/aw-page-aside.md) - Component reference
|
|
1417
|
+
- [AwPage](../../components/pages/aw-page.md) - Base page component
|
|
1418
|
+
- [Forms Guide](../forms-guide.md) - Form patterns and validation
|