npm package: @kodeme-io/next-core-partner
Description: Partner/Customer management module for Next.js + Odoo applications
Author: ABC Food
Latest version: 0.3.2

# @next-odoo/partner

Partner/Customer management module for Next.js + Odoo applications with Canvas pipeline, NOO workflow, and approval management.

## Features

- ✅ **Customer Management** - CRUD operations for partners/customers
- ✅ **Canvas Pipeline** - Prospect to customer conversion with stages
- ✅ **NOO Workflow** - New Outlet Opening application process
- ✅ **Approval System** - Tier validation for taking_order customers
- ✅ **React Query Hooks** - Optimistic updates and caching
- ✅ **Type Safety** - Full TypeScript support
- ✅ **Operating Unit** - Multi-branch/region filtering
- ✅ **Search** - Fuzzy search by name, ref, or mobile

## Installation

```bash
pnpm add @next-odoo/partner @next-odoo/odoo-api @tanstack/react-query
```

## Usage

### Setup

```typescript
import { createOdooClient } from '@next-odoo/odoo-api'
import { createPartnerAPI } from '@next-odoo/partner'

// Create Odoo client
const odoo = createOdooClient({
  url: process.env.NEXT_PUBLIC_ODOO_URL!,
  database: process.env.NEXT_PUBLIC_ODOO_DB!,
  sessionId: userSessionId
})

// Create Partner API
const partnerAPI = createPartnerAPI(odoo)
```

### Query Hooks

#### Get All Customers

```typescript
import { usePartners } from '@next-odoo/partner'

export function CustomerList() {
  const { data: customers, isLoading, error } = usePartners(partnerAPI, {
    domain: [['active', '=', true]],
    limit: 50,
    order: 'name ASC'
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <ul>
      {customers?.map(customer => (
        <li key={customer.id}>{customer.name}</li>
      ))}
    </ul>
  )
}
```

#### Get Single Customer

```typescript
import { usePartner } from '@next-odoo/partner'

export function CustomerDetail({ id }: { id: number }) {
  const { data: customer } = usePartner(partnerAPI, id)

  if (!customer) return null

  return (
    <div>
      <h1>{customer.name}</h1>
      <p>Email: {customer.email}</p>
      <p>Phone: {customer.mobile}</p>
      <p>Type: {customer.customer_type}</p>
    </div>
  )
}
```

#### Get Canvas Customers

```typescript
import { useCanvasCustomers } from '@next-odoo/partner'

export function CanvasDashboard() {
  const { data: canvasCustomers } = useCanvasCustomers(partnerAPI)

  return (
    <div>
      <h2>Canvas Customers ({canvasCustomers?.length})</h2>
      {/* Render canvas pipeline */}
    </div>
  )
}
```

#### Get Prospects

```typescript
import { useProspects } from '@next-odoo/partner'

export function ProspectList() {
  const { data: prospects } = useProspects(partnerAPI)

  return <div>Total Prospects: {prospects?.length}</div>
}
```

#### Get Customers Pending Approval

```typescript
import { usePendingApproval } from '@next-odoo/partner'

export function ApprovalQueue() {
  const { data: pending } = usePendingApproval(partnerAPI)

  return (
    <div>
      <h2>Pending Approval ({pending?.length})</h2>
      {pending?.map(customer => (
        <CustomerApprovalCard key={customer.id} customer={customer} />
      ))}
    </div>
  )
}
```

#### Get Canvas Stages

```typescript
import { useCanvasStages } from '@next-odoo/partner'

export function CanvasPipeline() {
  const { data: stages } = useCanvasStages(partnerAPI)

  return (
    <div className="flex gap-4">
      {stages?.map(stage => (
        <div key={stage.id} className="flex-1">
          <h3>{stage.name}</h3>
          {/* Render customers in this stage */}
        </div>
      ))}
    </div>
  )
}
```

### Mutation Hooks

#### Create Customer

```typescript
import { useCreatePartner } from '@next-odoo/partner'

export function CreateCustomerForm() {
  const createCustomer = useCreatePartner(partnerAPI)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()

    try {
      const customerId = await createCustomer.mutateAsync({
        name: 'New Customer',
        email: 'customer@example.com',
        mobile: '+1234567890',
        customer_type: 'canvas',
        is_canvas_customer: true
      })

      console.log('Created customer:', customerId)
    } catch (error) {
      console.error('Failed to create customer:', error)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit" disabled={createCustomer.isPending}>
        {createCustomer.isPending ? 'Creating...' : 'Create Customer'}
      </button>
    </form>
  )
}
```

#### Update Customer

```typescript
import { useUpdatePartner } from '@next-odoo/partner'

export function EditCustomer({ customerId }: { customerId: number }) {
  const updateCustomer = useUpdatePartner(partnerAPI)

  const handleUpdate = async () => {
    await updateCustomer.mutateAsync({
      id: customerId,
      updates: {
        phone: '+0987654321',
        email: 'updated@example.com'
      }
    })
  }

  return <button onClick={handleUpdate}>Update Customer</button>
}
```

#### Convert to Canvas

```typescript
import { useConvertToCanvas } from '@next-odoo/partner'

export function ConvertButton({ customerId, initialStageId }: Props) {
  const convertToCanvas = useConvertToCanvas(partnerAPI)

  const handleConvert = async () => {
    await convertToCanvas.mutateAsync({
      customerId,
      stageId: initialStageId
    })
  }

  return (
    <button onClick={handleConvert}>
      Convert to Canvas
    </button>
  )
}
```

#### Move to Stage

```typescript
import { useMoveToStage } from '@next-odoo/partner'

export function MoveStageButton({ customerId, newStageId }: Props) {
  const moveToStage = useMoveToStage(partnerAPI)

  return (
    <button onClick={() => moveToStage.mutate({ customerId, stageId: newStageId })}>
      Move to Next Stage
    </button>
  )
}
```

#### Approve Customer

```typescript
import { useApproveCustomer } from '@next-odoo/partner'

export function ApproveButton({ customerId }: { customerId: number }) {
  const approveCustomer = useApproveCustomer(partnerAPI)

  const handleApprove = async () => {
    await approveCustomer.mutateAsync({
      customerId,
      notes: 'Approved after verification'
    })
  }

  return <button onClick={handleApprove}>Approve</button>
}
```

#### Reject Customer

```typescript
import { useRejectCustomer } from '@next-odoo/partner'

export function RejectButton({ customerId }: { customerId: number }) {
  const rejectCustomer = useRejectCustomer(partnerAPI)

  const handleReject = async () => {
    await rejectCustomer.mutateAsync({
      customerId,
      reason: 'Incomplete information'
    })
  }

  return <button onClick={handleReject}>Reject</button>
}
```

### Search Customers

```typescript
import { useSearchCustomers } from '@next-odoo/partner'
import { useState } from 'react'

export function CustomerSearch() {
  const [query, setQuery] = useState('')
  const { data: results } = useSearchCustomers(partnerAPI, query, 20)

  return (
    <div>
      <input
        type="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search customers..."
      />
      {results?.map(customer => (
        <div key={customer.id}>{customer.name}</div>
      ))}
    </div>
  )
}
```

### NOO (New Outlet Opening)

#### Get NOO Candidates

```typescript
import { useNOOCandidates } from '@next-odoo/partner'

export function NOODashboard() {
  const { data: candidates } = useNOOCandidates(partnerAPI)

  return (
    <div>
      <h2>NOO Applications ({candidates?.length})</h2>
      {/* Render NOO applications */}
    </div>
  )
}
```

#### Create NOO Application

```typescript
import { useCreateNOO } from '@next-odoo/partner'

export function CreateNOOForm({ partnerId }: { partnerId: number }) {
  const createNOO = useCreateNOO(partnerAPI)

  const handleSubmit = async () => {
    await createNOO.mutateAsync({
      partner_id: partnerId,
      business_type: 'retail',
      estimated_monthly_volume: 10000000,
      investment_capacity: 50000000,
      business_experience: '5 years in retail'
    })
  }

  return <button onClick={handleSubmit}>Submit NOO Application</button>
}
```

## API Reference

### PartnerAPI

Direct API methods (used by hooks):

```typescript
const partnerAPI = createPartnerAPI(odooClient)

// Query methods
await partnerAPI.getCustomers(options)
await partnerAPI.getCustomerById(id)
await partnerAPI.getCanvasCustomers()
await partnerAPI.getTakingOrderCustomers()
await partnerAPI.getProspects()
await partnerAPI.getPendingApproval()
await partnerAPI.getCanvasStages()
await partnerAPI.searchCustomers(query, limit)

// Mutation methods
await partnerAPI.createCustomer(data)
await partnerAPI.updateCustomer(id, updates)
await partnerAPI.deleteCustomer(id)
await partnerAPI.convertToCanvas(customerId, stageId)
await partnerAPI.moveToStage(customerId, stageId)
await partnerAPI.approveCustomer(customerId, notes)
await partnerAPI.rejectCustomer(customerId, reason)
await partnerAPI.requestValidation(customerId)

// NOO methods
await partnerAPI.getNOOCandidates()
await partnerAPI.createNOOApplication(data)
await partnerAPI.approveNOO(nooId)
await partnerAPI.rejectNOO(nooId, reason)
```

## Types

### Customer

```typescript
interface Customer {
  id: number
  name: string
  ref?: string
  email?: string
  phone?: string
  mobile?: string
  street?: string
  city?: string
  customer_type: 'canvas' | 'taking_order'
  approval_status?: 'draft' | 'pending_approval' | 'approved' | 'rejected'
  is_canvas_customer: boolean
  canvas_stage_id?: number
  latitude?: number
  longitude?: number
  // ... many more fields
}
```

### CanvasStage

```typescript
interface CanvasStage {
  id: number
  name: string
  sequence: number
  is_initial_stage: boolean
  is_final_stage: boolean
}
```

### CandidateNOO

```typescript
interface CandidateNOO {
  id: number
  partner_id: number
  business_type: 'retail' | 'wholesaler' | 'distributor' | 'other'
  state: 'draft' | 'submitted' | 'under_review' | 'approved' | 'rejected' | 'converted'
  // ... more fields
}
```

## Best Practices

1. **Reuse API instance** - Create once, use across all hooks
2. **Use query keys** - Leverage `partnerKeys` for cache invalidation
3. **Handle loading states** - Always check `isLoading` and `error`
4. **Optimistic updates** - React Query handles this automatically
5. **Stale time** - Configured per hook (2-30 minutes)

## License

MIT