From 647aa5118799c0811947e1ea58ffe19999a483ee Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Jun 2025 18:29:32 +0700 Subject: [PATCH] partial update stock opname feature --- .../Controllers/TransactionController.php | 12 +- .../WarehouseManagement/OpnamesController.php | 282 ++++++-- dev-restart.sh | 100 +++ resources/views/transaction/index.blade.php | 607 +++++++++++++++++- 4 files changed, 926 insertions(+), 75 deletions(-) create mode 100755 dev-restart.sh diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index ab654b9..b14ae08 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -4,6 +4,8 @@ namespace App\Http\Controllers; use App\Models\Category; use App\Models\Dealer; +use App\Models\Product; +use App\Models\Stock; use App\Models\Transaction; use App\Models\User; use App\Models\Work; @@ -26,7 +28,15 @@ class TransactionController extends Controller ->select('d.name as dealer_name', 'd.id as dealer_id', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address') ->where('users.id', Auth::user()->id)->first(); $now = Carbon::now()->translatedFormat('d F Y'); - return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic')); + + // Get products with stock based on user role + $products = Product::with(['stocks' => function($query) { + $query->where('dealer_id', Auth::user()->dealer_id); + }, 'stocks.dealer']) + ->where('active', true) + ->get(); + + return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products')); } public function workcategory($category_id) diff --git a/app/Http/Controllers/WarehouseManagement/OpnamesController.php b/app/Http/Controllers/WarehouseManagement/OpnamesController.php index b4d1be6..b86d041 100644 --- a/app/Http/Controllers/WarehouseManagement/OpnamesController.php +++ b/app/Http/Controllers/WarehouseManagement/OpnamesController.php @@ -9,10 +9,12 @@ use App\Models\Opname; use App\Models\OpnameDetail; use App\Models\Product; use App\Models\Stock; +use App\Models\User; use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Illuminate\Validation\Rule; use Yajra\DataTables\Facades\DataTables; class OpnamesController extends Controller @@ -89,34 +91,117 @@ class OpnamesController extends Controller try { DB::beginTransaction(); - // 1. Validasi input - $validated = $request->validate([ - 'dealer' => 'required|exists:dealers,id', - 'product' => 'required|array|min:1', - 'product.*' => 'required|exists:products,id', - 'system_quantity' => 'required|array', - 'system_quantity.*' => 'required|numeric|min:0', - 'physical_quantity' => 'required|array', - 'physical_quantity.*' => 'required|numeric|min:0', - 'note' => 'nullable|string|max:1000', // note utama - 'item_notes' => 'nullable|array', // notes per item - 'item_notes.*' => 'required_if:physical_quantity.*,!=,system_quantity.*|nullable|string|max:255' - ]); + // Check if this is from transaction form or regular opname form + $isTransactionForm = $request->has('form') && $request->form === 'opname'; + + if ($isTransactionForm) { + // Custom validation for transaction form + $request->validate([ + 'dealer_id' => 'required|exists:dealers,id', + 'user_id' => 'required|exists:users,id', + 'opname_date' => [ + 'nullable', + 'string', + 'date_format:Y-m-d', + 'before_or_equal:today' + ], + 'description' => 'nullable|string|max:1000', + 'product_id' => 'required|array|min:1', + 'product_id.*' => 'required|exists:products,id', + 'system_stock' => 'required|array', + 'system_stock.*' => 'required|numeric|min:0', + 'physical_stock' => 'required|array', + 'physical_stock.*' => 'required|numeric|min:0' + ]); + + // Process transaction form data with proper date parsing + $dealerId = $request->dealer_id; + $userId = $request->user_id; + + // Parse opname date (YYYY-MM-DD format) or use today if empty + $inputDate = $request->opname_date ?: now()->format('Y-m-d'); + Log::info('Parsing opname date', ['input' => $request->opname_date, 'using' => $inputDate]); + $opnameDate = Carbon::createFromFormat('Y-m-d', $inputDate); + Log::info('Successfully parsed opname date', ['parsed' => $opnameDate->format('Y-m-d')]); + + $note = $request->description; + $productIds = $request->product_id; + $systemStocks = $request->system_stock; + $physicalStocks = $request->physical_stock; + + // Log input data untuk debugging + Log::info('Transaction form input data', [ + 'product_ids' => $productIds, + 'system_stocks' => $systemStocks, + 'physical_stocks' => $physicalStocks, + 'dealer_id' => $dealerId, + 'user_id' => $userId + ]); + + } else { + // Original validation for regular opname form + $request->validate([ + 'dealer' => 'required|exists:dealers,id', + 'product' => 'required|array|min:1', + 'product.*' => 'required|exists:products,id', + 'system_quantity' => 'required|array', + 'system_quantity.*' => 'required|numeric|min:0', + 'physical_quantity' => 'required|array', + 'physical_quantity.*' => 'required|numeric|min:0', + 'note' => 'nullable|string|max:1000', + 'item_notes' => 'nullable|array', + 'item_notes.*' => 'nullable|string|max:255', + 'opname_date' => 'nullable|date' // Add opname_date validation for regular form + ]); + + // Process regular form data + $dealerId = $request->dealer; + $userId = auth()->id(); + + // Use provided date or current date + $inputDate = $request->opname_date ?: now()->format('Y-m-d'); + $opnameDate = $request->opname_date ? + Carbon::createFromFormat('Y-m-d', $inputDate) : + now(); + + $note = $request->note; + $productIds = $request->product; + $systemStocks = $request->system_quantity; + $physicalStocks = $request->physical_quantity; + } - // 2. Validasi duplikasi produk - $productCounts = array_count_values(array_filter($request->product)); + // 2. Validasi minimal ada produk yang diisi (termasuk nilai 0) + $validProductIds = array_filter($productIds); + $validSystemStocks = array_filter($systemStocks, function($value) { return $value !== null && $value !== ''; }); + $validPhysicalStocks = array_filter($physicalStocks, function($value) { + return $value !== null && $value !== '' && is_numeric($value); + }); + + if (empty($validProductIds) || count($validProductIds) === 0) { + throw new \Exception('Minimal harus ada satu produk yang diisi untuk opname.'); + } + + if (count($validPhysicalStocks) === 0) { + throw new \Exception('Minimal harus ada satu stock fisik yang diisi (termasuk nilai 0).'); + } + + // 3. Validasi duplikasi produk + $productCounts = array_count_values($validProductIds); foreach ($productCounts as $productId => $count) { if ($count > 1) { - throw new \Exception('Product tidak boleh duplikat.'); + throw new \Exception('Produk tidak boleh duplikat dalam satu opname.'); } } - // 3. Validasi dealer - $dealer = Dealer::findOrFail($request->dealer); + // 4. Validasi dealer + $dealer = Dealer::findOrFail($dealerId); - // 4. Validasi produk aktif - $productIds = array_filter($request->product); - $inactiveProducts = Product::whereIn('id', $productIds) + // 5. Validasi user exists + $user = User::findOrFail($userId); + + // 6. Validasi produk aktif + $filteredProductIds = array_filter($productIds); + $inactiveProducts = Product::whereIn('id', $filteredProductIds) ->where('active', false) ->pluck('name') ->toArray(); @@ -125,48 +210,72 @@ class OpnamesController extends Controller throw new \Exception('Produk berikut tidak aktif: ' . implode(', ', $inactiveProducts)); } - // 5. Validasi stock dan note + // 7. Validasi stock difference (for transaction form, we'll allow any difference without note requirement) $stockDifferences = []; - foreach ($request->product as $index => $productId) { - if (!$productId) continue; + if (!$isTransactionForm) { + // Only validate notes for regular opname form + foreach ($productIds as $index => $productId) { + if (!$productId) continue; - $systemStock = floatval($request->system_quantity[$index] ?? 0); - $physicalStock = floatval($request->physical_quantity[$index] ?? 0); - $itemNote = $request->input("item_notes.{$index}"); + $systemStock = floatval($systemStocks[$index] ?? 0); + $physicalStock = floatval($physicalStocks[$index] ?? 0); + $itemNote = $request->input("item_notes.{$index}"); - // Jika ada perbedaan stock dan note kosong - if (abs($systemStock - $physicalStock) > 0.01 && empty($itemNote)) { - $product = Product::find($productId); - $stockDifferences[] = $product->name; + // Jika ada perbedaan stock dan note kosong + if (abs($systemStock - $physicalStock) > 0.01 && empty($itemNote)) { + $product = Product::find($productId); + $stockDifferences[] = $product->name; + } + } + + if (!empty($stockDifferences)) { + throw new \Exception( + 'Catatan harus diisi untuk produk berikut karena ada perbedaan stock: ' . + implode(', ', $stockDifferences) + ); } } - if (!empty($stockDifferences)) { - throw new \Exception( - 'Catatan harus diisi untuk produk berikut karena ada perbedaan stock: ' . - implode(', ', $stockDifferences) - ); - } - - // 6. Create Opname master record with approved status + // 8. Create Opname master record with approved status $opname = Opname::create([ - 'dealer_id' => $request->dealer, - 'opname_date' => now(), - 'user_id' => auth()->id(), - 'note' => $request->note, + 'dealer_id' => $dealerId, + 'opname_date' => $opnameDate, + 'user_id' => $userId, + 'note' => $note, 'status' => 'approved', // Set status langsung approved - 'approved_by' => auth()->id(), // Set current user sebagai approver + 'approved_by' => $userId, // Set current user sebagai approver 'approved_at' => now() // Set waktu approval ]); - // 7. Create OpnameDetails and update stock + // 9. Create OpnameDetails and update stock - only for valid entries $details = []; - foreach ($request->product as $index => $productId) { + $processedCount = 0; + + foreach ($productIds as $index => $productId) { if (!$productId) continue; + + // Skip only if physical stock is truly not provided (empty string or null) + // Accept 0 as valid input + if (!isset($physicalStocks[$index]) || $physicalStocks[$index] === '' || $physicalStocks[$index] === null) { + continue; + } + + // Validate that physical stock is numeric (including 0) + if (!is_numeric($physicalStocks[$index])) { + continue; + } - $systemStock = floatval($request->system_quantity[$index] ?? 0); - $physicalStock = floatval($request->physical_quantity[$index] ?? 0); + $systemStock = floatval($systemStocks[$index] ?? 0); + $physicalStock = floatval($physicalStocks[$index]); $difference = $physicalStock - $systemStock; + + $processedCount++; + + // Get item note (only for regular opname form) + $itemNote = null; + if (!$isTransactionForm) { + $itemNote = $request->input("item_notes.{$index}"); + } // Create opname detail $details[] = [ @@ -175,7 +284,7 @@ class OpnamesController extends Controller 'system_stock' => $systemStock, 'physical_stock' => $physicalStock, 'difference' => $difference, - 'note' => $request->input("item_notes.{$index}"), + 'note' => $itemNote, 'created_at' => now(), 'updated_at' => now() ]; @@ -184,7 +293,7 @@ class OpnamesController extends Controller $stock = Stock::firstOrCreate( [ 'product_id' => $productId, - 'dealer_id' => $request->dealer + 'dealer_id' => $dealerId ], ['quantity' => 0] ); @@ -197,35 +306,84 @@ class OpnamesController extends Controller ); } + // Validate we have at least one detail to insert + if (empty($details)) { + throw new \Exception('Tidak ada data stock fisik yang valid untuk diproses.'); + } + // Bulk insert untuk performa lebih baik OpnameDetail::insert($details); - // 8. Log aktivitas + // 10. Log aktivitas dengan detail produk yang diproses + $processedProducts = collect($details)->map(function($detail) { + return [ + 'product_id' => $detail['product_id'], + 'system_stock' => $detail['system_stock'], + 'physical_stock' => $detail['physical_stock'], + 'difference' => $detail['difference'] + ]; + }); + Log::info('Opname created and auto-approved', [ 'opname_id' => $opname->id, 'dealer_id' => $opname->dealer_id, - 'user_id' => auth()->id(), - 'approver_id' => auth()->id(), - 'product_count' => count($details) + 'user_id' => $userId, + 'approver_id' => $userId, + 'product_count' => count($details), + 'processed_count' => $processedCount, + 'form_type' => $isTransactionForm ? 'transaction' : 'regular', + 'opname_date' => $opnameDate->format('Y-m-d'), + 'processed_products' => $processedProducts->toArray() ]); DB::commit(); - return redirect() - ->route('opnames.index') - ->with('success', 'Opname berhasil disimpan dan disetujui.'); + if ($isTransactionForm) { + // Redirect back to transaction page with success message + return redirect() + ->route('transaction') + ->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses."); + } else { + // Redirect to opname index for regular form + return redirect() + ->route('opnames.index') + ->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses."); + } } catch (\Illuminate\Validation\ValidationException $e) { DB::rollBack(); - return back()->withErrors($e->validator)->withInput(); + Log::error('Validation error in OpnamesController@store', [ + 'errors' => $e->errors(), + 'input' => $request->all() + ]); + + if ($isTransactionForm) { + return redirect() + ->route('transaction') + ->withErrors($e->validator) + ->withInput() + ->with('error', 'Terjadi kesalahan validasi. Periksa kembali data yang dimasukkan.'); + } else { + return back()->withErrors($e->validator)->withInput(); + } } catch (\Exception $e) { DB::rollBack(); Log::error('Error in OpnamesController@store: ' . $e->getMessage()); Log::error($e->getTraceAsString()); + Log::error('Request data:', $request->all()); - return back() - ->with('error', $e->getMessage()) - ->withInput(); + $errorMessage = $e->getMessage(); + + if ($isTransactionForm) { + return redirect() + ->route('transaction') + ->with('error', $errorMessage) + ->withInput(); + } else { + return back() + ->with('error', $errorMessage) + ->withInput(); + } } } @@ -289,4 +447,6 @@ class OpnamesController extends Controller return response()->json(['error' => 'Terjadi kesalahan saat mengambil data stok'], 500); } } + + } diff --git a/dev-restart.sh b/dev-restart.sh new file mode 100755 index 0000000..fe75b0c --- /dev/null +++ b/dev-restart.sh @@ -0,0 +1,100 @@ +#!/bin/bash + +# Development Restart Script +# Usage: ./dev-restart.sh [cache|config|routes|all|container] + +set -e + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +ACTION=${1:-cache} + +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +case $ACTION in + cache) + print_info "Clearing Laravel cache..." + docker-compose exec app php artisan cache:clear + print_success "Cache cleared!" + ;; + + config) + print_info "Clearing configuration cache..." + docker-compose exec app php artisan config:clear + print_success "Config cache cleared!" + ;; + + routes) + print_info "Clearing route cache..." + docker-compose exec app php artisan route:clear + print_success "Route cache cleared!" + ;; + + all) + print_info "Clearing all Laravel caches..." + docker-compose exec app php artisan optimize:clear + print_success "All caches cleared!" + ;; + + container) + print_info "Restarting app container..." + docker-compose restart app + print_success "Container restarted!" + ;; + + build) + print_info "Rebuilding app container..." + docker-compose up -d --build app + print_success "Container rebuilt!" + ;; + + migrate) + print_info "Running database migrations..." + docker-compose exec app php artisan migrate + print_success "Migrations completed!" + ;; + + composer) + print_info "Installing/updating composer dependencies..." + docker-compose exec app composer install --optimize-autoloader + print_success "Composer dependencies updated!" + ;; + + npm) + print_info "Installing npm dependencies and building assets..." + docker-compose exec app npm install + docker-compose exec app npm run dev + print_success "Frontend assets built!" + ;; + + *) + echo "Development Restart Script" + echo "Usage: $0 [cache|config|routes|all|container|build|migrate|composer|npm]" + echo "" + echo "Options:" + echo " cache - Clear application cache only" + echo " config - Clear configuration cache" + echo " routes - Clear route cache" + echo " all - Clear all Laravel caches" + echo " container - Restart app container" + echo " build - Rebuild app container" + echo " migrate - Run database migrations" + echo " composer - Update composer dependencies" + echo " npm - Update npm and build assets" + exit 1 + ;; +esac \ No newline at end of file diff --git a/resources/views/transaction/index.blade.php b/resources/views/transaction/index.blade.php index 4ba8f93..74b1d06 100644 --- a/resources/views/transaction/index.blade.php +++ b/resources/views/transaction/index.blade.php @@ -1,5 +1,10 @@ @extends('layouts.frontapp') +@php +use App\Models\Dealer; +use Illuminate\Support\Facades\Auth; +@endphp + {{-- @section('contentHead')
@@ -19,6 +24,79 @@ cursor: not-allowed !important; pointer-events: none; } + .table-responsive { + max-height: 400px; + overflow-y: auto; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .text-success { + color: #28a745 !important; + } + .text-danger { + color: #dc3545 !important; + } + .text-bold { + font-weight: bold; + } + .system-stock { + font-weight: 600; + color: #007bff; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #dee2e6; + vertical-align: middle; + padding: 12px 8px; + } + .table thead th { + background-color: #f8f9fa; + color: #495057; + font-weight: 600; + font-size: 14px; + border-bottom: 2px solid #dee2e6; + } + .table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(0,0,0,.02); + } + .physical-stock { + font-weight: 500; + border: 2px solid #e9ecef; + transition: border-color 0.15s ease-in-out; + } + .physical-stock:focus { + border-color: #ffc107; + box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.25); + } + .difference { + font-size: 14px; + padding: 4px 8px; + border-radius: 4px; + background-color: #f8f9fa; + display: inline-block; + min-width: 60px; + } + .btn-lg { + padding: 12px 20px; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.5px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + transition: all 0.3s ease; + } + .btn-lg:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0,0,0,0.15); + } + .physical-stock.is-invalid { + border-color: #dc3545; + background-color: #fff5f5; + } + .physical-stock.is-invalid:focus { + border-color: #dc3545; + box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); + } @endsection @@ -92,22 +170,52 @@
@endif + + @if (session('error')) +
+
+ +
+
+ @endif
-
-
+ +
+ + + +
+ +
@csrf @@ -301,10 +409,12 @@
- - -
-
+ + +
+ + +
@csrf @@ -378,8 +488,180 @@
- - + + +
+
+
+ + +
+ + + +
+ +
+
+ @csrf + + + + +
+ + + @error('opname_date') +
+ {{ $message }} +
+ @enderror +
+ +
+ + + @error('description') +
+ {{ $message }} +
+ @enderror +
+ + +
+ + + + + + + + + + + + {{-- Dealer/Mechanic - Show products for current dealer only --}} + @foreach($products as $product) + @php + $stock = $product->stocks->first(); + $currentStock = $stock ? $stock->quantity : 0; + @endphp + + + + + + + + @endforeach + +
ProdukDealerStock SistemStock FisikSelisih
{{ $product->name }}{{ $mechanic->dealer_name }} + {{ number_format($currentStock, 2) }} + + + + + + @error('physical_stock.'.$loop->index) +
+ {{ $message }} +
+ @enderror +
+ 0.00 +
+
+ +
+ +
+
+
+ + +
+
+ @csrf + + + + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+ + + + + + + + + + + {{-- Dealer/Mechanic - Show products for current dealer only --}} + @foreach($products as $product) + @php + $stock = $product->stocks->first(); + $currentStock = $stock ? $stock->quantity : 0; + @endphp + + + + + + + @endforeach + +
+ + ProdukStock Saat IniJumlah Mutasi
+ + {{ $product->name }}{{ number_format($currentStock, 2) }} + + + +
+
+ +
+ +
+
+
+
@@ -498,8 +780,307 @@ return true; }) - $("#date-work").datepicker() - $("#date-wash").datepicker() + $("#opnameForm").submit(function(e) { + e.preventDefault(); + + // Validate form + var hasValidStock = false; + var invalidRows = []; + + $('.physical-stock').each(function(index) { + var value = $(this).val(); + var row = $(this).closest('tr'); + var productName = row.find('td:first').text().trim(); + + // Check if value is valid (including 0) + if (value !== '' && value !== null && value !== undefined) { + var numValue = parseFloat(value); + if (!isNaN(numValue) && numValue >= 0) { + hasValidStock = true; + // Ensure the value is properly formatted + $(this).val(numValue.toFixed(2)); + } else { + invalidRows.push(productName + ' (nilai tidak valid)'); + } + } + // Don't remove elements here - let them stay for re-editing + }); + + // Show error if no valid stock entries + if (!hasValidStock) { + resetSubmitButton(); + highlightInvalidFields(); + Swal.fire({ + icon: 'warning', + title: 'Peringatan', + text: 'Minimal harus ada satu produk dengan stock fisik yang diisi dengan benar!' + }); + return false; + } + + // Show error if there are invalid entries + if (invalidRows.length > 0) { + resetSubmitButton(); + highlightInvalidFields(); + Swal.fire({ + icon: 'warning', + title: 'Data Tidak Valid', + text: 'Perbaiki data berikut: ' + invalidRows.join(', ') + }); + return false; + } + + // Get opname date or use today as default + var opnameDate = $('#date-opname').val(); + if (!opnameDate) { + // Set default to today if empty + var today = new Date().toISOString().split('T')[0]; + $('#date-opname').val(today); + opnameDate = today; + } + + // Validate date format (YYYY-MM-DD) + var datePattern = /^(\d{4})-(\d{2})-(\d{2})$/; + if (!datePattern.test(opnameDate)) { + resetSubmitButton(); + Swal.fire({ + icon: 'warning', + title: 'Format Tanggal Salah', + text: 'Format tanggal harus YYYY-MM-DD (contoh: 2023-12-25)' + }); + return false; + } + + // Validate if date is valid + var dateParts = opnameDate.match(datePattern); + var year = parseInt(dateParts[1], 10); + var month = parseInt(dateParts[2], 10); + var day = parseInt(dateParts[3], 10); + + var testDate = new Date(year, month - 1, day); + if (testDate.getDate() !== day || testDate.getMonth() !== (month - 1) || testDate.getFullYear() !== year) { + resetSubmitButton(); + Swal.fire({ + icon: 'warning', + title: 'Tanggal Tidak Valid', + text: 'Tanggal yang dimasukkan tidak valid!' + }); + return false; + } + + // Check if date is not in the future + var today = new Date(); + today.setHours(23, 59, 59, 999); // Set to end of today + if (testDate > today) { + resetSubmitButton(); + Swal.fire({ + icon: 'warning', + title: 'Tanggal Tidak Valid', + text: 'Tanggal opname tidak boleh lebih dari hari ini!' + }); + return false; + } + + $(".button-save").attr("disabled", true); + $(".button-save").addClass("disabled"); + $(".button-save").html(' Menyimpan...'); + + // Date format is already YYYY-MM-DD, no conversion needed + + // Clean up empty rows before submit (remove hidden inputs for truly empty fields only) + $('.physical-stock').each(function() { + var value = $(this).val(); + var row = $(this).closest('tr'); + + // Only remove hidden inputs if physical stock is truly empty (not 0) + // Keep 0 values as they are valid input + if (value === '' || value === null || value === undefined) { + row.find('input[name="product_id[]"]').remove(); + row.find('input[name="dealer_id_stock[]"]').remove(); + row.find('input[name="system_stock[]"]').remove(); + // But keep the physical_stock input so it can be re-edited if form fails + } + }); + + // Submit form + this.submit(); + }) + + $("#mutasiForm").submit(function(e) { + $(".button-save").attr("disabled"); + $(".button-save").addClass("disabled"); + return true; + }) + + $("#date-work").datepicker({ + format: 'yyyy-mm-dd', + autoclose: true, + todayHighlight: true + }) + $("#date-wash").datepicker({ + format: 'yyyy-mm-dd', + autoclose: true, + todayHighlight: true + }) + $("#date-opname").datepicker({ + format: 'yyyy-mm-dd', + autoclose: true, + todayHighlight: true, + startDate: '-30d', + endDate: '+0d' + }) + $("#date-mutasi").datepicker({ + format: 'yyyy-mm-dd', + autoclose: true, + todayHighlight: true + }) + + // Calculate difference for opname + $(document).on('input change keyup', '.physical-stock', function() { + var systemStock = parseFloat($(this).data('system')) || 0; + var inputValue = $(this).val(); + + // Handle empty input - set to 0 for calculation + var physicalStock = 0; + if (inputValue !== '' && inputValue !== null && inputValue !== undefined) { + physicalStock = parseFloat(inputValue) || 0; + } + + var difference = physicalStock - systemStock; + + var differenceCell = $(this).closest('tr').find('.difference'); + differenceCell.text(difference.toFixed(2)); + + // Add color coding for difference + if (difference > 0) { + differenceCell.removeClass('text-danger').addClass('text-success'); + } else if (difference < 0) { + differenceCell.removeClass('text-success').addClass('text-danger'); + } else { + differenceCell.removeClass('text-success text-danger'); + } + + // Update product counter + updateProductCounter(); + }); + + // Function to update product counter + function updateProductCounter() { + var filledProducts = 0; + var totalProducts = $('.physical-stock').length; + + $('.physical-stock').each(function() { + var value = $(this).val(); + // Count as filled if it's a valid number (including 0) + if (value !== '' && value !== null && value !== undefined && !isNaN(parseFloat(value)) && parseFloat(value) >= 0) { + filledProducts++; + } + }); + + // Update button text to show progress + var buttonText = 'Simpan Opname'; + if (filledProducts > 0) { + buttonText += ` (${filledProducts}/${totalProducts} produk)`; + } + + if (!$('.button-save').hasClass('disabled')) { + $('.button-save').html(buttonText); + } + } + + // Handle when input loses focus - don't auto-fill, let user decide + $(document).on('blur', '.physical-stock', function() { + // Trigger calculation even for empty values + $(this).trigger('input'); + }); + + // Function to reset button state if validation fails + function resetSubmitButton() { + $(".button-save").attr("disabled", false); + $(".button-save").removeClass("disabled"); + updateProductCounter(); // Update with current counter + } + + // Function to show field error highlighting + function highlightInvalidFields() { + $('.physical-stock').each(function() { + var value = $(this).val(); + var $input = $(this); + + if (value !== '' && value !== null && value !== undefined) { + var numValue = parseFloat(value); + if (isNaN(numValue) || numValue < 0) { + $input.addClass('is-invalid'); + } else { + $input.removeClass('is-invalid'); + } + } else { + $input.removeClass('is-invalid'); + } + }); + } + + // Remove error styling when user starts typing + $(document).on('input', '.physical-stock', function() { + $(this).removeClass('is-invalid'); + }); + + // Handle server-side errors - scroll to first error and highlight + $(document).ready(function() { + // Set default date for opname if empty + if ($('#date-opname').val() === '') { + var today = new Date().toISOString().split('T')[0]; + $('#date-opname').val(today); + } + + // Check if there are validation errors + if ($('.is-invalid').length > 0) { + // Scroll to first error + $('html, body').animate({ + scrollTop: $('.is-invalid:first').offset().top - 100 + }, 500); + + // Show alert for validation errors + @if(session('error')) + Swal.fire({ + icon: 'error', + title: 'Terjadi Kesalahan', + text: '{{ session("error") }}', + confirmButtonText: 'OK' + }); + @endif + } + + // Auto-dismiss alerts after 5 seconds + setTimeout(function() { + $('.alert').fadeOut('slow'); + }, 5000); + }); + + // Handle opname date field - set default if becomes empty + $('#date-opname').on('blur', function() { + if ($(this).val() === '') { + var today = new Date().toISOString().split('T')[0]; + $(this).val(today); + } + }); + + // Handle mutasi product selection + $(document).on('change', '.mutasi-product-checkbox', function() { + var quantityInput = $(this).closest('tr').find('.mutasi-quantity'); + if ($(this).is(':checked')) { + quantityInput.prop('disabled', false); + } else { + quantityInput.prop('disabled', true).val(''); + } + }); + + // Select all mutasi products + $('#select-all-mutasi').change(function() { + $('.mutasi-product-checkbox').prop('checked', this.checked).trigger('change'); + }); + + function createTransaction(form) { let work_ids;