Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ docker compose exec web python manage.py createsuperuser
docker compose exec web python manage.py setup_groups
```

## Branching and Release Workflow

- **`main`** — production branch. Only updated via PR from `develop`.
- **`develop`** — integration branch. All feature work merges here first.
- **Feature branches** — branch from `develop`, PR back to `develop`.
- **Release flow**: `feature-branch → develop (PR) → main (PR)`. Never PR directly to `main` from a feature branch.
- **Hotfixes**: branch from `main`, PR to `main`, then cherry-pick or merge back to `develop`.
- When committing directly to `develop` (e.g. small fixes), no PR is needed for the `develop` commit itself — the PR happens when `develop` merges to `main`.

## Architecture

### Django Apps
Expand Down
34 changes: 25 additions & 9 deletions src/assets/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2351,12 +2351,23 @@ def location_detail(request, pk):
descendant_ids = [loc.pk for loc in location.get_descendants()]
all_location_ids = [location.pk] + descendant_ids

# Prefetch primary image to avoid N+1 queries
primary_image_prefetch = Prefetch(
"images",
queryset=AssetImage.objects.filter(is_primary=True),
to_attr="primary_images",
)

# Base querysets for three tabs
present_qs = Asset.objects.filter(
current_location_id__in=all_location_ids,
status="active",
checked_out_to__isnull=True,
).select_related("category", "category__department", "checked_out_to")
present_qs = (
Asset.objects.filter(
current_location_id__in=all_location_ids,
status="active",
checked_out_to__isnull=True,
)
.select_related("category", "category__department", "checked_out_to")
.prefetch_related(primary_image_prefetch)
)

# Subquery: due_date from most recent checkout transaction
latest_checkout_due = (
Expand All @@ -2370,13 +2381,18 @@ def location_detail(request, pk):
checked_out_to__isnull=False,
)
.select_related("category", "category__department", "checked_out_to")
.prefetch_related(primary_image_prefetch)
.annotate(checkout_due_date=Subquery(latest_checkout_due))
)

draft_qs = Asset.objects.filter(
current_location_id__in=all_location_ids,
status="draft",
).select_related("category", "category__department", "checked_out_to")
draft_qs = (
Asset.objects.filter(
current_location_id__in=all_location_ids,
status="draft",
)
.select_related("category", "category__department", "checked_out_to")
.prefetch_related(primary_image_prefetch)
)

# Tab counts
present_count = present_qs.count()
Expand Down
22 changes: 21 additions & 1 deletion src/templates/assets/location_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ <h3 class="font-medium text-stage-900 dark:text-cream">{{ child.name }}</h3>
<table class="w-full text-sm">
<thead>
<tr class="border-b border-stage-200 dark:border-white/5">
<th class="w-12 px-4 py-3"></th>
<th class="text-left px-4 py-3 text-stage-500 dark:text-cream/50 font-medium">Name</th>
<th class="text-left px-4 py-3 text-stage-500 dark:text-cream/50 font-medium">Barcode</th>
<th class="text-left px-4 py-3 text-stage-500 dark:text-cream/50 font-medium">Category</th>
Expand All @@ -175,6 +176,15 @@ <h3 class="font-medium text-stage-900 dark:text-cream">{{ child.name }}</h3>
<tbody class="divide-y divide-stage-200 dark:divide-white/5">
{% for asset in page_obj %}
<tr class="hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors">
<td class="py-2.5 px-4">
<div class="w-10 h-10 rounded-lg bg-stage-100 dark:bg-stage-700 flex items-center justify-center text-stage-500 dark:text-cream/30 overflow-hidden">
{% if asset.primary_image %}
<img src="{% if asset.primary_image.thumbnail %}{{ asset.primary_image.thumbnail.url }}{% else %}{{ asset.primary_image.image.url }}{% endif %}" class="w-full h-full object-cover" alt="" loading="lazy">
{% else %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{% endif %}
</div>
</td>
<td class="px-4 py-3">
<a href="{% url 'assets:asset_detail' asset.pk %}" class="text-stage-900 dark:text-cream hover:text-brand-400 transition-colors font-medium">{{ asset.name }}</a>
</td>
Expand Down Expand Up @@ -202,7 +212,15 @@ <h3 class="font-medium text-stage-900 dark:text-cream">{{ child.name }}</h3>
<div class="sm:hidden divide-y divide-stage-200 dark:divide-white/5">
{% for asset in page_obj %}
<a href="{% url 'assets:asset_detail' asset.pk %}" class="block p-4 hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors">
<div class="flex items-start justify-between gap-2">
<div class="flex items-start gap-3">
<div class="w-12 h-12 rounded-lg bg-stage-100 dark:bg-stage-700 flex items-center justify-center text-stage-500 dark:text-cream/30 overflow-hidden flex-shrink-0">
{% if asset.primary_image %}
<img src="{% if asset.primary_image.thumbnail %}{{ asset.primary_image.thumbnail.url }}{% else %}{{ asset.primary_image.image.url }}{% endif %}" class="w-full h-full object-cover" alt="" loading="lazy">
{% else %}
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{% endif %}
</div>
<div class="flex-1 min-w-0 flex items-start justify-between gap-2">
<div class="min-w-0">
<div class="font-medium text-stage-900 dark:text-cream truncate">{{ asset.name }}</div>
<div class="text-xs text-stage-500 dark:text-cream/40 mt-0.5 font-mono">{{ asset.barcode }}</div>
Expand All @@ -223,6 +241,8 @@ <h3 class="font-medium text-stage-900 dark:text-cream">{{ child.name }}</h3>
{% if asset.department %}<span>{{ asset.department.name }}</span>{% endif %}
<span>{{ asset.get_condition_display }}</span>
</div>
</div>
</div>
</a>
{% endfor %}
</div>
Expand Down
8 changes: 8 additions & 0 deletions src/templates/assets/location_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ <h2 class="text-lg font-medium text-stage-800 dark:text-cream/90 border-b border
{{ form.is_active }}
<label for="{{ form.is_active.id_for_label }}" class="text-sm font-medium text-stage-600 dark:text-cream/70">Active</label>
</div>

<div class="flex items-center gap-3">
{{ form.is_checkable }}
<div>
<label for="{{ form.is_checkable.id_for_label }}" class="text-sm font-medium text-stage-600 dark:text-cream/70">Checkable</label>
<p class="text-xs text-stage-500 dark:text-cream/40 mt-0.5">Enable check-out/check-in for this location as a unit. Intended for portable containers (boxes, cases, flight cases).</p>
</div>
</div>
</div>

<div class="flex gap-3">
Expand Down