Location Tab

Developer guide for the Location panel shared across Assembly, Component, and Kit detail pages. Left sidebar with location history cards, right panel with Google Map.

1. Screenshot

Location Tab (web)

Live screenshot from dev environment (Location tab with sidebar + map)

2. Component Tree

Assembly/Component/Kit [id].tsx         pages/{assemblies,components,kits}/[id].tsx
 |- PageHeader
 |- AssetDetailTabs
 |   '- tab navigation (Info | Specs | ... | Location)
 '- Location                            components/location/index.tsx
     props: asset, assetType, setAsset, permissions, fetchAsset
     |- Loading state (no data yet)
     |   '- Card with spinner
     |- EmptyState                       components/empty-state/index.tsx
     |   '- Map pin icon + "No location assigned" + Add button
     '- Main layout (has locations)
         |- LEFT: Sidebar card (lg:w-80)
         |   |- Card header: "Location" + "Add Location" button
         |   |- Loading spinner (refetch)
         |   '- Location cards (scrollable)
         |       |- Location name + Current/Previous badge
         |       |- Street address
         |       |- City, postal code, country
         |       |- Assigned date
         |       '- Remove link
         |- RIGHT: Google Map (flex-1)
         |   '- GoogleMap + MarkerF       @react-google-maps/api
         '- LocationSelector (portal)    components/location-selector/index.tsx
             |- Search input + location list + map preview
             '- LocationModal (create-only)  components/location-modal/index.tsx
                 '- Name + Google Places + map + create
         '- CascadeAssignModal           Checkbox list of child assets
         '- CascadeUnassignModal         Checkbox list of children sharing location
         '- ConfirmModal (unassign)      components/confirm-modal/index.tsx

3. Design Tokens

ElementTailwind Classes
Outer layoutflex flex-col gap-4 lg:flex-row
Height: style="height: calc(100vh - 200px)"
Sidebar cardflex w-full flex-shrink-0 flex-col overflow-hidden rounded-lg border border-gray-200 bg-white lg:w-80
Sidebar headerflex items-center justify-between border-b border-gray-100 px-4 py-4
Sidebar titletext-lg font-bold text-gray-900
Add Location buttonbg-ag_red hover:bg-ag_red/90 rounded-lg px-3 py-1.5 text-sm font-medium text-white transition-colors
Scrollable listflex-1 overflow-y-auto p-3 with space-y-2 for card spacing
Location cardcursor-pointer rounded-lg border p-4 transition
Default: border-gray-200 hover:border-gray-300 hover:shadow-sm
Location card (selected)border-ag_red/30 bg-red-50/30
Location nametext-sm font-medium
Current: text-gray-900   Previous: text-gray-500
Current badgerounded-full px-2 py-0.5 text-[9px] font-medium bg-green-50 text-green-700
Previous badgerounded-full px-2 py-0.5 text-[9px] font-medium bg-gray-100 text-gray-500
Street linemt-0.5 text-xs text-gray-500
City / country linetext-xs text-gray-400
Assigned datemt-1 text-[10px] text-gray-300
Remove linkmt-2 text-[10px] text-ag_red hover:underline
Map containerflex-1 overflow-hidden rounded-lg border border-gray-200
Loading spinnerh-6 w-6 animate-spin rounded-full border-2 border-gray-200 border-t-ag_red
Sidebar refetch uses h-5 w-5 variant
Loading card wrapperoverflow-hidden rounded-lg border border-gray-200 bg-white with centered spinner at py-16
Portal overlayfixed inset-0 z-50 flex items-center justify-center bg-black/50
Modal dialogw-full max-w-md rounded-xl bg-white p-6 shadow-xl
Cascade modalw-full max-w-lg rounded-xl bg-white p-6 shadow-xl
Disabled child rowopacity-50 cursor-not-allowed (no ManageXxxLocation permission)

4. Data Model

Location data is fetched from the V2 API via LocationV2Service.getAssetLocation(assetId). Returns an array of AssetLocationAssignment objects.

PropertyTypeDescription
DW_TransactionIDnumberUnique assignment identifier (used as key and for selection)
IsCurrentbooleanWhether this is the currently active location assignment
AssignedDatenumber (epoch)Unix timestamp (seconds) -- displayed via toLocaleDateString()
location.NamestringLocation display name
location.StreetNumberstringStreet number
location.StreetNamestringStreet name (fallback: Address)
location.LocalitystringCity / locality
location.PostalCodestringPostal / ZIP code
location.CountrystringCountry name
location.LatitudenumberMap center latitude
location.LongitudenumberMap center longitude
location.SKLocationnumberLocation surrogate key (used for unassign)

Address line is built as: [StreetNumber, StreetName || Address].filter(Boolean).join(' '). City line: [PostalCode, Locality].filter(Boolean).join(' ') + Country.

5. Permissions

ActionPermission CheckBehaviour
View locations--Always visible when Location tab is shown
Add LocationcanManageLocation
Resolved from permissions.{assetType}
Button hidden without permission. Opens LocationSelector modal.
Remove locationcanManageLocation"Remove" link hidden per card without permission
Cascade assigncanManageLocation (parent)
ManageXxxLocation (per child type)
User is prompted with a checkbox list of children. Each child requires its own ManageXxxLocation permission. Children without permission are shown disabled/grayed in the list.
Cascade unassigncanManageLocation (parent)
ManageXxxLocation (per child type)
Same pattern as cascade assign. Only shows children that currently share the same location. Children without permission shown disabled.

Permission is resolved per asset type: permissions.kit, permissions.assembly, or permissions.component. Cascade operations require the appropriate ManageXxxLocation permission for each child asset type (e.g., ManageAssemblyLocation, ManageComponentLocation).

5b. Business Rules — Cascade Behaviour

ASSIGN Cascade Assign Rules (Kit / Assembly)

  • 1.When assigning a location to a kit or assembly, the user is prompted to also assign to child assets.
  • 2.This is NOT automatic -- the user must confirm via a checkbox list of children.
  • 3.Each child asset requires its own ManageXxxLocation permission (e.g., ManageAssemblyLocation, ManageComponentLocation).
  • 4.Children for which the user lacks permission are shown in the list but disabled/grayed out (opacity-50 cursor-not-allowed).
  • 5.User can choose: "Assign to selected" (cascade) or "Assign to {asset} only" (skip children).
  • 6."Select All / Deselect All" toggle is provided for convenience.

UNASSIGN Cascade Unassign Rules (Kit / Assembly)

  • 1.When removing a location from a kit or assembly, the system checks for children that share the same location via getSharedLocationChildren().
  • 2.If shared children are found, a confirmation modal shows a checkbox list of affected children.
  • 3.Only children that currently have the same location assigned are shown.
  • 4.Each child requires ManageXxxLocation permission -- children without permission are shown disabled.
  • 5.User can choose: "Remove from selected" (cascade) or "Remove from {asset} only" (skip children).
  • 6.If no children share the location, a simple ConfirmModal is shown instead (no cascade).

Decision Flow

User clicks "Add Location"
  -> LocationSelector modal opens
  -> User searches existing locations
     -> Selects existing location -> proceed to assign
     -> Clicks "Create new location" -> LocationModal opens (create-only)
        -> User enters name + Google Places search
        -> Clicks "Create Location" -> API creates location
        -> New location auto-selected in dropdown -> proceed to assign
  -> Is asset a kit/assembly with children?
     NO  -> assign directly
     YES -> show CascadeAssignModal (checkbox list)
            -> "Assign to selected" -> assign parent + checked children
            -> "Assign to {asset} only" -> assign parent only

User clicks "Remove"
  -> Is asset a kit/assembly with children?
     NO  -> simple ConfirmModal
     YES -> getSharedLocationChildren()
            -> any shared? NO  -> simple ConfirmModal
            -> any shared? YES -> CascadeUnassignModal (checkbox list)
               -> "Remove from selected" -> unassign parent + checked children
               -> "Remove from {asset} only" -> unassign parent only

6. Variants

DEFAULT With locations

Split layout: left sidebar (lg:w-80) with scrollable location cards, right panel with Google Map. First location is auto-selected. Map centers on selected location at zoom 13. Clicking a card selects it and re-centers the map.

EMPTY No locations assigned

EmptyState component renders with map pin SVG icon, "No location assigned" title, contextual description ("Assign a location to this {assetType}..."), and an "Add Location" action button (if permission).

LOADING Initial loading (no data)

Scoped loading state: card with header "Location" and a centered spinner (h-6 w-6 border-t-ag_red) at py-16. Not a full-page loader -- only the content area spins.

LOADING Sidebar refetch

When data exists but a refresh is triggered, the sidebar shows a smaller spinner (h-5 w-5) centered at py-8 while the map remains visible.

MODAL Location selector

Rendered via createPortal as a fixed overlay (fixed inset-0 z-50 bg-black/50). Contains a max-w-md rounded-xl bg-white dialog for searching existing locations. "Create new location" button always visible at the bottom of results (or prominently in empty state).

MODAL Create new location (LocationModal)

Triggered from "Create new location" button inside LocationSelector. Opens LocationModal in create-only mode. Form fields: location name (required), Google Places autocomplete search, interactive map preview (click to adjust pin), resolved address summary. On create, calls LocationV2Service.createLocation(), adds location to selector dropdown, and auto-selects it.

CASCADE Cascade assign (kits/assemblies)

After selecting a location, if the asset is a kit/assembly with children, a confirmation modal shows a checkbox list of child assets. User selects which children should also receive the location. Requires ManageXxxLocation permission per child type.

CASCADE Cascade unassign (kits/assemblies)

For kits and assemblies, removing a location checks for children sharing that location. If found, a confirmation modal lists affected child assets with checkboxes before proceeding with bulk unassign.

7. Interactions

User ActionBehaviour
Click location cardCard becomes selected (border-ag_red/30 bg-red-50/30), map re-centers on that location at zoom 13. GoogleMap re-keys on DW_TransactionID to force re-render.
Click "Add Location"Opens LocationSelector modal via portal. User searches existing locations or clicks "Create new location" to open LocationModal. On selection, calls locationV2Svc.assignAssetLocation(). For kits/assemblies with children, shows CascadeAssignModal before executing.
Click "Create new location"Opens LocationModal in create-only mode. User enters location name + Google Places autocomplete search. On submit, calls LocationV2Service.createLocation(). New location is added to the selector dropdown and auto-selected, triggering the assign flow.
Click "Remove"e.stopPropagation() prevents card selection. For kits/assemblies, checks for shared children via getSharedLocationChildren(). If shared children found, shows CascadeUnassignModal. Otherwise simple ConfirmModal.
Cascade confirm (assign)"Assign to selected" cascades to checked children. "Assign to {asset} only" skips children. Cancel aborts entirely.
Cascade confirm (unassign)"Remove from selected" unassigns from parent + checked children. "Remove from {asset} only" unassigns parent only. Cancel aborts.
No locations + no map APIWhen isLoaded is false (Google Maps not loaded), map container is empty. When no selected location, map centers at (0, 0) zoom 2 (world view).

8. Status

Web implementation complete (Assembly, Component, Kit)
V2 API integration (LocationV2Service)
Location history with Current/Previous badges
Google Maps integration with marker
Cascade assign/unassign for kits and assemblies
Scoped loading states (not full-page)
Empty state with contextual description
Mobile (React Native) -- not started
Test coverage

9. Web Interactive Mockups

Location

Melbourne Depot

Current

42 Industrial Drive

3000 Melbourne, Australia

Assigned: 15/03/2024

Sydney Warehouse

Previous

7 Harbour Road

2000 Sydney, Australia

Assigned: 01/11/2023

Brisbane Site

Previous

15 Queen Street

4000 Brisbane, Australia

Assigned: 20/06/2023

Melbourne Depot
Sydney
Brisbane

Split layout: sidebar card (w-80) left, Google Map (flex-1) right. Selected card has border-ag_red/30 bg-red-50/30. Red pin = current, gray pins = previous.

Location

No location assigned

Assign a location to this assembly so it appears on the dashboard map and can be tracked.

EmptyState component with cardTitle "Location", pin icon, "No location assigned" message, and "Assign Location" button.

Location

Scoped loading: card with "Location" title header + centered spinner. NOT a full-page loader.

Assign Location

Select an existing location or create a new one.

Melbourne Depot

Synced

42 Industrial Drive, 3000 Melbourne

Melbourne CBD Office

100 Collins Street, 3000 Melbourne

Portal overlay (bg-black/50) with white modal (max-w-md). LocationSelector shows search dropdown with address, "Synced" badge, map preview, and "Create new location" button at the bottom of results. If no results match, the empty state also shows a prominent "Create new location" button.

Create Location

Enter a name and search for the address using Google Places.

Click map to adjust pin position

Resolved Address

Marienplatz 1, 80331 Munich, Germany

LocationModal in create-only mode. User enters a name, searches via Google Places autocomplete, previews on map, and clicks "Create Location". The new location is automatically added to the LocationSelector dropdown and selected. Address components (street, postal code, city, country, ISO code) are extracted from the Google Place response.

Assign to child items?

You are assigning Melbourne Depot to this kit. Select which children should also receive this location.

4 items

Cascade assign confirmation for kit/assembly. Checkbox list of children with Select All toggle. Children without ManageXxxLocation permission shown disabled. Three action buttons: "Assign to selected" (red), "Assign to kit only" (outline), Cancel.

Remove location from child items?

The following children also have Melbourne Depot assigned. Select which children should also have this location removed.

3 children share this location

Cascade unassign confirmation. Only children that share the same location are listed. Same permission check pattern. "Remove from selected" (red), "Remove from kit only" (outline), Cancel.

10. Mobile Mockups (React Native)

Proposed native layout for the Location tab. Same data model and permissions. Variant switcher below.

9:41

Assemblies

2" Gate Valve Assembly

Info Specs Components Service Location
Melbourne Depot

Location History

Melbourne Depot

Current

42 Industrial Drive

3000 Melbourne, Australia

Assigned: 15/03/2024

Sydney Warehouse

Previous

7 Harbour Road

2000 Sydney, Australia

Assigned: 01/11/2023

Brisbane Site

Previous

15 Queen Street

4000 Brisbane, Australia

Assigned: 20/06/2023

Dashboard
Search
Assets
QR Scan
Profile

Map at top (~40%), location history cards below (scrollable). Current card has green left border + "Current" badge. FAB for "Add Location".

9:41

Components

Valve Body

Info Specs Location

No location assigned

Assign a location to this component so it appears on the dashboard map and can be tracked.

Dashboard
Search
Assets
QR Scan
Profile

EmptyState component inside iPhone frame. Pin icon, "No location assigned", "Assign Location" button.

9:41

2" Gate Valve Assembly

Assign Location

Melbourne Depot

Synced

42 Industrial Drive, 3000 Melbourne

Melbourne CBD Office

100 Collins Street, 3000 Melbourne

Melbourne South Plant

88 Bay Road, 3205 South Melbourne

Bottom sheet (slides up ~70%). Drag handle, title, search input with "Create new location" button at bottom of results, Cancel button.

9:41

Create Location

Enter a name and search for the address.

Tap map to adjust pin

Resolved Address

Marienplatz 1, 80331 Munich, Germany

Bottom sheet form for creating a new location. Triggered by "Create new location" button in the Assign Location sheet. Location name (required), Google Places autocomplete search, interactive map preview with pin, resolved address summary. On create, location is added to selector and auto-selected.

9:41

2" Gate Valve Assembly

Assign to child items?

Select which children should also receive Melbourne Depot.

3 items

Bottom sheet cascade confirmation. Drag handle, title, scrollable checkbox list of children, "Assign to selected" + "This asset only" + Cancel.