star-cloud/resources/views/admin/sales/index.blade.php

329 lines
14 KiB
PHP

@extends('layouts.admin')
@section('content')
<div class="space-y-2 pb-20" x-data="salesCenter()"
@ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)">
{{-- Page Header --}}
<x-page-header :title="$title" :subtitle="$description" />
{{-- Tab Navigation --}}
<x-tab-nav model="activeTab">
<x-tab-nav-item value="orders" :label="__('Transaction Orders')" model="activeTab" />
<x-tab-nav-item value="invoices" :label="__('Electronic Invoices')" model="activeTab" />
<x-tab-nav-item value="dispense" :label="__('Dispense Records')" model="activeTab" />
</x-tab-nav>
{{-- Tab Contents --}}
<div class="mt-6">
<div class="relative min-h-[400px]">
{{-- Orders Tab --}}
<div x-show="activeTab === 'orders'" class="relative"
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">
<div class="luxury-card rounded-3xl p-8 relative overflow-hidden">
<x-luxury-spinner show="tabLoading === 'orders'" />
<div id="tab-orders-container" :class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': tabLoading === 'orders' }">
@include('admin.sales.partials.tab-orders')
</div>
</div>
</div>
{{-- Invoices Tab --}}
<div x-show="activeTab === 'invoices'" class="relative"
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>
<div class="luxury-card rounded-3xl p-8 relative overflow-hidden">
<x-luxury-spinner show="tabLoading === 'invoices'" />
<div id="tab-invoices-container" :class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': tabLoading === 'invoices' }">
@include('admin.sales.partials.tab-invoices')
</div>
</div>
</div>
{{-- Dispense Records Tab --}}
<div x-show="activeTab === 'dispense'" class="relative"
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>
<div class="luxury-card rounded-3xl p-8 relative overflow-hidden">
<x-luxury-spinner show="tabLoading === 'dispense'" />
<div id="tab-dispense-container" :class="{ 'opacity-30 pointer-events-none transition-opacity duration-300': tabLoading === 'dispense' }">
@include('admin.sales.partials.tab-dispense')
</div>
</div>
</div>
</div>
</div>
{{-- Slide-over Panel (Order Details) --}}
<div
class="fixed inset-0 z-[100] overflow-hidden"
x-show="showPanel"
x-cloak
@keydown.window.escape="showPanel = false"
>
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
x-show="showPanel"
x-transition:enter="ease-out duration-500"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-500"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="showPanel = false"
></div>
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
<div
class="pointer-events-auto w-screen max-w-2xl transform transition ease-in-out duration-500 sm:duration-700"
x-show="showPanel"
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"
>
<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">
<div id="order-detail-content">
{{-- AJAX injected content --}}
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
function salesCenter() {
return {
showPanel: false,
activeTab: '{{ $tab }}',
tabLoading: null,
init() {
const params = new URLSearchParams(window.location.search);
const urlTab = params.get('tab');
if (urlTab && ['orders', 'invoices', 'dispense'].includes(urlTab)) {
this.activeTab = urlTab;
}
// 監聽頁籤切換以更新網址列 (不觸發重整)
this.$watch('activeTab', (newTab) => {
const url = new URL(window.location.origin + window.location.pathname);
url.searchParams.set('tab', newTab);
window.history.pushState({}, '', url);
});
},
switchTab(tab, url = null) {
if (this.activeTab === tab && !url) return;
this.activeTab = tab;
// 如果有提供 URL (例如點擊跳轉),則使用 AJAX 載入新內容
if (url) {
this.fetchTabData(tab, url);
return;
}
},
async fetchTabData(tab, url = null) {
if (this.tabLoading === tab) return;
this.tabLoading = tab;
const container = document.getElementById('tab-' + tab + '-container');
if (!url) {
// 從表單建構 URL
let params = new URLSearchParams();
params.set('tab', tab);
if (container) {
const form = container.querySelector('form');
if (form) {
const formData = new FormData(form);
formData.forEach((value, key) => {
if (value && value.trim() !== '') params.append(key, value);
});
}
}
url = `${window.location.pathname}?${params.toString()}`;
}
try {
const response = await fetch(url, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
const data = await response.json();
if (data.success && data.html) {
if (container) container.innerHTML = data.html;
// 更新網址列但不重整頁面
window.history.pushState({}, '', url);
// 重新初始化 Preline 組件
this.$nextTick(() => {
if (window.HSStaticMethods) window.HSStaticMethods.autoInit();
});
}
} catch (err) {
console.error('Failed to fetch tab data:', err);
} finally {
this.tabLoading = null;
}
},
openDetail(orderId) {
this.showPanel = true;
document.getElementById('order-detail-content').innerHTML = `<div class="flex items-center justify-center h-full py-20"><x-luxury-spinner show="true" /></div>`;
fetch(`/admin/sales/${orderId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
document.getElementById('order-detail-content').innerHTML = data.html;
}
})
.catch(err => {
console.error('Failed to load order detail:', err);
this.showPanel = false;
});
},
exportData(type) {
const container = document.getElementById('tab-' + this.activeTab + '-container');
let params = new URLSearchParams();
params.set('tab', this.activeTab);
params.set('export', type);
if (container) {
const form = container.querySelector('form');
if (form) {
const formData = new FormData(form);
formData.forEach((value, key) => {
if (value && value.trim() !== '') {
if (key !== 'tab' && key !== 'export') {
params.append(key, value);
}
}
});
}
}
// 使用隱藏的 iframe 進行下載,避免觸發主視窗的 beforeunload 事件,
// 從而防止全域的頂部進度條 (top-loading-bar) 被觸發並卡在 95% 的位置。
let iframe = document.getElementById('export-download-iframe');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'export-download-iframe';
iframe.style.display = 'none';
document.body.appendChild(iframe);
}
iframe.src = `${window.location.pathname}?${params.toString()}`;
// 顯示成功提示訊息,與商品報表保持一致
if (type === 'csv') {
window.showToast?.('CSV 報表導出成功', 'success');
} else if (type === 'excel') {
window.showToast?.('Excel 報表導出成功', 'success');
}
},
copyTableData() {
const container = document.getElementById('tab-' + this.activeTab + '-container');
if (!container) return;
const table = container.querySelector('table');
if (!table) return;
let headers = [];
table.querySelectorAll('thead th').forEach((th, idx, arr) => {
if (idx < arr.length - 1) {
headers.push(th.innerText.trim().replace(/\s+/g, ' '));
}
});
let rows = [headers.join('\t')];
table.querySelectorAll('tbody tr').forEach(tr => {
if (tr.querySelector('td.colspan') || tr.querySelector('td[colspan]')) return;
let cells = [];
tr.querySelectorAll('td').forEach((td, idx, arr) => {
if (idx < arr.length - 1) {
let text = td.innerText.trim().replace(/\r?\n/g, ' ').replace(/\s+/g, ' ');
cells.push(text);
}
});
if (cells.length > 0) {
rows.push(cells.join('\t'));
}
});
navigator.clipboard.writeText(rows.join('\n')).then(() => {
window.showToast?.('已成功將明細複製至剪貼簿', 'success');
}).catch(err => {
console.error('Clipboard copy failed:', err);
window.showToast?.('複製失敗,瀏覽器權限可能不足', 'error');
});
},
printTable() {
const container = document.getElementById('tab-' + this.activeTab + '-container');
if (!container) return;
const table = container.querySelector('table');
if (!table) return;
const clonedTable = table.cloneNode(true);
// 移除所有 SVG 圖標,防止列印或 PDF 輸出時因為沒有載入 Tailwind 樣式而顯示巨大黑色區塊
clonedTable.querySelectorAll('svg').forEach(svg => svg.remove());
clonedTable.querySelectorAll('tr').forEach(tr => {
const cells = tr.querySelectorAll('th, td');
if (cells.length > 0) {
cells[cells.length - 1].remove();
}
});
let tabTitle = '';
if (this.activeTab === 'orders') tabTitle = '交易紀錄';
else if (this.activeTab === 'invoices') tabTitle = '電子發票';
else if (this.activeTab === 'dispense') tabTitle = '遠端出貨紀錄';
const printWindow = window.open('', '_blank');
let html = `<html><head><title>${tabTitle}</title>`;
html += `<style>
body { font-family: system-ui, -apple-system, sans-serif; padding: 30px; color: #1e293b; }
h1 { font-size: 24px; font-weight: 800; margin-bottom: 5px; }
.subtitle { font-size: 13px; font-weight: 600; color: #64748b; margin-bottom: 25px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 12px; }
th, td { border: 1px solid #cbd5e1; padding: 10px 12px; text-align: left; }
th { background-color: #f1f5f9; font-weight: bold; color: #334155; }
@media print {
body { padding: 0; }
}
</style></head><body>`;
html += `<h1>${tabTitle}</h1>`;
html += `<div class="subtitle">列印時間:${new Date().toLocaleString()}</div>`;
html += clonedTable.outerHTML;
html += `</body></html>`;
printWindow.document.write(html);
printWindow.document.close();
printWindow.focus();
setTimeout(() => {
printWindow.print();
printWindow.close();
}, 250);
}
}
}
</script>
@endsection