1. 實作「目前庫存」矩陣化視圖:將商品作為行、倉庫作為列,支援跨倉庫水位分析與自動加總。 2. 優化矩陣 UI:實作固定欄位 (Sticky Columns) 以應對多倉庫場景,並修正深色模式下的背景色對齊問題。 3. 強化倉儲操作驗證:實作調撥與補貨單的 Alpine.js 前端欄位驗證與 AJAX 提交邏輯,提升操作流暢度。 4. 優化機台地址定位:實作結構化地址解析與 Nominatim API 對接,提升經緯度自動獲取的準確性。 5. 標準化 UI 組件:更新 Breadcrumbs、刪除確認彈窗與搜尋下拉選單,確保全站視覺一致性。 6. 完善多語系支援:補全倉儲管理、機台設定與 UI 提示的繁體中文翻譯。 7. 增加 Product Model 的 stocks() 關聯,支援高效率的庫存矩陣查詢。
547 lines
39 KiB
PHP
547 lines
39 KiB
PHP
@extends('layouts.admin')
|
|
|
|
@section('content')
|
|
<script>
|
|
function replenishmentManager() {
|
|
return {
|
|
showModal: false,
|
|
fromId: '',
|
|
availableProducts: [],
|
|
allProducts: @json(\App\Models\Product\Product::orderBy('name')->get(['id', 'name'])),
|
|
isLoadingProducts: false,
|
|
items: [{ product_id: '', slot_no: '', quantity: 1 }],
|
|
loading: false,
|
|
|
|
// Details Panel State
|
|
showDetailsPanel: false,
|
|
detailsLoading: false,
|
|
selectedOrder: null,
|
|
orderItems: [],
|
|
|
|
async openDetails(id) {
|
|
this.showDetailsPanel = true;
|
|
this.detailsLoading = true;
|
|
this.selectedOrder = null;
|
|
this.orderItems = [];
|
|
|
|
try {
|
|
const response = await fetch(`{{ url('/admin/warehouses/replenishments') }}/${id}/details`, {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
this.selectedOrder = data.order;
|
|
this.orderItems = data.items;
|
|
} else {
|
|
throw new Error(data.message || 'Failed to load details');
|
|
}
|
|
} catch (e) {
|
|
console.error('openDetails error:', e);
|
|
window.showToast?.('{{ __("Failed to load details") }}', 'error');
|
|
} finally {
|
|
this.detailsLoading = false;
|
|
this.$nextTick(() => {
|
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
|
});
|
|
}
|
|
},
|
|
|
|
openCreateModal() {
|
|
this.items = [{ product_id: '', slot_no: '', quantity: 1 }];
|
|
this.showModal = true;
|
|
this.$nextTick(() => {
|
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
|
});
|
|
},
|
|
|
|
addItem() {
|
|
this.items.push({ product_id: '', slot_no: '', quantity: 1 });
|
|
this.$nextTick(() => {
|
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
|
});
|
|
},
|
|
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
|
|
|
async fetchStock() {
|
|
this.availableProducts = [];
|
|
if (!this.fromId || this.fromId.trim() === '') return;
|
|
|
|
this.isLoadingProducts = true;
|
|
try {
|
|
const res = await fetch('{{ route('admin.warehouses.ajax.stock') }}?warehouse_id=' + this.fromId);
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
this.availableProducts = json.data;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
this.isLoadingProducts = false;
|
|
this.$nextTick(() => {
|
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
|
});
|
|
}
|
|
},
|
|
|
|
async submitReplenishment() {
|
|
const form = document.getElementById('replenishmentForm');
|
|
const warehouseId = form.querySelector('[name="warehouse_id"]')?.value;
|
|
const machineId = form.querySelector('[name="machine_id"]')?.value;
|
|
|
|
if (!warehouseId) {
|
|
window.showToast?.('{{ __("Please select source warehouse") }}', 'error');
|
|
return;
|
|
}
|
|
if (!machineId) {
|
|
window.showToast?.('{{ __("Please select target machine") }}', 'error');
|
|
return;
|
|
}
|
|
|
|
if (this.items.length === 0) {
|
|
window.showToast?.('{{ __("Please add at least one item") }}', 'error');
|
|
return;
|
|
}
|
|
|
|
let hasError = false;
|
|
this.items.forEach(item => {
|
|
if (!item.slot_no || !item.product_id || !item.quantity || item.quantity < 1) {
|
|
hasError = true;
|
|
}
|
|
});
|
|
|
|
if (hasError) {
|
|
window.showToast?.('{{ __("Please select slot, product and valid quantity for all items") }}', 'error');
|
|
return;
|
|
}
|
|
|
|
this.loading = true;
|
|
try {
|
|
const formData = new FormData(form);
|
|
const response = await fetch(form.action, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
window.showToast?.(result.message, 'success');
|
|
this.showModal = false;
|
|
window.location.reload();
|
|
} else {
|
|
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
window.showToast?.('{{ __("System Error") }}', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="space-y-3 pb-10" x-data="replenishmentManager()" @open-details.window="openDetails($event.detail.id)">
|
|
|
|
{{-- Header --}}
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Replenishment') }}</h1>
|
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Create and manage replenishment orders from warehouse to machines') }}</p>
|
|
</div>
|
|
<button type="button" @click="openCreateModal()" class="btn-luxury-primary">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
|
|
<span>{{ __('New Replenishment') }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{{-- Filters --}}
|
|
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
|
<form action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="mb-8">
|
|
<div class="flex items-center p-1 bg-slate-100/50 dark:bg-slate-900/50 backdrop-blur-md rounded-2xl border border-slate-200/50 dark:border-slate-700/50 w-fit">
|
|
<a href="{{ route('admin.warehouses.replenishments') }}"
|
|
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ !request()->filled('status') ? 'bg-white dark:bg-slate-800 text-cyan-600 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
|
{{ __('All') }}
|
|
</a>
|
|
@foreach(['pending' => 'Pending', 'prepared' => 'Prepared', 'delivering' => 'Delivering', 'completed' => 'Completed'] as $key => $label)
|
|
<a href="{{ route('admin.warehouses.replenishments', ['status' => $key]) }}"
|
|
class="px-5 py-2 text-xs font-black tracking-widest uppercase transition-all duration-300 rounded-xl {{ request('status') === $key ? 'bg-white dark:bg-slate-800 text-cyan-600 shadow-lg shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600' }}">
|
|
{{ __($label) }}
|
|
</a>
|
|
@endforeach
|
|
</div>
|
|
</form>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left border-separate border-spacing-y-0">
|
|
<thead>
|
|
<tr class="bg-slate-50/50 dark:bg-slate-900/10">
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Order No.') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Machine') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Warehouse') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Items') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Status') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800">{{ __('Created By') }}</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-50 dark:divide-slate-800/80">
|
|
@forelse($orders as $order)
|
|
<tr class="group hover:bg-slate-50/80 dark:hover:bg-slate-800/40 transition-all duration-300">
|
|
<td class="px-6 py-3.5">
|
|
<div class="flex flex-col cursor-pointer" @click="openDetails('{{ $order->id }}')">
|
|
<span class="font-mono font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 transition-colors">{{ $order->order_no }}</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-3.5">
|
|
<div class="flex flex-col">
|
|
<span class="font-bold text-slate-700 dark:text-slate-200">{{ $order->machine?->name }}</span>
|
|
<span class="text-xs font-mono text-slate-400">{{ $order->machine?->serial_no }}</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-3.5"><span class="font-bold text-slate-600 dark:text-slate-300">{{ $order->warehouse?->name }}</span></td>
|
|
<td class="px-6 py-3.5 text-center"><span class="font-black text-slate-800 dark:text-white">{{ $order->items_count }}</span></td>
|
|
<td class="px-6 py-3.5 text-center">
|
|
@php
|
|
$colors = ['pending' => 'amber', 'prepared' => 'cyan', 'delivering' => 'indigo', 'completed' => 'emerald', 'cancelled' => 'slate'];
|
|
$sColor = $colors[$order->status] ?? 'cyan';
|
|
@endphp
|
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-[10px] font-black bg-{{ $sColor }}-500/10 text-{{ $sColor }}-500 border border-{{ $sColor }}-500/20 tracking-widest uppercase">{{ __(ucfirst($order->status)) }}</span>
|
|
</td>
|
|
<td class="px-6 py-3.5"><span class="text-sm font-bold text-slate-500">{{ $order->creator?->name ?? '-' }}</span></td>
|
|
<td class="px-6 py-3.5 text-right whitespace-nowrap">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button type="button" @click="openDetails('{{ $order->id }}')"
|
|
class="p-2.5 rounded-xl bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 transition-all border border-transparent hover:border-cyan-500/20"
|
|
title="{{ __('View Details') }}">
|
|
<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="M2.036 12.322a1.012 1.012 0 010-.644C3.399 8.049 7.21 5 12 5c4.79 0 8.601 3.049 9.964 6.678.045.133.045.278 0 .412C20.601 15.951 16.79 19 12 19c-4.79 0-8.601-3.049-9.964-6.678z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
@if(in_array($order->status, ['pending', 'prepared', 'delivering']))
|
|
<form action="{{ route('admin.warehouses.replenishments.confirm', $order->id) }}" method="POST" class="inline" onsubmit="return confirm('{{ __('Confirm replenishment completed?') }}')">
|
|
@csrf @method('PATCH')
|
|
<button type="submit" class="px-4 py-2 rounded-xl text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-all">{{ __('Complete') }}</button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr><td colspan="7" class="px-6 py-20 text-center text-slate-400 font-bold">{{ __('No replenishment orders found') }}</td></tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $orders->links('vendor.pagination.luxury') }}</div>
|
|
</div>
|
|
|
|
{{-- New Replenishment Modal --}}
|
|
<div x-show="showModal" class="fixed inset-0 z-[110] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true" x-cloak>
|
|
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
<!-- Background Backdrop -->
|
|
<div x-show="showModal" 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"
|
|
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
|
@click="showModal = false"></div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
|
|
|
<!-- Modal Panel -->
|
|
<div x-show="showModal"
|
|
x-transition:enter="ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave="ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
class="relative inline-flex flex-col align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] text-left shadow-2xl transform sm:my-8 sm:align-middle sm:max-w-3xl w-full border border-slate-100 dark:border-slate-800 z-10 max-h-[90vh]"
|
|
@click.stop>
|
|
|
|
<!-- Modal Header -->
|
|
<div class="px-10 py-8 pb-4 flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-3">
|
|
{{ __('New Replenishment') }}
|
|
</h3>
|
|
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
|
{{ __('Create and manage replenishment orders from warehouse to machines') }}
|
|
</p>
|
|
</div>
|
|
<button @click="showModal = false"
|
|
class="p-2.5 rounded-full bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-all border border-slate-100 dark:border-slate-700 shadow-sm">
|
|
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto px-10 py-2 custom-scrollbar overflow-x-visible">
|
|
<form id="replenishmentForm" action="{{ route('admin.warehouses.replenishments.store') }}" method="POST" class="space-y-6 pb-20">
|
|
@csrf
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="space-y-3">
|
|
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
|
{{ __('Source Warehouse') }} <span class="text-rose-500">*</span>
|
|
</label>
|
|
<x-searchable-select
|
|
name="warehouse_id"
|
|
:options="$warehouses ?? []"
|
|
:placeholder="__('Select Warehouse')"
|
|
class="w-full"
|
|
required
|
|
x-model="fromId"
|
|
@change="fetchStock()"
|
|
/>
|
|
</div>
|
|
<div class="space-y-3">
|
|
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
|
{{ __('Target Machine') }} <span class="text-rose-500">*</span>
|
|
</label>
|
|
<x-searchable-select
|
|
name="machine_id"
|
|
:options="$machines->map(fn($m) => (object)['id' => $m->id, 'name' => $m->name . ' (' . $m->serial_no . ')'])"
|
|
:placeholder="__('Select Machine')"
|
|
class="w-full"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] pl-1">
|
|
{{ __('Note') }}
|
|
</label>
|
|
<input type="text" name="note" class="luxury-input w-full px-6 py-4 bg-slate-50/50 dark:bg-slate-900/50"
|
|
placeholder="{{ __('Optional remarks...') }}">
|
|
</div>
|
|
|
|
{{-- Items --}}
|
|
<div class="space-y-5">
|
|
<div class="flex items-center justify-between pl-1">
|
|
<label class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em]">
|
|
{{ __('Replenishment Items') }} <span class="text-rose-500">*</span>
|
|
</label>
|
|
<button type="button" @click="addItem()"
|
|
class="text-xs font-black text-cyan-500 hover:text-cyan-400 uppercase tracking-widest flex items-center gap-1.5 transition-colors">
|
|
<svg class="w-4 h-4 stroke-[3]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
{{ __('Add Product') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<template x-for="(item, index) in items" :key="index">
|
|
<div class="group flex items-center gap-3 p-3 bg-slate-50/50 dark:bg-slate-900/30 rounded-2xl border border-slate-100 dark:border-slate-800/50 hover:border-cyan-500/30 transition-all duration-300">
|
|
<div class="w-20">
|
|
<input type="number" :name="'items['+index+'][slot_no]'" x-model="item.slot_no" min="1" required
|
|
class="luxury-input w-full px-3 py-2 text-center text-sm font-mono font-bold" placeholder="{{ __('Slot') }}">
|
|
</div>
|
|
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
|
<div class="flex-1 min-w-[200px]" :key="'select-wrapper-' + index + '-' + (fromId ? 'filtered-' + fromId + '-' + availableProducts.length : 'all')">
|
|
<select :id="'product-select-' + index + '-' + Date.now()" :name="'items['+index+'][product_id]'" x-model="item.product_id" required
|
|
data-hs-select='{"placeholder": "{{ __('Select Product') }}","toggleClasses": "hs-select-toggle luxury-select-toggle w-full text-left","toggleTemplate": "<button type=\"button\"><span class=\"text-slate-800 dark:text-slate-200\" data-title></span><div class=\"ms-auto\"><svg class=\"size-4 text-slate-400\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg></div></button>","dropdownClasses": "hs-select-menu w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-white/10 rounded-xl shadow-[0_20px_50px_rgba(0,0,0,0.3)] mt-2 z-[150] max-h-72 overflow-y-auto custom-scrollbar-thin","optionClasses": "hs-select-option py-2 px-3 text-sm text-slate-800 dark:text-slate-300 cursor-pointer hover:bg-slate-100 dark:hover:bg-cyan-500/10 dark:hover:text-cyan-400 rounded-lg","hasSearch": true,"searchPlaceholder": "{{ __('Search Product') }}","searchClasses": "block w-[calc(100%-16px)] mx-2 py-2 px-3 text-sm border-slate-200 dark:border-white/10 rounded-lg focus:border-cyan-500 focus:ring-cyan-500 bg-slate-50 dark:bg-slate-900/50 dark:text-slate-200","searchWrapperClasses": "sticky top-0 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md p-2 z-10","strategy": "fixed"}' class="hidden">
|
|
<option value="">{{ __('Select Product') }}</option>
|
|
<template x-for="p in (fromId ? availableProducts : allProducts)" :key="p.id">
|
|
<option :value="p.id"
|
|
x-text="p.name + (p.quantity !== undefined ? ' (' + '{{ __('Stock') }}: ' + p.quantity + ')' : '')"
|
|
:data-title="p.name + (p.quantity !== undefined ? ' (' + '{{ __('Stock') }}: ' + p.quantity + ')' : '')">
|
|
</option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="w-px h-6 bg-slate-200 dark:bg-slate-800/50"></div>
|
|
<div class="w-20">
|
|
<input type="number" :name="'items['+index+'][quantity]'" x-model="item.quantity" min="1" required
|
|
class="luxury-input w-full px-3 py-2 text-center text-sm font-mono font-bold" placeholder="{{ __('Qty') }}">
|
|
</div>
|
|
<button type="button" @click="removeItem(index)"
|
|
class="p-2 text-slate-300 hover:text-rose-500 transition-all transform hover:scale-110"
|
|
x-show="items.length > 1">
|
|
<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="M6 18 18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Modal Footer -->
|
|
<div class="px-10 py-6 border-t border-slate-100 dark:border-slate-800/50 flex items-center justify-end gap-4">
|
|
<button type="button" @click="showModal = false" class="btn-luxury-ghost px-8">
|
|
{{ __('Cancel') }}
|
|
</button>
|
|
<button type="button" @click="submitReplenishment()"
|
|
:disabled="loading"
|
|
class="btn-luxury-primary px-12 relative flex items-center justify-center">
|
|
<span :class="loading ? 'opacity-0' : ''">{{ __('Create') }}</span>
|
|
<template x-if="loading">
|
|
<div class="absolute inset-0 flex items-center justify-center">
|
|
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</div>
|
|
</template>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Details Slide-over -->
|
|
<div x-show="showDetailsPanel"
|
|
class="fixed inset-0 z-[120] overflow-hidden"
|
|
style="display: none;"
|
|
x-cloak>
|
|
<div class="absolute inset-0 overflow-hidden">
|
|
<!-- Backdrop -->
|
|
<div x-show="showDetailsPanel"
|
|
x-transition:enter="ease-in-out duration-500" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
|
x-transition:leave="ease-in-out duration-500" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
|
class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
|
@click="showDetailsPanel = false"></div>
|
|
|
|
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
|
|
<div x-show="showDetailsPanel"
|
|
x-transition:enter="transform transition ease-in-out duration-500 sm:duration-700" x-transition:enter-start="translate-x-full" x-transition:enter-end="translate-x-0"
|
|
x-transition:leave="transform transition ease-in-out duration-500 sm:duration-700" x-transition:leave-start="translate-x-0" x-transition:leave-end="translate-x-full"
|
|
class="pointer-events-auto w-screen max-w-md">
|
|
<div class="flex h-full flex-col overflow-y-scroll bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
|
<!-- Header -->
|
|
<div class="bg-slate-50/50 dark:bg-slate-900/50 px-6 py-8 border-b border-slate-100 dark:border-slate-800">
|
|
<div class="flex items-start justify-between">
|
|
<h2 class="text-xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none">
|
|
{{ __('Replenishment Details') }}
|
|
</h2>
|
|
<div class="ml-3 flex h-7 items-center">
|
|
<button type="button" @click="showDetailsPanel = false"
|
|
class="rounded-full bg-white dark:bg-slate-800 p-2 text-slate-400 hover:text-slate-500 dark:hover:text-slate-300 focus:outline-none border border-slate-100 dark:border-slate-700 shadow-sm transition-all">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4">
|
|
<template x-if="selectedOrder">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-2xl font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="selectedOrder.order_no"></span>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-[0.2em]" x-text="selectedOrder.warehouse_name"></span>
|
|
<svg class="w-3 h-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
|
|
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.2em]" x-text="selectedOrder.machine_name"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="relative flex-1 px-6 py-8">
|
|
<!-- Loading Spinner -->
|
|
<div x-show="detailsLoading" class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-[2px] z-10 flex items-center justify-center">
|
|
<div class="w-10 h-10 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin"></div>
|
|
</div>
|
|
|
|
<template x-if="selectedOrder">
|
|
<div class="space-y-10">
|
|
<!-- Info Grid -->
|
|
<div class="grid grid-cols-2 gap-6 p-6 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
|
|
<div class="space-y-1">
|
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Status') }}</p>
|
|
<div>
|
|
@php
|
|
$statusMap = [
|
|
'pending' => ['color' => 'amber', 'label' => __('Pending')],
|
|
'prepared' => ['color' => 'cyan', 'label' => __('Prepared')],
|
|
'delivering' => ['color' => 'indigo', 'label' => __('Delivering')],
|
|
'completed' => ['color' => 'emerald', 'label' => __('Completed')],
|
|
'cancelled' => ['color' => 'slate', 'label' => __('Cancelled')],
|
|
];
|
|
@endphp
|
|
<template x-for="(config, status) in {{ json_encode($statusMap) }}" :key="status">
|
|
<template x-if="selectedOrder.status === status">
|
|
<span :class="'inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black bg-' + config.color + '-500/10 text-' + config.color + '-500 border border-' + config.color + '-500/20 uppercase tracking-widest'" x-text="config.label"></span>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Created By') }}</p>
|
|
<p class="text-xs font-bold text-slate-700 dark:text-slate-300" x-text="selectedOrder.creator_name"></p>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Created At') }}</p>
|
|
<p class="text-[11px] font-mono font-bold text-slate-600 dark:text-slate-400" x-text="selectedOrder.created_at"></p>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Completed At') }}</p>
|
|
<p class="text-[11px] font-mono font-bold text-slate-600 dark:text-slate-400" x-text="selectedOrder.completed_at || '-'"></p>
|
|
</div>
|
|
<template x-if="selectedOrder.note">
|
|
<div class="col-span-2 space-y-1 pt-2 border-t border-slate-200/50 dark:border-slate-700/50">
|
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Note') }}</p>
|
|
<p class="text-xs font-bold text-slate-600 dark:text-slate-400 italic" x-text="selectedOrder.note"></p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Items List -->
|
|
<div class="space-y-5">
|
|
<div class="flex items-center justify-between px-1">
|
|
<h3 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Replenishment Items') }}</h3>
|
|
<span class="text-[10px] font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="orderItems.length + ' {{ __('Items') }}'"></span>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<template x-for="(item, idx) in orderItems" :key="idx">
|
|
<div class="group flex items-center gap-4 p-4 bg-white dark:bg-slate-900 rounded-2xl border border-slate-100 dark:border-slate-800 hover:border-cyan-500/30 transition-all duration-300">
|
|
<div class="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center font-mono font-bold text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-700" x-text="item.slot_no"></div>
|
|
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-100 dark:border-slate-800 group-hover:scale-105 transition-transform">
|
|
<template x-if="item.image_url">
|
|
<img :src="item.image_url" class="w-full h-full object-cover">
|
|
</template>
|
|
<template x-if="!item.image_url">
|
|
<svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</template>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm font-black text-slate-800 dark:text-slate-100 truncate tracking-tight" x-text="item.product_name"></p>
|
|
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Product ID') }}: <span class="text-slate-500 dark:text-slate-400" x-text="item.product_id"></span></p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-base font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="'x' + item.quantity"></p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|