@finema/finework-layer 0.2.49 → 0.2.51

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/EXAMPLES.md ADDED
@@ -0,0 +1,597 @@
1
+ # Real-World Examples
2
+
3
+ This document contains complete, copy-paste ready examples for common use cases.
4
+
5
+ ## 📋 Table of Contents
6
+
7
+ 1. [CRUD Operations](#crud-operations)
8
+ 2. [Dashboard Page](#dashboard-page)
9
+ 3. [User Management](#user-management)
10
+ 4. [Data Export](#data-export)
11
+ 5. [File Upload](#file-upload)
12
+ 6. [Advanced Filtering](#advanced-filtering)
13
+
14
+ ---
15
+
16
+ ## CRUD Operations
17
+
18
+ ### Complete CRUD Example
19
+
20
+ #### List Page
21
+
22
+ ```vue
23
+ <!-- pages/products/index.vue -->
24
+ <template>
25
+ <LayoutAdmin label="Products" :items="navItems">
26
+ <div class="space-y-6">
27
+ <!-- Header -->
28
+ <div class="flex items-center justify-between">
29
+ <div>
30
+ <h1 class="text-2xl font-bold">Products</h1>
31
+ <p class="text-gray-600">Manage your product catalog</p>
32
+ </div>
33
+ <Button
34
+ label="Add Product"
35
+ icon="ph:plus"
36
+ to="/products/new"
37
+ />
38
+ </div>
39
+
40
+ <!-- Filters -->
41
+ <Card>
42
+ <form @submit.prevent="handleFilter" class="grid grid-cols-1 md:grid-cols-4 gap-4">
43
+ <Input
44
+ v-model="filters.q"
45
+ placeholder="Search products..."
46
+ icon="ph:magnifying-glass"
47
+ />
48
+
49
+ <Select
50
+ v-model="filters.category"
51
+ :options="categoryOptions"
52
+ placeholder="Category"
53
+ />
54
+
55
+ <Select
56
+ v-model="filters.status"
57
+ :options="statusOptions"
58
+ placeholder="Status"
59
+ />
60
+
61
+ <div class="flex gap-2">
62
+ <Button type="submit" label="Filter" class="flex-1" />
63
+ <Button
64
+ type="button"
65
+ label="Reset"
66
+ variant="outline"
67
+ @click="handleReset"
68
+ />
69
+ </div>
70
+ </form>
71
+ </Card>
72
+
73
+ <!-- Products Table -->
74
+ <StatusBox :status="products.status.value" :data="products.data.value">
75
+ <template #default="{ data }">
76
+ <Card>
77
+ <Table :rows="data.items" :columns="columns">
78
+ <template #name="{ row }">
79
+ <div class="flex items-center gap-3">
80
+ <img
81
+ :src="row.image_url"
82
+ :alt="row.name"
83
+ class="w-10 h-10 rounded object-cover"
84
+ />
85
+ <div>
86
+ <p class="font-medium">{{ row.name }}</p>
87
+ <p class="text-sm text-gray-500">{{ row.sku }}</p>
88
+ </div>
89
+ </div>
90
+ </template>
91
+
92
+ <template #price="{ row }">
93
+ {{ NumberHelper.formatCurrency(row.price) }}
94
+ </template>
95
+
96
+ <template #status="{ row }">
97
+ <Badge
98
+ :label="row.status"
99
+ :color="row.status === 'active' ? 'success' : 'neutral'"
100
+ />
101
+ </template>
102
+
103
+ <template #actions="{ row }">
104
+ <div class="flex gap-2">
105
+ <ButtonActionIcon
106
+ icon="ph:eye"
107
+ :to="`/products/${row.id}`"
108
+ />
109
+ <ButtonActionIcon
110
+ icon="ph:pencil-simple"
111
+ color="primary"
112
+ :to="`/products/${row.id}/edit`"
113
+ />
114
+ <ButtonActionIcon
115
+ icon="ph:trash"
116
+ color="error"
117
+ @click="handleDelete(row.id)"
118
+ />
119
+ </div>
120
+ </template>
121
+ </Table>
122
+
123
+ <div class="mt-4">
124
+ <Pagination
125
+ v-model="page"
126
+ :total="data.total"
127
+ :per-page="perPage"
128
+ />
129
+ </div>
130
+ </Card>
131
+ </template>
132
+ </StatusBox>
133
+ </div>
134
+ </LayoutAdmin>
135
+ </template>
136
+
137
+ <script setup lang="ts">
138
+ definePageMeta({
139
+ middleware: ['auth', 'permissions'],
140
+ accessGuard: {
141
+ permissions: ['pmo:USER', 'pmo:ADMIN']
142
+ }
143
+ })
144
+
145
+ interface Product {
146
+ id: string
147
+ name: string
148
+ sku: string
149
+ price: number
150
+ category: string
151
+ status: string
152
+ image_url: string
153
+ }
154
+
155
+ const page = ref(1)
156
+ const perPage = ref(10)
157
+ const filters = reactive({
158
+ q: '',
159
+ category: '',
160
+ status: ''
161
+ })
162
+
163
+ const products = useListLoader<Product>({
164
+ method: 'GET',
165
+ url: '/api/products',
166
+ getRequestOptions: useRequestOptions().auth
167
+ })
168
+
169
+ const loadProducts = () => {
170
+ products.run({
171
+ params: {
172
+ page: page.value,
173
+ per_page: perPage.value,
174
+ ...filters
175
+ }
176
+ })
177
+ }
178
+
179
+ const handleFilter = () => {
180
+ page.value = 1
181
+ loadProducts()
182
+ }
183
+
184
+ const handleReset = () => {
185
+ Object.assign(filters, { q: '', category: '', status: '' })
186
+ handleFilter()
187
+ }
188
+
189
+ const handleDelete = async (id: string) => {
190
+ const dialog = useDialog()
191
+
192
+ const confirmed = await dialog.confirm({
193
+ title: 'Delete Product',
194
+ description: 'Are you sure you want to delete this product?'
195
+ })
196
+
197
+ if (!confirmed) return
198
+
199
+ const deleteLoader = useObjectLoader({
200
+ method: 'DELETE',
201
+ url: `/api/products/${id}`,
202
+ getRequestOptions: useRequestOptions().auth
203
+ })
204
+
205
+ await deleteLoader.run()
206
+
207
+ if (deleteLoader.status.value.isSuccess) {
208
+ loadProducts()
209
+ }
210
+ }
211
+
212
+ watch(page, () => loadProducts())
213
+
214
+ onMounted(() => loadProducts())
215
+
216
+ const columns = [
217
+ { key: 'name', label: 'Product' },
218
+ { key: 'category', label: 'Category' },
219
+ { key: 'price', label: 'Price' },
220
+ { key: 'status', label: 'Status' },
221
+ { key: 'actions', label: 'Actions' }
222
+ ]
223
+
224
+ const categoryOptions = [
225
+ { label: 'All Categories', value: '' },
226
+ { label: 'Electronics', value: 'electronics' },
227
+ { label: 'Clothing', value: 'clothing' },
228
+ { label: 'Food', value: 'food' }
229
+ ]
230
+
231
+ const statusOptions = [
232
+ { label: 'All Status', value: '' },
233
+ { label: 'Active', value: 'active' },
234
+ { label: 'Inactive', value: 'inactive' }
235
+ ]
236
+
237
+ const navItems = [
238
+ { label: 'Dashboard', to: '/dashboard', icon: 'i-heroicons-home' },
239
+ { label: 'Products', to: '/products', icon: 'i-heroicons-shopping-bag' }
240
+ ]
241
+ </script>
242
+ ```
243
+
244
+ #### Create/Edit Form
245
+
246
+ ```vue
247
+ <!-- pages/products/[id]/edit.vue -->
248
+ <template>
249
+ <LayoutAdmin label="Edit Product" :items="navItems">
250
+ <StatusBox :status="product.status.value" :data="product.data.value">
251
+ <template #default="{ data }">
252
+ <Card>
253
+ <form @submit="onSubmit" class="space-y-6">
254
+ <!-- Product Image -->
255
+ <div>
256
+ <label class="block text-sm font-medium mb-2">Product Image</label>
257
+ <div class="flex items-center gap-4">
258
+ <img
259
+ :src="imagePreview || data.image_url"
260
+ alt="Product"
261
+ class="w-32 h-32 rounded object-cover"
262
+ />
263
+ <input
264
+ type="file"
265
+ @change="handleImageChange"
266
+ accept="image/*"
267
+ />
268
+ </div>
269
+ </div>
270
+
271
+ <!-- Basic Info -->
272
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
273
+ <FormField name="name" label="Product Name">
274
+ <Input v-model="form.values.name" />
275
+ <FormError name="name" />
276
+ </FormField>
277
+
278
+ <FormField name="sku" label="SKU">
279
+ <Input v-model="form.values.sku" />
280
+ <FormError name="sku" />
281
+ </FormField>
282
+
283
+ <FormField name="category" label="Category">
284
+ <Select
285
+ v-model="form.values.category"
286
+ :options="categoryOptions"
287
+ />
288
+ <FormError name="category" />
289
+ </FormField>
290
+
291
+ <FormField name="price" label="Price">
292
+ <Input
293
+ v-model="form.values.price"
294
+ type="number"
295
+ step="0.01"
296
+ />
297
+ <FormError name="price" />
298
+ </FormField>
299
+
300
+ <FormField name="stock" label="Stock">
301
+ <Input
302
+ v-model="form.values.stock"
303
+ type="number"
304
+ />
305
+ <FormError name="stock" />
306
+ </FormField>
307
+
308
+ <FormField name="status" label="Status">
309
+ <Select
310
+ v-model="form.values.status"
311
+ :options="statusOptions"
312
+ />
313
+ <FormError name="status" />
314
+ </FormField>
315
+ </div>
316
+
317
+ <!-- Description -->
318
+ <FormField name="description" label="Description">
319
+ <Textarea
320
+ v-model="form.values.description"
321
+ rows="4"
322
+ />
323
+ <FormError name="description" />
324
+ </FormField>
325
+
326
+ <!-- Actions -->
327
+ <div class="flex gap-2">
328
+ <Button
329
+ type="submit"
330
+ label="Save Changes"
331
+ :loading="form.isSubmitting.value"
332
+ />
333
+ <ButtonBack to="/products" />
334
+ </div>
335
+ </form>
336
+ </Card>
337
+ </template>
338
+ </StatusBox>
339
+ </LayoutAdmin>
340
+ </template>
341
+
342
+ <script setup lang="ts">
343
+ import * as v from 'valibot'
344
+ import { toTypedSchema } from '@vee-validate/valibot'
345
+
346
+ definePageMeta({
347
+ middleware: ['auth', 'permissions'],
348
+ accessGuard: {
349
+ permissions: ['pmo:ADMIN']
350
+ }
351
+ })
352
+
353
+ const route = useRoute()
354
+ const productId = route.params.id as string
355
+
356
+ const imagePreview = ref<string | null>(null)
357
+ const imageFile = ref<File | null>(null)
358
+
359
+ // Load product data
360
+ const product = useObjectLoader({
361
+ method: 'GET',
362
+ url: `/api/products/${productId}`,
363
+ getRequestOptions: useRequestOptions().auth
364
+ })
365
+
366
+ // Form setup
367
+ const form = useForm({
368
+ validationSchema: toTypedSchema(
369
+ v.object({
370
+ name: v.pipe(v.string(), v.minLength(3)),
371
+ sku: v.pipe(v.string(), v.minLength(3)),
372
+ category: v.string(),
373
+ price: v.pipe(v.number(), v.minValue(0)),
374
+ stock: v.pipe(v.number(), v.minValue(0)),
375
+ status: v.string(),
376
+ description: v.optional(v.string(), '')
377
+ })
378
+ )
379
+ })
380
+
381
+ // Load product and populate form
382
+ onMounted(async () => {
383
+ await product.run()
384
+
385
+ if (product.status.value.isSuccess && product.data.value) {
386
+ form.setValues(product.data.value)
387
+ }
388
+ })
389
+
390
+ const handleImageChange = (event: Event) => {
391
+ const target = event.target as HTMLInputElement
392
+ const file = target.files?.[0]
393
+
394
+ if (file) {
395
+ imageFile.value = file
396
+ imagePreview.value = URL.createObjectURL(file)
397
+ }
398
+ }
399
+
400
+ const onSubmit = form.handleSubmit(async (values) => {
401
+ // Upload image first if changed
402
+ let imageUrl = product.data.value?.image_url
403
+
404
+ if (imageFile.value) {
405
+ const formData = new FormData()
406
+ formData.append('file', imageFile.value)
407
+
408
+ const uploadLoader = useObjectLoader({
409
+ method: 'POST',
410
+ url: '/api/upload',
411
+ getRequestOptions: useRequestOptions().file
412
+ })
413
+
414
+ await uploadLoader.run({ data: formData })
415
+
416
+ if (uploadLoader.status.value.isSuccess) {
417
+ imageUrl = uploadLoader.data.value.url
418
+ }
419
+ }
420
+
421
+ // Update product
422
+ const updateLoader = useObjectLoader({
423
+ method: 'PUT',
424
+ url: `/api/products/${productId}`,
425
+ getRequestOptions: useRequestOptions().auth
426
+ })
427
+
428
+ await updateLoader.run({
429
+ data: {
430
+ ...values,
431
+ image_url: imageUrl
432
+ }
433
+ })
434
+
435
+ if (updateLoader.status.value.isSuccess) {
436
+ navigateTo('/products')
437
+ }
438
+ })
439
+
440
+ const categoryOptions = [
441
+ { label: 'Electronics', value: 'electronics' },
442
+ { label: 'Clothing', value: 'clothing' },
443
+ { label: 'Food', value: 'food' }
444
+ ]
445
+
446
+ const statusOptions = [
447
+ { label: 'Active', value: 'active' },
448
+ { label: 'Inactive', value: 'inactive' }
449
+ ]
450
+
451
+ const navItems = [
452
+ { label: 'Products', to: '/products', icon: 'i-heroicons-shopping-bag' }
453
+ ]
454
+ </script>
455
+ ```
456
+
457
+ ---
458
+
459
+ ## Dashboard Page
460
+
461
+ ```vue
462
+ <!-- pages/dashboard.vue -->
463
+ <template>
464
+ <LayoutAdmin label="Dashboard" :items="navItems">
465
+ <div class="space-y-6">
466
+ <!-- Stats Cards -->
467
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
468
+ <Card v-for="stat in stats" :key="stat.label">
469
+ <div class="flex items-center justify-between">
470
+ <div>
471
+ <p class="text-sm text-gray-600">{{ stat.label }}</p>
472
+ <p class="text-2xl font-bold mt-1">{{ stat.value }}</p>
473
+ <p
474
+ :class="[
475
+ 'text-sm mt-1',
476
+ stat.change >= 0 ? 'text-success' : 'text-error'
477
+ ]"
478
+ >
479
+ {{ stat.change >= 0 ? '+' : '' }}{{ stat.change }}%
480
+ </p>
481
+ </div>
482
+ <div
483
+ :class="[
484
+ 'w-12 h-12 rounded-full flex items-center justify-center',
485
+ stat.color
486
+ ]"
487
+ >
488
+ <Icon :name="stat.icon" class="text-2xl" />
489
+ </div>
490
+ </div>
491
+ </Card>
492
+ </div>
493
+
494
+ <!-- Charts -->
495
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
496
+ <Card>
497
+ <h3 class="text-lg font-semibold mb-4">Revenue Overview</h3>
498
+ <!-- Add your chart component here -->
499
+ </Card>
500
+
501
+ <Card>
502
+ <h3 class="text-lg font-semibold mb-4">Top Products</h3>
503
+ <!-- Add your chart component here -->
504
+ </Card>
505
+ </div>
506
+
507
+ <!-- Recent Activities -->
508
+ <Card>
509
+ <h3 class="text-lg font-semibold mb-4">Recent Activities</h3>
510
+ <StatusBox :status="activities.status.value" :data="activities.data.value">
511
+ <template #default="{ data }">
512
+ <div class="space-y-3">
513
+ <div
514
+ v-for="activity in data"
515
+ :key="activity.id"
516
+ class="flex items-center gap-3 p-3 hover:bg-gray-50 rounded"
517
+ >
518
+ <Avatar :src="activity.user.avatar_url" />
519
+ <div class="flex-1">
520
+ <p class="font-medium">{{ activity.user.name }}</p>
521
+ <p class="text-sm text-gray-600">{{ activity.description }}</p>
522
+ </div>
523
+ <p class="text-sm text-gray-500">
524
+ {{ formatRelativeTime(activity.created_at) }}
525
+ </p>
526
+ </div>
527
+ </div>
528
+ </template>
529
+ </StatusBox>
530
+ </Card>
531
+ </div>
532
+ </LayoutAdmin>
533
+ </template>
534
+
535
+ <script setup lang="ts">
536
+ definePageMeta({
537
+ middleware: ['auth']
538
+ })
539
+
540
+ const stats = ref([
541
+ {
542
+ label: 'Total Revenue',
543
+ value: '$45,231',
544
+ change: 12.5,
545
+ icon: 'ph:currency-dollar',
546
+ color: 'bg-success/10 text-success'
547
+ },
548
+ {
549
+ label: 'Total Orders',
550
+ value: '1,234',
551
+ change: 8.2,
552
+ icon: 'ph:shopping-cart',
553
+ color: 'bg-primary/10 text-primary'
554
+ },
555
+ {
556
+ label: 'Total Customers',
557
+ value: '567',
558
+ change: -2.4,
559
+ icon: 'ph:users',
560
+ color: 'bg-warning/10 text-warning'
561
+ },
562
+ {
563
+ label: 'Conversion Rate',
564
+ value: '3.2%',
565
+ change: 5.1,
566
+ icon: 'ph:chart-line',
567
+ color: 'bg-info/10 text-info'
568
+ }
569
+ ])
570
+
571
+ const activities = useListLoader({
572
+ method: 'GET',
573
+ url: '/api/activities',
574
+ getRequestOptions: useRequestOptions().auth
575
+ })
576
+
577
+ onMounted(() => activities.run({ params: { limit: 10 } }))
578
+
579
+ const formatRelativeTime = (date: string) => {
580
+ // Implement relative time formatting
581
+ return new Date(date).toLocaleDateString()
582
+ }
583
+
584
+ const navItems = [
585
+ { label: 'Dashboard', to: '/dashboard', icon: 'i-heroicons-home' }
586
+ ]
587
+ </script>
588
+ ```
589
+
590
+ ---
591
+
592
+ **More examples available in [COMPONENT_EXAMPLES.md](./COMPONENT_EXAMPLES.md)**
593
+
594
+ ---
595
+
596
+ **Last Updated:** 2025-11-07
597
+