*/ public array $cart = []; public function mount(): void { $this->tanggal = date('Y-m-d'); $this->penerimaId = User::orderBy('nama', 'ASC')->value('id'); } public function updatingSearch(): void { $this->resetPage(); } public function scanLookup(): void { $service = app(SimbhpStockService::class); $raw = (string) $this->scan; $parsed = $service->parseBarcode($raw); $kode = (string) ($parsed['kode'] ?? ''); $jenisId = $parsed['jenis_id'] ?? null; $this->scan = ''; if ($kode === '' && (is_null($jenisId) || (int) $jenisId <= 0)) { $this->toast('error', 'Kode barcode kosong.'); return; } $jenis = $service->getJenisByKode($raw); if (!$jenis) { $this->toast('error', 'Kode barang tidak ditemukan.'); return; } $saldoBase = $service->getStockBaseByJenis((string) ($jenis->jenis ?? '')); if ($saldoBase <= 0) { $this->toast('error', 'Stok kosong untuk ' . ((string) ($jenis->jenis ?? 'barang')) . '.'); return; } $this->selectedKode = (string) ($jenis->kodejenis ?? ''); $this->qty = 1; $this->keterangan = ''; $this->satuanTransaksi = (string) ($parsed['satuan_transaksi'] ?? 'besar'); $this->syncSatuanAvailability(); $this->dispatch('gudangpos-open-modal'); } #[On('gudangpos-select')] public function selectProduct(string $kode): void { $service = app(SimbhpStockService::class); $kode = $service->sanitizeKode($kode); $jenis = $service->getJenisByKode($kode); if (!$jenis) { $this->toast('error', 'Kode barang tidak ditemukan: ' . $kode); return; } $saldoBase = $service->getStockBaseByJenis((string) ($jenis->jenis ?? '')); if ($saldoBase <= 0) { $this->toast('error', 'Stok kosong untuk ' . ((string) ($jenis->jenis ?? 'barang')) . '.'); return; } $this->selectedKode = (string) ($jenis->kodejenis ?? ''); $this->qty = 1; $this->keterangan = ''; $this->syncSatuanAvailability(); $this->dispatch('gudangpos-open-modal'); } #[On('gudangpos-refresh')] public function refreshComponent(): void { // Force rerender + keep UI responsive when stok berubah dari tab lain. $this->resetPage(); } public function addSelected(): void { $service = app(SimbhpStockService::class); $kode = $service->sanitizeKode($this->selectedKode); if ($kode === '') { $this->toast('error', 'Pilih barang dulu (scan barcode / klik daftar produk).'); return; } $jenis = $service->getJenisByKode($kode); if (!$jenis) { $this->toast('error', 'Kode barang tidak ditemukan: ' . $kode); return; } $qty = (int) $this->qty; $keterangan = trim((string) $this->keterangan); if ($qty <= 0) { $this->toast('error', 'Jumlah harus lebih dari 0.'); return; } $setting = $service->getUnitSetting($jenis); if ($this->satuanTransaksi === 'kecil' && !$setting['has_breakdown']) { $this->satuanTransaksi = 'besar'; $this->toast('info', 'Barang ini tidak memiliki satuan kecil, otomatis pakai satuan besar.'); } $found = false; foreach ($this->cart as $i => $line) { if ( ($line['kode'] ?? '') === $kode && ($line['satuan_transaksi'] ?? '') === $this->satuanTransaksi && trim((string) ($line['keterangan'] ?? '')) === $keterangan ) { $this->cart[$i]['qty'] = ((int) ($this->cart[$i]['qty'] ?? 0)) + $qty; $found = true; break; } } if (!$found) { $this->cart[] = [ 'kode' => $kode, 'jenis' => (string) ($jenis->jenis ?? ''), 'qty' => $qty, 'satuan_transaksi' => $this->satuanTransaksi, 'keterangan' => $keterangan, ]; } $this->qty = 1; $this->keterangan = ''; $this->dispatch('gudangpos-close-modal'); $this->dispatch('gudangpos-focus', field: 'scan'); } public function removeLine(int $index): void { if (!isset($this->cart[$index])) { return; } array_splice($this->cart, $index, 1); } public function processCart(): void { if (count($this->cart) === 0) { $this->toast('info', 'Keranjang masih kosong.'); return; } if (!$this->penerimaId) { $this->toast('error', 'Penerima wajib dipilih.'); return; } $tanggal = trim((string) $this->tanggal); if ($tanggal === '') { $this->toast('error', 'Tanggal wajib diisi.'); return; } try { $date = Carbon::parse($tanggal); } catch (\Throwable $e) { $this->toast('error', 'Format tanggal tidak valid.'); return; } $penerima = User::select('id', 'nama')->find($this->penerimaId); if (!$penerima) { $this->toast('error', 'Penerima tidak ditemukan.'); return; } $service = app(SimbhpStockService::class); $prepared = []; $stockCache = []; foreach ($this->cart as $line) { $kode = $service->sanitizeKode((string) ($line['kode'] ?? '')); $qty = (int) ($line['qty'] ?? 0); $satuan = (string) ($line['satuan_transaksi'] ?? 'besar'); $keterangan = trim((string) ($line['keterangan'] ?? '')); if ($kode === '' || $qty <= 0) { $this->toast('error', 'Ada item keranjang yang tidak valid.'); return; } $jenis = $service->getJenisByKode($kode); if (!$jenis) { $this->toast('error', 'Kode barang tidak ditemukan: ' . $kode); return; } $setting = $service->getUnitSetting($jenis); if ($satuan === 'kecil' && !$setting['has_breakdown']) { $satuan = 'besar'; } $qtyBase = $service->calculateBaseQty($jenis, $qty, $satuan); $jenisNama = (string) ($jenis->jenis ?? ''); if (!array_key_exists($jenisNama, $stockCache)) { $stockCache[$jenisNama] = $service->getStockBaseByJenis($jenisNama); } if ($qtyBase > $stockCache[$jenisNama]) { if ((int) $stockCache[$jenisNama] <= 0) { $this->toast('error', 'Stok kosong untuk ' . $jenisNama . '.'); } else { $this->toast('error', 'Stok tidak cukup untuk ' . $jenisNama . '.'); } return; } $stockCache[$jenisNama] -= $qtyBase; $prepared[] = [ 'jenis' => $jenis, 'qty' => $qty, 'qty_base' => $qtyBase, 'satuan_transaksi' => $satuan, 'keterangan' => $keterangan, ]; } DB::transaction(function () use ($prepared, $date, $penerima) { foreach ($prepared as $row) { /** @var SIMBHPJenis $jenis */ $jenis = $row['jenis']; SIMBHPReport::create([ 'tanggal' => (int) $date->format('d'), 'bulan' => (int) $date->format('m'), 'tahun' => (int) $date->format('Y'), 'deskripsi' => 'Diterima oleh ' . ($penerima->nama ?? 'Unkown'), 'pemasukan' => null, 'pengeluaran' => (int) $row['qty'], 'qty_base' => (int) $row['qty_base'], 'satuan_transaksi' => (string) $row['satuan_transaksi'], 'masa_expired' => null, 'jenis' => (string) ($jenis->jenis ?? ''), 'keterangan' => (string) ($row['keterangan'] ?? ''), 'marking' => '', ]); } }); $this->cart = []; $this->qty = 1; $this->keterangan = ''; $this->toast('success', 'Barang keluar berhasil diproses.'); $this->dispatch('gudang-refresh'); $this->dispatch('gudangpos-close-modal'); $this->dispatch('gudangpos-focus', field: 'scan'); } public function updatedSelectedKode(): void { $this->syncSatuanAvailability(); } public function updatedSatuanTransaksi(): void { $this->syncSatuanAvailability(); } private function syncSatuanAvailability(): void { $service = app(SimbhpStockService::class); $kode = $service->sanitizeKode($this->selectedKode); if ($kode === '') { return; } $jenis = $service->getJenisByKode($kode); if (!$jenis) { return; } $setting = $service->getUnitSetting($jenis); if (!$setting['has_breakdown'] && $this->satuanTransaksi === 'kecil') { $this->satuanTransaksi = 'besar'; } } private function toast(string $type, string $message): void { $this->dispatch('gudangpos-toast', type: $type, message: $message); } public function render() { $service = app(SimbhpStockService::class); $users = User::select('id', 'nama', 'previlage')->orderBy('nama', 'ASC')->get(); $query = SIMBHPJenis::query()->orderBy('jenis', 'ASC'); $search = trim((string) $this->search); if ($search !== '') { $searchLike = '%' . $search . '%'; $driver = DB::connection()->getDriverName(); $operator = ($driver === 'pgsql') ? 'ILIKE' : 'LIKE'; $query->where(function ($q) use ($searchLike, $operator) { $q->where('jenis', $operator, $searchLike) ->orWhere('kodejenis', $operator, $searchLike); }); } $products = $query->paginate(12); $jenisNames = $products->getCollection()->pluck('jenis')->filter()->values()->all(); $stockMap = $service->getStockBaseByJenisMany($jenisNames); $latestExpiredMap = []; if (count($jenisNames) > 0) { $expiredRows = SIMBHPReport::query() ->select('jenis', DB::raw('MAX(masa_expired) as latest_expired')) ->whereIn('jenis', $jenisNames) ->whereNotNull('masa_expired') ->whereNotNull('pemasukan') ->where('pemasukan', '>', 0) ->groupBy('jenis') ->get(); foreach ($expiredRows as $row) { $jenisNama = (string) ($row->jenis ?? ''); $expired = (string) ($row->latest_expired ?? ''); if ($jenisNama !== '' && $expired !== '') { $expiredDate = Carbon::parse($expired)->startOfDay(); $today = Carbon::today(); $daysLeft = $today->diffInDays($expiredDate, false); $badgeClass = 'success'; if ($daysLeft < 0) { $badgeClass = 'danger'; } elseif ($daysLeft <= 30) { $badgeClass = 'warning'; } $latestExpiredMap[$jenisNama] = [ 'date' => $expiredDate->format('d-m-Y'), 'badge_class' => $badgeClass, ]; } } } $selectedJenis = null; $selectedStockDisplay = null; $selectedSetting = null; $selectedLatestExpired = null; $kode = $service->sanitizeKode($this->selectedKode); if ($kode !== '') { $selectedJenis = $service->getJenisByKode($kode); if ($selectedJenis) { $saldoBase = $service->getStockBaseByJenis((string) $selectedJenis->jenis); $selectedSetting = $service->getUnitSetting($selectedJenis); $selectedStockDisplay = $service->formatStockDisplay( $saldoBase, (string) ($selectedJenis->satuan ?? ''), (string) ($selectedJenis->satuan_kecil ?? ''), (int) ($selectedJenis->konversi_kecil ?? 1), ); $selectedLatestExpired = $latestExpiredMap[(string) $selectedJenis->jenis] ?? null; } } return view('livewire.gudang-pos', [ 'users' => $users, 'products' => $products, 'stockMap' => $stockMap, 'latestExpiredMap' => $latestExpiredMap, 'selectedJenis' => $selectedJenis, 'selectedStockDisplay' => $selectedStockDisplay, 'selectedSetting' => $selectedSetting, 'selectedLatestExpired' => $selectedLatestExpired, ]); } }