@finema/finework-layer 0.2.50 โ†’ 0.2.52

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/QUICK_START.md ADDED
@@ -0,0 +1,678 @@
1
+ # Quick Start Guide
2
+
3
+ ## ๐Ÿš€ Getting Started
4
+
5
+ ### Prerequisites
6
+
7
+ - Node.js 18+ or Bun
8
+ - pnpm (recommended) or npm
9
+ - Basic knowledge of Vue 3 and Nuxt 3
10
+
11
+ ### Installation
12
+
13
+ #### 1. Install the Layer
14
+
15
+ ```bash
16
+ pnpm add @finema/finework-layer
17
+ ```
18
+
19
+ #### 2. Extend in Your Nuxt Config
20
+
21
+ ```typescript
22
+ // nuxt.config.ts
23
+ export default defineNuxtConfig({
24
+ extends: ['@finema/finework-layer']
25
+ })
26
+ ```
27
+
28
+ #### 3. Configure Runtime Variables
29
+
30
+ ```typescript
31
+ // nuxt.config.ts
32
+ export default defineNuxtConfig({
33
+ extends: ['@finema/finework-layer'],
34
+
35
+ runtimeConfig: {
36
+ public: {
37
+ baseAPI: process.env.NUXT_PUBLIC_BASE_API || 'http://localhost:8000',
38
+ baseAPIMock: process.env.NUXT_PUBLIC_BASE_API_MOCK || 'http://localhost:3000/api/mock'
39
+ }
40
+ }
41
+ })
42
+ ```
43
+
44
+ #### 4. Create Environment File
45
+
46
+ ```bash
47
+ # .env
48
+ NUXT_PUBLIC_BASE_API=https://api.yourapp.com
49
+ ```
50
+
51
+ ---
52
+
53
+ ## ๐Ÿ“ Your First Page
54
+
55
+ ### 1. Create a Protected Page
56
+
57
+ ```vue
58
+ <!-- pages/projects/index.vue -->
59
+ <template>
60
+ <LayoutAdmin label="Projects" :items="navItems">
61
+ <div class="space-y-6">
62
+ <h1 class="text-2xl font-bold">My Projects</h1>
63
+
64
+ <StatusBox :status="projects.status.value" :data="projects.data.value">
65
+ <template #default="{ data }">
66
+ <Card>
67
+ <div v-for="project in data" :key="project.id" class="p-4 border-b">
68
+ <h3 class="font-semibold">{{ project.name }}</h3>
69
+ <p class="text-gray-600">{{ project.description }}</p>
70
+ </div>
71
+ </Card>
72
+ </template>
73
+ </StatusBox>
74
+ </div>
75
+ </LayoutAdmin>
76
+ </template>
77
+
78
+ <script setup lang="ts">
79
+ // Define page metadata
80
+ definePageMeta({
81
+ middleware: ['auth', 'permissions'],
82
+ accessGuard: {
83
+ permissions: ['pmo:USER', 'pmo:ADMIN']
84
+ }
85
+ })
86
+
87
+ // Define navigation
88
+ const navItems = [
89
+ { label: 'Dashboard', to: '/dashboard', icon: 'i-heroicons-home' },
90
+ { label: 'Projects', to: '/projects', icon: 'i-heroicons-folder' }
91
+ ]
92
+
93
+ // Load data
94
+ interface Project {
95
+ id: string
96
+ name: string
97
+ description: string
98
+ }
99
+
100
+ const projects = useListLoader<Project>({
101
+ method: 'GET',
102
+ url: '/api/projects',
103
+ getRequestOptions: useRequestOptions().auth
104
+ })
105
+
106
+ onMounted(() => projects.run())
107
+ </script>
108
+ ```
109
+
110
+ ### 2. Create a Form Page
111
+
112
+ ```vue
113
+ <!-- pages/projects/new.vue -->
114
+ <template>
115
+ <LayoutAdmin label="New Project" :items="navItems">
116
+ <Card>
117
+ <form @submit="onSubmit" class="space-y-4">
118
+ <FormField name="name" label="Project Name">
119
+ <Input v-model="form.values.name" />
120
+ <FormError name="name" />
121
+ </FormField>
122
+
123
+ <FormField name="description" label="Description">
124
+ <Textarea v-model="form.values.description" />
125
+ <FormError name="description" />
126
+ </FormField>
127
+
128
+ <div class="flex gap-2">
129
+ <Button
130
+ type="submit"
131
+ label="Create Project"
132
+ :loading="form.isSubmitting.value"
133
+ />
134
+ <ButtonBack :to="routes.pmo.project.projects.to" />
135
+ </div>
136
+ </form>
137
+ </Card>
138
+ </LayoutAdmin>
139
+ </template>
140
+
141
+ <script setup lang="ts">
142
+ import * as v from 'valibot'
143
+ import { toTypedSchema } from '@vee-validate/valibot'
144
+
145
+ definePageMeta({
146
+ middleware: ['auth', 'permissions'],
147
+ accessGuard: {
148
+ permissions: ['pmo:ADMIN']
149
+ }
150
+ })
151
+
152
+ const form = useForm({
153
+ validationSchema: toTypedSchema(
154
+ v.object({
155
+ name: v.pipe(
156
+ v.string('Name is required'),
157
+ v.minLength(3, 'Name must be at least 3 characters')
158
+ ),
159
+ description: v.optional(v.string(), '')
160
+ })
161
+ )
162
+ })
163
+
164
+ const onSubmit = form.handleSubmit(async (values) => {
165
+ const loader = useObjectLoader({
166
+ method: 'POST',
167
+ url: '/api/projects',
168
+ getRequestOptions: useRequestOptions().auth
169
+ })
170
+
171
+ await loader.run({ data: values })
172
+
173
+ if (loader.status.value.isSuccess) {
174
+ navigateTo(routes.pmo.project.projects.to)
175
+ }
176
+ })
177
+
178
+ const navItems = [
179
+ { label: 'Projects', to: '/projects', icon: 'i-heroicons-folder' }
180
+ ]
181
+ </script>
182
+ ```
183
+
184
+ ---
185
+
186
+ ## ๐ŸŽฏ Common Tasks
187
+
188
+ ### Task 1: Add a New Route
189
+
190
+ #### Step 1: Define Route Constant
191
+
192
+ ```typescript
193
+ // app/constants/routes.ts (in your project)
194
+ export const myRoutes = {
195
+ ...routes, // Extend existing routes
196
+
197
+ myModule: {
198
+ dashboard: {
199
+ label: 'Dashboard',
200
+ to: '/my-module/dashboard',
201
+ icon: 'i-heroicons-chart-bar'
202
+ },
203
+ settings: {
204
+ label: 'Settings',
205
+ to: '/my-module/settings',
206
+ icon: 'i-heroicons-cog'
207
+ }
208
+ }
209
+ }
210
+ ```
211
+
212
+ #### Step 2: Create Page
213
+
214
+ ```vue
215
+ <!-- pages/my-module/dashboard.vue -->
216
+ <template>
217
+ <LayoutAdmin label="My Module" :items="navItems">
218
+ <h1>Dashboard</h1>
219
+ </LayoutAdmin>
220
+ </template>
221
+
222
+ <script setup lang="ts">
223
+ const navItems = [
224
+ { label: 'Dashboard', to: myRoutes.myModule.dashboard.to },
225
+ { label: 'Settings', to: myRoutes.myModule.settings.to }
226
+ ]
227
+ </script>
228
+ ```
229
+
230
+ ---
231
+
232
+ ### Task 2: Create a Custom Component
233
+
234
+ ```vue
235
+ <!-- components/ProjectCard.vue -->
236
+ <template>
237
+ <Card class="hover:shadow-lg transition-shadow">
238
+ <div class="space-y-2">
239
+ <div class="flex items-center justify-between">
240
+ <h3 class="text-lg font-semibold">{{ project.name }}</h3>
241
+ <Badge :label="project.status" :color="statusColor" />
242
+ </div>
243
+
244
+ <p class="text-gray-600">{{ project.description }}</p>
245
+
246
+ <div class="flex gap-2 pt-4">
247
+ <ButtonActionIcon
248
+ icon="ph:eye"
249
+ :to="`/projects/${project.id}`"
250
+ />
251
+ <ButtonActionIcon
252
+ icon="ph:pencil-simple"
253
+ color="primary"
254
+ :to="`/projects/${project.id}/edit`"
255
+ />
256
+ </div>
257
+ </div>
258
+ </Card>
259
+ </template>
260
+
261
+ <script setup lang="ts">
262
+ interface Project {
263
+ id: string
264
+ name: string
265
+ description: string
266
+ status: string
267
+ }
268
+
269
+ const props = defineProps<{
270
+ project: Project
271
+ }>()
272
+
273
+ const statusColor = computed(() => {
274
+ return props.project.status === 'active' ? 'success' : 'neutral'
275
+ })
276
+ </script>
277
+ ```
278
+
279
+ Usage:
280
+
281
+ ```vue
282
+ <ProjectCard
283
+ v-for="project in projects"
284
+ :key="project.id"
285
+ :project="project"
286
+ />
287
+ ```
288
+
289
+ ---
290
+
291
+ ### Task 3: Add Permission Check
292
+
293
+ ```vue
294
+ <template>
295
+ <div>
296
+ <!-- Show only to admins -->
297
+ <Button
298
+ v-if="canEdit"
299
+ label="Edit"
300
+ @click="handleEdit"
301
+ />
302
+ </div>
303
+ </template>
304
+
305
+ <script setup lang="ts">
306
+ const auth = useAuth()
307
+
308
+ const canEdit = computed(() =>
309
+ auth.hasPermission(UserModule.PMO, Permission.ADMIN, Permission.SUPER)
310
+ )
311
+
312
+ const handleEdit = () => {
313
+ // Edit logic
314
+ }
315
+ </script>
316
+ ```
317
+
318
+ ---
319
+
320
+ ### Task 4: Handle File Upload
321
+
322
+ ```vue
323
+ <template>
324
+ <form @submit.prevent="handleUpload">
325
+ <FormField name="file" label="Upload File">
326
+ <input
327
+ type="file"
328
+ @change="handleFileChange"
329
+ accept=".pdf,.doc,.docx"
330
+ />
331
+ </FormField>
332
+
333
+ <Button
334
+ type="submit"
335
+ label="Upload"
336
+ :loading="isUploading"
337
+ />
338
+ </form>
339
+ </template>
340
+
341
+ <script setup lang="ts">
342
+ const file = ref<File | null>(null)
343
+ const isUploading = ref(false)
344
+
345
+ const handleFileChange = (event: Event) => {
346
+ const target = event.target as HTMLInputElement
347
+ file.value = target.files?.[0] || null
348
+ }
349
+
350
+ const handleUpload = async () => {
351
+ if (!file.value) return
352
+
353
+ isUploading.value = true
354
+
355
+ const formData = new FormData()
356
+ formData.append('file', file.value)
357
+
358
+ const loader = useObjectLoader({
359
+ method: 'POST',
360
+ url: '/api/upload',
361
+ getRequestOptions: useRequestOptions().file
362
+ })
363
+
364
+ await loader.run({ data: formData })
365
+
366
+ isUploading.value = false
367
+
368
+ if (loader.status.value.isSuccess) {
369
+ console.log('Upload successful:', loader.data.value)
370
+ }
371
+ }
372
+ </script>
373
+ ```
374
+
375
+ ---
376
+
377
+ ### Task 5: Implement Search & Filter
378
+
379
+ ```vue
380
+ <template>
381
+ <div class="space-y-4">
382
+ <!-- Search Form -->
383
+ <Card>
384
+ <form @submit.prevent="handleSearch">
385
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
386
+ <Input
387
+ v-model="filters.q"
388
+ placeholder="Search..."
389
+ />
390
+
391
+ <Select
392
+ v-model="filters.status"
393
+ :options="statusOptions"
394
+ />
395
+
396
+ <div class="flex gap-2">
397
+ <Button type="submit" label="Search" />
398
+ <Button
399
+ type="button"
400
+ label="Reset"
401
+ variant="outline"
402
+ @click="handleReset"
403
+ />
404
+ </div>
405
+ </div>
406
+ </form>
407
+ </Card>
408
+
409
+ <!-- Results -->
410
+ <StatusBox :status="results.status.value" :data="results.data.value">
411
+ <template #default="{ data }">
412
+ <Card>
413
+ <div v-for="item in data.items" :key="item.id">
414
+ {{ item.name }}
415
+ </div>
416
+
417
+ <Pagination
418
+ v-model="page"
419
+ :total="data.total"
420
+ :per-page="perPage"
421
+ />
422
+ </Card>
423
+ </template>
424
+ </StatusBox>
425
+ </div>
426
+ </template>
427
+
428
+ <script setup lang="ts">
429
+ const page = ref(1)
430
+ const perPage = ref(10)
431
+ const filters = reactive({
432
+ q: '',
433
+ status: ''
434
+ })
435
+
436
+ const results = useListLoader({
437
+ method: 'GET',
438
+ url: '/api/search',
439
+ getRequestOptions: useRequestOptions().auth
440
+ })
441
+
442
+ const loadResults = () => {
443
+ results.run({
444
+ params: {
445
+ page: page.value,
446
+ per_page: perPage.value,
447
+ ...filters
448
+ }
449
+ })
450
+ }
451
+
452
+ const handleSearch = () => {
453
+ page.value = 1
454
+ loadResults()
455
+ }
456
+
457
+ const handleReset = () => {
458
+ filters.q = ''
459
+ filters.status = ''
460
+ handleSearch()
461
+ }
462
+
463
+ watch(page, () => loadResults())
464
+
465
+ onMounted(() => loadResults())
466
+
467
+ const statusOptions = [
468
+ { label: 'All', value: '' },
469
+ { label: 'Active', value: 'active' },
470
+ { label: 'Inactive', value: 'inactive' }
471
+ ]
472
+ </script>
473
+ ```
474
+
475
+ ---
476
+
477
+ ## ๐ŸŽจ Customization
478
+
479
+ ### Override UI Theme
480
+
481
+ ```typescript
482
+ // app/app.config.ts
483
+ export default defineAppConfig({
484
+ core: {
485
+ color: '#FF6B6B', // Primary color
486
+ date_format: 'dd/MM/yyyy',
487
+ locale: 'th'
488
+ },
489
+
490
+ ui: {
491
+ button: {
492
+ defaultVariants: {
493
+ color: 'primary',
494
+ variant: 'solid'
495
+ }
496
+ },
497
+
498
+ card: {
499
+ variants: {
500
+ variant: {
501
+ custom: {
502
+ root: 'bg-gradient-to-r from-blue-500 to-purple-500'
503
+ }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ })
509
+ ```
510
+
511
+ ### Add Custom Middleware
512
+
513
+ ```typescript
514
+ // middleware/analytics.ts
515
+ export default defineNuxtRouteMiddleware((to, from) => {
516
+ // Track page view
517
+ console.log('Page view:', to.path)
518
+
519
+ // Send to analytics
520
+ if (import.meta.client) {
521
+ // Your analytics code
522
+ }
523
+ })
524
+ ```
525
+
526
+ Use it:
527
+
528
+ ```typescript
529
+ definePageMeta({
530
+ middleware: ['auth', 'analytics']
531
+ })
532
+ ```
533
+
534
+ ---
535
+
536
+ ## ๐Ÿงช Testing
537
+
538
+ ### Component Test
539
+
540
+ ```typescript
541
+ // tests/components/ProjectCard.test.ts
542
+ import { mount } from '@nuxt/test-utils'
543
+ import ProjectCard from '~/components/ProjectCard.vue'
544
+
545
+ describe('ProjectCard', () => {
546
+ it('renders project name', () => {
547
+ const wrapper = mount(ProjectCard, {
548
+ props: {
549
+ project: {
550
+ id: '1',
551
+ name: 'Test Project',
552
+ description: 'Test Description',
553
+ status: 'active'
554
+ }
555
+ }
556
+ })
557
+
558
+ expect(wrapper.text()).toContain('Test Project')
559
+ })
560
+ })
561
+ ```
562
+
563
+ ### Integration Test
564
+
565
+ ```typescript
566
+ // tests/pages/projects.test.ts
567
+ import { setup, $fetch } from '@nuxt/test-utils'
568
+
569
+ describe('Projects Page', () => {
570
+ await setup({
571
+ // Test configuration
572
+ })
573
+
574
+ it('loads projects', async () => {
575
+ const html = await $fetch('/projects')
576
+ expect(html).toContain('My Projects')
577
+ })
578
+ })
579
+ ```
580
+
581
+ ---
582
+
583
+ ## ๐Ÿ“ฆ Deployment
584
+
585
+ ### Build for Production
586
+
587
+ ```bash
588
+ pnpm build
589
+ ```
590
+
591
+ ### Preview Production Build
592
+
593
+ ```bash
594
+ pnpm preview
595
+ ```
596
+
597
+ ### Environment Variables
598
+
599
+ ```bash
600
+ # .env.production
601
+ NUXT_PUBLIC_BASE_API=https://api.production.com
602
+ ```
603
+
604
+ ---
605
+
606
+ ## ๐Ÿ” Debugging
607
+
608
+ ### Enable Nuxt DevTools
609
+
610
+ ```typescript
611
+ // nuxt.config.ts
612
+ export default defineNuxtConfig({
613
+ devtools: {
614
+ enabled: true
615
+ }
616
+ })
617
+ ```
618
+
619
+ ### Debug Authentication
620
+
621
+ ```typescript
622
+ const auth = useAuth()
623
+
624
+ console.log('Token:', auth.token.value)
625
+ console.log('User:', auth.me.value)
626
+ console.log('Is Authenticated:', auth.isAuthenticated.value)
627
+ console.log('Permissions:', auth.me.value?.access_level)
628
+ ```
629
+
630
+ ### Debug Data Loading
631
+
632
+ ```typescript
633
+ const loader = useObjectLoader({...})
634
+
635
+ watch(() => loader.status.value, (status) => {
636
+ console.log('Loading:', status.isLoading)
637
+ console.log('Success:', status.isSuccess)
638
+ console.log('Error:', status.isError)
639
+ console.log('Data:', loader.data.value)
640
+ })
641
+ ```
642
+
643
+ ---
644
+
645
+ ## ๐Ÿ“š Next Steps
646
+
647
+ 1. **Read the [LLM Guide](./LLM_GUIDE.md)** - Comprehensive documentation
648
+ 2. **Check [Component Examples](./COMPONENT_EXAMPLES.md)** - Real-world examples
649
+ 3. **Review [API Reference](./API_REFERENCE.md)** - Detailed API docs
650
+ 4. **Study [Architecture](./ARCHITECTURE.md)** - System design
651
+
652
+ ---
653
+
654
+ ## ๐Ÿ’ก Tips
655
+
656
+ 1. **Use Auto-imports** - No need to import components, composables, or constants
657
+ 2. **Follow Existing Patterns** - Check existing code before creating new patterns
658
+ 3. **Type Everything** - Use TypeScript for better DX
659
+ 4. **Use StatusBox** - For consistent loading/error states
660
+ 5. **Check Permissions** - Always validate user permissions
661
+ 6. **Use Route Constants** - Never hardcode routes
662
+ 7. **Leverage @finema/core** - Many utilities already exist
663
+
664
+ ---
665
+
666
+ ## ๐Ÿ†˜ Getting Help
667
+
668
+ 1. Check existing components and pages for examples
669
+ 2. Review the documentation files
670
+ 3. Use Nuxt DevTools for debugging
671
+ 4. Check browser console for errors
672
+ 5. Review network requests in DevTools
673
+
674
+ ---
675
+
676
+ **Last Updated:** 2025-11-07
677
+ **Happy Coding! ๐Ÿš€**
678
+