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')