# Setup: CRUD Operations

**CRUD = Create, Read, Update, Delete.** Framework handles data ops with built-in security.

---

## Quick Import Reference

| What | Package |
|------|---------|
| **Hooks** (`useCrud`, `useCrudList`, `useCrudCardList`, `useEntityForm`, `useEntityFavorites`) | `@donotdev/crud` |
| **Display Utilities** (`formatValue`, `DisplayFieldRenderer`, `EntityFilters`, `translateFieldLabel`) | `@donotdev/crud` |
| **Form Components** (`FormFieldRenderer`, `Controlled*Field`, `UploadProvider`) | `@donotdev/crud` |
| **Routing-Aware Components** (`EntityFormRenderer`, `EntityList`, `EntityCardList`, `EntityDisplayRenderer`) | `@donotdev/ui` |
| **Specialized Templates** (`ProductCardListTemplate`, `CarCardListTemplate`, `InquiryFormTemplate`, `InquiryAdminTemplate`) | `@donotdev/templates` |
| **Routing** (`useNavigate`, `Link`, `useParams`) | `@donotdev/ui` |
| **Entity Definition** (`defineEntity`) | `@donotdev/core` |

**Architecture:** Hooks and display utilities live in `@donotdev/crud`. Routing-aware page components that compose CRUD with navigation live in `@donotdev/ui`.

---

## 1. Define Entity

```typescript
// entities/product.ts
import { defineEntity } from '@donotdev/core';

export const productEntity = defineEntity({
  name: 'Product',
  collection: 'products',
  // namespace: 'entity-product', // Optional: defaults to entity-${name.toLowerCase()}
  fields: {
    name: {
      name: 'name',
      label: 'name',
      type: 'text',
      visibility: 'guest',
      validation: { required: true }
    },
    price: {
      name: 'price',
      label: 'price',
      type: 'price',  // Structured: { amount, currency?, vatIncluded?, discountPercent? }
      visibility: 'guest',
      validation: { required: true },
      // Optional: omit options entirely → defaultCurrency 'EUR', list = [EUR, USD, GBP, CHF, JPY, KRW]
      options: {
        fieldSpecific: {
          defaultCurrency: 'EUR',       // Omit → 'EUR'. Used when value.currency is unset
          currencies: ['EUR', 'USD'],   // Omit → framework default (6 currencies). One item e.g. ['EUR'] → no dropdown, EUR only
        },
      },
    },
    image: {
      name: 'image',
      label: 'image',
      type: 'image',
      visibility: 'guest'
    },
    customerContact: {
      name: 'customerContact',
      label: 'customerContact',
      type: 'email',
      visibility: 'admin',
      editable: 'admin'
    }
  }
});
```

**Auto-added fields:** `id`, `createdAt`, `updatedAt`, `createdById`, `updatedById`, `status`

---

## 2. Multi-Tenancy (Company/Tenant Scoping)

For multi-tenant apps where data belongs to a company/tenant/workspace:

### Step 1: Register Scope Provider (once at app startup)

```typescript
// src/main.tsx or src/App.tsx
import { registerScopeProvider } from '@donotdev/core';
import { useCurrentCompanyStore } from './stores/currentCompanyStore';

// Register before app renders
registerScopeProvider('company', () =>
  useCurrentCompanyStore.getState().currentCompanyId
);
```

### Step 2: Add `scope` to Entity Definition

```typescript
// entities/client.ts
import { defineEntity } from '@donotdev/core';

export const clientEntity = defineEntity({
  name: 'Client',
  collection: 'clients',
  scope: { field: 'companyId', provider: 'company' }, // <-- Add this
  fields: {
    // NO need to manually define companyId - framework adds it automatically
    name: { name: 'name', label: 'name', type: 'text', visibility: 'user', validation: { required: true } },
    // ...
  }
});
```

### What Happens Automatically

| Operation | Behavior |
|-----------|----------|
| `add()` / `set()` | Auto-injects `companyId` from scope provider |
| `useCrudList()` | Auto-filters by `companyId == currentCompanyId` |
| `useCrudCardList()` | Auto-filters by `companyId == currentCompanyId` |
| `query()` | Auto-adds `where companyId == currentCompanyId` |

**Result:** Zero boilerplate. Users only see data from their current company.

### Scope Field Properties

The auto-added scope field has:
- `type: 'reference'` (references the scoped collection, e.g., `companies`)
- `visibility: 'technical'` (hidden from regular users)
- `editable: 'create-only'` (cannot change scope after creation)

### Multiple Scope Types

You can register multiple scope providers for different use cases:

```typescript
registerScopeProvider('company', () => companyStore.getState().currentCompanyId);
registerScopeProvider('workspace', () => workspaceStore.getState().currentWorkspaceId);
registerScopeProvider('tenant', () => tenantStore.getState().currentTenantId);
```

---

## 3. Visibility & Access

### Field Visibility (who SEES)
| Level | Who |
|-------|-----|
| `'guest'` | Everyone |
| `'user'` | Authenticated |
| `'admin'` | Admins |
| `'super'` | Super admins |
| `'technical'` | Admins only (read-only in forms; e.g. scope field) |
| `'owner'` | Only when the current user is a stakeholder (see **Stakeholder Access** below) |
| `'hidden'` | Never |

### Stakeholder Access (ownership)

For marketplace-style entities: documents **public when available**, **private to stakeholders** (e.g. partner, customer) when booked.

**1. Add `ownership` to the entity:**

```typescript
ownership: {
  ownerFields: ['providerId', 'customerId'],   // Document fields whose value is a user id
  publicCondition: [                            // When can anyone read? (AND together)
    { field: 'status', op: '==', value: 'available' },
  ],
},
```

**2. Use `visibility: 'owner'`** for fields only stakeholders should see (e.g. internal notes, customer contact after booking):

```typescript
internalNotes: {
  name: 'internalNotes',
  label: 'Internal notes',
  type: 'textarea',
  visibility: 'owner',   // Returned only when request.auth.uid matches one of ownerFields
  editable: true,
  validation: { required: false },
},
```

**3. Firestore rules (read and update):** Use the framework helper, then paste the condition into your hand-written `firestore.rules`:

```typescript
import { generateFirestoreRuleCondition } from '@donotdev/core';

const condition = generateFirestoreRuleCondition(scheduleEntity.ownership);
// Paste into firestore.rules: allow read, update: if <condition>;
```

**Behavior:** `listCard_` returns only documents matching `publicCondition` (e.g. public slots). `list_` returns only documents where the current user is in one of `ownerFields` ("mine"). Same condition is used for **allow read** and **allow update** so owners can update (e.g. status to cancelled, notes). For multiple owner fields, prefer an `ownerIds` array and `array-contains` in rules and queries.

### Entity Access (who CAN DO)
```typescript
access: {
  create: 'admin',  // default
  read: 'guest',    // default
  update: 'admin',  // default
  delete: 'admin',  // default
}
```

**Override:**
```typescript
// Contact form - guests submit, admins read
access: { create: 'guest', read: 'admin' }
```

---

## 3. Register Functions (Backend)

```typescript
// functions/src/index.ts
import { initializeApp } from 'firebase-admin/app';
import { createCrudFunctions } from '@donotdev/functions/firebase';
import * as entities from 'entities';

initializeApp();
export const crud = createCrudFunctions(entities);
```

**Deploy:** `dndev deploy`

---

## 4. Frontend Usage

Use components directly from `@donotdev/ui`. They handle routing automatically - no navigate handlers needed.

#### Create/Edit Page

```tsx
import { useCrud } from '@donotdev/crud';
import { EntityFormRenderer, useParams } from '@donotdev/ui';
import { productEntity } from 'entities';

export default function ProductPage() {
  const { id } = useParams<{ id: string }>();
  const { add, update, get } = useCrud(productEntity);
  const isNew = id === 'new';
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    if (!isNew && id) get(id).then(setData);
  }, [id]);

  const handleSubmit = async (formData: any) => {
    isNew ? await add(formData) : await update(id!, formData);
    // Navigation happens automatically via cancelPath (defaults to /products)
  };

  if (!isNew && !data) return <Spinner />;

  return (
    <EntityFormRenderer
      entity={productEntity}
      operation={isNew ? 'create' : 'edit'}
      defaultValues={data}
      onSubmit={handleSubmit}
      // Cancel automatically navigates to /products (or cancelPath if provided)
      cancelPath="/products"
    />
  );
}
```

#### List Page

```tsx
import { EntityList, PageContainer } from '@donotdev/ui';
import { useAuth } from '@donotdev/auth';
import { productEntity } from 'entities';

export default function ProductsPage() {
  const user = useAuth('user');

  return (
    <PageContainer>
      <EntityList
        entity={productEntity}
        userRole={user?.role}
        // Routing: basePath defaults to /products. View/Edit = basePath/:id, Create = basePath/new
        // Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
      />
    </PageContainer>
  );
}
```

#### Card Grid

```tsx
import { EntityCardList, PageContainer } from '@donotdev/ui';
import { productEntity } from 'entities';

export default function ShopPage() {
  return (
    <PageContainer>
      <EntityCardList
        entity={productEntity}
        // Routing: basePath defaults to /products. View = basePath/:id
        // Optional: basePath="/shop/products" or onClick={(id) => openSheet(id)}
      />
    </PageContainer>
  );
}
```

#### Detail/View Page

```tsx
import { EntityDisplayRenderer, useParams } from '@donotdev/ui';
import { productEntity } from 'entities';

export default function ProductDetailPage() {
  const { id } = useParams<{ id: string }>();

  return (
    <EntityDisplayRenderer
      entity={productEntity}
      id={id}
      // Auto-fetches data, shows loading state
      // Read-only display of all visible fields
    />
  );
}
```

#### Custom Detail Page with Favorites

For custom detail pages, add favorites support using `useEntityFavorites`:

```tsx
import { useState, useEffect } from 'react';
import { Heart } from 'lucide-react';
import { PageContainer, Section, Button, Stack } from '@donotdev/components';
import { useParams, useNavigate } from '@donotdev/ui';
import { useCrud, useEntityFavorites } from '@donotdev/crud';
import { useTranslation } from '@donotdev/core';
import { productEntity } from 'entities';

export default function ProductDetailPage() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const { get } = useCrud(productEntity);
  const { isFavorite, toggleFavorite } = useEntityFavorites({
    collection: productEntity.collection,
  });
  const { t } = useTranslation('crud');
  const [data, setData] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    if (id) {
      get(id).then((result) => {
        setData(result);
        setLoading(false);
      });
    }
  }, [id, get]);

  if (loading) return <div>Loading...</div>;
  if (!data) return <div>Not found</div>;

  return (
    <PageContainer>
      <Section>
        <Stack direction="row" gap="medium" align="center">
          <h1>{data.name}</h1>
          <Button
            variant={isFavorite(id) ? 'primary' : 'outline'}
            icon={<Heart size={18} fill={isFavorite(id) ? 'currentColor' : 'none'} />}
            onClick={() => toggleFavorite(id)}
          >
            {isFavorite(id) ? t('favorites.saved') : t('favorites.save')}
          </Button>
        </Stack>
        {/* Your custom detail page content */}
      </Section>
    </PageContainer>
  );
}
```

**Note:** `EntityCardList` automatically includes favorites support - heart icons on cards and a favorites filter toggle. No configuration needed.

#### Product Shop (Specialized Template)

For e-commerce/shop pages with price, image, and category support, use the specialized template:

```tsx
import { ProductCardListTemplate } from '@donotdev/templates';
import { productEntity } from 'entities';

export default function ShopPage() {
  return (
    <PageContainer>
      <ProductCardListTemplate
        entity={productEntity}
        // Auto-detects price, image, category fields
        // Routing: basePath defaults to /products. Optional: basePath or onClick
      />
    </PageContainer>
  );
}
```

#### Contact/Inquiry Form (Specialized Template)

For contact forms that create both a Customer and an Inquiry record:

```tsx
import { InquiryFormTemplate } from '@donotdev/templates';
import { customerEntity, inquiryEntity } from 'entities';

export default function ContactPage() {
  return (
    <PageContainer>
      <InquiryFormTemplate
        customerEntity={customerEntity}
        inquiryEntity={inquiryEntity}
        // Optional: contextId="car123" - links inquiry to a car/product
        // Optional: contextName="BMW X5 2024" - shown in message placeholder
        // Optional: contextDetails="BMW X5 2024 • 50,000 km • €45,000" - detailed placeholder
        // Optional: messageField="message" - defaults to 'message'
        // Optional: contextField="carId" - defaults to 'carId'
        // Optional: customerFields={['firstName', 'lastName', 'email', 'phone']} - fields to show
        // Optional: onSuccess={() => navigate('/thank-you')}
      />
    </PageContainer>
  );
}
```

**What it does:**
- Creates a Customer record (with `findOrCreate` logic via `uniqueKeys` - prevents duplicates by email/phone)
- Creates an Inquiry record linked to the customer
- Handles GDPR consent tracking
- Auto-fills message with context details if provided
- Shows success state after submission

**Requirements:**
- Customer entity must have `uniqueKeys` configured (e.g., `{ fields: ['email'], findOrCreate: true }`)
- Inquiry entity should have `customerId` field (type: `reference`)
- Both entities should have `status` field (defaults to `'draft'`)

#### Inquiry Admin Dashboard (Specialized Template)

For admin pages to review and respond to inquiries with one-click actions:

```tsx
import { InquiryAdminTemplate } from '@donotdev/templates';
import { customerEntity, inquiryEntity } from 'entities';

export default function InquiriesAdminPage() {
  return (
    <PageContainer>
      <InquiryAdminTemplate
        customerEntity={customerEntity}
        inquiryEntity={inquiryEntity}
        // Optional: customerBasePath="/customers" - defaults to '/customers'
      />
    </PageContainer>
  );
}
```

**What it does:**
- Shows all inquiries in a responsive card grid (1 column mobile, 2 columns desktop)
- Displays inquiry message preview (first 150 chars)
- Shows customer info inline (name, email, phone)
- One-click actions:
  - **Email** - Opens `mailto:` link (includes subject if `carId` present)
  - **Call** - Opens `tel:` link
  - **View Customer** - Navigates to customer detail page
  - **Mark Responded** - Updates inquiry status to `'responded'` (only shown if not already responded)
- Status badges with icons (✓ for responded, ⏰ for pending)
- Auto-sorted by priority: available → draft → responded → deleted (then by date, newest first)

**Features:**
- Fetches all inquiries and customers in parallel
- Efficient customer lookup via Map (no N+1 queries)
- Loading state with spinner
- Empty state message
- Responsive grid layout

---

## 5. Component Props

### EntityList
- `basePath` **(optional)** - Base path for view/edit/create. Defaults to `/${collection}`. View/Edit = basePath/:id, Create = basePath/new
- `onClick` **(optional)** - Called when user clicks a row. If provided, overrides navigation (e.g. open sheet)
- `userRole` **(optional)** - Current user role for field visibility

### EntityCardList
- `basePath` **(optional)** - Base path for view. Defaults to `/${collection}`. View = basePath/:id
- `onClick` **(optional)** - Called when user clicks a card. If provided, overrides navigation (e.g. open sheet)
- `cols` **(optional)** - `[mobile, tablet, desktop, wide]` (default: `[1, 2, 3, 4]`)
- `staleTime` **(optional)** - Cache stale time in ms (default: 30 min)
- `filter` **(optional)** - `(item: any) => boolean` - Client-side filter
- `hideFilters` **(optional)** - Hide filters section

**Favorites:** Automatically enabled - heart icons on cards, favorites filter toggle in filters section. Uses `useEntityFavorites` hook internally with `entity.collection` as storage key.

### EntityFormRenderer
- `entity` **(required)** - Entity definition
- `onSubmit` **(required)** - `(data: any) => Promise<void>` - Submit handler
- `defaultValues` **(optional)** - Initial form values (for edit mode)
- `operation` **(optional)** - `'create' | 'edit'` (auto-detected from defaultValues)
- `viewerRole` **(optional)** - Role for field visibility/editability
- `cancelPath` **(optional)** - Path to navigate on cancel (defaults to `/${collection}`)
- `onCancel` **(optional)** - Cancel button handler (takes precedence over cancelPath)
- `cancelText` **(optional)** - Cancel button text (null to hide)

### EntityDisplayRenderer
- `entity` **(required)** - Entity definition
- `id` **(required)** - Entity ID to fetch and display
- `viewerRole` **(optional)** - Role for field visibility

---

## 6. Field Types

### Text Inputs
- `text` - Single-line text input
- `email` - Email input with validation
- `tel` - Phone number input
- `url` - URL input
- `color` - Color picker
- `password` - Password input (masked)
- `textarea` - Multi-line text input
- `richtext` - Rich text editor

### Numbers
- `number` - Numeric input
- `currency` - Single amount with currency symbol (value: number; options: `fieldSpecific.currency`)
- `price` - **Structured price:** amount + currency, VAT label, discount %. Value: `{ amount, currency?, vatIncluded?, discountPercent? }`. Options: `fieldSpecific.defaultCurrency`, `fieldSpecific.currencies` (e.g. `['EUR', 'USD']`; one currency = no dropdown). Form: amount + collapsible options (currency if >1, VAT Incl., discount %). Display: formatted amount; when discounted shows original crossed out + effective price. Filterable by amount (range); "deals" = items with discount.
- `range` - Slider input
- `year` - Year input (combobox: type or select from dropdown)

### Boolean
- `checkbox` - Checkbox input
- `boolean` - Alias for checkbox
- `switch` - Toggle switch

### Dates & Time
- `date` - Date picker
- `datetime-local` - Date and time picker
- `time` - Time picker
- `week` - Week picker
- `month` - Month picker
- `timestamp` - Timestamp (Firestore Timestamp)

### Selection
- `select` - Dropdown select
- `combobox` - Searchable dropdown
- `multiselect` - Multiple selection dropdown
- `radio` - Radio button group

### Files & Media
- `file` - Single file upload (deferred: uploads on form submit)
- `files` - Multiple file uploads (deferred: uploads on form submit)
- `document` - Document upload (PDF, etc.) (deferred: uploads on form submit)
- `documents` - Multiple document uploads (deferred: uploads on form submit)
- `image` - Single image upload (deferred: uploads on form submit, optimistic UI updates)
- `images` - Multiple image uploads (deferred: uploads on form submit, optimistic UI updates)

**Note:** File uploads use deferred uploads when inside `EntityFormRenderer` - files upload to storage when you submit the form. Images show immediately with optimistic updates (blob URLs) and automatically replace with storage URLs after upload completes.

### Complex Types
- `geopoint` - Geographic coordinates (lat/lng)
- `address` - Address input with autocomplete
- `map` - Map picker
- `array` - Array of text inputs

### Special
- `avatar` - Avatar image upload
- `badge` - Badge display
- `hidden` - Hidden field (not displayed)
- `submit` - Submit button (uncontrolled)
- `reset` - Reset button (uncontrolled)

### Price field type (structured)

Use `type: 'price'` when you need amount, currency, VAT label, and optional discount in one field. Replaces separate "price" + "specialPrice" number fields.

**Definition:**
```typescript
price: {
  name: 'price',
  label: 'price',
  type: 'price',
  visibility: 'guest',
  validation: { required: true },
  options: {
    fieldSpecific: {
      defaultCurrency: 'EUR',           // Omit → 'EUR'. Used when value.currency is unset
      currencies: ['EUR', 'USD'],      // Omit → list = [EUR, USD, GBP, CHF, JPY, KRW]. One item e.g. ['EUR'] → no dropdown, single currency
      optionsTitle: 'Price options',   // Omit → 'Price options'. Aria-label for the expand button
    },
  },
},
```

**Value shape (stored in DB):**
```ts
{ amount: number; currency?: string; vatIncluded?: boolean; discountPercent?: number }
```
- Only `discountPercent` is stored for discount; effective price is computed as `amount * (1 - discountPercent/100)` for display.

**Form:** Main input = amount (number) with label "Price (symbol)". Collapsible "+" opens: currency select (if more than one in `currencies`), VAT included (checkbox), discount (%). Single currency → no dropdown, value still stored.

**Display:** Formatted amount + "VAT Incl." when `vatIncluded`. When `discountPercent > 0`: original amount crossed out + effective price.

**Templates:** `CarCardListTemplate`, `CarDetailTemplate`, `ProductCardListTemplate` use the price field when present and show a "Sale" (or `price.badge.sale` i18n) badge when discounted.

**Filtering:** Range filter on `amount`; optional "deals" filter (items with `discountPercent > 0`).

### Select Options

```typescript
category: {
  type: 'select',
  validation: {
    options: [
      { value: 'electronics', label: 'Electronics' },
      { value: 'clothing', label: 'Clothing' }
    ]
  }
}
```

### Year Field

Use `type: 'year'` for year inputs (combobox with type-or-select UX):

```typescript
year: {
  type: 'year',
  validation: {
    required: true,
    min: 1900,  // Optional: defaults to 1900
    max: 2100   // Optional: defaults to current year + 10
  }
}
```

### Switch with Custom Values

```typescript
transmission: {
  type: 'switch',
  options: {
    fieldSpecific: {
      uncheckedValue: 'Manual',
      checkedValue: 'Automatic',
    }
  }
}
```

---

## 7. Custom Fields & Schemas

### Custom Field Types with UI Components

For custom field types that need custom UI components:

```typescript
import { registerFieldType, useController } from '@donotdev/crud';
import type { ControlledFieldProps } from '@donotdev/crud';
import { defineEntity } from '@donotdev/core';
import * as v from 'valibot';

function RepairOperationsField({
  fieldConfig,
  control,
  errors,
  t,
}: ControlledFieldProps) {
  // REQUIRED: Use framework's useController (not react-hook-form's) - ensures type compatibility
  const { field, fieldState } = useController({
    name: fieldConfig.name,
    control: control,
  });

  // Use field.value and field.onChange for form state
  const value = (field.value as any) || [];

  return (
    <div>
      <label>{t(fieldConfig.label)}</label>
      {/* Your custom UI here */}
      {fieldState?.error && (
        <span className="error">{fieldState.error.message}</span>
      )}
    </div>
  );
}

// Register UI component
registerFieldType({
  type: 'repairOperations',
  controlledComponent: RepairOperationsField,
});

// Then define schema in entity
export const carEntity = defineEntity({
  name: 'Car',
  collection: 'cars',
  fields: {
    repairs: {
      name: 'repairs',
      label: 'repairs',
      type: 'repairOperations' as any,
      visibility: 'admin',
      validation: {
        required: false,
        // Custom Valibot schema - single source of truth
        schema: v.nullish(v.array(v.object({
          operation: v.string(),
          cost: v.number(),
        })))
      }
    }
  }
});
```

**Important:**
- Custom controlled components receive `control` prop, NOT `field` prop
- You MUST use `useController` hook to get `field` and `fieldState`
- Define schema in `validation.schema` - it's the single source of truth

### Custom Schemas (No Custom UI)

For custom validation without custom UI, just define the schema:

```typescript
import { defineEntity } from '@donotdev/core';
import * as v from 'valibot';

export const carEntity = defineEntity({
  name: 'Car',
  collection: 'cars',
  fields: {
    // Custom array field with inline schema
    repairs: {
      name: 'repairs',
      label: 'repairs',
      type: 'text',  // Can be any type, schema takes precedence
      visibility: 'admin',
      validation: {
        required: false,
        // Custom Valibot schema - takes priority over type-based schema
        schema: v.array(
          v.object({
            operation: v.string(),
            cost: v.number(),
            date: v.string(),  // ISO date string
          })
        )
      }
    },

    // Custom object field
    metadata: {
      name: 'metadata',
      label: 'metadata',
      type: 'map',
      visibility: 'admin',
      validation: {
        schema: v.object({
          source: v.string(),
          importedAt: v.string(),
          tags: v.array(v.string()),
        })
      }
    }
  }
});
```

**How It Works:**
- `validation.schema` is the **single source of truth** - works everywhere (client + server)
- Takes priority over built-in type schemas
- Entity is self-contained - no separate registrations needed

---

## 8. Hooks API

```typescript
import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';

// Actions
const { add, update, delete: remove, get } = useCrud(productEntity);

// List (auto-fetch for tables)
const { items, loading, refresh } = useCrudList(productEntity);

// Cards (optimized for public)
const { items, loading, refresh } = useCrudCardList(productEntity);
```

---

## 9. Custom Form Layouts

Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.

```tsx
import { useId, useEffect } from 'react';
import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
import { productEntity } from 'entities';

export function CustomProductForm() {
  const formId = useId();
  const { add } = useCrud(productEntity);
  const {
    control,
    handleSubmit,
    fields,
    formStatus,      // 'idle' | 'uploading' | 'submitting' | ...
    uploadProgress,  // 0-100 during uploads
    cleanup
  } = useEntityForm(productEntity, { formId });

  useEffect(() => cleanup, [cleanup]);

  return (
    <UploadProvider formId={formId}>
      <form onSubmit={handleSubmit(add)}>
        {/* Your custom layout here */}
        <Controller control={control} name="name" render={({ field }) => (
          <input {...field} placeholder="Product name" />
        )} />

        <button type="submit" disabled={formStatus !== 'idle'}>
          {formStatus === 'uploading' ? `Uploading ${uploadProgress}%...` : 'Save'}
        </button>
      </form>
    </UploadProvider>
  );
}
```

**Key points:**
- Pass `formId` to get automatic upload handling and loading states
- Wrap with `UploadProvider` if using file/image fields
- `handleSubmit` automatically uploads files before validation

---

## 10. i18n

**Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)

You can customize it by setting `namespace` in your entity definition:

```typescript
export const productEntity = defineEntity({
  name: 'Product',
  collection: 'products',
  namespace: 'custom-namespace', // Optional: override default
  fields: { ... }
});
```

**Default behavior:** If not specified, the namespace is automatically set to `entity-${entity.name.toLowerCase()}`.

**Translation file:** `locales/entity-product_en.json` (or your custom namespace)
```json
{
  "fields": {
    "name": "Product Name",
    "price": "Price"
  },
  "name": "Product"
}
```

Field `label` → translation key in `fields.*`

### Status Field Translations

The `status` field uses CRUD namespace translations with entity-specific overrides:

**Translation Fallback Order:**
1. **Entity namespace** (`entity-{name}`) - User overrides take priority
2. **CRUD namespace** (`crud`) - Framework defaults

**Framework Defaults** (in `crud` namespace):
- `crud:status.draft` → "Draft"
- `crud:status.available` → "Available"
- `crud:status.deleted` → "Deleted"

**Override in Entity Translation File:**

```json
// locales/entity-inquiry_en.json
{
  "status": {
    "draft": "New",
    "available": "Read",
    "deleted": "Closed"
  }
}
```

**How it works:**
- Framework components (`EntityList`, `EntityFormRenderer`, etc.) automatically use `[entity.namespace, 'crud']` translation namespaces
- Status labels are translated via `translateLabel()` which tries entity namespace first, then falls back to `crud`
- No `dndev` namespace fallback - CRUD is optional and doesn't pollute core framework translations

**Example:** For an `inquiry` entity:
- If `entity-inquiry_en.json` has `"status.draft": "New"` → displays "New"
- If not found → falls back to `crud:status.draft` → displays "Draft"
- Never falls back to `dndev` namespace

---

## 11. Display Utilities

For custom displays, use `formatValue` from `@donotdev/crud`:

```tsx
import { formatValue, translateFieldLabel } from '@donotdev/crud';
import { useTranslation } from '@donotdev/core';
import { productEntity } from 'entities';

function ProductCard({ item }) {
  const { t } = useTranslation(productEntity.namespace);

  // Format any field value using entity config
  const priceDisplay = formatValue(
    item.price,
    productEntity.fields.price,
    t,
    { compact: true }  // compact mode for lists/cards
  );

  // Get translated field label
  const priceLabel = translateFieldLabel('price', productEntity.fields.price, t);

  return (
    <div>
      <span>{priceLabel}: {priceDisplay}</span>
    </div>
  );
}
```

**`formatValue` handles all field types:** dates, numbers, selects (translates option labels), images, files, etc.

---

## 12. Routing Convention

| Route | Purpose |
|-------|---------|
| `/${collection}` | List |
| `/${collection}/new` | Create |
| `/${collection}/:id` | Detail/Edit |
