# Annkut 2026 — Backend API Contract

**Purpose of this file.** Drop it into the frontend repo. It is the complete,
self-contained description of the rebuilt backend, written so that someone (or
some agent) working only in the frontend codebase can migrate it without needing
access to the PHP.

Everything here was captured from the running API, not from memory.

- **Backend:** CodeIgniter 3, PHP 8.1, MySQL/MariaDB
- **Local base URL:** `http://localhost:8080/index.php/`
- **Production base URL:** `https://production.bharuchbaps.in/index.php/`
- **Status:** all endpoints below are implemented and tested end-to-end.

---

## 1. What changed, in one paragraph

The 2025 API had no sessions and no tokens: every request carried a `sevak_id`
in its JSON body and the server trusted it, which meant anyone could read anyone
else's data by editing one field. The 2026 API issues a **bearer token** at login
and derives the caller's identity and permissions from that token alone. A
`sevak_id` in a request body is now a *subject* ("tell me about this person"),
never a claim of identity. The data model also changed: a **Parivar** (family)
now sits between Mandal and Sevak, so one family holds many individuals.

---

## 2. Conventions

| | |
|---|---|
| Method | `POST` with a JSON body for everything except `seva/export_data` (`GET`) |
| Content type | `Content-Type: application/json` |
| Auth | `Authorization: Bearer <token>` on every endpoint except `login/login` |
| Success | HTTP 2xx, body always contains `"status": true` |
| Failure | HTTP 4xx/5xx, body is `{"status": false, "message": "..."}` |

### ⚠️ Numbers arrive as strings

MySQL columns come back as strings. `target_forms` is `"0"`, not `0`; `id` is
`"359"`, not `359`. Only values the PHP computes itself (`total`, `total_target`,
`year`, and the `seva_*` counters) are real numbers.

**Always coerce before arithmetic or comparison:**

```js
Number(sevak.target_forms) - Number(sevak.filled_forms)   // correct
sevak.target_forms - sevak.filled_forms                   // works by luck
sevak.id === 359                                          // false! it is "359"
```

### Status codes

| Code | Meaning |
|---|---|
| 200 / 201 | success |
| 400 | a required field is missing |
| 401 | no token, bad token, expired token, or wrong password |
| 403 | authenticated, but not allowed to touch that mandal / xetra / sevak |
| 404 | the thing does not exist |
| 409 | conflict — duplicate receipt number, book already exists, etc. |
| 422 | value is well-formed but invalid — receipt out of the book's range |

**401 means "log in again". 403 means "you may not see this" — do not retry, and
do not treat it as a login failure.**

---

## 3. Authentication

### `POST login/login` — the only unauthenticated endpoint

```json
{ "sevak_id": "AGSP004", "password": "annkut@2026" }
```

```json
{
  "status": true,
  "message": "Login successful",
  "token": "0eda16d1fd3e619cb1bb8a8ec0c4d9ab27952ac3630e3559f5202b938b1b29f2",
  "sevak": {
    "sevak_id": "AGSP004",
    "name": "PATEL PINAKINBHAI BHOGILAL",
    "surname": "PATEL", "first_name": "PINAKINBHAI", "middle_name": "BHOGILAL",
    "mobile": "9998213638",
    "pankh": "S", "pankh_label": "Sanyukt",
    "parivar": { "id": 2, "code": "NK002" },
    "mandal":  { "id": 7, "code": "NK", "name": "Narayan Kunj" },
    "xetra":   { "id": 1, "code": "BH01", "name": "Bharuch - 1" },
    "posts":   [ { "code": "SAH_NIRDESHAK", "name": "Sah Nirdeshak", "rank": "40" } ],
    "access":  { "global": false, "areas": [], "mandal_count": 6 },
    "must_change_password": true
  }
}
```

`parivar.members[]` carries the whole family, each with their own progress —
this is what the home-screen tabs are drawn from, so no second request is needed
after login. The logged-in sevak is always **first** and flagged `is_self: true`.

```json
"parivar": {
  "id": 432, "code": "MA004",
  "members": [
    { "id": 672, "sevak_code": "ASMA009", "full_name": "Makwana Prabhatsinh Somabhai",
      "surname": "Makwana", "first_name": "Prabhatsinh", "middle_name": "Somabhai",
      "mobile": "9924438401", "pankh": "S",
      "target_forms": 0, "filled_forms": 0, "collected_amount": 0, "is_self": true },
    { "id": 673, "sevak_code": "ASMA010", "full_name": "Gohil Prayosa Thakorbhai",
      "pankh": "BK", "target_forms": 0, "filled_forms": 0, "is_self": false }
  ]
}
```

Unlike the rest of the payload, `id`, `target_forms`, `filled_forms` and
`collected_amount` inside `members[]` are **real numbers**, not strings.

Store `token` and send it on every later request. Tokens last **30 days**.

`parivar`, `mandal` and `xetra` are **`null`** for sants and some senior
karyakars — they hold a post but belong to no mandal family. Guard for it:
`sevak.mandal?.name ?? "—"`.

### Everyone's first-login password is `annkut@2026`

All 1,140 accounts were seeded with it and `must_change_password: true`. If that
flag is set, send the user to a change-password screen before anything else.

### `POST login/me` → same `sevak` object. Use it to restore a session on reload.

### `POST login/change_password`

```json
{ "current_password": "annkut@2026", "new_password": "something-better" }
```

Minimum 6 characters. **On success every existing token for that user is
revoked, including the one that made the call** — send the user back to the login
screen afterwards.

### `POST login/forgot_password` — no token needed

For a user who cannot sign in at all. Proves identity with the Sevak ID plus the
mobile number on file.

```json
{ "sevak_id": "ASMA014", "phone_number": "7600259008", "password": "new-one" }
```

`404` if the pair does not match (deliberately the same message whether the
Sevak ID or the number is wrong, so this cannot be used to discover whose number
is whose). `422` if the new password is under 6 characters. On success every
existing token for that account is revoked.

> ⚠️ **This is a low bar, by design.** Sevak IDs appear in the source workbook
> and mobile numbers are widely known inside a mandal, so anyone holding both can
> take over an account. An OTP sent to that number would close the hole without
> changing this request shape.
>
> **`1234567890` is a placeholder, not a real number.** The 59 sevaks who had no
> mobile were given it so they are easy to spot and chase — which means **64
> accounts now share it** and can be reset by anyone who knows the Sevak ID.
> Treat it as "no number on file": prompt for a real one at first login, and do
> not show it as a contact number.

### `POST sevak/reset_password` — **ADMIN only**

For a sevak who cannot get in at all: no correct password, and a mobile that is
wrong or is the `1234567890` placeholder. An administrator sets a new one and
reads it back **once**, to pass on.

```json
{ "sevak_id": "ASMA010", "new_password": "temp12345" }
```

Omit `new_password` and the account goes back to `annkut@2026` with
`must_change_password: true`.

```json
{ "status": true,
  "message": "Password reset. Give this to the sevak - it will not be shown again.",
  "sevak_id": "ASMA010", "name": "Gohil Prayosa Thakorbhai",
  "password": "temp12345", "must_change_password": false }
```

`password` is the **only** time the value is readable — nothing is stored in
clear. All of that sevak's existing sessions are revoked, and the reset is
written to `activity_log` with the administrator's id, so there is a record of
who reset whose password and when.

`403` for anyone but an ADMIN — a Sanchalak cannot reset passwords even inside
his own mandal. `422` if a supplied password is under 6 characters.

### `POST login/logout` — revokes the current token.

---

## 4. The data model

```
Area (Xetra)          BH01, BH02, BH03  +  Yuva Pravrutti
  └─ Mandal           42 total; code is unique only WITHIN an area
       └─ Parivar     735; a FAMILY
            └─ Sevak  1,140; an INDIVIDUAL
```

Two ideas that are easy to conflate:

- **Parivar ID** (`NK001`) identifies a **family**. Several sevaks share one.
- **Sevak ID** (`ASNK001`) identifies **one person**. Unique across the whole org.

`NK001` → `ASNK001` (husband) + `ASNK002` (wife). Never show a Parivar ID where a
person is meant, and never assume one row = one person.

**Mandal codes repeat across xetras.** `BH01/NK` is Narayan Kunj; `BH02/NK` does
not exist. Always pass `mandal_id`, never a bare code.

**Pankh** values: `S` Sanyukt, `M` Mahila, `YK` Yuvak, `YT` Yuvati, `BL` Bal,
`BK` Balika. **19 sevaks have `pankh: null`** — 12 whose source value was
ambiguous (`Y`/`B`, deliberately not guessed) plus 7 sants and senior karyakars
who appear only on the leadership sheet and have no pankh at all. Render null
as "—".

### The Yuva Pravrutti filter

Every listing and totalling endpoint takes the same two optional keys:

| Key | Accepts | Meaning |
|---|---|---|
| `pravrutti` | `"YUVA"` | a grouping recorded on the pankh row — today Yuvak + Yuvati |
| `pankh` | `"M"` or `["YK","YT"]` | explicit codes; one or several |

Send `{"pravrutti": "YUVA"}` for the switch on the mandals screen. Prefer it
over `["YK","YT"]`: the grouping lives in the database, so if the Yuva Pravrutti
ever covers a different set of pankh the server follows and the app does not
need redeploying.

Both keys work on `seva/get_seva_count`, `sevak/get_sevak` and `seva/get_seva`,
and the filter is matched against **the sevak who collected** the receipt.

`get_seva_count` echoes `pankh_filter` — the codes the filter resolved to
(`["YK","YT"]`, or `[]` when unfiltered) — so the screen can label itself from
the server's answer instead of its own copy of the rule.

> **What the filter does to the target.** `filled_forms`, `collected_amount`,
> `sevaks` and `parivars` are simply narrowed. **`target_forms` changes source**:
> unfiltered it is the mandal's own target from `mandal_targets`; filtered it is
> the sum of the matching sevaks' individual targets from `sevak_targets`,
> because a mandal target holds one number with no breakdown by pankh. The two
> answer different questions and will not add up to each other — the filtered
> figure is the only honest answer to "what is the yuva target here".

---

## 5. Access model — what the frontend must and must not do

Each user holds scopes resolved server-side:

| Scope | Sees |
|---|---|
| `GLOBAL` | everything, all 42 mandals + Yuva Pravrutti |
| `AREA` | every mandal in that xetra |
| `MANDAL` | only the listed mandals |
| *(none)* | their own records **and their own parivar's** |

**Family access is separate from scope.** Every sevak can read, and record seva
for, any member of their own parivar — regardless of having no mandal scope at
all. That is what makes the home-screen tabs work: one household often shares a
single phone, so whoever logs in enters forms for everyone. It does **not**
extend to another family in the same mandal.

### Scope answers "which data". Permissions answer "which verbs".

They are independent, and a write needs **both**. A Sant Nirdeshak reaches a
whole xetra but holds no write permission; a Sanchalak may edit, but only inside
his own mandal.

Permissions come from **posts**, and arrive in the login payload:

```json
"access": {
  "global": false,
  "areas": [],
  "mandal_count": 1,
  "is_admin": false,
  "permissions": ["sevak.edit"]
}
```

| Post | Who | Permissions |
|---|---|---|
| `ADMIN` | 1 — the `ADMIN26` account | everything |
| `SANCHALAK` | 35 — one per mandal, `RK`-prefixed Sevak IDs | `sevak.edit`, `seva.create`, `book.assign` |
| `KOTHARI` `SANT_NIRDESHAK` `NIRDESHAK` `SAH_NIRDESHAK` `YUVA_NIRDESHAK` | 14 | **read-only** — the Sant and Agresar karyakar list |
| *(no post)* | 1,126 ordinary sevaks | read-only |

The eight permission codes:

| Code | Allows |
|---|---|
| `sevak.edit` | edit a sevak's name, mobile, pankh **and target** |
| `sevak.create` | add a sevak |
| `sevak.deactivate` | deactivate a sevak |
| `seva.create` | record a seva entry **for a mandal you look after** — writing in your *own family's* book needs no permission at all |
| `seva.manage` | edit or void a seva entry that already exists |
| `book.assign` | issue a book **that is in the mandal** to a parivar, and take it back |
| `book.manage` | add a book to a mandal, take one back, edit, submit, delete |
| `user.reset_password` | set another sevak's password when they are locked out |

### The full matrix — verified against the running API

| Action | ADMIN | SANCHALAK | Sant/Sah Nirdeshak | Ordinary sevak |
|---|---|---|---|---|
| Read own + family | ✅ | ✅ | ✅ | ✅ |
| Read a mandal | all | own mandal | within scope | ❌ |
| **Add** a seva in your **own family's** book | ✅ | ✅ | ✅ | ✅ |
| **Add** a seva in **someone else's** book (mandal-wide) | ✅ | ✅ own mandal | ❌ | ❌ |
| **Edit sevak** name / mobile / pankh / target | ✅ | ✅ own mandal | ❌ | ❌ |
| **Add / deactivate** a sevak | ✅ | ❌ | ❌ | ❌ |
| **Edit / void** a seva | ✅ | ❌ | ❌ | ❌ |
| **Issue a book** to a parivar | ✅ | ✅ own mandal | ❌ | ❌ |
| **Take a book back** from a parivar | ✅ | ✅ own mandal | ❌ | ❌ |
| **Submit** a book into the office | ✅ | ❌ | ❌ | ❌ |
| **Issue a SUBMITTED book** (anywhere) | ✅ | ❌ | ❌ | ❌ |
| **Add / delete** a book | ✅ | ❌ | ❌ | ❌ |
| **Reset another sevak's password** | ✅ | ❌ | ❌ | ❌ |

An ordinary sevak has exactly two write powers: **record a seva in their own
family's book** and **change their own password**.

> **Read scope is not write permission.** The Sant and Agresar karyakars hold
> the *widest* data scope in the organisation — the Kothari sees all 42 mandals —
> and the *narrowest* set of verbs: none. Seeing a mandal never implies writing
> in it. `add_seva` used to authorise on scope alone, which silently gave all 14
> of them write access across their xetras; `seva.create` is the verb that now
> separates the two. The selftest pins this down per account.

**Driving the UI:** show an edit control when
`access.is_admin || access.permissions.includes("sevak.edit")`, and only for
mandals the user can reach. Two distinct 403 messages come back so you can tell
the user the right thing:

- *"Only a Sanchalak or administrator can change sevak details."* — wrong role
- *"That is outside the mandals you look after."* — right role, wrong mandal

Real behaviour, same endpoint, four users:

| Endpoint | Kothari | Sant Nirdeshak | Sah Nirdeshak | Ordinary sevak |
|---|---|---|---|---|
| `get_mandal_list` | 42 | 14 | 6 | 0 |
| `get_sevak` total | 1140 | 464 | 275 | 0 |

**Use `sevak.access` for UI only** — to decide which menu items to draw. It is
not a security boundary. The server re-checks every request and returns **403**
if the caller asks for a mandal outside their scope, so a tampered request fails
regardless of what the UI shows. Equally: do not hide a feature the server would
allow, and do not assume hiding a button protects anything.

An ordinary sevak legitimately gets `0` mandals. That is not an error — render an
empty state, not a failure.

---

## 5b. The home screen, end to end

The screen the sevak lands on after login:

```
Annkut Sevak 2026  (MA004)          <- sevak.parivar.code, in brackets
Target 25   Filled 8

[ Prabhatsinh · ASMA009 ]  [ Prayosa · ASMA010 ]  [ Thakorbhai · ASMA012 ]  ...
       ^ self, selected by default        ^ dynamic: one tab per family member

  ...seva list for the SELECTED tab...                       [ + Add Seva ]
```

Wiring, with no extra calls beyond the ones listed:

1. **Tabs** come from `login.sevak.parivar.members[]` — already in localStorage.
   `is_self` marks the default. Tab count is however many the family has (1–7 in
   the current data).
2. **List for the selected tab:** `seva/get_seva { sevak_id: <selected> }`.
   Selecting your own tab shows your entries; selecting your father's shows his.
3. **Add Seva** posts `on_behalf_of: <selected sevak_code>`. Standing on your
   father's tab therefore credits the seva to your father — and when he logs in,
   he sees it under his own name.
4. **Book dropdown:** `receiptbooks/my_books {}` - the family's books. The same
   list whichever tab is selected, because the book belongs to the household.
   Pre-fill the receipt-number field from the chosen book's `next_receipt_no`.
5. **After a successful add:** `sevak/get_family` to refresh the tab counters,
   and re-fetch the list for the selected tab.

Because everyone in the family shares this view, a mother logging in sees every
member's entries — including ones her son recorded on her behalf.

---

## 6. Endpoints

### Sevaks & organisation

#### `POST sevak/get_sevak` — list sevaks in scope

Request (all fields optional):

```json
{ "mandal_id": 7, "area_id": 1, "parivar_id": 2, "pankh": "S",
  "pravrutti": "YUVA",
  "q": "patel", "year": 2026, "limit": 50, "offset": 0 }
```

`q` matches name, sevak code or mobile. `limit` is capped at 500.

`pankh` also accepts a list (`["YK","YT"]`), and `pravrutti` takes a grouping —
see **The Yuva Pravrutti filter** above. `total` is the count **after** filtering,
so it stays correct for paging.

```json
{
  "status": true,
  "total": 275,
  "sevak": [{
    "id": "359", "sevak_code": "ASMT001",
    "surname": "Patel", "first_name": "Ramkrushnabhai", "middle_name": "Bhikhabhai",
    "full_name": "Patel Ramkrushnabhai Bhikhabhai",
    "mobile": "9898051829", "active": "1",
    "pankh": "S", "pankh_label": "Sanyukt",
    "parivar_id": "237", "parivar_code": "MT001",
    "mandal_id": "12", "mandal_code": "MT", "mandal_name": "MaitriNagar",
    "area_id": "1", "area_code": "BH01", "area_name": "Bharuch - 1",
    "target_forms": "0", "filled_forms": "0", "collected_amount": "0.00",
    "has_login": "1"
  }]
}
```

`total` is the count before `limit` — use it for pagination.
Passing a `mandal_id` outside your scope returns **403**, not an empty list.

#### `POST sevak/get_sevak_by_id`
`{ "sevak_id": "ASNK001" }` — accepts the code **or** the numeric id.
Returns `{ status, sevak: {...} }` including `target_forms`.

#### `POST sevak/get_parivar` — a family and everyone in it

`{ "parivar_id": 1 }`

```json
{ "status": true, "parivar": {
    "id": "1", "code": "NK001",
    "mandal_id": "7", "mandal_code": "NK", "mandal_name": "Narayan Kunj",
    "area_id": "1", "area_code": "BH01", "area_name": "Bharuch - 1",
    "members": [
      { "id": "1", "sevak_code": "ASNK001", "full_name": "PATEL GHANSHYAMBHAI BHAVANBHAI",
        "surname": "PATEL", "first_name": "GHANSHYAMBHAI", "middle_name": "BHAVANBHAI",
        "mobile": "9898509128", "pankh": "S", "pankh_label": "Sanyukt" },
      { "id": "2", "sevak_code": "ASNK002", "full_name": "PATEL SAPANABEN GHANSHYAMBHAI",
        "pankh": "M", "pankh_label": "Mahila" }
    ]
} }
```

This is the endpoint the old UI had no equivalent for. Family sizes run 1–7.

#### `POST sevak/get_family` — refresh the home-screen tabs

Same payload as `login.sevak.parivar`, on its own endpoint. Call it after adding
a seva to update the tab counters without making the user sign in again.

```json
{ "status": true,
  "parivar": { "id": 432, "code": "MA004" },
  "members": [ /* same shape as login, is_self first */ ] }
```

Returns `parivar: null` and an empty list for sants, who belong to no family.

#### `POST sevak/get_parivar_list`
`{ "mandal_id": 7 }` (optional) → `{ status, parivar: [{ id, code, mandal_id, mandal_code, mandal_name, members }] }`

#### `POST sevak/get_mandal_list` — mandals with progress

`{ "area_id": 1, "year": 2026 }` (both optional)

```json
{ "status": true,
  "mandal_array": [{
    "id": "12", "code": "MT", "name": "MaitriNagar", "active": "1",
    "area_id": "1", "area_code": "BH01", "area_name": "Bharuch - 1",
    "target_forms": "0", "target_amount": "0.00",
    "filled_forms": "0", "collected_amount": "0.00",
    "parivars": "24", "sevaks": "40"
  }],
  "target": { "total_target": 0, "total_filled_form": 0 }
}
```

#### `POST sevak/get_area_list`
```json
{ "status": true, "area": [{ "id": "1", "code": "BH01", "name": "Bharuch - 1",
                             "type": "XETRA", "mandals": "6" }] }
```
`type` is `XETRA` or `YUVA_PRAVRUTTI`. `mandals` counts only mandals you can see.

#### Writes

> ⚠️ **All four are karyakar-only.** An ordinary sevak gets **403** — *"Only a
> karyakar can change sevak records. Please ask your Sanchalak."* — even for
> their own record. That includes setting a target: a sevak must not be able to
> raise their own. Family access covers **reading** relatives, never editing them.

| Endpoint | Body | Notes |
|---|---|---|
| `sevak/add_sevak` | `{ mandal_id, parivar_code, sevak_code, surname, first_name, middle_name, mobile, pankh, target_forms? }` | 201. Creates the parivar if that code is new to the mandal. 409 if the sevak code exists. |
| `sevak/edit_sevak` | `{ sevak_id, surname?, first_name?, middle_name?, mobile?, pankh?, target_forms? }` | |
| `sevak/set_target` | `{ sevak_id, target_forms, year? }` | Also moves the mandal's target by the same delta. |
| `sevak/delete_sevak` | `{ sevak_id }` | Deactivates, never deletes — receipts reference the collector. 422 on your own account. |

### Seva entries (receipts)

#### `POST seva/add_seva`

Exactly the fields the form collects — nothing else is required:

```json
{ "book_id": 1,
  "receipt_no": 1,
  "on_behalf_of": "ASMA012",
  "sahyogi_surname": "PATEL",
  "sahyogi_first_name": "RAMESH",
  "sahyogi_middle_name": "KANTIBHAI",
  "sahyogi_number": "9812345670",
  "seva_amount": 500 }
```

**`on_behalf_of`** is the family tab currently selected — omit it and the seva is
credited to the caller. Accepts a Sevak ID or numeric id. Allowed for **your own
parivar**, or any sevak in a mandal you oversee; anything else is **403**.

The **name is captured in three fields** rather than one, because sevaks never
agree on the order when given a single box. All three are optional individually.

**Not required, and safe to omit** — the server defaults them:

| Field | Default |
|---|---|
| `prasad_type` | `annkut_sevak` |
| `payment_method` | `cash` |
| `notes` | null |
| `year` | current year |

The mandal is taken from the book — never send `mandal_id`.

→ `201 { status, message, seva_id, filled_form }`

Errors: **409** that receipt number is already used in the book, **422** the
number is outside the book's range, **403** the book is not yours and not the
selected member's, **404** no such book or sevak.

For the **amount**, the UI offers 500 / 1000 / Other; `seva_amount` is just a
number, so "Other" sends whatever was typed.

#### `POST seva/get_seva`

`{ mandal_id?, sevak_id?, book_id?, year?, prasad_type?, q?, limit?, offset? }`
With no filters a user with no scope gets their own entries.

→ `{ status, seva: [...], total, achieved_target }`

Each row: `id, year, receipt_no, seva_amount, prasad_type, payment_method,
status, notes, collected_at, sahyogi_name, sahyogi_number, book_id, book_no,
start_no, end_no, mandal_id, mandal_code, mandal_name, area_code,
collected_by_id, collected_by_code, collected_by_name`.

#### `POST seva/get_seva_by_id` · `{ "seva_id": 1 }`

#### `POST seva/edit_seva` and `POST seva/delete_seva` — **karyakar only**

> ⚠️ **Ordinary sevaks cannot edit or void a seva entry — not even one they
> recorded themselves.** Once a paper receipt is written the record has to match
> it, so corrections go through a karyakar. Both endpoints return **403** with
> *"Only a karyakar can change or remove a seva entry. Please ask your
> Sanchalak."* for anyone whose access scope does not cover that mandal.
>
> **Hide the edit and delete buttons unless `sevak.access.global` is true or
> `sevak.access.mandal_count > 0`.** Reading stays open to the whole family.

`edit_seva` · `{ seva_id, seva_amount?, sahyogi_surname?, sahyogi_first_name?,
sahyogi_middle_name?, sahyogi_number?, receipt_no?, prasad_type?, payment_method?, notes? }`
A receipt can move to another number **in the same book**, never to a different book.

`delete_seva` · `{ seva_id }` — soft void; the receipt number stays used.

#### `POST seva/get_seva_count` — dashboard totals

`{ "year": 2026, "mandal_id": 7, "area_id": 1 }` (all optional)

```json
{ "status": true, "year": 2026,
  "seva_five_hundered": 0, "seva_thousand": 0, "seva_other": 0,
  "sahyogi_prasad": 0, "sevak_prasad": 0,
  "total_target": 0, "total_filled_form": 0, "collected_amount": 0,
  "mandal_array": [ /* same shape as get_mandal_list */ ] }
```

#### `GET seva/export_data?year=2026&mandal_id=7`
Streams a tab-separated `.xls`. **This is the one GET**, and the token still goes
in the `Authorization` header — so trigger it with `fetch` + `blob`, not a plain
`<a href>` (a link cannot carry the header).

### Receipt books

Books are addressed by **`book_id`** (primary key). The number printed on the
cover (`book_no`) repeats across mandals and is display-only.

**A Sanchalak circulates books inside his own mandal** - hand out, take back.
The moment a book is submitted it is with the office, and only an administrator
places it again, in any mandal.

**Book numbers are unique org-wide.** `create` and `update` both refuse a number
that another mandal holds, with a message naming that mandal. Deleted books
release their number.

**A book is held by a PARIVAR, not by an individual.** It stays in the house and
whoever is around writes in it. Which member the seva counts for is decided by
the selected tab (`on_behalf_of`), never by whose book it is.

| Endpoint | Body | Notes |
|---|---|---|
| `receiptbooks/list` | `{ mandal_id? }` | Defaults to your own mandal. **400** if you have none (sants) — pass `mandal_id`. Returns `all_books`. |
> **Who may do what.** Stock control is the administrator's: books enter a mandal
> and come back from a parivar only on his say-so. A mandal **Sanchalak** can do
> exactly one thing — hand a book his mandal already holds to one of its parivars.
>
> | | ADMIN | SANCHALAK | everyone else |
> |---|---|---|---|
> | `create` add a book to a mandal | ✅ | ❌ | ❌ |
> | `assign` a book **in the mandal** | ✅ | ✅ own mandal | ❌ |
> | `assign` a book **already SUBMITTED** | ✅ anywhere | ❌ | ❌ |
> | `deassign` take it back from a family | ✅ | ✅ own mandal | ❌ |
> | `submit` take it into the office | ✅ | ❌ | ❌ |
> | `transfer` / `update` / `delete` | ✅ | ❌ | ❌ |
> | `list` / `my_books` | ✅ | ✅ own mandal | own parivar |
> | `lookup` find a book by its cover number | ✅ anywhere | ✅ own mandal | ❌ |

| `receiptbooks/my_books` | `{}` | Books held by **your parivar** — same list for every member and every tab, no `sevak_id` needed. Returns `{ parivar_id, books, full_books }`. **`books` contains only writable books**; anything with no receipts left moves to `full_books`. Build the Add Seva dropdown from `books` alone. |
| `receiptbooks/lookup` | `{ book_no }` | **Search a book by the number printed on its cover** and get everything written in it — this backs the admin's book screen. Returns `{ book, receipts, summary, can_edit }`. `receipts` is in **page order** (receipt_no ascending), not date order. `summary` is `{ pages, filled, remaining, collected_amount, unused_numbers }`, where `unused_numbers` are pages below the high-water mark with nothing recorded — torn out, spoiled or voided. **404** if no book carries that number; **403** if it belongs to a mandal outside your scope — the two are kept distinct so the office can tell *wrong number* from *not yours*. Works on `CLOSED` books: a finished book still has to be readable. |
| `receiptbooks/create` | `{ mandal_id, book_no, pages }` | **ADMIN only.** `pages` is the printed size: **25 or 10** — the receipt range is derived from it, so a 10-page book can never be given a 25-receipt range. Omit it and you get 25. **422** for any other value. **Book numbers are unique across the whole organisation**, not per mandal - the number is printed on the cover. **409** naming the holder: *"Book number 2 is already assigned to Akshardham mandal."* A soft-deleted number is free to reissue. |
| `receiptbooks/assign` | `{ book_id, parivar_id }` or `{ book_id, sevak_id }` | Issues the book to a **family**. Naming any member hands it to their whole household. **ADMIN + SANCHALAK while the book is in the mandal; ADMIN only once it is `SUBMITTED`** — a submitted book is with the office, so a Sanchalak gets 403: *"This book is with the office. Only an administrator can issue it."* 409 if the parivar is in another mandal or the book is exhausted. Returns `next_receipt_no`. |
| `receiptbooks/deassign` | `{ book_id, last_used_no? }` | **ADMIN + SANCHALAK.** Frees the book for the next family. `last_used_no` is preserved, so the next parivar continues from the next unused receipt rather than restarting. 422 if it is below what is already recorded. |
| `receiptbooks/update` | `{ book_id, book_no?, start_no?, end_no? }` | 422 if the range would exclude a used receipt. |
| `receiptbooks/submit` | `{ book_id }` | **ADMIN only.** Takes the book into the office and out of the mandal's hands. Receipts left → `SUBMITTED`, free for any mandal. None left → `CLOSED`, locked permanently. Returns `{ status, remaining }`. |
| `receiptbooks/pool` | `{}` | Books with no family holding them and receipts still left, across every mandal in scope — the redistribution list. |
| `receiptbooks/transfer` | `{ book_id, mandal_id }` | **ADMIN only.** Moves a part-used book to another mandal. 409 if it is CLOSED, still with a parivar, or already in that mandal. |
| `receiptbooks/delete` | `{ book_id }` | 409 if issued or if it holds receipts. |

Every book row carries computed `last_used_no`, **`next_receipt_no`** and
`remaining`. Prefill the receipt-number field with `next_receipt_no`.

---

## 7. Migrating the existing frontend

### Do this first

1. Point `BACKEND_ENDPOINT` at the new base URL.
2. Store the `token` from login; attach `Authorization: Bearer <token>` to every request.
3. **Delete every `sevak_id` that was being sent to prove identity.** Keep it only where you genuinely mean "about this other person".
4. Add a 401 interceptor → clear the token, redirect to login.
5. Add a 403 path → show "you don't have access", do **not** log the user out.
6. Coerce numeric strings with `Number()` before arithmetic.

### Field renames

| Old | New |
|---|---|
| `sevak.role` (string) | `sevak.posts[]` — `[{code, name, rank}]`, may be empty |
| `sevak.permission_codes[]` | `sevak.access` — `{global, areas[], mandal_count}` |
| `sevak.mandal` (name string) | `sevak.mandal` — `{id, code, name}`, may be `null` |
| — | `sevak.xetra` — `{id, code, name}`, may be `null` |
| — | `sevak.parivar` — `{id, code}`, may be `null` |
| `sevak.sevak_target` | `target_forms` |
| `sevak.filled_form` | `filled_forms` |
| `book_no` used as an identifier | `book_id`; `book_no` is display-only |
| `seva.book_no` | `seva.book_id` + `seva.book_no` |
| `id` (receipt) | `seva_id` in requests, `id` in responses |

### Gone

- `sevak/assign_mandal` — was a no-op.
- `import_karyakar/annkut` — was unreachable (class/filename mismatch).
- `login/forgot_password` — replaced by `login/change_password`, which requires
  the current password. A self-service reset needs an OTP flow; ask before building one.
- `previous_target` — no prior-year data exists in the new database.

### New, worth using

`sevak/get_parivar`, `sevak/get_parivar_list`, `sevak/get_area_list`,
`sevak/set_target`, `receiptbooks/create`, `login/me`, `login/change_password`,
`login/logout`.

---

## 8. CORS

Allowed origins are `http://localhost:3000`, `http://localhost:5173` and the
Amplify production URL. Any other origin gets **no** CORS header and the browser
blocks it. Run the dev server on 3000 or 5173, or ask for your origin to be added
— it is one line in `application/core/MY_Controller.php`.

---

## 9. Test accounts

All use password `annkut@2026`.

| Sevak ID | Who | Sees |
|---|---|---|
| `SNBH001` | Pu Anirdesh Swami, Kothari | everything (42 mandals) |
| `SNBH002` | Pu Aksharnath Swami, Sant Nirdeshak | all of BH02 (14) |
| `AGSP001` | Ajaykumar M Bhatt, Nirdeshak | BH01 + BH02 + BH03 |
| `AGSP004` | Pinakinbhai B Patel, Sah Nirdeshak | 6 mandals in BH01 |
| `AGYP011` | Dhaval A Bhatt, Yuva Nirdeshak | Yuva Pravrutti |
| `ASNK001` | an ordinary sevak | only themselves |

Use `AGSP004` for permission work: they should see NK, SB, SJ, RK, NN, MT and get
403 for anything else.

---

## 10. Known gaps — ask before designing around these

- **No password reset without the old password.** Needs an OTP/SMS decision.
- **19 sevaks have `pankh: null`** (12 ambiguous + 7 leadership-only). Source values `Y` and `B` were ambiguous
  (`Y` could be YK or YT) and were not guessed.
- **6 mandals have no members** — MN, SK, NN (BH01), AG, RS, MU (BH03) — but
  leadership access still references them. Expect empty lists, not errors.
- **`AGSP011` "TBC Placeholder"** is a stand-in for an unnamed karyakar in the
  source workbook who supervises BH03 UM/KR/KJ/OS.
- **Receipts and books start empty.** The 2025 data was deliberately not carried
  over; the first real seva of the season has not been recorded yet.
