[STYLE] 機台分佈地圖行動端優化與補貨管理邏輯修正
1. 機台分佈頁面行動端優化:新增懸浮標題切換按鈕、抽屜式列表與毛玻璃視覺效果。 2. 修正補貨單指派視窗:優化下拉選單初始化邏輯,解決二次開啟時的選擇殘留問題。 3. 更新多語系檔 (zh_TW, en, ja):補齊機台分佈與補貨管理相關詞彙。 4. 移除暫存的簡報檔案 (star-cloud-demo-day.pptx)。
This commit is contained in:
parent
6b9b577978
commit
7356304d6f
@ -637,10 +637,11 @@ class WarehouseController extends Controller
|
|||||||
$products = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
$products = Product::with('translations')->orderBy('name')->get(['id', 'name', 'image_url', 'name_dictionary_key']);
|
||||||
|
|
||||||
// 同租戶帳號(用於指派人員)
|
// 同租戶帳號(用於指派人員)
|
||||||
$users = \App\Models\System\User::where('company_id', Auth::user()->company_id)
|
$userQuery = \App\Models\System\User::where('status', 1)->orderBy('name');
|
||||||
->where('status', 'active')
|
if (!Auth::user()->isSystemAdmin()) {
|
||||||
->orderBy('name')
|
$userQuery->where('company_id', Auth::user()->company_id);
|
||||||
->get(['id', 'name']);
|
}
|
||||||
|
$users = $userQuery->get(['id', 'name']);
|
||||||
|
|
||||||
return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines', 'products', 'users'));
|
return view('admin.warehouses.replenishments', compact('orders', 'warehouses', 'machines', 'products', 'users'));
|
||||||
}
|
}
|
||||||
@ -844,6 +845,7 @@ class WarehouseController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'status' => 'required|in:prepared,delivering,completed',
|
'status' => 'required|in:prepared,delivering,completed',
|
||||||
|
'assigned_to' => 'nullable|exists:users,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$newStatus = $validated['status'];
|
$newStatus = $validated['status'];
|
||||||
@ -855,9 +857,14 @@ class WarehouseController extends Controller
|
|||||||
], 422);
|
], 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::transaction(function () use ($replenishmentOrder, $newStatus) {
|
DB::transaction(function () use ($replenishmentOrder, $newStatus, $validated) {
|
||||||
$replenishmentOrder->loadMissing('items');
|
$replenishmentOrder->loadMissing('items');
|
||||||
|
|
||||||
|
// 更新指派人員 (如果有傳遞的話)
|
||||||
|
if (array_key_exists('assigned_to', $validated)) {
|
||||||
|
$replenishmentOrder->assigned_to = $validated['assigned_to'];
|
||||||
|
}
|
||||||
|
|
||||||
// pending → prepared:扣減倉庫庫存
|
// pending → prepared:扣減倉庫庫存
|
||||||
if ($newStatus === ReplenishmentOrder::STATUS_PREPARED) {
|
if ($newStatus === ReplenishmentOrder::STATUS_PREPARED) {
|
||||||
foreach ($replenishmentOrder->items as $item) {
|
foreach ($replenishmentOrder->items as $item) {
|
||||||
@ -1002,23 +1009,26 @@ class WarehouseController extends Controller
|
|||||||
public function assignReplenishment(Request $request, ReplenishmentOrder $replenishmentOrder)
|
public function assignReplenishment(Request $request, ReplenishmentOrder $replenishmentOrder)
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'assigned_to' => 'required|exists:users,id',
|
'assigned_to' => 'nullable|exists:users,id',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 確保指派人員屬於同租戶
|
if ($validated['assigned_to']) {
|
||||||
$user = \App\Models\System\User::where('id', $validated['assigned_to'])
|
// 確保指派人員屬於同租戶
|
||||||
->where('company_id', Auth::user()->company_id)
|
$user = \App\Models\System\User::where('id', $validated['assigned_to'])
|
||||||
->first();
|
->where('company_id', Auth::user()->company_id)
|
||||||
|
->first();
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => __('Invalid personnel selection')
|
'message' => __('Invalid personnel selection')
|
||||||
], 422);
|
], 422);
|
||||||
|
}
|
||||||
|
$replenishmentOrder->assigned_to = $user->id;
|
||||||
|
} else {
|
||||||
|
$replenishmentOrder->assigned_to = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$replenishmentOrder->update(['assigned_to' => $user->id]);
|
|
||||||
|
|
||||||
session()->flash('success', __('Personnel assigned successfully'));
|
session()->flash('success', __('Personnel assigned successfully'));
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
|
|||||||
@ -1470,5 +1470,8 @@
|
|||||||
"All Status": "All Status",
|
"All Status": "All Status",
|
||||||
"Add Pickup Code": "Add Pickup Code",
|
"Add Pickup Code": "Add Pickup Code",
|
||||||
"Add Pass Code": "Add Pass Code",
|
"Add Pass Code": "Add Pass Code",
|
||||||
"Expected Expiry Date & Time": "Expected Expiry Date & Time"
|
"Expected Expiry Date & Time": "Expected Expiry Date & Time",
|
||||||
}
|
"Assign Personnel (Optional)": "Assign Personnel (Optional)",
|
||||||
|
"Unassigned / No Change": "Unassigned / No Change",
|
||||||
|
"You can assign or change the personnel handling this order": "You can assign or change the personnel handling this order"
|
||||||
|
}
|
||||||
|
|||||||
@ -299,5 +299,8 @@
|
|||||||
"Expired": "期限切れ",
|
"Expired": "期限切れ",
|
||||||
"Cancelled": "キャンセル済み",
|
"Cancelled": "キャンセル済み",
|
||||||
"Active": "有効",
|
"Active": "有効",
|
||||||
"All Status": "すべてのステータス"
|
"All Status": "すべてのステータス",
|
||||||
}
|
"Assign Personnel (Optional)": "担当者指定 (オプション)",
|
||||||
|
"Unassigned / No Change": "未指定 / 変更なし",
|
||||||
|
"You can assign or change the personnel handling this order": "この注文の担当者を割り当て、または変更できます"
|
||||||
|
}
|
||||||
|
|||||||
@ -132,6 +132,7 @@
|
|||||||
"Assign Advertisement": "投放廣告",
|
"Assign Advertisement": "投放廣告",
|
||||||
"Assign Machines": "分配機台",
|
"Assign Machines": "分配機台",
|
||||||
"Assign Personnel": "指派人員",
|
"Assign Personnel": "指派人員",
|
||||||
|
"Assign Personnel (Optional)": "指派人員 (選填)",
|
||||||
"Assign replenishment staff": "指派補貨人員",
|
"Assign replenishment staff": "指派補貨人員",
|
||||||
"Assign warehouse managers": "指派倉庫負責人",
|
"Assign warehouse managers": "指派倉庫負責人",
|
||||||
"Assigned Machines": "授權機台",
|
"Assigned Machines": "授權機台",
|
||||||
@ -1476,6 +1477,7 @@
|
|||||||
"Type to search or leave blank for system defaults.": "輸入關鍵字搜尋,或留空以使用系統預設。",
|
"Type to search or leave blank for system defaults.": "輸入關鍵字搜尋,或留空以使用系統預設。",
|
||||||
"UI Elements": "UI元素",
|
"UI Elements": "UI元素",
|
||||||
"Unassigned": "未指派",
|
"Unassigned": "未指派",
|
||||||
|
"Unassigned / No Change": "未指派 / 不變動",
|
||||||
"Unauthorized login attempt: :account": "越權登入嘗試::account",
|
"Unauthorized login attempt: :account": "越權登入嘗試::account",
|
||||||
"Unauthorized Status": "未授權",
|
"Unauthorized Status": "未授權",
|
||||||
"Unauthorized: Account not authorized for this machine": "授權失敗:此帳號無存取該機台的權限",
|
"Unauthorized: Account not authorized for this machine": "授權失敗:此帳號無存取該機台的權限",
|
||||||
@ -1606,5 +1608,5 @@
|
|||||||
"Delete Pass Code": "刪除通行碼",
|
"Delete Pass Code": "刪除通行碼",
|
||||||
"Are you sure you want to delete this pass code?": "確定要刪除此通行碼嗎?",
|
"Are you sure you want to delete this pass code?": "確定要刪除此通行碼嗎?",
|
||||||
"Yes, Delete": "確認刪除",
|
"Yes, Delete": "確認刪除",
|
||||||
"Delete Code": "刪除代碼"
|
"You can assign or change the personnel handling this order": "您可以指派或變更處理此訂單的人員"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
.marker-error { background-color: #ef4444; }
|
.marker-error { background-color: #ef4444; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full bg-slate-50 dark:bg-slate-950 overflow-hidden">
|
<body class="h-full bg-slate-50 dark:bg-slate-950 overflow-hidden" x-data="{ mobileMenuOpen: false }">
|
||||||
<div class="flex h-full relative">
|
<div class="flex h-full relative">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="hidden md:flex flex-col w-96 glass-panel border-r border-slate-200/50 dark:border-slate-800/50 z-20 overflow-hidden">
|
<div class="hidden md:flex flex-col w-96 glass-panel border-r border-slate-200/50 dark:border-slate-800/50 z-20 overflow-hidden">
|
||||||
@ -120,8 +120,8 @@
|
|||||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-slate-400">
|
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-slate-400">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" id="machine-search" onkeyup="updateSearchFilter(this.value)" placeholder="{{ __('Search name or serial...') }}"
|
<input type="text" onkeyup="updateSearchFilter(this.value)" placeholder="{{ __('Search name or serial...') }}"
|
||||||
class="w-full pl-10 pr-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl text-xs font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 focus:border-cyan-500 transition-all shadow-sm">
|
class="machine-search-input w-full pl-10 pr-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl text-xs font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 focus:border-cyan-500 transition-all shadow-sm">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -177,19 +177,119 @@
|
|||||||
|
|
||||||
<!-- Floating Top Header (Mobile) -->
|
<!-- Floating Top Header (Mobile) -->
|
||||||
<div class="md:hidden absolute top-4 left-4 right-4 z-[1000]">
|
<div class="md:hidden absolute top-4 left-4 right-4 z-[1000]">
|
||||||
<div class="glass-panel p-4 rounded-2xl luxury-shadow border border-slate-200/50 dark:border-slate-800/50 flex items-center justify-between">
|
<div class="glass-panel p-3 rounded-2xl luxury-shadow border border-slate-200/50 dark:border-slate-800/50 flex items-center justify-between shadow-lg">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-lg font-black text-slate-800 dark:text-white tracking-tight outfit-font line-clamp-1">
|
<h1 class="text-lg font-black text-slate-800 dark:text-white tracking-tight outfit-font line-clamp-1">
|
||||||
{{ __('Machine Distribution Map') }}
|
{{ __('Machine Distribution Map') }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500 border border-cyan-500/20">
|
<button @click="mobileMenuOpen = true"
|
||||||
|
class="w-10 h-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all active:scale-95 shadow-sm">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M4 6h16M4 12h16m-7 6h7" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Drawer -->
|
||||||
|
<div x-show="mobileMenuOpen"
|
||||||
|
class="md:hidden fixed inset-0 z-[2000] overflow-hidden"
|
||||||
|
x-cloak>
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div x-show="mobileMenuOpen"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
@click="mobileMenuOpen = false"
|
||||||
|
class="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"></div>
|
||||||
|
|
||||||
|
<!-- Drawer Content -->
|
||||||
|
<div x-show="mobileMenuOpen"
|
||||||
|
x-transition:enter="transition ease-out duration-300 transform"
|
||||||
|
x-transition:enter-start="translate-x-full"
|
||||||
|
x-transition:enter-end="translate-x-0"
|
||||||
|
x-transition:leave="transition ease-in duration-200 transform"
|
||||||
|
x-transition:leave-start="translate-x-0"
|
||||||
|
x-transition:leave-end="translate-x-full"
|
||||||
|
class="absolute inset-y-0 right-0 w-[85%] max-w-sm glass-panel shadow-2xl border-l border-white/20 dark:border-slate-800/50 flex flex-col">
|
||||||
|
|
||||||
|
<!-- Drawer Header -->
|
||||||
|
<div class="p-6 border-b border-slate-200/50 dark:border-slate-800/50 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-black text-slate-800 dark:text-white tracking-tight outfit-font">
|
||||||
|
{{ __('Machine List') }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest mt-1">
|
||||||
|
{{ $machines->count() }} {{ __('Units') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button @click="mobileMenuOpen = false" class="p-2 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-white transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drawer Search -->
|
||||||
|
<div class="p-4 bg-slate-50/30 dark:bg-slate-900/30 border-b border-slate-200/50 dark:border-slate-800/50">
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-slate-400">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
|
||||||
|
</span>
|
||||||
|
<input type="text" onkeyup="updateSearchFilter(this.value)" placeholder="{{ __('Search...') }}"
|
||||||
|
class="machine-search-input w-full pl-10 pr-4 py-3 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-cyan-500/20 focus:border-cyan-500 transition-all shadow-sm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drawer Body -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-4 space-y-3 custom-scrollbar">
|
||||||
|
<!-- Reusing model filters -->
|
||||||
|
<div class="mb-6 p-4 rounded-2xl bg-slate-100/50 dark:bg-slate-800/50">
|
||||||
|
<h3 class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3">{{ __('Filter: Model') }}</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
@foreach($groupedByModel as $modelName => $group)
|
||||||
|
@php $color = $modelColors[$modelName] ?? $getFallbackColor($modelName); @endphp
|
||||||
|
<button onclick="filterByModel('{{ $modelName }}')"
|
||||||
|
class="px-3 py-1.5 rounded-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 flex items-center gap-2 hover:border-cyan-500 transition-all">
|
||||||
|
<span class="w-2 h-2 rounded-full" style="background-color: {{ $color }}"></span>
|
||||||
|
<span class="text-[10px] font-bold text-slate-600 dark:text-slate-300">{{ $modelName }}</span>
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
<button onclick="resetFilter()" class="px-3 py-1.5 rounded-full bg-slate-200 dark:bg-slate-700 text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest hover:bg-cyan-500 hover:text-white transition-all">
|
||||||
|
{{ __('All') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($machines as $machine)
|
||||||
|
@php
|
||||||
|
$modelName = $machine->machineModel?->name ?? __('Unknown Model');
|
||||||
|
$color = $modelColors[$modelName] ?? $getFallbackColor($modelName);
|
||||||
|
@endphp
|
||||||
|
<div class="machine-item p-4 rounded-2xl bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 hover:border-cyan-500/30 transition-all cursor-pointer group"
|
||||||
|
data-model="{{ $modelName }}"
|
||||||
|
data-id="{{ $machine->id }}"
|
||||||
|
onclick="focusMachine({{ $machine->latitude }}, {{ $machine->longitude }}, {{ $machine->id }})">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2 h-2 rounded-full" style="background-color: {{ $color }}"></span>
|
||||||
|
<span class="text-sm font-black text-slate-800 dark:text-white group-hover:text-cyan-500 transition-colors">
|
||||||
|
{{ $machine->name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<x-status-badge :status="$machine->status" size="sm" />
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] font-medium text-slate-500 dark:text-slate-400 leading-relaxed truncate">
|
||||||
|
{{ $machine->address ?: $machine->location ?: __('No address information') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -274,6 +374,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function focusMachine(lat, lng, id) {
|
function focusMachine(lat, lng, id) {
|
||||||
|
// Close mobile menu if open
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
try {
|
||||||
|
// Access Alpine state via data-stack
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
if (body && body._x_dataStack) {
|
||||||
|
body._x_dataStack[0].mobileMenuOpen = false;
|
||||||
|
}
|
||||||
|
} catch (e) { console.log('Alpine sync error:', e); }
|
||||||
|
}
|
||||||
|
|
||||||
map.flyTo([lat, lng], 15, {
|
map.flyTo([lat, lng], 15, {
|
||||||
duration: 1.5
|
duration: 1.5
|
||||||
});
|
});
|
||||||
@ -329,13 +440,21 @@
|
|||||||
|
|
||||||
function updateSearchFilter(query) {
|
function updateSearchFilter(query) {
|
||||||
currentSearchQuery = query.toLowerCase().trim();
|
currentSearchQuery = query.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Sync all search inputs
|
||||||
|
document.querySelectorAll('.machine-search-input').forEach(input => {
|
||||||
|
if (input.value.toLowerCase().trim() !== currentSearchQuery) {
|
||||||
|
input.value = query;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFilter() {
|
function resetFilter() {
|
||||||
currentModelFilter = null;
|
currentModelFilter = null;
|
||||||
currentSearchQuery = '';
|
currentSearchQuery = '';
|
||||||
document.getElementById('machine-search').value = '';
|
document.querySelectorAll('.machine-search-input').forEach(input => input.value = '');
|
||||||
applyFilters();
|
applyFilters();
|
||||||
map.setView([23.973875, 120.982024], 8);
|
map.setView([23.973875, 120.982024], 8);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,19 +3,23 @@
|
|||||||
<div class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
<div class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div x-show="showAssignModal" 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"></div>
|
<div x-show="showAssignModal" 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"></div>
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
<div x-show="showAssignModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-white/10">
|
<div x-show="showAssignModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" class="inline-block px-4 pt-5 pb-4 overflow-visible text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-white/10">
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight uppercase">{{ __('Assign Personnel') }}</h3>
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight uppercase">{{ __('Assign Personnel') }}</h3>
|
||||||
<p class="text-sm font-bold text-slate-500 mt-2">{{ __('Select a team member to handle this replenishment order') }}</p>
|
<p class="text-sm font-bold text-slate-500 mt-2">{{ __('Select a team member to handle this replenishment order') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 mb-8">
|
<div class="space-y-3 mb-8">
|
||||||
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Select Personnel') }}</label>
|
<label class="block text-xs font-black text-slate-500 uppercase tracking-[0.15em] pl-1">{{ __('Select Personnel') }}</label>
|
||||||
<select x-model="assignUserId" class="luxury-input w-full px-6 py-3">
|
<x-searchable-select
|
||||||
<option value="">{{ __('Select Personnel') }}</option>
|
id="assign-personnel-select"
|
||||||
|
name="assigned_to"
|
||||||
|
:placeholder="__('Select Personnel')"
|
||||||
|
@change="assignUserId = $event.target.value"
|
||||||
|
>
|
||||||
@foreach($users as $u)
|
@foreach($users as $u)
|
||||||
<option value="{{ $u->id }}">{{ $u->name }}</option>
|
<option value="{{ $u->id }}" data-title="{{ $u->name }}">{{ $u->name }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</x-searchable-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button type="button" @click="showAssignModal = false" class="btn-luxury-ghost px-6">{{ __('Cancel') }}</button>
|
<button type="button" @click="showAssignModal = false" class="btn-luxury-ghost px-6">{{ __('Cancel') }}</button>
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
<div class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div x-show="showStatusModal" 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="showStatusModal = false"></div>
|
<div x-show="showStatusModal" 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="showStatusModal = false"></div>
|
||||||
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen">​</span>
|
||||||
<div x-show="showStatusModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-white/10">
|
<div x-show="showStatusModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" class="inline-block px-4 pt-5 pb-4 overflow-visible text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-8 border border-slate-100 dark:border-white/10">
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div :class="{
|
<div :class="{
|
||||||
'bg-cyan-50 dark:bg-cyan-500/10 text-cyan-600': statusTargetValue === 'prepared',
|
'bg-cyan-50 dark:bg-cyan-500/10 text-cyan-600': statusTargetValue === 'prepared',
|
||||||
@ -21,6 +21,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- 指派人員 (僅在備貨或配送階段顯示,或者使用者想在完成時也能指派) --}}
|
||||||
|
<div class="mt-8 pt-8 border-t border-slate-100 dark:border-white/5 space-y-4">
|
||||||
|
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2 pl-1">{{ __('Assign Personnel (Optional)') }}</label>
|
||||||
|
<div class="relative group">
|
||||||
|
<x-searchable-select
|
||||||
|
id="status-assignee-select"
|
||||||
|
name="assigned_to"
|
||||||
|
:placeholder="__('Unassigned / No Change')"
|
||||||
|
@change="assignUserId = $event.target.value"
|
||||||
|
>
|
||||||
|
@foreach($users as $u)
|
||||||
|
<option value="{{ $u->id }}" data-title="{{ $u->name }}">{{ $u->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</x-searchable-select>
|
||||||
|
</div>
|
||||||
|
<p class="text-[10px] font-bold text-slate-400 pl-1 italic">* {{ __('You can assign or change the personnel handling this order') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse gap-3">
|
<div class="mt-8 sm:mt-10 sm:flex sm:flex-row-reverse gap-3">
|
||||||
<button type="button" @click="executeStatusUpdate()" :disabled="loading"
|
<button type="button" @click="executeStatusUpdate()" :disabled="loading"
|
||||||
:class="{
|
:class="{
|
||||||
|
|||||||
@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
{{-- 狀態推進按鈕(依當前狀態顯示) --}}
|
{{-- 狀態推進按鈕(依當前狀態顯示) --}}
|
||||||
@if($order->status === 'pending')
|
@if($order->status === 'pending')
|
||||||
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'prepared')"
|
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'prepared', '{{ $order->assigned_to }}')"
|
||||||
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
||||||
title="{{ __('Confirm Prepare') }}">
|
title="{{ __('Confirm Prepare') }}">
|
||||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -84,7 +84,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@elseif($order->status === 'prepared')
|
@elseif($order->status === 'prepared')
|
||||||
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'delivering')"
|
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'delivering', '{{ $order->assigned_to }}')"
|
||||||
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 transition-all border border-transparent hover:border-indigo-500/20"
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-indigo-500 hover:bg-indigo-500/5 transition-all border border-transparent hover:border-indigo-500/20"
|
||||||
title="{{ __('Start Delivery') }}">
|
title="{{ __('Start Delivery') }}">
|
||||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -92,7 +92,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@elseif($order->status === 'delivering')
|
@elseif($order->status === 'delivering')
|
||||||
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'completed')"
|
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'completed', '{{ $order->assigned_to }}')"
|
||||||
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
class="p-2.5 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-emerald-500 hover:bg-emerald-500/5 transition-all border border-transparent hover:border-emerald-500/20"
|
||||||
title="{{ __('Confirm Complete') }}">
|
title="{{ __('Confirm Complete') }}">
|
||||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -185,7 +185,7 @@
|
|||||||
{{ __('Details') }}
|
{{ __('Details') }}
|
||||||
</button>
|
</button>
|
||||||
@if($order->status === 'pending')
|
@if($order->status === 'pending')
|
||||||
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'prepared')"
|
<button type="button" @click.stop="advanceStatus('{{ $order->id }}', 'prepared', '{{ $order->assigned_to }}')"
|
||||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-emerald-500/10 text-emerald-500 font-black text-xs uppercase tracking-widest hover:bg-emerald-500 hover:text-white transition-all duration-300 border border-emerald-500/20 shadow-sm">
|
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-emerald-500/10 text-emerald-500 font-black text-xs uppercase tracking-widest hover:bg-emerald-500 hover:text-white transition-all duration-300 border border-emerald-500/20 shadow-sm">
|
||||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
|
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
|
||||||
{{ __('Confirm') }}
|
{{ __('Confirm') }}
|
||||||
|
|||||||
@ -370,9 +370,10 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 狀態推進 (顯示 Modal)
|
// 狀態推進 (顯示 Modal)
|
||||||
async advanceStatus(orderId, newStatus) {
|
async advanceStatus(orderId, newStatus, currentAssigneeId = '') {
|
||||||
this.statusTargetId = orderId;
|
this.statusTargetId = orderId;
|
||||||
this.statusTargetValue = newStatus;
|
this.statusTargetValue = newStatus;
|
||||||
|
this.assignUserId = currentAssigneeId || '';
|
||||||
|
|
||||||
const labels = {
|
const labels = {
|
||||||
prepared: '{{ __("Confirm Prepare") }}',
|
prepared: '{{ __("Confirm Prepare") }}',
|
||||||
@ -388,6 +389,13 @@
|
|||||||
this.statusModalTitle = labels[newStatus];
|
this.statusModalTitle = labels[newStatus];
|
||||||
this.statusModalMessage = messages[newStatus];
|
this.statusModalMessage = messages[newStatus];
|
||||||
this.showStatusModal = true;
|
this.showStatusModal = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const select = window.HSSelect.getInstance('#status-assignee-select');
|
||||||
|
if (select) {
|
||||||
|
select.setValue(currentAssigneeId || ' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 執行狀態更新
|
// 執行狀態更新
|
||||||
@ -402,7 +410,10 @@
|
|||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ status: this.statusTargetValue })
|
body: JSON.stringify({
|
||||||
|
status: this.statusTargetValue,
|
||||||
|
assigned_to: this.assignUserId || null
|
||||||
|
})
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.showStatusModal = false;
|
this.showStatusModal = false;
|
||||||
@ -440,7 +451,18 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 指派
|
// 指派
|
||||||
openAssignModal(orderId) { this.assignOrderId = orderId; this.assignUserId = ''; this.showAssignModal = true; },
|
openAssignModal(orderId) {
|
||||||
|
this.assignOrderId = orderId;
|
||||||
|
this.assignUserId = '';
|
||||||
|
this.showAssignModal = true;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const select = window.HSSelect.getInstance('#assign-personnel-select');
|
||||||
|
if (select) {
|
||||||
|
select.setValue(' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
async executeAssign() {
|
async executeAssign() {
|
||||||
if (!this.assignUserId) { window.showToast?.('{{ __("Select Personnel") }}', 'error'); return; }
|
if (!this.assignUserId) { window.showToast?.('{{ __("Select Personnel") }}', 'error'); return; }
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|||||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user