@finema/finework-layer 0.2.50 → 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.
@@ -0,0 +1,893 @@
1
+ # Component Examples & Usage Guide
2
+
3
+ ## 📚 Table of Contents
4
+
5
+ 1. [StatusBox Component](#statusbox-component)
6
+ 2. [InfoItemList Component](#infoitemlist-component)
7
+ 3. [Button Components](#button-components)
8
+ 4. [Layout Components](#layout-components)
9
+ 5. [Form Patterns](#form-patterns)
10
+ 6. [Data Loading Patterns](#data-loading-patterns)
11
+
12
+ ---
13
+
14
+ ## StatusBox Component
15
+
16
+ ### Basic Usage
17
+
18
+ ```vue
19
+ <template>
20
+ <StatusBox :status="loader.status.value" :data="loader.data.value">
21
+ <template #default="{ data }">
22
+ <Card>
23
+ <h1>{{ data.title }}</h1>
24
+ <p>{{ data.description }}</p>
25
+ </Card>
26
+ </template>
27
+ </StatusBox>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ interface Project {
32
+ id: string
33
+ title: string
34
+ description: string
35
+ }
36
+
37
+ const loader = useObjectLoader<Project>({
38
+ method: 'GET',
39
+ url: '/api/projects/123',
40
+ getRequestOptions: useRequestOptions().auth
41
+ })
42
+
43
+ onMounted(() => loader.run())
44
+ </script>
45
+ ```
46
+
47
+ ### With Skip Condition
48
+
49
+ ```vue
50
+ <template>
51
+ <StatusBox
52
+ :status="loader.status.value"
53
+ :data="loader.data.value"
54
+ :skip="!shouldLoad"
55
+ >
56
+ <template #default="{ data }">
57
+ <!-- Content -->
58
+ </template>
59
+ </StatusBox>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ const shouldLoad = ref(true)
64
+ </script>
65
+ ```
66
+
67
+ ### States Handled
68
+
69
+ 1. **Loading State** - Shows spinner
70
+ 2. **Error State** - Shows error message with icon
71
+ 3. **Not Found State** - Shows "ไม่พบข้อมูล" message
72
+ 4. **Success State** - Renders slot content
73
+
74
+ ---
75
+
76
+ ## InfoItemList Component
77
+
78
+ ### Basic Two-Column Layout
79
+
80
+ ```vue
81
+ <template>
82
+ <InfoItemList
83
+ :items="[
84
+ { label: 'ชื่อโครงการ', value: project.name },
85
+ { label: 'รหัสโครงการ', value: project.code },
86
+ { label: 'สถานะ', value: project.status },
87
+ { label: 'วันที่เริ่มต้น', value: project.start_date, type: 'date' }
88
+ ]"
89
+ />
90
+ </template>
91
+ ```
92
+
93
+ ### Vertical Layout
94
+
95
+ ```vue
96
+ <template>
97
+ <InfoItemList
98
+ :items="items"
99
+ :vertical="true"
100
+ />
101
+ </template>
102
+ ```
103
+
104
+ ### Inline Layout
105
+
106
+ ```vue
107
+ <template>
108
+ <InfoItemList
109
+ :items="[
110
+ { label: 'Status:', value: 'Active' },
111
+ { label: 'Count:', value: '42' }
112
+ ]"
113
+ :inline="true"
114
+ />
115
+ </template>
116
+ ```
117
+
118
+ ### With Text Truncation
119
+
120
+ ```vue
121
+ <template>
122
+ <InfoItemList
123
+ :items="[
124
+ {
125
+ label: 'Description',
126
+ value: longDescription,
127
+ max: 200 // Truncate at 200 characters
128
+ }
129
+ ]"
130
+ />
131
+ </template>
132
+ ```
133
+
134
+ ### With Custom Components
135
+
136
+ ```vue
137
+ <template>
138
+ <InfoItemList
139
+ :items="[
140
+ {
141
+ label: 'Status',
142
+ component: Badge,
143
+ props: {
144
+ label: project.status,
145
+ color: getStatusColor(project.status)
146
+ }
147
+ },
148
+ {
149
+ label: 'Actions',
150
+ component: Button,
151
+ props: {
152
+ label: 'Edit',
153
+ icon: 'ph:pencil-simple',
154
+ onClick: handleEdit
155
+ }
156
+ }
157
+ ]"
158
+ />
159
+ </template>
160
+
161
+ <script setup lang="ts">
162
+ import { Badge, Button } from '#components'
163
+
164
+ const getStatusColor = (status: string) => {
165
+ return status === 'active' ? 'success' : 'neutral'
166
+ }
167
+
168
+ const handleEdit = () => {
169
+ console.log('Edit clicked')
170
+ }
171
+ </script>
172
+ ```
173
+
174
+ ### With Slots
175
+
176
+ ```vue
177
+ <template>
178
+ <InfoItemList :items="items">
179
+ <template #status-item="{ value }">
180
+ <Badge
181
+ :label="value"
182
+ :color="value === 'active' ? 'success' : 'error'"
183
+ />
184
+ </template>
185
+
186
+ <template #actions-item>
187
+ <div class="flex gap-2">
188
+ <Button label="Edit" size="sm" />
189
+ <Button label="Delete" size="sm" color="error" />
190
+ </div>
191
+ </template>
192
+ </InfoItemList>
193
+ </template>
194
+
195
+ <script setup lang="ts">
196
+ const items = [
197
+ { label: 'Name', value: 'John Doe' },
198
+ { label: 'Status', value: 'active', key: 'status' },
199
+ { label: 'Actions', key: 'actions' }
200
+ ]
201
+ </script>
202
+ ```
203
+
204
+ ### All Item Types
205
+
206
+ ```vue
207
+ <template>
208
+ <InfoItemList
209
+ :items="[
210
+ { label: 'Text', value: 'Sample text', type: 'text' },
211
+ { label: 'Number', value: 42, type: 'number' },
212
+ { label: 'Currency', value: 1500.50, type: 'currency' },
213
+ { label: 'Date', value: '2025-11-07', type: 'date' },
214
+ { label: 'DateTime', value: '2025-11-07T10:30:00', type: 'date_time' },
215
+ { label: 'Active', value: true, type: 'boolean' }
216
+ ]"
217
+ />
218
+ </template>
219
+ ```
220
+
221
+ ### Conditional Display
222
+
223
+ ```vue
224
+ <template>
225
+ <InfoItemList
226
+ :items="[
227
+ { label: 'Name', value: user.name },
228
+ { label: 'Email', value: user.email },
229
+ {
230
+ label: 'Admin Email',
231
+ value: user.admin_email,
232
+ hide: !user.is_admin // Conditionally hide
233
+ }
234
+ ]"
235
+ />
236
+ </template>
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Button Components
242
+
243
+ ### ActionIcon
244
+
245
+ ```vue
246
+ <template>
247
+ <div class="flex gap-2">
248
+ <!-- Edit button -->
249
+ <ButtonActionIcon
250
+ icon="ph:pencil-simple"
251
+ color="primary"
252
+ @click="handleEdit"
253
+ />
254
+
255
+ <!-- Delete button -->
256
+ <ButtonActionIcon
257
+ icon="ph:trash"
258
+ color="error"
259
+ @click="handleDelete"
260
+ />
261
+
262
+ <!-- View button with navigation -->
263
+ <ButtonActionIcon
264
+ icon="ph:eye"
265
+ color="neutral"
266
+ :to="`/view/${id}`"
267
+ />
268
+
269
+ <!-- Loading state -->
270
+ <ButtonActionIcon
271
+ icon="ph:check"
272
+ color="success"
273
+ :loading="isSubmitting"
274
+ :disabled="isSubmitting"
275
+ />
276
+ </div>
277
+ </template>
278
+
279
+ <script setup lang="ts">
280
+ const isSubmitting = ref(false)
281
+
282
+ const handleEdit = () => {
283
+ console.log('Edit')
284
+ }
285
+
286
+ const handleDelete = async () => {
287
+ isSubmitting.value = true
288
+ await deleteItem()
289
+ isSubmitting.value = false
290
+ }
291
+ </script>
292
+ ```
293
+
294
+ ### Back Button
295
+
296
+ ```vue
297
+ <template>
298
+ <div class="space-y-4">
299
+ <!-- Default back button -->
300
+ <ButtonBack />
301
+
302
+ <!-- Custom label -->
303
+ <ButtonBack label="กลับไปหน้ารายการ" />
304
+
305
+ <!-- With route -->
306
+ <ButtonBack
307
+ label="กลับไปหน้าหลัก"
308
+ :to="routes.home.to"
309
+ />
310
+
311
+ <!-- With custom handler -->
312
+ <ButtonBack
313
+ label="ยกเลิก"
314
+ @click="handleCancel"
315
+ />
316
+ </div>
317
+ </template>
318
+
319
+ <script setup lang="ts">
320
+ const handleCancel = () => {
321
+ // Custom logic before going back
322
+ if (confirm('Are you sure?')) {
323
+ navigateTo(-1)
324
+ }
325
+ }
326
+ </script>
327
+ ```
328
+
329
+ ---
330
+
331
+ ## Layout Components
332
+
333
+ ### Admin Layout
334
+
335
+ ```vue
336
+ <template>
337
+ <LayoutAdmin
338
+ label="PMO Admin"
339
+ :items="navigationItems"
340
+ >
341
+ <!-- Page Header -->
342
+ <template #page-header>
343
+ <div class="flex items-center justify-between">
344
+ <h1 class="text-2xl font-bold">Dashboard</h1>
345
+ <Button label="New Project" icon="ph:plus" />
346
+ </div>
347
+ </template>
348
+
349
+ <!-- Main Content -->
350
+ <div class="space-y-6">
351
+ <Card>
352
+ <h2>Statistics</h2>
353
+ <!-- Content -->
354
+ </Card>
355
+ </div>
356
+ </LayoutAdmin>
357
+ </template>
358
+
359
+ <script setup lang="ts">
360
+ import type { NavigationMenuItem } from '@nuxt/ui'
361
+
362
+ const navigationItems: NavigationMenuItem[] = [
363
+ {
364
+ label: 'Dashboard',
365
+ icon: 'i-heroicons-home',
366
+ to: '/admin/dashboard'
367
+ },
368
+ {
369
+ label: 'Projects',
370
+ icon: 'i-heroicons-folder',
371
+ children: [
372
+ { label: 'All Projects', to: '/admin/projects' },
373
+ { label: 'Active Projects', to: '/admin/projects/active' },
374
+ { label: 'Archived', to: '/admin/projects/archived' }
375
+ ]
376
+ },
377
+ {
378
+ label: 'Users',
379
+ icon: 'i-heroicons-users',
380
+ to: '/admin/users'
381
+ },
382
+ {
383
+ label: 'Settings',
384
+ icon: 'i-heroicons-cog',
385
+ to: '/admin/settings'
386
+ }
387
+ ]
388
+ </script>
389
+ ```
390
+
391
+ ### User Layout
392
+
393
+ ```vue
394
+ <template>
395
+ <LayoutUser>
396
+ <div class="container mx-auto px-4 py-8">
397
+ <h1>My Projects</h1>
398
+
399
+ <StatusBox :status="projects.status.value" :data="projects.data.value">
400
+ <template #default="{ data }">
401
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
402
+ <Card v-for="project in data" :key="project.id">
403
+ <h3>{{ project.name }}</h3>
404
+ <p>{{ project.description }}</p>
405
+ </Card>
406
+ </div>
407
+ </template>
408
+ </StatusBox>
409
+ </div>
410
+ </LayoutUser>
411
+ </template>
412
+
413
+ <script setup lang="ts">
414
+ const projects = useListLoader({
415
+ method: 'GET',
416
+ url: '/api/my-projects',
417
+ getRequestOptions: useRequestOptions().auth
418
+ })
419
+
420
+ onMounted(() => projects.run())
421
+ </script>
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Form Patterns
427
+
428
+ ### Basic Form with Validation
429
+
430
+ ```vue
431
+ <template>
432
+ <form @submit="onSubmit">
433
+ <div class="space-y-4">
434
+ <FormField name="name" label="Project Name">
435
+ <Input v-model="form.values.name" />
436
+ <FormError name="name" />
437
+ </FormField>
438
+
439
+ <FormField name="description" label="Description">
440
+ <Textarea v-model="form.values.description" />
441
+ <FormError name="description" />
442
+ </FormField>
443
+
444
+ <FormField name="status" label="Status">
445
+ <Select
446
+ v-model="form.values.status"
447
+ :options="statusOptions"
448
+ />
449
+ <FormError name="status" />
450
+ </FormField>
451
+
452
+ <div class="flex gap-2">
453
+ <Button
454
+ type="submit"
455
+ label="Save"
456
+ :loading="form.isSubmitting.value"
457
+ />
458
+ <ButtonBack />
459
+ </div>
460
+ </div>
461
+ </form>
462
+ </template>
463
+
464
+ <script setup lang="ts">
465
+ import * as v from 'valibot'
466
+ import { toTypedSchema } from '@vee-validate/valibot'
467
+
468
+ const form = useForm({
469
+ validationSchema: toTypedSchema(
470
+ v.object({
471
+ name: v.pipe(
472
+ v.string('Name is required'),
473
+ v.minLength(3, 'Name must be at least 3 characters')
474
+ ),
475
+ description: v.optional(v.string(), ''),
476
+ status: v.pipe(
477
+ v.string('Status is required'),
478
+ v.picklist(['active', 'inactive', 'pending'])
479
+ )
480
+ })
481
+ )
482
+ })
483
+
484
+ const statusOptions = [
485
+ { label: 'Active', value: 'active' },
486
+ { label: 'Inactive', value: 'inactive' },
487
+ { label: 'Pending', value: 'pending' }
488
+ ]
489
+
490
+ const onSubmit = form.handleSubmit(async (values) => {
491
+ const loader = useObjectLoader({
492
+ method: 'POST',
493
+ url: '/api/projects',
494
+ getRequestOptions: useRequestOptions().auth
495
+ })
496
+
497
+ await loader.run({ data: values })
498
+
499
+ if (loader.status.value.isSuccess) {
500
+ navigateTo('/projects')
501
+ }
502
+ })
503
+ </script>
504
+ ```
505
+
506
+ ### Form with Date Range
507
+
508
+ ```vue
509
+ <template>
510
+ <form @submit="onSubmit">
511
+ <FormField name="date_range" label="Date Range">
512
+ <DatePicker
513
+ v-model="form.values.date_range"
514
+ range
515
+ />
516
+ <FormError name="date_range" />
517
+ </FormField>
518
+ </form>
519
+ </template>
520
+
521
+ <script setup lang="ts">
522
+ import * as v from 'valibot'
523
+ import { toTypedSchema } from '@vee-validate/valibot'
524
+
525
+ const form = useForm({
526
+ validationSchema: toTypedSchema(
527
+ v.object({
528
+ date_range: v.nullish(
529
+ v.object({
530
+ start: v.union([v.date(), v.string()]),
531
+ end: v.union([v.date(), v.string()])
532
+ })
533
+ )
534
+ })
535
+ )
536
+ })
537
+ </script>
538
+ ```
539
+
540
+ ---
541
+
542
+ ## Data Loading Patterns
543
+
544
+ ### Single Object Loading
545
+
546
+ ```vue
547
+ <template>
548
+ <StatusBox :status="project.status.value" :data="project.data.value">
549
+ <template #default="{ data }">
550
+ <Card>
551
+ <InfoItemList
552
+ :items="[
553
+ { label: 'Name', value: data.name },
554
+ { label: 'Code', value: data.code },
555
+ { label: 'Status', value: data.status }
556
+ ]"
557
+ />
558
+ </Card>
559
+ </template>
560
+ </StatusBox>
561
+ </template>
562
+
563
+ <script setup lang="ts">
564
+ interface Project {
565
+ id: string
566
+ name: string
567
+ code: string
568
+ status: string
569
+ }
570
+
571
+ const route = useRoute()
572
+ const projectId = route.params.id as string
573
+
574
+ const project = useObjectLoader<Project>({
575
+ method: 'GET',
576
+ url: `/api/projects/${projectId}`,
577
+ getRequestOptions: useRequestOptions().auth
578
+ })
579
+
580
+ onMounted(() => project.run())
581
+ </script>
582
+ ```
583
+
584
+ ### List Loading with Pagination
585
+
586
+ ```vue
587
+ <template>
588
+ <div class="space-y-4">
589
+ <!-- Filters -->
590
+ <Card>
591
+ <form @submit.prevent="handleFilter">
592
+ <div class="flex gap-2">
593
+ <Input
594
+ v-model="filters.q"
595
+ placeholder="Search..."
596
+ />
597
+ <Button type="submit" label="Search" />
598
+ </div>
599
+ </form>
600
+ </Card>
601
+
602
+ <!-- List -->
603
+ <StatusBox :status="projects.status.value" :data="projects.data.value">
604
+ <template #default="{ data }">
605
+ <Card>
606
+ <Table :rows="data.items" :columns="columns" />
607
+
608
+ <Pagination
609
+ v-model="page"
610
+ :total="data.total"
611
+ :per-page="perPage"
612
+ @update:model-value="loadProjects"
613
+ />
614
+ </Card>
615
+ </template>
616
+ </StatusBox>
617
+ </div>
618
+ </template>
619
+
620
+ <script setup lang="ts">
621
+ const page = ref(1)
622
+ const perPage = ref(10)
623
+ const filters = reactive({
624
+ q: '',
625
+ status: ''
626
+ })
627
+
628
+ const projects = useListLoader({
629
+ method: 'GET',
630
+ url: '/api/projects',
631
+ getRequestOptions: useRequestOptions().auth
632
+ })
633
+
634
+ const loadProjects = () => {
635
+ projects.run({
636
+ params: {
637
+ page: page.value,
638
+ per_page: perPage.value,
639
+ ...filters
640
+ }
641
+ })
642
+ }
643
+
644
+ const handleFilter = () => {
645
+ page.value = 1
646
+ loadProjects()
647
+ }
648
+
649
+ onMounted(() => loadProjects())
650
+
651
+ const columns = [
652
+ { key: 'name', label: 'Name' },
653
+ { key: 'code', label: 'Code' },
654
+ { key: 'status', label: 'Status' }
655
+ ]
656
+ </script>
657
+ ```
658
+
659
+ ### Multiple Parallel Requests
660
+
661
+ ```vue
662
+ <template>
663
+ <div class="space-y-4">
664
+ <StatusBox :status="user.status.value" :data="user.data.value">
665
+ <template #default="{ data: userData }">
666
+ <Card>
667
+ <h2>User: {{ userData.name }}</h2>
668
+ </Card>
669
+ </template>
670
+ </StatusBox>
671
+
672
+ <StatusBox :status="projects.status.value" :data="projects.data.value">
673
+ <template #default="{ data: projectsData }">
674
+ <Card>
675
+ <h2>Projects ({{ projectsData.length }})</h2>
676
+ </Card>
677
+ </template>
678
+ </StatusBox>
679
+ </div>
680
+ </template>
681
+
682
+ <script setup lang="ts">
683
+ const user = useObjectLoader({
684
+ method: 'GET',
685
+ url: '/api/user/123',
686
+ getRequestOptions: useRequestOptions().auth
687
+ })
688
+
689
+ const projects = useListLoader({
690
+ method: 'GET',
691
+ url: '/api/user/123/projects',
692
+ getRequestOptions: useRequestOptions().auth
693
+ })
694
+
695
+ onMounted(async () => {
696
+ // Load in parallel
697
+ await Promise.all([
698
+ user.run(),
699
+ projects.run()
700
+ ])
701
+ })
702
+ </script>
703
+ ```
704
+
705
+ ### Dependent Requests
706
+
707
+ ```vue
708
+ <script setup lang="ts">
709
+ const user = useObjectLoader({
710
+ method: 'GET',
711
+ url: '/api/user/123',
712
+ getRequestOptions: useRequestOptions().auth
713
+ })
714
+
715
+ const projects = useListLoader({
716
+ method: 'GET',
717
+ url: computed(() => `/api/teams/${user.data.value?.team_id}/projects`),
718
+ getRequestOptions: useRequestOptions().auth
719
+ })
720
+
721
+ onMounted(async () => {
722
+ // Load user first
723
+ await user.run()
724
+
725
+ // Then load projects based on user's team
726
+ if (user.status.value.isSuccess && user.data.value?.team_id) {
727
+ await projects.run()
728
+ }
729
+ })
730
+ </script>
731
+ ```
732
+
733
+ ---
734
+
735
+ ## Complete Page Example
736
+
737
+ ```vue
738
+ <template>
739
+ <LayoutAdmin label="Projects" :items="navItems">
740
+ <div class="space-y-6">
741
+ <!-- Page Header -->
742
+ <div class="flex items-center justify-between">
743
+ <div>
744
+ <h1 class="text-2xl font-bold">Projects</h1>
745
+ <p class="text-gray-600">Manage your projects</p>
746
+ </div>
747
+ <Button
748
+ label="New Project"
749
+ icon="ph:plus"
750
+ :to="routes.pmo.project.new.to"
751
+ />
752
+ </div>
753
+
754
+ <!-- Filters -->
755
+ <Card>
756
+ <form @submit.prevent="handleFilter">
757
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
758
+ <FormField name="q" label="Search">
759
+ <Input v-model="form.values.q" placeholder="Search..." />
760
+ </FormField>
761
+
762
+ <FormField name="status" label="Status">
763
+ <Select
764
+ v-model="form.values.status"
765
+ :options="statusOptions"
766
+ />
767
+ </FormField>
768
+
769
+ <div class="flex items-end gap-2">
770
+ <Button type="submit" label="Filter" />
771
+ <Button
772
+ type="button"
773
+ label="Reset"
774
+ variant="outline"
775
+ @click="handleReset"
776
+ />
777
+ </div>
778
+ </div>
779
+ </form>
780
+ </Card>
781
+
782
+ <!-- Projects List -->
783
+ <StatusBox :status="projects.status.value" :data="projects.data.value">
784
+ <template #default="{ data }">
785
+ <Card>
786
+ <Table
787
+ :rows="data.items"
788
+ :columns="columns"
789
+ >
790
+ <template #actions="{ row }">
791
+ <div class="flex gap-2">
792
+ <ButtonActionIcon
793
+ icon="ph:eye"
794
+ :to="`/projects/${row.id}`"
795
+ />
796
+ <ButtonActionIcon
797
+ icon="ph:pencil-simple"
798
+ color="primary"
799
+ :to="`/projects/${row.id}/edit`"
800
+ />
801
+ </div>
802
+ </template>
803
+ </Table>
804
+
805
+ <Pagination
806
+ v-model="page"
807
+ :total="data.total"
808
+ :per-page="perPage"
809
+ />
810
+ </Card>
811
+ </template>
812
+ </StatusBox>
813
+ </div>
814
+ </LayoutAdmin>
815
+ </template>
816
+
817
+ <script setup lang="ts">
818
+ import * as v from 'valibot'
819
+ import { toTypedSchema } from '@vee-validate/valibot'
820
+
821
+ definePageMeta({
822
+ middleware: ['auth', 'permissions'],
823
+ accessGuard: {
824
+ permissions: ['pmo:USER', 'pmo:ADMIN']
825
+ }
826
+ })
827
+
828
+ const page = ref(1)
829
+ const perPage = ref(10)
830
+
831
+ const form = useForm({
832
+ validationSchema: toTypedSchema(
833
+ v.object({
834
+ q: v.optional(v.string(), ''),
835
+ status: v.optional(v.string(), '')
836
+ })
837
+ )
838
+ })
839
+
840
+ const projects = useListLoader({
841
+ method: 'GET',
842
+ url: '/api/projects',
843
+ getRequestOptions: useRequestOptions().auth
844
+ })
845
+
846
+ const loadProjects = () => {
847
+ projects.run({
848
+ params: {
849
+ page: page.value,
850
+ per_page: perPage.value,
851
+ ...form.values
852
+ }
853
+ })
854
+ }
855
+
856
+ const handleFilter = () => {
857
+ page.value = 1
858
+ loadProjects()
859
+ }
860
+
861
+ const handleReset = () => {
862
+ form.resetForm()
863
+ handleFilter()
864
+ }
865
+
866
+ watch(page, () => loadProjects())
867
+
868
+ onMounted(() => loadProjects())
869
+
870
+ const columns = [
871
+ { key: 'name', label: 'Name' },
872
+ { key: 'code', label: 'Code' },
873
+ { key: 'status', label: 'Status' },
874
+ { key: 'actions', label: 'Actions' }
875
+ ]
876
+
877
+ const statusOptions = [
878
+ { label: 'All', value: '' },
879
+ { label: 'Active', value: 'active' },
880
+ { label: 'Inactive', value: 'inactive' }
881
+ ]
882
+
883
+ const navItems = [
884
+ { label: 'Dashboard', to: '/admin', icon: 'i-heroicons-home' },
885
+ { label: 'Projects', to: '/admin/projects', icon: 'i-heroicons-folder' }
886
+ ]
887
+ </script>
888
+ ```
889
+
890
+ ---
891
+
892
+ **Last Updated:** 2025-11-07
893
+