# Region Picker — Replication Guide for Another Project

## Goal

Add a **multi-level administrative region picker** (e.g. Province → City → District → Postal Code) to a Laravel project, using a tabbed modal driven by jQuery. The hierarchy is **configurable** — supports any country's administrative divisions with optional postal code lookup.

---

## How It Works (Abstract)

- **Levels** are defined in a single `regions` table with a `type` column (e.g. `province`, `city`, `district`, `village`).
- A `region_postal_codes` table is **optional** — include it only if you need postal code selection.
- The JavaScript singleton accepts **level labels** as an option so tabs adapt to any hierarchy.
- Code hierarchy uses dot-notation (e.g. `"11"`, `"11.01"`, `"11.01.01"`) but the level is determined by `type`, not code length.

---

## Files to Create (in order)

### 1. Database — Migrations & Seed Data

**`database/migrations/YYYY_MM_DD_create_regions_table.php`**
- Columns: `code` (string, unique), `name` (string), `type` (string, e.g. `province`/`city`/`district`/`village`)
- Add an index on `type` for fast filtering.
- Disable timestamps.
- Hierarchy is encoded in dot-notation `code` (e.g. parent of `"11.01"` is `"11"`).

**`database/migrations/YYYY_MM_DD_create_region_postal_codes_table.php`** *(optional — skip if not needed)*
- Columns: `code` (string, indexed), `postal_code` (string)

**`database/data/regions.csv`** — columns: `code,name,type`
**`database/data/region_postal_codes.csv`** *(optional)* — columns: `code,postal_code`

**`database/seeders/RegionSeeder.php`** — reads CSV, upserts in chunks
**`database/seeders/RegionPostalCodeSeeder.php`** *(optional)* — reads CSV, upserts in chunks
**`database/seeders/DatabaseSeeder.php`** — call the seeders you include

### 2. Model

**`app/Models/Region.php`**
- `$fillable = ['code', 'name', 'type']`, `$timestamps = false`
- Optionally: `scopeOfType($type)`, `scopeChildrenOf($code)`
- If using postal codes: `hasMany(RegionPostalCode::class, 'code', 'code')`

**`app/Models/RegionPostalCode.php`** *(optional)*
- `$fillable = ['code', 'postal_code']`, `$timestamps = false`
- `belongsTo(Region::class, 'code', 'code')`

### 3. Controller & Routes

**`app/Http/Controllers/RegionController.php`**
Public JSON endpoints (number of level endpoints = your hierarchy depth + optional postal):

| Method | URI | Query |
|--------|-----|-------|
| `provinces()` | `GET /api/regions/provinces` | `WHERE type = 'province'` |
| `cities($code)` | `GET /api/regions/cities/{code}` | `WHERE type = 'city' AND code LIKE '$code.%'` |
| `districts($code)` | `GET /api/regions/districts/{code}` | `WHERE type = 'district' AND code LIKE '$code.%'` |
| `postalCodes($code)` | `GET /api/regions/postal-codes/{code}` *(optional)* | `FROM region_postal_codes WHERE code LIKE '$code.%'` |

**Adapt the endpoint names to your hierarchy.** For a different country, rename them accordingly (e.g. `states`, `counties`, `wards`). The JS will map these via its config.

**`routes/api.php`** — routes for each endpoint, no auth middleware.

### 4. Frontend Assets

**`public/js/region-picker.js`** — Singleton IIFE `var RegionPicker = (function($) { ... })(jQuery);`
- Closures: `state` (codes), `names` (labels), `onConfirmCallback`
- Methods: `init(options)`, `open()`, `close()`, `reset()`, `getValues()`
- **Configurable `init()` options:**
  - `trigger` — CSS selector for the clickable input
  - `levels` — array of level configs, e.g.:
    ```javascript
    levels: [
      { key: 'province', label: 'Province', endpoint: '/api/regions/provinces' },
      { key: 'city',     label: 'City',     endpoint: '/api/regions/cities' },
      { key: 'district', label: 'District', endpoint: '/api/regions/districts' },
      { key: 'postal',   label: 'Postal Code', endpoint: '/api/regions/postal-codes' }
    ]
    ```
  - `onConfirm` — callback with selection data
  - `includePostal` — boolean (default: true), hides the last tab if false
- Dynamic tab generation from `levels` array — no hardcoded tab names.
- `onConfirm` fires with `{ province: { code, name }, city: { code, name }, district: { code, name }, postalCode: '12345', display: '...' }`

**`public/css/region-picker.css`** — Modal overlay, 640px modal, tab bar with active underline, list item hover, footer buttons.

### 5. Blade Modal Template

**`resources/views/layouts/region-picker.blade.php`**
- JS-generated tabs and panes (driven by `levels` config on init).
- Container: a static `<div id="region-picker-modal">` shell that JS populates.
- Footer: Cancel + Confirm buttons.
- Include this modal in the master layout.

### 6. Consumer Integration Pattern

```blade
<div class="form-floating" id="region-selector-wrapper" style="cursor: pointer;">
    <input type="text" class="form-control" id="region_display" readonly style="caret-color: transparent;">
    <label>Province, City, District</label>
</div>

<input type="hidden" name="province" id="input_province">
<input type="hidden" name="city" id="input_city">
<input type="hidden" name="district" id="input_district">
{{-- Only if postal: --}}
<input type="hidden" name="postal_code" id="input_postal_code">
```

```javascript
RegionPicker.init({
    trigger: '#region-selector-wrapper, #region_display',
    levels: [
        { key: 'province', label: 'Province', endpoint: '/api/regions/provinces' },
        { key: 'city',     label: 'City',     endpoint: '/api/regions/cities' },
        { key: 'district', label: 'District', endpoint: '/api/regions/districts' }
    ],
    includePostal: false, // omit postal code tab
    onConfirm: function(data) {
        $('#input_province').val(data.province.code);
        $('#input_city').val(data.city.code);
        $('#input_district').val(data.district.code);
        $('#region_display').val(data.display);
    }
});
```

For multiple addresses on one page, use separate trigger IDs and `RegionPicker.reset()` before re-initializing.

---

## Configuration Examples

### Indonesia (4 levels + postal)
```javascript
levels: [
    { key: 'province', label: 'Provinsi',  endpoint: '/api/regions/provinces' },
    { key: 'city',     label: 'Kota',      endpoint: '/api/regions/cities' },
    { key: 'district', label: 'Kecamatan', endpoint: '/api/regions/districts' }
],
includePostal: true
```

### USA (3 levels)
```javascript
levels: [
    { key: 'state',   label: 'State',    endpoint: '/api/regions/states' },
    { key: 'county',  label: 'County',   endpoint: '/api/regions/counties' },
    { key: 'city',    label: 'City',     endpoint: '/api/regions/cities' }
],
includePostal: false
```

### Philippines (4 levels)
```javascript
levels: [
    { key: 'region',    label: 'Region',    endpoint: '/api/regions/regions' },
    { key: 'province',  label: 'Province',  endpoint: '/api/regions/provinces' },
    { key: 'city',      label: 'City/Mun.', endpoint: '/api/regions/cities' },
    { key: 'barangay',  label: 'Barangay',  endpoint: '/api/regions/barangays' }
],
includePostal: false
```

---

## Dependencies

- jQuery (any version 1.x+)
- Laravel with any SQL database (use parameterised LIKE, not LENGTH tricks)
- CSV data files for your target country

---

## Key Design Decisions

- **`type` column over code-length tricks**: Level is explicit, not inferred. Works with any country's hierarchy without guessing.
- **Configurable `levels` array**: The JS generates tabs dynamically — no hardcoded "Provinsi/Kota/Kecamatan".
- **Optional postal**: Toggle `includePostal`; if false the controller doesn't need the postal endpoint.
- **Public API routes**: No auth on region endpoints.
- **Singleton JS**: One modal instance; re-init with different callback for multiple pickers on a page.
- **read-only input trigger**: The visible field is uneditable — click opens modal, callback fills hidden fields.

---

## Provided Assets (source reference)

Copy these from the source project and adapt:
- `database/data/regions.csv` — add a `type` column
- `database/data/region_postal_codes.csv` — optional
- `public/js/region-picker.js` — refactor to accept configurable `levels`
- `public/css/region-picker.css`
- `resources/views/layouts/region-picker.blade.php` — make the shell dynamic
- `app/Models/Region.php` — add `type`
- `app/Models/RegionPostalCode.php` — optional
- `app/Http/Controllers/RegionController.php` — use `WHERE type = ?`
