Skip to content

Commit 4c2c1ae

Browse files
author
CyberReady
committed
Add photo management features: implement quick add functionality, update InventoryService for photo retrieval, and modify MovingItem model to include photo property
1 parent 1304024 commit 4c2c1ae

5 files changed

Lines changed: 200 additions & 3 deletions

File tree

MoveIT/Components/Pages/Inventory.razor

Lines changed: 182 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
@rendermode InteractiveServer
33
@using MoveIT.Models
44
@using MoveIT.Services
5+
@using Microsoft.AspNetCore.Components.Forms
6+
@using SixLabors.ImageSharp
7+
@using SixLabors.ImageSharp.Processing
8+
@using SixLabors.ImageSharp.Formats.Jpeg
59
@inject InventoryService Svc
610

711
<PageTitle>Inventory — MoveIT</PageTitle>
812

913
<div class="d-flex justify-content-between align-items-center mb-3">
1014
<h1 class="h3 mb-0">Moving Inventory</h1>
11-
<button class="btn btn-primary" @onclick="OpenAdd">+ Add Item</button>
15+
<div class="d-flex gap-2">
16+
<button class="btn btn-success" @onclick="OpenQuickAdd">📸 Quick Add</button>
17+
<button class="btn btn-primary" @onclick="OpenAdd">+ Add Item</button>
18+
</div>
1219
</div>
1320

1421
<!-- Stats -->
@@ -119,6 +126,7 @@
119126
<table class="table table-hover align-middle">
120127
<thead class="table-light">
121128
<tr>
129+
<th style="width:72px"></th>
122130
<th>Name</th>
123131
<th>Category</th>
124132
<th>From → To</th>
@@ -136,14 +144,26 @@
136144
@if (!FilteredItems.Any())
137145
{
138146
<tr>
139-
<td colspan="11" class="text-center text-muted py-5">
140-
No items yet. Click <strong>+ Add Item</strong> to start tracking.
147+
<td colspan="12" class="text-center text-muted py-5">
148+
No items yet. Click <strong>+ Add Item</strong> or <strong>📸 Quick Add</strong> to start tracking.
141149
</td>
142150
</tr>
143151
}
144152
@foreach (var item in FilteredItems)
145153
{
146154
<tr>
155+
<td>
156+
@if (item.Photo is { Length: > 0 })
157+
{
158+
<img src="/api/photos/@item.Id" alt=""
159+
style="width:56px;height:56px;object-fit:cover;border-radius:6px;cursor:pointer"
160+
@onclick="() => OpenPhotoPreview(item)" />
161+
}
162+
else
163+
{
164+
<div class="text-muted small text-center" style="width:56px;height:56px;line-height:56px;background:#f3f4f6;border-radius:6px">—</div>
165+
}
166+
</td>
147167
<td class="fw-semibold">@item.Name</td>
148168
<td><span class="badge bg-secondary">@item.Category</span></td>
149169
<td class="small text-nowrap">
@@ -292,6 +312,28 @@
292312
}
293313
</div>
294314

315+
<div class="col-12">
316+
<label class="form-label fw-semibold">Photo</label>
317+
<div class="d-flex align-items-center gap-3">
318+
@if (_form.Photo is { Length: > 0 })
319+
{
320+
<img src="data:image/jpeg;base64,@Convert.ToBase64String(_form.Photo)" alt=""
321+
style="width:120px;height:120px;object-fit:cover;border-radius:8px" />
322+
}
323+
else
324+
{
325+
<div class="text-muted small text-center" style="width:120px;height:120px;line-height:120px;background:#f3f4f6;border-radius:8px">No photo</div>
326+
}
327+
<div class="d-flex flex-column gap-2">
328+
<InputFile OnChange="OnEditPhotoSelected" accept="image/*" capture="environment" class="form-control form-control-sm" />
329+
@if (_form.Photo is { Length: > 0 })
330+
{
331+
<button type="button" class="btn btn-outline-danger btn-sm" @onclick="ClearEditPhoto">Remove photo</button>
332+
}
333+
</div>
334+
</div>
335+
</div>
336+
295337
<div class="col-12">
296338
<label class="form-label fw-semibold">Notes</label>
297339
<textarea class="form-control" @bind="_form.Notes" rows="2"
@@ -317,6 +359,81 @@
317359
</div>
318360
}
319361

362+
<!-- Quick Add Modal -->
363+
@if (_showQuickAdd)
364+
{
365+
<div class="modal show d-block" tabindex="-1" style="background:rgba(0,0,0,0.5)">
366+
<div class="modal-dialog">
367+
<div class="modal-content">
368+
<div class="modal-header">
369+
<h5 class="modal-title">📸 Quick Add</h5>
370+
<button type="button" class="btn-close" @onclick="CloseQuickAdd"></button>
371+
</div>
372+
<div class="modal-body">
373+
<div class="mb-3 text-center">
374+
@if (_quickForm.Photo is { Length: > 0 })
375+
{
376+
<img src="data:image/jpeg;base64,@Convert.ToBase64String(_quickForm.Photo)" alt=""
377+
style="max-width:100%;max-height:280px;border-radius:8px" />
378+
}
379+
else
380+
{
381+
<div class="text-muted d-flex align-items-center justify-content-center"
382+
style="height:200px;background:#f3f4f6;border-radius:8px">
383+
Tap below to capture
384+
</div>
385+
}
386+
</div>
387+
<div class="mb-3">
388+
<InputFile OnChange="OnQuickPhotoSelected" accept="image/*" capture="environment"
389+
class="form-control form-control-lg" />
390+
</div>
391+
<div class="mb-3">
392+
<label class="form-label fw-semibold">Name</label>
393+
<input class="form-control form-control-lg" @bind="_quickForm.Name"
394+
placeholder="What is it?" @ref="_quickNameInput" />
395+
</div>
396+
<div class="mb-3">
397+
<label class="form-label fw-semibold">Room</label>
398+
<input class="form-control" @bind="_quickForm.OriginRoom" list="rooms-list-quick" placeholder="Origin room" />
399+
<datalist id="rooms-list-quick">
400+
@foreach (var r in _commonRooms)
401+
{
402+
<option value="@r" />
403+
}
404+
</datalist>
405+
</div>
406+
@if (_quickSavedCount > 0)
407+
{
408+
<div class="text-success small">✓ @_quickSavedCount saved this session</div>
409+
}
410+
</div>
411+
<div class="modal-footer">
412+
<button class="btn btn-secondary" @onclick="CloseQuickAdd">Done</button>
413+
<button class="btn btn-success" @onclick="QuickSaveAndNext"
414+
disabled="@string.IsNullOrWhiteSpace(_quickForm.Name)">
415+
Save &amp; Next
416+
</button>
417+
</div>
418+
</div>
419+
</div>
420+
</div>
421+
}
422+
423+
<!-- Photo Preview Modal -->
424+
@if (_photoPreview is not null)
425+
{
426+
<div class="modal show d-block" tabindex="-1" style="background:rgba(0,0,0,0.85)" @onclick="() => _photoPreview = null">
427+
<div class="modal-dialog modal-lg modal-dialog-centered">
428+
<div class="modal-content bg-transparent border-0">
429+
<img src="/api/photos/@_photoPreview.Id" alt=""
430+
style="max-width:100%;max-height:90vh;border-radius:8px" />
431+
<div class="text-center text-white mt-2">@_photoPreview.Name</div>
432+
</div>
433+
</div>
434+
</div>
435+
}
436+
320437
<!-- Delete Confirm Modal -->
321438
@if (_deleteTarget is not null)
322439
{
@@ -345,6 +462,13 @@
345462
private MovingItem _form = new();
346463
private MovingItem? _deleteTarget;
347464

465+
private bool _showQuickAdd;
466+
private MovingItem _quickForm = new();
467+
private int _quickSavedCount;
468+
private string _lastQuickRoom = "";
469+
private MovingItem? _photoPreview;
470+
private ElementReference _quickNameInput;
471+
348472
private string _search = "";
349473
private string _filterCategory = "";
350474
private string _filterRoom = "";
@@ -435,6 +559,61 @@
435559
_editingId = null;
436560
}
437561

562+
private void OpenQuickAdd()
563+
{
564+
_quickForm = new MovingItem { OriginRoom = _lastQuickRoom };
565+
_quickSavedCount = 0;
566+
_showQuickAdd = true;
567+
}
568+
569+
private void CloseQuickAdd()
570+
{
571+
_showQuickAdd = false;
572+
_quickForm = new();
573+
}
574+
575+
private async Task QuickSaveAndNext()
576+
{
577+
if (string.IsNullOrWhiteSpace(_quickForm.Name)) return;
578+
_lastQuickRoom = _quickForm.OriginRoom;
579+
Svc.Add(_quickForm);
580+
_quickSavedCount++;
581+
// Reset for next snap, keep room.
582+
_quickForm = new MovingItem { OriginRoom = _lastQuickRoom };
583+
}
584+
585+
private async Task OnQuickPhotoSelected(InputFileChangeEventArgs e)
586+
{
587+
var bytes = await ProcessPhotoAsync(e.File);
588+
if (bytes is not null) _quickForm.Photo = bytes;
589+
}
590+
591+
private async Task OnEditPhotoSelected(InputFileChangeEventArgs e)
592+
{
593+
var bytes = await ProcessPhotoAsync(e.File);
594+
if (bytes is not null) _form.Photo = bytes;
595+
}
596+
597+
private void ClearEditPhoto() => _form.Photo = null;
598+
599+
private void OpenPhotoPreview(MovingItem item) => _photoPreview = item;
600+
601+
// Read uploaded image, resize to max 1600px wide, re-encode as JPEG ~80%.
602+
private static async Task<byte[]?> ProcessPhotoAsync(IBrowserFile? file)
603+
{
604+
if (file is null) return null;
605+
const int maxBytes = 20 * 1024 * 1024;
606+
await using var input = file.OpenReadStream(maxBytes);
607+
using var image = await Image.LoadAsync(input);
608+
if (image.Width > 1600)
609+
{
610+
image.Mutate(x => x.Resize(1600, 0));
611+
}
612+
using var ms = new MemoryStream();
613+
await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = 80 });
614+
return ms.ToArray();
615+
}
616+
438617
private static string FormatMinutes(int? minutes)
439618
{
440619
if (!minutes.HasValue || minutes.Value <= 0) return "";

MoveIT/Models/MovingItem.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public class MovingItem
3030
public int? UnpackMinutes { get; set; }
3131
public int? SetupMinutes { get; set; }
3232

33+
[JsonIgnore]
34+
public byte[]? Photo { get; set; }
35+
3336
[JsonIgnore]
3437
public int? TotalMinutes => (UnpackMinutes.HasValue || SetupMinutes.HasValue)
3538
? (UnpackMinutes ?? 0) + (SetupMinutes ?? 0)

MoveIT/MoveIT.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
12+
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
1213
</ItemGroup>
1314

1415
</Project>

MoveIT/Program.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
using var db = factory.CreateDbContext();
2828
db.Database.EnsureCreated();
2929

30+
// Idempotent column migration for DBs created before Photo existed.
31+
try { db.Database.ExecuteSqlRaw("ALTER TABLE Items ADD COLUMN Photo BLOB NULL"); }
32+
catch { /* column already exists */ }
33+
3034
if (!db.Items.Any())
3135
{
3236
var jsonPath = Path.Combine(app.Environment.ContentRootPath, "inventory.json");
@@ -64,4 +68,12 @@
6468
app.MapRazorComponents<App>()
6569
.AddInteractiveServerRenderMode();
6670

71+
app.MapGet("/api/photos/{id:guid}", (Guid id, InventoryService svc) =>
72+
{
73+
var bytes = svc.GetPhoto(id);
74+
return bytes is null
75+
? Results.NotFound()
76+
: Results.File(bytes, "image/jpeg");
77+
});
78+
6779
app.Run();

MoveIT/Services/InventoryService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ public void Remove(Guid id)
5252
db.SaveChanges();
5353
_items.RemoveAll(x => x.Id == id);
5454
}
55+
56+
public byte[]? GetPhoto(Guid id) => _items.FirstOrDefault(x => x.Id == id)?.Photo;
5557
}

0 commit comments

Comments
 (0)