diff --git a/.agents/skills/ui-minimal-luxury/SKILL.md b/.agents/skills/ui-minimal-luxury/SKILL.md index 1801eee..c59a849 100644 --- a/.agents/skills/ui-minimal-luxury/SKILL.md +++ b/.agents/skills/ui-minimal-luxury/SKILL.md @@ -328,4 +328,149 @@ description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範 --- > [!TIP] -> 當遇到未定義的 UI 區塊時,優先參考 `admin.dashboard.blade.php` 的卡片與即時動態實作方式進行衍生。 \ No newline at end of file +> 當遇到未定義的 UI 區塊時,優先參考 `admin.dashboard.blade.php` 的卡片與即時動態實作方式進行衍生。 + +## 13. 加載指示器規範 (Loading Indicator Pattern) + +當使用 AJAX 進行局部頁面刷新或異步操作時,必須使用統一的「極奢華加載遮罩」。 + +### 13.1 標準 HTML 結構 +```html + +
+ +
+ +
+ +
+ +
+ + + +
+
+

{{ __('Loading Data') }}...

+
+``` + +### 13.2 設計要點 +1. **多重動畫**:結合 `animate-spin` (不同速度/方向) 與 `animate-pulse`,營造出高科技且細膩的層次感。 +2. **顏色規範**:主要使用 `cyan-500` 作為亮點,搭配 `slate` 系列的半透明背景。 +3. **字體細節**:文字使用極細小尺寸 (`text-[10px]`)、極粗字重 (`font-black`) 並大幅度拉開字距 (`tracking-[0.4em]`)。 +4. **背景質感**:使用 `backdrop-blur-[1px]` 與 `bg-white/40` (或 `dark:bg-slate-900/40`),確保遮罩具有玻璃質感但不完全阻斷視線。 +5. **防止閃爍**: 必須加上 `x-cloak` 配合對應的 CSS 樣式。 + +## 14. 頁面佈局與頁籤間距規範 (Page Layout & Tab Spacing) + +為了確保管理後台全站視覺的「高度整合感」與「緊湊性」,頁面頂部結構與頁籤導覽必須遵循以下間距規範。 + +### 14.1 核心佈局間距 (Core Spacing Scale) +- **根容器間距 (Root Space)**: 頂部 Header、Tabs 與內容區域之間的垂直間距統一使用 **`space-y-2`**。 +- **標題與描述 (Title & Subtitle)**: 描述文字應緊貼標題,使用 `mt-1`。 +- **頁籤外層間距 (Tabs Outer)**: 頁籤容器與 Header 之間應由根容器的 `space-y-2` 自然推開,不額外加 `mt`。 + +### 14.2 頁籤導覽樣式 (Tab Navigation Styles) +- **容器內距**: 使用 `p-1.5` 的圓角膠囊背景。 +- **按鈕內距**: 頁籤按鈕統一使用 **`px-8 py-3`**。 +- **內容間隙**: 頁籤下方內容區與頁籤列之間,建議不額外添加 `mt-6` 等大間距。若內容區域內部需要層次感,使用 `space-y-3` 即可。 + +### 14.3 實作範例結構 (Reference Structure) +```html +
+ +
+
+

標題

+

描述文字

+
+
+ + +
+ + +
+ + +
+ +
+
+``` + +### 14.4 統計卡片間距 (Stats Grid Spacing) +- **網格間距**: 頂部統計卡片網格 (Grid) 建議使用 **`gap-4`** (16px),避免使用過大的 `gap-6` 或 `gap-8` 導致版面鬆散。 +- **卡片內距**: 統計卡片內部統一使用 `p-6`。 + +### 15. Premium AJAX Interactions & Loaders + +To maintain a seamless "SPA-like" experience without full-page reloads, all administrative modules must follow these AJAX patterns. + +#### 15.1 The Premium Multi-Ring Loader +Used for table overlays and state transitions. It features a dual-spin ring with a pulsing core icon. + +```html + +
+ +
+ +
+ +
+ +
+ + + +
+
+

Syncing Data...

+
+``` + +#### 15.2 Standard fetchPage Pattern +All lists should implement the following `fetchPage` method in their `x-data` and wrap the dynamic content in `#ajax-content-container`. + +```javascript +async fetchPage(url) { + if (!url || this.isLoadingTable) return; + this.isLoadingTable = true; + try { + const res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); + const html = await res.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const newContent = doc.querySelector('#ajax-content-container'); + if (newContent) { + document.querySelector('#ajax-content-container').innerHTML = newContent.innerHTML; + window.history.pushState({}, '', url); + } + } catch (e) { + console.error('AJAX Load Failed:', e); + } finally { + this.isLoadingTable = false; + } +} +``` + +#### 15.3 Triggering AJAX Navigation +- **Forms**: Use `@submit.prevent="fetchPage($el.action + '?' + new URLSearchParams(new FormData($el)).toString())"`. +- **Selects**: Use `onchange="this.dispatchEvent(new CustomEvent('ajax:navigate', { detail: { url: ... }, bubbles: true }))"`. +- **Links/Pagination**: Use a delegated `@click` listener on the container or `@ajax:navigate.prevent.stop`. \ No newline at end of file diff --git a/app/Console/Commands/MqttSyncAuth.php b/app/Console/Commands/MqttSyncAuth.php index 28bf72e..fbafb52 100644 --- a/app/Console/Commands/MqttSyncAuth.php +++ b/app/Console/Commands/MqttSyncAuth.php @@ -34,8 +34,8 @@ class MqttSyncAuth extends Command \Illuminate\Support\Facades\Redis::hSet($gatewayKey, 'password', $gatewayPass); $this->info("Gateway auth synced."); - // 2. 同步所有機台的認證資料 - $machines = Machine::withoutGlobalScopes()->get(); + // 2. 同步所有機台的認證資料 (僅限未刪除的) + $machines = Machine::get(); $this->info("Syncing " . $machines->count() . " machines to Redis..."); $bar = $this->output->createProgressBar($machines->count()); diff --git a/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php b/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php index 726cfb3..8295302 100644 --- a/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php +++ b/app/Http/Controllers/Admin/BasicSettings/PaymentConfigController.php @@ -81,11 +81,13 @@ class PaymentConfigController extends AdminController { $request->validate([ 'name' => 'required|string|max:255', + 'company_id' => 'required|exists:companies,id', 'settings' => 'required|array', ]); $paymentConfig->update([ 'name' => $request->name, + 'company_id' => $request->company_id, 'settings' => $request->settings, 'updater_id' => auth()->id(), ]); diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php index aba2016..0a5389f 100644 --- a/app/Http/Controllers/Admin/CompanyController.php +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -178,6 +178,13 @@ class CompanyController extends Controller if (isset($validated['settings'])) { $validated['settings']['enable_material_code'] = filter_var($validated['settings']['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN); $validated['settings']['enable_points'] = filter_var($validated['settings']['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN); + $validated['settings']['tax_invoice'] = filter_var($validated['settings']['tax_invoice'] ?? false, FILTER_VALIDATE_BOOLEAN); + $validated['settings']['card_terminal'] = filter_var($validated['settings']['card_terminal'] ?? false, FILTER_VALIDATE_BOOLEAN); + $validated['settings']['scan_pay_esun'] = filter_var($validated['settings']['scan_pay_esun'] ?? false, FILTER_VALIDATE_BOOLEAN); + $validated['settings']['scan_pay_linepay'] = filter_var($validated['settings']['scan_pay_linepay'] ?? false, FILTER_VALIDATE_BOOLEAN); + $validated['settings']['shopping_cart'] = filter_var($validated['settings']['shopping_cart'] ?? false, FILTER_VALIDATE_BOOLEAN); + $validated['settings']['welcome_gift'] = filter_var($validated['settings']['welcome_gift'] ?? false, FILTER_VALIDATE_BOOLEAN); + $validated['settings']['cash_module'] = filter_var($validated['settings']['cash_module'] ?? false, FILTER_VALIDATE_BOOLEAN); } DB::transaction(function () use ($validated, $company) { @@ -205,6 +212,33 @@ class CompanyController extends Controller return redirect()->back()->with('success', __('Customer updated successfully.')); } + /** + * 更新客戶的系統設定 + */ + public function updateSettings(Request $request, Company $company) + { + $settings = $request->input('settings', []); + + $formattedSettings = [ + 'enable_material_code' => filter_var($settings['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'enable_points' => filter_var($settings['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'tax_invoice' => filter_var($settings['tax_invoice'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'card_terminal' => filter_var($settings['card_terminal'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'scan_pay_esun' => filter_var($settings['scan_pay_esun'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'scan_pay_linepay' => filter_var($settings['scan_pay_linepay'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'shopping_cart' => filter_var($settings['shopping_cart'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'welcome_gift' => filter_var($settings['welcome_gift'] ?? false, FILTER_VALIDATE_BOOLEAN), + 'cash_module' => filter_var($settings['cash_module'] ?? false, FILTER_VALIDATE_BOOLEAN), + ]; + + // Ensure we force save the JSON cast properly + $company->settings = $formattedSettings; + $company->save(); + + return redirect()->to(route('admin.permission.companies.index') . '?tab=settings') + ->with('success', __('System settings updated successfully.')); + } + /** * 切換客戶狀態 */ diff --git a/app/Http/Controllers/Admin/Machine/MachinePermissionController.php b/app/Http/Controllers/Admin/Machine/MachinePermissionController.php index a9eec69..bdc80b3 100644 --- a/app/Http/Controllers/Admin/Machine/MachinePermissionController.php +++ b/app/Http/Controllers/Admin/Machine/MachinePermissionController.php @@ -111,10 +111,13 @@ class MachinePermissionController extends AdminController } $user->machines()->sync($request->machine_ids ?? []); + + $message = __('Machine permissions updated successfully.'); + session()->flash('success', $message); return response()->json([ 'success' => true, - 'message' => __('Permissions updated successfully'), + 'message' => $message, 'assigned_machines' => $user->machines()->select('machines.id', 'machines.name', 'machines.serial_no')->get() ]); } diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index d8b9633..879b801 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -265,7 +265,10 @@ class ProductController extends Controller DB::beginTransaction(); $dictKey = $product->name_dictionary_key ?: \Illuminate\Support\Str::uuid()->toString(); - $company_id = $product->company_id; + // Determine company_id: prioritized from request (for sys admin) then from product's current company + $company_id = (auth()->user()->isSystemAdmin() && $request->filled('company_id')) + ? $request->company_id + : $product->company_id; // 更新或建立多語系翻譯(繞過 TenantScoped,避免系統管理員操作租戶資料時被過濾) foreach ($request->names as $locale => $name) { @@ -292,6 +295,7 @@ class ProductController extends Controller } $data = [ + 'company_id' => $company_id, 'category_id' => $request->category_id, 'name' => $request->names['zh_TW'] ?? ($product->name ?? 'Untitled'), 'name_dictionary_key' => $dictKey, diff --git a/app/Http/Controllers/Admin/WarehouseController.php b/app/Http/Controllers/Admin/WarehouseController.php index 3f91ec4..6563c84 100644 --- a/app/Http/Controllers/Admin/WarehouseController.php +++ b/app/Http/Controllers/Admin/WarehouseController.php @@ -3,137 +3,507 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Models\Machine\Machine; +use App\Models\Machine\MachineSlot; +use App\Models\Product\Product; +use App\Models\Warehouse\ReplenishmentOrder; +use App\Models\Warehouse\ReplenishmentOrderItem; +use App\Models\Warehouse\StockInOrder; +use App\Models\Warehouse\StockInOrderItem; +use App\Models\Warehouse\StockMovement; +use App\Models\Warehouse\TransferOrder; +use App\Models\Warehouse\TransferOrderItem; +use App\Models\Warehouse\Warehouse; +use App\Models\Warehouse\WarehouseStock; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; class WarehouseController extends Controller { - // 倉庫列表(全部) - public function index() + // ─── 模組 1:倉庫總覽 ─── + + public function index(Request $request) { - return view('admin.placeholder', [ - 'title' => '倉庫列表(全部)', - 'description' => '顯示所有倉庫的資訊與庫存狀態', - 'features' => [ - '查看所有倉庫列表', - '即時庫存數量顯示', - '倉庫狀態監控', - '快速搜尋與篩選', - ] - ]); + $query = Warehouse::query() + ->withCount('stocks as products_count') + ->withSum('stocks as total_stock', 'quantity'); + + if ($search = $request->input('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('address', 'like', "%{$search}%"); + }); + } + + if ($request->filled('type')) { + $query->where('type', $request->input('type')); + } + + $warehouses = $query->orderBy('type')->orderBy('name') + ->paginate($request->input('per_page', 10)) + ->withQueryString(); + + $stats = [ + 'total' => Warehouse::count(), + 'main' => Warehouse::main()->count(), + 'branch' => Warehouse::branch()->count(), + 'low_stock' => WarehouseStock::lowStock() + ->whereHas('warehouse', fn($q) => $q)->count(), + ]; + + return view('admin.warehouses.index', compact('warehouses', 'stats')); } - // 倉庫列表(個人) - public function personal() + public function store(Request $request) { - return view('admin.placeholder', [ - 'title' => '倉庫列表(個人)', - 'description' => '顯示個人負責的倉庫資訊', - 'features' => [ - '查看個人負責倉庫', - '個人庫存管理', - '權限範圍內的操作', - ] + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'type' => 'required|in:main,branch', + 'address' => 'nullable|string|max:500', ]); + $validated['company_id'] = Auth::user()->company_id; + Warehouse::create($validated); + + return redirect()->route('admin.warehouses.index') + ->with('success', __('Warehouse created successfully')); } - // 庫存管理單 - public function stockManagement() + public function update(Request $request, Warehouse $warehouse) { - return view('admin.placeholder', [ - 'title' => '庫存管理單', - 'description' => '倉庫庫存異動管理', - 'features' => [ - '庫存盤點', - '庫存調整', - '異動記錄查詢', - ] + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'type' => 'required|in:main,branch', + 'address' => 'nullable|string|max:500', + 'is_active' => 'required|boolean', ]); + $warehouse->update($validated); + + return redirect()->route('admin.warehouses.index') + ->with('success', __('Warehouse updated successfully')); } - // 調撥單 - public function transfers() + public function destroy(Warehouse $warehouse) { - return view('admin.placeholder', [ - 'title' => '調撥單', - 'description' => '倉庫間商品調撥作業', - 'features' => [ - '建立調撥單', - '調撥單審核', - '調撥歷史記錄', - ] - ]); + if ($warehouse->stocks()->where('quantity', '>', 0)->exists()) { + return redirect()->route('admin.warehouses.index') + ->with('error', __('Cannot delete warehouse with existing stock')); + } + $warehouse->delete(); + + return redirect()->route('admin.warehouses.index') + ->with('success', __('Warehouse deleted successfully')); } - // 採購單 - public function purchases() + public function toggleStatus(Warehouse $warehouse) { - return view('admin.placeholder', [ - 'title' => '採購單', - 'description' => '商品採購申請與管理', - 'features' => [ - '建立採購申請', - '採購單追蹤', - '供應商管理', - ] - ]); + $warehouse->update(['is_active' => !$warehouse->is_active]); + $status = $warehouse->is_active ? __('enabled') : __('disabled'); + + return redirect()->route('admin.warehouses.index') + ->with('success', __('Warehouse :status successfully', ['status' => $status])); } - // 機台補貨單 - public function replenishments() + // ─── 模組 2:庫存管理 ─── + + public function inventory(Request $request) { - return view('admin.placeholder', [ - 'title' => '機台補貨單', - 'description' => '機台補貨工單建立與管理', - 'features' => [ - '建立補貨單', - '補貨排程', - '補貨人員指派', - ] - ]); + $tab = $request->input('tab', 'stock'); + $warehouses = Warehouse::active()->orderBy('name')->get(); + + $data = compact('warehouses', 'tab'); + + if ($tab === 'stock') { + $query = WarehouseStock::with(['warehouse:id,name,type', 'product:id,name,image_url']) + ->whereHas('warehouse', fn($q) => $q); + + if ($request->filled('warehouse_id')) { + $query->where('warehouse_id', $request->input('warehouse_id')); + } + if ($search = $request->input('search')) { + $query->whereHas('product', fn($q) => $q->where('name', 'like', "%{$search}%")); + } + + $data['stocks'] = $query->orderBy('warehouse_id') + ->paginate($request->input('per_page', 15)) + ->withQueryString(); + + } elseif ($tab === 'stock-in') { + $query = StockInOrder::with(['warehouse:id,name', 'creator:id,name']) + ->latest(); + + if ($request->filled('status')) { + $query->where('status', $request->input('status')); + } + $data['orders'] = $query->paginate(10)->withQueryString(); + $data['products'] = Product::orderBy('name')->get(['id', 'name', 'image_url']); + + } elseif ($tab === 'movements') { + $query = StockMovement::with(['warehouse:id,name', 'product:id,name', 'creator:id,name']) + ->orderBy('created_at', 'desc'); + + if ($request->filled('warehouse_id')) { + $query->where('warehouse_id', $request->input('warehouse_id')); + } + if ($request->filled('type')) { + $query->where('type', $request->input('type')); + } + $data['movements'] = $query->paginate(15)->withQueryString(); + } + + return view('admin.warehouses.inventory', $data); } - // 機台補貨紀錄 - public function replenishmentRecords() + /** + * 建立入庫單 + */ + public function storeStockIn(Request $request) { - return view('admin.placeholder', [ - 'title' => '機台補貨紀錄', - 'description' => '個別機台的補貨歷史記錄', + $validated = $request->validate([ + 'warehouse_id' => 'required|exists:warehouses,id', + 'note' => 'nullable|string|max:500', + 'items' => 'required|array|min:1', + 'items.*.product_id' => 'required|exists:products,id', + 'items.*.quantity' => 'required|integer|min:1', ]); + + DB::transaction(function () use ($validated) { + $order = StockInOrder::create([ + 'company_id' => Auth::user()->company_id, + 'warehouse_id' => $validated['warehouse_id'], + 'order_no' => 'SI-' . now()->format('Ymd') . '-' . str_pad(StockInOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT), + 'status' => 'draft', + 'note' => $validated['note'] ?? null, + 'created_by' => Auth::id(), + ]); + + foreach ($validated['items'] as $item) { + $order->items()->create($item); + } + }); + + return redirect()->route('admin.warehouses.inventory', ['tab' => 'stock-in']) + ->with('success', __('Stock-in order created')); } - // 機台補貨紀錄(總) - public function replenishmentRecordsAll() + /** + * 確認入庫單(草稿 → 完成,同時增加庫存) + */ + public function confirmStockIn(StockInOrder $stockInOrder) { - return view('admin.placeholder', [ - 'title' => '機台補貨紀錄(總)', - 'description' => '所有機台的補貨總覽', - ]); + if ($stockInOrder->status !== 'draft') { + return back()->with('error', __('Order already processed')); + } + + DB::transaction(function () use ($stockInOrder) { + $stockInOrder->update([ + 'status' => 'completed', + 'completed_at' => now(), + ]); + + foreach ($stockInOrder->items as $item) { + // 更新或建立倉庫庫存 + $stock = WarehouseStock::firstOrCreate( + ['warehouse_id' => $stockInOrder->warehouse_id, 'product_id' => $item->product_id], + ['quantity' => 0, 'safety_stock' => 0] + ); + + $before = $stock->quantity; + $stock->increment('quantity', $item->quantity); + + // 記錄異動 + StockMovement::create([ + 'company_id' => $stockInOrder->company_id, + 'warehouse_id' => $stockInOrder->warehouse_id, + 'product_id' => $item->product_id, + 'type' => 'in', + 'quantity' => $item->quantity, + 'before_qty' => $before, + 'after_qty' => $before + $item->quantity, + 'reference_type' => StockInOrder::class, + 'reference_id' => $stockInOrder->id, + 'note' => __('Stock-in order') . ' #' . $stockInOrder->order_no, + 'created_by' => Auth::id(), + 'created_at' => now(), + ]); + } + }); + + return back()->with('success', __('Stock-in order confirmed and inventory updated')); } - // 機台庫存 - public function machineStock() + // ─── 模組 3:調撥單 ─── + + public function transfers(Request $request) { - return view('admin.placeholder', [ - 'title' => '機台庫存', - 'description' => '各機台即時庫存查詢', - ]); + $query = TransferOrder::with(['fromWarehouse:id,name', 'toWarehouse:id,name', 'fromMachine:id,name,serial_no', 'creator:id,name', 'items']) + ->latest(); + + if ($request->filled('type')) { + $query->where('type', $request->input('type')); + } + if ($request->filled('status')) { + $query->where('status', $request->input('status')); + } + + $orders = $query->paginate(10)->withQueryString(); + $warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name', 'type']); + $machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']); + $products = Product::orderBy('name')->get(['id', 'name', 'image_url']); + + return view('admin.warehouses.transfers', compact('orders', 'warehouses', 'machines', 'products')); } - // 人員庫存 - public function staffStock() + /** + * 建立調撥單(W2W 或 M2W) + */ + public function storeTransfer(Request $request) { - return view('admin.placeholder', [ - 'title' => '人員庫存', - 'description' => '補貨人員持有庫存', + $validated = $request->validate([ + 'type' => 'required|in:warehouse_to_warehouse,machine_to_warehouse', + 'from_warehouse_id' => 'required_if:type,warehouse_to_warehouse|nullable|exists:warehouses,id', + 'from_machine_id' => 'required_if:type,machine_to_warehouse|nullable|exists:machines,id', + 'to_warehouse_id' => 'required|exists:warehouses,id', + 'note' => 'nullable|string|max:500', + 'items' => 'required|array|min:1', + 'items.*.product_id' => 'required|exists:products,id', + 'items.*.quantity' => 'required|integer|min:1', ]); + + DB::transaction(function () use ($validated) { + $order = TransferOrder::create([ + 'company_id' => Auth::user()->company_id, + 'order_no' => 'TR-' . now()->format('Ymd') . '-' . str_pad(TransferOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT), + 'type' => $validated['type'], + 'from_warehouse_id' => $validated['from_warehouse_id'] ?? null, + 'from_machine_id' => $validated['from_machine_id'] ?? null, + 'to_warehouse_id' => $validated['to_warehouse_id'], + 'status' => TransferOrder::STATUS_DRAFT, + 'note' => $validated['note'] ?? null, + 'created_by' => Auth::id(), + ]); + + foreach ($validated['items'] as $item) { + $order->items()->create($item); + } + }); + + return redirect()->route('admin.warehouses.transfers') + ->with('success', __('Transfer order created')); } - // 回庫單 - public function returns() + /** + * 確認調撥(扣除來源庫存 + 增加目標庫存) + */ + public function confirmTransfer(TransferOrder $transferOrder) { - return view('admin.placeholder', [ - 'title' => '回庫單', - 'description' => '商品退回倉庫管理', + if ($transferOrder->status !== TransferOrder::STATUS_DRAFT) { + return back()->with('error', __('Order already processed')); + } + + DB::transaction(function () use ($transferOrder) { + $transferOrder->loadMissing('items'); + + foreach ($transferOrder->items as $item) { + // 扣除來源庫存(僅 W2W) + if ($transferOrder->type === TransferOrder::TYPE_W2W && $transferOrder->from_warehouse_id) { + $fromStock = WarehouseStock::where('warehouse_id', $transferOrder->from_warehouse_id) + ->where('product_id', $item->product_id)->first(); + + if (!$fromStock || $fromStock->quantity < $item->quantity) { + throw new \Exception(__('Insufficient stock for transfer')); + } + + $beforeFrom = $fromStock->quantity; + $fromStock->decrement('quantity', $item->quantity); + + StockMovement::create([ + 'company_id' => $transferOrder->company_id, + 'warehouse_id' => $transferOrder->from_warehouse_id, + 'product_id' => $item->product_id, + 'type' => 'transfer_out', + 'quantity' => $item->quantity, + 'before_qty' => $beforeFrom, + 'after_qty' => $beforeFrom - $item->quantity, + 'reference_type' => TransferOrder::class, + 'reference_id' => $transferOrder->id, + 'note' => __('Transfer out') . ' #' . $transferOrder->order_no, + 'created_by' => Auth::id(), + 'created_at' => now(), + ]); + } + + // 增加目標倉庫庫存 + $toStock = WarehouseStock::firstOrCreate( + ['warehouse_id' => $transferOrder->to_warehouse_id, 'product_id' => $item->product_id], + ['quantity' => 0, 'safety_stock' => 0] + ); + $beforeTo = $toStock->quantity; + $toStock->increment('quantity', $item->quantity); + + StockMovement::create([ + 'company_id' => $transferOrder->company_id, + 'warehouse_id' => $transferOrder->to_warehouse_id, + 'product_id' => $item->product_id, + 'type' => 'transfer_in', + 'quantity' => $item->quantity, + 'before_qty' => $beforeTo, + 'after_qty' => $beforeTo + $item->quantity, + 'reference_type' => TransferOrder::class, + 'reference_id' => $transferOrder->id, + 'note' => __('Transfer in') . ' #' . $transferOrder->order_no, + 'created_by' => Auth::id(), + 'created_at' => now(), + ]); + } + + $transferOrder->update([ + 'status' => TransferOrder::STATUS_COMPLETED, + 'completed_at' => now(), + ]); + }); + + return back()->with('success', __('Transfer completed')); + } + + // ─── 模組 4:機台庫存總覽 ─── + + public function machineInventory(Request $request) + { + $query = Machine::with(['slots' => fn($q) => $q->with('product:id,name,image_url')->orderByRaw('CAST(slot_no AS UNSIGNED) ASC')]) + ->withCount('slots as total_slots') + ->withSum('slots as total_stock', 'stock'); + + if ($search = $request->input('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('serial_no', 'like', "%{$search}%"); + }); + } + + $machines = $query->orderBy('name') + ->paginate($request->input('per_page', 10)) + ->withQueryString(); + + return view('admin.warehouses.machine-inventory', compact('machines')); + } + + /** + * AJAX:取得單台機台貨道詳情 + */ + public function machineSlots(Machine $machine) + { + $slots = $machine->slots() + ->with('product:id,name,image_url') + ->orderByRaw('CAST(slot_no AS UNSIGNED) ASC') + ->get(); + + return response()->json(['success' => true, 'slots' => $slots]); + } + + // ─── 模組 5:機台補貨 ─── + + public function replenishments(Request $request) + { + $query = ReplenishmentOrder::with(['warehouse:id,name', 'machine:id,name,serial_no', 'assignee:id,name', 'creator:id,name']) + ->withCount('items') + ->latest(); + + if ($request->filled('status')) { + $query->where('status', $request->input('status')); + } + + $orders = $query->paginate(10)->withQueryString(); + $warehouses = Warehouse::active()->orderBy('name')->get(['id', 'name']); + $machines = Machine::orderBy('name')->get(['id', 'name', 'serial_no']); + + return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines')); + } + + /** + * 建立補貨單 + */ + public function storeReplenishment(Request $request) + { + $validated = $request->validate([ + 'warehouse_id' => 'required|exists:warehouses,id', + 'machine_id' => 'required|exists:machines,id', + 'note' => 'nullable|string|max:500', + 'items' => 'required|array|min:1', + 'items.*.product_id' => 'required|exists:products,id', + 'items.*.slot_no' => 'required|integer', + 'items.*.quantity' => 'required|integer|min:1', ]); + + DB::transaction(function () use ($validated) { + $order = ReplenishmentOrder::create([ + 'company_id' => Auth::user()->company_id, + 'order_no' => 'RP-' . now()->format('Ymd') . '-' . str_pad(ReplenishmentOrder::whereDate('created_at', today())->count() + 1, 4, '0', STR_PAD_LEFT), + 'warehouse_id' => $validated['warehouse_id'], + 'machine_id' => $validated['machine_id'], + 'status' => ReplenishmentOrder::STATUS_PENDING, + 'note' => $validated['note'] ?? null, + 'created_by' => Auth::id(), + ]); + + foreach ($validated['items'] as $item) { + $order->items()->create($item); + } + }); + + return redirect()->route('admin.warehouses.replenishments') + ->with('success', __('Replenishment order created')); + } + + /** + * 確認補貨完成(扣倉庫庫存 + 增加機台貨道庫存) + */ + public function confirmReplenishment(ReplenishmentOrder $replenishmentOrder) + { + if ($replenishmentOrder->status === ReplenishmentOrder::STATUS_COMPLETED) { + return back()->with('error', __('Order already completed')); + } + + DB::transaction(function () use ($replenishmentOrder) { + $replenishmentOrder->loadMissing('items'); + + foreach ($replenishmentOrder->items as $item) { + // 扣除倉庫庫存 + $stock = WarehouseStock::where('warehouse_id', $replenishmentOrder->warehouse_id) + ->where('product_id', $item->product_id)->first(); + + if ($stock && $stock->quantity >= $item->quantity) { + $before = $stock->quantity; + $stock->decrement('quantity', $item->quantity); + + StockMovement::create([ + 'company_id' => $replenishmentOrder->company_id, + 'warehouse_id' => $replenishmentOrder->warehouse_id, + 'product_id' => $item->product_id, + 'type' => 'out', + 'quantity' => $item->quantity, + 'before_qty' => $before, + 'after_qty' => $before - $item->quantity, + 'reference_type' => ReplenishmentOrder::class, + 'reference_id' => $replenishmentOrder->id, + 'note' => __('Replenishment') . ' #' . $replenishmentOrder->order_no, + 'created_by' => Auth::id(), + 'created_at' => now(), + ]); + } + + // 增加機台貨道庫存 + MachineSlot::where('machine_id', $replenishmentOrder->machine_id) + ->where('slot_no', $item->slot_no) + ->increment('stock', $item->quantity); + } + + $replenishmentOrder->update([ + 'status' => ReplenishmentOrder::STATUS_COMPLETED, + 'completed_at' => now(), + ]); + }); + + return back()->with('success', __('Replenishment completed')); } } diff --git a/app/Models/Machine/Machine.php b/app/Models/Machine/Machine.php index 409b7c6..d22fe74 100644 --- a/app/Models/Machine/Machine.php +++ b/app/Models/Machine/Machine.php @@ -31,10 +31,18 @@ class Machine extends Model }); }); - // 當 api_token 發生變動時,自動同步至 Redis (供 MQTT 認證使用) + // 當 api_token 或 serial_no 發生變動時,自動同步至 Redis (供 MQTT 認證使用) static::saved(function ($machine) { - if ($machine->wasChanged('api_token') || $machine->wasRecentlyCreated) { + if ($machine->wasChanged('api_token') || $machine->wasChanged('serial_no') || $machine->wasRecentlyCreated) { app(\App\Services\Machine\MachineService::class)->syncMqttAuth($machine); + + // 如果是修改序號,則刪除舊的 Redis Key 避免殘留 + if ($machine->wasChanged('serial_no') && !$machine->wasRecentlyCreated) { + $oldSerial = $machine->getOriginal('serial_no'); + if ($oldSerial) { + \Illuminate\Support\Facades\Redis::del("machine_auth:{$oldSerial}"); + } + } } }); } diff --git a/app/Models/Warehouse/ReplenishmentOrder.php b/app/Models/Warehouse/ReplenishmentOrder.php new file mode 100644 index 0000000..aa480eb --- /dev/null +++ b/app/Models/Warehouse/ReplenishmentOrder.php @@ -0,0 +1,78 @@ + 'datetime', + ]; + + /** + * 狀態常數 + */ + public const STATUS_PENDING = 'pending'; + public const STATUS_PREPARED = 'prepared'; + public const STATUS_DELIVERING = 'delivering'; + public const STATUS_COMPLETED = 'completed'; + public const STATUS_CANCELLED = 'cancelled'; + + /** + * 出貨倉庫 + */ + public function warehouse() + { + return $this->belongsTo(Warehouse::class); + } + + /** + * 目標機台 + */ + public function machine() + { + return $this->belongsTo(\App\Models\Machine\Machine::class); + } + + /** + * 補貨單明細 + */ + public function items() + { + return $this->hasMany(ReplenishmentOrderItem::class); + } + + /** + * 指派人員 + */ + public function assignee() + { + return $this->belongsTo(\App\Models\System\User::class, 'assigned_to'); + } + + /** + * 建立人 + */ + public function creator() + { + return $this->belongsTo(\App\Models\System\User::class, 'created_by'); + } +} diff --git a/app/Models/Warehouse/ReplenishmentOrderItem.php b/app/Models/Warehouse/ReplenishmentOrderItem.php new file mode 100644 index 0000000..aa48489 --- /dev/null +++ b/app/Models/Warehouse/ReplenishmentOrderItem.php @@ -0,0 +1,38 @@ + 'integer', + ]; + + /** + * 所屬補貨單 + */ + public function replenishmentOrder() + { + return $this->belongsTo(ReplenishmentOrder::class); + } + + /** + * 對應商品 + */ + public function product() + { + return $this->belongsTo(\App\Models\Product\Product::class); + } +} diff --git a/app/Models/Warehouse/StockInOrder.php b/app/Models/Warehouse/StockInOrder.php new file mode 100644 index 0000000..af6428c --- /dev/null +++ b/app/Models/Warehouse/StockInOrder.php @@ -0,0 +1,51 @@ + 'datetime', + ]; + + /** + * 所屬倉庫 + */ + public function warehouse() + { + return $this->belongsTo(Warehouse::class); + } + + /** + * 入庫單明細 + */ + public function items() + { + return $this->hasMany(StockInOrderItem::class); + } + + /** + * 建立人 + */ + public function creator() + { + return $this->belongsTo(\App\Models\System\User::class, 'created_by'); + } +} diff --git a/app/Models/Warehouse/StockInOrderItem.php b/app/Models/Warehouse/StockInOrderItem.php new file mode 100644 index 0000000..54cd584 --- /dev/null +++ b/app/Models/Warehouse/StockInOrderItem.php @@ -0,0 +1,40 @@ + 'integer', + 'expiry_date' => 'date:Y-m-d', + ]; + + /** + * 所屬入庫單 + */ + public function stockInOrder() + { + return $this->belongsTo(StockInOrder::class); + } + + /** + * 對應商品 + */ + public function product() + { + return $this->belongsTo(\App\Models\Product\Product::class); + } +} diff --git a/app/Models/Warehouse/StockMovement.php b/app/Models/Warehouse/StockMovement.php new file mode 100644 index 0000000..91bbdb3 --- /dev/null +++ b/app/Models/Warehouse/StockMovement.php @@ -0,0 +1,82 @@ + 'integer', + 'before_qty' => 'integer', + 'after_qty' => 'integer', + 'created_at' => 'datetime', + ]; + + /** + * 異動類型對應中文 + */ + public const TYPE_LABELS = [ + 'in' => 'Stock In', + 'out' => 'Stock Out', + 'adjust' => 'Adjustment', + 'damage' => 'Damage', + 'transfer_in' => 'Transfer In', + 'transfer_out' => 'Transfer Out', + ]; + + /** + * 所屬倉庫 + */ + public function warehouse() + { + return $this->belongsTo(Warehouse::class); + } + + /** + * 對應商品 + */ + public function product() + { + return $this->belongsTo(\App\Models\Product\Product::class); + } + + /** + * 操作人 + */ + public function creator() + { + return $this->belongsTo(\App\Models\System\User::class, 'created_by'); + } + + /** + * 關聯單據(多態) + */ + public function reference() + { + return $this->morphTo(); + } +} diff --git a/app/Models/Warehouse/TransferOrder.php b/app/Models/Warehouse/TransferOrder.php new file mode 100644 index 0000000..6431c0b --- /dev/null +++ b/app/Models/Warehouse/TransferOrder.php @@ -0,0 +1,85 @@ + 'datetime', + ]; + + /** + * 調撥類型常數 + */ + public const TYPE_W2W = 'warehouse_to_warehouse'; + public const TYPE_M2W = 'machine_to_warehouse'; + + /** + * 狀態常數 + */ + public const STATUS_DRAFT = 'draft'; + public const STATUS_PENDING = 'pending'; + public const STATUS_IN_TRANSIT = 'in_transit'; + public const STATUS_COMPLETED = 'completed'; + public const STATUS_CANCELLED = 'cancelled'; + + /** + * 來源倉庫 + */ + public function fromWarehouse() + { + return $this->belongsTo(Warehouse::class, 'from_warehouse_id'); + } + + /** + * 來源機台(機台退回時) + */ + public function fromMachine() + { + return $this->belongsTo(\App\Models\Machine\Machine::class, 'from_machine_id'); + } + + /** + * 目標倉庫 + */ + public function toWarehouse() + { + return $this->belongsTo(Warehouse::class, 'to_warehouse_id'); + } + + /** + * 調撥單明細 + */ + public function items() + { + return $this->hasMany(TransferOrderItem::class); + } + + /** + * 建立人 + */ + public function creator() + { + return $this->belongsTo(\App\Models\System\User::class, 'created_by'); + } +} diff --git a/app/Models/Warehouse/TransferOrderItem.php b/app/Models/Warehouse/TransferOrderItem.php new file mode 100644 index 0000000..f08d17d --- /dev/null +++ b/app/Models/Warehouse/TransferOrderItem.php @@ -0,0 +1,37 @@ + 'integer', + ]; + + /** + * 所屬調撥單 + */ + public function transferOrder() + { + return $this->belongsTo(TransferOrder::class); + } + + /** + * 對應商品 + */ + public function product() + { + return $this->belongsTo(\App\Models\Product\Product::class); + } +} diff --git a/app/Models/Warehouse/Warehouse.php b/app/Models/Warehouse/Warehouse.php new file mode 100644 index 0000000..d4ec8e5 --- /dev/null +++ b/app/Models/Warehouse/Warehouse.php @@ -0,0 +1,82 @@ + 'boolean', + ]; + + /** + * Scope:僅篩選啟用的倉庫 + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Scope:僅篩選總倉 + */ + public function scopeMain($query) + { + return $query->where('type', 'main'); + } + + /** + * Scope:僅篩選分倉 + */ + public function scopeBranch($query) + { + return $query->where('type', 'branch'); + } + + /** + * 倉庫負責人 + */ + public function manager() + { + return $this->belongsTo(\App\Models\System\User::class, 'manager_user_id'); + } + + /** + * 倉庫庫存(各商品) + */ + public function stocks() + { + return $this->hasMany(WarehouseStock::class); + } + + /** + * 庫存異動紀錄 + */ + public function movements() + { + return $this->hasMany(StockMovement::class); + } + + /** + * 入庫單 + */ + public function stockInOrders() + { + return $this->hasMany(StockInOrder::class); + } +} diff --git a/app/Models/Warehouse/WarehouseStock.php b/app/Models/Warehouse/WarehouseStock.php new file mode 100644 index 0000000..817232c --- /dev/null +++ b/app/Models/Warehouse/WarehouseStock.php @@ -0,0 +1,48 @@ + 'integer', + 'safety_stock' => 'integer', + ]; + + /** + * Scope:低於安全庫存 + */ + public function scopeLowStock($query) + { + return $query->whereColumn('quantity', '<=', 'safety_stock') + ->where('safety_stock', '>', 0); + } + + /** + * 所屬倉庫 + */ + public function warehouse() + { + return $this->belongsTo(Warehouse::class); + } + + /** + * 對應商品 + */ + public function product() + { + return $this->belongsTo(\App\Models\Product\Product::class); + } +} diff --git a/compose.yaml b/compose.yaml index 58cf142..2c881fb 100644 --- a/compose.yaml +++ b/compose.yaml @@ -145,7 +145,7 @@ services: MQTT_GATEWAY_CLIENT_ID: '${MQTT_GATEWAY_CLIENT_ID:-star-cloud-gateway}' MQTT_REDIS_ADDR: '${MQTT_REDIS_ADDR:-redis:6379}' MQTT_REDIS_PASSWORD: '${REDIS_PASSWORD:-}' - MQTT_REDIS_PREFIX: '${MQTT_REDIS_PREFIX:-starcloud_database_}' + MQTT_REDIS_PREFIX: '${MQTT_REDIS_PREFIX:-star_cloud_database_}' MQTT_INCOMING_QUEUE: '${MQTT_INCOMING_QUEUE:-mqtt_incoming_jobs}' MQTT_OUTGOING_QUEUE: '${MQTT_OUTGOING_QUEUE:-mqtt_outgoing_commands}' MQTT_USER: '${MQTT_USER:-star-cloud-gateway}' diff --git a/config/api-docs.php b/config/api-docs.php index 31b9196..d8ed015 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -437,6 +437,20 @@ return [ 'firmware_version' => '2.1.6' ], ], + [ + 'name' => '機台連線狀態上報 (Status/LWT)', + 'slug' => 'mqtt-status', + 'action' => 'PUB', + 'topic' => 'machine/{serial_no}/status', + 'qos' => 0, + 'description' => '用於機台連線與斷線的即時狀態同步。通常用於 MQTT 的遺囑訊息 (LWT) 設定,或由 Broker 連線事件觸發。', + 'payload_parameters' => [ + 'status' => ['type' => 'string', 'description' => '連線狀態。接受值:online, offline'], + ], + 'payload_example' => [ + 'status' => 'online' + ], + ], [ 'name' => '機台異常上報 (Error)', 'slug' => 'mqtt-error', @@ -446,7 +460,7 @@ return [ 'description' => '當機台發生硬體故障(如卡貨、通訊中斷)時即時上報。', 'payload_parameters' => [ 'tid' => ['type' => 'integer', 'description' => '貨道編號或任務 ID'], - 'error_code' => ['type' => 'string', 'description' => '硬體錯誤代碼 (如 0403 代表卡貨)'], + 'error_code' => ['type' => 'string', 'description' => '硬體錯誤代碼,詳見下方 B013 代碼表'], ], 'payload_example' => [ 'tid' => 12, @@ -466,7 +480,7 @@ return [ 'amount' => ['type' => 'number', 'description' => '交易金額 (create/invoice 時使用)'], 'slot_no' => ['type' => 'string', 'description' => '出貨貨道號碼'], 'invoice_no' => ['type' => 'string', 'description' => '發票號碼 (action 為 invoice 時必填)'], - 'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,如 success, failed)'], + 'status' => ['type' => 'string', 'description' => '出貨結果狀態 (action 為 dispense 時必填,見狀態定義表)'], ], 'payload_example' => [ 'action' => 'create', @@ -582,7 +596,7 @@ return [ 'description' => '機台執行完雲端指令後的主動回報。此回報極為重要,雲端將依此決定交易是否完成或需進行庫存回滾。', 'payload_parameters' => [ 'command_id' => ['type' => 'string', 'description' => '對應下發時的指令 ID'], - 'result' => ['type' => 'string', 'description' => '執行結果,僅接受: success 或 failed'], + 'result' => ['type' => 'string', 'description' => '執行結果,見狀態定義表'], 'stock' => ['type' => 'integer', 'description' => '執行指令後的最終剩餘庫存 (選填,通常出貨指令需帶上)'], 'message' => ['type' => 'string', 'description' => '額外的錯誤訊息 (選填)'], ], @@ -594,5 +608,39 @@ return [ ] ] ] + ], + + 'status_definitions' => [ + [ + 'name' => 'B013: 硬體錯誤代碼 (MQTT Error Topic)', + 'description' => '當機台透過 machine/{serial_no}/error 上報時,error_code 欄位所使用的代碼。', + 'codes' => [ + ['code' => '0402', 'label' => '出貨成功', 'level' => 'info', 'description' => '商品成功掉落至取貨口。'], + ['code' => '0403', 'label' => '貨道卡貨', 'level' => 'error', 'description' => '馬達轉動異常或偵測到卡貨,需人工排除。'], + ['code' => '0202', 'label' => '貨道缺貨', 'level' => 'warning', 'description' => '偵測到實體貨道已無商品。'], + ['code' => '0412', 'label' => '昇降機上升異常', 'level' => 'error', 'description' => '昇降馬達無法正確到達指定位置。'], + ['code' => '0415', 'label' => '取貨門異常', 'level' => 'error', 'description' => '取貨門馬達損壞或感應器失效。'], + ['code' => '5402', 'label' => '取貨門未關', 'level' => 'warning', 'description' => '取貨逾時或門片未正確閉合。'], + ['code' => '5403', 'label' => '昇降系統故障', 'level' => 'error', 'description' => '昇降機通訊中斷或驅動板異常。'], + ] + ], + [ + 'name' => '交易與指令執行狀態 (Transaction & Command)', + 'description' => '用於 Transaction (dispense) 與 Command ACK 的執行結果判定。', + 'codes' => [ + ['code' => 'success', 'label' => '成功', 'description' => '動作執行圓滿完成。'], + ['code' => 'failed', 'label' => '失敗', 'description' => '動作執行失敗,雲端將視情況發起退款或記錄異常。'], + ['code' => '1', 'label' => '成功 (數字型態)', 'description' => '部分舊型指令或 B602 使用的成功代碼。'], + ['code' => '0', 'label' => '失敗 (數字型態)', 'description' => '部分舊型指令或 B602 使用的失敗代碼。'], + ] + ], + [ + 'name' => '機台連線狀態 (Connection Status)', + 'description' => '用於 machine/{serial_no}/status 主題,反映機台與 Broker 的連線情況。', + 'codes' => [ + ['code' => 'online', 'label' => '在線', 'description' => '機台已成功連線至 MQTT Broker。'], + ['code' => 'offline', 'label' => '離線', 'description' => '機台已斷開連線(通常由 LWT 遺囑訊息觸發)。'], + ] + ] ] ]; \ No newline at end of file diff --git a/database/migrations/2026_04_23_090000_create_warehouses_table.php b/database/migrations/2026_04_23_090000_create_warehouses_table.php new file mode 100644 index 0000000..44a00a4 --- /dev/null +++ b/database/migrations/2026_04_23_090000_create_warehouses_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶'); + $table->string('name')->comment('倉庫名稱'); + $table->enum('type', ['main', 'branch'])->default('branch')->comment('倉庫類型:總倉/分倉'); + $table->string('address')->nullable()->comment('地址'); + $table->foreignId('manager_user_id')->nullable()->constrained('users')->nullOnDelete()->comment('負責人'); + $table->boolean('is_active')->default(true)->comment('啟用/停用'); + $table->timestamps(); + $table->softDeletes(); + + // 索引:常用篩選條件 + $table->index(['company_id', 'type']); + $table->index(['company_id', 'is_active']); + }); + } + + public function down(): void + { + Schema::dropIfExists('warehouses'); + } +}; diff --git a/database/migrations/2026_04_23_090001_create_warehouse_stocks_table.php b/database/migrations/2026_04_23_090001_create_warehouse_stocks_table.php new file mode 100644 index 0000000..d594858 --- /dev/null +++ b/database/migrations/2026_04_23_090001_create_warehouse_stocks_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('warehouse_id')->constrained()->cascadeOnDelete()->comment('所屬倉庫'); + $table->foreignId('product_id')->constrained()->comment('商品'); + $table->integer('quantity')->default(0)->comment('現有庫存數量'); + $table->integer('safety_stock')->default(0)->comment('安全庫存量'); + $table->timestamps(); + + // 唯一約束:同一倉庫同一商品只能有一筆記錄 + $table->unique(['warehouse_id', 'product_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('warehouse_stocks'); + } +}; diff --git a/database/migrations/2026_04_23_090002_create_stock_in_orders_table.php b/database/migrations/2026_04_23_090002_create_stock_in_orders_table.php new file mode 100644 index 0000000..5783bf1 --- /dev/null +++ b/database/migrations/2026_04_23_090002_create_stock_in_orders_table.php @@ -0,0 +1,50 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶'); + $table->foreignId('warehouse_id')->constrained()->comment('入庫目標倉庫'); + $table->string('order_no')->unique()->comment('入庫單號'); + $table->enum('status', ['draft', 'completed'])->default('draft')->comment('狀態'); + $table->text('note')->nullable()->comment('備註'); + $table->foreignId('created_by')->constrained('users')->comment('建立人'); + $table->timestamp('completed_at')->nullable()->comment('入庫完成時間'); + $table->timestamps(); + $table->softDeletes(); + + // 索引 + $table->index(['company_id', 'status']); + $table->index(['company_id', 'created_at']); + }); + + Schema::create('stock_in_order_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('stock_in_order_id')->constrained()->cascadeOnDelete()->comment('所屬入庫單'); + $table->foreignId('product_id')->constrained()->comment('商品'); + $table->integer('quantity')->comment('入庫數量'); + $table->string('batch_no')->nullable()->comment('批號'); + $table->date('expiry_date')->nullable()->comment('有效期限'); + $table->timestamps(); + + // 索引 + $table->index('stock_in_order_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('stock_in_order_items'); + Schema::dropIfExists('stock_in_orders'); + } +}; diff --git a/database/migrations/2026_04_23_090003_create_stock_movements_table.php b/database/migrations/2026_04_23_090003_create_stock_movements_table.php new file mode 100644 index 0000000..f770a32 --- /dev/null +++ b/database/migrations/2026_04_23_090003_create_stock_movements_table.php @@ -0,0 +1,48 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶'); + $table->foreignId('warehouse_id')->constrained()->comment('異動倉庫'); + $table->foreignId('product_id')->constrained()->comment('商品'); + $table->enum('type', [ + 'in', // 入庫 + 'out', // 出庫(補貨扣減) + 'adjust', // 盤點調整 + 'damage', // 報損 + 'transfer_in', // 調撥入庫 + 'transfer_out', // 調撥出庫 + ])->comment('異動類型'); + $table->integer('quantity')->comment('異動數量(正數入庫、負數出庫)'); + $table->integer('before_qty')->comment('異動前數量'); + $table->integer('after_qty')->comment('異動後數量'); + $table->string('reference_type')->nullable()->comment('關聯單據類型(多態)'); + $table->unsignedBigInteger('reference_id')->nullable()->comment('關聯單據 ID'); + $table->string('note')->nullable()->comment('備註/原因'); + $table->foreignId('created_by')->constrained('users')->comment('操作人'); + $table->timestamp('created_at')->useCurrent(); + + // 索引:高頻查詢 + $table->index(['company_id', 'created_at']); + $table->index(['warehouse_id', 'product_id']); + $table->index(['reference_type', 'reference_id']); + $table->index('type'); + }); + } + + public function down(): void + { + Schema::dropIfExists('stock_movements'); + } +}; diff --git a/database/migrations/2026_04_23_090004_create_transfer_orders_table.php b/database/migrations/2026_04_23_090004_create_transfer_orders_table.php new file mode 100644 index 0000000..c32c4dd --- /dev/null +++ b/database/migrations/2026_04_23_090004_create_transfer_orders_table.php @@ -0,0 +1,55 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶'); + $table->string('order_no')->unique()->comment('調撥單號'); + $table->enum('type', ['warehouse_to_warehouse', 'machine_to_warehouse'])->comment('調撥類型'); + $table->foreignId('from_warehouse_id')->nullable()->constrained('warehouses')->comment('來源倉庫'); + $table->unsignedBigInteger('from_machine_id')->nullable()->comment('來源機台(機台退回時)'); + $table->foreignId('to_warehouse_id')->constrained('warehouses')->comment('目標倉庫'); + $table->enum('status', ['draft', 'pending', 'in_transit', 'completed', 'cancelled'])->default('draft')->comment('狀態'); + $table->text('note')->nullable()->comment('備註'); + $table->foreignId('created_by')->constrained('users')->comment('建立人'); + $table->timestamp('completed_at')->nullable()->comment('完成時間'); + $table->timestamps(); + $table->softDeletes(); + + // 外鍵:from_machine_id 手動建立(因為可能為 null 且跨表) + $table->foreign('from_machine_id')->references('id')->on('machines')->nullOnDelete(); + + // 索引 + $table->index(['company_id', 'status']); + $table->index(['company_id', 'created_at']); + $table->index('type'); + }); + + Schema::create('transfer_order_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('transfer_order_id')->constrained()->cascadeOnDelete()->comment('所屬調撥單'); + $table->foreignId('product_id')->constrained()->comment('商品'); + $table->integer('quantity')->comment('調撥數量'); + $table->timestamps(); + + // 索引 + $table->index('transfer_order_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('transfer_order_items'); + Schema::dropIfExists('transfer_orders'); + } +}; diff --git a/database/migrations/2026_04_23_090005_create_replenishment_orders_table.php b/database/migrations/2026_04_23_090005_create_replenishment_orders_table.php new file mode 100644 index 0000000..8550603 --- /dev/null +++ b/database/migrations/2026_04_23_090005_create_replenishment_orders_table.php @@ -0,0 +1,56 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->comment('所屬租戶'); + $table->string('order_no')->unique()->comment('補貨單號'); + $table->foreignId('warehouse_id')->constrained()->comment('出貨倉庫'); + $table->unsignedBigInteger('machine_id')->comment('目標機台'); + $table->enum('status', ['pending', 'prepared', 'delivering', 'completed', 'cancelled'])->default('pending')->comment('狀態'); + $table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete()->comment('指派補貨人員'); + $table->text('note')->nullable()->comment('備註'); + $table->foreignId('created_by')->constrained('users')->comment('建立人'); + $table->timestamp('completed_at')->nullable()->comment('完成時間'); + $table->timestamps(); + $table->softDeletes(); + + // 外鍵 + $table->foreign('machine_id')->references('id')->on('machines'); + + // 索引 + $table->index(['company_id', 'status']); + $table->index(['company_id', 'created_at']); + $table->index('machine_id'); + $table->index('assigned_to'); + }); + + Schema::create('replenishment_order_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('replenishment_order_id')->constrained()->cascadeOnDelete()->comment('所屬補貨單'); + $table->foreignId('product_id')->constrained()->comment('商品'); + $table->string('slot_no')->nullable()->comment('貨道編號'); + $table->integer('quantity')->comment('補貨數量'); + $table->timestamps(); + + // 索引 + $table->index('replenishment_order_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('replenishment_order_items'); + Schema::dropIfExists('replenishment_orders'); + } +}; diff --git a/database/seeders/MachineSeeder.php b/database/seeders/MachineSeeder.php index 33285d9..49cf986 100644 --- a/database/seeders/MachineSeeder.php +++ b/database/seeders/MachineSeeder.php @@ -10,8 +10,8 @@ class MachineSeeder extends Seeder { public function run(): void { - // 建立 50 台機台 - Machine::factory()->count(50)->create()->each(function ($machine) { + // 建立 2 台機台 + Machine::factory()->count(2)->create()->each(function ($machine) { // 每台機台隨機建立 5-10 筆初始日誌 MachineLog::factory()->count(rand(5, 10))->create([ 'machine_id' => $machine->id, diff --git a/lang/en.json b/lang/en.json index 37e0f7e..a9dae4a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -702,6 +702,7 @@ "Please check the form for errors.": "Please check the form for errors.", "Please confirm the details below": "Please confirm the details below", "Please select a machine first": "Please select a machine first", + "Please select a machine model": "Please select a machine model", "Please select a machine to view and manage its advertisements.": "Please select a machine to view and manage its advertisements.", "Please select a machine to view metrics": "請選擇機台以查看數據", "Please select a material": "Please select a material", @@ -1188,5 +1189,162 @@ "This slot has a pending update. Please wait for the previous command to complete.": "This slot has a pending update. Please wait for the previous command to complete.", "This slot has a pending command. Please wait.": "This slot has a pending command. Please wait.", "Out of stock.": "Out of stock.", - "Save error:": "Save error:" + "Save error:": "Save error:", + "Warehouse Overview": "Warehouse Overview", + "Inventory Management": "Inventory Management", + "Transfer Orders": "Transfer Orders", + "Machine Replenishment": "Machine Replenishment", + "Machine Inventory Overview": "Machine Inventory Overview", + "Manage your warehouses, including main and branch warehouses": "Manage your warehouses, including main and branch warehouses", + "View all warehouses": "View all warehouses", + "Create main and branch warehouses": "Create main and branch warehouses", + "Assign warehouse managers": "Assign warehouse managers", + "Monitor warehouse stock summary": "Monitor warehouse stock summary", + "Manage warehouse stock levels, stock-in orders, adjustments, and movement history": "Manage warehouse stock levels, stock-in orders, adjustments, and movement history", + "Stock overview by warehouse": "Stock overview by warehouse", + "Create stock-in orders": "Create stock-in orders", + "Stock adjustments and damage reports": "Stock adjustments and damage reports", + "Complete movement history log": "Complete movement history log", + "Manage stock transfers between warehouses and machine returns": "Manage stock transfers between warehouses and machine returns", + "Warehouse to warehouse transfers": "Warehouse to warehouse transfers", + "Machine to warehouse returns": "Machine to warehouse returns", + "Transfer order status tracking": "Transfer order status tracking", + "Create replenishment orders and manage restocking from warehouse to machines": "Create replenishment orders and manage restocking from warehouse to machines", + "Smart replenishment suggestions": "Smart replenishment suggestions", + "One-click replenishment order generation": "One-click replenishment order generation", + "Assign replenishment staff": "Assign replenishment staff", + "Replenishment history": "Replenishment history", + "Real-time inventory status across all machines with expiry tracking": "Real-time inventory status across all machines with expiry tracking", + "View slot-level inventory for each machine": "View slot-level inventory for each machine", + "Low stock and out-of-stock alerts": "Low stock and out-of-stock alerts", + "Expiry date tracking and warnings": "Expiry date tracking and warnings", + "Quick replenishment from this view": "Quick replenishment from this view", + "Cash Module": "Cash Module", + "Save Settings": "Save Settings", + "System Settings": "System Settings", + "Tax System": "Tax System", + "Electronic Invoice": "Electronic Invoice", + "Card System": "Card System", + "Credit Card / Contactless": "Credit Card / Contactless", + "Scan Pay": "Scan Pay", + "ESUN Scan Pay": "ESUN Scan Pay", + "LinePay": "LinePay", + "Other Features": "Other Features", + "Shopping Cart": "Shopping Cart", + "Goods & System Settings": "Goods & System Settings", + "Display Material Code": "Display Material Code", + "Enable Points Mechanism": "Enable Points Mechanism", + "Taxation System": "Taxation System", + "Card Terminal System": "Card Terminal System", + "QR Scan Payment": "QR Scan Payment", + "Shopping Cart Feature": "Shopping Cart Feature", + "Welcome Gift Feature": "Welcome Gift Feature", + "Cash Module Feature": "Cash Module Feature", + "Includes Credit Card/Mobile Pay": "Includes Credit Card/Mobile Pay", + "E.SUN QR Pay": "E.SUN QR Pay", + "LinePay Payment": "LinePay Payment", + "Coin/Bill Acceptor": "Coin/Bill Acceptor", + "System settings updated successfully.": "System settings updated successfully.", + "Update Settings": "Update Settings", + "Material Code": "Material Code", + "Points": "Points", + "Warehouse Management": "Warehouse Management", + "Warehouse Overview": "Warehouse Overview", + "Add Warehouse": "Add Warehouse", + "Total Warehouses": "Total Warehouses", + "Main Warehouses": "Main Warehouses", + "Branch Warehouses": "Branch Warehouses", + "Low Stock Alerts": "Low Stock Alerts", + "Search warehouses...": "Search warehouses...", + "Main": "Main", + "Branch": "Branch", + "Warehouse Info": "Warehouse Info", + "Products / Stock": "Products / Stock", + "Products": "Products", + "Stock": "Stock", + "Warehouse created successfully.": "Warehouse created successfully.", + "Warehouse updated successfully.": "Warehouse updated successfully.", + "Warehouse deleted successfully.": "Warehouse deleted successfully.", + "Cannot delete warehouse with existing stock": "Cannot delete warehouse with existing stock", + "Inventory Management": "Inventory Management", + "Track stock levels, stock-in orders, and movement history": "Track stock levels, stock-in orders, and movement history", + "New Stock-In Order": "New Stock-In Order", + "Stock Levels": "Stock Levels", + "Stock-In Orders": "Stock-In Orders", + "Movement History": "Movement History", + "Search products...": "Search products...", + "All Warehouses": "All Warehouses", + "Product": "Product", + "Warehouse": "Warehouse", + "Quantity": "Quantity", + "Safety Stock": "Safety Stock", + "Low Stock": "Low Stock", + "Out of Stock": "Out of Stock", + "Normal": "Normal", + "No stock data found": "No stock data found", + "Order No.": "Order No.", + "Created By": "Created By", + "Created At": "Created At", + "Draft": "Draft", + "Completed": "Completed", + "Confirm this stock-in order?": "Confirm this stock-in order?", + "Confirm": "Confirm", + "No stock-in orders found": "No stock-in orders found", + "Time": "Time", + "Qty Change": "Qty Change", + "No movement records found": "No movement records found", + "Target Warehouse": "Target Warehouse", + "Select Warehouse": "Select Warehouse", + "Add Item": "Add Item", + "Select Product": "Select Product", + "Qty": "Qty", + "Note": "Note", + "Optional": "Optional", + "Transfer Orders": "Transfer Orders", + "Manage stock transfers between warehouses and machine returns": "Manage stock transfers between warehouses and machine returns", + "New Transfer": "New Transfer", + "All Types": "All Types", + "Warehouse to Warehouse": "Warehouse to Warehouse", + "Machine to Warehouse": "Machine to Warehouse", + "All Statuses": "All Statuses", + "Cancelled": "Cancelled", + "From": "From", + "To": "To", + "Confirm this transfer?": "Confirm this transfer?", + "No transfer orders found": "No transfer orders found", + "Transfer Type": "Transfer Type", + "From Warehouse": "From Warehouse", + "From Machine": "From Machine", + "Select Machine": "Select Machine", + "To Warehouse": "To Warehouse", + "Note (optional)": "Note (optional)", + "Machine Inventory Overview": "Machine Inventory Overview", + "Real-time slot-level inventory across all machines": "Real-time slot-level inventory across all machines", + "Search machines...": "Search machines...", + "Serial No.": "Serial No.", + "Total Stock": "Total Stock", + "Fill Rate": "Fill Rate", + "View Slots": "View Slots", + "Loading...": "Loading...", + "Empty": "Empty", + "Expiry": "Expiry", + "No slot data": "No slot data", + "Machine Replenishment": "Machine Replenishment", + "Create and manage replenishment orders from warehouse to machines": "Create and manage replenishment orders from warehouse to machines", + "New Replenishment": "New Replenishment", + "Pending": "Pending", + "Prepared": "Prepared", + "Delivering": "Delivering", + "Confirm replenishment completed?": "Confirm replenishment completed?", + "No replenishment orders found": "No replenishment orders found", + "Source Warehouse": "Source Warehouse", + "Target Machine": "Target Machine", + "Replenishment Items": "Replenishment Items", + "Slot": "Slot", + "Stock In": "Stock In", + "Stock Out": "Stock Out", + "Adjustment": "Adjustment", + "Damage": "Damage", + "Transfer In": "Transfer In", + "Transfer Out": "Transfer Out" } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index ec747fa..3d786ad 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1 +1,120 @@ -null \ No newline at end of file +{ + "Goods & System Settings": "商品とシステム設定", + "Display Material Code": "素材コードを表示", + "Enable Points Mechanism": "ポイント機能を有効化", + "Taxation System": "税務システム", + "Card Terminal System": "カード端末システム", + "QR Scan Payment": "QRスキャン決済機能", + "Shopping Cart Feature": "ショッピングカート機能", + "Welcome Gift Feature": "来店特典機能", + "Cash Module Feature": "現金モジュール機能", + "Includes Credit Card/Mobile Pay": "クレジットカード/モバイル決済を含む", + "E.SUN QR Pay": "玉山QR決済", + "LinePay Payment": "LinePay決済", + "Coin/Bill Acceptor": "コイン/紙幣機", + "System settings updated successfully.": "システム設定が正常に更新されました。", + "Update Settings": "設定を更新", + "Material Code": "素材コード", + "Points": "ポイント", + "Please enter configuration name": "設定名を入力してください", + "Please select a company": "会社を選択してください", + "Warehouse Management": "倉庫管理", + "Warehouse Overview": "倉庫概要", + "Add Warehouse": "倉庫を追加", + "Total Warehouses": "倉庫総数", + "Main Warehouses": "総倉庫数", + "Branch Warehouses": "分倉庫数", + "Low Stock Alerts": "低在庫アラート", + "Search warehouses...": "倉庫を検索...", + "Main": "総倉庫", + "Branch": "分倉庫", + "Warehouse Info": "倉庫情報", + "Products / Stock": "商品 / 在庫", + "Products": "商品", + "Stock": "在庫", + "Warehouse created successfully.": "倉庫が正常に作成されました。", + "Warehouse updated successfully.": "倉庫が正常に更新されました。", + "Warehouse deleted successfully.": "倉庫が正常に削除されました。", + "Cannot delete warehouse with existing stock": "在庫がある倉庫は削除できません。", + "Inventory Management": "在庫管理", + "Track stock levels, stock-in orders, and movement history": "在庫レベル、入庫伝票、および移動履歴の追跡", + "New Stock-In Order": "新規入庫伝票", + "Stock Levels": "在庫レベル", + "Stock-In Orders": "入庫伝票", + "Movement History": "移動履歴", + "Search products...": "商品を検索...", + "All Warehouses": "すべての倉庫", + "Product": "商品", + "Warehouse": "倉庫", + "Quantity": "数量", + "Safety Stock": "安全在庫", + "Low Stock": "在庫不足", + "Out of Stock": "在庫切れ", + "Normal": "通常", + "No stock data found": "在庫データが見つかりません", + "Order No.": "伝票番号", + "Created By": "作成者", + "Created At": "作成日時", + "Draft": "下書き", + "Completed": "完了", + "Confirm this stock-in order?": "この入庫伝票を確認しますか?", + "Confirm": "確認", + "No stock-in orders found": "入庫伝票が見つかりません", + "Time": "時間", + "Qty Change": "数量変更", + "No movement records found": "移動履歴が見つかりません", + "Target Warehouse": "対象倉庫", + "Select Warehouse": "倉庫を選択", + "Add Item": "項目を追加", + "Select Product": "商品を選択", + "Qty": "数量", + "Note": "備考", + "Optional": "任意", + "Transfer Orders": "転送伝票", + "Manage stock transfers between warehouses and machine returns": "倉庫間の転送および機台からの返品の管理", + "New Transfer": "新規転送", + "All Types": "すべてのタイプ", + "Warehouse to Warehouse": "倉庫間転送", + "Machine to Warehouse": "機台から倉庫", + "All Statuses": "すべてのステータス", + "Cancelled": "キャンセル済み", + "From": "発送元", + "To": "配送先", + "Confirm this transfer?": "この転送を確認しますか?", + "No transfer orders found": "転送伝票が見つかりません", + "Transfer Type": "転送タイプ", + "From Warehouse": "発送元倉庫", + "From Machine": "発送元機台", + "Select Machine": "機台を選択", + "To Warehouse": "配送先倉庫", + "Note (optional)": "備考 (任意)", + "Machine Inventory Overview": "機台在庫概要", + "Real-time slot-level inventory across all machines": "すべての機台におけるリアルタイムの貨道別在庫", + "Search machines...": "機台を検索...", + "Serial No.": "シリアル番号", + "Total Stock": "総在庫", + "Fill Rate": "充填率", + "View Slots": "貨道を表示", + "Loading...": "読み込み中...", + "Empty": "空", + "Expiry": "有効期限", + "No slot data": "貨道データなし", + "Machine Replenishment": "機台補充", + "Create and manage replenishment orders from warehouse to machines": "倉庫から機台への補充伝票の作成と管理", + "New Replenishment": "新規補充", + "Pending": "保留中", + "Prepared": "準備済み", + "Delivering": "配送中", + "Confirm replenishment completed?": "補充が完了したことを確認しますか?", + "No replenishment orders found": "補充伝票が見つかりません", + "Source Warehouse": "発送元倉庫", + "Target Machine": "対象機台", + "Replenishment Items": "補充項目", + "Slot": "貨道", + "Stock In": "入庫", + "Stock Out": "出庫", + "Adjustment": "在庫調整", + "Damage": "破損", + "Transfer In": "転送入", + "Transfer Out": "転送出" +} \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 94bfa9e..00671b3 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -240,6 +240,7 @@ "Customer Details": "客戶詳情", "Customer enabled successfully.": "客戶已成功啟用。", "Customer Info": "客戶資訊", + "Customer List": "客戶列表", "Customer Management": "客戶管理", "Customer Payment Config": "客戶金流設定", "Customer updated successfully.": "客戶更新成功。", @@ -469,6 +470,7 @@ "Machine model deleted successfully.": "機台型號已成功刪除。", "Machine Model Settings": "機台型號設定", "Machine model updated successfully.": "機台型號已成功更新。", + "Machine permissions updated successfully.": "機台權限已成功更新。", "Machine Name": "機台名稱", "Machine Serial No": "機台序號", "Machine Permissions": "機台權限", @@ -704,8 +706,10 @@ "Playback Order": "播放順序", "Please check the following errors:": "請檢查以下錯誤:", "Please check the form for errors.": "請檢查欄位內容是否正確。", - "Please confirm the details below": "請確認以下指令詳情", + "Please enter configuration name": "請輸入設定名稱", + "Please select a company": "請選擇公司", "Please select a machine first": "請先選擇機台", + "Please select a machine model": "請選擇機台型號", "Please select a machine to view and manage its advertisements.": "請選擇一個機台以查看並管理其廣告。", "Please select a machine to view metrics": "請選擇機台以查看數據", "Please select a material": "請選擇素材", @@ -1197,5 +1201,276 @@ "This slot has a pending update. Please wait for the previous command to complete.": "此貨道已有更新指令在執行中,請等候前一個指令完成。", "This slot has a pending command. Please wait.": "此貨道已有指令正在執行,請稍後。", "Out of stock.": "庫存不足。", - "Save error:": "儲存錯誤:" + "Save error:": "儲存錯誤:", + "Warehouse Overview": "倉庫總覽", + "Inventory Management": "庫存管理", + "Transfer Orders": "調撥單", + "Machine Replenishment": "機台補貨", + "Machine Inventory Overview": "機台庫存總覽", + "Manage your warehouses, including main and branch warehouses": "管理您的倉庫,包含總倉與分倉設定", + "View all warehouses": "查看所有倉庫", + "Create main and branch warehouses": "建立總倉與分倉", + "Assign warehouse managers": "指派倉庫負責人", + "Monitor warehouse stock summary": "監控倉庫庫存摘要", + "Manage warehouse stock levels, stock-in orders, adjustments, and movement history": "管理倉庫庫存水位、入庫單、盤點調整與異動紀錄", + "Stock overview by warehouse": "依倉庫查看庫存概況", + "Create stock-in orders": "建立入庫單", + "Stock adjustments and damage reports": "庫存調整與報損處理", + "Complete movement history log": "完整的庫存異動歷程", + "Manage stock transfers between warehouses and machine returns": "管理倉庫間調撥與機台退回作業", + "Warehouse to warehouse transfers": "倉庫對倉庫調撥", + "Machine to warehouse returns": "機台退回倉庫", + "Transfer order status tracking": "調撥單狀態追蹤", + "Create replenishment orders and manage restocking from warehouse to machines": "建立補貨單,管理從倉庫到機台的補貨流程", + "Smart replenishment suggestions": "智能補貨建議", + "One-click replenishment order generation": "一鍵產生補貨單", + "Assign replenishment staff": "指派補貨人員", + "Replenishment history": "補貨歷史紀錄", + "Real-time inventory status across all machines with expiry tracking": "即時掌握所有機台庫存狀態與有效期限追蹤", + "View slot-level inventory for each machine": "查看各機台貨道庫存", + "Low stock and out-of-stock alerts": "低庫存與缺貨警示", + "Expiry date tracking and warnings": "有效期限追蹤與預警", + "Quick replenishment from this view": "從此畫面快速補貨", + "Add Warehouse": "新增倉庫", + "Edit Warehouse": "編輯倉庫", + "Warehouse Name": "倉庫名稱", + "Warehouse Type": "倉庫類型", + "Warehouse Info": "倉庫資訊", + "Total Warehouses": "倉庫總數", + "Main Warehouses": "總倉數量", + "Branch Warehouses": "分倉數量", + "Low Stock Alerts": "低庫存警示", + "Products / Stock": "商品 / 庫存", + "Search warehouses...": "搜尋倉庫...", + "e.g. Main Warehouse": "如:總倉A", + "Warehouse created successfully": "倉庫建立成功", + "Warehouse updated successfully": "倉庫更新成功", + "Warehouse deleted successfully": "倉庫已刪除", + "Cannot delete warehouse with existing stock": "無法刪除仍有庫存的倉庫", + "Warehouse :status successfully": "倉庫已:status", + "enabled": "啟用", + "disabled": "停用", + "No warehouses found": "未找到倉庫", + "Disable Warehouse": "停用倉庫", + "Are you sure to delete this warehouse? This action cannot be undone.": "確定要刪除此倉庫嗎?此操作無法復原。", + "Are you sure to disable this warehouse?": "確定要停用此倉庫嗎?", + "System Settings": "系統設定", + "Tax System": "稅務系統", + "Electronic Invoice": "電子發票", + "Card System": "刷卡機系統", + "Credit Card / Contactless": "支援信用卡 / 感應支付", + "Scan Pay": "掃碼支付", + "ESUN Scan Pay": "玉山掃碼支付", + "LinePay": "LinePay", + "Other Features": "其他功能", + "Shopping Cart": "購物車功能", + "Cash Module": "硬幣機 / 紙鈔機模組", + "Save Settings": "儲存設定", + "Track stock levels, stock-in orders, and movement history": "追蹤庫存水位、入庫單與異動紀錄", + "Stock Levels": "庫存水位", + "Stock-In Orders": "入庫單", + "Movement History": "異動紀錄", + "New Stock-In Order": "新增入庫單", + "All Warehouses": "所有倉庫", + "Quantity": "數量", + "Safety Stock": "安全庫存", + "Low Stock": "低庫存", + "Out of Stock": "缺貨", + "Normal": "正常", + "No stock data found": "未找到庫存資料", + "Order No.": "單號", + "Created By": "建立者", + "Confirm": "確認", + "Confirm this stock-in order?": "確認此入庫單?", + "No stock-in orders found": "未找到入庫單", + "All Types": "所有類型", + "All Statuses": "所有狀態", + "Stock In": "入庫", + "Stock Out": "出庫", + "Adjustment": "調整", + "Damage": "報損", + "Transfer In": "調入", + "Transfer Out": "調出", + "Qty Change": "數量變動", + "Operator": "操作人員", + "No movement records found": "未找到異動紀錄", + "Target Warehouse": "目標倉庫", + "Select Warehouse": "選擇倉庫", + "Select Product": "選擇商品", + "Add Item": "新增項目", + "Qty": "數量", + "Stock-in order created": "入庫單已建立", + "Order already processed": "訂單已處理", + "Stock-in order confirmed and inventory updated": "入庫單已確認,庫存已更新", + "Stock-in order": "入庫單", + "Manage stock transfers between warehouses and machine returns": "管理倉庫間調撥與機台退貨", + "New Transfer": "新增調撥", + "Transfer Type": "調撥類型", + "Warehouse to Warehouse": "倉庫對倉庫", + "Machine to Warehouse": "機台退回倉庫", + "From Warehouse": "來源倉庫", + "From Machine": "來源機台", + "To Warehouse": "目標倉庫", + "Select Machine": "選擇機台", + "Note (optional)": "備註(選填)", + "From": "來源", + "To": "目標", + "No transfer orders found": "未找到調撥單", + "Transfer order created": "調撥單已建立", + "Confirm this transfer?": "確認此調撥?", + "Insufficient stock for transfer": "庫存不足,無法調撥", + "Transfer completed": "調撥完成", + "Transfer out": "調出", + "Transfer in": "調入", + "Real-time slot-level inventory across all machines": "即時查看各機台貨道庫存", + "Serial No.": "序號", + "Slots": "貨道數", + "Total Stock": "總庫存", + "Fill Rate": "補貨率", + "View Slots": "查看貨道", + "Search machines...": "搜尋機台...", + "Empty": "空置", + "Expiry": "有效期限", + "No slot data": "無貨道資料", + "Create and manage replenishment orders from warehouse to machines": "建立及管理從倉庫到機台的補貨單", + "New Replenishment": "新增補貨單", + "Source Warehouse": "來源倉庫", + "Target Machine": "目標機台", + "Replenishment Items": "補貨項目", + "Slot": "貨道", + "Pending": "待處理", + "Prepared": "已備貨", + "Delivering": "配送中", + "No replenishment orders found": "未找到補貨單", + "Replenishment order created": "補貨單已建立", + "Order already completed": "訂單已完成", + "Replenishment completed": "補貨完成", + "Replenishment": "補貨", + "Confirm replenishment completed?": "確認補貨已完成?", + "Complete": "完成", + "Goods & System Settings": "商品與系統設定", + "Display Material Code": "顯示物料代碼", + "Enable Points Mechanism": "啟用點數機制", + "Taxation System": "稅務系統", + "Card Terminal System": "刷卡機系統", + "QR Scan Payment": "掃碼支付功能", + "Shopping Cart Feature": "購物車功能", + "Welcome Gift Feature": "來店禮功能", + "Cash Module Feature": "現金模組功能", + "Includes Credit Card/Mobile Pay": "包括 /信用卡/卡片支付/手機支付", + "E.SUN QR Pay": "玉山掃碼支付", + "LinePay Payment": "LinePay 支付", + "Coin/Bill Acceptor": "硬幣機/紙鈔機", + "System settings updated successfully.": "系統設定更新成功。", + "Update Settings": "更新設定", + "Disable Customer Confirmation": "停用客戶確認", + "Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?": "停用此客戶將會連帶停用該客戶下的所有帳號,確定要繼續嗎?", + "Are you sure to delete this customer?": "確定要刪除此客戶嗎?", + "Customer List": "客戶列表", + "Manage all tenant accounts and validity": "管理所有租戶帳號及其效期", + "Add Customer": "新增客戶", + "Edit Settings": "編輯設定", + "Delete Customer": "刪除客戶", + "Customer Details": "客戶詳情", + "Close Panel": "關閉面板", + "Settings Saved": "設定已儲存", + "Warehouse Management": "倉庫管理", + "Warehouse Overview": "倉庫總覽", + "Add Warehouse": "新增倉庫", + "Total Warehouses": "倉庫總數", + "Main Warehouses": "總倉數量", + "Branch Warehouses": "分倉數量", + "Low Stock Alerts": "低庫存警示", + "Search warehouses...": "搜尋倉庫...", + "Main": "總倉", + "Branch": "分倉", + "Warehouse Info": "倉庫資訊", + "Products / Stock": "商品 / 庫存", + "Products": "商品", + "Stock": "庫存", + "Warehouse created successfully.": "倉庫已成功建立。", + "Warehouse updated successfully.": "倉庫已成功更新。", + "Warehouse deleted successfully.": "倉庫已成功刪除。", + "Cannot delete warehouse with existing stock": "無法刪除仍有庫存的倉庫", + "Inventory Management": "庫存管理", + "Track stock levels, stock-in orders, and movement history": "追蹤庫存水準、進貨單與異動紀錄", + "New Stock-In Order": "新增進貨單", + "Stock Levels": "庫存水準", + "Stock-In Orders": "進貨紀錄", + "Movement History": "異動紀錄", + "Search products...": "搜尋商品...", + "All Warehouses": "所有倉庫", + "Product": "商品", + "Warehouse": "倉庫", + "Quantity": "數量", + "Safety Stock": "安全庫存", + "Low Stock": "庫存過低", + "Out of Stock": "缺貨", + "Normal": "正常", + "No stock data found": "找不到庫存資料", + "Order No.": "單號", + "Created By": "建立者", + "Created At": "建立時間", + "Draft": "草稿", + "Completed": "已完成", + "Confirm this stock-in order?": "確定要確認此進貨單嗎?", + "Confirm": "確認", + "No stock-in orders found": "找不到進貨單", + "Time": "時間", + "Qty Change": "異動數量", + "No movement records found": "找不到異動紀錄", + "Target Warehouse": "目標倉庫", + "Select Warehouse": "選擇倉庫", + "Add Item": "新增項目", + "Select Product": "選擇商品", + "Qty": "數量", + "Note": "備註", + "Optional": "選填", + "Transfer Orders": "轉倉單", + "Manage stock transfers between warehouses and machine returns": "管理倉庫間轉倉與機台退庫作業", + "New Transfer": "新增轉倉", + "All Types": "所有類型", + "Warehouse to Warehouse": "倉庫轉倉庫", + "Machine to Warehouse": "機台退庫", + "All Statuses": "所有狀態", + "Cancelled": "已取消", + "From": "來源", + "To": "目的地", + "Confirm this transfer?": "確定要確認此轉倉作業嗎?", + "No transfer orders found": "找不到轉倉單", + "Transfer Type": "轉倉類型", + "From Warehouse": "來源倉庫", + "From Machine": "來源機台", + "Select Machine": "選擇機台", + "To Warehouse": "目標倉庫", + "Note (optional)": "備註 (選填)", + "Machine Inventory Overview": "機台庫存概覽", + "Real-time slot-level inventory across all machines": "跨機台的即時貨道庫存追蹤", + "Search machines...": "搜尋機台...", + "Serial No.": "序列號", + "Total Stock": "總庫存", + "Fill Rate": "滿載率", + "View Slots": "查看貨道", + "Loading...": "載入中...", + "Empty": "空白", + "Expiry": "效期", + "No slot data": "找不到貨道資料", + "Machine Replenishment": "機台補貨", + "Create and manage replenishment orders from warehouse to machines": "建立並管理從倉庫到機台的補貨單", + "New Replenishment": "新增補貨", + "Pending": "待處理", + "Prepared": "已備貨", + "Delivering": "配送中", + "Confirm replenishment completed?": "確定補貨已完成?", + "No replenishment orders found": "找不到補貨單", + "Source Warehouse": "來源倉庫", + "Target Machine": "目標機台", + "Replenishment Items": "補貨項目", + "Slot": "貨道", + "Stock In": "庫存入庫", + "Stock Out": "庫存出庫", + "Adjustment": "庫存調整", + "Damage": "報廢", + "Transfer In": "轉入", + "Transfer Out": "轉出" } \ No newline at end of file diff --git a/mqtt-gateway/config/config.go b/mqtt-gateway/config/config.go index 87ac2a9..7b340a7 100644 --- a/mqtt-gateway/config/config.go +++ b/mqtt-gateway/config/config.go @@ -26,7 +26,7 @@ func LoadConfig() *Config { RedisPassword: getEnv("MQTT_REDIS_PASSWORD", ""), IncomingQueueKey: getEnv("MQTT_INCOMING_QUEUE", "mqtt_incoming_jobs"), OutgoingQueueKey: getEnv("MQTT_OUTGOING_QUEUE", "mqtt_outgoing_commands"), - RedisPrefix: getEnv("MQTT_REDIS_PREFIX", "starcloud_database_"), + RedisPrefix: getEnv("MQTT_REDIS_PREFIX", "star_cloud_database_"), } } diff --git a/resources/views/admin/ads/index.blade.php b/resources/views/admin/ads/index.blade.php index a0ca20f..d43eed8 100644 --- a/resources/views/admin/ads/index.blade.php +++ b/resources/views/admin/ads/index.blade.php @@ -32,7 +32,7 @@ $baseRoute = 'admin.data-config.advertisements';
@@ -136,11 +136,11 @@ $baseRoute = 'admin.data-config.advertisements';
- -
@@ -186,11 +186,11 @@ $baseRoute = 'admin.data-config.advertisements'; @@ -199,7 +199,7 @@ $baseRoute = 'admin.data-config.advertisements';
@@ -209,23 +209,23 @@ $baseRoute = 'admin.data-config.advertisements';
@@ -234,7 +234,7 @@ $baseRoute = 'admin.data-config.advertisements';

@@ -273,7 +273,7 @@ $baseRoute = 'admin.data-config.advertisements'; @@ -313,7 +313,7 @@ $baseRoute = 'admin.data-config.advertisements'; @@ -349,15 +349,15 @@ $baseRoute = 'admin.data-config.advertisements';
diff --git a/resources/views/admin/ads/partials/ad-modal.blade.php b/resources/views/admin/ads/partials/ad-modal.blade.php index f10b302..b959123 100644 --- a/resources/views/admin/ads/partials/ad-modal.blade.php +++ b/resources/views/admin/ads/partials/ad-modal.blade.php @@ -20,7 +20,7 @@

{{ __('Manage your ad material details') }}

@@ -112,7 +112,7 @@ class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400" placeholder="YYYY/MM/DD HH:MM">
- +
@@ -133,7 +133,7 @@ class="w-full h-12 bg-slate-50 dark:bg-slate-800/50 border-none rounded-xl px-4 pr-10 text-sm font-bold text-slate-800 dark:text-white focus:ring-2 focus:ring-cyan-500/20 transition-all placeholder:text-slate-400" placeholder="YYYY/MM/DD HH:MM">
- +
@@ -152,7 +152,7 @@