Skip to content

Quy trình Frontend Development (SOP)

Quick Reference

  • Who: Frontend Developer
  • Stack: Angular 19 (Signals, Standalone), TailwindCSS 4, Vitest
  • Package Manager: Bun (dev) / npm (CI)
  • Rule: Conventional commit messages

1. Thiết lập Dự án

bash
git clone <repo>
cd boxme-insight
bun install   # hoặc npm install
bash
npm start
# → http://localhost:4200 (demoMode=true mặc định)
bash
npm test          # Watch mode
npm test -- --watch=false  # CI mode (Vitest)
bash
npm run build -- --configuration=production
# Output: dist/boxme-insight/browser/

2. Tạo Feature mới

2a. Tạo Component

bash
npx ng g c features/ten-tinh-nang --standalone

Đặt tại: src/app/features/ten-tinh-nang/

2b. Tạo Routes

Tạo file ten-tinh-nang.routes.ts:

typescript
import { Routes } from '@angular/router';

export const FEATURE_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./ten-tinh-nang.component').then(m => m.TenTinhNangComponent),
  },
];

2c. Đăng ký trong App Routes

Thêm vào src/app/app.routes.ts:

typescript
{
  path: 'ten-tinh-nang',
  loadChildren: () =>
    import('./features/ten-tinh-nang/ten-tinh-nang.routes').then(m => m.FEATURE_ROUTES),
  canActivate: [authGuard],
},

TIP

Luôn dùng loadComponent / loadChildren cho lazy-loading. Không import component trực tiếp trong routes.

3. Signal State Management

Tạo service quản lý state cho feature:

typescript
@Injectable({ providedIn: 'root' })
export class FeatureService {
  // Writable signals
  private readonly _items = signal<Item[]>([]);
  private readonly _isLoading = signal(false);
  private readonly _error = signal<string | null>(null);

  // Public read-only
  readonly items = this._items.asReadonly();
  readonly isLoading = this._isLoading.asReadonly();
  readonly error = this._error.asReadonly();

  // Computed
  readonly hasItems = computed(() => this._items().length > 0);
  readonly itemCount = computed(() => this._items().length);

  async loadItems(): Promise<void> {
    this._isLoading.set(true);
    this._error.set(null);
    try {
      const items = await firstValueFrom(this.api.listItems());
      this._items.set(items);
    } catch (e) {
      this._error.set('Failed to load');
    } finally {
      this._isLoading.set(false);
    }
  }
}

Pattern: Private writable signals + public readonly + computed derivations.

4. Sử dụng Chart Renderer

html
<app-chart-renderer
  [data]="chartData()"
  [chartHint]="chartHint()"
  [height]="300"
/>
  • Dùng <app-chart-renderer> thay vì ngx-echarts trực tiếp
  • Component tự build ECharts config từ ChartHint
  • Hỗ trợ: line, area, bar, grouped_bar, stacked_bar, pie, heatmap
  • Export PNG: chartRenderer.getChartImage()

Source: (src/app/shared/components/chart-renderer.component.ts)

5. Sử dụng Data Table

html
<app-data-table
  [data]="tableData()"
  [pageSize]="10"
/>
  • Auto-detect columns từ data keys
  • Phân trang: 10/25/50 rows
  • Export CSV/Excel built-in

Source: (src/app/shared/components/data-table.component.ts)

6. Thêm Widget cho Dashboard

Khi tạo tính năng cho phép pin dữ liệu vào dashboard:

typescript
// Tạo widget từ artifact
const widget: DashboardWidgetCreateRequest = {
  title: 'Widget title',
  widget_type: 'chart',        // 'chart' | 'table' | 'kpi'
  display_size: 'medium',      // 'small' | 'medium' | 'large' | 'full'
  data_source: 'snapshot',     // 'live_insight' | 'snapshot'
  snapshot_data: currentData,
  snapshot_chart_hint: currentChartHint,
};

await this.dashboardService.createWidget(dashboardId, widget);

7. Định nghĩa Types

Luôn định nghĩa interfaces trong src/app/shared/models/api.models.ts hoặc src/app/core/models/:

typescript
// DO: Interface với types rõ ràng
interface MyResponse {
  data: DataRow[];
  total: number;
}

// DON'T: any
const response: any = await fetch(...); // ❌

8. Tailwind CSS Guidelines

  • Viết class inline trên template (utility-first)
  • Custom colors: xem src/styles.css cho brand palette
  • Dark mode: dùng dark: prefix
  • Responsive: sm:, md:, lg:, xl:

9. Demo Mode

Khi thêm API endpoint mới, thêm mock handler trong demo.interceptor.ts:

typescript
// Trong handleRequest()
if (url.includes('/api/v1/your-endpoint') && method === 'GET') {
  return of(new HttpResponse({
    status: 200,
    body: MOCK_YOUR_DATA,
  })).pipe(delay(demoDelay('list')));
}

10. Commit Convention

feat: add dashboard sharing feature
fix: resolve chart rendering on mobile
chore: update dependencies
docs: update API reference

Liên kết

Hệ thống tài liệu Boxme AI Insight — Powered by CodyMaster DocKit