# Templates Frontend - Application Skill

> Ces templates génèrent le code React/TypeScript pour les nouvelles applications/modules.
> **RAPPEL:** Toutes les pages client sont sous `/business/...`

---

## ARCHITECTURE FRONTEND

```
web/smartstack-web/src/
├── pages/business/$APP/$MODULE/
│   ├── $MODULE_PASCALPage.tsx          # Page principale (liste)
│   ├── $MODULE_PASCALDetailPage.tsx    # Page détail
│   └── Create$MODULE_PASCALPage.tsx    # Page création
├── components/$MODULE/
│   ├── $MODULE_PASCALListView.tsx      # Composant liste réutilisable
│   ├── $MODULE_PASCALForm.tsx          # Formulaire CRUD
│   └── $MODULE_PASCALFilters.tsx       # Filtres
├── components/common/
│   └── (composants partagés)
├── hooks/
│   ├── use$MODULE_PASCALPreferences.ts # Hook préférences
│   └── use$MODULE_PASCAL.ts            # Hook API
├── services/api/
│   └── $moduleApi.ts                   # Service API
└── i18n/locales/
    ├── fr/$module.json
    ├── en/$module.json
    ├── it/$module.json
    └── de/$module.json
```

---

## TEMPLATE: PAGE PRINCIPALE

```tsx
// pages/business/$APP/$MODULE/$MODULE_PASCALPage.tsx

import { useTranslation } from 'react-i18next';
import { $MODULE_PASCALListView } from '@/components/$MODULE/$MODULE_PASCALListView';

export function $MODULE_PASCALPage() {
  const { t } = useTranslation(['$module', 'common']);

  return (
    <$MODULE_PASCALListView
      title={t('$module:title')}
      subtitle={t('$module:subtitle')}
      basePath="/business/$APP/$MODULE"
      createPath="/business/$APP/$MODULE/new"
    />
  );
}
```

---

## TEMPLATE: LISTE VIEW (Composant réutilisable)

```tsx
// components/$MODULE/$MODULE_PASCALListView.tsx

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Plus, Search, MoreVertical, Pencil, Trash2 } from 'lucide-react';
import { api } from '@/services/api/apiClient';
import { use$MODULE_PASCALPreferences } from '@/hooks/use$MODULE_PASCALPreferences';
import { Pagination } from '@/components/common/Pagination';
import { ColumnSelector } from '@/components/common/ColumnSelector';
import { ViewModeToggle } from '@/components/common/ViewModeToggle';

interface $ENTITY_PASCALDto {
  id: string;
  name: string;
  description: string | null;
  isActive: boolean;
  createdAt: string;
  updatedAt: string | null;
}

interface PagedResult {
  items: $ENTITY_PASCALDto[];
  totalCount: number;
  page: number;
  pageSize: number;
}

interface $MODULE_PASCALListViewProps {
  title: string;
  subtitle?: string;
  basePath: string;
  createPath?: string;
}

const DEFAULT_COLUMNS = ['name', 'description', 'isActive', 'createdAt'];

export function $MODULE_PASCALListView({
  title,
  subtitle,
  basePath,
  createPath,
}: $MODULE_PASCALListViewProps) {
  const { t } = useTranslation(['$module', 'common']);
  const navigate = useNavigate();
  const {
    pageSize,
    sortColumn,
    sortDirection,
    visibleColumns,
    viewMode,
    setPageSize,
    setSortColumn,
    setSortDirection,
    setVisibleColumns,
    setViewMode,
  } = use$MODULE_PASCALPreferences();

  const [data, setData] = useState<PagedResult | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [page, setPage] = useState(1);
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    loadData();
  }, [page, pageSize, sortColumn, sortDirection, searchTerm]);

  const loadData = async () => {
    try {
      setLoading(true);
      setError(null);
      const params = new URLSearchParams({
        page: page.toString(),
        pageSize: pageSize.toString(),
        sortColumn,
        sortDirection,
        ...(searchTerm && { searchTerm }),
      });
      const result = await api.get<PagedResult>(`/api/business/$MODULE?${params}`);
      setData(result);
    } catch (err) {
      setError(t('common:errors.loadFailed'));
      console.error('Failed to load $module:', err);
    } finally {
      setLoading(false);
    }
  };

  const handleSort = (column: string) => {
    if (sortColumn === column) {
      setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
    } else {
      setSortColumn(column);
      setSortDirection('asc');
    }
  };

  const handleDelete = async (id: string) => {
    if (!confirm(t('common:confirmDelete'))) return;

    try {
      await api.delete(`/api/business/$MODULE/${id}`);
      loadData();
    } catch (err) {
      console.error('Failed to delete:', err);
    }
  };

  const columns = [
    { key: 'name', label: t('$module:columns.name'), sortable: true },
    { key: 'description', label: t('$module:columns.description'), sortable: false },
    { key: 'isActive', label: t('$module:columns.status'), sortable: true },
    { key: 'createdAt', label: t('$module:columns.createdAt'), sortable: true },
  ];

  return (
    <div className="space-y-6">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold text-[var(--text-primary)]">{title}</h1>
          {subtitle && (
            <p className="text-[var(--text-secondary)] mt-1">{subtitle}</p>
          )}
        </div>
        {createPath && (
          <button
            onClick={() => navigate(createPath)}
            className="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white font-medium transition-colors"
            style={{ borderRadius: 'var(--radius-button)' }}
          >
            <Plus className="w-4 h-4" />
            {t('common:actions.create')}
          </button>
        )}
      </div>

      {/* Toolbar */}
      <div className="flex items-center justify-between gap-4">
        <div className="relative flex-1 max-w-md">
          <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
          <input
            type="text"
            placeholder={t('common:search.placeholder')}
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            className="w-full pl-10 pr-4 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] text-sm focus:outline-none focus:border-[var(--color-accent-500)]"
            style={{ borderRadius: 'var(--radius-input)' }}
          />
        </div>
        <div className="flex items-center gap-2">
          <ColumnSelector
            columns={columns}
            visibleColumns={visibleColumns}
            onChange={setVisibleColumns}
          />
          <ViewModeToggle value={viewMode} onChange={setViewMode} />
        </div>
      </div>

      {/* Content */}
      {loading ? (
        <div className="flex items-center justify-center py-12">
          <div className="animate-spin h-8 w-8 border-4 border-[var(--color-accent-500)] border-t-transparent rounded-full" />
        </div>
      ) : error ? (
        <div className="p-4 bg-[var(--error-bg)] border border-[var(--error-border)] text-[var(--error-text)]" style={{ borderRadius: 'var(--radius-card)' }}>
          {error}
        </div>
      ) : viewMode === 'list' ? (
        <div className="bg-[var(--bg-card)] border border-[var(--item-color-border)] overflow-hidden" style={{ borderRadius: 'var(--radius-card)' }}>
          <table className="w-full">
            <thead>
              <tr className="border-b border-[var(--item-color-border)]">
                {columns.filter(col => visibleColumns.includes(col.key)).map((col) => (
                  <th
                    key={col.key}
                    className={`px-4 py-3 text-left text-sm font-medium text-[var(--text-secondary)] ${
                      col.sortable ? 'cursor-pointer hover:text-[var(--text-primary)]' : ''
                    }`}
                    onClick={() => col.sortable && handleSort(col.key)}
                  >
                    {col.label}
                    {sortColumn === col.key && (
                      <span className="ml-1">{sortDirection === 'asc' ? '↑' : '↓'}</span>
                    )}
                  </th>
                ))}
                <th className="px-4 py-3 text-right text-sm font-medium text-[var(--text-secondary)]">
                  {t('common:actions.title')}
                </th>
              </tr>
            </thead>
            <tbody>
              {data?.items.map((item) => (
                <tr
                  key={item.id}
                  className="border-b border-[var(--item-color-border)] hover:bg-[var(--bg-hover)] cursor-pointer"
                  onClick={() => navigate(`${basePath}/${item.id}`)}
                >
                  {visibleColumns.includes('name') && (
                    <td className="px-4 py-3 text-sm text-[var(--text-primary)] font-medium">
                      {item.name}
                    </td>
                  )}
                  {visibleColumns.includes('description') && (
                    <td className="px-4 py-3 text-sm text-[var(--text-secondary)]">
                      {item.description || '-'}
                    </td>
                  )}
                  {visibleColumns.includes('isActive') && (
                    <td className="px-4 py-3">
                      <span
                        className={`inline-flex items-center px-2 py-1 text-xs font-medium ${
                          item.isActive
                            ? 'bg-green-100 text-green-800'
                            : 'bg-gray-100 text-gray-800'
                        }`}
                        style={{ borderRadius: 'var(--radius-badge)' }}
                      >
                        {item.isActive ? t('common:status.active') : t('common:status.inactive')}
                      </span>
                    </td>
                  )}
                  {visibleColumns.includes('createdAt') && (
                    <td className="px-4 py-3 text-sm text-[var(--text-secondary)]">
                      {new Date(item.createdAt).toLocaleDateString()}
                    </td>
                  )}
                  <td className="px-4 py-3 text-right">
                    <div className="flex items-center justify-end gap-2">
                      <button
                        onClick={(e) => {
                          e.stopPropagation();
                          navigate(`${basePath}/${item.id}/edit`);
                        }}
                        className="p-1 hover:bg-[var(--bg-tertiary)]"
                        style={{ borderRadius: 'var(--radius-button)' }}
                      >
                        <Pencil className="w-4 h-4 text-[var(--text-secondary)]" />
                      </button>
                      <button
                        onClick={(e) => {
                          e.stopPropagation();
                          handleDelete(item.id);
                        }}
                        className="p-1 hover:bg-[var(--error-bg)]"
                        style={{ borderRadius: 'var(--radius-button)' }}
                      >
                        <Trash2 className="w-4 h-4 text-[var(--error-text)]" />
                      </button>
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : (
        /* Grid view */
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
          {data?.items.map((item) => (
            <div
              key={item.id}
              className="bg-[var(--bg-card)] border border-[var(--item-color-border)] p-4 hover:border-[var(--color-accent-500)] cursor-pointer transition-colors"
              style={{ borderRadius: 'var(--radius-card)' }}
              onClick={() => navigate(`${basePath}/${item.id}`)}
            >
              <h3 className="font-medium text-[var(--text-primary)]">{item.name}</h3>
              <p className="text-sm text-[var(--text-secondary)] mt-1 line-clamp-2">
                {item.description || '-'}
              </p>
              <div className="flex items-center justify-between mt-3">
                <span
                  className={`inline-flex items-center px-2 py-1 text-xs font-medium ${
                    item.isActive
                      ? 'bg-green-100 text-green-800'
                      : 'bg-gray-100 text-gray-800'
                  }`}
                  style={{ borderRadius: 'var(--radius-badge)' }}
                >
                  {item.isActive ? t('common:status.active') : t('common:status.inactive')}
                </span>
                <span className="text-xs text-[var(--text-muted)]">
                  {new Date(item.createdAt).toLocaleDateString()}
                </span>
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Pagination */}
      {data && (
        <Pagination
          page={data.page}
          pageSize={data.pageSize}
          totalCount={data.totalCount}
          onPageChange={setPage}
          onPageSizeChange={setPageSize}
        />
      )}
    </div>
  );
}
```

---

## TEMPLATE: HOOK PRÉFÉRENCES

```tsx
// hooks/use$MODULE_PASCALPreferences.ts

import { useUserPreferences } from '@/contexts/UserPreferencesContext';

const DEFAULT_COLUMNS = ['name', 'description', 'isActive', 'createdAt'];

export function use$MODULE_PASCALPreferences() {
  const { preferences, updatePreference } = useUserPreferences();
  const modulePrefs = preferences.$module || {};

  return {
    // Getters with defaults
    pageSize: modulePrefs.pageSize ?? 10,
    sortColumn: modulePrefs.sortColumn ?? 'createdAt',
    sortDirection: modulePrefs.sortDirection ?? 'desc',
    filters: modulePrefs.filters ?? {},
    visibleColumns: modulePrefs.visibleColumns ?? DEFAULT_COLUMNS,
    viewMode: modulePrefs.viewMode ?? 'list',

    // Setters
    setPageSize: (size: number) =>
      updatePreference('$module.pageSize', size),
    setSortColumn: (col: string) =>
      updatePreference('$module.sortColumn', col),
    setSortDirection: (dir: 'asc' | 'desc') =>
      updatePreference('$module.sortDirection', dir),
    setFilters: (filters: Record<string, unknown>) =>
      updatePreference('$module.filters', filters),
    setVisibleColumns: (cols: string[]) =>
      updatePreference('$module.visibleColumns', cols),
    setViewMode: (mode: 'list' | 'grid') =>
      updatePreference('$module.viewMode', mode),
  };
}
```

---

## TEMPLATE: SERVICE API

```tsx
// services/api/$moduleApi.ts

import { api } from './apiClient';

export interface $ENTITY_PASCALDto {
  id: string;
  name: string;
  description: string | null;
  isActive: boolean;
  createdAt: string;
  updatedAt: string | null;
}

export interface Create$ENTITY_PASCALRequest {
  name: string;
  description?: string;
}

export interface Update$ENTITY_PASCALRequest {
  name: string;
  description?: string;
}

export interface PagedResult<T> {
  items: T[];
  totalCount: number;
  page: number;
  pageSize: number;
}

export interface QueryParameters {
  page?: number;
  pageSize?: number;
  searchTerm?: string;
  sortColumn?: string;
  sortDirection?: 'asc' | 'desc';
}

export const $moduleApi = {
  getAll: async (params: QueryParameters = {}): Promise<PagedResult<$ENTITY_PASCALDto>> => {
    const queryParams = new URLSearchParams();
    if (params.page) queryParams.set('page', params.page.toString());
    if (params.pageSize) queryParams.set('pageSize', params.pageSize.toString());
    if (params.searchTerm) queryParams.set('searchTerm', params.searchTerm);
    if (params.sortColumn) queryParams.set('sortColumn', params.sortColumn);
    if (params.sortDirection) queryParams.set('sortDirection', params.sortDirection);

    return api.get(`/api/business/$MODULE?${queryParams}`);
  },

  getById: async (id: string): Promise<$ENTITY_PASCALDto> => {
    return api.get(`/api/business/$MODULE/${id}`);
  },

  create: async (data: Create$ENTITY_PASCALRequest): Promise<$ENTITY_PASCALDto> => {
    return api.post('/api/business/$MODULE', data);
  },

  update: async (id: string, data: Update$ENTITY_PASCALRequest): Promise<$ENTITY_PASCALDto> => {
    return api.put(`/api/business/$MODULE/${id}`, data);
  },

  delete: async (id: string): Promise<void> => {
    return api.delete(`/api/business/$MODULE/${id}`);
  },
};
```

---

## TEMPLATE: ROUTES (App.tsx)

```tsx
// Ajouter dans App.tsx

import { $MODULE_PASCALPage } from '@/pages/business/$APP/$MODULE/$MODULE_PASCALPage';
import { $MODULE_PASCALDetailPage } from '@/pages/business/$APP/$MODULE/$MODULE_PASCALDetailPage';
import { Create$MODULE_PASCALPage } from '@/pages/business/$APP/$MODULE/Create$MODULE_PASCALPage';

// Dans les routes - TOUJOURS sous /business/
<Route path="/business/$APP/$MODULE" element={<$MODULE_PASCALPage />} />
<Route path="/business/$APP/$MODULE/new" element={<Create$MODULE_PASCALPage />} />
<Route path="/business/$APP/$MODULE/:id" element={<$MODULE_PASCALDetailPage />} />
<Route path="/business/$APP/$MODULE/:id/edit" element={<Create$MODULE_PASCALPage />} />
```

---

## CHECKLIST FRONTEND

| Vérification | Status |
|--------------|--------|
| ☐ Page principale créée (sous /business/) | |
| ☐ Composant ListView créé | |
| ☐ Hook préférences créé | |
| ☐ Service API créé (endpoint /api/business/) | |
| ☐ Routes ajoutées dans App.tsx (sous /business/) | |
| ☐ Utilise apiClient (pas d'appel direct) | |
| ☐ Pas d'import Infrastructure | |
| ☐ npm run build réussi | |
| ☐ npm run lint réussi | |
