@flusys/ng-shared 0.1.0-beta.1 → 0.1.0-beta.2

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 (2) hide show
  1. package/README.md +987 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,987 @@
1
+ # @flusys/ng-shared Package Guide
2
+
3
+ ## Overview
4
+
5
+ `@flusys/ng-shared` provides reusable components, directives, API services, and utilities used across all FLUSYS applications. This package extends ng-core with UI components, API integration patterns, and provider interfaces for package independence.
6
+
7
+ **Key Principle:** ng-shared depends only on ng-core, never on ng-layout or feature packages.
8
+
9
+ ## Package Information
10
+
11
+ - **Package:** `@flusys/ng-shared`
12
+ - **Dependencies:** ng-core
13
+ - **Dependents:** ng-layout, ng-auth, ng-iam, ng-storage, flusysng
14
+ - **Build Command:** `npm run build:ng-shared`
15
+
16
+ ---
17
+
18
+ ## 1. Interfaces
19
+
20
+ ### Data Interfaces
21
+
22
+ #### IBaseEntity
23
+
24
+ Base entity interface matching backend Identity entity.
25
+
26
+ ```typescript
27
+ interface IBaseEntity {
28
+ id: string;
29
+ createdAt: Date;
30
+ updatedAt: Date;
31
+ deletedAt?: Date | null;
32
+ createdById?: string | null;
33
+ updatedById?: string | null;
34
+ deletedById?: string | null;
35
+ }
36
+ ```
37
+
38
+ **Entity Mixins:**
39
+
40
+ | Interface | Fields | Purpose |
41
+ | --------------- | ---------------------------------- | ---------------------- |
42
+ | `ISoftDeletable`| `deletedAt?: Date \| null` | Soft delete support |
43
+ | `ITimestampable`| `createdAt, updatedAt` | Timestamp tracking |
44
+ | `IActivatable` | `isActive: boolean` | Active status toggle |
45
+ | `IOrderable` | `serial?: number \| null` | Ordering/sorting |
46
+ | `IMetadata` | `metadata?: Record<string, unknown>` | JSON metadata field |
47
+
48
+ #### ILoggedUserInfo
49
+
50
+ Current logged-in user info with optional company context.
51
+
52
+ ```typescript
53
+ interface ILoggedUserInfo {
54
+ id: string;
55
+ email: string;
56
+ name?: string;
57
+ phone?: string;
58
+ profilePictureId?: string;
59
+ companyId?: string;
60
+ branchId?: string;
61
+ companyLogoId?: string;
62
+ branchLogoId?: string;
63
+ }
64
+ ```
65
+
66
+ #### IFilterData
67
+
68
+ Filter, pagination, and sort payload for list queries.
69
+
70
+ ```typescript
71
+ interface IPagination {
72
+ pageSize: number;
73
+ currentPage: number;
74
+ }
75
+
76
+ interface ISort { [key: string]: 'ASC' | 'DESC' }
77
+ interface IFilter { [key: string]: any }
78
+
79
+ interface IFilterData {
80
+ filter?: IFilter;
81
+ pagination?: IPagination;
82
+ select?: string[];
83
+ sort?: ISort;
84
+ withDeleted?: boolean;
85
+ extraKey?: string[];
86
+ }
87
+ ```
88
+
89
+ #### IDeleteData
90
+
91
+ Delete request payload matching backend DeleteDto.
92
+
93
+ ```typescript
94
+ type DeleteType = 'delete' | 'restore' | 'permanent';
95
+
96
+ interface IDeleteData {
97
+ id: string | string[]; // Single or batch delete
98
+ type: DeleteType;
99
+ }
100
+ ```
101
+
102
+ #### IDropDown
103
+
104
+ Simple dropdown item for select components.
105
+
106
+ ```typescript
107
+ interface IDropDown {
108
+ label: string;
109
+ value: string;
110
+ }
111
+ ```
112
+
113
+ ### Response Interfaces
114
+
115
+ Type-safe interfaces matching FLUSYS_NEST backend response DTOs.
116
+
117
+ ```typescript
118
+ // Single item response (POST /resource/insert, /update, /get/:id)
119
+ interface ISingleResponse<T> {
120
+ success: boolean;
121
+ message: string;
122
+ data?: T;
123
+ _meta?: IRequestMeta;
124
+ }
125
+
126
+ // List response with pagination (POST /resource/get-all)
127
+ interface IListResponse<T> {
128
+ success: boolean;
129
+ message: string;
130
+ data?: T[];
131
+ meta: IPaginationMeta;
132
+ _meta?: IRequestMeta;
133
+ }
134
+
135
+ // Bulk operation response (POST /resource/insert-many, /update-many)
136
+ interface IBulkResponse<T> {
137
+ success: boolean;
138
+ message: string;
139
+ data?: T[];
140
+ meta: IBulkMeta;
141
+ _meta?: IRequestMeta;
142
+ }
143
+
144
+ // Message-only response (POST /resource/delete, /logout)
145
+ interface IMessageResponse {
146
+ success: boolean;
147
+ message: string;
148
+ _meta?: IRequestMeta;
149
+ }
150
+
151
+ // Error response (validation errors, exceptions)
152
+ interface IErrorResponse {
153
+ success: false;
154
+ message: string;
155
+ code?: string;
156
+ errors?: IValidationError[];
157
+ _meta?: IRequestMeta;
158
+ }
159
+
160
+ // Union type
161
+ type ApiResponse<T> = ISingleResponse<T> | IListResponse<T> | IBulkResponse<T> | IMessageResponse | IErrorResponse;
162
+ ```
163
+
164
+ **Metadata types:**
165
+
166
+ | Interface | Fields |
167
+ | ------------------ | -------------------------------------------- |
168
+ | `IRequestMeta` | `requestId?, timestamp?, responseTime?` |
169
+ | `IPaginationMeta` | `total, page, pageSize, count, hasMore?, totalPages?` |
170
+ | `IBulkMeta` | `count, failed?, total?` |
171
+ | `IValidationError` | `field, message, constraint?` |
172
+
173
+ ### Auth & Storage Response Types
174
+
175
+ ```typescript
176
+ interface ILoginResponse {
177
+ success: boolean;
178
+ message: string;
179
+ data: { accessToken: string; refreshToken: string; user: ILoginUserData };
180
+ }
181
+
182
+ interface IRefreshTokenResponse {
183
+ success: boolean;
184
+ message: string;
185
+ data: { accessToken: string; refreshToken?: string };
186
+ }
187
+
188
+ interface IFileData {
189
+ id: string;
190
+ name: string;
191
+ originalName: string;
192
+ contentType: string;
193
+ size: number;
194
+ key: string;
195
+ url?: string;
196
+ thumbnailUrl?: string;
197
+ createdAt: Date;
198
+ }
199
+ ```
200
+
201
+ ### Permission Interfaces
202
+
203
+ Discriminated union for building complex permission logic trees.
204
+
205
+ ```typescript
206
+ // Single permission check
207
+ interface IActionNode {
208
+ type: 'action';
209
+ actionId: string;
210
+ }
211
+
212
+ // Group with AND/OR logic
213
+ interface IGroupNode {
214
+ type: 'group';
215
+ operator: 'AND' | 'OR';
216
+ children: ILogicNode[];
217
+ }
218
+
219
+ // Union type
220
+ type ILogicNode = IActionNode | IGroupNode;
221
+ ```
222
+
223
+ ---
224
+
225
+ ## 2. Enums
226
+
227
+ ```typescript
228
+ enum ContactTypeEnum {
229
+ PHONE = 1,
230
+ EMAIL = 2,
231
+ }
232
+
233
+ enum IconTypeEnum {
234
+ PRIMENG_ICON = 1,
235
+ IMAGE_FILE_LINK = 2,
236
+ DIRECT_TAG_SVG = 3,
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ## 3. Services
243
+
244
+ ### ApiResourceService
245
+
246
+ Signal-based CRUD service using Angular 21 `resource()` API. All endpoints use POST (RPC-style).
247
+
248
+ **Define a service:**
249
+
250
+ ```typescript
251
+ import { Injectable } from '@angular/core';
252
+ import { HttpClient } from '@angular/common/http';
253
+ import { ApiResourceService } from '@flusys/ng-shared';
254
+
255
+ @Injectable({ providedIn: 'root' })
256
+ export class UserService extends ApiResourceService<UserDto, IUser> {
257
+ constructor(http: HttpClient) {
258
+ super('users', http); // Base URL: APP_CONFIG.apiBaseUrl + '/users'
259
+ }
260
+ }
261
+ ```
262
+
263
+ **Endpoint mapping:**
264
+
265
+ | Method | HTTP | Endpoint | Response |
266
+ | -------------- | ---- | ------------------------- | ------------------------ |
267
+ | `insert(dto)` | POST | `/{resource}/insert` | `ISingleResponse<T>` |
268
+ | `insertMany(dtos)` | POST | `/{resource}/insert-many` | `IBulkResponse<T>` |
269
+ | `findById(id, select?)` | POST | `/{resource}/get/:id` | `ISingleResponse<T>` |
270
+ | `getAll(search, filter)` | POST | `/{resource}/get-all?q=` | `IListResponse<T>` |
271
+ | `update(dto)` | POST | `/{resource}/update` | `ISingleResponse<T>` |
272
+ | `updateMany(dtos)` | POST | `/{resource}/update-many` | `IBulkResponse<T>` |
273
+ | `delete(deleteDto)` | POST | `/{resource}/delete` | `IMessageResponse` |
274
+
275
+ All methods above return `Observable`. Async (Promise) variants are also available: `insertAsync`, `insertManyAsync`, `findByIdAsync`, `updateAsync`, `updateManyAsync`, `deleteAsync`.
276
+
277
+ **Reactive signals:**
278
+
279
+ | Signal | Type | Description |
280
+ | ------------ | ------------------------- | ------------------------ |
281
+ | `isLoading` | `Signal<boolean>` | Whether data is loading |
282
+ | `data` | `Signal<T[]>` | Current list data |
283
+ | `total` | `Signal<number>` | Total item count |
284
+ | `pageInfo` | `Signal<IPaginationMeta>` | Pagination metadata |
285
+ | `hasMore` | `Signal<boolean>` | More pages available |
286
+ | `searchTerm` | `WritableSignal<string>` | Current search term |
287
+ | `filterData` | `WritableSignal<IFilterData>` | Filter/pagination state |
288
+
289
+ **List management:**
290
+
291
+ ```typescript
292
+ // Trigger data fetch (updates searchTerm and filterData signals, resource auto-reloads)
293
+ userService.fetchList('search term', { pagination: { currentPage: 0, pageSize: 10 } });
294
+
295
+ // Pagination helpers
296
+ userService.setPagination({ currentPage: 1, pageSize: 20 });
297
+ userService.nextPage();
298
+ userService.resetPagination();
299
+ userService.reload();
300
+ ```
301
+
302
+ **Component usage:**
303
+
304
+ ```typescript
305
+ @Component({...})
306
+ export class UserListComponent {
307
+ private readonly userService = inject(UserService);
308
+
309
+ readonly users = this.userService.data; // Signal<IUser[]>
310
+ readonly isLoading = this.userService.isLoading; // Signal<boolean>
311
+ readonly total = this.userService.total; // Signal<number>
312
+
313
+ constructor() {
314
+ this.userService.fetchList('', { pagination: { currentPage: 0, pageSize: 10 } });
315
+ }
316
+
317
+ async createUser(dto: UserDto) {
318
+ await this.userService.insertAsync(dto);
319
+ this.userService.reload(); // Refresh list
320
+ }
321
+ }
322
+ ```
323
+
324
+ > **Note:** `ApiService` is exported as an alias for `ApiResourceService` for backward compatibility.
325
+
326
+ ### FileUrlService
327
+
328
+ Fetches file URLs from the backend. Supports presigned URLs for cloud storage (S3, Azure).
329
+
330
+ **CRITICAL:** Never construct file URLs manually. Always use this service.
331
+
332
+ ```typescript
333
+ import { FileUrlService } from '@flusys/ng-shared';
334
+
335
+ @Component({...})
336
+ export class ProductComponent {
337
+ private readonly fileUrlService = inject(FileUrlService);
338
+
339
+ loadImage(fileId: string) {
340
+ // Fetch from backend: POST /file-manager/get-files
341
+ this.fileUrlService.fetchSingleFileUrl(fileId).subscribe(file => {
342
+ this.imageUrl.set(file?.url ?? null);
343
+ });
344
+ }
345
+
346
+ loadMultiple(fileIds: string[]) {
347
+ this.fileUrlService.fetchFileUrls(fileIds).subscribe(files => {
348
+ // files: FilesResponseDto[] with { id, name, contentType, url }
349
+ });
350
+ }
351
+ }
352
+ ```
353
+
354
+ **Methods:**
355
+
356
+ | Method | Returns | Description |
357
+ | ----------------------------- | ----------------------------------- | ---------------------------------- |
358
+ | `getFileUrl(fileId)` | `string \| null` | Get cached URL (synchronous) |
359
+ | `fileUrlSignal(fileId)` | `Signal<string \| null>` | Computed signal from cache |
360
+ | `fetchFileUrls(fileIds[])` | `Observable<FilesResponseDto[]>` | Fetch from backend, updates cache |
361
+ | `fetchSingleFileUrl(fileId)` | `Observable<FilesResponseDto \| null>` | Fetch single file |
362
+ | `clearCache()` | `void` | Clear all cached URLs |
363
+ | `removeFromCache(fileId)` | `void` | Remove specific entry from cache |
364
+
365
+ ### PermissionValidatorService
366
+
367
+ Signal-based permission state management. Used by `HasPermissionDirective`, permission guards, and IAM.
368
+
369
+ ```typescript
370
+ import { PermissionValidatorService } from '@flusys/ng-shared';
371
+
372
+ @Component({...})
373
+ export class MyComponent {
374
+ private readonly permissionValidator = inject(PermissionValidatorService);
375
+
376
+ ngOnInit() {
377
+ // Set permissions (typically done by IAM PermissionStateService)
378
+ this.permissionValidator.setPermissions(['user.view', 'user.create']);
379
+
380
+ // Check single permission
381
+ if (this.permissionValidator.hasPermission('user.view')) {
382
+ // User has permission
383
+ }
384
+
385
+ // Check loaded state
386
+ if (this.permissionValidator.isPermissionsLoaded()) {
387
+ // Permissions have been loaded
388
+ }
389
+ }
390
+ }
391
+ ```
392
+
393
+ **Methods:**
394
+
395
+ | Method | Returns | Description |
396
+ | ---------------------------- | --------- | ---------------------------------- |
397
+ | `setPermissions(codes[])` | `void` | Replace all permissions |
398
+ | `clearPermissions()` | `void` | Clear all permissions |
399
+ | `hasPermission(code)` | `boolean` | Check single permission |
400
+ | `isPermissionsLoaded()` | `boolean` | Whether permissions have been set |
401
+
402
+ **Signals:**
403
+
404
+ | Signal | Type | Description |
405
+ | ------------- | ------------------ | ------------------------------ |
406
+ | `permissions` | `Signal<string[]>` | Readonly current permissions |
407
+
408
+ ### CookieService
409
+
410
+ SSR-aware cookie reading service.
411
+
412
+ ```typescript
413
+ import { CookieService } from '@flusys/ng-shared';
414
+
415
+ const cookies = inject(CookieService).get(); // Returns document.cookie (browser) or request header cookie (server)
416
+ ```
417
+
418
+ ### PlatformService
419
+
420
+ SSR environment detection service.
421
+
422
+ ```typescript
423
+ import { PlatformService } from '@flusys/ng-shared';
424
+
425
+ const platform = inject(PlatformService);
426
+ if (!platform.isServer) {
427
+ // Browser-only code (localStorage, window, etc.)
428
+ }
429
+ ```
430
+
431
+ ---
432
+
433
+ ## 4. Components
434
+
435
+ ### IconComponent
436
+
437
+ Flexible icon renderer supporting PrimeNG icons, image files, and SVG.
438
+
439
+ - **Selector:** `lib-icon`
440
+ - **Inputs:** `icon` (required string), `iconType` (optional `IconTypeEnum`, default: `PRIMENG_ICON`)
441
+
442
+ ```html
443
+ <!-- PrimeNG icon (default) -->
444
+ <lib-icon icon="pi pi-user" />
445
+
446
+ <!-- Image file -->
447
+ <lib-icon icon="/assets/logo.png" [iconType]="IconTypeEnum.IMAGE_FILE_LINK" />
448
+
449
+ <!-- SVG tag -->
450
+ <lib-icon icon="<svg>...</svg>" [iconType]="IconTypeEnum.DIRECT_TAG_SVG" />
451
+ ```
452
+
453
+ ### LazySelectComponent
454
+
455
+ Single-select dropdown with lazy loading, search, and scroll pagination.
456
+
457
+ - **Selector:** `lib-lazy-select`
458
+ - **Extends:** `BaseFormControl<string | null>`
459
+ - **Supports:** Template-driven, reactive forms, signal forms
460
+
461
+ ```typescript
462
+ @Component({
463
+ imports: [LazySelectComponent],
464
+ template: `
465
+ <lib-lazy-select
466
+ [(value)]="selectedId"
467
+ [selectDataList]="items()"
468
+ [optionLabel]="'label'"
469
+ [optionValue]="'value'"
470
+ [isEditMode]="true"
471
+ [isLoading]="loading()"
472
+ [total]="total()"
473
+ [pagination]="pagination()"
474
+ [placeHolder]="'Select item'"
475
+ (onSearch)="handleSearch($event)"
476
+ (onPagination)="handlePagination($event)"
477
+ />
478
+ `,
479
+ })
480
+ export class MyComponent {
481
+ readonly selectedId = signal<string | null>(null);
482
+ readonly items = signal<IDropDown[]>([]);
483
+ readonly loading = signal(false);
484
+ readonly total = signal<number | undefined>(undefined);
485
+ readonly pagination = signal<IPagination>({ currentPage: 0, pageSize: 20 });
486
+ }
487
+ ```
488
+
489
+ **Inputs:**
490
+
491
+ | Input | Type | Description |
492
+ | ---------------- | ----------------- | -------------------------------- |
493
+ | `selectDataList` | `Array<IDropDown>` | Dropdown options (required) |
494
+ | `optionLabel` | `string` | Label field name (required) |
495
+ | `optionValue` | `string` | Value field name (required) |
496
+ | `isEditMode` | `boolean` | Enable/disable editing (required)|
497
+ | `isLoading` | `boolean` | Loading state (required) |
498
+ | `total` | `number \| undefined` | Total items for pagination |
499
+ | `pagination` | `IPagination` | Current pagination state |
500
+ | `placeHolder` | `string` | Placeholder text (default: `'Select Option'`) |
501
+
502
+ **Model:** `value` - Two-way bound selected value (`string | null`)
503
+
504
+ **Outputs:** `onSearch` (debounced 500ms), `onPagination` (scroll-triggered)
505
+
506
+ ### LazyMultiSelectComponent
507
+
508
+ Multi-select dropdown with lazy loading, search, select-all, and scroll pagination.
509
+
510
+ - **Selector:** `lib-lazy-multi-select`
511
+ - **Extends:** `BaseFormControl<string[] | null>`
512
+ - **Supports:** Template-driven, reactive forms, signal forms
513
+
514
+ ```typescript
515
+ @Component({
516
+ imports: [LazyMultiSelectComponent],
517
+ template: `
518
+ <lib-lazy-multi-select
519
+ [(value)]="selectedIds"
520
+ [selectDataList]="items()"
521
+ [isEditMode]="true"
522
+ [isLoading]="loading()"
523
+ [total]="total()"
524
+ [pagination]="pagination()"
525
+ [placeHolder]="'Select items'"
526
+ (onSearch)="handleSearch($event)"
527
+ (onPagination)="handlePagination($event)"
528
+ />
529
+ `,
530
+ })
531
+ export class MyComponent {
532
+ readonly selectedIds = signal<string[] | null>(null);
533
+ }
534
+ ```
535
+
536
+ **Inputs:** `selectDataList`, `isEditMode`, `isLoading`, `total`, `pagination`, `placeHolder` (same as `LazySelectComponent`). Does not have `optionLabel`/`optionValue` (uses `IDropDown.label`/`IDropDown.value` directly).
537
+
538
+ **Model:** `value` - Two-way bound selected values (`string[] | null`)
539
+
540
+ **Computed signals:** `selectedValueDisplay` (display text), `isSelectAll` (all items selected)
541
+
542
+ ---
543
+
544
+ ## 5. Directives
545
+
546
+ ### HasPermissionDirective
547
+
548
+ Structural directive for permission-based rendering. Fail-closed: hides content when permissions not loaded.
549
+
550
+ - **Selector:** `[hasPermission]`
551
+ - **Input:** `hasPermission` - `string | ILogicNode | null`
552
+
553
+ ```typescript
554
+ import { HasPermissionDirective, ILogicNode } from '@flusys/ng-shared';
555
+
556
+ @Component({
557
+ imports: [HasPermissionDirective],
558
+ template: `
559
+ <!-- Simple permission check -->
560
+ <button *hasPermission="'user.create'">Create User</button>
561
+
562
+ <!-- Complex AND/OR logic -->
563
+ <div *hasPermission="editLogic">Edit Panel</div>
564
+ `,
565
+ })
566
+ export class MyComponent {
567
+ readonly editLogic: ILogicNode = {
568
+ type: 'group',
569
+ operator: 'AND',
570
+ children: [
571
+ { type: 'action', actionId: 'user.view' },
572
+ { type: 'action', actionId: 'user.update' },
573
+ ],
574
+ };
575
+ }
576
+ ```
577
+
578
+ ### EditModeElementChangerDirective
579
+
580
+ Toggles readonly/disabled state for form controls based on edit mode. Supports `<input>`, `<p-select>`, and `<p-calendar>`.
581
+
582
+ - **Selector:** `[appEditModeElementChanger]`
583
+ - **Input:** `isEditMode` (required boolean)
584
+
585
+ ```html
586
+ <input [appEditModeElementChanger] [isEditMode]="isEditing()" />
587
+ <p-select [appEditModeElementChanger] [isEditMode]="isEditing()" />
588
+ ```
589
+
590
+ ### IsEmptyImageDirective
591
+
592
+ Automatically replaces broken or empty image `src` with a default fallback image.
593
+
594
+ - **Selector:** `img` (applies to all `<img>` elements)
595
+ - **Input:** `src` (standard img src)
596
+ - **Default image:** `lib/assets/images/default/default-image.jpg`
597
+
598
+ ```html
599
+ <img [src]="imageUrl()" alt="Product" />
600
+ <!-- Falls back to default image on error or empty src -->
601
+ ```
602
+
603
+ ### PreventDefaultDirective
604
+
605
+ Prevents default browser behavior on specified events and emits the event.
606
+
607
+ - **Selector:** `[appPreventDefault]`
608
+ - **Inputs:** `eventType` (`'click' | 'keydown' | 'keyup'`, default: `'click'`), `preventKey` (optional key filter)
609
+ - **Output:** `action` - Emits the prevented event
610
+
611
+ ```html
612
+ <a href="#" appPreventDefault (action)="handleClick($event)">Click me</a>
613
+ <input appPreventDefault eventType="keydown" preventKey="Enter" (action)="onEnter($event)" />
614
+ ```
615
+
616
+ ---
617
+
618
+ ## 6. Guards
619
+
620
+ Route-level guards for permission-based access control. All guards deny access when permissions are not loaded (fail-closed).
621
+
622
+ ### permissionGuard
623
+
624
+ Single permission or complex logic check.
625
+
626
+ ```typescript
627
+ import { permissionGuard } from '@flusys/ng-shared';
628
+
629
+ const routes: Routes = [
630
+ // Simple permission
631
+ { path: 'users', canActivate: [permissionGuard('user.view')] },
632
+
633
+ // Complex logic (ILogicNode)
634
+ { path: 'admin', canActivate: [permissionGuard({
635
+ type: 'group',
636
+ operator: 'AND',
637
+ children: [
638
+ { type: 'action', actionId: 'admin.view' },
639
+ { type: 'action', actionId: 'admin.manage' },
640
+ ],
641
+ })] },
642
+
643
+ // Custom redirect on deny
644
+ { path: 'settings', canActivate: [permissionGuard('settings.view', '/access-denied')] },
645
+ ];
646
+ ```
647
+
648
+ ### anyPermissionGuard
649
+
650
+ OR logic - allows access if user has ANY of the specified permissions.
651
+
652
+ ```typescript
653
+ { path: 'reports', canActivate: [anyPermissionGuard(['report.view', 'report.export'])] }
654
+ ```
655
+
656
+ ### allPermissionsGuard
657
+
658
+ AND logic - allows access only if user has ALL specified permissions.
659
+
660
+ ```typescript
661
+ { path: 'admin', canActivate: [allPermissionsGuard(['admin.view', 'admin.manage'])] }
662
+ ```
663
+
664
+ ---
665
+
666
+ ## 7. Utilities
667
+
668
+ ### Permission Evaluator
669
+
670
+ Pure functions for permission logic evaluation. Used internally by `HasPermissionDirective` and guards.
671
+
672
+ ```typescript
673
+ import { evaluatePermission, evaluateLogicNode, hasAnyPermission, hasAllPermissions } from '@flusys/ng-shared';
674
+
675
+ const userPermissions = ['user.view', 'user.create'];
676
+
677
+ // Evaluate string or ILogicNode
678
+ evaluatePermission('user.view', userPermissions); // true
679
+ evaluatePermission(null, userPermissions); // false
680
+
681
+ // Evaluate ILogicNode tree recursively
682
+ evaluateLogicNode(logicNode, userPermissions);
683
+
684
+ // Simple OR/AND checks
685
+ hasAnyPermission(['user.view', 'user.delete'], userPermissions); // true (has user.view)
686
+ hasAllPermissions(['user.view', 'user.delete'], userPermissions); // false (missing user.delete)
687
+ ```
688
+
689
+ ---
690
+
691
+ ## 8. Classes
692
+
693
+ ### BaseFormControl
694
+
695
+ Abstract base class for custom form controls. Implements both `ControlValueAccessor` (reactive forms) and `FormValueControl` (signal forms).
696
+
697
+ ```typescript
698
+ import { BaseFormControl, provideValueAccessor } from '@flusys/ng-shared';
699
+
700
+ @Component({
701
+ selector: 'my-select',
702
+ providers: [provideValueAccessor(MySelectComponent)],
703
+ })
704
+ export class MySelectComponent extends BaseFormControl<string | null> {
705
+ override readonly value = model<string | null>(null);
706
+
707
+ constructor() {
708
+ super();
709
+ this.initializeFormControl(); // Required for ControlValueAccessor sync
710
+ }
711
+ }
712
+
713
+ // Usage in all form patterns:
714
+ // Template-driven: <my-select [(value)]="selectedId" />
715
+ // Reactive forms: <my-select [formControl]="myControl" />
716
+ // Signal forms: <my-select [formField]="formTree.myField" />
717
+ ```
718
+
719
+ **Abstract property:** `value: ModelSignal<T>` - Must override with `model<T>()`
720
+
721
+ **Models:** `disabled` (boolean), `touched` (boolean)
722
+
723
+ **Methods:** `initializeFormControl()` (call in constructor), `markAsTouched()` (call on blur)
724
+
725
+ **Helper:** `provideValueAccessor(ComponentClass)` - Factory for `NG_VALUE_ACCESSOR` provider
726
+
727
+ ---
728
+
729
+ ## 9. Modules
730
+
731
+ ### AngularModule
732
+
733
+ Re-exports common Angular modules for convenience.
734
+
735
+ ```typescript
736
+ import { AngularModule } from '@flusys/ng-shared';
737
+ // Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterLink, RouterOutlet, + directives
738
+ // Providers: DatePipe
739
+ ```
740
+
741
+ ### PrimeModule
742
+
743
+ Re-exports PrimeNG component modules for convenience.
744
+
745
+ ```typescript
746
+ import { PrimeModule } from '@flusys/ng-shared';
747
+ // Includes: Button, Table, Card, Dialog, InputText, Select, MultiSelect,
748
+ // DatePicker, Checkbox, FileUpload, Image, Tag, Tabs, TreeTable, and more (27 modules)
749
+ ```
750
+
751
+ ---
752
+
753
+ ## 10. Provider Interfaces (Package Independence)
754
+
755
+ ng-shared defines **provider interfaces** to enable feature packages (ng-iam, ng-storage) to access auth functionality without direct dependencies.
756
+
757
+ ### Architecture
758
+
759
+ ```
760
+ ng-shared (defines interfaces + tokens)
761
+ |
762
+ ng-auth (implements interfaces with adapters)
763
+ |
764
+ ng-iam/ng-storage (consume interfaces via DI)
765
+ ```
766
+
767
+ ### Available Providers
768
+
769
+ #### IUserProvider / `USER_PROVIDER`
770
+
771
+ User list access for IAM user selection.
772
+
773
+ ```typescript
774
+ interface IUserBasicInfo { id: string; name: string; email: string }
775
+
776
+ interface IUserProvider {
777
+ getUsers(filter?: {
778
+ page?: number; pageSize?: number; search?: string;
779
+ companyId?: string; branchId?: string;
780
+ }): Observable<IListResponse<IUserBasicInfo>>;
781
+ }
782
+ ```
783
+
784
+ #### ICompanyApiProvider / `COMPANY_API_PROVIDER`
785
+
786
+ Company list access for IAM company selection.
787
+
788
+ ```typescript
789
+ interface ICompanyBasicInfo { id: string; name: string; slug?: string }
790
+
791
+ interface ICompanyApiProvider {
792
+ getCompanies(filter?: {
793
+ page?: number; pageSize?: number; search?: string;
794
+ }): Observable<IListResponse<ICompanyBasicInfo>>;
795
+ }
796
+ ```
797
+
798
+ #### IUserPermissionProvider / `USER_PERMISSION_PROVIDER`
799
+
800
+ User permission queries for IAM.
801
+
802
+ ```typescript
803
+ interface IUserPermissionProvider {
804
+ getUserBranchPermissions(userId: string): Observable<ISingleResponse<any>>;
805
+ }
806
+ ```
807
+
808
+ ### Usage in Consuming Packages
809
+
810
+ ```typescript
811
+ import { USER_PROVIDER } from '@flusys/ng-shared';
812
+
813
+ export class UserSelectorComponent {
814
+ private readonly userProvider = inject(USER_PROVIDER);
815
+
816
+ loadUsers() {
817
+ this.userProvider.getUsers({ page: 0, pageSize: 50 }).subscribe(response => {
818
+ this.users.set(response.data ?? []);
819
+ });
820
+ }
821
+ }
822
+ ```
823
+
824
+ ### Wiring in App
825
+
826
+ ```typescript
827
+ // app.config.ts
828
+ import { provideAuthProviders } from '@flusys/ng-auth';
829
+
830
+ export const appConfig: ApplicationConfig = {
831
+ providers: [
832
+ ...provideAuthProviders(), // Registers all auth adapters for provider tokens
833
+ ],
834
+ };
835
+ ```
836
+
837
+ ### See Also
838
+
839
+ - [AUTH-GUIDE.md](AUTH-GUIDE.md) - Adapter implementations
840
+ - [IAM-GUIDE.md](IAM-GUIDE.md) - IAM usage examples
841
+ - [../CLAUDE.md](../CLAUDE.md) - Complete pattern documentation
842
+
843
+ ---
844
+
845
+ ## Best Practices
846
+
847
+ ### API Services
848
+
849
+ - **Extend `ApiResourceService`** for new services (signal-based)
850
+ - Use reactive signals (`data`, `isLoading`, `total`) in templates
851
+ - Use `fetchList()` to trigger queries, `reload()` to refresh
852
+ - Use async methods (`insertAsync`, `updateAsync`) for one-off operations
853
+
854
+ ### File URLs
855
+
856
+ - **Always use `FileUrlService`** - never construct URLs manually
857
+ - Use `fetchSingleFileUrl()` for one-off fetches, `fetchFileUrls()` for batches
858
+ - Use `fileUrlSignal()` for reactive template bindings
859
+
860
+ ### Components
861
+
862
+ - Use `AngularModule` and `PrimeModule` for common imports
863
+ - Use signal inputs via `input()` and `input.required()`
864
+ - Components use `lib-` prefix for selectors
865
+
866
+ ### Directives
867
+
868
+ - Directives use `app` prefix for selectors (`appPreventDefault`, `appEditModeElementChanger`)
869
+ - Exception: `HasPermissionDirective` uses `[hasPermission]`
870
+ - Exception: `IsEmptyImageDirective` uses `img` selector (auto-applies to all images)
871
+
872
+ ### Permissions
873
+
874
+ - Use `HasPermissionDirective` for template-level permission checks
875
+ - Use permission guards for route-level access control
876
+ - Use `PermissionValidatorService` for programmatic checks in services
877
+ - Permissions follow fail-closed model: no access by default
878
+
879
+ ---
880
+
881
+ ## Common Issues
882
+
883
+ ### ApiResourceService Not Updating UI
884
+
885
+ **Problem:** UI doesn't update after operations.
886
+
887
+ **Solution:** Use signal syntax in templates:
888
+
889
+ ```html
890
+ <!-- Correct -->
891
+ <div>{{ users() }}</div>
892
+
893
+ <!-- Wrong -->
894
+ <div>{{ users }}</div>
895
+ ```
896
+
897
+ ### FileUrlService Returns Error
898
+
899
+ **Problem:** File URL fetching fails.
900
+
901
+ **Solution:**
902
+
903
+ 1. Ensure storage service is enabled in environment config
904
+ 2. Verify file ID exists in database
905
+ 3. Check storage provider configuration (local/S3/Azure)
906
+
907
+ ### Circular Dependency with ng-layout
908
+
909
+ **Problem:** Build fails with circular dependency.
910
+
911
+ **Solution:** ng-shared must NEVER import from ng-layout. Move shared components to ng-shared, layout-specific components to ng-layout.
912
+
913
+ ---
914
+
915
+ ## API Reference
916
+
917
+ ### Services
918
+
919
+ | Service | Description |
920
+ | ------------------------------ | ------------------------------------- |
921
+ | `ApiResourceService<DTO, T>` | Signal-based CRUD with resource() API |
922
+ | `FileUrlService` | Cloud storage URL fetching |
923
+ | `PermissionValidatorService` | Permission state management |
924
+ | `CookieService` | SSR-aware cookie reading |
925
+ | `PlatformService` | SSR environment detection |
926
+
927
+ ### Components
928
+
929
+ | Component | Selector | Description |
930
+ | -------------------------- | ---------------------- | ------------------------- |
931
+ | `IconComponent` | `lib-icon` | Flexible icon renderer |
932
+ | `LazySelectComponent` | `lib-lazy-select` | Lazy-loading dropdown |
933
+ | `LazyMultiSelectComponent` | `lib-lazy-multi-select`| Lazy-loading multi-select |
934
+
935
+ ### Directives
936
+
937
+ | Directive | Selector | Description |
938
+ | --------------------------------- | --------------------------- | ----------------------------------- |
939
+ | `HasPermissionDirective` | `[hasPermission]` | Permission-based rendering |
940
+ | `EditModeElementChangerDirective` | `[appEditModeElementChanger]` | Toggle edit mode on form controls |
941
+ | `IsEmptyImageDirective` | `img` | Image fallback on error/empty |
942
+ | `PreventDefaultDirective` | `[appPreventDefault]` | Prevent default event behavior |
943
+
944
+ ### Guards
945
+
946
+ | Guard | Description |
947
+ | ---------------------- | -------------------------------------- |
948
+ | `permissionGuard` | Single permission or ILogicNode check |
949
+ | `anyPermissionGuard` | OR logic (any of listed permissions) |
950
+ | `allPermissionsGuard` | AND logic (all of listed permissions) |
951
+
952
+ ### Interfaces
953
+
954
+ | Interface | Description |
955
+ | -------------------- | ------------------------------------ |
956
+ | `IBaseEntity` | Base entity with ID and timestamps |
957
+ | `ILoggedUserInfo` | Current user info with company ctx |
958
+ | `IFilterData` | Filter, pagination, sort payload |
959
+ | `IDeleteData` | Delete request payload |
960
+ | `IDropDown` | Simple label/value pair |
961
+ | `ISingleResponse<T>` | Single item response |
962
+ | `IListResponse<T>` | List with pagination |
963
+ | `IBulkResponse<T>` | Bulk operation response |
964
+ | `IMessageResponse` | Message-only response |
965
+ | `IErrorResponse` | Error with validation details |
966
+ | `ILogicNode` | Permission logic tree (AND/OR nodes) |
967
+
968
+ ### Injection Tokens
969
+
970
+ | Token | Interface | Description |
971
+ | -------------------------- | ------------------------- | ---------------------------- |
972
+ | `USER_PROVIDER` | `IUserProvider` | User list for IAM |
973
+ | `COMPANY_API_PROVIDER` | `ICompanyApiProvider` | Company list for IAM |
974
+ | `USER_PERMISSION_PROVIDER` | `IUserPermissionProvider` | User permission queries |
975
+
976
+ ## See Also
977
+
978
+ - **[CORE-GUIDE.md](./CORE-GUIDE.md)** - Configuration, interceptors
979
+ - **[LAYOUT-GUIDE.md](./LAYOUT-GUIDE.md)** - Layout system
980
+ - **[AUTH-GUIDE.md](./AUTH-GUIDE.md)** - Auth adapters for provider interfaces
981
+ - **[IAM-GUIDE.md](./IAM-GUIDE.md)** - IAM permission usage
982
+
983
+ ---
984
+
985
+ **Last Updated:** 2026-02-07
986
+ **Package Version:** 1.0.0
987
+ **Angular Version:** 21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/ng-shared",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "Shared components and utilities for FLUSYS Angular packages",
5
5
  "license": "MIT",
6
6
  "peerDependencies": {