[STYLE] 庫存管理響應式 UI 優化與卡片顯示格式修正
1. 優化「目前庫存」分頁:修復平板模式下卡片展開導致的高度拉伸問題,並確保 Alpine.js 變數 (localCardOpen) 作用域隔離。 2. 優化「異動紀錄」分頁:將時間戳記從獨立行移入 2x2 內容網格,補上「時間」標題並修正字體顏色。 3. 優化「進貨單管理」分頁:卡片日期顯示格式修正為完整時分秒 (Y-m-d H:i:s)。 4. 統一搜尋佈局:將庫存管理各分頁與機台補貨單的「搜尋」與「重置」按鈕群組化,確保在窄螢幕換行時能保持連動。 5. 修正進貨單詳情:優化側邊欄 (Slide-over) 的資料載入邏輯與多語系顯示。
This commit is contained in:
parent
9d7fdc9c7f
commit
772c7a62e8
@ -4,6 +4,11 @@ namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Transaction\PickupCode;
|
||||
use App\Models\Transaction\PassCode;
|
||||
use App\Models\Machine\Machine;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class SalesController extends Controller
|
||||
{
|
||||
@ -23,14 +28,87 @@ class SalesController extends Controller
|
||||
}
|
||||
|
||||
// 取貨碼設定
|
||||
public function pickupCodes()
|
||||
public function pickupCodes(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
$query = PickupCode::with(['machine', 'creator'])->latest();
|
||||
|
||||
if ($request->search) {
|
||||
$query->where(function($q) use ($request) {
|
||||
$q->where('code', 'like', "%{$request->search}%")
|
||||
->orWhereHas('machine', function($mq) use ($request) {
|
||||
$mq->where('name', 'like', "%{$request->search}%")
|
||||
->orWhere('serial_no', 'like', "%{$request->search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->status) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$pickupCodes = $query->paginate(15)->withQueryString();
|
||||
|
||||
// 供新增彈窗使用的機台清單
|
||||
$machines = Machine::all();
|
||||
|
||||
return view('admin.sales.pickup-codes.index', [
|
||||
'title' => '取貨碼設定',
|
||||
'description' => '取貨驗證碼管理',
|
||||
'description' => '產生與管理商品取貨驗證碼',
|
||||
'pickupCodes' => $pickupCodes,
|
||||
'machines' => $machines,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 產生取貨碼
|
||||
*/
|
||||
public function storePickupCode(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'machine_id' => 'required|exists:machines,id',
|
||||
'slot_no' => 'required|string',
|
||||
'expires_hours' => 'nullable|integer|min:1|max:720', // 最長一個月
|
||||
]);
|
||||
|
||||
$expiresAt = now()->addHours($request->expires_hours ?? 24);
|
||||
|
||||
$pickupCode = PickupCode::create([
|
||||
'machine_id' => $validated['machine_id'],
|
||||
'slot_no' => $validated['slot_no'],
|
||||
'code' => PickupCode::generateUniqueCode($validated['machine_id']),
|
||||
'expires_at' => $expiresAt,
|
||||
'status' => 'active',
|
||||
'created_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Pickup code generated: :code', ['code' => $pickupCode->code]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新取貨碼 (僅限修改時間)
|
||||
*/
|
||||
public function updatePickupCode(Request $request, PickupCode $pickupCode)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'expires_at' => 'required|date|after:now',
|
||||
]);
|
||||
|
||||
$pickupCode->update([
|
||||
'expires_at' => $validated['expires_at'],
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Pickup code updated.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除/取消取貨碼
|
||||
*/
|
||||
public function destroyPickupCode(PickupCode $pickupCode)
|
||||
{
|
||||
$pickupCode->update(['status' => 'cancelled']);
|
||||
return back()->with('success', __('Pickup code cancelled.'));
|
||||
}
|
||||
|
||||
// 購買單
|
||||
public function orders()
|
||||
{
|
||||
@ -50,14 +128,83 @@ class SalesController extends Controller
|
||||
}
|
||||
|
||||
// 通行碼設定
|
||||
public function passCodes()
|
||||
public function passCodes(Request $request)
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
$query = PassCode::with(['machine', 'creator'])->latest();
|
||||
|
||||
if ($request->search) {
|
||||
$query->where(function($q) use ($request) {
|
||||
$q->where('code', 'like', "%{$request->search}%")
|
||||
->orWhere('name', 'like', "%{$request->search}%")
|
||||
->orWhereHas('machine', function($mq) use ($request) {
|
||||
$mq->where('name', 'like', "%{$request->search}%")
|
||||
->orWhere('serial_no', 'like', "%{$request->search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$passCodes = $query->paginate(15)->withQueryString();
|
||||
$machines = Machine::all();
|
||||
|
||||
return view('admin.sales.pass-codes.index', [
|
||||
'title' => '通行碼設定',
|
||||
'description' => '特殊通行碼權限管理',
|
||||
'description' => '特殊通行權限碼管理 (測試/補貨用)',
|
||||
'passCodes' => $passCodes,
|
||||
'machines' => $machines,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 產生通行碼
|
||||
*/
|
||||
public function storePassCode(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'machine_id' => 'required|exists:machines,id',
|
||||
'name' => 'nullable|string|max:50',
|
||||
'expires_days' => 'nullable|integer|min:1',
|
||||
'custom_code' => 'nullable|string|min:4|max:12',
|
||||
]);
|
||||
|
||||
$expiresAt = $request->expires_days ? now()->addDays($request->expires_days) : null;
|
||||
|
||||
$passCode = PassCode::create([
|
||||
'machine_id' => $validated['machine_id'],
|
||||
'name' => $validated['name'] ?? 'Manual Generate',
|
||||
'code' => $validated['custom_code'] ?? PassCode::generateUniqueCode($validated['machine_id']),
|
||||
'expires_at' => $expiresAt,
|
||||
'status' => 'active',
|
||||
'created_by' => Auth::id(),
|
||||
]);
|
||||
|
||||
return back()->with('success', __('Pass code created: :code', ['code' => $passCode->code]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新通行碼
|
||||
*/
|
||||
public function updatePassCode(Request $request, PassCode $passCode)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'nullable|string|max:50',
|
||||
'expires_at' => 'nullable|date',
|
||||
'status' => 'required|in:active,disabled',
|
||||
]);
|
||||
|
||||
$passCode->update($validated);
|
||||
|
||||
return back()->with('success', __('Pass code updated.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除通行碼
|
||||
*/
|
||||
public function destroyPassCode(PassCode $passCode)
|
||||
{
|
||||
$passCode->delete();
|
||||
return back()->with('success', __('Pass code deleted.'));
|
||||
}
|
||||
|
||||
// 來店禮設定
|
||||
public function storeGifts()
|
||||
{
|
||||
|
||||
80
app/Models/Transaction/PassCode.php
Normal file
80
app/Models/Transaction/PassCode.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Transaction;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\User;
|
||||
use App\Traits\TenantScoped;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PassCode extends Model
|
||||
{
|
||||
use TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'machine_id',
|
||||
'name',
|
||||
'code',
|
||||
'expires_at',
|
||||
'status',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 關聯機台
|
||||
*/
|
||||
public function machine(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯建立者
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判斷是否可用
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
if ($this->status !== 'active') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->expires_at && $this->expires_at->isPast()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 產生唯一的通行碼 (8位)
|
||||
*/
|
||||
public static function generateUniqueCode(int $machineId): string
|
||||
{
|
||||
do {
|
||||
$code = str_pad(rand(0, 99999999), 8, '0', STR_PAD_LEFT);
|
||||
$exists = self::where('machine_id', $machineId)
|
||||
->where('code', $code)
|
||||
->where('status', 'active')
|
||||
->where(function($query) {
|
||||
$query->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->exists();
|
||||
} while ($exists);
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
71
app/Models/Transaction/PickupCode.php
Normal file
71
app/Models/Transaction/PickupCode.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Transaction;
|
||||
|
||||
use App\Models\Machine\Machine;
|
||||
use App\Models\User;
|
||||
use App\Traits\TenantScoped;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PickupCode extends Model
|
||||
{
|
||||
use TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'machine_id',
|
||||
'slot_no',
|
||||
'code',
|
||||
'expires_at',
|
||||
'used_at',
|
||||
'status',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'used_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 關聯機台
|
||||
*/
|
||||
public function machine(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Machine::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 關聯建立者
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判斷是否可用
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->status === 'active' && $this->expires_at->isFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* 產生唯一的取貨碼 (4位)
|
||||
*/
|
||||
public static function generateUniqueCode(int $machineId): string
|
||||
{
|
||||
do {
|
||||
$code = str_pad(rand(0, 9999), 4, '0', STR_PAD_LEFT);
|
||||
$exists = self::where('machine_id', $machineId)
|
||||
->where('code', $code)
|
||||
->where('status', 'active')
|
||||
->where('expires_at', '>', now())
|
||||
->exists();
|
||||
} while ($exists);
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('pickup_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->onDelete('cascade');
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->string('slot_no')->comment('貨道編號');
|
||||
$table->string('code', 10)->comment('取貨碼 (通常為 4 位)');
|
||||
$table->timestamp('expires_at')->comment('到期時間');
|
||||
$table->timestamp('used_at')->nullable()->comment('使用時間');
|
||||
$table->string('status')->default('active')->comment('狀態: active, used, expired, cancelled');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['company_id', 'machine_id', 'code', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('pickup_codes');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('pass_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->onDelete('cascade');
|
||||
$table->foreignId('machine_id')->constrained('machines')->onDelete('cascade');
|
||||
$table->string('name')->nullable()->comment('通行碼名稱/用途');
|
||||
$table->string('code', 20)->comment('通行碼 (預設 8 位)');
|
||||
$table->timestamp('expires_at')->nullable()->comment('到期時間 (null 為永久有效)');
|
||||
$table->string('status')->default('active')->comment('狀態: active, expired, disabled');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['company_id', 'machine_id', 'code', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('pass_codes');
|
||||
}
|
||||
};
|
||||
@ -341,6 +341,7 @@
|
||||
"Describe the repair or maintenance status...": "Describe the repair or maintenance status...",
|
||||
"Deselect All": "Deselect All",
|
||||
"Detail": "Detail",
|
||||
"Details": "Details",
|
||||
"Device Information": "Device Information",
|
||||
"Device Status Logs": "Device Status Logs",
|
||||
"Devices": "Devices",
|
||||
|
||||
@ -227,6 +227,7 @@
|
||||
"Update Settings": "設定を更新",
|
||||
"Video": "動画",
|
||||
"View Details": "詳細を表示",
|
||||
"Details": "詳細",
|
||||
"View Inventory": "在庫を表示",
|
||||
"View Slots": "貨道を表示",
|
||||
"Visual overview of all machine locations": "全機器位置のビジュアル概要",
|
||||
|
||||
@ -363,6 +363,7 @@
|
||||
"Describe the repair or maintenance status...": "請描述維修或保養狀況...",
|
||||
"Deselect All": "取消全選",
|
||||
"Detail": "詳細",
|
||||
"Details": "明細",
|
||||
"Device Information": "設備資訊",
|
||||
"Device Status Logs": "設備狀態紀錄",
|
||||
"Devices": "台設備",
|
||||
|
||||
193
resources/views/admin/sales/pass-codes/index.blade.php
Normal file
193
resources/views/admin/sales/pass-codes/index.blade.php
Normal file
@ -0,0 +1,193 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="{
|
||||
showCreateModal: false,
|
||||
selectedMachine: '',
|
||||
name: '',
|
||||
expiresDays: 7,
|
||||
customCode: '',
|
||||
useCustomCode: false,
|
||||
|
||||
generateRandomCode() {
|
||||
this.customCode = Math.floor(Math.random() * 90000000 + 10000000).toString();
|
||||
}
|
||||
}">
|
||||
{{-- Page Header --}}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Pass Codes') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage multi-use authorization codes for testing and maintenance') }}</p>
|
||||
</div>
|
||||
<button @click="showCreateModal = true; generateRandomCode()" class="luxury-btn-primary group">
|
||||
<svg class="w-5 h-5 mr-2 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ __('Create Pass Code') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Search & Filters --}}
|
||||
<div class="luxury-card rounded-3xl p-6 mb-8">
|
||||
<form action="{{ route('admin.sales.pass-codes') }}" method="GET" class="flex flex-col md:flex-row gap-4">
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search by code, name or machine...') }}">
|
||||
</div>
|
||||
<button type="submit" class="luxury-btn-secondary px-8">
|
||||
{{ __('Search') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="luxury-card rounded-3xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Pass Code') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Name / Machine') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Expires At') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
|
||||
@forelse ($passCodes as $code)
|
||||
<tr class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-lg font-black font-mono text-slate-700 dark:text-slate-200 tracking-widest bg-white dark:bg-slate-900 px-4 py-1.5 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm group-hover:border-cyan-500/50 transition-colors">
|
||||
{{ $code->code }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-black text-slate-800 dark:text-white">{{ $code->name }}</div>
|
||||
<div class="text-xs font-bold text-slate-400 uppercase tracking-tighter">{{ $code->machine->name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@if($code->expires_at)
|
||||
<div class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $code->expires_at->format('Y-m-d H:i') }}</div>
|
||||
<div class="text-xs font-medium {{ $code->expires_at->isPast() ? 'text-rose-500' : 'text-slate-400' }}">
|
||||
{{ $code->expires_at->isPast() ? __('Expired') : $code->expires_at->diffForHumans() }}
|
||||
</div>
|
||||
@else
|
||||
<span class="text-xs font-black uppercase tracking-widest text-emerald-500">{{ __('Permanent') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@php
|
||||
$isValid = $code->isValid();
|
||||
@endphp
|
||||
<span class="px-3 py-1 text-xs font-black uppercase tracking-widest rounded-full border {{ $isValid ? 'bg-emerald-50 text-emerald-600 border-emerald-100 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20' : 'bg-slate-100 text-slate-500 border-slate-200 dark:bg-slate-700 dark:text-slate-400 dark:border-slate-600' }}">
|
||||
{{ $isValid ? __('Active') : __('Inactive') }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<form action="{{ route('admin.sales.pass-codes.destroy', $code) }}" method="POST" onsubmit="return confirm('{{ __('Are you sure you want to delete this pass code?') }}')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="p-2 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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 1v2m3 4h.01" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-12 text-center">
|
||||
<p class="text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest text-xs">{{ __('No pass codes found') }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if ($passCodes->hasPages())
|
||||
<div class="px-6 py-4 border-t border-slate-100 dark:border-slate-700">
|
||||
{{ $passCodes->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Create Modal --}}
|
||||
<div x-show="showCreateModal"
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
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"
|
||||
x-cloak>
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showCreateModal = false"></div>
|
||||
|
||||
<div class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-200/50 dark:border-slate-700/50 animate-luxury-in">
|
||||
<form action="{{ route('admin.sales.pass-codes.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('Create Pass Code') }}</h3>
|
||||
<button type="button" @click="showCreateModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Machine Selection --}}
|
||||
<div>
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Target Machine') }}</label>
|
||||
<select name="machine_id" x-model="selectedMachine" class="luxury-input w-full" required>
|
||||
<option value="">{{ __('Please select a machine') }}</option>
|
||||
@foreach($machines as $machine)
|
||||
<option value="{{ $machine->id }}">{{ $machine->name }} ({{ $machine->serial_no }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Name --}}
|
||||
<div>
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Description / Name') }}</label>
|
||||
<input type="text" name="name" x-model="name" class="luxury-input w-full" placeholder="{{ __('e.g. Test Code for Maintenance') }}" required>
|
||||
</div>
|
||||
|
||||
{{-- Code --}}
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Pass Code (8 Digits)') }}</label>
|
||||
<button type="button" @click="generateRandomCode()" class="text-xs font-bold text-cyan-500 hover:text-cyan-600 uppercase tracking-widest">{{ __('Regenerate') }}</button>
|
||||
</div>
|
||||
<input type="text" name="custom_code" x-model="customCode" class="luxury-input w-full font-mono text-lg tracking-widest" maxlength="12" required>
|
||||
</div>
|
||||
|
||||
{{-- Validity --}}
|
||||
<div>
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Validity Period (Days)') }}</label>
|
||||
<input type="number" name="expires_days" x-model="expiresDays" class="luxury-input w-full" min="1" placeholder="{{ __('Leave empty for permanent') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-8 py-6 bg-slate-50/50 dark:bg-slate-800/50 flex justify-end gap-4 border-t border-slate-100 dark:border-slate-700">
|
||||
<button type="button" @click="showCreateModal = false" class="luxury-btn-secondary">
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="luxury-btn-primary px-8">
|
||||
{{ __('Create') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
226
resources/views/admin/sales/pickup-codes/index.blade.php
Normal file
226
resources/views/admin/sales/pickup-codes/index.blade.php
Normal file
@ -0,0 +1,226 @@
|
||||
@extends('layouts.admin')
|
||||
|
||||
@section('content')
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8 w-full max-w-9xl mx-auto" x-data="{
|
||||
showCreateModal: false,
|
||||
selectedMachine: '',
|
||||
selectedSlot: '',
|
||||
expiresHours: 24,
|
||||
slots: [],
|
||||
loadingSlots: false,
|
||||
|
||||
async fetchSlots() {
|
||||
if (!this.selectedMachine) {
|
||||
this.slots = [];
|
||||
return;
|
||||
}
|
||||
this.loadingSlots = true;
|
||||
try {
|
||||
const response = await fetch(`/admin/machines/${this.selectedMachine}/slots-ajax`);
|
||||
const data = await response.json();
|
||||
this.slots = data.slots || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching slots:', error);
|
||||
} finally {
|
||||
this.loadingSlots = false;
|
||||
}
|
||||
}
|
||||
}">
|
||||
{{-- Page Header --}}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Pickup Codes') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Generate and manage one-time pickup codes for customers') }}</p>
|
||||
</div>
|
||||
<button @click="showCreateModal = true" class="luxury-btn-primary group">
|
||||
<svg class="w-5 h-5 mr-2 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ __('Generate New Code') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Search & Filters --}}
|
||||
<div class="luxury-card rounded-3xl p-6 mb-8">
|
||||
<form action="{{ route('admin.sales.pickup-codes') }}" method="GET" class="flex flex-col md:flex-row gap-4">
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search by code, machine name or serial...') }}">
|
||||
</div>
|
||||
<div class="w-full md:w-48">
|
||||
<select name="status" class="luxury-input w-full" onchange="this.form.submit()">
|
||||
<option value="">{{ __('All Status') }}</option>
|
||||
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>{{ __('Active') }}</option>
|
||||
<option value="used" {{ request('status') === 'used' ? 'selected' : '' }}>{{ __('Used') }}</option>
|
||||
<option value="expired" {{ request('status') === 'expired' ? 'selected' : '' }}>{{ __('Expired') }}</option>
|
||||
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }}>{{ __('Cancelled') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="luxury-btn-secondary px-8">
|
||||
{{ __('Filter') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="luxury-card rounded-3xl overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-separate border-spacing-0">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-800/50">
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Code') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Machine / Slot') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Expires At') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700">{{ __('Status') }}</th>
|
||||
<th class="px-6 py-4 text-xs font-black uppercase tracking-wider text-slate-500 dark:text-slate-400 border-b border-slate-100 dark:border-slate-700 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-700">
|
||||
@forelse ($pickupCodes as $code)
|
||||
<tr class="group hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-lg font-black font-mono text-cyan-600 dark:text-cyan-400 tracking-tighter bg-cyan-50 dark:bg-cyan-500/10 px-3 py-1 rounded-lg border border-cyan-100 dark:border-cyan-500/20 shadow-sm">
|
||||
{{ $code->code }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-black text-slate-800 dark:text-white">{{ $code->machine->name }}</div>
|
||||
<div class="text-xs font-bold text-slate-400 uppercase tracking-tighter">{{ __('Slot') }}: {{ $code->slot_no }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-bold text-slate-600 dark:text-slate-300">{{ $code->expires_at->format('Y-m-d H:i') }}</div>
|
||||
<div class="text-xs font-medium text-slate-400">{{ $code->expires_at->diffForHumans() }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
@php
|
||||
$statusClasses = [
|
||||
'active' => 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400 border-emerald-100 dark:border-emerald-500/20',
|
||||
'used' => 'bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400 border-blue-100 dark:border-blue-500/20',
|
||||
'expired' => 'bg-rose-50 text-rose-600 dark:bg-rose-500/10 dark:text-rose-400 border-rose-100 dark:border-rose-500/20',
|
||||
'cancelled' => 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400 border-slate-200 dark:border-slate-600',
|
||||
];
|
||||
$displayStatus = $code->status === 'active' && $code->expires_at->isPast() ? 'expired' : $code->status;
|
||||
@endphp
|
||||
<span class="px-3 py-1 text-xs font-black uppercase tracking-widest rounded-full border {{ $statusClasses[$displayStatus] ?? $statusClasses['cancelled'] }}">
|
||||
{{ __($displayStatus) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
@if($code->status === 'active' && $code->expires_at->isFuture())
|
||||
<form action="{{ route('admin.sales.pickup-codes.destroy', $code) }}" method="POST" onsubmit="return confirm('{{ __('Are you sure you want to cancel this code?') }}')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="p-2 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-colors" title="{{ __('Cancel Code') }}">
|
||||
<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="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-16 h-16 bg-slate-100 dark:bg-slate-800 rounded-full flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-500 dark:text-slate-400 font-bold uppercase tracking-widest text-xs">{{ __('No pickup codes found') }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if ($pickupCodes->hasPages())
|
||||
<div class="px-6 py-4 border-t border-slate-100 dark:border-slate-700">
|
||||
{{ $pickupCodes->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Create Modal --}}
|
||||
<div x-show="showCreateModal"
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
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"
|
||||
x-cloak>
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 transition-opacity bg-slate-900/60 backdrop-blur-sm" @click="showCreateModal = false"></div>
|
||||
|
||||
<div class="inline-block overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-slate-900 rounded-3xl shadow-2xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-200/50 dark:border-slate-700/50 animate-luxury-in">
|
||||
<form action="{{ route('admin.sales.pickup-codes.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('Generate Pickup Code') }}</h3>
|
||||
<button type="button" @click="showCreateModal = false" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Machine Selection --}}
|
||||
<div>
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Select Machine') }}</label>
|
||||
<select name="machine_id" x-model="selectedMachine" @change="fetchSlots()" class="luxury-input w-full" required>
|
||||
<option value="">{{ __('Please select a machine') }}</option>
|
||||
@foreach($machines as $machine)
|
||||
<option value="{{ $machine->id }}">{{ $machine->name }} ({{ $machine->serial_no }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Slot Selection --}}
|
||||
<div>
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Select Slot') }}</label>
|
||||
<div class="relative">
|
||||
<select name="slot_no" x-model="selectedSlot" class="luxury-input w-full" :disabled="!selectedMachine || loadingSlots" required>
|
||||
<option value="">{{ __('Select Slot') }}</option>
|
||||
<template x-for="slot in slots" :key="slot.slot_no">
|
||||
<option :value="slot.slot_no" x-text="`${slot.slot_no} - ${slot.product_name || 'Empty'}`"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div x-show="loadingSlots" class="absolute right-10 top-2.5">
|
||||
<svg class="animate-spin h-5 w-5 text-cyan-500" 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Validity --}}
|
||||
<div>
|
||||
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-2">{{ __('Validity Period (Hours)') }}</label>
|
||||
<input type="number" name="expires_hours" x-model="expiresHours" class="luxury-input w-full" min="1" max="720" required>
|
||||
<p class="mt-1 text-xs text-slate-400 font-medium">{{ __('Default is 24 hours. Max is 720 hours (30 days).') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-8 py-6 bg-slate-50/50 dark:bg-slate-800/50 flex justify-end gap-4 border-t border-slate-100 dark:border-slate-700">
|
||||
<button type="button" @click="showCreateModal = false" class="luxury-btn-secondary">
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="luxury-btn-primary px-8" :disabled="!selectedSlot">
|
||||
{{ __('Generate') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -270,9 +270,9 @@
|
||||
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex 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>
|
||||
<h1 class="text-2xl sm: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>
|
||||
|
||||
@ -84,18 +84,20 @@
|
||||
</button>
|
||||
</template>
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Machine Inventory Overview') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Real-time slot-level inventory across all machines') }}</p>
|
||||
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{
|
||||
__('Machine Inventory Overview') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{
|
||||
__('Real-time slot-level inventory across all machines') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- List Mode --}}
|
||||
<div x-show="viewMode === 'list'" class="space-y-6 animate-luxury-in" x-cloak>
|
||||
<div class="luxury-card rounded-3xl p-8 hover:translate-y-0 transition-transform duration-300">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
||||
<div class="luxury-card rounded-3xl p-8 hover:translate-y-0 transition-transform duration-300">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
|
||||
<form action="{{ route('admin.warehouses.machine-inventory') }}" method="GET"
|
||||
class="flex items-center gap-4">
|
||||
<div class="relative group">
|
||||
class="flex flex-wrap items-center gap-4 flex-1">
|
||||
<div class="relative group w-full sm:w-72">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
@ -104,30 +106,34 @@
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
class="py-2.5 pl-12 pr-6 block w-72 luxury-input"
|
||||
class="py-2.5 pl-12 pr-6 block w-full luxury-input"
|
||||
placeholder="{{ __('Search machines...') }}">
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group"
|
||||
title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 flex-none ml-auto sm:ml-0">
|
||||
<button type="submit"
|
||||
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95"
|
||||
title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button type="button" @click="
|
||||
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
|
||||
$el.closest('form').submit();
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group"
|
||||
title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click="
|
||||
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
|
||||
$el.closest('form').submit();
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95"
|
||||
title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -184,7 +190,8 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="font-mono font-bold text-slate-500 uppercase tracking-widest text-sm">{{
|
||||
<span
|
||||
class="font-mono font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest text-sm group-hover:text-cyan-500/70 transition-colors">{{
|
||||
$machine->serial_no }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-center">
|
||||
@ -326,7 +333,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Active Slots -->
|
||||
<div class="luxury-card rounded-[2rem] p-6 border-emerald-500/10 bg-emerald-500/[0.02] hover:translate-y-0 transition-transform duration-300">
|
||||
<div
|
||||
class="luxury-card rounded-[2rem] p-6 border-emerald-500/10 bg-emerald-500/[0.02] hover:translate-y-0 transition-transform duration-300">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-emerald-500/10 flex items-center justify-center text-emerald-500">
|
||||
@ -343,7 +351,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty Slots -->
|
||||
<div class="luxury-card rounded-[2rem] p-6 border-slate-500/10 bg-slate-500/[0.02] hover:translate-y-0 transition-transform duration-300">
|
||||
<div
|
||||
class="luxury-card rounded-[2rem] p-6 border-slate-500/10 bg-slate-500/[0.02] hover:translate-y-0 transition-transform duration-300">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-xl bg-slate-500/10 flex items-center justify-center text-slate-500">
|
||||
@ -360,7 +369,8 @@
|
||||
</div>
|
||||
|
||||
<!-- High Stock -->
|
||||
<div class="luxury-card rounded-[2rem] p-6 border-cyan-500/10 bg-cyan-500/[0.02] sm:col-span-2 hover:translate-y-0 transition-transform duration-300">
|
||||
<div
|
||||
class="luxury-card rounded-[2rem] p-6 border-cyan-500/10 bg-cyan-500/[0.02] sm:col-span-2 hover:translate-y-0 transition-transform duration-300">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
@ -418,7 +428,8 @@
|
||||
|
||||
<!-- Grid View -->
|
||||
<div x-show="displayMode === 'grid'" class="space-y-6">
|
||||
<div class="luxury-card rounded-[2.5rem] p-8 border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[400px] hover:translate-y-0 transition-transform duration-300">
|
||||
<div
|
||||
class="luxury-card rounded-[2.5rem] p-8 border-slate-200/50 dark:border-slate-800/50 bg-white/30 dark:bg-slate-900/40 backdrop-blur-xl relative overflow-hidden min-h-[400px] hover:translate-y-0 transition-transform duration-300">
|
||||
<div x-show="loading"
|
||||
class="absolute inset-0 bg-white/60 dark:bg-slate-900/60 backdrop-blur-md z-20 flex items-center justify-center transition-all duration-500">
|
||||
<div class="w-12 h-12 border-4 border-cyan-500/20 border-t-cyan-500 rounded-full animate-spin">
|
||||
@ -465,7 +476,8 @@
|
||||
|
||||
<!-- Table View -->
|
||||
<div x-show="displayMode === 'table'" class="animate-luxury-in" x-cloak>
|
||||
<div class="luxury-card rounded-[2.5rem] overflow-hidden border-slate-200/50 dark:border-slate-800/50 shadow-xl hover:translate-y-0 transition-transform duration-300">
|
||||
<div
|
||||
class="luxury-card rounded-[2.5rem] overflow-hidden border-slate-200/50 dark:border-slate-800/50 shadow-xl hover:translate-y-0 transition-transform duration-300">
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 dark:bg-slate-900/50">
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4"
|
||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center justify-between mb-8 gap-4"
|
||||
@submit.prevent="handleFilterSubmit('movements')">
|
||||
<input type="hidden" name="tab" value="movements">
|
||||
|
||||
<div class="flex flex-wrap gap-4 items-center">
|
||||
<div class="w-64">
|
||||
<div class="flex flex-wrap gap-4 items-center flex-1">
|
||||
<div class="w-full sm:w-64">
|
||||
<x-searchable-select
|
||||
name="warehouse_id"
|
||||
:options="$warehouses"
|
||||
@ -13,7 +13,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-64">
|
||||
<div class="w-full sm:w-64">
|
||||
<x-searchable-select
|
||||
name="type"
|
||||
:selected="request('type')"
|
||||
@ -28,7 +28,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Date Range --}}
|
||||
<div class="relative group w-72"
|
||||
<div class="relative group w-full sm:w-72"
|
||||
x-data="{ fp: null }"
|
||||
x-init="fp = flatpickr($refs.dateRange, {
|
||||
mode: 'range',
|
||||
@ -59,8 +59,8 @@
|
||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||
<div class="flex items-center gap-2 ml-auto sm:ml-0">
|
||||
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
@ -74,9 +74,10 @@
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
|
||||
handleFilterSubmit('movements');
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
@ -85,7 +86,8 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
{{-- Table Mode (Desktop) --}}
|
||||
<div class="hidden xl:block 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">
|
||||
@ -162,6 +164,60 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Card Mode (Mobile & Tablet) --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8">
|
||||
@forelse($movements as $mv)
|
||||
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
|
||||
<div class="flex items-start justify-between gap-4 mb-6">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
|
||||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $mv->product?->localized_name }}</h3>
|
||||
<p class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">ID: {{ $mv->product_id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
@php $typeColor = in_array($mv->type, ['in', 'transfer_in']) ? 'cyan' : (in_array($mv->type, ['out', 'transfer_out']) ? 'rose' : 'amber'); @endphp
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-{{ $typeColor }}-500/10 text-{{ $typeColor }}-500 border border-{{ $typeColor }}-500/20 uppercase tracking-widest shadow-sm">
|
||||
{{ __(\App\Models\Warehouse\StockMovement::TYPE_LABELS[$mv->type] ?? $mv->type) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-y-4 mb-4 border-y border-slate-100 dark:border-slate-800/50 py-4">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Warehouse') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{ $mv->warehouse?->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Qty Change') }}</p>
|
||||
<p class="text-sm font-extrabold {{ in_array($mv->type, ['in', 'transfer_in']) ? 'text-cyan-500' : 'text-rose-500' }}">
|
||||
{{ in_array($mv->type, ['in', 'transfer_in']) ? '+' : '-' }}{{ $mv->quantity }}
|
||||
<span class="text-[10px] text-slate-400 font-bold ml-1">({{ $mv->before_qty }}→{{ $mv->after_qty }})</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Operator') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $mv->creator?->name ?? '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Time') }}</p>
|
||||
<p class="text-sm font-mono font-bold text-slate-700 dark:text-slate-300">{{ $mv->created_at?->format('Y-m-d H:i:s') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No movement records found') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $movements->links('vendor.pagination.luxury') }}
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
{{-- Filters --}}
|
||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4"
|
||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center justify-between mb-8 gap-4"
|
||||
@submit.prevent="handleFilterSubmit('stock-in')">
|
||||
<input type="hidden" name="tab" value="stock-in">
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div class="relative group">
|
||||
<div class="flex flex-wrap gap-4 items-center flex-1">
|
||||
<div class="relative group w-full sm:w-72">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
@ -15,11 +15,11 @@
|
||||
</span>
|
||||
<input type="text" name="search_order" value="{{ request('search_order') }}"
|
||||
@keydown.enter.prevent="handleFilterSubmit('stock-in')"
|
||||
class="py-2.5 pl-12 pr-6 block w-64 luxury-input" placeholder="{{ __('Search order number...') }}">
|
||||
class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
|
||||
</div>
|
||||
|
||||
{{-- Date Range --}}
|
||||
<div class="relative group w-72"
|
||||
<div class="relative group w-full sm:w-72"
|
||||
x-data="{ fp: null }"
|
||||
x-init="fp = flatpickr($refs.dateRange, {
|
||||
mode: 'range',
|
||||
@ -50,31 +50,33 @@
|
||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 ml-auto sm:ml-0">
|
||||
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@click="
|
||||
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
handleFilterSubmit('stock-in');
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button"
|
||||
@click="
|
||||
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
handleFilterSubmit('stock-in');
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(isset($stock_in_status_options))
|
||||
<div class="w-48">
|
||||
<div class="w-full sm:w-48">
|
||||
<x-searchable-select
|
||||
name="status"
|
||||
:selected="request('status')"
|
||||
@ -89,8 +91,8 @@
|
||||
@endif
|
||||
</form>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="overflow-x-auto">
|
||||
{{-- Table Mode (Desktop) --}}
|
||||
<div class="hidden xl:block 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">
|
||||
@ -220,6 +222,82 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Card Mode (Mobile & Tablet) --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8">
|
||||
@forelse($orders as $order)
|
||||
<div class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group"
|
||||
@click="openOrderDetails('{{ $order->id }}')">
|
||||
<div class="flex items-start justify-between gap-4 mb-6">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<div class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-400 border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 group-hover:text-white transition-all duration-300 overflow-hidden shadow-sm shrink-0">
|
||||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="text-base sm:text-lg font-black text-slate-800 dark:text-slate-100 break-all group-hover:text-cyan-600 transition-colors tracking-tight">{{ $order->order_no }}</h3>
|
||||
<p class="text-[10px] font-mono font-bold text-slate-400 uppercase tracking-widest truncate mt-0.5">{{ $order->created_at->format('Y-m-d H:i:s') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
@if($order->status === 'draft')
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-amber-500/10 text-amber-500 border border-amber-500/20 uppercase tracking-widest shadow-sm">
|
||||
{{ __('Draft') }}
|
||||
</span>
|
||||
@elseif($order->status === 'cancelled')
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-slate-500/10 text-slate-500 border border-slate-500/20 uppercase tracking-widest shadow-sm">
|
||||
{{ __('Cancelled') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-[10px] font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 uppercase tracking-widest shadow-sm">
|
||||
{{ __('Completed') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-y-4 mb-6 border-y border-slate-100 dark:border-slate-800/50 py-4">
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Warehouse') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300 truncate">{{ $order->warehouse?->name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Items') }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ $order->items_count }}</p>
|
||||
</div>
|
||||
@if($order->note)
|
||||
<div class="col-span-2">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{{ __('Note') }}</p>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 italic line-clamp-1">"{{ $order->note }}"</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" @click="openOrderDetails('{{ $order->id }}')"
|
||||
class="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black text-[10px] sm:text-xs tracking-widest hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-300 border border-slate-200 dark:border-slate-700 shadow-sm">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>
|
||||
{{ __('Details') }}
|
||||
</button>
|
||||
@if($order->status === 'draft')
|
||||
<form action="{{ route('admin.warehouses.inventory.stock-in.confirm', $order) }}" method="POST" class="flex-1" @submit.prevent="confirmStockIn($el)">
|
||||
@csrf @method('PATCH')
|
||||
<button type="submit"
|
||||
class="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-emerald-500/10 text-emerald-500 font-black text-xs uppercase tracking-widest hover:bg-emerald-500 hover:text-white transition-all duration-300 border border-emerald-500/20 shadow-sm">
|
||||
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>
|
||||
{{ __('Confirm') }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full py-20 text-center flex flex-col items-center gap-4 bg-white/50 dark:bg-slate-900/50 rounded-3xl border border-slate-100 dark:border-slate-800/50 luxury-card">
|
||||
<p class="text-slate-400 font-extrabold tracking-widest uppercase text-xs">{{ __('No stock-in orders found') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-8 py-6 border-t border-slate-50 dark:border-slate-800/50">
|
||||
{{ $orders->links('vendor.pagination.luxury', ['ajax_navigate_event' => 'ajax:navigate:stock-in']) }}
|
||||
</div>
|
||||
|
||||
@ -1,59 +1,75 @@
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8"
|
||||
@submit.prevent="handleFilterSubmit('stock')">
|
||||
<form action="{{ route('admin.warehouses.inventory') }}" method="GET" class="flex flex-wrap items-center gap-4 mb-8"
|
||||
@submit.prevent="handleFilterSubmit('stock')">
|
||||
<input type="hidden" name="tab" value="stock">
|
||||
<div class="relative group">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"><svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></span>
|
||||
|
||||
<div class="relative group w-full sm:w-72">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10"><svg
|
||||
class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg></span>
|
||||
<input type="text" name="search" value="{{ request('search') }}"
|
||||
class="py-2.5 pl-12 pr-6 block w-64 luxury-input"
|
||||
placeholder="{{ __('Search products...') }}"
|
||||
@keydown.enter.prevent="handleFilterSubmit('stock')">
|
||||
class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search products...') }}"
|
||||
@keydown.enter.prevent="handleFilterSubmit('stock')">
|
||||
</div>
|
||||
|
||||
{{-- 多選倉庫篩選器 --}}
|
||||
<div class="min-w-[260px]">
|
||||
<x-multi-select
|
||||
name="warehouse_ids"
|
||||
:options="$warehouses"
|
||||
:selected="request('warehouse_ids', [])"
|
||||
:placeholder="__('Filter by Warehouse Presence')"
|
||||
/>
|
||||
<div class="w-full sm:w-72">
|
||||
<x-multi-select name="warehouse_ids" :options="$warehouses" :selected="request('warehouse_ids', [])"
|
||||
:placeholder="__('Filter by Warehouse Presence')" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||
<div class="flex items-center gap-2 ml-auto sm:ml-0">
|
||||
<button type="submit"
|
||||
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95"
|
||||
title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@click="
|
||||
$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = '');
|
||||
<button type="button" @click="
|
||||
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=tab])').forEach(i => i.value = '');
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
$el.closest('form').dispatchEvent(new CustomEvent('multi-select-reset', { bubbles: true }));
|
||||
handleFilterSubmit('stock');
|
||||
$nextTick(() => handleFilterSubmit('stock'));
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95"
|
||||
title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="overflow-x-auto -mx-8 px-8 custom-scrollbar">
|
||||
<div class="hidden xl:block overflow-x-auto -mx-8 px-8 custom-scrollbar">
|
||||
<table class="w-full text-left border-separate border-spacing-y-0 min-w-max">
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-white/[0.02]">
|
||||
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 sticky left-0 bg-white dark:bg-[#1e293b] z-20 w-[180px] md:w-[260px] min-w-[180px] md:min-w-[260px]">{{ __('Product') }}</th>
|
||||
<th class="px-6 py-5 text-sm font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.1em] border-b border-slate-100 dark:border-slate-800 text-center bg-white dark:bg-[#1e293b] sticky md:left-[260px] z-20 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[1px_0_0_0_rgba(0,0,0,0.05)] dark:shadow-[1px_0_0_0_rgba(255,255,255,0.05)]">
|
||||
<th
|
||||
class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 sticky left-0 bg-white dark:bg-[#1e293b] z-20 w-[180px] xl:w-[260px] min-w-[180px] xl:min-w-[260px]">
|
||||
{{ __('Product') }}</th>
|
||||
<th
|
||||
class="px-6 py-5 text-sm font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.1em] border-b border-slate-100 dark:border-slate-800 text-center bg-white dark:bg-[#1e293b] sticky xl:left-[260px] z-20 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[1px_0_0_0_rgba(0,0,0,0.05)] dark:shadow-[1px_0_0_0_rgba(255,255,255,0.05)]">
|
||||
{{ __('Total') }}
|
||||
</th>
|
||||
@foreach($inventory_warehouses as $w)
|
||||
<th class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
<th
|
||||
class="px-6 py-5 text-sm font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.15em] border-b border-slate-100 dark:border-slate-800 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-base font-black text-slate-800 dark:text-slate-200 tracking-tight">{{ $w->name }}</span>
|
||||
<span class="text-[10px] font-bold opacity-60 tracking-widest mt-0.5 uppercase">{{ $w->type === 'main' ? __('Main') : __('Branch') }}</span>
|
||||
<span class="text-base font-black text-slate-800 dark:text-slate-200 tracking-tight">{{
|
||||
$w->name }}</span>
|
||||
<span class="text-[10px] font-bold opacity-60 tracking-widest mt-0.5 uppercase">{{ $w->type
|
||||
=== 'main' ? __('Main') : __('Branch') }}</span>
|
||||
</div>
|
||||
</th>
|
||||
@endforeach
|
||||
@ -62,61 +78,82 @@
|
||||
<tbody class="divide-y divide-slate-50 dark:divide-white/[0.05]">
|
||||
@forelse($products as $product)
|
||||
@php
|
||||
$viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray();
|
||||
$totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity');
|
||||
$viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray();
|
||||
$totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity');
|
||||
@endphp
|
||||
<tr class="group hover:bg-slate-50 dark:hover:bg-white/[0.03] transition-all duration-300">
|
||||
<td class="px-6 py-4 sticky left-0 bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] z-10 transition-colors w-[180px] md:w-[260px] min-w-[180px] md:min-w-[260px]">
|
||||
<div class="flex items-center gap-3 md:gap-4">
|
||||
<div class="w-8 h-8 md:w-11 md:h-11 rounded-xl md:rounded-2xl bg-slate-100 dark:bg-slate-800/50 flex items-center justify-center overflow-hidden border border-slate-200/50 dark:border-white/5 group-hover:border-cyan-500/30 transition-colors shrink-0">
|
||||
<td
|
||||
class="px-6 py-4 sticky left-0 bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] z-10 transition-colors w-[180px] xl:w-[260px] min-w-[180px] xl:min-w-[260px]">
|
||||
<div class="flex items-center gap-3 lg:gap-4">
|
||||
<div
|
||||
class="w-8 h-8 lg:w-11 lg:h-11 rounded-xl lg:rounded-2xl bg-slate-100 dark:bg-slate-800/50 flex items-center justify-center overflow-hidden border border-slate-200/50 dark:border-white/5 group-hover:border-cyan-500/30 transition-colors shrink-0">
|
||||
@if($product->image_url)
|
||||
<img src="{{ $product->image_url }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<svg class="w-4 h-4 md:w-5 md:h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" /></svg>
|
||||
<svg class="w-4 h-4 lg:w-5 lg:h-5 text-slate-400" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-xs md:text-sm font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors truncate">{{ $product->localized_name }}</span>
|
||||
<span
|
||||
class="text-xs lg:text-sm font-extrabold text-slate-800 dark:text-slate-100 group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors truncate">{{
|
||||
$product->localized_name }}</span>
|
||||
@if($product->barcode)
|
||||
<span class="text-[9px] md:text-[10px] font-bold text-slate-400 mt-0.5 tracking-tight truncate">{{ $product->barcode }}</span>
|
||||
<span
|
||||
class="text-[9px] lg:text-[10px] font-bold text-slate-400 mt-0.5 tracking-tight truncate">{{
|
||||
$product->barcode }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 md:px-6 py-4 text-center bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] transition-colors sticky md:left-[260px] z-10 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[4px_0_12px_-4px_rgba(0,0,0,0.1)] dark:shadow-[4px_0_12px_-4px_rgba(0,0,0,0.3)]">
|
||||
<span class="text-lg md:text-xl font-mono font-black text-cyan-600 dark:text-cyan-400 drop-shadow-[0_0_10px_rgba(6,182,212,0.1)]">
|
||||
<td
|
||||
class="px-4 lg:px-6 py-4 text-center bg-white dark:bg-[#1e293b] group-hover:bg-slate-50 dark:group-hover:bg-white/[0.03] transition-colors sticky xl:left-[260px] z-10 border-r border-slate-200/50 dark:border-slate-700/50 shadow-[4px_0_12px_-4px_rgba(0,0,0,0.1)] dark:shadow-[4px_0_12px_-4px_rgba(0,0,0,0.3)]">
|
||||
<span
|
||||
class="text-lg lg:text-xl font-mono font-black text-cyan-600 dark:text-cyan-400 drop-shadow-[0_0_10px_rgba(6,182,212,0.1)]">
|
||||
{{ $totalQuantity }}
|
||||
</span>
|
||||
</td>
|
||||
@foreach($inventory_warehouses as $w)
|
||||
@php
|
||||
$stock = $product->stocks->firstWhere('warehouse_id', $w->id);
|
||||
$isLow = $stock && $stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock;
|
||||
@endphp
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($stock && $stock->quantity > 0)
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-base font-black {{ $isLow ? 'text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.2)]' : 'text-slate-700 dark:text-slate-200' }}">
|
||||
{{ $stock->quantity }}
|
||||
</span>
|
||||
@if($isLow)
|
||||
<span class="text-[9px] font-black text-rose-500 uppercase tracking-tighter opacity-80">{{ __('Low') }}</span>
|
||||
$stock = $product->stocks->firstWhere('warehouse_id', $w->id);
|
||||
$isLow = $stock && $stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock;
|
||||
@endphp
|
||||
<td class="px-6 py-4 text-center">
|
||||
@if($stock && $stock->quantity > 0)
|
||||
<div class="flex flex-col items-center">
|
||||
<span
|
||||
class="text-base font-black {{ $isLow ? 'text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.2)]' : 'text-slate-700 dark:text-slate-200' }}">
|
||||
{{ $stock->quantity }}
|
||||
</span>
|
||||
@if($isLow)
|
||||
<span
|
||||
class="text-[9px] font-black text-rose-500 uppercase tracking-tighter opacity-80">{{
|
||||
__('Low') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-sm font-bold text-slate-300 dark:text-slate-700/50">-</span>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-sm font-bold text-slate-300 dark:text-slate-700/50">-</span>
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($inventory_warehouses) + 2 }}" class="px-6 py-20 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<div
|
||||
class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}</p>
|
||||
<p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -124,5 +161,112 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{ $products->links('vendor.pagination.luxury') }}</div>
|
||||
|
||||
{{-- Mobile Card View --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:hidden gap-6 mt-8 items-start">
|
||||
@forelse($products as $product)
|
||||
@php
|
||||
$viewableWarehouseIds = $inventory_warehouses->pluck('id')->toArray();
|
||||
$totalQuantity = $product->stocks->whereIn('warehouse_id', $viewableWarehouseIds)->sum('quantity');
|
||||
@endphp
|
||||
<div x-data="{ localCardOpen: false }"
|
||||
class="luxury-card p-6 rounded-[2rem] border border-slate-100 dark:border-slate-800 bg-white/50 dark:bg-slate-900/50 transition-all duration-300 group">
|
||||
|
||||
{{-- Card Header --}}
|
||||
<div class="flex items-center justify-between gap-4 cursor-pointer" @click.stop="localCardOpen = !localCardOpen">
|
||||
<div class="flex items-center gap-4 min-w-0">
|
||||
<div
|
||||
class="w-14 h-14 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center overflow-hidden border border-slate-200 dark:border-slate-700 group-hover:bg-cyan-500 transition-all duration-300 shadow-sm shrink-0">
|
||||
@if($product->image_url)
|
||||
<img src="{{ $product->image_url }}"
|
||||
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
|
||||
@else
|
||||
<svg class="w-7 h-7 text-slate-400 group-hover:text-white transition-colors" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
|
||||
</svg>
|
||||
@endif
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h3
|
||||
class="text-lg font-black text-slate-800 dark:text-slate-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors tracking-tight">
|
||||
{{ $product->localized_name }}
|
||||
</h3>
|
||||
@if($product->barcode)
|
||||
<p
|
||||
class="text-[10px] font-mono font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest truncate mt-0.5">
|
||||
{{ $product->barcode }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-1 shrink-0">
|
||||
<span class="text-xl font-mono font-black text-cyan-600 dark:text-cyan-400">
|
||||
{{ $totalQuantity }}
|
||||
</span>
|
||||
<svg class="w-4 h-4 text-slate-400 transition-transform duration-300"
|
||||
:class="{ 'rotate-180': localCardOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Collapsible Content --}}
|
||||
<div x-show="localCardOpen" x-collapse x-cloak
|
||||
class="mt-6 pt-6 border-t border-slate-100 dark:border-slate-800/50 space-y-2">
|
||||
@foreach($inventory_warehouses->sortBy('name', SORT_NATURAL) as $w)
|
||||
@php
|
||||
$stock = $product->stocks->firstWhere('warehouse_id', $w->id);
|
||||
$isLow = $stock && $stock->safety_stock > 0 && $stock->quantity <= $stock->safety_stock;
|
||||
@endphp
|
||||
<div
|
||||
class="flex items-center justify-between p-4 rounded-2xl bg-slate-50/50 dark:bg-white/[0.02] border border-transparent hover:border-slate-200 dark:hover:border-slate-700 transition-all">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-base font-black text-slate-800 dark:text-slate-100">{{ $w->name }}</span>
|
||||
<span
|
||||
class="text-[10px] font-bold text-slate-400 bg-slate-100 dark:bg-slate-800/50 px-2 py-0.5 rounded-lg uppercase tracking-tight">
|
||||
{{ $w->type === 'main' ? __('Main') : __('Branch') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@if($stock && $stock->quantity > 0)
|
||||
<span
|
||||
class="text-base font-mono font-black {{ $isLow ? 'text-rose-500' : 'text-slate-800 dark:text-slate-100' }}">
|
||||
{{ $stock->quantity }}
|
||||
</span>
|
||||
@if($isLow)
|
||||
<div class="px-2 py-0.5 rounded-full bg-rose-500/10 border border-rose-500/20">
|
||||
<span
|
||||
class="text-[10px] font-black text-rose-600 dark:text-rose-400 uppercase tracking-tighter">{{
|
||||
__('Low') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-sm font-bold text-slate-300 dark:text-slate-700/50">-</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-6 py-20 text-center luxury-card rounded-[2rem]">
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4">
|
||||
<svg class="w-8 h-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold tracking-widest uppercase">{{ __('No products found') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-6 py-6 border-t border-slate-50 dark:border-slate-800/50">{{
|
||||
$products->links('vendor.pagination.luxury') }}</div>
|
||||
</div>
|
||||
@ -151,7 +151,7 @@
|
||||
|
||||
const oldSelect = wrapper.querySelector('select');
|
||||
if (oldSelect && window.HSSelect?.getInstance(oldSelect)) {
|
||||
try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) {}
|
||||
try { window.HSSelect.getInstance(oldSelect).destroy(); } catch (e) { }
|
||||
}
|
||||
wrapper.innerHTML = '';
|
||||
|
||||
@ -279,10 +279,10 @@
|
||||
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
if (!this.items[i].product_id) {
|
||||
window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', i+1), 'error'); return;
|
||||
window.showToast?.('{{ __("Please select product for item :num") }}'.replace(':num', i + 1), 'error'); return;
|
||||
}
|
||||
if (!this.items[i].quantity || this.items[i].quantity < 1) {
|
||||
window.showToast?.('{{ __("Please enter valid quantity for item :num") }}'.replace(':num', i+1), 'error'); return;
|
||||
window.showToast?.('{{ __("Please enter valid quantity for item :num") }}'.replace(':num', i + 1), 'error'); return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,15 +483,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4 pb-10" x-data="replenishmentManager()"
|
||||
@open-details.window="openOrderDetails($event.detail.id)"
|
||||
<div class="space-y-4 pb-10" x-data="replenishmentManager()" @open-details.window="openOrderDetails($event.detail.id)"
|
||||
@ajax:navigate:replenishments.window.prevent="fetchTabData($event.detail.url)">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl sm: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>
|
||||
<h1 class="text-2xl sm: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>
|
||||
<div class="flex items-center gap-3">
|
||||
<button type="button" @click="
|
||||
@ -510,11 +511,16 @@
|
||||
});
|
||||
});
|
||||
" class="btn-luxury-ghost border-cyan-500/30 text-cyan-600 hover:bg-cyan-50 dark:hover:bg-cyan-500/10">
|
||||
<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="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /></svg>
|
||||
<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="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
|
||||
</svg>
|
||||
<span>{{ __('Auto Replenishment') }}</span>
|
||||
</button>
|
||||
<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>
|
||||
<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>
|
||||
@ -522,29 +528,52 @@
|
||||
|
||||
{{-- Filters --}}
|
||||
<div class="luxury-card rounded-3xl p-8 animate-luxury-in">
|
||||
<form id="replenishment-filter-form" action="{{ route('admin.warehouses.replenishments') }}" method="GET" class="flex flex-wrap items-center gap-4 mb-8">
|
||||
<div class="relative group flex-1 sm:flex-none sm:min-w-[240px]">
|
||||
<form id="replenishment-filter-form" action="{{ route('admin.warehouses.replenishments') }}" method="GET"
|
||||
class="flex flex-wrap items-center gap-4 mb-8">
|
||||
<div class="relative group flex-1 sm:flex-none sm:w-80">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="text" name="search_order" value="{{ request('search_order') }}" @keydown.enter.prevent="fetchTabData()" class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
|
||||
<input type="text" name="search_order" value="{{ request('search_order') }}"
|
||||
@keydown.enter.prevent="fetchTabData()" class="py-2.5 pl-12 pr-6 block w-full luxury-input"
|
||||
placeholder="{{ __('Search order number...') }}">
|
||||
</div>
|
||||
<div class="flex-1 sm:flex-none sm:min-w-[180px]">
|
||||
<x-searchable-select name="status" :selected="request('status')" :placeholder="__('All Statuses')">
|
||||
<option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option>
|
||||
<option value="pending" {{ request('status') === 'pending' ? 'selected' : '' }} data-title="{{ __('Pending') }}">{{ __('Pending') }}</option>
|
||||
<option value="prepared" {{ request('status') === 'prepared' ? 'selected' : '' }} data-title="{{ __('Prepared') }}">{{ __('Prepared') }}</option>
|
||||
<option value="delivering" {{ request('status') === 'delivering' ? 'selected' : '' }} data-title="{{ __('Delivering') }}">{{ __('Delivering') }}</option>
|
||||
<option value="completed" {{ request('status') === 'completed' ? 'selected' : '' }} data-title="{{ __('Completed') }}">{{ __('Completed') }}</option>
|
||||
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }} data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
|
||||
<option value="pending" {{ request('status')==='pending' ? 'selected' : '' }}
|
||||
data-title="{{ __('Pending') }}">{{ __('Pending') }}</option>
|
||||
<option value="prepared" {{ request('status')==='prepared' ? 'selected' : '' }}
|
||||
data-title="{{ __('Prepared') }}">{{ __('Prepared') }}</option>
|
||||
<option value="delivering" {{ request('status')==='delivering' ? 'selected' : '' }}
|
||||
data-title="{{ __('Delivering') }}">{{ __('Delivering') }}</option>
|
||||
<option value="completed" {{ request('status')==='completed' ? 'selected' : '' }}
|
||||
data-title="{{ __('Completed') }}">{{ __('Completed') }}</option>
|
||||
<option value="cancelled" {{ request('status')==='cancelled' ? 'selected' : '' }}
|
||||
data-title="{{ __('Cancelled') }}">{{ __('Cancelled') }}</option>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-none ml-auto sm:ml-0">
|
||||
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
<button type="button" @click="fetchTabData()"
|
||||
class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95"
|
||||
title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => { s.value = ' '; const inst = window.HSSelect?.getInstance(s); if(inst) inst.setValue(' '); }); fetchTabData();" class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||
<button type="button"
|
||||
@click="$el.closest('form').querySelectorAll('input[type=text]').forEach(i => i.value = ''); $el.closest('form').querySelectorAll('select').forEach(s => { s.value = ' '; const inst = window.HSSelect?.getInstance(s); if(inst) inst.setValue(' '); }); fetchTabData();"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95"
|
||||
title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -318,9 +318,9 @@
|
||||
@ajax:navigate:transfers.window.prevent="fetchTabData($event.detail.url)">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Transfer Orders') }}</h1>
|
||||
<h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Transfer Orders') }}</h1>
|
||||
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">{{ __('Manage stock transfers between warehouses and machine returns') }}</p>
|
||||
</div>
|
||||
<button type="button" @click="openCreateModal()" class="btn-luxury-primary">
|
||||
@ -350,10 +350,9 @@
|
||||
<p class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-[0.4em] animate-pulse">{{ __('Loading Data') }}...</p>
|
||||
</div>
|
||||
|
||||
<form id="transfer-filter-form" action="{{ route('admin.warehouses.transfers') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 mb-8">
|
||||
<input type="hidden" name="per_page" value="{{ request('per_page', 15) }}">
|
||||
<form id="transfer-filter-form" action="{{ route('admin.warehouses.transfers') }}" method="GET" class="flex flex-wrap items-center gap-4 mb-8">
|
||||
|
||||
<div class="relative group flex-1 md:max-w-[240px]">
|
||||
<div class="relative group flex-1 sm:flex-none sm:w-80">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none z-10">
|
||||
<svg class="h-4 w-4 text-slate-400 group-focus-within:text-cyan-500 transition-colors"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
@ -363,27 +362,29 @@
|
||||
</span>
|
||||
<input type="text" name="search_order" value="{{ request('search_order') }}"
|
||||
@keydown.enter.prevent="$dispatch('ajax:filter')"
|
||||
class="py-2.5 pl-12 pr-6 block w-full luxury-input" placeholder="{{ __('Search order number...') }}">
|
||||
class="py-2.5 pl-12 pr-6 block w-full luxury-input text-sm font-bold" placeholder="{{ __('Search order number...') }}">
|
||||
</div>
|
||||
|
||||
<div class="flex-1 md:max-w-[220px]">
|
||||
<div class="flex-1 sm:flex-none sm:min-w-[220px]">
|
||||
<x-searchable-select
|
||||
name="type"
|
||||
:selected="request('type')"
|
||||
:placeholder="__('All Types')"
|
||||
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
|
||||
class="luxury-select-sm"
|
||||
>
|
||||
<option value="" data-title="{{ __('All Types') }}">{{ __('All Types') }}</option>
|
||||
<option value="warehouse_to_warehouse" {{ request('type') === 'warehouse_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Warehouse to Warehouse') }}">{{ __('Warehouse to Warehouse') }}</option>
|
||||
<option value="machine_to_warehouse" {{ request('type') === 'machine_to_warehouse' ? 'selected' : '' }} data-title="{{ __('Machine to Warehouse') }}">{{ __('Machine to Warehouse') }}</option>
|
||||
</x-searchable-select>
|
||||
</div>
|
||||
<div class="flex-1 md:max-w-[180px]">
|
||||
<div class="flex-1 sm:flex-none sm:min-w-[180px]">
|
||||
<x-searchable-select
|
||||
name="status"
|
||||
:selected="request('status')"
|
||||
:placeholder="__('All Statuses')"
|
||||
onchange="this.dispatchEvent(new CustomEvent('ajax:filter', { bubbles: true }))"
|
||||
class="luxury-select-sm"
|
||||
>
|
||||
<option value="" data-title="{{ __('All Statuses') }}">{{ __('All Statuses') }}</option>
|
||||
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }} data-title="{{ __('Draft') }}">{{ __('Draft') }}</option>
|
||||
@ -393,7 +394,7 @@
|
||||
</div>
|
||||
|
||||
{{-- Date Range --}}
|
||||
<div class="flex-1 md:max-w-[320px]">
|
||||
<div class="flex-1 sm:flex-none sm:min-w-[320px]">
|
||||
<div class="relative group"
|
||||
x-data="{ fp: null }"
|
||||
x-init="fp = flatpickr($refs.dateRange, {
|
||||
@ -422,29 +423,33 @@
|
||||
<input type="hidden" name="end_date" x-ref="endDate" value="{{ request('end_date') }}">
|
||||
<input type="text" x-ref="dateRange"
|
||||
value="{{ request('start_date') && request('end_date') ? request('start_date') . ' 至 ' . request('end_date') : (request('start_date') ?: '') }}"
|
||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input py-2.5 pl-12 pr-6 block w-full cursor-pointer">
|
||||
placeholder="{{ __('Select Date Range') }}" class="luxury-input luxury-input-sm py-2.5 pl-12 pr-6 block w-full cursor-pointer font-bold">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group" title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 flex-none ml-auto sm:ml-0">
|
||||
<button type="button" @click="fetchTabData()" class="p-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 shadow-lg shadow-cyan-500/25 group transition-all active:scale-95" title="{{ __('Search') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@click="
|
||||
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=per_page])').forEach(i => i.value = '');
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
fetchTabData();
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group" title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
<button type="button"
|
||||
@click="
|
||||
$el.closest('form').querySelectorAll('input[type=text], input[type=hidden]:not([name=per_page])').forEach(i => i.value = '');
|
||||
$el.closest('form').querySelectorAll('select').forEach(s => {
|
||||
s.value = ' ';
|
||||
const instance = window.HSSelect.getInstance(s);
|
||||
if (instance) instance.setValue(' ');
|
||||
});
|
||||
fetchTabData();
|
||||
"
|
||||
class="p-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-500 hover:bg-slate-200 dark:hover:bg-slate-700 group transition-all active:scale-95" title="{{ __('Reset') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@ -116,10 +116,21 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name(
|
||||
// 6. 銷售管理
|
||||
Route::prefix('sales')->name('sales.')->group(function () {
|
||||
Route::get('/', [App\Http\Controllers\Admin\SalesController::class, 'index'])->name('index');
|
||||
|
||||
// 取貨碼
|
||||
Route::get('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'pickupCodes'])->name('pickup-codes');
|
||||
Route::post('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'storePickupCode'])->name('pickup-codes.store');
|
||||
Route::patch('/pickup-codes/{pickupCode}', [App\Http\Controllers\Admin\SalesController::class, 'updatePickupCode'])->name('pickup-codes.update');
|
||||
Route::delete('/pickup-codes/{pickupCode}', [App\Http\Controllers\Admin\SalesController::class, 'destroyPickupCode'])->name('pickup-codes.destroy');
|
||||
|
||||
// 通行碼
|
||||
Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'passCodes'])->name('pass-codes');
|
||||
Route::post('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'storePassCode'])->name('pass-codes.store');
|
||||
Route::patch('/pass-codes/{passCode}', [App\Http\Controllers\Admin\SalesController::class, 'updatePassCode'])->name('pass-codes.update');
|
||||
Route::delete('/pass-codes/{passCode}', [App\Http\Controllers\Admin\SalesController::class, 'destroyPassCode'])->name('pass-codes.destroy');
|
||||
|
||||
Route::get('/orders', [App\Http\Controllers\Admin\SalesController::class, 'orders'])->name('orders');
|
||||
Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class, 'promotions'])->name('promotions');
|
||||
Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'passCodes'])->name('pass-codes');
|
||||
Route::get('/store-gifts', [App\Http\Controllers\Admin\SalesController::class, 'storeGifts'])->name('store-gifts');
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user