feat: 實作調撥單與機台補貨單的 A4 極簡奢華風列印功能與多租戶雙重防護
This commit is contained in:
parent
1678fd1775
commit
dac00291a7
@ -1407,4 +1407,47 @@ class WarehouseController extends Controller
|
|||||||
'items' => $items
|
'items' => $items
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列印調撥單
|
||||||
|
*/
|
||||||
|
public function printTransfer($id)
|
||||||
|
{
|
||||||
|
$order = TransferOrder::with(['fromWarehouse', 'toWarehouse', 'fromMachine', 'creator'])
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
// 雙重防護:驗證租戶資料隔離安全
|
||||||
|
$user = auth()->user();
|
||||||
|
if ($user && $user->company_id && $order->company_id !== $user->company_id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = \App\Models\Warehouse\TransferOrderItem::with(['product' => fn($q) => $q->with('translations')])
|
||||||
|
->where('transfer_order_id', $id)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.warehouses.print.transfer', compact('order', 'items'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列印機台補貨單
|
||||||
|
*/
|
||||||
|
public function printReplenishment($id)
|
||||||
|
{
|
||||||
|
$order = ReplenishmentOrder::with(['machine', 'warehouse', 'creator', 'assignee'])
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
// 雙重防護:驗證租戶資料隔離安全
|
||||||
|
$user = auth()->user();
|
||||||
|
if ($user && $user->company_id && $order->company_id !== $user->company_id) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = ReplenishmentOrderItem::with(['product' => fn($q) => $q->with('translations')])
|
||||||
|
->where('replenishment_order_id', $id)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.warehouses.print.replenishment', compact('order', 'items'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,9 +15,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" @click="showOrderDetails = false" class="p-2.5 rounded-full hover:bg-slate-50 dark:hover:bg-slate-800 text-slate-400 transition-colors border border-slate-100 dark:border-slate-800 shadow-sm">
|
<div class="flex items-center gap-3">
|
||||||
<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 type="button" @click="window.open('/admin/warehouses/replenishments/' + activeOrder.id + '/print', '_blank')" class="p-2.5 rounded-full hover:bg-slate-50 dark:hover:bg-slate-800 text-cyan-500 hover:text-cyan-600 transition-colors border border-slate-100 dark:border-slate-800 shadow-sm flex items-center justify-center" title="{{ __('Print Order') }}">
|
||||||
</button>
|
<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.72 13.82l-.024-.03c-1.285-1.56-1.93-3.19-1.93-5.166C4.766 4.902 6.713 3 9.12 3c2.407 0 4.354 1.902 4.354 4.624 0 1.977-.645 3.607-1.93 5.167-.004.005-.008.01-.012.015L9.12 15.652l-2.4-1.832z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 12h2m-2 4h2m-2-8h2M3 12h2m-2 4h2m-2-8h2M12 18H8.5c-.83 0-1.5-.67-1.5-1.5V14m8.5 4H16m0 0v-4.5c0-.83-.67-1.5-1.5-1.5H11" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="showOrderDetails = false" class="p-2.5 rounded-full hover:bg-slate-50 dark:hover:bg-slate-800 text-slate-400 transition-colors border border-slate-100 dark:border-slate-800 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>
|
</div>
|
||||||
|
|
||||||
{{-- Content --}}
|
{{-- Content --}}
|
||||||
@ -68,23 +76,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Action Buttons --}}
|
{{-- Action Buttons --}}
|
||||||
<template x-if="activeOrder.status !== 'completed' && activeOrder.status !== 'cancelled'">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="flex flex-wrap gap-2">
|
{{-- 列印按鈕永遠顯示 --}}
|
||||||
<button type="button" @click="openAssignModal(activeOrder.id)" class="px-4 py-2 rounded-xl text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all border border-slate-200 dark:border-slate-700">
|
<button type="button" @click="window.open('/admin/warehouses/replenishments/' + activeOrder.id + '/print', '_blank')" class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500 text-white hover:bg-cyan-600 transition-all shadow-lg shadow-cyan-500/10 flex items-center gap-1.5">
|
||||||
{{ __('Assign Personnel') }}
|
<svg class="w-3.5 h-3.5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</button>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.82l-.024-.03c-1.285-1.56-1.93-3.19-1.93-5.166C4.766 4.902 6.713 3 9.12 3c2.407 0 4.354 1.902 4.354 4.624 0 1.977-.645 3.607-1.93 5.167-.004.005-.008.01-.012.015L9.12 15.652l-2.4-1.832z" />
|
||||||
<template x-if="activeOrder.status === 'pending'">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 12h2m-2 4h2m-2-8h2M3 12h2m-2 4h2m-2-8h2M12 18H8.5c-.83 0-1.5-.67-1.5-1.5V14m8.5 4H16m0 0v-4.5c0-.83-.67-1.5-1.5-1.5H11" />
|
||||||
<button type="button" @click="advanceStatus(activeOrder.id, 'prepared')" class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all">{{ __('Confirm Prepare') }}</button>
|
</svg>
|
||||||
</template>
|
{{ __('Print Order') }}
|
||||||
<template x-if="activeOrder.status === 'prepared'">
|
</button>
|
||||||
<button type="button" @click="advanceStatus(activeOrder.id, 'delivering')" class="px-4 py-2 rounded-xl text-xs font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 hover:bg-indigo-500 hover:text-white transition-all">{{ __('Start Delivery') }}</button>
|
|
||||||
</template>
|
{{-- 其他需要依狀態顯示的動作 --}}
|
||||||
<template x-if="activeOrder.status === 'delivering'">
|
<template x-if="activeOrder.status !== 'completed' && activeOrder.status !== 'cancelled'">
|
||||||
<button type="button" @click="advanceStatus(activeOrder.id, 'completed')" 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">{{ __('Confirm Complete') }}</button>
|
<div class="flex flex-wrap gap-2">
|
||||||
</template>
|
<button type="button" @click="openAssignModal(activeOrder.id)" class="px-4 py-2 rounded-xl text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all border border-slate-200 dark:border-slate-700">
|
||||||
<button type="button" @click="confirmCancel(activeOrder.id)" class="px-4 py-2 rounded-xl text-xs font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 hover:bg-rose-500 hover:text-white transition-all">{{ __('Cancel Order') }}</button>
|
{{ __('Assign Personnel') }}
|
||||||
</div>
|
</button>
|
||||||
</template>
|
<template x-if="activeOrder.status === 'pending'">
|
||||||
|
<button type="button" @click="advanceStatus(activeOrder.id, 'prepared')" class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500/10 text-cyan-500 border border-cyan-500/20 hover:bg-cyan-500 hover:text-white transition-all">{{ __('Confirm Prepare') }}</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeOrder.status === 'prepared'">
|
||||||
|
<button type="button" @click="advanceStatus(activeOrder.id, 'delivering')" class="px-4 py-2 rounded-xl text-xs font-black bg-indigo-500/10 text-indigo-500 border border-indigo-500/20 hover:bg-indigo-500 hover:text-white transition-all">{{ __('Start Delivery') }}</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="activeOrder.status === 'delivering'">
|
||||||
|
<button type="button" @click="advanceStatus(activeOrder.id, 'completed')" 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">{{ __('Confirm Complete') }}</button>
|
||||||
|
</template>
|
||||||
|
<button type="button" @click="confirmCancel(activeOrder.id)" class="px-4 py-2 rounded-xl text-xs font-black bg-rose-500/10 text-rose-500 border border-rose-500/20 hover:bg-rose-500 hover:text-white transition-all">{{ __('Cancel Order') }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{-- Items List --}}
|
{{-- Items List --}}
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
|
|||||||
264
resources/views/admin/warehouses/print/replenishment.blade.php
Normal file
264
resources/views/admin/warehouses/print/replenishment.blade.php
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full bg-white">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ __('Replenishment Order') }} - {{ $order->order_no }}</title>
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800;900&family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Tailwind CSS (Vite setup) -->
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #0f172a;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
.font-display {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.print-border {
|
||||||
|
border-color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
.print-bg-gray {
|
||||||
|
background-color: #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
@page {
|
||||||
|
size: A4 portrait;
|
||||||
|
margin: 15mm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="p-6 sm:p-12 min-h-screen flex flex-col justify-between">
|
||||||
|
|
||||||
|
<!-- Container -->
|
||||||
|
<div class="max-w-4xl mx-auto w-full flex-1">
|
||||||
|
|
||||||
|
<!-- Top Operations (No Print) -->
|
||||||
|
<div class="no-print flex items-center justify-between gap-4 mb-8 p-4 bg-slate-50 dark:bg-slate-900 rounded-2xl border border-slate-200/50 dark:border-slate-800/50">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-cyan-500 animate-pulse"></span>
|
||||||
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Ready to print') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick="window.print()" class="px-5 py-2.5 bg-cyan-500 text-white rounded-xl text-xs font-black uppercase tracking-widest hover:bg-cyan-600 transition-all active:scale-95 flex items-center gap-2 shadow-lg shadow-cyan-500/20">
|
||||||
|
<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="M6.72 13.82l-.024-.03c-1.285-1.56-1.93-3.19-1.93-5.166C4.766 4.902 6.713 3 9.12 3c2.407 0 4.354 1.902 4.354 4.624 0 1.977-.645 3.607-1.93 5.167-.004.005-.008.01-.012.015L9.12 15.652l-2.4-1.832z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 12h2m-2 4h2m-2-8h2M3 12h2m-2 4h2m-2-8h2M12 18H8.5c-.83 0-1.5-.67-1.5-1.5V14m8.5 4H16m0 0v-4.5c0-.83-.67-1.5-1.5-1.5H11" />
|
||||||
|
</svg>
|
||||||
|
{{ __('Print') }}
|
||||||
|
</button>
|
||||||
|
<button onclick="window.close()" class="px-5 py-2.5 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
|
||||||
|
{{ __('Close') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-6 pb-6 border-b-2 border-slate-900 print-border">
|
||||||
|
<div>
|
||||||
|
<!-- Brand -->
|
||||||
|
<p class="text-xs font-black text-cyan-600 uppercase tracking-[0.3em] font-display mb-1">Star Cloud System</p>
|
||||||
|
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-4">Tenant: {{ Auth::user()->company?->name ?? __('Platform Operator') }}</p>
|
||||||
|
|
||||||
|
<!-- Document Title -->
|
||||||
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight font-display uppercase">{{ __('Replenishment Order') }}</h1>
|
||||||
|
<p class="text-xs font-bold text-slate-500 mt-1 uppercase tracking-widest">{{ __('Vending Machine Product Replenishment') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-left sm:text-right flex flex-col sm:items-end">
|
||||||
|
<!-- Status Badge -->
|
||||||
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-xs font-black uppercase tracking-widest border border-slate-900 print-border text-slate-900 mb-3 w-fit">
|
||||||
|
{{ __($order->status) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Mono Order No -->
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Order Number') }}</p>
|
||||||
|
<p class="text-2xl font-black text-slate-900 font-mono tracking-tighter">{{ $order->order_no }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-8 py-8 border-b border-slate-200">
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Source Warehouse') }}</p>
|
||||||
|
<p class="text-sm font-extrabold text-slate-800">{{ $order->warehouse?->name ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Target Machine') }}</p>
|
||||||
|
<p class="text-sm font-extrabold text-slate-800">{{ $order->machine?->name ?? '-' }} <span class="text-xs font-mono font-bold text-slate-400 tracking-widest uppercase">({{ $order->machine?->serial_no }})</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created By') }}</p>
|
||||||
|
<p class="text-sm font-bold text-slate-700">{{ $order->creator?->name ?? __('System') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Assigned Personnel') }}</p>
|
||||||
|
<p class="text-sm font-bold text-slate-700">{{ $order->assignee?->name ?? __('Unassigned') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created At') }}</p>
|
||||||
|
<p class="text-sm font-mono font-bold text-slate-600">{{ $order->created_at->timezone('Asia/Taipei')->format('Y-m-d H:i:s') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Print Time') }}</p>
|
||||||
|
<p class="text-sm font-mono font-bold text-slate-600">{{ now()->timezone('Asia/Taipei')->format('Y-m-d H:i:s') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($order->note)
|
||||||
|
<div class="col-span-1 md:col-span-2 space-y-1 pt-2 border-t border-dashed border-slate-200">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Note') }}</p>
|
||||||
|
<p class="text-xs font-bold text-slate-600 italic">{{ $order->note }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items Table -->
|
||||||
|
<div class="py-8">
|
||||||
|
<h3 class="text-xs font-black text-slate-900 uppercase tracking-[0.2em] mb-4 font-display">{{ __('Replenishment Details') }} ({{ $items->count() }} {{ __('Items') }})</h3>
|
||||||
|
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b-2 border-slate-900 print-border bg-slate-50 print-bg-gray">
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-16 text-center">
|
||||||
|
{{ __('Slot') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-12">
|
||||||
|
{{ __('Image') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest">
|
||||||
|
{{ __('Product Name') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-36">
|
||||||
|
{{ __('Barcode') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-24 text-right">
|
||||||
|
{{ __('Stock Snap') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-24 text-right">
|
||||||
|
{{ __('Replenish Qty') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-28 text-center no-print">
|
||||||
|
{{ __('Check') }}
|
||||||
|
</th>
|
||||||
|
<!-- 列印時顯示的手寫格 -->
|
||||||
|
<th class="hidden print:table-cell px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-24 text-center">
|
||||||
|
{{ __('Actual Qty') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200">
|
||||||
|
@foreach($items as $index => $item)
|
||||||
|
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||||
|
<td class="px-4 py-3 font-mono font-black text-slate-900 text-center text-sm">
|
||||||
|
<span class="inline-block px-2 py-1 rounded bg-slate-100 print-bg-gray border border-slate-200 print-border">
|
||||||
|
{{ $item->slot_no }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="w-10 h-10 rounded-lg border border-slate-200 print-border overflow-hidden bg-slate-50 flex items-center justify-center shrink-0">
|
||||||
|
@if($item->product?->image_url)
|
||||||
|
<img src="{{ $item->product->image_url }}" class="w-full h-full object-cover">
|
||||||
|
@else
|
||||||
|
<svg class="w-5 h-5 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>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<p class="text-sm font-extrabold text-slate-800 tracking-tight">{{ $item->product?->localized_name ?? __('Unknown') }}</p>
|
||||||
|
<p class="text-[9px] font-mono font-bold text-slate-400 uppercase tracking-widest mt-0.5">ID: {{ $item->product_id }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 font-mono text-xs font-bold text-slate-500">
|
||||||
|
{{ $item->product?->barcode ?? '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right text-xs font-bold text-slate-500 font-mono">
|
||||||
|
{{ $item->current_stock }} / {{ $item->max_stock }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<p class="text-lg font-black text-cyan-600 font-display tracking-tight">x{{ $item->quantity }}</p>
|
||||||
|
</td>
|
||||||
|
<!-- UI 頁面上的 check 欄位 -->
|
||||||
|
<td class="px-4 py-3 text-center no-print">
|
||||||
|
<div class="inline-flex items-center justify-center w-5 h-5 border-2 border-slate-300 rounded-md"></div>
|
||||||
|
</td>
|
||||||
|
<!-- 列印時的手寫框 -->
|
||||||
|
<td class="hidden print:table-cell px-4 py-3 text-center vertical-middle">
|
||||||
|
<div class="mx-auto w-12 h-6 border border-slate-400 print-border rounded"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Footer Warning (No Print) -->
|
||||||
|
<div class="no-print mt-4 p-4 bg-amber-500/10 border border-amber-500/20 text-amber-600 rounded-2xl flex items-start gap-4 font-bold text-xs">
|
||||||
|
<svg class="w-4 h-4 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-extrabold uppercase tracking-widest mb-1">{{ __('Print Notice') }}</p>
|
||||||
|
<p>{{ __('Please make sure to enable "Background graphics" in your browser print settings to print product images and styled components correctly.') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sign-off Section -->
|
||||||
|
<div class="max-w-4xl mx-auto w-full mt-12 pt-8 border-t border-slate-200">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Prepared By') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Warehouse Stock Out') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Courier/Replenisher') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Machine Inbound Confirmation') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 text-center text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||||
|
Star Cloud © 2026. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto Print Script -->
|
||||||
|
<script>
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 自動觸發列印
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
268
resources/views/admin/warehouses/print/transfer.blade.php
Normal file
268
resources/views/admin/warehouses/print/transfer.blade.php
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full bg-white">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ __('Transfer Order') }} - {{ $order->order_no }}</title>
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800;900&family=Plus+Jakarta+Sans:wght@400;500;700;800&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Tailwind CSS (Vite setup) -->
|
||||||
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #0f172a;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
.font-display {
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.print-border {
|
||||||
|
border-color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
.print-bg-gray {
|
||||||
|
background-color: #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
@page {
|
||||||
|
size: A4 portrait;
|
||||||
|
margin: 15mm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="p-6 sm:p-12 min-h-screen flex flex-col justify-between">
|
||||||
|
|
||||||
|
<!-- Container -->
|
||||||
|
<div class="max-w-4xl mx-auto w-full flex-1">
|
||||||
|
|
||||||
|
<!-- Top Operations (No Print) -->
|
||||||
|
<div class="no-print flex items-center justify-between gap-4 mb-8 p-4 bg-slate-50 dark:bg-slate-900 rounded-2xl border border-slate-200/50 dark:border-slate-800/50">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2.5 h-2.5 rounded-full bg-cyan-500 animate-pulse"></span>
|
||||||
|
<span class="text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Ready to print') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick="window.print()" class="px-5 py-2.5 bg-cyan-500 text-white rounded-xl text-xs font-black uppercase tracking-widest hover:bg-cyan-600 transition-all active:scale-95 flex items-center gap-2 shadow-lg shadow-cyan-500/20">
|
||||||
|
<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="M6.72 13.82l-.024-.03c-1.285-1.56-1.93-3.19-1.93-5.166C4.766 4.902 6.713 3 9.12 3c2.407 0 4.354 1.902 4.354 4.624 0 1.977-.645 3.607-1.93 5.167-.004.005-.008.01-.012.015L9.12 15.652l-2.4-1.832z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 12h2m-2 4h2m-2-8h2M3 12h2m-2 4h2m-2-8h2M12 18H8.5c-.83 0-1.5-.67-1.5-1.5V14m8.5 4H16m0 0v-4.5c0-.83-.67-1.5-1.5-1.5H11" />
|
||||||
|
</svg>
|
||||||
|
{{ __('Print') }}
|
||||||
|
</button>
|
||||||
|
<button onclick="window.close()" class="px-5 py-2.5 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-slate-200 dark:hover:bg-slate-700 transition-all active:scale-95">
|
||||||
|
{{ __('Close') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-6 pb-6 border-b-2 border-slate-900 print-border">
|
||||||
|
<div>
|
||||||
|
<!-- Brand -->
|
||||||
|
<p class="text-xs font-black text-cyan-600 uppercase tracking-[0.3em] font-display mb-1">Star Cloud System</p>
|
||||||
|
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-4">Tenant: {{ Auth::user()->company?->name ?? __('Platform Operator') }}</p>
|
||||||
|
|
||||||
|
<!-- Document Title -->
|
||||||
|
<h1 class="text-3xl font-black text-slate-900 tracking-tight font-display uppercase">{{ __('Transfer Order') }}</h1>
|
||||||
|
<p class="text-xs font-bold text-slate-500 mt-1 uppercase tracking-widest">
|
||||||
|
{{ $order->type === \App\Models\Warehouse\TransferOrder::TYPE_W2W ? __('Warehouse to Warehouse Transfer') : __('Machine to Warehouse Return') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-left sm:text-right flex flex-col sm:items-end">
|
||||||
|
<!-- Status Badge -->
|
||||||
|
<span class="inline-flex items-center px-3 py-1.5 rounded-xl text-xs font-black uppercase tracking-widest border border-slate-900 print-border text-slate-900 mb-3 w-fit">
|
||||||
|
{{ __($order->status) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Mono Order No -->
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Order Number') }}</p>
|
||||||
|
<p class="text-2xl font-black text-slate-900 font-mono tracking-tighter">{{ $order->order_no }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Info Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-8 py-8 border-b border-slate-200">
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
@if($order->type === \App\Models\Warehouse\TransferOrder::TYPE_W2W)
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Source Warehouse') }}</p>
|
||||||
|
<p class="text-sm font-extrabold text-slate-800">{{ $order->fromWarehouse?->name ?? '-' }}</p>
|
||||||
|
@else
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Source Machine') }}</p>
|
||||||
|
<p class="text-sm font-extrabold text-slate-800">
|
||||||
|
{{ $order->fromMachine?->name ?? '-' }}
|
||||||
|
<span class="text-xs font-mono font-bold text-slate-400 tracking-widest uppercase">({{ $order->fromMachine?->serial_no }})</span>
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Target Warehouse') }}</p>
|
||||||
|
<p class="text-sm font-extrabold text-slate-800">{{ $order->toWarehouse?->name ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created By') }}</p>
|
||||||
|
<p class="text-sm font-bold text-slate-700">{{ $order->creator?->name ?? __('System') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Transfer Type') }}</p>
|
||||||
|
<p class="text-sm font-bold text-slate-700">
|
||||||
|
{{ $order->type === \App\Models\Warehouse\TransferOrder::TYPE_W2W ? __('Warehouse to Warehouse') : __('Machine to Warehouse') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Created At') }}</p>
|
||||||
|
<p class="text-sm font-mono font-bold text-slate-600">{{ $order->created_at->timezone('Asia/Taipei')->format('Y-m-d H:i:s') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Print Time') }}</p>
|
||||||
|
<p class="text-sm font-mono font-bold text-slate-600">{{ now()->timezone('Asia/Taipei')->format('Y-m-d H:i:s') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($order->note)
|
||||||
|
<div class="col-span-1 md:col-span-2 space-y-1 pt-2 border-t border-dashed border-slate-200">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Note') }}</p>
|
||||||
|
<p class="text-xs font-bold text-slate-600 italic">{{ $order->note }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items Table -->
|
||||||
|
<div class="py-8">
|
||||||
|
<h3 class="text-xs font-black text-slate-900 uppercase tracking-[0.2em] mb-4 font-display">{{ __('Transfer Details') }} ({{ $items->count() }} {{ __('Items') }})</h3>
|
||||||
|
|
||||||
|
<table class="w-full text-left border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b-2 border-slate-900 print-border bg-slate-50 print-bg-gray">
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-16 text-center">
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-12">
|
||||||
|
{{ __('Image') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest">
|
||||||
|
{{ __('Product Name') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-40">
|
||||||
|
{{ __('Barcode') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-28 text-right">
|
||||||
|
{{ __('Transfer Qty') }}
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-28 text-center no-print">
|
||||||
|
{{ __('Check') }}
|
||||||
|
</th>
|
||||||
|
<!-- 列印時顯示的手寫格 -->
|
||||||
|
<th class="hidden print:table-cell px-4 py-3 text-[10px] font-black text-slate-500 uppercase tracking-widest w-24 text-center">
|
||||||
|
{{ __('Actual Qty') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200">
|
||||||
|
@foreach($items as $index => $item)
|
||||||
|
<tr class="hover:bg-slate-50/50 transition-colors">
|
||||||
|
<td class="px-4 py-3 font-mono font-black text-slate-900 text-center text-sm">
|
||||||
|
{{ $index + 1 }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="w-10 h-10 rounded-lg border border-slate-200 print-border overflow-hidden bg-slate-50 flex items-center justify-center shrink-0">
|
||||||
|
@if($item->product?->image_url)
|
||||||
|
<img src="{{ $item->product->image_url }}" class="w-full h-full object-cover">
|
||||||
|
@else
|
||||||
|
<svg class="w-5 h-5 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>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<p class="text-sm font-extrabold text-slate-800 tracking-tight">{{ $item->product?->localized_name ?? __('Unknown') }}</p>
|
||||||
|
<p class="text-[9px] font-mono font-bold text-slate-400 uppercase tracking-widest mt-0.5">ID: {{ $item->product_id }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 font-mono text-xs font-bold text-slate-500">
|
||||||
|
{{ $item->product?->barcode ?? '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<p class="text-lg font-black text-cyan-600 font-display tracking-tight">x{{ $item->quantity }}</p>
|
||||||
|
</td>
|
||||||
|
<!-- UI 頁面上的 check 欄位 -->
|
||||||
|
<td class="px-4 py-3 text-center no-print">
|
||||||
|
<div class="inline-flex items-center justify-center w-5 h-5 border-2 border-slate-300 rounded-md"></div>
|
||||||
|
</td>
|
||||||
|
<!-- 列印時的手寫框 -->
|
||||||
|
<td class="hidden print:table-cell px-4 py-3 text-center vertical-middle">
|
||||||
|
<div class="mx-auto w-12 h-6 border border-slate-400 print-border rounded"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Footer Warning (No Print) -->
|
||||||
|
<div class="no-print mt-4 p-4 bg-amber-500/10 border border-amber-500/20 text-amber-600 rounded-2xl flex items-start gap-4 font-bold text-xs">
|
||||||
|
<svg class="w-4 h-4 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-extrabold uppercase tracking-widest mb-1">{{ __('Print Notice') }}</p>
|
||||||
|
<p>{{ __('Please make sure to enable "Background graphics" in your browser print settings to print product images and styled components correctly.') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sign-off Section -->
|
||||||
|
<div class="max-w-4xl mx-auto w-full mt-12 pt-8 border-t border-slate-200">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Prepared By') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Transfer Out Confirmation') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Courier/Dispatcher') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">{{ __('Transfer In Confirmation') }}</p>
|
||||||
|
<div class="mx-auto w-32 border-b border-slate-400 print-border h-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 text-center text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
||||||
|
Star Cloud © 2026. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auto Print Script -->
|
||||||
|
<script>
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 自動觸發列印
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -707,12 +707,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" @click="showOrderDetails = false"
|
<div class="flex items-center gap-3">
|
||||||
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">
|
<button type="button" @click="window.open('/admin/warehouses/transfers/' + activeOrder.id + '/print', '_blank')" class="p-2 text-cyan-500 hover:text-cyan-600 bg-white dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-full focus:outline-none transition duration-300 shadow-sm border border-slate-200 dark:border-slate-700 flex items-center justify-center" title="{{ __('Print Order') }}">
|
||||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.82l-.024-.03c-1.285-1.56-1.93-3.19-1.93-5.166C4.766 4.902 6.713 3 9.12 3c2.407 0 4.354 1.902 4.354 4.624 0 1.977-.645 3.607-1.93 5.167-.004.005-.008.01-.012.015L9.12 15.652l-2.4-1.832z" />
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 12h2m-2 4h2m-2-8h2M3 12h2m-2 4h2m-2-8h2M12 18H8.5c-.83 0-1.5-.67-1.5-1.5V14m8.5 4H16m0 0v-4.5c0-.83-.67-1.5-1.5-1.5H11" />
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -792,6 +800,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" @click="window.open('/admin/warehouses/transfers/' + activeOrder.id + '/print', '_blank')" class="px-4 py-2 rounded-xl text-xs font-black bg-cyan-500 text-white hover:bg-cyan-600 transition-all shadow-lg shadow-cyan-500/10 flex items-center gap-1.5">
|
||||||
|
<svg class="w-3.5 h-3.5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.82l-.024-.03c-1.285-1.56-1.93-3.19-1.93-5.166C4.766 4.902 6.713 3 9.12 3c2.407 0 4.354 1.902 4.354 4.624 0 1.977-.645 3.607-1.93 5.167-.004.005-.008.01-.012.015L9.12 15.652l-2.4-1.832z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 12h2m-2 4h2m-2-8h2M3 12h2m-2 4h2m-2-8h2M12 18H8.5c-.83 0-1.5-.67-1.5-1.5V14m8.5 4H16m0 0v-4.5c0-.83-.67-1.5-1.5-1.5H11" />
|
||||||
|
</svg>
|
||||||
|
{{ __('Print Order') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Items List -->
|
<!-- Items List -->
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div class="flex items-center justify-between px-1">
|
<div class="flex items-center justify-between px-1">
|
||||||
|
|||||||
@ -99,6 +99,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
|
|||||||
Route::patch('/transfers/{transferOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmTransfer'])->name('transfers.confirm');
|
Route::patch('/transfers/{transferOrder}/confirm', [App\Http\Controllers\Admin\WarehouseController::class, 'confirmTransfer'])->name('transfers.confirm');
|
||||||
Route::delete('/transfers/{transferOrder}', [App\Http\Controllers\Admin\WarehouseController::class, 'destroyTransfer'])->name('transfers.destroy');
|
Route::delete('/transfers/{transferOrder}', [App\Http\Controllers\Admin\WarehouseController::class, 'destroyTransfer'])->name('transfers.destroy');
|
||||||
Route::get('/transfers/{id}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'transferOrderDetails'])->name('transfers.details');
|
Route::get('/transfers/{id}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'transferOrderDetails'])->name('transfers.details');
|
||||||
|
Route::get('/transfers/{id}/print', [App\Http\Controllers\Admin\WarehouseController::class, 'printTransfer'])->name('transfers.print');
|
||||||
|
|
||||||
// 模組 4:機台庫存總覽 (Force Route Refresh)
|
// 模組 4:機台庫存總覽 (Force Route Refresh)
|
||||||
Route::get('/machine-inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventory'])->name('machine-inventory');
|
Route::get('/machine-inventory', [App\Http\Controllers\Admin\WarehouseController::class, 'machineInventory'])->name('machine-inventory');
|
||||||
@ -114,6 +115,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix
|
|||||||
Route::patch('/replenishments/{replenishmentOrder}/assign', [App\Http\Controllers\Admin\WarehouseController::class, 'assignReplenishment'])->name('replenishments.assign');
|
Route::patch('/replenishments/{replenishmentOrder}/assign', [App\Http\Controllers\Admin\WarehouseController::class, 'assignReplenishment'])->name('replenishments.assign');
|
||||||
Route::get('/replenishments/machine-slots/{machine}', [App\Http\Controllers\Admin\WarehouseController::class, 'getMachineSlotsForReplenishment'])->name('replenishments.machine-slots');
|
Route::get('/replenishments/machine-slots/{machine}', [App\Http\Controllers\Admin\WarehouseController::class, 'getMachineSlotsForReplenishment'])->name('replenishments.machine-slots');
|
||||||
Route::get('/replenishments/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentOrderDetails'])->name('replenishments.details');
|
Route::get('/replenishments/{order}/details', [App\Http\Controllers\Admin\WarehouseController::class, 'replenishmentOrderDetails'])->name('replenishments.details');
|
||||||
|
Route::get('/replenishments/{id}/print', [App\Http\Controllers\Admin\WarehouseController::class, 'printReplenishment'])->name('replenishments.print');
|
||||||
|
|
||||||
// AJAX 庫存查詢
|
// AJAX 庫存查詢
|
||||||
Route::get('/ajax/stock', [App\Http\Controllers\Admin\WarehouseController::class, 'getStockAjax'])->name('ajax.stock');
|
Route::get('/ajax/stock', [App\Http\Controllers\Admin\WarehouseController::class, 'getStockAjax'])->name('ajax.stock');
|
||||||
|
|||||||
174
tests/Feature/Admin/PrintStockOrdersTest.php
Normal file
174
tests/Feature/Admin/PrintStockOrdersTest.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Admin;
|
||||||
|
|
||||||
|
use App\Models\Machine\Machine;
|
||||||
|
use App\Models\System\Company;
|
||||||
|
use App\Models\System\User;
|
||||||
|
use App\Models\Warehouse\Warehouse;
|
||||||
|
use App\Models\Warehouse\TransferOrder;
|
||||||
|
use App\Models\Warehouse\ReplenishmentOrder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PrintStockOrdersTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private $companyA;
|
||||||
|
private $companyB;
|
||||||
|
private $userA;
|
||||||
|
private $userB;
|
||||||
|
private $warehouseA;
|
||||||
|
private $warehouseB;
|
||||||
|
private $machineA;
|
||||||
|
private $machineB;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// 1. 建立租戶公司
|
||||||
|
$this->companyA = Company::create(['name' => 'Company A', 'code' => 'COA']);
|
||||||
|
$this->companyB = Company::create(['name' => 'Company B', 'code' => 'COB']);
|
||||||
|
|
||||||
|
// 2. 建立使用者
|
||||||
|
$this->userA = User::factory()->create([
|
||||||
|
'company_id' => $this->companyA->id,
|
||||||
|
'name' => 'User A',
|
||||||
|
'email' => 'userA@star-cloud.com'
|
||||||
|
]);
|
||||||
|
$this->userB = User::factory()->create([
|
||||||
|
'company_id' => $this->companyB->id,
|
||||||
|
'name' => 'User B',
|
||||||
|
'email' => 'userB@star-cloud.com'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 3. 建立倉庫與機台(包含 serial_no 避免 Integrity constraint violation 錯誤)
|
||||||
|
$this->warehouseA = Warehouse::create([
|
||||||
|
'company_id' => $this->companyA->id,
|
||||||
|
'name' => 'Warehouse A',
|
||||||
|
'type' => 'main',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
$this->warehouseB = Warehouse::create([
|
||||||
|
'company_id' => $this->companyB->id,
|
||||||
|
'name' => 'Warehouse B',
|
||||||
|
'type' => 'main',
|
||||||
|
'is_active' => true
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->machineA = Machine::create([
|
||||||
|
'company_id' => $this->companyA->id,
|
||||||
|
'name' => 'Machine A',
|
||||||
|
'serial_no' => 'SN-MAA-01'
|
||||||
|
]);
|
||||||
|
$this->machineB = Machine::create([
|
||||||
|
'company_id' => $this->companyB->id,
|
||||||
|
'name' => 'Machine B',
|
||||||
|
'serial_no' => 'SN-MBB-02'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 測試租戶可以成功列印自己公司的庫存調撥單
|
||||||
|
*/
|
||||||
|
public function test_tenant_can_print_own_transfer_order()
|
||||||
|
{
|
||||||
|
$this->actingAs($this->userA);
|
||||||
|
|
||||||
|
// 建立調撥單
|
||||||
|
$transfer = TransferOrder::create([
|
||||||
|
'company_id' => $this->companyA->id,
|
||||||
|
'order_no' => 'TF-202605210001',
|
||||||
|
'type' => 'warehouse_to_warehouse',
|
||||||
|
'from_warehouse_id' => $this->warehouseA->id,
|
||||||
|
'to_warehouse_id' => $this->warehouseA->id, // 本地測試可以使用同一個以簡化
|
||||||
|
'status' => 'draft',
|
||||||
|
'created_by' => $this->userA->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get("/admin/warehouses/transfers/{$transfer->id}/print");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('TF-202605210001');
|
||||||
|
$response->assertSee('Star Cloud System');
|
||||||
|
$response->assertSee('Prepared By');
|
||||||
|
$response->assertSee('Transfer Out Confirmation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 測試租戶可以成功列印自己公司的機台補貨單
|
||||||
|
*/
|
||||||
|
public function test_tenant_can_print_own_replenishment_order()
|
||||||
|
{
|
||||||
|
$this->actingAs($this->userA);
|
||||||
|
|
||||||
|
// 建立補貨單
|
||||||
|
$replenishment = ReplenishmentOrder::create([
|
||||||
|
'company_id' => $this->companyA->id,
|
||||||
|
'order_no' => 'RP-202605210002',
|
||||||
|
'warehouse_id' => $this->warehouseA->id,
|
||||||
|
'machine_id' => $this->machineA->id,
|
||||||
|
'status' => 'pending',
|
||||||
|
'created_by' => $this->userA->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get("/admin/warehouses/replenishments/{$replenishment->id}/print");
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertSee('RP-202605210002');
|
||||||
|
$response->assertSee('Star Cloud System');
|
||||||
|
$response->assertSee('Prepared By');
|
||||||
|
$response->assertSee('Warehouse Stock Out');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 測試租戶嘗試越權存取(列印)其他租戶的調撥單會被阻斷(返回 404)
|
||||||
|
*/
|
||||||
|
public function test_tenant_cannot_print_other_tenant_transfer_order()
|
||||||
|
{
|
||||||
|
// 先以 User B 建立調撥單
|
||||||
|
$this->actingAs($this->userB);
|
||||||
|
$transferB = TransferOrder::create([
|
||||||
|
'company_id' => $this->companyB->id,
|
||||||
|
'order_no' => 'TF-B-9999',
|
||||||
|
'type' => 'warehouse_to_warehouse',
|
||||||
|
'from_warehouse_id' => $this->warehouseB->id,
|
||||||
|
'to_warehouse_id' => $this->warehouseB->id,
|
||||||
|
'status' => 'draft',
|
||||||
|
'created_by' => $this->userB->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 切換為 User A 請求列印 User B 的調撥單
|
||||||
|
$this->actingAs($this->userA);
|
||||||
|
$response = $this->get("/admin/warehouses/transfers/{$transferB->id}/print");
|
||||||
|
|
||||||
|
// 由於 TenantScoped Trait 自動隔離,會直接拋出 ModelNotFoundException 進而由 Laravel 轉換為 404
|
||||||
|
$response->assertStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 測試租戶嘗試越權存取(列印)其他租戶的補貨單會被阻斷(返回 404)
|
||||||
|
*/
|
||||||
|
public function test_tenant_cannot_print_other_tenant_replenishment_order()
|
||||||
|
{
|
||||||
|
// 先以 User B 建立補貨單
|
||||||
|
$this->actingAs($this->userB);
|
||||||
|
$replenishmentB = ReplenishmentOrder::create([
|
||||||
|
'company_id' => $this->companyB->id,
|
||||||
|
'order_no' => 'RP-B-9999',
|
||||||
|
'warehouse_id' => $this->warehouseB->id,
|
||||||
|
'machine_id' => $this->machineB->id,
|
||||||
|
'status' => 'pending',
|
||||||
|
'created_by' => $this->userB->id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 切換為 User A 請求列印 User B 的補貨單
|
||||||
|
$this->actingAs($this->userA);
|
||||||
|
$response = $this->get("/admin/warehouses/replenishments/{$replenishmentB->id}/print");
|
||||||
|
|
||||||
|
// 由於 TenantScoped Trait 自動隔離,會直接拋出 ModelNotFoundException 進而由 Laravel 轉換為 404
|
||||||
|
$response->assertStatus(404);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user