diff --git a/app/Http/Controllers/Admin/Master/ProductController.php b/app/Http/Controllers/Admin/Master/ProductController.php index 0dbb005..b573681 100644 --- a/app/Http/Controllers/Admin/Master/ProductController.php +++ b/app/Http/Controllers/Admin/Master/ProductController.php @@ -32,6 +32,7 @@ public function index(Request $request): Response return Inertia::render('admin/master/products/Index', [ 'products' => $this->productService->paginateForIndex($tableQuery, $isActive), + 'outOfStockGroups' => $this->productService->outOfStockGroups(), 'filters' => $this->dataTableFilters($tableQuery, [ 'is_active' => $isActive, ]), diff --git a/app/Http/Controllers/Admin/Master/RawMaterialController.php b/app/Http/Controllers/Admin/Master/RawMaterialController.php index 81ea21e..ab7772f 100644 --- a/app/Http/Controllers/Admin/Master/RawMaterialController.php +++ b/app/Http/Controllers/Admin/Master/RawMaterialController.php @@ -32,6 +32,7 @@ public function index(Request $request): Response return Inertia::render('admin/master/raw-materials/Index', [ 'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive), + 'outOfStockGroups' => $this->rawMaterialService->outOfStockGroups(), 'filters' => $this->dataTableFilters($tableQuery, [ 'is_active' => $isActive, ]), diff --git a/app/Services/Master/ProductService.php b/app/Services/Master/ProductService.php index eb58f93..eef5af6 100644 --- a/app/Services/Master/ProductService.php +++ b/app/Services/Master/ProductService.php @@ -20,6 +20,52 @@ public function __construct( private readonly MediaService $mediaService, ) {} + /** + * @return list>, + * }>, + * }> + */ + public function outOfStockGroups(): array + { + return ProductVariant::query() + ->with(['product', 'media']) + ->where('stock', '<=', 0) + ->whereHas('product', fn (Builder $query) => $query->where('is_active', true)) + ->get() + ->sortBy([ + fn (ProductVariant $variant) => $variant->product->name, + fn (ProductVariant $variant) => $variant->name, + ]) + ->groupBy('product_id') + ->map(function ($variants, $productId) { + $product = $variants->first()->product; + + return [ + 'id' => (int) $productId, + 'name' => $product->name, + 'edit_url' => route('admin.master.products.edit', $productId), + 'variants' => $variants->map(function (ProductVariant $variant): array { + return [ + 'id' => $variant->id, + 'name' => $variant->name, + 'stock_formatted' => number_format($variant->stock, 0, ',', '.').' pcs', + 'images' => MediaPresenter::collection($variant, 'images'), + ]; + })->values()->all(), + ]; + }) + ->values() + ->all(); + } + /** * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery */ diff --git a/app/Services/Master/RawMaterialService.php b/app/Services/Master/RawMaterialService.php index 2c76fb5..f6b32b4 100644 --- a/app/Services/Master/RawMaterialService.php +++ b/app/Services/Master/RawMaterialService.php @@ -18,6 +18,64 @@ public function __construct( private readonly MediaService $mediaService, ) {} + /** + * @return list>, + * }>, + * }> + */ + public function outOfStockGroups(): array + { + return RawMaterialPrice::query() + ->with(['rawMaterial', 'media']) + ->where('stock', '<=', 0) + ->get() + ->sortBy([ + fn (RawMaterialPrice $price) => $price->rawMaterial->name, + fn (RawMaterialPrice $price) => $price->variant, + ]) + ->groupBy('raw_material_id') + ->map(function ($prices, $rawMaterialId) { + $material = $prices->first()->rawMaterial; + + return [ + 'id' => (int) $rawMaterialId, + 'name' => $material->name, + 'unit_label' => $material->unit->label(), + 'edit_url' => route('admin.master.raw-materials.edit', $rawMaterialId), + 'variants' => $prices->map(function (RawMaterialPrice $price) use ($material): array { + return [ + 'id' => $price->id, + 'name' => $price->variant, + 'stock_formatted' => $this->formatStockAmount((float) $price->stock, $material->unit->abbreviation()), + 'images' => MediaPresenter::collection($price, 'images'), + ]; + })->values()->all(), + ]; + }) + ->values() + ->all(); + } + + private function formatStockAmount(float $amount, ?string $unit = null): string + { + $formatted = rtrim(rtrim(number_format($amount, 4, ',', '.'), '0'), ','); + + if ($unit === null) { + return $formatted; + } + + return "{$formatted} {$unit}"; + } + /** * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery */ diff --git a/package-lock.json b/package-lock.json index 60ce9b8..697bc31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@fullcalendar/vue3": "^6.1.20", "@inertiajs/vite": "^3.0.0", "@inertiajs/vue3": "^3.0.0", - "@lucide/vue": "^1.17.0", + "@lucide/vue": "^1.18.0", "@point-of-sale/receipt-printer-encoder": "^3.0.3", "@point-of-sale/webbluetooth-receipt-printer": "^2.0.0", "@point-of-sale/webserial-receipt-printer": "^2.0.0", @@ -2618,9 +2618,9 @@ "license": "MIT" }, "node_modules/@lucide/vue": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.17.0.tgz", - "integrity": "sha512-6Q1ZHgr5FbmJzKWe5BxlNdjLj2lbmuH1zwDtVzUJofX0w9UREwKgq4F4jwKqFYyyIS4Rj3FiJvDi2k6djukmmw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.18.0.tgz", + "integrity": "sha512-DmnUpDB85PlMZ+ofjZLcKq3JoJnaD1bk7SIj9xwUvqerfNqA6hCLa0/m3gIybH6rdrErABbqvTD8yYJdNqiZ3Q==", "license": "ISC", "peerDependencies": { "vue": ">=3.0.1" diff --git a/package.json b/package.json index 177df18..b6ec731 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@fullcalendar/vue3": "^6.1.20", "@inertiajs/vite": "^3.0.0", "@inertiajs/vue3": "^3.0.0", - "@lucide/vue": "^1.17.0", + "@lucide/vue": "^1.18.0", "@point-of-sale/receipt-printer-encoder": "^3.0.3", "@point-of-sale/webbluetooth-receipt-printer": "^2.0.0", "@point-of-sale/webserial-receipt-printer": "^2.0.0", diff --git a/resources/js/components/admin/manage/PosCatalogCard.vue b/resources/js/components/admin/manage/PosCatalogCard.vue index d48888a..b023911 100644 --- a/resources/js/components/admin/manage/PosCatalogCard.vue +++ b/resources/js/components/admin/manage/PosCatalogCard.vue @@ -2,15 +2,18 @@ import { Package } from '@lucide/vue'; import type { MediaItem } from '@/types/media'; -defineProps<{ +withDefaults(defineProps<{ title: string; coverImage?: MediaItem; -}>(); + showCover?: boolean; +}>(), { + showCover: true, +});