@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.
Files changed (239) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/assets/css/components/_index.css +7 -1
  3. package/assets/css/components/animation.css +38 -32
  4. package/assets/css/components/content-placeholder.css +103 -0
  5. package/assets/css/components/empty-container.css +69 -1
  6. package/assets/css/components/filter-chosen.css +6 -0
  7. package/assets/css/components/filter-date-range.css +17 -1
  8. package/assets/css/components/filter-month.css +23 -17
  9. package/assets/css/components/filter-select.css +11 -0
  10. package/assets/css/components/layout.css +1 -32
  11. package/assets/css/components/modal.css +1 -1
  12. package/assets/css/components/number.css +12 -0
  13. package/assets/css/components/page-aside.css +54 -0
  14. package/assets/js/css.js +1 -1
  15. package/assets/js/icons/mono.js +59 -91
  16. package/assets/js/icons/multicolor.js +1 -31
  17. package/components/1_atoms/AwContentPlaceholder.vue +60 -0
  18. package/components/1_atoms/AwFlow.vue +21 -48
  19. package/components/1_atoms/AwLabel.vue +1 -1
  20. package/components/2_molecules/AwButton.vue +1 -1
  21. package/components/2_molecules/AwEmptyContainer.vue +74 -72
  22. package/components/2_molecules/AwNumber.vue +180 -0
  23. package/components/2_molecules/AwSelect.vue +11 -4
  24. package/components/3_organisms/AwFilterChosen.vue +73 -0
  25. package/components/3_organisms/AwFilterDateRange.vue +177 -0
  26. package/components/3_organisms/AwFilterMonth.vue +37 -40
  27. package/components/3_organisms/AwFilterSelect.vue +368 -0
  28. package/components/3_organisms/AwImageUpload.vue +1 -1
  29. package/components/3_organisms/AwMarkdownEditor.vue +0 -0
  30. package/components/3_organisms/AwMultiBlockBuilder.vue +1 -1
  31. package/components/3_organisms/AwTable/AwTableBuilder.vue +12 -60
  32. package/components/4_pages/AwPageAside.vue +108 -0
  33. package/components/5_layouts/AwLayoutCenter.vue +3 -8
  34. package/components/5_layouts/_AwUserMenu.vue +1 -1
  35. package/dist/css/aw-icons.css +26 -0
  36. package/dist/fonts/aw-icons.svg +18 -0
  37. package/dist/fonts/aw-icons.ttf +0 -0
  38. package/dist/fonts/aw-icons.woff +0 -0
  39. package/dist/fonts/aw-icons.woff2 +0 -0
  40. package/docs/_template.md +80 -0
  41. package/docs/components/atoms/aw-accordion-fold.md +91 -0
  42. package/docs/components/atoms/aw-action-card-body.md +67 -0
  43. package/docs/components/atoms/aw-action-card.md +94 -0
  44. package/docs/components/atoms/aw-action-icon.md +88 -0
  45. package/docs/components/atoms/aw-avatar.md +106 -0
  46. package/docs/components/atoms/aw-card.md +112 -0
  47. package/docs/components/atoms/aw-checkbox.md +112 -0
  48. package/docs/components/atoms/aw-content-placeholder.md +116 -0
  49. package/docs/components/atoms/aw-description.md +83 -0
  50. package/docs/components/atoms/aw-dock.md +84 -0
  51. package/docs/components/atoms/aw-dropdown-button.md +94 -0
  52. package/docs/components/atoms/aw-dropdown.md +128 -0
  53. package/docs/components/atoms/aw-file.md +73 -0
  54. package/docs/components/atoms/aw-flow.md +92 -0
  55. package/docs/components/atoms/aw-grid.md +91 -0
  56. package/docs/components/atoms/aw-headline.md +71 -0
  57. package/docs/components/atoms/aw-icon-system-color.md +121 -0
  58. package/docs/components/atoms/aw-icon-system-mono.md +205 -0
  59. package/docs/components/atoms/aw-icon.md +235 -0
  60. package/docs/components/atoms/aw-info.md +85 -0
  61. package/docs/components/atoms/aw-input.md +120 -0
  62. package/docs/components/atoms/aw-label.md +83 -0
  63. package/docs/components/atoms/aw-link.md +99 -0
  64. package/docs/components/atoms/aw-list.md +88 -0
  65. package/docs/components/atoms/aw-progress.md +70 -0
  66. package/docs/components/atoms/aw-radio.md +109 -0
  67. package/docs/components/atoms/aw-refresh-wrapper.md +81 -0
  68. package/docs/components/atoms/aw-select-native.md +106 -0
  69. package/docs/components/atoms/aw-slider.md +82 -0
  70. package/docs/components/atoms/aw-sub-headline.md +73 -0
  71. package/docs/components/atoms/aw-switcher.md +115 -0
  72. package/docs/components/atoms/aw-tag.md +80 -0
  73. package/docs/components/atoms/aw-title.md +70 -0
  74. package/docs/components/atoms/aw-toggler.md +69 -0
  75. package/docs/components/layouts/aw-layout-center.md +168 -0
  76. package/docs/components/layouts/aw-layout-error.md +153 -0
  77. package/docs/components/layouts/aw-layout-provider.md +238 -0
  78. package/docs/components/layouts/aw-layout.md +88 -0
  79. package/docs/components/molecules/aw-action-button.md +91 -0
  80. package/docs/components/molecules/aw-alert.md +96 -0
  81. package/docs/components/molecules/aw-badge.md +108 -0
  82. package/docs/components/molecules/aw-banner-text.md +90 -0
  83. package/docs/components/molecules/aw-button-nav.md +46 -0
  84. package/docs/components/molecules/aw-button.md +123 -0
  85. package/docs/components/molecules/aw-description-input.md +67 -0
  86. package/docs/components/molecules/aw-empty-container.md +86 -0
  87. package/docs/components/molecules/aw-island.md +234 -0
  88. package/docs/components/molecules/aw-number.md +138 -0
  89. package/docs/components/molecules/aw-select-object.md +401 -0
  90. package/docs/components/molecules/aw-select.md +215 -0
  91. package/docs/components/molecules/aw-tab-nav.md +108 -0
  92. package/docs/components/molecules/aw-tel.md +129 -0
  93. package/docs/components/molecules/aw-textarea.md +83 -0
  94. package/docs/components/molecules/aw-userpic.md +115 -0
  95. package/docs/components/organisms/aw-address-block.md +64 -0
  96. package/docs/components/organisms/aw-address.md +132 -0
  97. package/docs/components/organisms/aw-birthday-picker.md +73 -0
  98. package/docs/components/organisms/aw-bottom-bar.md +66 -0
  99. package/docs/components/organisms/aw-calendar-days.md +115 -0
  100. package/docs/components/organisms/aw-calendar-nav.md +98 -0
  101. package/docs/components/organisms/aw-calendar-view.md +98 -0
  102. package/docs/components/organisms/aw-calendar.md +166 -0
  103. package/docs/components/organisms/aw-chart.md +154 -0
  104. package/docs/components/organisms/aw-chip-select.md +164 -0
  105. package/docs/components/organisms/aw-chip.md +126 -0
  106. package/docs/components/organisms/aw-code-snippet.md +94 -0
  107. package/docs/components/organisms/aw-code.md +132 -0
  108. package/docs/components/organisms/aw-context-menu.md +117 -0
  109. package/docs/components/organisms/aw-cropper.md +151 -0
  110. package/docs/components/organisms/aw-date.md +161 -0
  111. package/docs/components/organisms/aw-display-date.md +33 -0
  112. package/docs/components/organisms/aw-download-link.md +46 -0
  113. package/docs/components/organisms/aw-fetch-data.md +161 -0
  114. package/docs/components/organisms/aw-filter-chosen.md +226 -0
  115. package/docs/components/organisms/aw-filter-date-range.md +205 -0
  116. package/docs/components/organisms/aw-filter-month.md +43 -0
  117. package/docs/components/organisms/aw-filter-select.md +225 -0
  118. package/docs/components/organisms/aw-form.md +174 -0
  119. package/docs/components/organisms/aw-gmap-marker.md +86 -0
  120. package/docs/components/organisms/aw-gmap.md +90 -0
  121. package/docs/components/organisms/aw-image-upload.md +56 -0
  122. package/docs/components/organisms/aw-island-avatar.md +87 -0
  123. package/docs/components/organisms/aw-markdown-editor.md +104 -0
  124. package/docs/components/organisms/aw-modal-buttons.md +57 -0
  125. package/docs/components/organisms/aw-modal.md +246 -0
  126. package/docs/components/organisms/aw-model-edit.md +74 -0
  127. package/docs/components/organisms/aw-money.md +53 -0
  128. package/docs/components/organisms/aw-multi-block-builder.md +165 -0
  129. package/docs/components/organisms/aw-pagination.md +121 -0
  130. package/docs/components/organisms/aw-password.md +103 -0
  131. package/docs/components/organisms/aw-preview-card.md +45 -0
  132. package/docs/components/organisms/aw-search.md +116 -0
  133. package/docs/components/organisms/aw-subnav.md +122 -0
  134. package/docs/components/organisms/aw-table-builder.md +165 -0
  135. package/docs/components/organisms/aw-table-col.md +123 -0
  136. package/docs/components/organisms/aw-table-head.md +92 -0
  137. package/docs/components/organisms/aw-table-row.md +91 -0
  138. package/docs/components/organisms/aw-table.md +172 -0
  139. package/docs/components/organisms/aw-tags.md +54 -0
  140. package/docs/components/organisms/aw-toggle-show-aside.md +43 -0
  141. package/docs/components/organisms/aw-uploader-files.md +125 -0
  142. package/docs/components/organisms/aw-uploader.md +163 -0
  143. package/docs/components/organisms/aw-user-menu.md +87 -0
  144. package/docs/components/pages/aw-page-aside.md +296 -0
  145. package/docs/components/pages/aw-page-menu-buttons.md +172 -0
  146. package/docs/components/pages/aw-page-modal.md +198 -0
  147. package/docs/components/pages/aw-page-single.md +253 -0
  148. package/docs/components/pages/aw-page.md +194 -0
  149. package/docs/configuration.md +493 -0
  150. package/docs/cookbook/advanced-patterns.md +1388 -0
  151. package/docs/cookbook/common-patterns.md +965 -0
  152. package/docs/cookbook/index.md +786 -0
  153. package/docs/getting-started.md +596 -0
  154. package/docs/guides/best-practices.md +1106 -0
  155. package/docs/guides/data-fetching.md +852 -0
  156. package/docs/guides/error-handling.md +1172 -0
  157. package/docs/guides/forms-guide.md +1329 -0
  158. package/docs/guides/mobile-subnavigation.md +359 -0
  159. package/docs/guides/page-patterns/aside-pages.md +1418 -0
  160. package/docs/guides/page-patterns/dashboard-pages.md +990 -0
  161. package/docs/guides/page-patterns/detail-pages.md +1493 -0
  162. package/docs/guides/page-patterns/list-pages.md +1094 -0
  163. package/docs/index.md +263 -1
  164. package/docs/integrations.md +870 -0
  165. package/docs/reference/menu.md +462 -0
  166. package/docs/reference/plugins.md +970 -0
  167. package/docs/reference/troubleshooting.md +945 -0
  168. package/nuxt/awes.config.js +9 -8
  169. package/nuxt/icons.css +26 -0
  170. package/nuxt/index.js +2 -2
  171. package/nuxt/pages/more.vue +1 -1
  172. package/package.json +5 -3
  173. package/readme.md +171 -1
  174. package/docs/aw-accordion-fold.md +0 -46
  175. package/docs/aw-address.md +0 -44
  176. package/docs/aw-avatar.md +0 -51
  177. package/docs/aw-badge.md +0 -32
  178. package/docs/aw-button-nav.md +0 -44
  179. package/docs/aw-button.md +0 -50
  180. package/docs/aw-calendar-days.md +0 -46
  181. package/docs/aw-calendar-nav.md +0 -25
  182. package/docs/aw-calendar-view.md +0 -12
  183. package/docs/aw-calendar.md +0 -59
  184. package/docs/aw-card.md +0 -48
  185. package/docs/aw-chart.md +0 -51
  186. package/docs/aw-checkbox.md +0 -56
  187. package/docs/aw-chip-select.md +0 -46
  188. package/docs/aw-chip.md +0 -53
  189. package/docs/aw-code-snippet.md +0 -18
  190. package/docs/aw-code.md +0 -56
  191. package/docs/aw-content-wrapper.md +0 -40
  192. package/docs/aw-context-menu.md +0 -31
  193. package/docs/aw-cropper.md +0 -60
  194. package/docs/aw-dashboard-card.md +0 -37
  195. package/docs/aw-dashboard-donut.md +0 -30
  196. package/docs/aw-dashboard-line.md +0 -20
  197. package/docs/aw-dashboard-progress.md +0 -33
  198. package/docs/aw-dashboard-section.md +0 -32
  199. package/docs/aw-dashboard-speed.md +0 -30
  200. package/docs/aw-date.md +0 -52
  201. package/docs/aw-dropdown-button.md +0 -31
  202. package/docs/aw-dropdown.md +0 -69
  203. package/docs/aw-fetch-data.md +0 -45
  204. package/docs/aw-form.md +0 -52
  205. package/docs/aw-grid.md +0 -48
  206. package/docs/aw-icon.md +0 -50
  207. package/docs/aw-info.md +0 -53
  208. package/docs/aw-input.md +0 -55
  209. package/docs/aw-layout-default.md +0 -30
  210. package/docs/aw-layout-frame-center.md +0 -29
  211. package/docs/aw-layout-simple.md +0 -49
  212. package/docs/aw-link.md +0 -54
  213. package/docs/aw-markdown-editor.md +0 -51
  214. package/docs/aw-modal.md +0 -63
  215. package/docs/aw-multi-block-builder.md +0 -66
  216. package/docs/aw-page.md +0 -36
  217. package/docs/aw-pagination.md +0 -54
  218. package/docs/aw-password.md +0 -48
  219. package/docs/aw-radio.md +0 -54
  220. package/docs/aw-search.md +0 -49
  221. package/docs/aw-select.md +0 -93
  222. package/docs/aw-slider.md +0 -40
  223. package/docs/aw-svg-image.md +0 -19
  224. package/docs/aw-switcher.md +0 -51
  225. package/docs/aw-tab-nav.md +0 -55
  226. package/docs/aw-table-builder.md +0 -58
  227. package/docs/aw-table-col.md +0 -33
  228. package/docs/aw-table-head.md +0 -28
  229. package/docs/aw-table-row.md +0 -33
  230. package/docs/aw-table.md +0 -59
  231. package/docs/aw-tel.md +0 -47
  232. package/docs/aw-textarea.md +0 -47
  233. package/docs/aw-timeline-builder.md +0 -50
  234. package/docs/aw-toggler.md +0 -41
  235. package/docs/aw-uploader-files.md +0 -20
  236. package/docs/aw-uploader.md +0 -60
  237. package/docs/aw-user-menu.md +0 -34
  238. package/docs/aw-userpic.md +0 -34
  239. /package/components/{3_organisms → 2_molecules}/AwTel.vue +0 -0
@@ -0,0 +1,1388 @@
1
+ # Advanced Patterns
2
+
3
+ Complex patterns for sophisticated application features.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Multi-Step Wizard](#multi-step-wizard)
8
+ - [Nested Forms](#nested-forms)
9
+ - [Optimistic Updates](#optimistic-updates)
10
+ - [Real-Time Data](#real-time-data)
11
+ - [File Upload with Progress](#file-upload-with-progress)
12
+ - [Infinite Scroll](#infinite-scroll)
13
+ - [Complex Filtering & URL State](#complex-filtering--url-state)
14
+ - [Permission-Based UI](#permission-based-ui)
15
+
16
+ ## Multi-Step Wizard
17
+
18
+ Guide users through multi-step processes with state management.
19
+
20
+ ### Complete Wizard Example
21
+
22
+ ```markup
23
+ <template>
24
+ <AwPageSingle
25
+ hide-menu
26
+ title="New Campaign Setup"
27
+ :action="primaryAction"
28
+ :progress="stepProgress"
29
+ @action="handleAction"
30
+ >
31
+ <template #buttons>
32
+ <AwButton
33
+ v-if="currentStep > 1"
34
+ @click="previousStep"
35
+ text="Back"
36
+ />
37
+ </template>
38
+
39
+ <!-- Step 1: Basic Info -->
40
+ <AwCard v-show="currentStep === 1" title="Campaign Details">
41
+ <AwInput
42
+ v-model="campaign.name"
43
+ :error="errors.name"
44
+ label="Campaign Name"
45
+ required
46
+ />
47
+
48
+ <AwTextarea
49
+ v-model="campaign.description"
50
+ :error="errors.description"
51
+ label="Description"
52
+ rows="4"
53
+ />
54
+
55
+ <AwSelect
56
+ v-model="campaign.type"
57
+ :error="errors.type"
58
+ :options="campaignTypes"
59
+ label="Campaign Type"
60
+ required
61
+ />
62
+ </AwCard>
63
+
64
+ <!-- Step 2: Audience -->
65
+ <AwCard v-show="currentStep === 2" title="Target Audience">
66
+ <AwSelect
67
+ v-model="campaign.audience_type"
68
+ :error="errors.audience_type"
69
+ :options="['all_customers', 'segment', 'custom']"
70
+ label="Audience Type"
71
+ required
72
+ />
73
+
74
+ <AwSelect
75
+ v-if="campaign.audience_type === 'segment'"
76
+ v-model="campaign.segment_id"
77
+ :error="errors.segment_id"
78
+ :options="loadSegments"
79
+ option-label="name"
80
+ track-by="id"
81
+ label="Segment"
82
+ />
83
+
84
+ <div v-if="campaign.audience_type === 'custom'">
85
+ <AwDescription>Select specific customers</AwDescription>
86
+ <CustomersSelector v-model="campaign.customer_ids" />
87
+ </div>
88
+ </AwCard>
89
+
90
+ <!-- Step 3: Content -->
91
+ <AwCard v-show="currentStep === 3" title="Campaign Content">
92
+ <AwSelect
93
+ v-model="campaign.template_key"
94
+ :error="errors.template_key"
95
+ :options="loadTemplates"
96
+ :option-label="formatTemplateLabel"
97
+ track-by="template_key"
98
+ label="Message Template"
99
+ required
100
+ />
101
+
102
+ <AwMarkdownEditor
103
+ v-model="campaign.content"
104
+ :error="errors.content"
105
+ label="Message Content"
106
+ />
107
+
108
+ <AwCard title="Preview" class="mt-4">
109
+ <div v-html="compiledPreview" />
110
+ </AwCard>
111
+ </AwCard>
112
+
113
+ <!-- Step 4: Schedule -->
114
+ <AwCard v-show="currentStep === 4" title="Schedule Campaign">
115
+ <AwSwitcher
116
+ v-model="campaign.send_immediately"
117
+ label="Send Immediately"
118
+ />
119
+
120
+ <div v-if="!campaign.send_immediately">
121
+ <AwDate
122
+ v-model="campaign.scheduled_date"
123
+ :error="errors.scheduled_date"
124
+ label="Schedule Date"
125
+ :min="minScheduleDate"
126
+ />
127
+
128
+ <AwInput
129
+ v-model="campaign.scheduled_time"
130
+ :error="errors.scheduled_time"
131
+ type="time"
132
+ label="Schedule Time"
133
+ />
134
+ </div>
135
+ </AwCard>
136
+
137
+ <!-- Step 5: Review -->
138
+ <AwCard v-show="currentStep === 5" title="Review & Confirm">
139
+ <AwGrid :col="2">
140
+ <div>
141
+ <AwDescription>Campaign Name</AwDescription>
142
+ <p class="font-medium">{{ campaign.name }}</p>
143
+ </div>
144
+
145
+ <div>
146
+ <AwDescription>Type</AwDescription>
147
+ <p class="font-medium">{{ campaign.type }}</p>
148
+ </div>
149
+
150
+ <div>
151
+ <AwDescription>Audience</AwDescription>
152
+ <p class="font-medium">{{ audienceDescription }}</p>
153
+ </div>
154
+
155
+ <div>
156
+ <AwDescription>Schedule</AwDescription>
157
+ <p class="font-medium">{{ scheduleDescription }}</p>
158
+ </div>
159
+ </AwGrid>
160
+
161
+ <AwAlert type="info" class="mt-6">
162
+ Please review all details before creating the campaign.
163
+ </AwAlert>
164
+ </AwCard>
165
+ </AwPageSingle>
166
+ </template>
167
+
168
+ <script>
169
+ export default {
170
+ data() {
171
+ return {
172
+ currentStep: 1,
173
+ totalSteps: 5,
174
+ stepTitles: ['Details', 'Audience', 'Content', 'Schedule', 'Review'],
175
+ campaign: {
176
+ name: '',
177
+ description: '',
178
+ type: null,
179
+ audience_type: 'all_customers',
180
+ segment_id: null,
181
+ customer_ids: [],
182
+ template_key: null,
183
+ content: '',
184
+ send_immediately: true,
185
+ scheduled_date: null,
186
+ scheduled_time: null
187
+ },
188
+ errors: {},
189
+ saving: false,
190
+ campaignTypes: ['email', 'sms', 'push_notification']
191
+ }
192
+ },
193
+
194
+ computed: {
195
+ primaryAction() {
196
+ // Primary action changes based on step
197
+ if (this.currentStep < this.totalSteps) {
198
+ return {
199
+ key: 'next',
200
+ label: 'Next',
201
+ disabled: !this.isStepValid,
202
+ color: 'accent'
203
+ }
204
+ }
205
+
206
+ return {
207
+ key: 'submit',
208
+ label: 'Create Campaign',
209
+ loading: this.saving,
210
+ color: 'accent'
211
+ }
212
+ },
213
+
214
+ stepProgress() {
215
+ return Math.round((this.currentStep / this.totalSteps) * 100)
216
+ },
217
+
218
+ isStepValid() {
219
+ switch (this.currentStep) {
220
+ case 1:
221
+ return this.campaign.name && this.campaign.type
222
+ case 2:
223
+ if (this.campaign.audience_type === 'segment') {
224
+ return !!this.campaign.segment_id
225
+ }
226
+ if (this.campaign.audience_type === 'custom') {
227
+ return this.campaign.customer_ids.length > 0
228
+ }
229
+ return true
230
+ case 3:
231
+ return this.campaign.template_key && this.campaign.content
232
+ case 4:
233
+ if (this.campaign.send_immediately) {
234
+ return true
235
+ }
236
+ return this.campaign.scheduled_date && this.campaign.scheduled_time
237
+ case 5:
238
+ return true
239
+ default:
240
+ return false
241
+ }
242
+ },
243
+
244
+ audienceDescription() {
245
+ if (this.campaign.audience_type === 'all_customers') {
246
+ return 'All Customers'
247
+ }
248
+ if (this.campaign.audience_type === 'segment') {
249
+ return `Segment: ${this.campaign.segment_id}`
250
+ }
251
+ return `${this.campaign.customer_ids.length} selected customers`
252
+ },
253
+
254
+ scheduleDescription() {
255
+ if (this.campaign.send_immediately) {
256
+ return 'Send Immediately'
257
+ }
258
+ return `${this.$dayjs(this.campaign.scheduled_date).format('ll')} at ${this.campaign.scheduled_time}`
259
+ },
260
+
261
+ compiledPreview() {
262
+ // Compile template with sample data
263
+ return this.campaign.content
264
+ },
265
+
266
+ minScheduleDate() {
267
+ return this.$dayjs().format('YYYY-MM-DD')
268
+ }
269
+ },
270
+
271
+ methods: {
272
+ handleAction(action) {
273
+ if (action.key === 'next') {
274
+ this.nextStep()
275
+ } else if (action.key === 'submit') {
276
+ this.submit()
277
+ }
278
+ },
279
+
280
+ nextStep() {
281
+ if (this.isStepValid && this.currentStep < this.totalSteps) {
282
+ this.currentStep++
283
+ }
284
+ },
285
+
286
+ previousStep() {
287
+ if (this.currentStep > 1) {
288
+ this.currentStep--
289
+ }
290
+ },
291
+
292
+ async submit() {
293
+ this.saving = true
294
+
295
+ try {
296
+ await this.$axios.post('/api/campaigns', {
297
+ shop_uuid: this.$route.params.shop_uuid,
298
+ ...this.campaign
299
+ })
300
+
301
+ this.$notify({
302
+ message: 'Campaign created successfully',
303
+ type: 'success'
304
+ })
305
+
306
+ this.$router.push(`/shops/${this.$route.params.shop_uuid}/campaigns`)
307
+ } catch (error) {
308
+ if (error.response?.status === 422) {
309
+ this.errors = error.response.data.errors
310
+ this.$notify({
311
+ message: 'Please fix validation errors',
312
+ type: 'error'
313
+ })
314
+ } else {
315
+ this.$notify({
316
+ message: 'Failed to create campaign',
317
+ type: 'error'
318
+ })
319
+ }
320
+ } finally {
321
+ this.saving = false
322
+ }
323
+ },
324
+
325
+ loadSegments(search) {
326
+ return `/api/shops/${this.$route.params.shop_uuid}/segments?search=${search}`
327
+ },
328
+
329
+ loadTemplates(search) {
330
+ return `/api/shops/${this.$route.params.shop_uuid}/templates?search=${search}&type=${this.campaign.type}`
331
+ },
332
+
333
+ formatTemplateLabel(template) {
334
+ return `${template.name} (${template.channel})`
335
+ }
336
+ }
337
+ }
338
+ </script>
339
+ ```
340
+
341
+ ## Nested Forms
342
+
343
+ Manage parent-child relationships with dynamic fields.
344
+
345
+ ### Order with Line Items
346
+
347
+ ```markup
348
+ <template>
349
+ <AwPageSingle
350
+ hide-menu
351
+ :title="order.isNew() ? 'New Order' : 'Edit Order'"
352
+ :action="{
353
+ key: 'save',
354
+ label: 'Save Order',
355
+ loading: saving,
356
+ color: 'accent'
357
+ }"
358
+ @action="handleAction"
359
+ >
360
+
361
+ <!-- Customer Selection -->
362
+ <AwCard title="Customer">
363
+ <AwSelect
364
+ v-model="order.customer_id"
365
+ :error="order.errors.customer_id"
366
+ :options="loadCustomers"
367
+ option-label="name"
368
+ track-by="id"
369
+ label="Customer"
370
+ required
371
+ />
372
+ </AwCard>
373
+
374
+ <!-- Line Items -->
375
+ <AwCard title="Order Items" class="mt-6">
376
+ <div
377
+ v-for="(item, index) in order.items"
378
+ :key="item.uuid"
379
+ class="mb-4 p-4 border rounded"
380
+ >
381
+ <AwFlow justify="between" align="start">
382
+ <AwGrid :col="3" class="flex-1">
383
+ <AwSelect
384
+ v-model="item.product_id"
385
+ :error="getItemError(index, 'product_id')"
386
+ :options="loadProducts"
387
+ option-label="name"
388
+ track-by="id"
389
+ label="Product"
390
+ @input="updateItemPrice(index)"
391
+ />
392
+
393
+ <AwInput
394
+ v-model.number="item.quantity"
395
+ :error="getItemError(index, 'quantity')"
396
+ type="number"
397
+ label="Quantity"
398
+ min="1"
399
+ @input="updateItemTotal(index)"
400
+ />
401
+
402
+ <AwInput
403
+ v-model.number="item.price"
404
+ :error="getItemError(index, 'price')"
405
+ type="number"
406
+ label="Price"
407
+ step="0.01"
408
+ @input="updateItemTotal(index)"
409
+ />
410
+ </AwGrid>
411
+
412
+ <AwButton
413
+ @click="removeItem(index)"
414
+ color="error"
415
+ size="sm"
416
+ >
417
+ Remove
418
+ </AwButton>
419
+ </AwFlow>
420
+
421
+ <div class="mt-2 text-right">
422
+ <strong>Total: ${{ item.total.toFixed(2) }}</strong>
423
+ </div>
424
+ </div>
425
+
426
+ <AwButton @click="addItem" icon="plus">
427
+ Add Item
428
+ </AwButton>
429
+ </AwCard>
430
+
431
+ <!-- Order Summary -->
432
+ <AwCard title="Order Summary" class="mt-6">
433
+ <AwGrid :col="2">
434
+ <div>
435
+ <AwDescription>Subtotal</AwDescription>
436
+ <p class="text-xl font-medium">${{ subtotal.toFixed(2) }}</p>
437
+ </div>
438
+
439
+ <div>
440
+ <AwDescription>Tax ({{ taxRate }}%)</AwDescription>
441
+ <p class="text-xl font-medium">${{ tax.toFixed(2) }}</p>
442
+ </div>
443
+
444
+ <div class="col-span-2">
445
+ <AwDescription>Total</AwDescription>
446
+ <p class="text-3xl font-bold">${{ total.toFixed(2) }}</p>
447
+ </div>
448
+ </AwGrid>
449
+ </AwCard>
450
+ </AwPageSingle>
451
+ </template>
452
+
453
+ <script>
454
+ import { v4 as uuidv4 } from 'uuid'
455
+
456
+ export default {
457
+ data() {
458
+ return {
459
+ order: {
460
+ customer_id: null,
461
+ items: [],
462
+ errors: {
463
+ get(field) {
464
+ return this[field]
465
+ },
466
+ isEmpty() {
467
+ return Object.keys(this).length === 1 // Only 'get' method
468
+ }
469
+ }
470
+ },
471
+ itemErrors: [],
472
+ saving: false,
473
+ taxRate: 10
474
+ }
475
+ },
476
+
477
+ computed: {
478
+ subtotal() {
479
+ return this.order.items.reduce((sum, item) => sum + (item.total || 0), 0)
480
+ },
481
+
482
+ tax() {
483
+ return (this.subtotal * this.taxRate) / 100
484
+ },
485
+
486
+ total() {
487
+ return this.subtotal + this.tax
488
+ }
489
+ },
490
+
491
+ mounted() {
492
+ // Start with one empty item
493
+ this.addItem()
494
+ },
495
+
496
+ methods: {
497
+ handleAction(action) {
498
+ if (action.key === 'save') {
499
+ this.save()
500
+ }
501
+ },
502
+
503
+ addItem() {
504
+ this.order.items.push({
505
+ uuid: uuidv4(),
506
+ product_id: null,
507
+ quantity: 1,
508
+ price: 0,
509
+ total: 0
510
+ })
511
+ },
512
+
513
+ removeItem(index) {
514
+ this.order.items.splice(index, 1)
515
+ },
516
+
517
+ async updateItemPrice(index) {
518
+ const item = this.order.items[index]
519
+ if (!item.product_id) return
520
+
521
+ try {
522
+ const { data } = await this.$axios.get(`/api/products/${item.product_id}`)
523
+ item.price = data.price
524
+ this.updateItemTotal(index)
525
+ } catch (error) {
526
+ this.$notify({
527
+ message: 'Failed to load product price',
528
+ type: 'error'
529
+ })
530
+ }
531
+ },
532
+
533
+ updateItemTotal(index) {
534
+ const item = this.order.items[index]
535
+ item.total = (item.quantity || 0) * (item.price || 0)
536
+ },
537
+
538
+ getItemError(index, field) {
539
+ return this.itemErrors[index]?.[field]
540
+ },
541
+
542
+ async save() {
543
+ this.saving = true
544
+
545
+ try {
546
+ const payload = {
547
+ shop_uuid: this.$route.params.shop_uuid,
548
+ customer_id: this.order.customer_id,
549
+ items: this.order.items.map(item => ({
550
+ product_id: item.product_id,
551
+ quantity: item.quantity,
552
+ price: item.price
553
+ })),
554
+ subtotal: this.subtotal,
555
+ tax: this.tax,
556
+ total: this.total
557
+ }
558
+
559
+ const { data } = await this.$axios.post('/api/orders', payload)
560
+
561
+ this.$notify({
562
+ message: 'Order saved',
563
+ type: 'success'
564
+ })
565
+
566
+ this.$router.push(`/shops/${this.$route.params.shop_uuid}/orders/${data.id}`)
567
+ } catch (error) {
568
+ if (error.response?.status === 422) {
569
+ const errors = error.response.data.errors
570
+
571
+ // Separate item errors
572
+ this.itemErrors = []
573
+ Object.keys(errors).forEach(key => {
574
+ const match = key.match(/^items\.(\d+)\.(.+)$/)
575
+ if (match) {
576
+ const [, index, field] = match
577
+ if (!this.itemErrors[index]) {
578
+ this.itemErrors[index] = {}
579
+ }
580
+ this.itemErrors[index][field] = errors[key][0]
581
+ } else {
582
+ this.order.errors[key] = errors[key][0]
583
+ }
584
+ })
585
+
586
+ this.$notify({
587
+ message: 'Please fix validation errors',
588
+ type: 'error'
589
+ })
590
+ } else {
591
+ this.$notify({
592
+ message: 'Failed to save order',
593
+ type: 'error'
594
+ })
595
+ }
596
+ } finally {
597
+ this.saving = false
598
+ }
599
+ },
600
+
601
+ loadCustomers(search) {
602
+ return `/api/shops/${this.$route.params.shop_uuid}/customers?search=${search}`
603
+ },
604
+
605
+ loadProducts(search) {
606
+ return `/api/shops/${this.$route.params.shop_uuid}/products?search=${search}`
607
+ }
608
+ }
609
+ }
610
+ </script>
611
+ ```
612
+
613
+ ## Optimistic Updates
614
+
615
+ Update UI immediately before server confirmation for better UX.
616
+
617
+ ### Toggle with Optimistic Update
618
+
619
+ ```markup
620
+ <template>
621
+ <AwCard>
622
+ <AwList :items="settings">
623
+ <template #item="{ item: setting }">
624
+ <AwFlow justify="between" align="center">
625
+ <div>
626
+ <div class="font-medium">{{ setting.label }}</div>
627
+ <AwDescription>{{ setting.description }}</AwDescription>
628
+ </div>
629
+
630
+ <AwSwitcher
631
+ :value="setting.enabled"
632
+ @input="toggleSetting(setting)"
633
+ />
634
+ </AwFlow>
635
+ </template>
636
+ </AwList>
637
+ </AwCard>
638
+ </template>
639
+
640
+ <script>
641
+ export default {
642
+ data() {
643
+ return {
644
+ settings: [
645
+ {
646
+ key: 'email_notifications',
647
+ label: 'Email Notifications',
648
+ description: 'Receive email notifications for new orders',
649
+ enabled: true
650
+ },
651
+ {
652
+ key: 'sms_notifications',
653
+ label: 'SMS Notifications',
654
+ description: 'Receive SMS notifications for urgent updates',
655
+ enabled: false
656
+ }
657
+ ]
658
+ }
659
+ },
660
+
661
+ methods: {
662
+ async toggleSetting(setting) {
663
+ // Store previous value for rollback
664
+ const previousValue = setting.enabled
665
+
666
+ // Optimistically update UI
667
+ setting.enabled = !setting.enabled
668
+
669
+ try {
670
+ await this.$axios.patch(`/api/settings/${setting.key}`, {
671
+ shop_uuid: this.$route.params.shop_uuid,
672
+ enabled: setting.enabled
673
+ })
674
+
675
+ this.$notify({
676
+ message: `${setting.label} ${setting.enabled ? 'enabled' : 'disabled'}`,
677
+ type: 'success'
678
+ })
679
+ } catch (error) {
680
+ // Rollback on error
681
+ setting.enabled = previousValue
682
+
683
+ this.$notify({
684
+ message: 'Failed to update setting',
685
+ type: 'error'
686
+ })
687
+ }
688
+ }
689
+ }
690
+ }
691
+ </script>
692
+ ```
693
+
694
+ ### List with Optimistic Delete
695
+
696
+ ```markup
697
+ <script>
698
+ export default {
699
+ methods: {
700
+ async deleteCustomer(customer) {
701
+ const confirmed = await this.$confirm({
702
+ title: 'Delete Customer',
703
+ message: `Are you sure you want to delete ${customer.name}?`
704
+ })
705
+
706
+ if (!confirmed) return
707
+
708
+ // Store index for rollback
709
+ const index = this.customers.models.indexOf(customer)
710
+ const deletedCustomer = { ...customer }
711
+
712
+ // Optimistically remove from list
713
+ this.customers.models.splice(index, 1)
714
+
715
+ try {
716
+ await this.$axios.delete(`/api/customers/${customer.id}`, {
717
+ params: {
718
+ shop_uuid: this.$route.params.shop_uuid
719
+ }
720
+ })
721
+
722
+ this.$notify({
723
+ message: 'Customer deleted',
724
+ type: 'success'
725
+ })
726
+ } catch (error) {
727
+ // Rollback on error
728
+ this.customers.models.splice(index, 0, deletedCustomer)
729
+
730
+ this.$notify({
731
+ message: 'Failed to delete customer',
732
+ type: 'error'
733
+ })
734
+ }
735
+ }
736
+ }
737
+ }
738
+ </script>
739
+ ```
740
+
741
+ ## Real-Time Data
742
+
743
+ Update UI automatically with WebSocket or polling.
744
+
745
+ ### With Laravel Echo (WebSockets)
746
+
747
+ ```markup
748
+ <template>
749
+ <AwPage title="Orders">
750
+ <AwAlert v-if="hasNewOrders" type="info" class="mb-6">
751
+ New orders received.
752
+ <AwButton size="sm" @click="loadNewOrders">
753
+ Refresh
754
+ </AwButton>
755
+ </AwAlert>
756
+
757
+ <AwTableBuilder :collection="orders">
758
+ <AwTableCol field="number" title="Order #" />
759
+ <AwTableCol field="customer_name" title="Customer" />
760
+ <AwTableCol field="total" title="Total" />
761
+ <AwTableCol field="status" title="Status" />
762
+ </AwTableBuilder>
763
+ </AwPage>
764
+ </template>
765
+
766
+ <script>
767
+ import Orders from '~/collections/Orders'
768
+
769
+ export default {
770
+ data() {
771
+ return {
772
+ orders: new Orders([], {
773
+ shop_uuid: this.$route.params.shop_uuid
774
+ }),
775
+ hasNewOrders: false
776
+ }
777
+ },
778
+
779
+ mounted() {
780
+ // Subscribe to shop channel
781
+ window.Echo.private(`shop.${this.$route.params.shop_uuid}`)
782
+ .listen('OrderCreated', this.onOrderCreated)
783
+ .listen('OrderUpdated', this.onOrderUpdated)
784
+ },
785
+
786
+ beforeDestroy() {
787
+ // Unsubscribe
788
+ window.Echo.leave(`shop.${this.$route.params.shop_uuid}`)
789
+ },
790
+
791
+ methods: {
792
+ onOrderCreated(event) {
793
+ console.log('New order:', event.order)
794
+ this.hasNewOrders = true
795
+
796
+ this.$notify({
797
+ message: `New order #${event.order.number}`,
798
+ type: 'info'
799
+ })
800
+ },
801
+
802
+ onOrderUpdated(event) {
803
+ // Find and update order in collection
804
+ const order = this.orders.models.find(o => o.id === event.order.id)
805
+ if (order) {
806
+ Object.assign(order, event.order)
807
+ }
808
+ },
809
+
810
+ async loadNewOrders() {
811
+ await this.orders.fetch()
812
+ this.hasNewOrders = false
813
+ }
814
+ }
815
+ }
816
+ </script>
817
+ ```
818
+
819
+ ### With Polling
820
+
821
+ ```markup
822
+ <script>
823
+ export default {
824
+ data() {
825
+ return {
826
+ pollingInterval: null,
827
+ lastUpdated: null
828
+ }
829
+ },
830
+
831
+ mounted() {
832
+ // Initial load
833
+ this.loadData()
834
+
835
+ // Poll every 30 seconds
836
+ this.pollingInterval = setInterval(() => {
837
+ this.loadData()
838
+ }, 30000)
839
+
840
+ // Stop polling when page hidden
841
+ document.addEventListener('visibilitychange', this.handleVisibilityChange)
842
+ },
843
+
844
+ beforeDestroy() {
845
+ if (this.pollingInterval) {
846
+ clearInterval(this.pollingInterval)
847
+ }
848
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange)
849
+ },
850
+
851
+ methods: {
852
+ async loadData() {
853
+ // Only poll if page is visible
854
+ if (document.hidden) return
855
+
856
+ await this.collection.fetch()
857
+ this.lastUpdated = Date.now()
858
+ },
859
+
860
+ handleVisibilityChange() {
861
+ if (document.hidden) {
862
+ // Stop polling
863
+ if (this.pollingInterval) {
864
+ clearInterval(this.pollingInterval)
865
+ this.pollingInterval = null
866
+ }
867
+ } else {
868
+ // Resume polling
869
+ this.loadData()
870
+ this.pollingInterval = setInterval(() => {
871
+ this.loadData()
872
+ }, 30000)
873
+ }
874
+ }
875
+ }
876
+ }
877
+ </script>
878
+ ```
879
+
880
+ ## File Upload with Progress
881
+
882
+ Track upload progress for large files.
883
+
884
+ ### Upload with Progress Bar
885
+
886
+ ```markup
887
+ <template>
888
+ <AwCard title="Upload Files">
889
+ <AwUploader
890
+ v-model="files"
891
+ :uploading="uploading"
892
+ :progress="uploadProgress"
893
+ multiple
894
+ @change="handleUpload"
895
+ />
896
+
897
+ <AwProgress
898
+ v-if="uploading"
899
+ :value="uploadProgress"
900
+ class="mt-4"
901
+ />
902
+
903
+ <AwUploaderFiles
904
+ v-model="uploadedFiles"
905
+ @remove="removeFile"
906
+ />
907
+ </AwCard>
908
+ </template>
909
+
910
+ <script>
911
+ export default {
912
+ data() {
913
+ return {
914
+ files: [],
915
+ uploadedFiles: [],
916
+ uploading: false,
917
+ uploadProgress: 0
918
+ }
919
+ },
920
+
921
+ methods: {
922
+ async handleUpload(files) {
923
+ if (!files || files.length === 0) return
924
+
925
+ this.uploading = true
926
+ this.uploadProgress = 0
927
+
928
+ const formData = new FormData()
929
+ files.forEach(file => {
930
+ formData.append('files[]', file)
931
+ })
932
+ formData.append('shop_uuid', this.$route.params.shop_uuid)
933
+
934
+ try {
935
+ const { data } = await this.$axios.post('/api/files', formData, {
936
+ headers: {
937
+ 'Content-Type': 'multipart/form-data'
938
+ },
939
+ onUploadProgress: (progressEvent) => {
940
+ this.uploadProgress = Math.round(
941
+ (progressEvent.loaded * 100) / progressEvent.total
942
+ )
943
+ }
944
+ })
945
+
946
+ this.uploadedFiles.push(...data.files)
947
+
948
+ this.$notify({
949
+ message: `${data.files.length} file(s) uploaded`,
950
+ type: 'success'
951
+ })
952
+ } catch (error) {
953
+ this.$notify({
954
+ message: 'Failed to upload files',
955
+ type: 'error'
956
+ })
957
+ } finally {
958
+ this.uploading = false
959
+ this.uploadProgress = 0
960
+ this.files = []
961
+ }
962
+ },
963
+
964
+ async removeFile(file) {
965
+ try {
966
+ await this.$axios.delete(`/api/files/${file.id}`)
967
+
968
+ const index = this.uploadedFiles.indexOf(file)
969
+ this.uploadedFiles.splice(index, 1)
970
+
971
+ this.$notify({
972
+ message: 'File removed',
973
+ type: 'success'
974
+ })
975
+ } catch (error) {
976
+ this.$notify({
977
+ message: 'Failed to remove file',
978
+ type: 'error'
979
+ })
980
+ }
981
+ }
982
+ }
983
+ }
984
+ </script>
985
+ ```
986
+
987
+ ## Infinite Scroll
988
+
989
+ Load more data as user scrolls.
990
+
991
+ ### Infinite Scroll Table
992
+
993
+ ```markup
994
+ <template>
995
+ <AwPage title="Products">
996
+ <div ref="scrollContainer" class="overflow-auto" style="max-height: 80vh;">
997
+ <AwCard
998
+ v-for="product in products"
999
+ :key="product.id"
1000
+ class="mb-4"
1001
+ >
1002
+ <h3>{{ product.name }}</h3>
1003
+ <p>{{ product.description }}</p>
1004
+ <p class="text-2xl font-bold">${{ product.price }}</p>
1005
+ </AwCard>
1006
+
1007
+ <div v-if="loading" class="py-6 text-center">
1008
+ <AwProgress indeterminate />
1009
+ </div>
1010
+
1011
+ <div v-if="!hasMore" class="py-6 text-center text-gray-500">
1012
+ No more products
1013
+ </div>
1014
+ </div>
1015
+ </AwPage>
1016
+ </template>
1017
+
1018
+ <script>
1019
+ export default {
1020
+ data() {
1021
+ return {
1022
+ products: [],
1023
+ page: 1,
1024
+ loading: false,
1025
+ hasMore: true
1026
+ }
1027
+ },
1028
+
1029
+ mounted() {
1030
+ this.loadProducts()
1031
+
1032
+ // Add scroll listener
1033
+ this.$refs.scrollContainer.addEventListener('scroll', this.handleScroll)
1034
+ },
1035
+
1036
+ beforeDestroy() {
1037
+ this.$refs.scrollContainer.removeEventListener('scroll', this.handleScroll)
1038
+ },
1039
+
1040
+ methods: {
1041
+ async loadProducts() {
1042
+ if (this.loading || !this.hasMore) return
1043
+
1044
+ this.loading = true
1045
+
1046
+ try {
1047
+ const { data } = await this.$axios.get('/api/products', {
1048
+ params: {
1049
+ shop_uuid: this.$route.params.shop_uuid,
1050
+ page: this.page,
1051
+ per_page: 20
1052
+ }
1053
+ })
1054
+
1055
+ this.products.push(...data.data)
1056
+ this.page++
1057
+ this.hasMore = data.meta.current_page < data.meta.last_page
1058
+ } catch (error) {
1059
+ this.$notify({
1060
+ message: 'Failed to load products',
1061
+ type: 'error'
1062
+ })
1063
+ } finally {
1064
+ this.loading = false
1065
+ }
1066
+ },
1067
+
1068
+ handleScroll() {
1069
+ const container = this.$refs.scrollContainer
1070
+ const scrollPosition = container.scrollTop + container.clientHeight
1071
+ const scrollHeight = container.scrollHeight
1072
+
1073
+ // Load more when scrolled to 80% of content
1074
+ if (scrollPosition >= scrollHeight * 0.8) {
1075
+ this.loadProducts()
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+ </script>
1081
+ ```
1082
+
1083
+ ## Complex Filtering & URL State
1084
+
1085
+ Sync filters with URL query parameters for shareable links.
1086
+
1087
+ ### URL-Synced Filters
1088
+
1089
+ ```markup
1090
+ <template>
1091
+ <AwPage title="Orders">
1092
+ <AwCard class="mb-6">
1093
+ <AwGrid :col="4">
1094
+ <AwSelect
1095
+ v-model="filters.status"
1096
+ :options="statusOptions"
1097
+ label="Status"
1098
+ @input="updateFilters"
1099
+ />
1100
+
1101
+ <AwSelect
1102
+ v-model="filters.payment_method"
1103
+ :options="paymentOptions"
1104
+ label="Payment"
1105
+ @input="updateFilters"
1106
+ />
1107
+
1108
+ <AwDate
1109
+ v-model="filters.date_from"
1110
+ label="From"
1111
+ @input="updateFilters"
1112
+ />
1113
+
1114
+ <AwDate
1115
+ v-model="filters.date_to"
1116
+ label="To"
1117
+ @input="updateFilters"
1118
+ />
1119
+ </AwGrid>
1120
+
1121
+ <AwFlow justify="between" class="mt-4">
1122
+ <AwButton @click="resetFilters">
1123
+ Reset Filters
1124
+ </AwButton>
1125
+
1126
+ <AwButton @click="exportResults" color="accent">
1127
+ Export Results
1128
+ </AwButton>
1129
+ </AwFlow>
1130
+ </AwCard>
1131
+
1132
+ <AwTableBuilder :collection="orders">
1133
+ <AwTableCol field="number" title="Order #" />
1134
+ <AwTableCol field="total" title="Total" />
1135
+ <AwTableCol field="status" title="Status" />
1136
+ </AwTableBuilder>
1137
+ </AwPage>
1138
+ </template>
1139
+
1140
+ <script>
1141
+ import Orders from '~/collections/Orders'
1142
+
1143
+ export default {
1144
+ data() {
1145
+ return {
1146
+ orders: new Orders([], {
1147
+ shop_uuid: this.$route.params.shop_uuid
1148
+ }),
1149
+ filters: {
1150
+ status: null,
1151
+ payment_method: null,
1152
+ date_from: null,
1153
+ date_to: null
1154
+ },
1155
+ statusOptions: ['all', 'pending', 'processing', 'completed'],
1156
+ paymentOptions: ['all', 'card', 'cash', 'bank_transfer']
1157
+ }
1158
+ },
1159
+
1160
+ mounted() {
1161
+ // Load filters from URL
1162
+ this.loadFiltersFromURL()
1163
+
1164
+ // Apply filters
1165
+ this.applyFilters()
1166
+ },
1167
+
1168
+ watch: {
1169
+ '$route.query': {
1170
+ handler() {
1171
+ this.loadFiltersFromURL()
1172
+ this.applyFilters()
1173
+ },
1174
+ deep: true
1175
+ }
1176
+ },
1177
+
1178
+ methods: {
1179
+ loadFiltersFromURL() {
1180
+ this.filters = {
1181
+ status: this.$route.query.status || null,
1182
+ payment_method: this.$route.query.payment_method || null,
1183
+ date_from: this.$route.query.date_from || null,
1184
+ date_to: this.$route.query.date_to || null
1185
+ }
1186
+ },
1187
+
1188
+ updateFilters() {
1189
+ // Update URL query params
1190
+ const query = {}
1191
+
1192
+ Object.keys(this.filters).forEach(key => {
1193
+ if (this.filters[key] && this.filters[key] !== 'all') {
1194
+ query[key] = this.filters[key]
1195
+ }
1196
+ })
1197
+
1198
+ this.$router.push({ query })
1199
+ },
1200
+
1201
+ applyFilters() {
1202
+ const options = {
1203
+ shop_uuid: this.$route.params.shop_uuid
1204
+ }
1205
+
1206
+ Object.keys(this.filters).forEach(key => {
1207
+ if (this.filters[key] && this.filters[key] !== 'all') {
1208
+ options[key] = this.filters[key]
1209
+ }
1210
+ })
1211
+
1212
+ this.orders.setOptions(options)
1213
+ this.orders.fetch()
1214
+ },
1215
+
1216
+ resetFilters() {
1217
+ this.$router.push({ query: {} })
1218
+ },
1219
+
1220
+ async exportResults() {
1221
+ try {
1222
+ const response = await this.$axios.get('/api/orders/export', {
1223
+ params: {
1224
+ shop_uuid: this.$route.params.shop_uuid,
1225
+ ...this.$route.query
1226
+ },
1227
+ responseType: 'blob'
1228
+ })
1229
+
1230
+ // Download file
1231
+ const url = window.URL.createObjectURL(new Blob([response.data]))
1232
+ const link = document.createElement('a')
1233
+ link.href = url
1234
+ link.setAttribute('download', `orders-${Date.now()}.csv`)
1235
+ document.body.appendChild(link)
1236
+ link.click()
1237
+ link.remove()
1238
+
1239
+ this.$notify({
1240
+ message: 'Export completed',
1241
+ type: 'success'
1242
+ })
1243
+ } catch (error) {
1244
+ this.$notify({
1245
+ message: 'Failed to export',
1246
+ type: 'error'
1247
+ })
1248
+ }
1249
+ }
1250
+ }
1251
+ }
1252
+ </script>
1253
+ ```
1254
+
1255
+ ## Permission-Based UI
1256
+
1257
+ Show/hide features based on user permissions.
1258
+
1259
+ ### With CASL Permissions
1260
+
1261
+ ```markup
1262
+ <template>
1263
+ <AwPage title="Customers">
1264
+ <template #buttons>
1265
+ <AwButton
1266
+ v-if="$can('create', 'Customer')"
1267
+ :href="`/shops/${$route.params.shop_uuid}/customers/new`"
1268
+ color="accent"
1269
+ >
1270
+ Add Customer
1271
+ </AwButton>
1272
+
1273
+ <AwButton
1274
+ v-if="$can('export', 'Customer')"
1275
+ @click="exportCustomers"
1276
+ >
1277
+ Export
1278
+ </AwButton>
1279
+ </template>
1280
+
1281
+ <AwTableBuilder :collection="customers">
1282
+ <AwTableCol field="name" title="Name" />
1283
+ <AwTableCol field="email" title="Email" />
1284
+
1285
+ <template #dropdown="{ cell }">
1286
+ <AwDropdownButton>
1287
+ <AwButton
1288
+ v-if="$can('update', cell)"
1289
+ @click="editCustomer(cell)"
1290
+ >
1291
+ Edit
1292
+ </AwButton>
1293
+
1294
+ <AwButton
1295
+ v-if="$can('delete', cell)"
1296
+ @click="deleteCustomer(cell)"
1297
+ >
1298
+ Delete
1299
+ </AwButton>
1300
+
1301
+ <AwButton
1302
+ v-if="$can('view', 'Order') && model.orders_count > 0"
1303
+ @click="viewOrders(model)"
1304
+ >
1305
+ View Orders ({{ model.orders_count }})
1306
+ </AwButton>
1307
+ </AwDropdownButton>
1308
+ </template>
1309
+ </AwTableBuilder>
1310
+ </AwPage>
1311
+ </template>
1312
+
1313
+ <script>
1314
+ import Customers from '~/collections/Customers'
1315
+
1316
+ export default {
1317
+ data() {
1318
+ return {
1319
+ customers: new Customers([], {
1320
+ shop_uuid: this.$route.params.shop_uuid
1321
+ })
1322
+ }
1323
+ },
1324
+
1325
+ methods: {
1326
+ editCustomer(customer) {
1327
+ if (!this.$can('update', customer)) {
1328
+ this.$notify({
1329
+ message: 'You do not have permission to edit this customer',
1330
+ type: 'error'
1331
+ })
1332
+ return
1333
+ }
1334
+
1335
+ this.$router.push(`/shops/${this.$route.params.shop_uuid}/customers/${customer.id}/edit`)
1336
+ },
1337
+
1338
+ async deleteCustomer(customer) {
1339
+ if (!this.$can('delete', customer)) {
1340
+ this.$notify({
1341
+ message: 'You do not have permission to delete this customer',
1342
+ type: 'error'
1343
+ })
1344
+ return
1345
+ }
1346
+
1347
+ const confirmed = await this.$confirm({
1348
+ title: 'Delete Customer',
1349
+ message: 'Are you sure?'
1350
+ })
1351
+
1352
+ if (!confirmed) return
1353
+
1354
+ try {
1355
+ await this.$axios.delete(`/api/customers/${customer.id}`)
1356
+
1357
+ this.$notify({
1358
+ message: 'Customer deleted',
1359
+ type: 'success'
1360
+ })
1361
+
1362
+ await this.customers.fetch()
1363
+ } catch (error) {
1364
+ this.$notify({
1365
+ message: 'Failed to delete customer',
1366
+ type: 'error'
1367
+ })
1368
+ }
1369
+ },
1370
+
1371
+ async exportCustomers() {
1372
+ // Implementation
1373
+ },
1374
+
1375
+ viewOrders(customer) {
1376
+ this.$router.push(`/shops/${this.$route.params.shop_uuid}/orders?customer_id=${customer.id}`)
1377
+ }
1378
+ }
1379
+ }
1380
+ </script>
1381
+ ```
1382
+
1383
+ ## See Also
1384
+
1385
+ - [Common Patterns](./common-patterns.md) - Standard application patterns
1386
+ - [Page Patterns](../guides/page-patterns/) - Page structure guides
1387
+ - [Data Fetching Guide](../guides/data-fetching.md) - Collection and model patterns
1388
+ - [Best Practices](../guides/best-practices.md) - Framework best practices