npm package: @flusys/ng-shared
Description: Shared components and utilities for FLUSYS Angular packages
Latest version: 4.1.1

# @flusys/ng-shared

> Shared utilities, response interfaces, provider interfaces, base classes, and reusable UI components for the FLUSYS Angular platform.

[![npm version](https://img.shields.io/npm/v/@flusys/ng-shared.svg)](https://www.npmjs.com/package/@flusys/ng-shared)
[![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.io)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

---

## Table of Contents

- [Overview](#overview)
- [Features](#features)
- [Compatibility](#compatibility)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Response Interfaces](#response-interfaces)
- [Provider Interface Pattern](#provider-interface-pattern)
  - [Injection Tokens](#injection-tokens)
  - [USER_PROVIDER](#user_provider)
  - [COMPANY_PROVIDER](#company_provider)
  - [FILE_PROVIDER](#file_provider)
- [Base Classes](#base-classes)
  - [ApiResourceService](#apiresourceservice)
  - [BaseListPage](#baselistpage)
  - [BaseFormPage](#baseformpage)
- [Services](#services)
  - [FileUrlService](#fileurlservice)
  - [PermissionValidatorService](#permissionvalidatorservice)
- [Reusable Components](#reusable-components)
- [Directives](#directives)
- [Pipes](#pipes)
- [Guards](#guards)
- [Modules](#modules)
- [Troubleshooting](#troubleshooting)
- [License](#license)

---

## Overview

`@flusys/ng-shared` is the second layer in the FLUSYS Angular dependency hierarchy. It depends on `@flusys/ng-core` and provides everything that feature packages (`ng-auth`, `ng-iam`, `ng-storage`, etc.) need to function **independently** of each other.

The cornerstone of `ng-shared` is the **Provider Interface Pattern**: injection tokens that define contracts between feature packages. `ng-auth` provides implementations; `ng-iam`, `ng-storage`, and other packages inject the interfaces — never the concrete implementations.

---

## Features

- ✅ Typed response interfaces (`ISingleResponse`, `IListResponse`, `IBulkResponse`, `IMessageResponse`)
- ✅ Provider Interface Pattern — `USER_PROVIDER`, `COMPANY_PROVIDER`, `FILE_PROVIDER`
- ✅ `ApiResourceService` — typed CRUD service base class
- ✅ `BaseListPage` / `BaseFormPage` — page scaffolding with signal state
- ✅ `FileUrlService` — safe presigned URL fetching (never construct URLs manually)
- ✅ `PermissionValidatorService` — client-side permission evaluation
- ✅ `LazySelectComponent` — virtualized dropdown with server-side search
- ✅ `FileSelectorDialogComponent` — file picker dialog
- ✅ `HasPermissionDirective` — structural directive for permission-gated content
- ✅ `TranslatePipe` — i18n pipe with parameter interpolation
- ✅ `AngularModule` / `PrimeModule` — pre-aggregated Angular and PrimeNG imports

---

## Compatibility

| Package | Version |
|---------|---------|
| Angular | 21+ |
| @flusys/ng-core | 4.x |
| PrimeNG | 18+ |

---

## Installation

```bash
npm install @flusys/ng-shared @flusys/ng-core
```

---

## Quick Start

```typescript
// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { APP_CONFIG } from '@flusys/ng-core';
import { environment } from './environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: APP_CONFIG, useValue: environment },
    provideHttpClient(),
    // Feature providers register against ng-shared tokens (see below)
  ],
};
```

---

## Response Interfaces

All FLUSYS backend responses conform to one of four shapes:

### ISingleResponse\<T\>

```typescript
interface ISingleResponse<T> {
  data: T;
}
```

### IListResponse\<T\>

```typescript
interface IListResponse<T> {
  data: T[];
  total: number;
  page: number;
  pageSize: number;
}
```

### IBulkResponse\<T\>

```typescript
interface IBulkResponse<T> {
  data: T[];
  successCount: number;
  failureCount: number;
  errors?: string[];
}
```

### IMessageResponse

```typescript
interface IMessageResponse {
  message: string;
}
```

### IErrorResponse

```typescript
interface IErrorResponse {
  statusCode: number;
  message: string;
  error?: string;
}
```

### IListParams (Common Request Shape)

```typescript
interface IListParams {
  page?: number;
  pageSize?: number;
  search?: string;
  sortBy?: string;
  sortOrder?: 'ASC' | 'DESC';
  filters?: Record<string, unknown>;
}
```

---

## Provider Interface Pattern

Feature packages never import each other directly. Instead, `ng-shared` defines **injection token interfaces** that decouple consumers from providers.

```
ng-auth          ──implements──► USER_PROVIDER token  ◄──injects── ng-iam
                 ──implements──► COMPANY_PROVIDER      ◄──injects── ng-storage
ng-storage       ──implements──► FILE_PROVIDER         ◄──injects── ng-shared components
```

### Injection Tokens

| Token | Interface | Provided By | Used By |
|-------|-----------|-------------|---------|
| `USER_PROVIDER` | `IUserProvider` | `ng-auth` | `ng-iam`, `ng-storage` |
| `COMPANY_PROVIDER` | `ICompanyProvider` | `ng-auth` | `ng-storage`, `ng-notification` |
| `FILE_PROVIDER` | `IFileProvider` | `ng-storage` | `ng-shared` components |
| `LAYOUT_AUTH_STATE` | `ILayoutAuthState` | `ng-auth` | `ng-layout` |
| `LAYOUT_AUTH_API` | `ILayoutAuthApi` | `ng-auth` | `ng-layout` |
| `LAYOUT_NOTIFICATION_BELL` | `INotificationBellProvider` | `ng-notification` | `ng-layout` |
| `LAYOUT_LANGUAGE_SELECTOR` | `ILanguageSelectorProvider` | `ng-localization` | `ng-layout` |

### USER_PROVIDER

```typescript
interface IUserProvider {
  user: Signal<ICurrentUser | null>;
  isAuthenticated: Signal<boolean>;
}

// Consuming (in ng-iam)
import { USER_PROVIDER, IUserProvider } from '@flusys/ng-shared';

@Injectable({ providedIn: 'root' })
export class IamService {
  private userProvider = inject<IUserProvider>(USER_PROVIDER);

  getCurrentUserId(): string | null {
    return this.userProvider.user()?.id ?? null;
  }
}
```

### COMPANY_PROVIDER

```typescript
interface ICompanyProvider {
  company: Signal<ICompany | null>;
  branch: Signal<IBranch | null>;
  companyId: Signal<string | null>;
}
```

### FILE_PROVIDER

```typescript
interface IFileProvider {
  getFileUrl(fileId: string): Observable<string | null>;
  uploadFile(file: File, folder?: string): Observable<IUploadedFile>;
}
```

---

## Base Classes

### ApiResourceService

Generic typed CRUD service that maps to FLUSYS POST-only RPC endpoints. Extend this to create a fully-typed API service in seconds.

```typescript
import { ApiResourceService } from '@flusys/ng-shared';

@Injectable({ providedIn: 'root' })
export class ProductApiService extends ApiResourceService<Product, CreateProductDto, UpdateProductDto> {
  protected override resource = 'product';
}

// Automatically provides:
// - getAll(params): Observable<IListResponse<Product>>
// - getById(id): Observable<ISingleResponse<Product>>
// - insert(dto): Observable<ISingleResponse<Product>>
// - update(dto): Observable<ISingleResponse<Product>>
// - delete(id): Observable<IMessageResponse>
// - bulkDelete(ids): Observable<IBulkResponse<Product>>
```

### BaseListPage

Scaffold for list pages with built-in pagination, search, and loading state using signals.

```typescript
import { BaseListPage } from '@flusys/ng-shared';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
})
export class ProductListComponent extends BaseListPage<Product> {
  protected apiService = inject(ProductApiService);

  // Inherited signals:
  // items        Signal<Product[]>
  // total        Signal<number>
  // page         Signal<number>
  // pageSize     Signal<number>
  // isLoading    Signal<boolean>
  // searchQuery  Signal<string>
}
```

### BaseFormPage

Scaffold for create/edit forms with validation state and submission handling.

```typescript
import { BaseFormPage } from '@flusys/ng-shared';

@Component({
  selector: 'app-product-form',
  templateUrl: './product-form.component.html',
})
export class ProductFormComponent extends BaseFormPage<Product> {
  protected apiService = inject(ProductApiService);

  // Inherited signals:
  // isSaving     Signal<boolean>
  // isEditMode   Signal<boolean>
  // formErrors   Signal<Record<string, string>>
}
```

---

## Services

### FileUrlService

**CRITICAL:** Always use `FileUrlService` to fetch file URLs. Never construct URLs manually. FLUSYS supports S3, Azure, and SFTP with presigned URLs that expire — only the backend knows the current URL.

```typescript
import { FileUrlService } from '@flusys/ng-shared';

@Component({ ... })
export class AvatarComponent {
  private fileUrlService = inject(FileUrlService);
  avatarUrl = signal<string | null>(null);

  async loadAvatar(fileId: string): Promise<void> {
    const file = await this.fileUrlService.fetchSingleFileUrl(fileId);
    this.avatarUrl.set(file?.url ?? null);
  }
}
```

```typescript
// ❌ WRONG — never construct file URLs manually
const url = `${apiBaseUrl}/storage/${fileId}`;

// ✅ CORRECT — always use FileUrlService
const file = await this.fileUrlService.fetchSingleFileUrl(fileId);
const url = file?.url ?? null;
```

**Methods:**

| Method | Description |
|--------|-------------|
| `fetchSingleFileUrl(fileId)` | Fetch presigned URL for a single file |
| `fetchMultipleFileUrls(fileIds)` | Batch fetch presigned URLs |
| `clearCache(fileId?)` | Invalidate cached URL(s) |

### PermissionValidatorService

Client-side permission evaluation without direct ng-iam dependency.

```typescript
import { PermissionValidatorService } from '@flusys/ng-shared';

@Component({ ... })
export class MyComponent {
  private permValidator = inject(PermissionValidatorService);

  canEdit = computed(() =>
    this.permValidator.hasPermission('product:update')
  );
}
```

---

## Reusable Components

### LazySelectComponent

Virtualized dropdown with server-side search. Ideal for large datasets.

```html
<flusys-lazy-select
  [apiService]="productApiService"
  [labelField]="'name'"
  [valueField]="'id'"
  [(ngModel)]="selectedProductId"
  placeholder="Search products..."
/>
```

### FileSelectorDialogComponent

File picker dialog that integrates with `@flusys/ng-storage`.

```html
<flusys-file-selector
  [accept]="'image/*'"
  [maxSize]="5242880"
  (fileSelected)="onFileSelected($event)"
/>
```

---

## Directives

### HasPermissionDirective

Structural directive that removes elements from the DOM if the user lacks the required permission:

```html
<!-- Single permission -->
<button *hasPermission="'product:delete'">Delete</button>

<!-- Multiple permissions (AND logic) -->
<div *hasPermission="['product:update', 'product:read']">Edit Panel</div>

<!-- OR logic -->
<div *hasPermission="'product:update'" [permissionOr]="true">Edit Panel</div>

<!-- Show fallback content -->
<button *hasPermission="'admin:manage'; else noAccess">Admin</button>
<ng-template #noAccess>
  <span>No access</span>
</ng-template>
```

---

## Pipes

### TranslatePipe

Translate i18n keys with optional parameter interpolation:

```html
<!-- Basic translation -->
<span>{{ 'common.save' | translate }}</span>

<!-- With parameters -->
<span>{{ 'pagination.showing' | translate: { from: 1, to: 10, total: 100 } }}</span>
```

The pipe automatically subscribes to language change events and re-renders on switch.

---

## Guards

| Guard | Description |
|-------|-------------|
| `AuthenticatedGuard` | Redirects unauthenticated users to `/auth/login` |
| `GuestGuard` | Redirects authenticated users away from auth pages |
| `PermissionGuard` | Blocks route if user lacks required permission |

```typescript
// app.routes.ts
export const routes: Routes = [
  {
    path: 'products',
    canActivate: [AuthenticatedGuard],
    loadComponent: () => import('./pages/product-list.component'),
  },
  {
    path: 'admin',
    canActivate: [PermissionGuard],
    data: { permission: 'admin:manage' },
    loadComponent: () => import('./pages/admin.component'),
  },
];
```

---

## Modules

### AngularModule

Pre-aggregated Angular common imports for use in components:

```typescript
import { AngularModule } from '@flusys/ng-shared';

@Component({
  imports: [AngularModule],
  // Includes: CommonModule, FormsModule, ReactiveFormsModule, RouterModule, etc.
})
export class MyComponent {}
```

### PrimeModule

Pre-aggregated PrimeNG component imports:

```typescript
import { PrimeModule } from '@flusys/ng-shared';

@Component({
  imports: [PrimeModule],
  // Includes: ButtonModule, TableModule, DialogModule, InputTextModule, etc.
})
export class MyComponent {}
```

---

## Troubleshooting

**`No provider for USER_PROVIDER`**

You need to register the auth providers in `app.config.ts`:

```typescript
import { provideAuthProviders } from '@flusys/ng-auth';

providers: [
  ...provideAuthProviders(),
]
```

**`FileUrlService returns null for valid fileId`**

The file may not exist or the storage module isn't registered:

```typescript
// Ensure storage is enabled in APP_CONFIG
services: {
  storage: { enabled: true }
}
```

**`TranslatePipe shows raw key (e.g., "common.save")`**

Localization isn't initialized or the key doesn't exist in translation files. Provide `TRANSLATE_ADAPTER` in `app.config.ts` via `provideLocalization()`.

**`HasPermissionDirective always hides content`**

`PermissionValidatorService` has no data — ensure `ng-iam` is enabled and permissions are loaded after login.

---

## License

MIT © FLUSYS