| name | add-address-autocomplete |
| description | Add an address autocomplete field to a form that pins lat/lng to DB. Use when a form has a new address field that needs to be geocoded / shown on the map (e.g. branch office, warehouse, project site). Wires up the AddressAutocompleteField component + snake_case lat/lng columns in the mapper + migration if needed. |
Skill: add-address-autocomplete
Thêm 1 field địa chỉ có autocomplete + lưu tọa độ vào DB, tương thích MapComponent (OSM/Photon).
Khi nào dùng
- Form có field "địa chỉ" mà muốn đưa lên map (companies, branches, offices, projects, events, etc.)
- Cần toạ độ chính xác thay vì text tự do
- Đã có
AddressAutocompleteField + pattern trong [[../../docs/Architecture/Geocoding]]
Bắt buộc tham khảo
src/components/shared/forms/AddressAutocompleteField.tsx — component có sẵn, KHÔNG viết lại
src/components/shared/dialogs/EditInformationDialog.tsx — ví dụ tích hợp 2 field
supabase/migrations/20260424180000_company_geo_coords.sql — ví dụ migration columns
src/lib/companyMapper.ts — ví dụ mapper strip-undefined với *_lat / *_lng
- [[../../docs/Architecture/Field Naming Convention]] — ⚠️ strip-undefined pattern
Checklist
1. DB migration — thêm 2 cột
File: supabase/migrations/YYYYMMDDHHMMSS_<name>_geo_coords.sql
alter table public.<table>
add column if not exists <field>_lat double precision,
add column if not exists <field>_lng double precision;
create index if not exists <table>_<field>_latlng_idx
on public.<table> (<field>_lat, <field>_lng);
Push: npm run sb:push → npm run sb:types.
2. App type
File: src/shared/types.ts (hoặc feature type file)
export interface FooType {
<field>Lat?: number | null;
<field>Lng?: number | null;
}
⚠️ Giữ naming convention — camelCase trong app, snake_case trong DB. KHÔNG alias. Xem [[../../docs/Architecture/Field Naming Convention]].
3. Mapper — cả 2 chiều
File: src/lib/<table>Mapper.ts (hoặc trong service)
export function toFoo(row: FooRow): FooType {
return {
<field>Lat: row.<field>_lat ?? null,
<field>Lng: row.<field>_lng ?? null,
};
}
export function toFooRow(app: Partial<FooType>): Partial<FooInsert> {
const row: Record<string, unknown> = {
<field>_lat: app.<field>Lat,
<field>_lng: app.<field>Lng,
};
for (const k of Object.keys(row)) if (row[k] === undefined) delete row[k];
return row as Partial<FooInsert>;
}
4. Form — dùng AddressAutocompleteField
import { AddressAutocompleteField, type AddressCoords } from '@/components/shared/forms/AddressAutocompleteField';
<AddressAutocompleteField
label="Địa chỉ"
value={formData.<field> || ''}
coords={
formData.<field>Lat != null && formData.<field>Lng != null
? { lat: formData.<field>Lat, lng: formData.<field>Lng }
: null
}
onChange={(text, coords: AddressCoords | null) =>
setFormData((prev) => ({
...prev,
<field>: text,
<field>Lat: coords?.lat ?? null,
<field>Lng: coords?.lng ?? null,
}))
}
placeholder="Nhập địa chỉ, chọn gợi ý để ghim toạ độ"
/>
⚠️ KHÔNG dùng handleChange('<field>', value) helper — vì nó chỉ update 1 field và sẽ mất *Lat/*Lng. Phải update 3 field cùng lúc.
5. (Optional) Hiển thị trên map
Nếu muốn đưa entity mới lên MapComponent:
- Update
src/features/network/services/companyLocationsService.ts (hoặc tạo service mới tương tự)
- Select thêm
<field>_lat, <field>_lng từ DB
- Prefer stored coords, fallback geocode nếu
null
Rules
- Không tự viết autocomplete — đã có
AddressAutocompleteField reusable
- Luôn có cả lat + lng khi có coords (nullable chung)
- Khi user gõ tự do (không chọn gợi ý): coords = null — map sẽ fallback geocode runtime
- Khi user clear text: cũng set coords = null
- Strip-undefined trong mapper để tránh silent wipe — xem [[../../docs/Architecture/Field Naming Convention]]
Chạy sau khi xong
npm run sb:push
npm run sb:types
npm run lint
Related skills
- [[new-migration]] — cho bước DB migration
- [[audit-field-mapping]] — verify 3-field cặp lat/lng/text map đúng 3 tầng (form ↔ mapper ↔ DB)
Trường hợp đặc biệt — signup form (auth.users.raw_user_meta_data)
Khi field địa chỉ ở signup form (chưa có user_id, chưa có row trong company_profiles), không update được trực tiếp như form bình thường. Pattern ở RegisterView (2026-05-05):
- Form lưu
cityLat/cityLng vào local state formData
registerMutation (use-auth.tsx) đẩy 2 field này vào metadata của supabase.auth.signUp({ data: metadata })
- Trigger
handle_new_user (v5+) đọc raw_user_meta_data->>'cityLat'/'cityLng', cast double precision, set vào columns:
- Path A (có
taxId): forward qua claim_company_profile RPC metadata jsonb keys location_lat/location_lng → RPC update profile
- Path B (không taxId): INSERT thẳng
location_lat/lng
→ Khi thêm field địa chỉ mới ở signup, phải update cả 3 chỗ (form + use-auth metadata + trigger DB function). Migration trigger cần CREATE OR REPLACE FUNCTION (non-destructive) + notify pgrst, 'reload schema'; cuối cùng để PostgREST cache fresh.
Reference: supabase/migrations/20260505030000_signup_save_location_coords.sql + src/features/auth/RegisterView.tsx Step 1 city field.