Skip to content
sysout.dev

Technical Design: Digital Twin Polish

Technical design for completing item, grid, assignment, location, and deletion interactions in Homey's digital twin.

1. Overview

This document provides the technical specification for completing the Digital Twin feature. It covers the missing interactions identified in MVP-STATUS.adoc:

  1. Item Detail Dialog - View complete item information

  2. Item Editing - Modify existing items

  3. Item Deletion - Remove items with confirmation

  4. Quick Assign Dialog - Assign unassigned items to grid cells

  5. Cell Detail Dialog - View/manage items in a grid cell (when multiple items exist)

  6. Location Editing - Rename/reconfigure locations

  7. Location Deletion - Remove locations with cascade warnings

2. Feature Specifications

2.1 Item Detail Dialog

Purpose

Display complete item information in a modal dialog when user taps an item in grid or list view.

UI Design

+------------------------------------------+
|              Item Details            [X] |
+------------------------------------------+
|                                          |
|  [Item Photo or Category Icon]           |
|                                          |
|  Name:        Organic Walnuts            |
|  Brand:       Nature's Best              |
|  Category:    PANTRY                     |
|  Quantity:    500g                       |
|  Expiry:      Jan 30, 2026 (7 days)      |
|  Location:    Kitchen > Pantry > Shelf 2 |
|  Position:    Row 1, Col 3               |
|                                          |
|  +--------+  +--------+  +------------+  |
|  |  Edit  |  | Delete |  | Move/Assign|  |
|  +--------+  +--------+  +------------+  |
|                                          |
|              [ Close ]                   |
+------------------------------------------+

Data Model

No new entities required. Uses existing GroceryItem domain model.

// New UI state for dialog
data class ItemDetailState(
    val item: Item?,
    val isVisible: Boolean = false,
    val locationPath: String = "" // "Kitchen > Pantry > Shelf 2"
)

Interactions

Trigger Action Result

Tap item in grid cell

Show ItemDetailDialog

Dialog opens with item data

Tap item in list view

Show ItemDetailDialog

Dialog opens with item data

Tap "Edit" button

Transition to Edit mode

Dialog transforms to edit form

Tap "Delete" button

Show delete confirmation

Confirmation dialog appears

Tap "Move/Assign" button

Show QuickAssignDialog

Position picker appears

Tap [X] or "Close"

Dismiss dialog

Dialog closes

Tap outside dialog

Dismiss dialog

Dialog closes

Expiry Visual Indicators

Condition Visual

Expired

Red background, "EXPIRED" badge

Expires today

Red text, "Expires today!"

Expires in 1-3 days

Orange text, "Expires in X days"

Expires in 4-7 days

Yellow text, "Expires in X days"

Expires > 7 days

Normal text, formatted date

No expiry set

Gray text, "No expiry date"

Component Specification

@Composable
fun ItemDetailDialog(
    item: Item,
    locationPath: String,
    onDismiss: () -> Unit,
    onEdit: (Item) -> Unit,
    onDelete: (Item) -> Unit,
    onMove: (Item) -> Unit
)

2.2 Item Editing

Purpose

Allow users to modify existing item properties.

UI Design

Two approaches considered:

Option A: In-Dialog Editing (Recommended) Transform the detail dialog into edit mode with editable fields.

+------------------------------------------+
|              Edit Item               [X] |
+------------------------------------------+
|                                          |
|  [Photo]  [Change Photo]                 |
|                                          |
|  Name:     [Organic Walnuts_____]        |
|  Brand:    [Nature's Best_______]        |
|  Category: [ PANTRY          v ]         |
|  Quantity: [500_] [g    v]               |
|  Expiry:   [Jan 30, 2026    ๐Ÿ“…]          |
|                                          |
|  +--------+  +----------+                |
|  | Cancel |  |   Save   |                |
|  +--------+  +----------+                |
+------------------------------------------+

Option B: Separate Edit Screen Navigate to full-screen edit. More space but loses context.

Decision: Option A - In-dialog editing maintains context and is consistent with Add Item dialog.

Data Flow

User taps Edit
    -> Dialog enters edit mode (isEditing = true)
    -> Fields become editable
    -> User modifies fields
    -> User taps Save
        -> Validate inputs
        -> Call viewModel.updateItem(...)
        -> Exit edit mode
        -> Refresh item display

Validation Rules

Field Validation Error Message

Name

Required, non-blank

"Item name is required"

Quantity

Positive number

"Quantity must be greater than 0"

Expiry Date

Future or today (warning if past)

"This date is in the past" (warning, allow save)

ViewModel Additions

// ItemViewModel.kt additions
fun updateItem(
    itemId: String,
    name: String,
    category: ItemCategory,
    brand: String?,
    quantity: Double,
    unit: String,
    expiryDate: Date?
) {
    viewModelScope.launch {
        itemRepository.updateGroceryItem(
            itemId = itemId,
            name = name,
            category = category.name,
            brand = brand,
            quantity = quantity,
            unit = unit,
            expiryDate = expiryDate
        )
    }
}

Repository Additions

// ItemRepository interface
suspend fun updateGroceryItem(
    itemId: String,
    name: String,
    category: String,
    brand: String?,
    quantity: Double,
    unit: String,
    expiryDate: Date?
)

DAO Additions

// ItemDao.kt additions
@Query("""
    UPDATE items
    SET name = :name, category = :category
    WHERE id = :itemId
""")
suspend fun updateItemBase(itemId: String, name: String, category: String)

@Query("""
    UPDATE grocery_items
    SET brand = :brand, quantity = :quantity, unit = :unit, expiryDate = :expiryDate
    WHERE itemId = :itemId
""")
suspend fun updateGroceryDetails(
    itemId: String,
    brand: String?,
    quantity: Double,
    unit: String,
    expiryDate: Date?
)

@Transaction
suspend fun updateGroceryItem(
    itemId: String,
    name: String,
    category: String,
    brand: String?,
    quantity: Double,
    unit: String,
    expiryDate: Date?
) {
    updateItemBase(itemId, name, category)
    updateGroceryDetails(itemId, brand, quantity, unit, expiryDate)
}

2.3 Item Deletion

Purpose

Allow users to remove items from inventory.

UI Design

+------------------------------------------+
|           Delete Item?                   |
+------------------------------------------+
|                                          |
|  Are you sure you want to delete         |
|  "Organic Walnuts"?                      |
|                                          |
|  This action cannot be undone.           |
|                                          |
|  +----------+  +-----------------+       |
|  |  Cancel  |  | Delete (Red)    |       |
|  +----------+  +-----------------+       |
+------------------------------------------+

Interactions

Trigger Result

User taps Delete in ItemDetailDialog

Show DeleteConfirmationDialog

User confirms deletion

Item removed from DB, dialog closes, list updates

User cancels

Return to ItemDetailDialog

ViewModel Additions

fun deleteItem(itemId: String) {
    viewModelScope.launch {
        itemRepository.deleteItem(itemId)
    }
}

DAO Additions

@Query("DELETE FROM items WHERE id = :itemId")
suspend fun deleteItem(itemId: String)
// Note: grocery_items row auto-deleted via CASCADE

Edge Cases

  • Last item in location: Show empty state after deletion

  • Assigned item deleted: Grid cell becomes empty (no special handling needed)

  • Item in meal plan: Future consideration - warn user if item is referenced


2.4 Quick Assign Dialog

Purpose

Allow users to assign an unassigned item to a specific grid cell.

UI Design

+------------------------------------------+
|        Assign "Walnuts" to Grid      [X] |
+------------------------------------------+
|                                          |
|  Select a cell to place this item:       |
|                                          |
|  +-------+-------+-------+               |
|  | [Milk]|  [x3] |   +   |  <- Row 1     |
|  +-------+-------+-------+               |
|  |   +   |   +   | [Oats]|  <- Row 2     |
|  +-------+-------+-------+               |
|  |   +   |   +   |   +   |  <- Row 3     |
|  +-------+-------+-------+               |
|     ^-- Empty cells show "+"             |
|     ^-- Occupied cells show item/count   |
|                                          |
|  Tip: Tap an empty cell (+) to assign    |
|       Tap occupied cell to stack items   |
|                                          |
|              [ Cancel ]                  |
+------------------------------------------+

Interactions

Trigger Action Result

Tap unassigned item chip

Open QuickAssignDialog

Grid picker appears

Tap empty cell (+)

Assign item to cell

Item moves to cell, dialog closes

Tap occupied cell

Assign item (stack)

Item added to cell, dialog closes

Tap Cancel

Dismiss

Dialog closes, no change

Component Specification

@Composable
fun QuickAssignDialog(
    item: Item,
    gridConfig: LocationGridConfig,
    existingItems: Map<GridPosition, List<Item>>,
    onAssign: (row: Int, column: Int) -> Unit,
    onDismiss: () -> Unit
)

State Management

// Add to ItemViewState
data class ItemViewState(
    // ... existing fields ...
    val quickAssignItem: Item? = null // Item being assigned, null = dialog hidden
)

// ViewModel functions
fun startQuickAssign(item: Item) {
    _viewState.value = _viewState.value.copy(quickAssignItem = item)
}

fun cancelQuickAssign() {
    _viewState.value = _viewState.value.copy(quickAssignItem = null)
}

fun confirmQuickAssign(row: Int, column: Int) {
    viewState.value.quickAssignItem?.let { item ->
        assignItemToPosition(item.id.toString(), row, column)
    }
    cancelQuickAssign()
}

2.5 Cell Detail Dialog

Purpose

When a grid cell contains multiple items, show a dialog listing all items in that cell.

UI Design

+------------------------------------------+
|         Cell (Row 1, Col 2)          [X] |
+------------------------------------------+
|                                          |
|  3 items in this cell:                   |
|                                          |
|  +------------------------------------+  |
|  | [icon] Coffee Beans                |  |
|  |        Brand X - Expires Feb 15    |  |
|  +------------------------------------+  |
|  | [icon] Tea Bags                    |  |
|  |        Lipton - No expiry          |  |
|  +------------------------------------+  |
|  | [icon] Hot Chocolate               |  |
|  |        Cadbury - Expires Mar 1     |  |
|  +------------------------------------+  |
|                                          |
|  Tap item for details                    |
|                                          |
|              [ Close ]                   |
+------------------------------------------+

Interactions

Trigger Action Result

Tap grid cell with multiple items

Open CellDetailDialog

List of items shown

Tap individual item in list

Open ItemDetailDialog

Item details shown

Tap Close

Dismiss

Dialog closes

Component Specification

@Composable
fun CellDetailDialog(
    row: Int,
    column: Int,
    items: List<Item>,
    onItemClick: (Item) -> Unit,
    onDismiss: () -> Unit
)

State Management

// Add to ItemViewState
data class ItemViewState(
    // ... existing fields ...
    val selectedCell: GridPosition? = null // null = no cell selected
)

fun selectCell(row: Int, column: Int) {
    _viewState.value = _viewState.value.copy(
        selectedCell = GridPosition(row, column)
    )
}

fun clearCellSelection() {
    _viewState.value = _viewState.value.copy(selectedCell = null)
}

2.6 Location Editing

Purpose

Allow users to rename locations and modify their grid configuration.

UI Design

+------------------------------------------+
|           Edit Location              [X] |
+------------------------------------------+
|                                          |
|  Name:     [Top Shelf______________]     |
|                                          |
|  Parent:   Kitchen > Pantry (readonly)   |
|                                          |
|  --- Grid Settings ---                   |
|                                          |
|  Enable Grid: [X]                        |
|  Rows:        [===|====] 3               |
|  Columns:     [=====|==] 4               |
|                                          |
|  Preview:                                |
|  +---+---+---+---+                       |
|  |   |   |   |   |                       |
|  +---+---+---+---+                       |
|  |   |   |   |   |                       |
|  +---+---+---+---+                       |
|  |   |   |   |   |                       |
|  +---+---+---+---+                       |
|                                          |
|  +----------+  +----------+              |
|  |  Cancel  |  |   Save   |              |
|  +----------+  +----------+              |
+------------------------------------------+

Access Points

  1. Long-press location in LocationAccordion

  2. "Edit" option in location context menu

  3. Settings icon in ItemScreen top bar (for current location)

ViewModel Additions (LocationViewModel)

fun updateLocation(locationId: String, newName: String) {
    viewModelScope.launch {
        locationRepository.updateLocationName(locationId, newName)
    }
}

DAO Additions

@Query("UPDATE locations SET name = :name WHERE id = :id")
suspend fun updateLocationName(id: String, name: String)

Grid Resize Logic

When grid dimensions are reduced: 1. Check if any items exist outside new bounds 2. If yes, show warning: "X items will become unassigned" 3. On confirm, set those items' gridRow/gridColumn to NULL

@Query("""
    UPDATE items
    SET gridRow = NULL, gridColumn = NULL
    WHERE locationId = :locationId
    AND (gridRow >= :newRows OR gridColumn >= :newColumns)
""")
suspend fun unassignItemsOutsideBounds(
    locationId: String,
    newRows: Int,
    newColumns: Int
)

2.7 Location Deletion

Purpose

Allow users to delete locations with appropriate warnings about cascading effects.

UI Design

+------------------------------------------+
|           Delete Location?               |
+------------------------------------------+
|                                          |
|  Are you sure you want to delete         |
|  "Top Shelf"?                            |
|                                          |
|  WARNING: This will also delete:         |
|  - 12 items in this location             |
|  - 2 child locations                     |
|    - Middle Shelf (8 items)              |
|    - Bottom Shelf (5 items)              |
|                                          |
|  Total: 25 items will be deleted         |
|                                          |
|  This action cannot be undone.           |
|                                          |
|  +----------+  +------------------+      |
|  |  Cancel  |  | Delete All (Red) |      |
|  +----------+  +------------------+      |
+------------------------------------------+

Pre-Delete Analysis Query

data class LocationDeletionImpact(
    val location: Location,
    val directItemCount: Int,
    val childLocations: List<LocationDeletionImpact>,
    val totalItemCount: Int
)

// Repository function to analyze deletion impact
suspend fun analyzeDeletionImpact(locationId: String): LocationDeletionImpact

DAO Additions

@Query("SELECT COUNT(*) FROM items WHERE locationId = :locationId")
suspend fun getItemCountForLocation(locationId: String): Int

@Query("SELECT * FROM locations WHERE parentId = :parentId")
suspend fun getChildLocations(parentId: String): List<LocationEntity>

@Query("DELETE FROM locations WHERE id = :locationId")
suspend fun deleteLocation(locationId: String)
// Note: Items and children auto-deleted via CASCADE

Interactions

Trigger Result

Long-press location > Delete

Show deletion impact dialog

Confirm delete

Location + all children + all items deleted

Cancel

Dialog closes, no change


3. State Management Summary

3.1 Updated ItemViewState

data class ItemViewState(
    // Existing
    val viewMode: ViewMode = ViewMode.LIST,
    val searchQuery: String = "",
    val selectedCategories: Set<ItemCategory> = emptySet(),
    val sortBy: SortOption = SortOption.NAME,
    val showUnassignedOnly: Boolean = false,

    // New - Dialog states
    val selectedItem: Item? = null,           // For ItemDetailDialog
    val isEditingItem: Boolean = false,       // Edit mode in detail dialog
    val quickAssignItem: Item? = null,        // For QuickAssignDialog
    val selectedCell: GridPosition? = null,   // For CellDetailDialog
    val itemToDelete: Item? = null            // For delete confirmation
)

3.2 Updated LocationViewState

data class LocationViewState(
    // Existing
    val expandedLocationIds: Set<String> = emptySet(),

    // New
    val selectedLocation: Location? = null,     // For editing
    val locationToDelete: Location? = null,     // For delete confirmation
    val deletionImpact: LocationDeletionImpact? = null
)

4. Component Hierarchy

ItemScreen
โ”œโ”€โ”€ TopAppBar (with view toggle, settings)
โ”œโ”€โ”€ ItemSearchBar
โ”œโ”€โ”€ Content Area
โ”‚   โ”œโ”€โ”€ FlexibleGrid (grid mode)
โ”‚   โ”‚   โ””โ”€โ”€ GridCell
โ”‚   โ”‚       โ””โ”€โ”€ ItemCard (compact)
โ”‚   โ””โ”€โ”€ CategoryGroupList (list mode)
โ”‚       โ””โ”€โ”€ ItemCard (expanded)
โ”œโ”€โ”€ UnassignedItemsBar
โ”‚   โ””โ”€โ”€ ItemChip
โ””โ”€โ”€ Dialogs
    โ”œโ”€โ”€ AddItemDialog (existing)
    โ”œโ”€โ”€ GridConfigDialog (existing)
    โ”œโ”€โ”€ ItemDetailDialog (NEW)
    โ”‚   โ””โ”€โ”€ Edit mode transforms in-place
    โ”œโ”€โ”€ DeleteConfirmationDialog (NEW)
    โ”œโ”€โ”€ QuickAssignDialog (NEW)
    โ””โ”€โ”€ CellDetailDialog (NEW)

LocationScreen
โ”œโ”€โ”€ TopAppBar
โ”œโ”€โ”€ LocationAccordion (recursive)
โ”‚   โ””โ”€โ”€ LocationRow
โ”‚       โ””โ”€โ”€ Context menu (edit, delete)
โ”œโ”€โ”€ FAB (add location)
โ””โ”€โ”€ Dialogs
    โ”œโ”€โ”€ AddLocationDialog (existing)
    โ”œโ”€โ”€ EditLocationDialog (NEW)
    โ””โ”€โ”€ DeleteLocationDialog (NEW)

5. Files to Create/Modify

New Files

File Purpose

item/components/ItemDetailDialog.kt

View/edit item details

item/components/DeleteConfirmationDialog.kt

Generic delete confirmation

item/components/QuickAssignDialog.kt

Grid position picker

item/components/CellDetailDialog.kt

Multi-item cell view

location/components/EditLocationDialog.kt

Location editing

location/components/DeleteLocationDialog.kt

Location deletion with impact

Modified Files

File Changes

ItemViewModel.kt

Add update/delete functions, dialog state

ItemScreen.kt

Wire up new dialogs, implement click handlers

ItemDao.kt

Add update/delete queries

ItemRepository.kt

Add update/delete methods

LocalItemRepository.kt

Implement new methods

LocationViewModel.kt

Add edit/delete functions

LocationDao.kt

Add update/delete queries

LocationRepository.kt

Add update/delete/analyze methods

LocalLocationRepository.kt

Implement new methods

LocationScreen.kt

Add edit/delete UI

LocationAccordion.kt

Add long-press context menu


6. Implementation Order

Recommended sequence to minimize dependencies:

Phase Feature Rationale

1

Item Detail Dialog (view only)

Foundation for other item interactions

2

Item Deletion

Simple, tests DAO deletion

3

Item Editing

Builds on detail dialog

4

Quick Assign Dialog

Enables core grid functionality

5

Cell Detail Dialog

Handles multi-item cells

6

Location Editing

Independent of item features

7

Location Deletion

Requires impact analysis


7. Edge Cases & Error Handling

Item Operations

Scenario Handling UI Feedback

Edit item while offline

Queue for sync (future)

"Saved locally"

Delete last item

Show empty state

"No items. Add one!"

Assign to occupied cell

Stack items

Show count badge

Grid disabled while items assigned

Keep positions, hide grid

Items visible in list

Location Operations

Scenario Handling UI Feedback

Delete root with children

Cascade delete all

Show impact summary

Rename to empty string

Validation error

"Name cannot be empty"

Resize grid with items outside

Unassign items

"X items will be unassigned"

Delete location with items in meal plan

Future: warn

N/A for MVP


8. Testing Considerations

Unit Tests

  • ItemViewModel: test all state transitions

  • LocationViewModel: test deletion impact calculation

  • Repository methods: test CRUD operations

Integration Tests

  • Database cascade deletes work correctly

  • Grid resize properly unassigns items

  • Flow updates propagate to UI

Manual Test Scenarios

  1. Add item > View details > Edit > Save > Verify changes

  2. Assign item to grid > Delete item > Verify cell empty

  3. Add multiple items to cell > View cell > Verify all shown

  4. Resize grid smaller > Verify items unassigned

  5. Delete location with children > Verify cascade


9. Accessibility

Content Descriptions

  • All interactive elements have meaningful descriptions

  • Grid cells announce position: "Row 1, Column 2, contains Milk"

  • Dialogs have proper focus management

Touch Targets

  • Minimum 48dp for all interactive elements

  • Long-press detection with haptic feedback


10. Open Questions

  1. Drag-and-drop: Should we implement in this phase or defer?

    • Recommendation: Defer to Phase 2 - adds complexity, quick assign covers use case

  2. Undo support: Should deletion be undoable?

    • Recommendation: Defer - would require soft delete, adds complexity

  3. Batch operations: Multi-select for bulk delete/move?

    • Recommendation: Defer to Phase 2 - good feature but not MVP critical


11. Implementation Status

All features have been implemented:

Feature Status Files Created/Modified

Item Detail Dialog

DONE

ItemDetailDialog.kt

Item Deletion

DONE

DeleteConfirmationDialog.kt, ItemDao.kt, ItemRepository.kt

Item Editing

DONE

ItemEditDialog.kt, ItemDao.kt, ItemRepository.kt, ItemViewModel.kt

Quick Assign Dialog

DONE

QuickAssignDialog.kt, ItemViewModel.kt, ItemScreen.kt

Cell Detail Dialog

DONE

CellDetailDialog.kt, ItemViewModel.kt, ItemScreen.kt

Location Editing

DONE

EditLocationDialog.kt, LocationDao.kt, LocationRepository.kt, LocationViewModel.kt

Location Deletion

DONE

DeleteLocationDialog.kt, LocationDao.kt, LocationRepository.kt, LocationViewModel.kt, LocationAccordion.kt


Document Version: 1.1 Created: 2026-01-23 Updated: 2026-01-23 Status: IMPLEMENTED