diff --git a/src/animaltrack/web/routes/actions.py b/src/animaltrack/web/routes/actions.py index d41a3cf..16025f6 100644 --- a/src/animaltrack/web/routes/actions.py +++ b/src/animaltrack/web/routes/actions.py @@ -408,13 +408,13 @@ def promote_index(request: Request, animal_id: str): if animal.status != "alive": return HTMLResponse(content="Only alive animals can be promoted", status_code=400) - if animal.identified: - return HTMLResponse(content="Animal is already identified", status_code=400) + # Title depends on whether animal is already identified (rename vs promote) + title = "Rename Animal - AnimalTrack" if animal.identified else "Promote Animal - AnimalTrack" return render_page( request, promote_form(animal), - title="Promote Animal - AnimalTrack", + title=title, active_nav=None, ) @@ -448,9 +448,6 @@ async def animal_promote(request: Request): if animal.status != "alive": return _render_promote_error(request, animal, "Only alive animals can be promoted", form) - if animal.identified: - return _render_promote_error(request, animal, "Animal is already identified", form) - # Create payload try: payload = AnimalPromotedPayload( diff --git a/src/animaltrack/web/routes/feed.py b/src/animaltrack/web/routes/feed.py index 6fd6495..a8fab74 100644 --- a/src/animaltrack/web/routes/feed.py +++ b/src/animaltrack/web/routes/feed.py @@ -273,7 +273,7 @@ async def feed_purchased(request: Request, session): feed_type_code = form.get("feed_type_code", "") bag_size_kg_str = form.get("bag_size_kg", "0") bags_count_str = form.get("bags_count", "0") - bag_price_cents_str = form.get("bag_price_cents", "0") + bag_price_euros_str = form.get("bag_price_euros", "0") vendor = form.get("vendor") or None notes = form.get("notes") or None nonce = form.get("nonce") @@ -329,9 +329,10 @@ async def feed_purchased(request: Request, session): "Bags count must be at least 1", ) - # Validate bag_price_cents + # Validate bag_price_euros and convert to cents try: - bag_price_cents = int(bag_price_cents_str) + bag_price_euros = float(bag_price_euros_str) + bag_price_cents = int(round(bag_price_euros * 100)) except ValueError: return _render_purchase_error( request, diff --git a/src/animaltrack/web/templates/actions.py b/src/animaltrack/web/templates/actions.py index 12d8d69..b2f276e 100644 --- a/src/animaltrack/web/templates/actions.py +++ b/src/animaltrack/web/templates/actions.py @@ -4,7 +4,7 @@ from collections.abc import Callable from typing import Any -from fasthtml.common import H2, H3, Div, Form, Hidden, Input, Option, P, Span +from fasthtml.common import H2, H3, Div, Form, Hidden, Input, Option, P, Select, Span from monsterui.all import ( Alert, AlertT, @@ -436,6 +436,11 @@ def promote_form( """ display_id = f"{animal.animal_id[:8]}..." + # Title and action text depend on whether animal is already identified + is_rename = animal.identified + form_title = "Rename Animal" if is_rename else "Promote Animal" + action_text = "Renaming" if is_rename else "Promoting" + # Build sex options (optional - can refine current value) sexes = [ ("", "Keep current"), @@ -446,13 +451,13 @@ def promote_form( for code, label in sexes: sex_options.append(Option(label, value=code, selected=code == selected_sex)) - # Build repro status options (optional) + # Build repro status options (optional) - must match ReproStatus enum repro_statuses = [ - ("", "Unknown"), - ("breeding", "Breeding"), - ("non_breeding", "Non-Breeding"), - ("broody", "Broody"), - ("molting", "Molting"), + ("", "Keep current"), + ("intact", "Intact"), + ("wether", "Wether (castrated male)"), + ("spayed", "Spayed (female)"), + ("unknown", "Unknown"), ] repro_status_options = [] for code, label in repro_statuses: @@ -466,36 +471,34 @@ def promote_form( error_component = Alert(error, cls=AlertT.warning) return Form( - H2("Promote Animal", cls="text-xl font-bold mb-4"), + H2(form_title, cls="text-xl font-bold mb-4"), # Animal info Div( - P(f"Promoting: {display_id}", cls="text-sm text-stone-400"), + P(f"{action_text}: {display_id}", cls="text-sm text-stone-400"), P(f"Species: {animal.species_code}, Sex: {animal.sex}", cls="text-sm text-stone-400"), cls="p-3 bg-slate-800 rounded-md mb-4", ), # Error message if present error_component, - # Nickname input (optional) + # Nickname input - label changes for rename vs promote LabelInput( - "Nickname (optional)", + "New Name" if is_rename else "Nickname (optional)", id="nickname", name="nickname", - value=nickname_value, - placeholder="Give this animal a name", + value=nickname_value or (animal.nickname or ""), + placeholder="Enter a name for this animal", ), - # Sex refinement dropdown (optional) - LabelSelect( - *sex_options, - label="Refine Sex (optional)", - id="sex", - name="sex", + # Sex refinement dropdown (optional) - using raw Select to fix value handling + Div( + FormLabel("Refine Sex (optional)", _for="sex"), + Select(*sex_options, name="sex", id="sex", cls="uk-select"), + cls="space-y-2", ), - # Repro status dropdown (optional) - LabelSelect( - *repro_status_options, - label="Reproductive Status", - id="repro_status", - name="repro_status", + # Repro status dropdown (optional) - using raw Select to fix value handling + Div( + FormLabel("Reproductive Status", _for="repro_status"), + Select(*repro_status_options, name="repro_status", id="repro_status", cls="uk-select"), + cls="space-y-2", ), # Distinguishing traits (optional) LabelTextArea( @@ -515,8 +518,12 @@ def promote_form( # Hidden fields Hidden(name="animal_id", value=animal.animal_id), Hidden(name="nonce", value=str(ULID())), - # Submit button - Button("Promote to Identified", type="submit", cls=ButtonT.primary), + # Submit button - text changes for rename vs promote + Button( + "Save Changes" if is_rename else "Promote to Identified", + type="submit", + cls=ButtonT.primary, + ), # Form submission via standard action/method (hx-boost handles AJAX) action=action, method="post", @@ -1071,24 +1078,21 @@ def attrs_form( ), # Selection container - updated via HTMX when filter changes selection_container, - # Attribute dropdowns - LabelSelect( - *sex_options, - label="Sex", - id="sex", - name="sex", + # Attribute dropdowns - using raw Select to fix value handling + Div( + FormLabel("Sex", _for="sex"), + Select(*sex_options, name="sex", id="sex", cls="uk-select"), + cls="space-y-2", ), - LabelSelect( - *life_stage_options, - label="Life Stage", - id="life_stage", - name="life_stage", + Div( + FormLabel("Life Stage", _for="life_stage"), + Select(*life_stage_options, name="life_stage", id="life_stage", cls="uk-select"), + cls="space-y-2", ), - LabelSelect( - *repro_status_options, - label="Reproductive Status", - id="repro_status", - name="repro_status", + Div( + FormLabel("Reproductive Status", _for="repro_status"), + Select(*repro_status_options, name="repro_status", id="repro_status", cls="uk-select"), + cls="space-y-2", ), # Optional notes LabelTextArea( diff --git a/src/animaltrack/web/templates/animal_detail.py b/src/animaltrack/web/templates/animal_detail.py index fec9f45..e1096ec 100644 --- a/src/animaltrack/web/templates/animal_detail.py +++ b/src/animaltrack/web/templates/animal_detail.py @@ -7,6 +7,7 @@ from typing import Any from fasthtml.common import H2, H3, A, Div, Li, P, Span, Ul from monsterui.all import Button, ButtonT, Card, Grid +from animaltrack.id_gen import format_animal_id from animaltrack.repositories.animal_timeline import ( AnimalDetail, MergeInfo, @@ -61,7 +62,7 @@ def back_to_registry_link() -> Div: def animal_header_card(animal: AnimalDetail, merge_info: MergeInfo | None) -> Card: """Header card with animal summary.""" - display_name = animal.nickname or f"{animal.animal_id[:8]}..." + display_name = format_animal_id(animal.animal_id, animal.nickname) status_badge = status_badge_component(animal.status) tags_display = ( @@ -160,13 +161,14 @@ def quick_actions_card(animal: AnimalDetail) -> Card: href=f"/actions/tag-add?filter=animal_id:{animal.animal_id}", ) ) - if not animal.identified: - actions.append( - A( - Button("Promote", cls=ButtonT.default + " w-full"), - href=f"/actions/promote/{animal.animal_id}", - ) + # Show "Promote" for unidentified animals, "Rename" for identified ones + promote_label = "Rename" if animal.identified else "Promote" + actions.append( + A( + Button(promote_label, cls=ButtonT.default + " w-full"), + href=f"/actions/promote/{animal.animal_id}", ) + ) actions.append( A( Button("Record Outcome", cls=ButtonT.destructive + " w-full"), diff --git a/src/animaltrack/web/templates/animal_select.py b/src/animaltrack/web/templates/animal_select.py index ffb5504..c2728c1 100644 --- a/src/animaltrack/web/templates/animal_select.py +++ b/src/animaltrack/web/templates/animal_select.py @@ -47,9 +47,9 @@ def animal_checkbox_list( cls="uk-checkbox mr-2", hx_on_change="updateSelectionCount()", ), - Span(display_name, cls="text-stone-200"), + Span(display_name, cls="text-stone-200 mr-1"), Span( - f" ({animal.species_code}, {sex_code}, {stage_abbr}, {animal.location_name})", + f"({animal.species_code}, {sex_code}, {stage_abbr}, {animal.location_name})", cls="text-stone-500 text-sm", ), cls="flex items-center py-1 hover:bg-stone-800/30 px-2 rounded cursor-pointer", diff --git a/src/animaltrack/web/templates/feed.py b/src/animaltrack/web/templates/feed.py index e7a0462..0defdc7 100644 --- a/src/animaltrack/web/templates/feed.py +++ b/src/animaltrack/web/templates/feed.py @@ -4,12 +4,12 @@ from collections.abc import Callable from typing import Any -from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Ul +from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Select, Ul from monsterui.all import ( Button, ButtonT, + FormLabel, LabelInput, - LabelSelect, LabelTextArea, TabContainer, ) @@ -170,19 +170,17 @@ def give_feed_form( H2("Give Feed", cls="text-xl font-bold mb-4"), error_component, warning_component, - # Location dropdown - LabelSelect( - *location_options, - label="Location", - id="location_id", - name="location_id", + # Location dropdown - using raw Select to fix value handling + Div( + FormLabel("Location", _for="location_id"), + Select(*location_options, name="location_id", id="location_id", cls="uk-select"), + cls="space-y-2", ), - # Feed type dropdown - LabelSelect( - *feed_type_options, - label="Feed Type", - id="feed_type_code", - name="feed_type_code", + # Feed type dropdown - using raw Select to fix value handling + Div( + FormLabel("Feed Type", _for="feed_type_code"), + Select(*feed_type_options, name="feed_type_code", id="feed_type_code", cls="uk-select"), + cls="space-y-2", ), # Amount input LabelInput( @@ -247,12 +245,16 @@ def purchase_feed_form( return Form( H2("Purchase Feed", cls="text-xl font-bold mb-4"), error_component, - # Feed type dropdown - LabelSelect( - *feed_type_options, - label="Feed Type", - id="purchase_feed_type_code", - name="feed_type_code", + # Feed type dropdown - using raw Select to fix value handling + Div( + FormLabel("Feed Type", _for="purchase_feed_type_code"), + Select( + *feed_type_options, + name="feed_type_code", + id="purchase_feed_type_code", + cls="uk-select", + ), + cls="space-y-2", ), # Bag size LabelInput( @@ -276,15 +278,15 @@ def purchase_feed_form( value="1", required=True, ), - # Price per bag (cents) + # Price per bag (euros) LabelInput( - "Price per Bag (cents)", - id="bag_price_cents", - name="bag_price_cents", + "Price per Bag (€)", + id="bag_price_euros", + name="bag_price_euros", type="number", min="0", - step="1", - placeholder="e.g., 2400 for 24.00", + step="0.01", + placeholder="e.g., 24.00", required=True, ), # Optional vendor diff --git a/src/animaltrack/web/templates/registry.py b/src/animaltrack/web/templates/registry.py index 5e802dc..a33244a 100644 --- a/src/animaltrack/web/templates/registry.py +++ b/src/animaltrack/web/templates/registry.py @@ -5,8 +5,8 @@ from datetime import UTC, datetime from typing import Any from urllib.parse import urlencode -from fasthtml.common import H2, A, Div, Form, P, Span, Table, Tbody, Td, Th, Thead, Tr -from monsterui.all import Button, ButtonT, Grid, LabelInput +from fasthtml.common import H2, A, Div, Form, Input, P, Span, Table, Tbody, Td, Th, Thead, Tr +from monsterui.all import Button, ButtonT, FormLabel, Grid from animaltrack.id_gen import format_animal_id from animaltrack.models.reference import Location, Species @@ -75,21 +75,28 @@ def registry_header(filter_str: str, total_count: int) -> Div: ), # Filter form - full width, prominent Form( + # Label above the input row + FormLabel("Filter", _for="filter", cls="mb-2 block"), Div( - # Filter input - wider with flex-grow - Div( - LabelInput( - "Filter", - id="filter", - name="filter", - value=filter_str, - placeholder='species:duck status:alive location:"Strip 1"', - ), - cls="flex-1 min-w-0", # min-w-0 prevents flex overflow + # Filter input - takes most of the width + Input( + id="filter", + name="filter", + value=filter_str, + placeholder='species:duck status:alive location:"Strip 1"', + cls="uk-input flex-1", ), - # Apply button - fixed size - Button("Apply", type="submit", cls=f"{ButtonT.primary} shrink-0"), - cls="flex gap-3 items-end", + # Apply button + Button("Apply", type="submit", cls=f"{ButtonT.primary} px-4"), + # Clear button (only shown if filter is active) + A( + "Clear", + href="/registry", + cls="px-3 py-2 text-stone-400 hover:text-stone-200", + ) + if filter_str + else None, + cls="flex gap-2 items-center", ), action="/registry", method="get",