diff --git a/htdocs/app/Http/Controllers/GudangController.php b/htdocs/app/Http/Controllers/GudangController.php index ea8c1f9f..58c05a21 100644 --- a/htdocs/app/Http/Controllers/GudangController.php +++ b/htdocs/app/Http/Controllers/GudangController.php @@ -4,10 +4,12 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use App\XFiles; use App\SIMBHPJenis; use App\SIMBHPReport; use App\User; +use App\Services\SimbhpStockService; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use Validator; @@ -25,7 +27,9 @@ class GudangController extends Controller $cdatane = SIMBHPJenis::all(); $cjenis = count($cdatane); if ($cjenis == 0){ + $tasks['jjenis'][0]['id'] = null; $tasks['jjenis'][0]['jenis'] = 'Belum Ada Jenis Barang'; + $tasks['jjenis'][0]['kodejenis'] = ''; $tasks['jjenis'][0]['satuan'] = ''; $tasks['jjenis'][0]['satuan_kecil'] = ''; $tasks['jjenis'][0]['konversi_kecil'] = 1; @@ -33,7 +37,9 @@ class GudangController extends Controller } else { $i = 0; foreach($cdatane as $rdata){ + $tasks['jjenis'][$i]['id'] = $rdata->id; $tasks['jjenis'][$i]['jenis'] = $rdata->jenis; + $tasks['jjenis'][$i]['kodejenis'] = $rdata->kodejenis ?? ''; $tasks['jjenis'][$i]['satuan'] = $rdata->satuan; $tasks['jjenis'][$i]['satuan_kecil'] = $rdata->satuan_kecil ?? ''; $tasks['jjenis'][$i]['konversi_kecil'] = $rdata->konversi_kecil ?? 1; @@ -99,6 +105,63 @@ class GudangController extends Controller echo json_encode($arraysurat); } + public function jsonSnapshot() + { + if (Session::get('previlage') == '') { + return response()->json(['message' => 'Unauthorized'], 401); + } + + $jjenis = []; + $cdatane = SIMBHPJenis::orderBy('jenis', 'ASC')->get(); + if ($cdatane->count() == 0) { + $jjenis[] = [ + 'id' => null, + 'jenis' => 'Belum Ada Jenis Barang', + 'kodejenis' => '', + 'satuan' => '', + 'satuan_kecil' => '', + 'konversi_kecil' => 1, + 'stok_minimum' => 0, + ]; + } else { + foreach ($cdatane as $rdata) { + $jjenis[] = [ + 'id' => $rdata->id, + 'jenis' => $rdata->jenis, + 'kodejenis' => $rdata->kodejenis ?? '', + 'satuan' => $rdata->satuan, + 'satuan_kecil' => $rdata->satuan_kecil ?? '', + 'konversi_kecil' => $rdata->konversi_kecil ?? 1, + 'stok_minimum' => $rdata->stok_minimum ?? 0, + ]; + } + } + + return response()->json([ + 'jjenis' => $jjenis, + 'jenisRows' => $this->getJenisRows(), + 'warningstok' => $this->getLowStockWarnings(), + 'expiringSoon' => $this->getExpiringSoonItems(), + 'stat' => [ + 'harian' => [ + 'masuk' => $this->getAggregateBaseQty('pemasukan', 'harian'), + 'keluar' => $this->getAggregateBaseQty('pengeluaran', 'harian'), + 'perjenis' => $this->getUsagePerJenis('harian'), + ], + 'bulanan' => [ + 'masuk' => $this->getAggregateBaseQty('pemasukan', 'bulanan'), + 'keluar' => $this->getAggregateBaseQty('pengeluaran', 'bulanan'), + 'perjenis' => $this->getUsagePerJenis('bulanan'), + ], + 'tahunan' => [ + 'masuk' => $this->getAggregateBaseQty('pemasukan', 'tahunan'), + 'keluar' => $this->getAggregateBaseQty('pengeluaran', 'tahunan'), + 'perjenis' => $this->getUsagePerJenis('tahunan'), + ], + ], + ]); + } + public function jsonReportbhp(Request $request) { $bulan = $request->input('val01'); $tahun = $request->input('val02'); @@ -293,7 +356,7 @@ class GudangController extends Controller $tanggal = $request->input('set03'); $jumlah = $request->input('set04'); $jenis = $request->input('set05'); - $postujuan = $request->input('set06'); + $postujuan = $request->input('set06'); // also used as kodejenis when $jenis == 'jenis' $alasan = $request->input('set07'); $set08 = $request->input('set08'); $set09 = $request->input('set09'); @@ -310,6 +373,7 @@ class GudangController extends Controller $jenis = $request->input('set02'); $satuan = $request->input('set03'); $idne = $request->input('set01'); + $kodeInput = trim((string) $request->input('set06')); $satuanKecil = trim((string) $set08); $konversiKecil = (int) $set09; if ($konversiKecil <= 0){ $konversiKecil = 1; } @@ -320,7 +384,21 @@ class GudangController extends Controller } elseif ($konversiKecil <= 1) { return response()->json(['icon' => 'error', 'warna' => '#bf441d', 'status' => 'Gagal', 'message' => 'Jika memakai satuan kecil, konversi harus lebih dari 1']); } - $kodejenis = preg_replace('/\s+/', '', $jenis); + + if ($kodeInput !== '') { + $kodejenis = strtoupper($kodeInput); + $kodejenis = preg_replace('/\s+/', '', $kodejenis); + $kodejenis = preg_replace('/[^A-Z0-9._-]/', '', $kodejenis); + } else { + $kodejenis = strtoupper((string) $jenis); + $kodejenis = preg_replace('/\s+/', '', $kodejenis); + $kodejenis = preg_replace('/[^A-Z0-9._-]/', '', $kodejenis); + } + + if ($kodejenis === '' || strlen($kodejenis) < 2 || strlen($kodejenis) > 40) { + return response()->json(['icon' => 'error', 'warna' => '#bf441d', 'status' => 'Gagal', 'message' => 'Kode barang wajib (2-40 karakter). Gunakan huruf/angka dan simbol - _ .']); + } + if ($idne == 'new' OR $idne == ''){ $ceksudah = SIMBHPJenis::where('kodejenis', $kodejenis)->where('satuan', $satuan)->count(); if ($ceksudah != 0){ @@ -335,6 +413,14 @@ class GudangController extends Controller 'stok_minimum' => $stokMinimum, ]); if ($input){ + if (Schema::hasColumn('simbhpjenis', 'barcode_besar')) { + $service = app(SimbhpStockService::class); + $setting = $service->getUnitSetting($input); + $input->update([ + 'barcode_besar' => $service->makeBarcodeValue((int) $input->id, 'besar'), + 'barcode_kecil' => ($setting['has_breakdown'] ?? false) ? $service->makeBarcodeValue((int) $input->id, 'kecil') : null, + ]); + } return response()->json(['status' => 'Success', 'message' => 'Data '.$jenis.' Sukses Ditambahkan']); } else { return response()->json(['icon' => 'error', 'warna' => '#bf441d', 'status' => 'Gagal', 'message' => $jenis.' Gagal di masukkan, silahkan ulangi beberapa saat lagi']); @@ -354,6 +440,16 @@ class GudangController extends Controller 'stok_minimum' => $stokMinimum, ]); if ($input){ + if (Schema::hasColumn('simbhpjenis', 'barcode_besar')) { + $row = SIMBHPJenis::find($idne); + if ($row) { + $service = app(SimbhpStockService::class); + $setting = $service->getUnitSetting($row); + $row->barcode_besar = $service->makeBarcodeValue((int) $row->id, 'besar'); + $row->barcode_kecil = ($setting['has_breakdown'] ?? false) ? $service->makeBarcodeValue((int) $row->id, 'kecil') : null; + $row->save(); + } + } return response()->json(['status' => 'Success', 'message' => 'Data '.$jenis.' Sukses Diupdate']); } else { return response()->json(['icon' => 'error', 'warna' => '#bf441d', 'status' => 'Gagal', 'message' => $jenis.' Gagal di masukkan, silahkan ulangi beberapa saat lagi']); @@ -794,6 +890,7 @@ class GudangController extends Controller if ($konversi <= 0) { $konversi = 1; } $rows[] = [ 'id' => $item->id, + 'kodejenis' => $item->kodejenis ?? '', 'jenis' => $item->jenis, 'satuan' => $item->satuan, 'satuan_kecil' => $item->satuan_kecil ?? '', diff --git a/htdocs/app/Livewire/GudangPos.php b/htdocs/app/Livewire/GudangPos.php new file mode 100644 index 00000000..32eb3067 --- /dev/null +++ b/htdocs/app/Livewire/GudangPos.php @@ -0,0 +1,342 @@ + + */ + 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; + } + + $this->selectedKode = (string) ($jenis->kodejenis ?? ''); + $this->qty = 1; + $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; + } + + $this->selectedKode = (string) ($jenis->kodejenis ?? ''); + $this->qty = 1; + $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; + 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) { + $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, + ]; + } + + $this->qty = 1; + $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'); + 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]) { + $this->toast('error', 'Stok tidak cukup untuk ' . $jenisNama . '.'); + return; + } + $stockCache[$jenisNama] -= $qtyBase; + + $prepared[] = [ + 'jenis' => $jenis, + 'qty' => $qty, + 'qty_base' => $qtyBase, + 'satuan_transaksi' => $satuan, + ]; + } + + 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' => '', + 'marking' => '', + ]); + } + }); + + $this->cart = []; + $this->qty = 1; + $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 . '%'; + $query->where(function ($q) use ($searchLike) { + $q->where('jenis', 'LIKE', $searchLike) + ->orWhere('kodejenis', 'LIKE', $searchLike); + }); + } + + $products = $query->paginate(12); + + $jenisNames = $products->getCollection()->pluck('jenis')->filter()->values()->all(); + $stockMap = $service->getStockBaseByJenisMany($jenisNames); + + $selectedJenis = null; + $selectedStockDisplay = null; + $selectedSetting = 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), + ); + } + } + + return view('livewire.gudang-pos', [ + 'users' => $users, + 'products' => $products, + 'stockMap' => $stockMap, + 'selectedJenis' => $selectedJenis, + 'selectedStockDisplay' => $selectedStockDisplay, + 'selectedSetting' => $selectedSetting, + ]); + } +} diff --git a/htdocs/app/Services/SimbhpStockService.php b/htdocs/app/Services/SimbhpStockService.php new file mode 100644 index 00000000..2d647516 --- /dev/null +++ b/htdocs/app/Services/SimbhpStockService.php @@ -0,0 +1,212 @@ +sanitizeKode($raw); + if ($raw === '') { + return [ + 'kode' => '', + 'jenis_id' => null, + 'satuan_transaksi' => 'besar', + 'barcode_value' => '', + ]; + } + + if (preg_match('/^91(\d{8})([12])$/', $raw, $m)) { + $id = (int) ltrim((string) $m[1], '0'); + $unitDigit = (string) $m[2]; + + return [ + 'kode' => '', + 'jenis_id' => $id > 0 ? $id : null, + 'satuan_transaksi' => ($unitDigit === '2') ? 'kecil' : 'besar', + 'barcode_value' => $raw, + ]; + } + + if (preg_match('/^(.*)-(B|K)$/', $raw, $m)) { + $base = $this->sanitizeKode((string) $m[1]); + $suffix = (string) $m[2]; + return [ + 'kode' => $base, + 'jenis_id' => null, + 'satuan_transaksi' => ($suffix === 'K') ? 'kecil' : 'besar', + 'barcode_value' => $base . '-' . $suffix, + ]; + } + + return [ + 'kode' => $raw, + 'jenis_id' => null, + 'satuan_transaksi' => 'besar', + 'barcode_value' => $raw, + ]; + } + + public function sanitizeKode(string $kode): string + { + $kode = strtoupper(trim($kode)); + $kode = preg_replace('/\s+/', '', $kode); + $kode = preg_replace('/[^A-Z0-9._-]/', '', $kode); + + return substr($kode, 0, 40); + } + + public function getJenisByKode(string $kode): ?SIMBHPJenis + { + $parsed = $this->parseBarcode($kode); + $jenisId = $parsed['jenis_id'] ?? null; + if (!is_null($jenisId) && (int) $jenisId > 0) { + return SIMBHPJenis::where('id', (int) $jenisId)->first(); + } + + $kode = $parsed['kode'] ?? ''; + if ($kode === '') { + return null; + } + + return SIMBHPJenis::where('kodejenis', $kode)->first(); + } + + public function makeBarcodeValue(int $jenisId, string $satuanTransaksi = 'besar'): string + { + if ($jenisId <= 0) { + return ''; + } + + $idPart = str_pad((string) $jenisId, 8, '0', STR_PAD_LEFT); + $unitDigit = ($satuanTransaksi === 'kecil') ? '2' : '1'; + + return '91' . $idPart . $unitDigit; + } + + /** + * @return array{has_breakdown: bool, konversi: int, satuan_kecil: string} + */ + public function getUnitSetting(SIMBHPJenis $jenis): array + { + $satuanKecil = trim((string) ($jenis->satuan_kecil ?? '')); + $konversi = (int) ($jenis->konversi_kecil ?? 1); + if ($konversi <= 0) { + $konversi = 1; + } + $hasBreakdown = ($satuanKecil !== '' && $konversi > 1); + + return [ + 'has_breakdown' => $hasBreakdown, + 'konversi' => $konversi, + 'satuan_kecil' => $satuanKecil, + ]; + } + + public function calculateBaseQty(SIMBHPJenis $jenis, int $qtyInput, string $satuanTransaksi = 'besar'): int + { + $qtyInput = (int) $qtyInput; + if ($qtyInput < 0) { + $qtyInput = 0; + } + + $setting = $this->getUnitSetting($jenis); + $konversi = (int) ($setting['konversi'] ?? 1); + if ($konversi <= 0) { + $konversi = 1; + } + + if ($satuanTransaksi === 'kecil') { + return $qtyInput; + } + + return $qtyInput * $konversi; + } + + public function getStockBaseByJenis(string $jenisNama): int + { + $jenisNama = trim((string) $jenisNama); + if ($jenisNama === '') { + return 0; + } + + $row = DB::table('simbhpreport') + ->where('jenis', $jenisNama) + ->selectRaw("SUM(CASE WHEN pemasukan IS NOT NULL AND pemasukan > 0 THEN COALESCE(qty_base, pemasukan) ELSE 0 END) AS masuk_base") + ->selectRaw("SUM(CASE WHEN pengeluaran IS NOT NULL AND pengeluaran > 0 THEN COALESCE(qty_base, pengeluaran) ELSE 0 END) AS keluar_base") + ->first(); + + $masuk = (int) ($row->masuk_base ?? 0); + $keluar = (int) ($row->keluar_base ?? 0); + + return $masuk - $keluar; + } + + /** + * @param array $jenisNames + * @return array map jenis => saldo_base + */ + public function getStockBaseByJenisMany(array $jenisNames): array + { + $jenisNames = array_values(array_filter(array_map(function ($v) { + return trim((string) $v); + }, $jenisNames))); + + if (count($jenisNames) === 0) { + return []; + } + + $rows = DB::table('simbhpreport') + ->whereIn('jenis', $jenisNames) + ->select('jenis') + ->selectRaw("SUM(CASE WHEN pemasukan IS NOT NULL AND pemasukan > 0 THEN COALESCE(qty_base, pemasukan) ELSE 0 END) AS masuk_base") + ->selectRaw("SUM(CASE WHEN pengeluaran IS NOT NULL AND pengeluaran > 0 THEN COALESCE(qty_base, pengeluaran) ELSE 0 END) AS keluar_base") + ->groupBy('jenis') + ->get(); + + $map = []; + foreach ($rows as $row) { + $jenis = (string) ($row->jenis ?? ''); + if ($jenis === '') { + continue; + } + $map[$jenis] = ((int) ($row->masuk_base ?? 0)) - ((int) ($row->keluar_base ?? 0)); + } + + foreach ($jenisNames as $jenis) { + if (!array_key_exists($jenis, $map)) { + $map[$jenis] = 0; + } + } + + return $map; + } + + public function formatStockDisplay(int $saldoBase, string $satuanBesar, string $satuanKecil, int $konversi): string + { + $saldoBase = (int) $saldoBase; + $konversi = (int) $konversi; + if ($konversi <= 1 || trim($satuanKecil) === '') { + return number_format($saldoBase, 0, '.', ',') . ' ' . $satuanBesar; + } + + $besar = intdiv($saldoBase, $konversi); + $kecil = $saldoBase % $konversi; + + return number_format($besar, 0, '.', ',') . ' ' . $satuanBesar . ' + ' . number_format($kecil, 0, '.', ',') . ' ' . $satuanKecil; + } +} diff --git a/htdocs/composer.json b/htdocs/composer.json index 32e619a6..b04a9608 100644 --- a/htdocs/composer.json +++ b/htdocs/composer.json @@ -16,6 +16,7 @@ "laravel/passport": "^11.10", "laravel/sanctum": "^3.3", "laravel/tinker": "^2.8", + "livewire/livewire": "^3.5", "onecentlin/laravel-adminer": "^7.0", "opcodesio/log-viewer": "^3.21", "phpoffice/phpspreadsheet": "^3.3", diff --git a/htdocs/composer.lock b/htdocs/composer.lock index fd878a1e..4bb17fe8 100644 --- a/htdocs/composer.lock +++ b/htdocs/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "22c48f025e8e682420fe5c8bb1eb55e3", + "content-hash": "8a29deb08f7a54f984f29f89ce4209d4", "packages": [ { "name": "aamdsam/bridging-bpjs", @@ -2995,6 +2995,82 @@ ], "time": "2026-01-15T06:54:53+00:00" }, + { + "name": "livewire/livewire", + "version": "v3.7.11", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "addd6e8e9234df75f29e6a327ee2a745a7d67bb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/addd6e8e9234df75f29e6a327ee2a745a7d67bb6", + "reference": "addd6e8e9234df75f29e6a327ee2a745a7d67bb6", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0|^11.0|^12.0|^13.0", + "illuminate/routing": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/validation": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "league/mime-type-detection": "^1.9", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-kernel": "^6.2|^7.0|^8.0" + }, + "require-dev": { + "calebporzio/sushi": "^2.1", + "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0", + "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0", + "phpunit/phpunit": "^10.4|^11.5|^12.5", + "psy/psysh": "^0.11.22|^0.12" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Livewire": "Livewire\\Livewire" + }, + "providers": [ + "Livewire\\LivewireServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "A front-end framework for Laravel.", + "support": { + "issues": "https://github.com/livewire/livewire/issues", + "source": "https://github.com/livewire/livewire/tree/v3.7.11" + }, + "funding": [ + { + "url": "https://github.com/livewire", + "type": "github" + } + ], + "time": "2026-02-26T00:58:19+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.2.1", @@ -10951,5 +11027,5 @@ "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/htdocs/database/migrations/2026_03_13_000010_alter_simbhpjenis_add_barcode_fields.php b/htdocs/database/migrations/2026_03_13_000010_alter_simbhpjenis_add_barcode_fields.php new file mode 100644 index 00000000..d22f764f --- /dev/null +++ b/htdocs/database/migrations/2026_03_13_000010_alter_simbhpjenis_add_barcode_fields.php @@ -0,0 +1,78 @@ +string('barcode_besar', 20)->nullable()->after('kodejenis'); + } + if (!Schema::hasColumn('simbhpjenis', 'barcode_kecil')) { + $table->string('barcode_kecil', 20)->nullable()->after('barcode_besar'); + } + }); + + if (!Schema::hasColumn('simbhpjenis', 'barcode_besar')) { + return; + } + + DB::table('simbhpjenis') + ->orderBy('id', 'ASC') + ->chunkById(200, function ($rows) { + foreach ($rows as $row) { + $id = (int) ($row->id ?? 0); + if ($id <= 0) { + continue; + } + + $idPart = str_pad((string) $id, 8, '0', STR_PAD_LEFT); + $barcodeBesar = '91' . $idPart . '1'; + + $existingBesar = isset($row->barcode_besar) ? trim((string) $row->barcode_besar) : ''; + $existingKecil = isset($row->barcode_kecil) ? trim((string) $row->barcode_kecil) : ''; + + $satuanKecil = isset($row->satuan_kecil) ? trim((string) $row->satuan_kecil) : ''; + $konversi = (int) ($row->konversi_kecil ?? 1); + if ($konversi <= 0) { + $konversi = 1; + } + $hasBreakdown = ($satuanKecil !== '' && $konversi > 1); + + $update = []; + if ($existingBesar === '') { + $update['barcode_besar'] = $barcodeBesar; + } + + if ($hasBreakdown) { + $barcodeKecil = '91' . $idPart . '2'; + if ($existingKecil === '') { + $update['barcode_kecil'] = $barcodeKecil; + } + } + + if (count($update) > 0) { + DB::table('simbhpjenis')->where('id', $id)->update($update); + } + } + }, 'id'); + } + + public function down(): void + { + Schema::table('simbhpjenis', function (Blueprint $table) { + if (Schema::hasColumn('simbhpjenis', 'barcode_kecil')) { + $table->dropColumn('barcode_kecil'); + } + if (Schema::hasColumn('simbhpjenis', 'barcode_besar')) { + $table->dropColumn('barcode_besar'); + } + }); + } +}; + diff --git a/htdocs/resources/views/admin/gudang.blade.php b/htdocs/resources/views/admin/gudang.blade.php index 1db87727..f02662b8 100644 --- a/htdocs/resources/views/admin/gudang.blade.php +++ b/htdocs/resources/views/admin/gudang.blade.php @@ -15,77 +15,148 @@ -
-
-
-
-
- - - - - -
-

{{ Session('nama') }}

-

{{ Session('previlage') }}

-

{{ config('global.Title') }} | {{ config('global.namaapps') }}

-
-
-
-
-
-
- -

{{ $masuk ?? 0 }}

- Barang Masuk
- Add -
-
-
-
- -

{{ $keluar ?? 0 }}

- Barang keluar
- Add -
-
-
-
- -

{{ count($warningstok ?? []) }}

- Warning Stok Menipis
- Lihat -
-
-
-
-
-
-
-
-
+
+ @livewire('gudang-pos') +
+ +
+
+
+
+

Barang Masuk (Stok Opname)

+
+ + +
+
+ + +
+
+ + +
+
+ + + Kode internal barang (bukan barcode). Otomatis terisi jika pilih barang dari list. +
+
+ + + Barcode dibedakan: satuan besar vs satuan kecil (nilai barcode berbeda). +
+ +
+ + + Scan akan otomatis memilih barang & set satuan (besar/kecil) sesuai barcode. +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+

Daftar Barang Masuk

+
+ + + + + + + + + + + + + + +
KodeBarangQtySatuanExpiredAksi
Belum ada item.
+
+ Catatan: sistem akan menyimpan per-item menjadi baris transaksi (mirip stok opname). +
+
+
+
+ +
-
Data Bulan {{date("m")}} Tahun {{date("Y")}}
+
Report Transaksi

@@ -124,70 +195,104 @@
-
Sisa Barang
+
Master Barang / Stok

-
-
- - - - - - - - - - - - - - - @foreach(($jenisRows ?? []) as $row) - - - - - - - - - - - @endforeach - -
JenisSatuanSatuan KecilKonversiStok MinSisaWarningEdit
{{ $row['jenis'] }}{{ $row['satuan'] }}{{ $row['satuan_kecil'] }}{{ number_format($row['konversi_kecil'], 0, '.', ',') }}{{ number_format($row['stok_minimum'], 0, '.', ',') }}{{ $row['saldo'] }}{{ $row['warning'] }} - -
-
-
-
-
-
-
-
+
+
+ + + + + + + + + + + + + + + + + + @foreach(($jenisRows ?? []) as $row) + @php($kode = ($row['kodejenis'] ?? '') ?: preg_replace('/\s+/', '', (string) ($row['jenis'] ?? ''))) + @php($hasBreakdown = trim((string)($row['satuan_kecil'] ?? '')) !== '' && (int)($row['konversi_kecil'] ?? 1) > 1) + + + + + + + + + + + + + + @endforeach + +
KodeJenisSatuanSatuan KecilKonversiStok MinSisaWarningBarcodeDetailEdit
{{ $kode }}{{ $row['jenis'] }}{{ $row['satuan'] }}{{ $row['satuan_kecil'] }}{{ number_format($row['konversi_kecil'], 0, '.', ',') }}{{ number_format($row['stok_minimum'], 0, '.', ',') }}{{ $row['saldo'] }}{{ $row['warning'] }} + + @if($hasBreakdown) + + @endif + + + + +
+
+
+
+
+
+
Statistik Harian (Base Unit)
-
Masuk: {{ number_format($stat_harian_masuk ?? 0, 0, '.', ',') }}
-
Keluar: {{ number_format($stat_harian_keluar ?? 0, 0, '.', ',') }}
+
Masuk: {{ number_format($stat_harian_masuk ?? 0, 0, '.', ',') }}
+
Keluar: {{ number_format($stat_harian_keluar ?? 0, 0, '.', ',') }}

- + @forelse($stat_perjenis_harian ?? [] as $row) @empty @@ -201,13 +306,13 @@
Statistik Bulanan (Base Unit)
-
Masuk: {{ number_format($stat_bulanan_masuk ?? 0, 0, '.', ',') }}
-
Keluar: {{ number_format($stat_bulanan_keluar ?? 0, 0, '.', ',') }}
+
Masuk: {{ number_format($stat_bulanan_masuk ?? 0, 0, '.', ',') }}
+
Keluar: {{ number_format($stat_bulanan_keluar ?? 0, 0, '.', ',') }}

JenisMasukKeluar
{{ $row['jenis'] }}{{ number_format($row['masuk'],0,'.',',') }}{{ number_format($row['keluar'],0,'.',',') }}
- + @forelse($stat_perjenis_bulanan ?? [] as $row) @empty @@ -221,13 +326,13 @@
Statistik Tahunan (Base Unit)
-
Masuk: {{ number_format($stat_tahunan_masuk ?? 0, 0, '.', ',') }}
-
Keluar: {{ number_format($stat_tahunan_keluar ?? 0, 0, '.', ',') }}
+
Masuk: {{ number_format($stat_tahunan_masuk ?? 0, 0, '.', ',') }}
+
Keluar: {{ number_format($stat_tahunan_keluar ?? 0, 0, '.', ',') }}

JenisMasukKeluar
{{ $row['jenis'] }}{{ number_format($row['masuk'],0,'.',',') }}{{ number_format($row['keluar'],0,'.',',') }}
- + @forelse($stat_perjenis_tahunan ?? [] as $row) @empty @@ -239,29 +344,26 @@ - -
-
-
+ +
+

Warning Barang Segera Habis

- @if(isset($warningstok) && count($warningstok) > 0) - @foreach($warningstok as $w) -
- {{ $w['jenis'] }} tersisa {{ $w['saldo'] }}. - Stok minimum: {{ number_format($w['minimum'], 0, '.', ',') }} {{ $w['satuan_kecil'] }}. -
- @endforeach - @else -
Semua stok masih aman.
- @endif +
+ @if(isset($warningstok) && count($warningstok) > 0) + @foreach($warningstok as $w) +
+ {{ $w['jenis'] }} tersisa {{ $w['saldo'] }}. + Stok minimum: {{ number_format($w['minimum'], 0, '.', ',') }} {{ $w['satuan_kecil'] }}. +
+ @endforeach + @else +
Semua stok masih aman.
+ @endif +
-
-
-
-
-
+

Barang Mendekati Expired (H-30)

@if(isset($expiringSoon) && count($expiringSoon) > 0) @@ -278,7 +380,7 @@
- + @foreach($expiringSoon as $e) @@ -315,10 +417,20 @@ @@ -489,6 +603,58 @@ +
JenisMasukKeluar
{{ $row['jenis'] }}{{ number_format($row['masuk'],0,'.',',') }}{{ number_format($row['keluar'],0,'.',',') }}
Sisa Hari
{{ $e['jenis'] }}
+ + + + + + +
Kode-
Nama-
Satuan-
Satuan Kecil-
Konversi-
Stok-
+
+
+
+
+ + +
+ +
+ Barcode: - +
+
+ +
+ Format: CODE128 +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
@@ -496,14 +662,17 @@ @endsection @push('script') +