diff --git a/app/Console/Commands/CleanMutationsData.php b/app/Console/Commands/CleanMutationsData.php new file mode 100644 index 0000000..b6e71f2 --- /dev/null +++ b/app/Console/Commands/CleanMutationsData.php @@ -0,0 +1,97 @@ +option('force')) { + if (!$this->confirm('This will delete ALL mutations data. Are you sure?')) { + $this->info('Operation cancelled.'); + return 0; + } + } + + try { + DB::beginTransaction(); + + // Delete mutations data in proper order (foreign key constraints) + $this->info('Cleaning mutations data...'); + + // 1. Delete stock logs related to mutations + if (Schema::hasTable('stock_logs')) { + $deleted = DB::table('stock_logs') + ->where('source_type', 'App\\Models\\Mutation') + ->delete(); + $this->info("Deleted {$deleted} stock logs related to mutations"); + } + + // 2. Delete mutation details + if (Schema::hasTable('mutation_details')) { + $deleted = DB::table('mutation_details')->delete(); + $this->info("Deleted {$deleted} mutation details"); + } + + // 3. Delete mutations + if (Schema::hasTable('mutations')) { + $deleted = DB::table('mutations')->delete(); + $this->info("Deleted {$deleted} mutations"); + } + + // 4. Reset auto increment + if (Schema::hasTable('mutations')) { + DB::statement('ALTER TABLE mutations AUTO_INCREMENT = 1'); + $this->info('Reset mutations auto increment'); + } + + if (Schema::hasTable('mutation_details')) { + DB::statement('ALTER TABLE mutation_details AUTO_INCREMENT = 1'); + $this->info('Reset mutation_details auto increment'); + } + + DB::commit(); + + $this->info('✅ Mutations data cleaned successfully!'); + $this->info('You can now rollback and re-run migrations.'); + + return 0; + + } catch (\Exception $e) { + DB::rollback(); + $this->error('❌ Error cleaning mutations data: ' . $e->getMessage()); + return 1; + } + } +} diff --git a/app/Enums/MutationStatus.php b/app/Enums/MutationStatus.php index 073d5d7..c5771e9 100755 --- a/app/Enums/MutationStatus.php +++ b/app/Enums/MutationStatus.php @@ -4,23 +4,19 @@ namespace App\Enums; enum MutationStatus: string { - case PENDING = 'pending'; case SENT = 'sent'; case RECEIVED = 'received'; case APPROVED = 'approved'; case REJECTED = 'rejected'; - case COMPLETED = 'completed'; case CANCELLED = 'cancelled'; public function label(): string { return match($this) { - self::PENDING => 'Menunggu Konfirmasi', self::SENT => 'Terkirim ke Dealer', self::RECEIVED => 'Diterima Dealer', - self::APPROVED => 'Disetujui', + self::APPROVED => 'Disetujui & Stock Dipindahkan', self::REJECTED => 'Ditolak', - self::COMPLETED => 'Selesai', self::CANCELLED => 'Dibatalkan', }; } @@ -28,12 +24,10 @@ enum MutationStatus: string public function color(): string { return match($this) { - self::PENDING => 'warning', self::SENT => 'primary', self::RECEIVED => 'info', self::APPROVED => 'brand', self::REJECTED => 'danger', - self::COMPLETED => 'success', self::CANCELLED => 'secondary', }; } @@ -55,12 +49,10 @@ enum MutationStatus: string public static function getOptions(): array { return [ - self::PENDING->value => self::PENDING->label(), self::SENT->value => self::SENT->label(), self::RECEIVED->value => self::RECEIVED->label(), self::APPROVED->value => self::APPROVED->label(), self::REJECTED->value => self::REJECTED->label(), - self::COMPLETED->value => self::COMPLETED->label(), self::CANCELLED->value => self::CANCELLED->label(), ]; } diff --git a/app/Http/Controllers/WarehouseManagement/MutationsController.php b/app/Http/Controllers/WarehouseManagement/MutationsController.php index 249aecf..743a8f7 100755 --- a/app/Http/Controllers/WarehouseManagement/MutationsController.php +++ b/app/Http/Controllers/WarehouseManagement/MutationsController.php @@ -182,8 +182,18 @@ class MutationsController extends Controller $mutation->receive(auth()->id(), $request->reception_notes); DB::commit(); - return redirect()->route('mutations.index') - ->with('success', 'Mutasi berhasil diterima dan menunggu persetujuan pengirim'); + + // Check user role and redirect accordingly + if (!auth()->user()->dealer_id) { + // Users without dealer_id are likely admin, redirect to mutations index + return redirect()->route('mutations.index') + ->with('success', 'Mutasi berhasil diterima dan siap untuk disetujui. Stock akan dipindahkan setelah disetujui.'); + } else { + // Dealer users redirect back to transaction page + return redirect()->route('transaction') + ->with('success', 'Mutasi berhasil diterima. Silakan setujui mutasi ini untuk memindahkan stock.') + ->with('active_tab', 'penerimaan'); + } } catch (\Exception $e) { DB::rollback(); @@ -202,11 +212,25 @@ class MutationsController extends Controller } try { - // Approve mutation (quantity_approved sudah diisi saat receive) + // Approve mutation (stock will move automatically) $mutation->approve(auth()->id(), $request->approval_notes); - return redirect()->route('mutations.index') - ->with('success', 'Mutasi berhasil disetujui'); + // Check user role and redirect accordingly + if (!auth()->user()->dealer_id) { + // Admin users redirect to mutations index + return redirect()->route('mutations.index') + ->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan'); + } else { + // Dealer users + if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) { + return redirect()->route('transaction') + ->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan') + ->with('active_tab', 'penerimaan'); + } else { + return redirect()->route('mutations.index') + ->with('success', 'Mutasi berhasil disetujui dan stock telah dipindahkan'); + } + } } catch (\Exception $e) { return back()->withErrors(['error' => 'Gagal menyetujui mutasi: ' . $e->getMessage()]); @@ -226,30 +250,29 @@ class MutationsController extends Controller try { $mutation->reject(auth()->id(), $request->rejection_reason); - return redirect()->route('mutations.index') - ->with('success', 'Mutasi berhasil ditolak'); + // Check user role and redirect accordingly + if (!auth()->user()->dealer_id) { + // Admin users redirect to mutations index + return redirect()->route('mutations.index') + ->with('success', 'Mutasi berhasil ditolak'); + } else { + // Dealer users + if ($request->has('from_transaction_page') || str_contains($request->header('referer', ''), '/transaction')) { + return redirect()->route('transaction') + ->with('success', 'Mutasi berhasil ditolak') + ->with('active_tab', 'penerimaan'); + } else { + return redirect()->route('mutations.index') + ->with('success', 'Mutasi berhasil ditolak'); + } + } } catch (\Exception $e) { return back()->withErrors(['error' => 'Gagal menolak mutasi: ' . $e->getMessage()]); } } - public function complete(Mutation $mutation) - { - if (!$mutation->canBeCompleted()) { - return back()->withErrors(['error' => 'Mutasi tidak dapat diselesaikan dalam status saat ini']); - } - - try { - $mutation->complete(); - - return redirect()->route('mutations.index') - ->with('success', 'Mutasi berhasil diselesaikan dan stock telah dipindahkan'); - - } catch (\Exception $e) { - return back()->withErrors(['error' => 'Gagal menyelesaikan mutasi: ' . $e->getMessage()]); - } - } + // Complete method removed - Stock moves automatically after approval public function cancel(Request $request, Mutation $mutation) { @@ -294,9 +317,20 @@ class MutationsController extends Controller { $dealerId = $request->dealer_id; + // Get mutations that need action from this dealer: + // 1. 'sent' status where this dealer is the recipient (need to receive) + // 2. 'received' status where this dealer is the recipient (need to approve) - CORRECTED LOGIC $data = Mutation::with(['fromDealer', 'toDealer', 'requestedBy.role']) - ->where('to_dealer_id', $dealerId) - ->where('status', 'sent') + ->where(function($query) use ($dealerId) { + // Mutations sent to this dealer that need to be received + $query->where('to_dealer_id', $dealerId) + ->where('status', 'sent'); + // OR mutations received by this dealer that need approval from recipient + $query->orWhere(function($subQuery) use ($dealerId) { + $subQuery->where('to_dealer_id', $dealerId) + ->where('status', 'received'); + }); + }) ->select('mutations.*'); return DataTables::of($data) @@ -307,6 +341,9 @@ class MutationsController extends Controller ->addColumn('from_dealer', function($row) { return $row->fromDealer->name ?? '-'; }) + ->addColumn('to_dealer', function($row) { + return $row->toDealer->name ?? '-'; + }) ->addColumn('status', function($row) { $statusColor = $row->status_color; $statusLabel = $row->status_label; @@ -330,10 +367,28 @@ class MutationsController extends Controller ->addColumn('created_at', function($row) { return $row->created_at->format('d/m/Y H:i'); }) - ->addColumn('action', function($row) { - return ''; + ->addColumn('action', function($row) use ($dealerId) { + $buttons = ''; + + if ($row->status->value === 'sent' && $row->to_dealer_id == $dealerId) { + // For sent mutations where current dealer is recipient - show detail button for receiving + $buttons .= ''; + } elseif ($row->status->value === 'received' && $row->to_dealer_id == $dealerId) { + // For received mutations where current dealer is recipient - show approve/reject buttons + $buttons .= ''; + $buttons .= ''; + $buttons .= ''; + } + + return $buttons; }) ->rawColumns(['status', 'action']) ->make(true); diff --git a/app/Models/Mutation.php b/app/Models/Mutation.php index 40e7498..5a8c70e 100755 --- a/app/Models/Mutation.php +++ b/app/Models/Mutation.php @@ -116,11 +116,6 @@ class Mutation extends Model return $this->mutationDetails()->sum('quantity_approved'); } - public function canBeSent() - { - return $this->status === MutationStatus::PENDING; - } - public function canBeReceived() { return $this->status === MutationStatus::SENT; @@ -131,28 +126,9 @@ class Mutation extends Model return $this->status === MutationStatus::RECEIVED; } - public function canBeCompleted() - { - return $this->status === MutationStatus::APPROVED; - } - public function canBeCancelled() { - return in_array($this->status, [MutationStatus::PENDING, MutationStatus::SENT]); - } - - // Send mutation to destination dealer - public function send($userId) - { - if (!$this->canBeSent()) { - throw new \Exception('Mutasi tidak dapat dikirim dalam status saat ini'); - } - - $this->update([ - 'status' => MutationStatus::SENT - ]); - - return $this; + return $this->status === MutationStatus::SENT; } // Receive mutation by destination dealer @@ -172,19 +148,37 @@ class Mutation extends Model return $this; } - // Approve mutation + // Approve mutation and move stock immediately public function approve($userId, $approvalNotes = null) { if (!$this->canBeApproved()) { throw new \Exception('Mutasi tidak dapat disetujui dalam status saat ini'); } - $this->update([ - 'status' => MutationStatus::APPROVED, - 'approved_by' => $userId, - 'approved_at' => now(), - 'approval_notes' => $approvalNotes - ]); + DB::beginTransaction(); + try { + // Update status to approved first + $this->update([ + 'status' => MutationStatus::APPROVED, + 'approved_by' => $userId, + 'approved_at' => now(), + 'approval_notes' => $approvalNotes + ]); + + // Immediately move stock after approval + foreach ($this->mutationDetails as $detail) { + // Process all details that have quantity_requested > 0 + // because goods have been sent from source dealer + if ($detail->quantity_requested > 0) { + $this->processStockMovement($detail); + } + } + + DB::commit(); + } catch (\Exception $e) { + DB::rollback(); + throw $e; + } return $this; } @@ -223,32 +217,7 @@ class Mutation extends Model return $this; } - // Complete mutation (actually move the stock) - public function complete() - { - if (!$this->canBeCompleted()) { - throw new \Exception('Mutasi tidak dapat diselesaikan dalam status saat ini'); - } - - DB::beginTransaction(); - try { - foreach ($this->mutationDetails as $detail) { - // Proses semua detail yang memiliki quantity_requested > 0 - // karena barang sudah dikirim dari dealer asal - if ($detail->quantity_requested > 0) { - $this->processStockMovement($detail); - } - } - - $this->update(['status' => MutationStatus::COMPLETED]); - DB::commit(); - } catch (\Exception $e) { - DB::rollback(); - throw $e; - } - - return $this; - } + // Complete method removed - Stock moves automatically after approval private function processStockMovement(MutationDetail $detail) { diff --git a/app/Models/MutationDetail.php b/app/Models/MutationDetail.php index 0138c6d..15f91c9 100755 --- a/app/Models/MutationDetail.php +++ b/app/Models/MutationDetail.php @@ -52,7 +52,7 @@ class MutationDetail extends Model { // Hanya dianggap ditolak jika mutasi sudah di-approve/reject dan quantity_approved = 0 $mutationStatus = $this->mutation->status->value ?? null; - return in_array($mutationStatus, ['approved', 'completed', 'rejected']) && $this->quantity_approved == 0; + return in_array($mutationStatus, ['approved', 'rejected']) && $this->quantity_approved == 0; } public function getApprovalStatusAttribute() @@ -60,11 +60,11 @@ class MutationDetail extends Model $mutationStatus = $this->mutation->status->value ?? null; // Jika mutasi belum di-approve, semua detail statusnya "Menunggu" - if (!in_array($mutationStatus, ['approved', 'completed', 'rejected'])) { + if (!in_array($mutationStatus, ['approved', 'rejected'])) { return 'Menunggu'; } - // Jika mutasi sudah di-approve/complete, baru cek quantity_approved + // Jika mutasi sudah di-approve, baru cek quantity_approved if ($this->isFullyApproved()) { return 'Disetujui Penuh'; } elseif ($this->isPartiallyApproved()) { @@ -81,11 +81,11 @@ class MutationDetail extends Model $mutationStatus = $this->mutation->status->value ?? null; // Jika mutasi belum di-approve, semua detail statusnya "info" (menunggu) - if (!in_array($mutationStatus, ['approved', 'completed', 'rejected'])) { + if (!in_array($mutationStatus, ['approved', 'rejected'])) { return 'info'; } - // Jika mutasi sudah di-approve/complete, baru cek quantity_approved + // Jika mutasi sudah di-approve, baru cek quantity_approved if ($this->isFullyApproved()) { return 'success'; } elseif ($this->isPartiallyApproved()) { diff --git a/database/migrations/2025_06_12_111500_update_mutation_details_quantity_approved_default.php b/database/migrations/2025_06_12_111500_update_mutation_details_quantity_approved_default.php index 9c2f9cc..f2bc89c 100755 --- a/database/migrations/2025_06_12_111500_update_mutation_details_quantity_approved_default.php +++ b/database/migrations/2025_06_12_111500_update_mutation_details_quantity_approved_default.php @@ -16,7 +16,7 @@ return new class extends Migration DB::table('mutation_details') ->join('mutations', 'mutations.id', '=', 'mutation_details.mutation_id') ->where('mutation_details.quantity_approved', 0) - ->whereNotIn('mutations.status', ['approved', 'completed', 'rejected']) + ->whereNotIn('mutations.status', ['approved', 'rejected']) ->update(['mutation_details.quantity_approved' => null]); Schema::table('mutation_details', function (Blueprint $table) { diff --git a/database/migrations/2025_06_13_130135_add_additional_notes_and_rejection_fields_to_mutations_table.php b/database/migrations/2025_06_13_130135_add_additional_notes_and_rejection_fields_to_mutations_table.php index b25acfa..32785d1 100755 --- a/database/migrations/2025_06_13_130135_add_additional_notes_and_rejection_fields_to_mutations_table.php +++ b/database/migrations/2025_06_13_130135_add_additional_notes_and_rejection_fields_to_mutations_table.php @@ -40,6 +40,9 @@ class AddAdditionalNotesAndRejectionFieldsToMutationsTable extends Migration public function down() { Schema::table('mutations', function (Blueprint $table) { + $table->dropForeign(['rejected_by']); + $table->dropForeign(['cancelled_by']); + // Remove new fields $table->dropColumn([ 'reception_notes', diff --git a/database/migrations/2025_06_15_015234_remove_pending_and_completed_from_mutations_status.php b/database/migrations/2025_06_15_015234_remove_pending_and_completed_from_mutations_status.php new file mode 100644 index 0000000..cd257a0 --- /dev/null +++ b/database/migrations/2025_06_15_015234_remove_pending_and_completed_from_mutations_status.php @@ -0,0 +1,36 @@ +where('status', 'pending') + ->update(['status' => 'sent']); + + DB::table('mutations') + ->where('status', 'completed') + ->update(['status' => 'approved']); + + // Then update the enum column to remove 'pending' and 'completed' + DB::statement("ALTER TABLE mutations MODIFY COLUMN status ENUM('sent', 'received', 'approved', 'rejected', 'cancelled') DEFAULT 'sent'"); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Restore the original enum with 'pending' and 'completed' + DB::statement("ALTER TABLE mutations MODIFY COLUMN status ENUM('pending', 'sent', 'received', 'approved', 'rejected', 'completed', 'cancelled') DEFAULT 'sent'"); + } +}; diff --git a/database/migrations/2025_06_15_015235_manual_cleanup_mutations.php b/database/migrations/2025_06_15_015235_manual_cleanup_mutations.php new file mode 100644 index 0000000..d81563b --- /dev/null +++ b/database/migrations/2025_06_15_015235_manual_cleanup_mutations.php @@ -0,0 +1,50 @@ +where('status', 'pending') + ->update(['status' => 'sent']); + + // Update existing 'completed' status to 'approved' + DB::table('mutations') + ->where('status', 'completed') + ->update(['status' => 'approved']); + + // Update the enum to only include valid statuses + DB::statement("ALTER TABLE mutations MODIFY COLUMN status ENUM('sent', 'received', 'approved', 'rejected', 'cancelled') DEFAULT 'sent'"); + + // Update any orphaned mutation_details that reference completed status + DB::statement(" + UPDATE mutation_details md + JOIN mutations m ON md.mutation_id = m.id + SET md.quantity_approved = md.quantity_requested + WHERE m.status = 'approved' AND md.quantity_approved IS NULL + "); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Restore original enum (if needed) + DB::statement("ALTER TABLE mutations MODIFY COLUMN status ENUM('pending', 'sent', 'received', 'approved', 'rejected', 'completed', 'cancelled') DEFAULT 'sent'"); + } +}; \ No newline at end of file diff --git a/database/seeders/MutationTestSeeder.php b/database/seeders/MutationTestSeeder.php index e573498..d23920f 100755 --- a/database/seeders/MutationTestSeeder.php +++ b/database/seeders/MutationTestSeeder.php @@ -99,18 +99,18 @@ class MutationTestSeeder extends Seeder 'notes' => 'Produk approved parsial' ]); - // 4. Mutation dengan status COMPLETED (selesai) - $this->command->info('Creating COMPLETED mutation...'); + // 4. Additional APPROVED mutation (completed workflow is now just approved) + $this->command->info('Creating additional APPROVED mutation...'); $mutation4 = Mutation::create([ 'from_dealer_id' => $dealers[0]->id, 'to_dealer_id' => $dealers[1]->id, - 'status' => MutationStatus::COMPLETED, + 'status' => MutationStatus::APPROVED, 'requested_by' => $users[0]->id, 'received_by' => $users[1]->id ?? $users[0]->id, 'received_at' => now()->subDay(), 'approved_by' => $users[0]->id, 'approved_at' => now()->subHours(6), - 'notes' => 'Mutasi test - status COMPLETED' + 'notes' => 'Mutasi test - status APPROVED (stock moved)' ]); MutationDetail::create([ @@ -118,7 +118,7 @@ class MutationTestSeeder extends Seeder 'product_id' => $products[1]->id, 'quantity_requested' => 6.00, 'quantity_approved' => 6.00, - 'notes' => 'Produk completed' + 'notes' => 'Produk approved & stock moved' ]); // 5. Mutation dengan status REJECTED @@ -147,8 +147,8 @@ class MutationTestSeeder extends Seeder $this->command->info('Test mutations created successfully!'); $this->command->info('- Mutation SENT: ' . $mutation1->mutation_number); $this->command->info('- Mutation RECEIVED: ' . $mutation2->mutation_number); - $this->command->info('- Mutation APPROVED: ' . $mutation3->mutation_number); - $this->command->info('- Mutation COMPLETED: ' . $mutation4->mutation_number); + $this->command->info('- Mutation APPROVED (1): ' . $mutation3->mutation_number); + $this->command->info('- Mutation APPROVED (2): ' . $mutation4->mutation_number); $this->command->info('- Mutation REJECTED: ' . $mutation5->mutation_number); } catch (\Exception $e) { diff --git a/resources/views/transaction/index.blade.php b/resources/views/transaction/index.blade.php index e785df2..00a51ad 100755 --- a/resources/views/transaction/index.blade.php +++ b/resources/views/transaction/index.blade.php @@ -164,6 +164,24 @@ use Illuminate\Support\Facades\Auth; .mutation-detail-table input { font-size: 14px; } + + .bg-warning-light { + background-color: #fff3cd !important; + border: 1px solid #ffeaa7; + } + + .form-control-plaintext.border { + background-color: #f8f9fa; + font-style: normal; + } + + .alert ul { + padding-left: 1.2rem; + } + + .alert ul li { + margin-bottom: 0.25rem; + } @endsection @@ -718,16 +736,17 @@ use Illuminate\Support\Facades\Auth;
| No. Mutasi | -Dealer Asal | -Status | -Total Item | -Tanggal | +No. Mutasi | +Dealer Asal | +Dealer Tujuan | +Status | +Total Item | +Tanggal | Aksi |
|---|
Setelah disetujui, stock akan siap untuk dipindahkan.
+Setelah disetujui, stock akan dipindahkan secara otomatis.
Setelah menerima, mutasi akan menunggu persetujuan dari pengirim sebelum stock dipindahkan.
+Setelah menerima, mutasi akan menunggu persetujuan dari pengirim. Stock akan dipindahkan otomatis setelah disetujui.
Setelah menerima, mutasi akan menunggu persetujuan dari pengirim sebelum stock dipindahkan.
+Setelah menerima, mutasi akan menunggu persetujuan dari pengirim. Stock akan dipindahkan otomatis setelah disetujui.
-Setelah disetujui, stock akan siap untuk dipindahkan.
+Setelah disetujui, stock akan dipindahkan secara otomatis.
@endif -@if($mutation->status->value === 'approved') - -