1. 修正客戶管理頁面 H1 標題與麵包屑導覽路徑。 2. 在機台編輯頁面實作分頁功能(基本、營運、機台系統設定)。 3. 實作極簡奢華風的機台系統設定 UI,按功能模組(電子發票、刷卡、支付、購物車等)進行卡片式分組。 4. 將系統設定資料從 Company JSON 遷移至 Machine 資料表獨立欄位,並更新關聯控制器與模型邏輯。 5. 清理 CompanyController 與視圖中的冗餘設定邏輯。 6. 完成現有資料遷移,確保機台正確繼承客戶層級之設定。 7. 優化倉儲管理「現有庫存」矩陣視圖與導覽功能。
636 lines
40 KiB
PHP
636 lines
40 KiB
PHP
@extends('layouts.admin')
|
|
|
|
@section('content')
|
|
<script>
|
|
function inventoryManager() {
|
|
return {
|
|
activeTab: '{{ $tab }}',
|
|
tabLoading: null,
|
|
loading: false,
|
|
showStockInModal: false,
|
|
showConfirmModal: false,
|
|
pendingForm: null,
|
|
items: [{ product_id: '', quantity: 1 }],
|
|
|
|
// Order Details (Renamed to match index.blade.php style)
|
|
showOrderDetails: false,
|
|
detailsLoading: false,
|
|
activeOrder: null,
|
|
activeItems: [],
|
|
|
|
openStockInModal() {
|
|
this.items = [{ product_id: '', quantity: 1 }];
|
|
this.showStockInModal = true;
|
|
this.$nextTick(() => {
|
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
|
});
|
|
},
|
|
|
|
async openOrderDetails(id) {
|
|
console.log('openOrderDetails triggered for ID:', id);
|
|
this.showOrderDetails = true;
|
|
this.detailsLoading = true;
|
|
this.activeOrder = null;
|
|
this.activeItems = [];
|
|
|
|
try {
|
|
const response = await fetch(`/admin/warehouses/inventory/stock-in/${id}/details`, {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
this.activeOrder = data.order;
|
|
this.activeItems = data.items;
|
|
} else {
|
|
throw new Error(data.message || 'Failed to load details');
|
|
}
|
|
} catch (e) {
|
|
console.error('openOrderDetails error:', e);
|
|
window.showToast?.('{{ __("Failed to load details") }}', 'error');
|
|
} finally {
|
|
this.detailsLoading = false;
|
|
}
|
|
},
|
|
|
|
init() {
|
|
this.$watch('activeTab', (newTab, oldTab) => {
|
|
const container = document.getElementById('tab-' + newTab + '-container');
|
|
if (container && container.innerHTML.trim() === '') {
|
|
this.fetchTabData(newTab);
|
|
}
|
|
|
|
// Update URL
|
|
const url = new URL(window.location.origin + window.location.pathname);
|
|
url.searchParams.set('tab', newTab);
|
|
window.history.pushState({}, '', url);
|
|
});
|
|
},
|
|
|
|
addItem() {
|
|
this.items.push({ product_id: '', quantity: 1 });
|
|
this.$nextTick(() => {
|
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit('select');
|
|
});
|
|
},
|
|
removeItem(i) { if (this.items.length > 1) this.items.splice(i, 1); },
|
|
|
|
async fetchTabData(tab, url = null) {
|
|
this.tabLoading = tab;
|
|
const container = document.getElementById('tab-' + tab + '-container');
|
|
|
|
if (!url) {
|
|
const form = container?.querySelector('form');
|
|
let params = new URLSearchParams();
|
|
params.set('tab', tab);
|
|
params.set('_ajax', '1');
|
|
|
|
if (form) {
|
|
const formData = new FormData(form);
|
|
formData.forEach((value, key) => {
|
|
if (value.trim() !== '') params.set(key, value);
|
|
});
|
|
}
|
|
url = `${window.location.pathname}?${params.toString()}`;
|
|
} else {
|
|
const urlObj = new URL(url, window.location.origin);
|
|
urlObj.searchParams.set('tab', tab);
|
|
urlObj.searchParams.set('_ajax', '1');
|
|
url = urlObj.toString();
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
if (container) {
|
|
container.innerHTML = data.html;
|
|
this.$nextTick(() => {
|
|
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
|
|
});
|
|
}
|
|
|
|
const historyUrl = new URL(url, window.location.origin);
|
|
historyUrl.searchParams.delete('_ajax');
|
|
window.history.pushState({}, '', historyUrl.toString());
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
window.showToast?.('{{ __("Loading failed") }}', 'error');
|
|
} finally {
|
|
this.tabLoading = null;
|
|
}
|
|
},
|
|
|
|
async submitStockIn() {
|
|
const form = document.getElementById('stockInForm');
|
|
const warehouseId = form.querySelector('[name="warehouse_id"]')?.value;
|
|
|
|
if (!warehouseId) {
|
|
window.showToast?.('{{ __("Please select target warehouse") }}', '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.product_id || !item.quantity || item.quantity < 1) {
|
|
hasError = true;
|
|
}
|
|
});
|
|
|
|
if (hasError) {
|
|
window.showToast?.('{{ __("Please select 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.showStockInModal = false;
|
|
this.fetchTabData('stock-in');
|
|
} else {
|
|
window.showToast?.(result.message || '{{ __("Validation failed") }}', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
window.showToast?.('{{ __("System Error") }}', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
handleFilterSubmit(tab) {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete('page');
|
|
this.fetchTabData(tab);
|
|
},
|
|
confirmStockIn(form) {
|
|
this.pendingForm = form;
|
|
this.showConfirmModal = true;
|
|
},
|
|
|
|
async doConfirmStockIn() {
|
|
if (!this.pendingForm) return;
|
|
|
|
const form = this.pendingForm;
|
|
this.showConfirmModal = false;
|
|
this.loading = true;
|
|
|
|
try {
|
|
const response = await fetch(form.action, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: new FormData(form)
|
|
});
|
|
const data = await response.json();
|
|
if (data.success || response.ok) {
|
|
window.showToast?.('{{ __("Confirmed") }}', 'success');
|
|
this.fetchTabData('stock-in');
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
window.showToast?.('{{ __("Operation failed") }}', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
this.pendingForm = null;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
|
|
<div class="relative space-y-2 pb-20" x-data="inventoryManager()" data-tab="{{ $tab }}"
|
|
data-index-url="{{ route('admin.warehouses.inventory') }}"
|
|
@ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)">
|
|
|
|
|
|
<!-- Header -->
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Inventory Management') }}</h1>
|
|
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
|
|
{{ __('Track stock-in orders and movement history') }}
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<template x-if="activeTab === 'stock'">
|
|
<button type="button" @click="openStockInModal()"
|
|
class="btn-luxury-primary transition-all duration-300">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
<span>{{ __('New Stock-In Order') }}</span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-1 p-1.5 bg-slate-100 dark:bg-slate-900/50 rounded-2xl w-fit border border-slate-200/50 dark:border-slate-800/50"
|
|
aria-label="Tabs">
|
|
<button type="button" @click="activeTab = 'stock'"
|
|
:class="activeTab === 'stock' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
|
{{ __('Current Stocks') }}
|
|
</button>
|
|
<button type="button" @click="activeTab = 'stock-in'"
|
|
:class="activeTab === 'stock-in' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
|
{{ __('Stock-In Orders') }}
|
|
</button>
|
|
<button type="button" @click="activeTab = 'movements'"
|
|
:class="activeTab === 'movements' ? 'bg-white dark:bg-slate-800 text-cyan-600 dark:text-cyan-400 shadow-sm shadow-cyan-500/10' : 'text-slate-400 hover:text-slate-600 dark:hover:text-slate-200'"
|
|
class="px-8 py-3 rounded-xl text-sm font-black uppercase tracking-widest transition-all duration-300">
|
|
{{ __('Movement History') }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab Contents -->
|
|
<div class="mt-6">
|
|
<!-- Stock List Tab -->
|
|
<div x-show="activeTab === 'stock'" class="relative min-h-[300px]"
|
|
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
|
x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div x-show="tabLoading === 'stock'" 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"
|
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center rounded-3xl"
|
|
x-cloak>
|
|
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
|
|
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
|
|
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
|
|
<div class="relative w-8 h-8 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
|
</div>
|
|
|
|
<div id="tab-stock-container" class="relative">
|
|
@if($tab === 'stock') @include('admin.warehouses.partials.tab-stock') @endif
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Stock-In Orders Tab -->
|
|
<div x-show="activeTab === 'stock-in'" class="relative min-h-[300px]"
|
|
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
|
x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div x-show="tabLoading === 'stock-in'" 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"
|
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center rounded-3xl"
|
|
x-cloak>
|
|
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
|
|
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
|
|
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
|
|
<div class="relative w-8 h-8 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
|
</div>
|
|
|
|
<div id="tab-stock-in-container" class="relative">
|
|
@if($tab === 'stock-in') @include('admin.warehouses.partials.tab-stock-in') @endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Movements Tab -->
|
|
<div x-show="activeTab === 'movements'" class="relative min-h-[300px]"
|
|
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
|
x-transition:enter-end="opacity-100 translate-y-0" x-cloak>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div x-show="tabLoading === 'movements'" 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"
|
|
class="absolute inset-0 z-20 bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px] flex flex-col items-center justify-center rounded-3xl"
|
|
x-cloak>
|
|
<div class="relative w-16 h-16 mb-4 flex items-center justify-center">
|
|
<div class="absolute inset-0 rounded-full border-2 border-transparent border-t-cyan-500 border-r-cyan-500/30 animate-spin"></div>
|
|
<div class="absolute inset-2 rounded-full border border-cyan-500/10 animate-spin" style="animation-duration: 3s; direction: reverse;"></div>
|
|
<div class="relative w-8 h-8 flex items-center justify-center">
|
|
<svg class="w-6 h-6 text-cyan-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
|
</div>
|
|
|
|
<div id="tab-movements-container" class="relative">
|
|
@if($tab === 'movements') @include('admin.warehouses.partials.tab-movements') @endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stock-In Modal -->
|
|
<div x-show="showStockInModal" 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="showStockInModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
|
|
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="showStockInModal = false"></div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">​</span>
|
|
|
|
<!-- Modal Panel -->
|
|
<div x-show="showStockInModal"
|
|
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 Stock-In Order') }}
|
|
</h3>
|
|
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">
|
|
{{ __('Add new inventory items to your warehouse') }}
|
|
</p>
|
|
</div>
|
|
<button @click="showStockInModal = 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="stockInForm" action="{{ route('admin.warehouses.inventory.stock-in.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">
|
|
{{ __('Target Warehouse') }} <span class="text-rose-500">*</span>
|
|
</label>
|
|
<x-searchable-select
|
|
name="warehouse_id"
|
|
:options="$warehouses ?? []"
|
|
:selected="request('warehouse_id')"
|
|
:placeholder="__('Select Warehouse')"
|
|
/>
|
|
</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 py-3" placeholder="{{ __('Optional remarks') }}">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Products Section -->
|
|
<div class="space-y-4 pt-4">
|
|
<div class="flex items-center justify-between px-1">
|
|
<h4 class="text-sm font-black text-slate-800 dark:text-white uppercase tracking-widest">{{ __('Product List') }}</h4>
|
|
<button type="button" @click="addItem()" class="text-xs font-black text-cyan-600 hover:text-cyan-700 flex items-center gap-1 transition-colors uppercase tracking-widest">
|
|
<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>
|
|
{{ __('Add Product') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<template x-for="(item, index) in items" :key="index">
|
|
<div class="flex items-end gap-4 p-4 bg-slate-50 dark:bg-slate-800/50 rounded-2xl border border-slate-100 dark:border-slate-800 transition-all">
|
|
<div class="flex-1 space-y-2">
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Product') }}</label>
|
|
<x-searchable-select
|
|
x-bind:name="'items['+index+'][product_id]'"
|
|
:options="$products ?? []"
|
|
placeholder="{{ __('Select Product') }}"
|
|
/>
|
|
</div>
|
|
<div class="w-32 space-y-2">
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Quantity') }}</label>
|
|
<input type="number" x-model="item.quantity" x-bind:name="'items['+index+'][quantity]'" min="1"
|
|
class="luxury-input py-2.5">
|
|
</div>
|
|
<button type="button" @click="removeItem(index)"
|
|
class="p-2.5 mb-0.5 rounded-xl text-slate-400 hover:text-rose-500 hover:bg-rose-500/10 transition-all">
|
|
<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" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></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="showStockInModal = false" class="btn-luxury-ghost px-8">
|
|
{{ __('Cancel') }}
|
|
</button>
|
|
<button type="button" @click="submitStockIn()"
|
|
:disabled="loading"
|
|
class="btn-luxury-primary px-12 relative flex items-center justify-center">
|
|
<span :class="loading ? 'opacity-0' : ''">{{ __('Confirm Stock-In') }}</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>
|
|
|
|
<x-confirm-modal
|
|
alpineVar="showConfirmModal"
|
|
confirmAction="doConfirmStockIn()"
|
|
iconType="info"
|
|
confirmColor="emerald"
|
|
:title="__('Confirm Stock-in Order')"
|
|
:message="__('Are you sure you want to confirm this stock-in order? This action will update your inventory levels.')"
|
|
:confirmText="__('Confirm')"
|
|
/>
|
|
|
|
<!-- Order Details Slide-over (Rewritten to match index.blade.php structure) -->
|
|
<div x-show="showOrderDetails" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;" x-cloak>
|
|
<div class="absolute inset-0 overflow-hidden">
|
|
<!-- Backdrop -->
|
|
<div x-show="showOrderDetails"
|
|
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="showOrderDetails = false"></div>
|
|
|
|
<div class="fixed inset-y-0 right-0 max-w-full flex">
|
|
<div x-show="showOrderDetails"
|
|
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="w-screen max-w-2xl">
|
|
|
|
<div class="h-full flex flex-col bg-white dark:bg-slate-900 shadow-2xl border-l border-slate-100 dark:border-slate-800">
|
|
<!-- Header -->
|
|
<div class="px-5 py-6 sm:px-8 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="min-w-0 flex-1">
|
|
<h2 class="text-xl sm:text-2xl font-black text-slate-800 dark:text-white font-display flex items-center gap-2 sm:gap-3">
|
|
<svg class="w-5 h-5 sm:w-6 sm:h-6 text-cyan-500 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
</svg>
|
|
<span class="truncate">{{ __('Order Details') }}</span>
|
|
</h2>
|
|
<template x-if="activeOrder">
|
|
<div class="mt-2 flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest">
|
|
<span x-text="activeOrder.order_no" class="text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter text-lg"></span>
|
|
<span class="mx-1">•</span>
|
|
<span x-text="activeOrder.warehouse_name"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<button type="button" @click="showOrderDetails = false"
|
|
class="bg-white dark:bg-slate-800 rounded-full p-2 text-slate-400 hover:text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 focus:outline-none transition duration-300 shadow-sm border border-slate-200 dark:border-slate-700">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 overflow-y-auto p-6 sm:p-8">
|
|
<div class="relative min-h-[200px]">
|
|
<!-- Loading State -->
|
|
<div x-show="detailsLoading"
|
|
class="absolute inset-0 z-50 flex flex-col items-center justify-center bg-white/40 dark:bg-slate-900/40 backdrop-blur-[1px]">
|
|
<div class="w-10 h-10 border-2 border-cyan-500 border-t-transparent rounded-full animate-spin"></div>
|
|
</div>
|
|
|
|
<template x-if="activeOrder">
|
|
<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>
|
|
<template x-if="activeOrder.status === 'draft'">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 uppercase tracking-widest">{{ __('Draft') }}</span>
|
|
</template>
|
|
<template x-if="activeOrder.status === 'completed'">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-lg text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 uppercase tracking-widest">{{ __('Completed') }}</span>
|
|
</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="activeOrder.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="activeOrder.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">{{ __('Approved At') }}</p>
|
|
<p class="text-[11px] font-mono font-bold text-slate-600 dark:text-slate-400" x-text="activeOrder.completed_at || '-'"></p>
|
|
</div>
|
|
<template x-if="activeOrder.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="activeOrder.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">{{ __('Product Details') }}</h3>
|
|
<span class="text-[10px] font-black text-cyan-500 bg-cyan-500/10 px-2 py-0.5 rounded-md uppercase" x-text="activeItems.length + ' {{ __('Items') }}'"></span>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<template x-for="(item, idx) in activeItems" :key="idx">
|
|
<div class="luxury-card p-4 rounded-2xl flex items-center gap-4 group/item transition-all hover:border-cyan-500/30">
|
|
<div class="w-16 h-16 rounded-xl bg-slate-100 dark:bg-slate-800 flex-shrink-0 overflow-hidden border border-slate-200 dark:border-slate-700 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">
|
|
<div class="w-full h-full flex items-center justify-center text-slate-400">
|
|
<svg class="w-8 h-8" 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>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-base font-extrabold text-slate-800 dark:text-white truncate" x-text="item.product_name"></p>
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<span class="text-xs font-black text-slate-400 uppercase tracking-widest">{{ __('Product ID') }}:</span>
|
|
<span class="text-xs font-mono font-bold text-cyan-600 dark:text-cyan-400" x-text="item.product_id"></span>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-2xl font-black text-cyan-600 dark:text-cyan-400 font-mono tracking-tighter" x-text="'x' + item.quantity"></p>
|
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Quantity') }}</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection |