star-cloud/resources/views/admin/warehouses/inventory.blade.php
sky121113 315c226560 [FEAT] 倉儲管理系統模組實作與全站 UI/UX 標準化優化
1. 實作倉儲管理系統 (Warehouse Management):包含總覽、庫存、調撥、機台庫存與補貨模組。
2. 建立倉儲相關資料結構:新增 warehouses, warehouse_stocks, stock_in_orders, stock_movements, transfer_orders, replenishment_orders 等資料表。
3. 優化商品管理 (Product Management) 介面:調整商品與類別管理的分頁、搜尋與編輯邏輯,並導入極簡奢華風 UI。
4. 增強遠端機台管理 (Remote Machine Management):優化機台清單、庫存監控與遠端出貨介面,提升載入效能。
5. 更新全站導覽選單 (Sidebar):整合倉儲管理入口,並修復側邊欄語法錯誤。
6. 標準化分頁組件 (Pagination):套用 luxury 奢華風分頁樣式。
7. 補齊多語系語系檔 (i18n):更新 zh_TW.json,包含倉儲與商品模組相關詞彙。
2026-04-23 11:51:08 +08:00

276 lines
22 KiB
PHP

@extends('layouts.admin')
@section('content')
<div class="space-y-3 pb-20" x-data="{
activeTab: '{{ $tab }}',
showStockInModal: false,
items: [{ product_id: '', quantity: 1 }],
addItem() { this.items.push({ product_id: '', quantity: 1 }); },
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
}">
{{-- Header --}}
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Inventory Management') }}</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Track stock levels, stock-in orders, and movement history') }}</p>
</div>
<div x-show="activeTab === 'stock-in'">
<button @click="showStockInModal = true; items = [{ product_id: '', quantity: 1 }]" class="btn-luxury-primary">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
<span>{{ __('New Stock-In Order') }}</span>
</button>
</div>
</div>
{{-- Tab Navigation --}}
<div class="flex items-center p-1.5 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50 w-fit">
<a href="{{ route('admin.warehouses.inventory', ['tab' => 'stock']) }}"
class="px-6 py-2.5 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ $tab === 'stock' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
{{ __('Stock Levels') }}
</a>
<a href="{{ route('admin.warehouses.inventory', ['tab' => 'stock-in']) }}"
class="px-6 py-2.5 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ $tab === 'stock-in' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
{{ __('Stock-In Orders') }}
</a>
<a href="{{ route('admin.warehouses.inventory', ['tab' => 'movements']) }}"
class="px-6 py-2.5 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ $tab === 'movements' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
{{ __('Movement History') }}
</a>
</div>
{{-- Tab: Stock Levels --}}
@if($tab === 'stock')
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
<input type="hidden" name="tab" value="stock">
<div class="relative group">
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none"><svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></span>
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search products...') }}">
</div>
<div class="min-w-[200px]">
<select name="warehouse_id" class="luxury-select w-full" onchange="this.form.submit()">
<option value="">{{ __('All Warehouses') }}</option>
@foreach($warehouses as $wh)
<option value="{{ $wh->id }}" {{ request('warehouse_id') == $wh->id ? 'selected' : '' }}>{{ $wh->name }}</option>
@endforeach
</select>
</div>
</form>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Product') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Quantity') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Safety Stock') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($stocks as $stock)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-5">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center overflow-hidden">
@if($stock->product?->image_url)
<img src="{{ $stock->product->image_url }}" class="w-full h-full object-cover">
@else
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" /></svg>
@endif
</div>
<span class="font-extrabold text-slate-800 dark:text-slate-100">{{ $stock->product?->name ?? '-' }}</span>
</div>
</td>
<td class="px-6 py-5">
<span class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $stock->warehouse?->name ?? '-' }}</span>
@if($stock->warehouse?->type === 'main')
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-xs font-black bg-cyan-400/20 text-cyan-400 border border-cyan-400/30 uppercase tracking-widest ml-2 shadow-sm shadow-cyan-400/10">{{ __('Main') }}</span>
@endif
</td>
<td class="px-6 py-5 text-center">
<span class="text-lg font-black {{ $stock->quantity <= $stock->safety_stock && $stock->safety_stock > 0 ? 'text-rose-500' : 'text-slate-800 dark:text-white' }}">{{ $stock->quantity }}</span>
</td>
<td class="px-6 py-5 text-center">
<span class="text-sm font-bold text-slate-500">{{ $stock->safety_stock }}</span>
</td>
<td class="px-6 py-5 text-center">
@if($stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock)
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 tracking-widest uppercase">{{ __('Low Stock') }}</span>
@elseif($stock->quantity === 0)
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-slate-400/10 text-slate-400 border border-slate-400/20 tracking-widest uppercase">{{ __('Out of Stock') }}</span>
@else
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Normal') }}</span>
@endif
</td>
</tr>
@empty
<tr><td colspan="5" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No stock data found') }}</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $stocks->links('vendor.pagination.luxury') }}</div>
</div>
@endif
{{-- Tab: Stock-In Orders --}}
@if($tab === 'stock-in')
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Order No.') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Created By') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Created At') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($orders as $order)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-5"><span class="font-mono font-extrabold text-slate-800 dark:text-slate-100">{{ $order->order_no }}</span></td>
<td class="px-6 py-5"><span class="font-bold text-slate-600 dark:text-slate-300">{{ $order->warehouse?->name }}</span></td>
<td class="px-6 py-5 text-center">
@if($order->status === 'draft')
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 tracking-widest uppercase">{{ __('Draft') }}</span>
@else
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">{{ __('Completed') }}</span>
@endif
</td>
<td class="px-6 py-5"><span class="text-sm font-bold text-slate-500">{{ $order->creator?->name ?? '-' }}</span></td>
<td class="px-6 py-5"><span class="text-sm font-bold text-slate-500">{{ $order->created_at?->format('Y-m-d H:i') }}</span></td>
<td class="px-6 py-5 text-right">
@if($order->status === 'draft')
<form action="{{ route('admin.warehouses.inventory.stock-in.confirm', $order->id) }}" method="POST" class="inline" onsubmit="return confirm('{{ __('Confirm this stock-in order?') }}')">
@csrf @method('PATCH')
<button type="submit" class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all">{{ __('Confirm') }}</button>
</form>
@else
<span class="text-xs font-bold text-slate-400">{{ $order->completed_at?->format('Y-m-d H:i') }}</span>
@endif
</td>
</tr>
@empty
<tr><td colspan="6" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No stock-in orders found') }}</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $orders->links('vendor.pagination.luxury') }}</div>
</div>
@endif
{{-- Tab: Movement History --}}
@if($tab === 'movements')
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
<input type="hidden" name="tab" value="movements">
<select name="warehouse_id" class="luxury-select min-w-[200px]" onchange="this.form.submit()">
<option value="">{{ __('All Warehouses') }}</option>
@foreach($warehouses as $wh)
<option value="{{ $wh->id }}" {{ request('warehouse_id') == $wh->id ? 'selected' : '' }}>{{ $wh->name }}</option>
@endforeach
</select>
<select name="type" class="luxury-select min-w-[180px]" onchange="this.form.submit()">
<option value="">{{ __('All Types') }}</option>
@foreach(\App\Models\Warehouse\StockMovement::TYPE_LABELS as $key => $label)
<option value="{{ $key }}" {{ request('type') === $key ? 'selected' : '' }}>{{ __($label) }}</option>
@endforeach
</select>
</form>
<div class="overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-y-0">
<thead>
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Time') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Product') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Type') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Qty Change') }}</th>
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Operator') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
@forelse($movements as $mv)
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
<td class="px-6 py-4 text-sm font-bold text-slate-500">{{ $mv->created_at?->format('m-d H:i') }}</td>
<td class="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{{ $mv->warehouse?->name }}</td>
<td class="px-6 py-4 font-bold text-slate-700 dark:text-slate-200">{{ $mv->product?->name }}</td>
<td class="px-6 py-4 text-center">
@php $typeColor = in_array($mv->type, ['in', 'transfer_in']) ? 'emerald' : (in_array($mv->type, ['out', 'transfer_out']) ? 'rose' : 'amber'); @endphp
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-{{ $typeColor }}-500/10 text-{{ $typeColor }}-500 border border-{{ $typeColor }}-500/20 tracking-widest uppercase">{{ __(\App\Models\Warehouse\StockMovement::TYPE_LABELS[$mv->type] ?? $mv->type) }}</span>
</td>
<td class="px-6 py-4 text-center font-mono font-extrabold {{ in_array($mv->type, ['in', 'transfer_in']) ? 'text-emerald-500' : 'text-rose-500' }}">
{{ in_array($mv->type, ['in', 'transfer_in']) ? '+' : '-' }}{{ $mv->quantity }}
<span class="text-xs font-bold text-slate-400 ml-1">({{ $mv->before_qty }}{{ $mv->after_qty }})</span>
</td>
<td class="px-6 py-4 text-sm font-bold text-slate-500">{{ $mv->creator?->name ?? '-' }}</td>
</tr>
@empty
<tr><td colspan="6" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No movement records found') }}</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $movements->links('vendor.pagination.luxury') }}</div>
</div>
@endif
{{-- Stock-In Modal --}}
<div x-show="showStockInModal" class="fixed inset-0 z-[100] overflow-y-auto" x-cloak>
<div class="flex items-center justify-center min-h-screen px-4">
<div x-show="showStockInModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" @click="showStockInModal = false"></div>
<div x-show="showStockInModal" x-transition class="relative inline-block px-8 py-10 luxury-card rounded-3xl shadow-2xl sm:max-w-2xl w-full z-10">
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight mb-8">{{ __('New Stock-In Order') }}</h3>
<form action="{{ route('admin.warehouses.inventory.stock-in.store') }}" method="POST" class="space-y-6">
@csrf
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Target Warehouse') }}</label>
<select name="warehouse_id" required class="luxury-select w-full">
<option value="">{{ __('Select Warehouse') }}</option>
@foreach($warehouses ?? [] as $wh)
<option value="{{ $wh->id }}">{{ $wh->name }}</option>
@endforeach
</select>
</div>
<div class="space-y-2">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Note') }}</label>
<input type="text" name="note" class="luxury-input w-full" placeholder="{{ __('Optional') }}">
</div>
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Items') }}</label>
<button type="button" @click="addItem()" class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest">+ {{ __('Add Item') }}</button>
</div>
<template x-for="(item, index) in items" :key="index">
<div class="flex items-center gap-3 p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
<select :name="'items['+index+'][product_id]'" x-model="item.product_id" required class="luxury-select flex-1">
<option value="">{{ __('Select Product') }}</option>
@foreach($products ?? [] as $p)
<option value="{{ $p->id }}">{{ $p->name }}</option>
@endforeach
</select>
<input type="number" :name="'items['+index+'][quantity]'" x-model="item.quantity" min="1" required class="luxury-input w-24 text-center" placeholder="{{ __('Qty') }}">
<button type="button" @click="removeItem(index)" class="p-2 text-slate-400 hover:text-rose-500 transition-colors" x-show="items.length > 1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>
</button>
</div>
</template>
</div>
<div class="flex justify-end gap-4 pt-6">
<button type="button" @click="showStockInModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
<button type="submit" class="btn-luxury-primary px-12">{{ __('Create') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection