update: pisah tabel stock activity, refactor controller & report, tambah integrasi WhatsApp gateway, perbaikan responsive layout untuk mobile
This commit is contained in:
parent
6fcb4b445e
commit
7a4c70bee2
|
|
@ -64,3 +64,8 @@ AWS_BUCKET=
|
|||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
WHATSAPP_GATEWAY_ENDPOINT=https://api.fonnte.com/send
|
||||
WHATSAPP_GATEWAY_TOKEN=
|
||||
WHATSAPP_TARGET_NUMBER=6281234567890
|
||||
WHATSAPP_TARGET_NUMBERS=6281234567890,6289876543210
|
||||
|
|
|
|||
|
|
@ -46,6 +46,17 @@ public function index(Request $request): View
|
|||
->latest(StockMovement::columnDibuat())
|
||||
->limit(6)
|
||||
->get();
|
||||
$pesanWhatsAppStokMinimum = "Peringatan Stok Minimum\n\n";
|
||||
if ($produkStokMenipis->isNotEmpty()) {
|
||||
$pesanWhatsAppStokMinimum .= "Barang berikut perlu segera direstock:\n";
|
||||
foreach ($produkStokMenipis as $produk) {
|
||||
$pesanWhatsAppStokMinimum .= "- {$produk->nama}: stok {$produk->stok}, minimum {$produk->stok_minimum}\n";
|
||||
}
|
||||
$pesanWhatsAppStokMinimum .= "\nMohon segera dilakukan pengadaan barang.";
|
||||
} else {
|
||||
$pesanWhatsAppStokMinimum .= 'Semua stok barang masih aman.';
|
||||
}
|
||||
$nomorPenerimaWhatsApp = collect(config('services.whatsapp.target_numbers', []));
|
||||
$stokAman = max(0, $totalProduk - $stokMenipis);
|
||||
$persentaseStokAman = $totalProduk > 0 ? (int) round(($stokAman / $totalProduk) * 100) : 100;
|
||||
$topPergerakanProduk = StockMovement::with('product')
|
||||
|
|
@ -164,6 +175,8 @@ public function index(Request $request): View
|
|||
'perubahanBarangKeluar' => $perubahanBarangKeluar,
|
||||
'produkStokMenipis' => $produkStokMenipis,
|
||||
'aktivitasTerbaru' => $aktivitasTerbaru,
|
||||
'pesanWhatsAppStokMinimum' => $pesanWhatsAppStokMinimum,
|
||||
'nomorPenerimaWhatsApp' => $nomorPenerimaWhatsApp,
|
||||
'persentaseStokAman' => $persentaseStokAman,
|
||||
'topPergerakanProduk' => $topPergerakanProduk,
|
||||
'produkTercepatTerjual' => $produkTercepatTerjual,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ProductController extends Controller
|
|||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
$this->ensureAdminOrOwner();
|
||||
|
||||
$opsiUrut = $request->input('urut', 'nama_asc');
|
||||
$query = Product::query();
|
||||
|
|
@ -38,8 +38,11 @@ public function store(Request $request): RedirectResponse
|
|||
'nama' => ['required', 'string', 'max:255'],
|
||||
'kategori' => ['nullable', 'string', 'max:100'],
|
||||
'stok' => ['required', 'integer', 'min:0'],
|
||||
'stok_minimum' => ['required', 'integer', 'min:0'],
|
||||
'deskripsi' => ['nullable', 'string'],
|
||||
'stok_minimum' => ['required', 'integer', 'min:0', 'lte:stok'],
|
||||
'keterangan' => ['nullable', 'string'],
|
||||
], [
|
||||
'kode.unique' => 'Kode barang sudah digunakan, gunakan kode lain.',
|
||||
'stok_minimum.lte' => 'Stok minimum tidak boleh lebih besar dari stok barang.',
|
||||
]);
|
||||
|
||||
Product::create($validated);
|
||||
|
|
@ -56,8 +59,11 @@ public function update(Request $request, Product $product): RedirectResponse
|
|||
'nama' => ['required', 'string', 'max:255'],
|
||||
'kategori' => ['nullable', 'string', 'max:100'],
|
||||
'stok' => ['required', 'integer', 'min:0'],
|
||||
'stok_minimum' => ['required', 'integer', 'min:0'],
|
||||
'deskripsi' => ['nullable', 'string'],
|
||||
'stok_minimum' => ['required', 'integer', 'min:0', 'lte:stok'],
|
||||
'keterangan' => ['nullable', 'string'],
|
||||
], [
|
||||
'kode.unique' => 'Kode barang sudah digunakan, gunakan kode lain.',
|
||||
'stok_minimum.lte' => 'Stok minimum tidak boleh lebih besar dari stok barang.',
|
||||
]);
|
||||
|
||||
$product->update($validated);
|
||||
|
|
@ -78,4 +84,10 @@ private function ensureAdmin(): void
|
|||
{
|
||||
abort_unless(auth()->user()?->role === 'admin', 403);
|
||||
}
|
||||
|
||||
private function ensureAdminOrOwner(): void
|
||||
{
|
||||
abort_unless(in_array(auth()->user()?->role, ['admin', 'owner'], true), 403);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\LostStock;
|
||||
use App\Models\StockMovement;
|
||||
use Carbon\Carbon;
|
||||
use Dompdf\Dompdf;
|
||||
|
|
@ -21,12 +22,7 @@ public function pembelian(Request $request): View
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_MASUK)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(StockMovement::columnDibuat())
|
||||
->get();
|
||||
$data = $this->detailMutasi(StockMovement::KATEGORI_MASUK, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return view('reports.pembelian', [
|
||||
'tanggalMulai' => $tanggalMulai,
|
||||
|
|
@ -41,12 +37,7 @@ public function penjualan(Request $request): View
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_KELUAR)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(StockMovement::columnDibuat())
|
||||
->get();
|
||||
$data = $this->detailMutasi(StockMovement::KATEGORI_KELUAR, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return view('reports.penjualan', [
|
||||
'tanggalMulai' => $tanggalMulai,
|
||||
|
|
@ -61,14 +52,7 @@ public function rekapPembelian(Request $request): View
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with('product')
|
||||
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
|
||||
->kategori(StockMovement::KATEGORI_MASUK)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->groupBy('id_barang')
|
||||
->orderByDesc('total_jumlah')
|
||||
->get();
|
||||
$data = $this->rekapMutasi(StockMovement::KATEGORI_MASUK, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return view('reports.rekap-pembelian', [
|
||||
'tanggalMulai' => $tanggalMulai,
|
||||
|
|
@ -83,14 +67,7 @@ public function rekapPenjualan(Request $request): View
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with('product')
|
||||
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
|
||||
->kategori(StockMovement::KATEGORI_KELUAR)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->groupBy('id_barang')
|
||||
->orderByDesc('total_jumlah')
|
||||
->get();
|
||||
$data = $this->rekapMutasi(StockMovement::KATEGORI_KELUAR, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return view('reports.rekap-penjualan', [
|
||||
'tanggalMulai' => $tanggalMulai,
|
||||
|
|
@ -105,12 +82,7 @@ public function barangHilang(Request $request): View
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_HILANG)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(StockMovement::columnDibuat())
|
||||
->get();
|
||||
$data = $this->detailBarangHilang($tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return view('reports.barang-hilang', [
|
||||
'tanggalMulai' => $tanggalMulai,
|
||||
|
|
@ -125,24 +97,19 @@ public function exportPembelian(Request $request)
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_MASUK)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(StockMovement::columnDibuat())
|
||||
->get();
|
||||
$data = $this->detailMutasi(StockMovement::KATEGORI_MASUK, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return $this->exportReport(
|
||||
$request,
|
||||
'Laporan Pembelian',
|
||||
$data,
|
||||
['Tanggal', 'Produk', 'Jumlah', 'User', 'Keterangan'],
|
||||
['Waktu', 'Barang', 'Jumlah', 'Keterangan', 'Input Oleh'],
|
||||
fn ($item) => [
|
||||
$item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '-',
|
||||
$item->created_at?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-',
|
||||
$item->product->nama ?? '-',
|
||||
$item->jumlah,
|
||||
$item->user->name ?? '-',
|
||||
$item->keterangan ?? '-',
|
||||
$item->user->name ?? '-',
|
||||
],
|
||||
'laporan-pembelian-' . now()->format('YmdHis')
|
||||
);
|
||||
|
|
@ -153,24 +120,19 @@ public function exportPenjualan(Request $request)
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_KELUAR)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(StockMovement::columnDibuat())
|
||||
->get();
|
||||
$data = $this->detailMutasi(StockMovement::KATEGORI_KELUAR, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return $this->exportReport(
|
||||
$request,
|
||||
'Laporan Penjualan',
|
||||
$data,
|
||||
['Tanggal', 'Produk', 'Jumlah', 'User', 'Keterangan'],
|
||||
['Waktu', 'Barang', 'Jumlah', 'Keterangan', 'Input Oleh'],
|
||||
fn ($item) => [
|
||||
$item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '-',
|
||||
$item->created_at?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-',
|
||||
$item->product->nama ?? '-',
|
||||
$item->jumlah,
|
||||
$item->user->name ?? '-',
|
||||
$item->keterangan ?? '-',
|
||||
$item->user->name ?? '-',
|
||||
],
|
||||
'laporan-penjualan-' . now()->format('YmdHis')
|
||||
);
|
||||
|
|
@ -181,24 +143,27 @@ public function exportRekapPembelian(Request $request)
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with('product')
|
||||
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
|
||||
->kategori(StockMovement::KATEGORI_MASUK)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->groupBy('id_barang')
|
||||
->orderByDesc('total_jumlah')
|
||||
->get();
|
||||
$data = $this->rekapMutasi(StockMovement::KATEGORI_MASUK, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return $this->exportReport(
|
||||
$request,
|
||||
'Rekap Pembelian',
|
||||
$data,
|
||||
['Produk', 'Total Qty', 'Total Transaksi'],
|
||||
['Kode', 'Barang', 'Kategori', 'Stok Saat Ini', 'Total Qty', 'Aktivitas', 'Rata-rata', 'Qty Terkecil', 'Qty Terbesar', 'Aktivitas Pertama', 'Aktivitas Terakhir', 'Input Oleh', 'Keterangan'],
|
||||
fn ($item) => [
|
||||
$item->product->kode ?? '-',
|
||||
$item->product->nama ?? '-',
|
||||
$item->product->kategori ?? '-',
|
||||
(int) ($item->product->stok ?? 0),
|
||||
(int) $item->total_jumlah,
|
||||
(int) $item->total_transaksi,
|
||||
number_format((float) $item->rata_rata, 2, ',', '.'),
|
||||
(int) $item->jumlah_terkecil,
|
||||
(int) $item->jumlah_terbesar,
|
||||
$item->aktivitas_pertama?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-',
|
||||
$item->aktivitas_terakhir?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-',
|
||||
$item->input_oleh,
|
||||
$item->keterangan,
|
||||
],
|
||||
'rekap-pembelian-' . now()->format('YmdHis')
|
||||
);
|
||||
|
|
@ -209,24 +174,27 @@ public function exportRekapPenjualan(Request $request)
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with('product')
|
||||
->selectRaw('id_barang, SUM(jumlah) as total_jumlah, COUNT(*) as total_transaksi')
|
||||
->kategori(StockMovement::KATEGORI_KELUAR)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->groupBy('id_barang')
|
||||
->orderByDesc('total_jumlah')
|
||||
->get();
|
||||
$data = $this->rekapMutasi(StockMovement::KATEGORI_KELUAR, $tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return $this->exportReport(
|
||||
$request,
|
||||
'Rekap Penjualan',
|
||||
$data,
|
||||
['Produk', 'Total Qty', 'Total Transaksi'],
|
||||
['Kode', 'Barang', 'Kategori', 'Stok Saat Ini', 'Total Qty', 'Aktivitas', 'Rata-rata', 'Qty Terkecil', 'Qty Terbesar', 'Aktivitas Pertama', 'Aktivitas Terakhir', 'Input Oleh', 'Keterangan'],
|
||||
fn ($item) => [
|
||||
$item->product->kode ?? '-',
|
||||
$item->product->nama ?? '-',
|
||||
$item->product->kategori ?? '-',
|
||||
(int) ($item->product->stok ?? 0),
|
||||
(int) $item->total_jumlah,
|
||||
(int) $item->total_transaksi,
|
||||
number_format((float) $item->rata_rata, 2, ',', '.'),
|
||||
(int) $item->jumlah_terkecil,
|
||||
(int) $item->jumlah_terbesar,
|
||||
$item->aktivitas_pertama?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-',
|
||||
$item->aktivitas_terakhir?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-',
|
||||
$item->input_oleh,
|
||||
$item->keterangan,
|
||||
],
|
||||
'rekap-penjualan-' . now()->format('YmdHis')
|
||||
);
|
||||
|
|
@ -237,12 +205,7 @@ public function exportBarangHilang(Request $request)
|
|||
$this->ensureReportAccess();
|
||||
[$tanggalMulai, $tanggalSelesai] = $this->resolveTanggal($request);
|
||||
|
||||
$data = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_HILANG)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(StockMovement::columnDibuat())
|
||||
->get();
|
||||
$data = $this->detailBarangHilang($tanggalMulai, $tanggalSelesai);
|
||||
|
||||
return $this->exportReport(
|
||||
$request,
|
||||
|
|
@ -251,9 +214,9 @@ public function exportBarangHilang(Request $request)
|
|||
['Tanggal', 'Produk', 'Jumlah', 'User', 'Keterangan'],
|
||||
fn ($item) => [
|
||||
$item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '-',
|
||||
$item->product->nama ?? '-',
|
||||
$item->barang ?? $item->product->nama ?? '-',
|
||||
$item->jumlah,
|
||||
$item->user->name ?? '-',
|
||||
$item->input_oleh ?? $item->user->name ?? '-',
|
||||
$item->keterangan ?? '-',
|
||||
],
|
||||
'barang-hilang-' . now()->format('YmdHis')
|
||||
|
|
@ -274,6 +237,59 @@ private function exportReport(Request $request, string $title, iterable $data, a
|
|||
return $this->exportExcel($title, $filenameBase . '.xls', $columns, $rows);
|
||||
}
|
||||
|
||||
private function detailMutasi(string $kategori, ?string $tanggalMulai, ?string $tanggalSelesai)
|
||||
{
|
||||
return StockMovement::with(['product', 'user'])
|
||||
->kategori($kategori)
|
||||
->when($tanggalMulai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where($this->kolomWaktuMutasi(), '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(StockMovement::columnDibuat())
|
||||
->get();
|
||||
}
|
||||
|
||||
private function detailBarangHilang(?string $tanggalMulai, ?string $tanggalSelesai)
|
||||
{
|
||||
return LostStock::with(['product', 'user'])
|
||||
->when($tanggalMulai, fn ($q) => $q->where('waktu', '>=', $this->mulaiHariDiJakarta($tanggalMulai)))
|
||||
->when($tanggalSelesai, fn ($q) => $q->where('waktu', '<=', $this->akhirHariDiJakarta($tanggalSelesai)))
|
||||
->latest(LostStock::CREATED_AT)
|
||||
->get();
|
||||
}
|
||||
|
||||
private function rekapMutasi(string $kategori, ?string $tanggalMulai, ?string $tanggalSelesai)
|
||||
{
|
||||
return $this->detailMutasi($kategori, $tanggalMulai, $tanggalSelesai)
|
||||
->groupBy('id_barang')
|
||||
->map(function ($items) {
|
||||
$sorted = $items->sortBy(fn ($item) => $item->created_at?->timestamp ?? 0);
|
||||
$keterangan = $items->pluck('keterangan')
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->implode('; ');
|
||||
$inputOleh = $items->map(fn ($item) => $item->user?->name)
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->implode(', ');
|
||||
|
||||
return (object) [
|
||||
'product' => $items->first()->product,
|
||||
'total_jumlah' => (int) $items->sum('jumlah'),
|
||||
'total_transaksi' => $items->count(),
|
||||
'rata_rata' => (float) $items->avg('jumlah'),
|
||||
'jumlah_terkecil' => (int) $items->min('jumlah'),
|
||||
'jumlah_terbesar' => (int) $items->max('jumlah'),
|
||||
'aktivitas_pertama' => $sorted->first()?->created_at,
|
||||
'aktivitas_terakhir' => $sorted->last()?->created_at,
|
||||
'input_oleh' => $inputOleh !== '' ? $inputOleh : '-',
|
||||
'keterangan' => $keterangan !== '' ? $keterangan : '-',
|
||||
];
|
||||
})
|
||||
->sortByDesc('total_jumlah')
|
||||
->values();
|
||||
}
|
||||
|
||||
private function exportExcel(string $title, string $filename, array $columns, array $rows)
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\IncomingStock;
|
||||
use App\Models\LostStock;
|
||||
use App\Models\OutgoingStock;
|
||||
use App\Models\StockMovement;
|
||||
use App\Models\StockActivity;
|
||||
use App\Services\WhatsAppStockNotifier;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
|
@ -13,6 +18,10 @@
|
|||
|
||||
class StockMovementController extends Controller
|
||||
{
|
||||
public function __construct(private readonly WhatsAppStockNotifier $whatsAppStockNotifier)
|
||||
{
|
||||
}
|
||||
|
||||
// public function incoming(): View
|
||||
// {
|
||||
// $this->ensureAdmin();
|
||||
|
|
@ -29,18 +38,17 @@ class StockMovementController extends Controller
|
|||
|
||||
public function incoming(Request $request): View
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
$this->ensureAdminOrOwner();
|
||||
|
||||
$query = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_MASUK)
|
||||
->latest(StockMovement::columnDibuat());
|
||||
$query = IncomingStock::with(['product', 'user'])
|
||||
->latest(IncomingStock::CREATED_AT);
|
||||
|
||||
if ($request->filled('tanggal_mulai')) {
|
||||
$query->whereDate(StockMovement::columnDibuat(), '>=', $request->tanggal_mulai);
|
||||
$query->whereDate(IncomingStock::CREATED_AT, '>=', $request->tanggal_mulai);
|
||||
}
|
||||
|
||||
if ($request->filled('tanggal_selesai')) {
|
||||
$query->whereDate(StockMovement::columnDibuat(), '<=', $request->tanggal_selesai);
|
||||
$query->whereDate(IncomingStock::CREATED_AT, '<=', $request->tanggal_selesai);
|
||||
}
|
||||
|
||||
if (!$request->filled('tanggal_mulai') && !$request->filled('tanggal_selesai')) {
|
||||
|
|
@ -69,18 +77,17 @@ public function incoming(Request $request): View
|
|||
|
||||
public function outgoing(Request $request): View
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
$this->ensureAdminOrOwner();
|
||||
|
||||
$query = StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_KELUAR)
|
||||
->latest(StockMovement::columnDibuat());
|
||||
$query = OutgoingStock::with(['product', 'user'])
|
||||
->latest(OutgoingStock::CREATED_AT);
|
||||
|
||||
if ($request->filled('tanggal_mulai')) {
|
||||
$query->whereDate(StockMovement::columnDibuat(), '>=', $request->tanggal_mulai);
|
||||
$query->whereDate(OutgoingStock::CREATED_AT, '>=', $request->tanggal_mulai);
|
||||
}
|
||||
|
||||
if ($request->filled('tanggal_selesai')) {
|
||||
$query->whereDate(StockMovement::columnDibuat(), '<=', $request->tanggal_selesai);
|
||||
$query->whereDate(OutgoingStock::CREATED_AT, '<=', $request->tanggal_selesai);
|
||||
}
|
||||
|
||||
if (!$request->filled('tanggal_mulai') && !$request->filled('tanggal_selesai')) {
|
||||
|
|
@ -99,9 +106,8 @@ public function lost(): View
|
|||
|
||||
return view('mutasi-stok.barang-hilang', [
|
||||
'products' => Product::orderBy('nama')->get(),
|
||||
'movements' => StockMovement::with(['product', 'user'])
|
||||
->kategori(StockMovement::KATEGORI_HILANG)
|
||||
->latest(StockMovement::columnDibuat())
|
||||
'movements' => LostStock::with(['product', 'user'])
|
||||
->latest(LostStock::CREATED_AT)
|
||||
->limit(50)
|
||||
->get(),
|
||||
]);
|
||||
|
|
@ -120,7 +126,20 @@ public function storeOutgoing(Request $request): RedirectResponse
|
|||
{
|
||||
$this->ensureAdmin();
|
||||
|
||||
$this->saveMovement($request, 'keluar');
|
||||
$minimumStockNotificationSent = $this->saveMovement($request, 'keluar');
|
||||
|
||||
if ($minimumStockNotificationSent === true) {
|
||||
return redirect()
|
||||
->route('stock-movements.outgoing')
|
||||
->with('success', 'Barang keluar berhasil disimpan dan notifikasi stok minimum berhasil dikirim.');
|
||||
}
|
||||
|
||||
if ($minimumStockNotificationSent === false) {
|
||||
return redirect()
|
||||
->route('stock-movements.outgoing')
|
||||
->with('success', 'Barang keluar berhasil disimpan.')
|
||||
->with('warning', 'Notifikasi WhatsApp stok minimum gagal dikirim. Pastikan perangkat WhatsApp gateway terhubung.');
|
||||
}
|
||||
|
||||
return redirect()->route('stock-movements.outgoing')->with('success', 'Barang keluar berhasil disimpan.');
|
||||
}
|
||||
|
|
@ -129,24 +148,61 @@ public function storeLost(Request $request): RedirectResponse
|
|||
{
|
||||
$this->ensureAdmin();
|
||||
|
||||
$this->saveMovement($request, 'keluar', true);
|
||||
$minimumStockNotificationSent = $this->saveMovement($request, 'keluar', true);
|
||||
|
||||
if ($minimumStockNotificationSent === true) {
|
||||
return redirect()
|
||||
->route('stock-movements.lost')
|
||||
->with('success', 'Input barang hilang berhasil disimpan dan notifikasi stok minimum berhasil dikirim.');
|
||||
}
|
||||
|
||||
if ($minimumStockNotificationSent === false) {
|
||||
return redirect()
|
||||
->route('stock-movements.lost')
|
||||
->with('success', 'Input barang hilang berhasil disimpan.')
|
||||
->with('warning', 'Notifikasi WhatsApp stok minimum gagal dikirim. Pastikan perangkat WhatsApp gateway terhubung.');
|
||||
}
|
||||
|
||||
return redirect()->route('stock-movements.lost')->with('success', 'Input barang hilang berhasil disimpan.');
|
||||
}
|
||||
|
||||
private function saveMovement(Request $request, string $type, bool $isLost = false): void
|
||||
private function saveMovement(Request $request, string $type, bool $isLost = false): ?bool
|
||||
{
|
||||
$validated = $request->validate([
|
||||
$rules = [
|
||||
'id_barang' => ['required', 'exists:barang,id'],
|
||||
'jumlah' => ['required', 'integer', 'min:1'],
|
||||
'keterangan' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
];
|
||||
|
||||
DB::transaction(function () use ($validated, $type, $isLost): void {
|
||||
if (!$isLost) {
|
||||
$rules['kategori'] = ['required', 'string', 'max:100'];
|
||||
}
|
||||
|
||||
if ($type === 'masuk') {
|
||||
$rules['keterangan'] = ['nullable', 'string', 'max:255'];
|
||||
}
|
||||
|
||||
if ($type === 'keluar') {
|
||||
$rules['keterangan'] = ['required', 'string', 'max:255'];
|
||||
}
|
||||
|
||||
$validated = $request->validate($rules);
|
||||
$description = $this->movementDescription($validated, $type, $isLost);
|
||||
|
||||
$minimumStockAlertProduct = DB::transaction(function () use ($validated, $description, $type, $isLost): ?Product {
|
||||
$product = Product::lockForUpdate()->findOrFail($validated['id_barang']);
|
||||
|
||||
if (!$isLost && strtolower(trim((string) $product->kategori)) !== strtolower(trim($validated['kategori']))) {
|
||||
throw ValidationException::withMessages([
|
||||
'kategori' => 'Kategori yang dipilih tidak sesuai dengan barang.',
|
||||
]);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$waktu = now();
|
||||
$oldStock = (int) $product->stok;
|
||||
$newStock = $type === 'masuk'
|
||||
? $product->stok + $validated['jumlah']
|
||||
: $product->stok - $validated['jumlah'];
|
||||
? $oldStock + $validated['jumlah']
|
||||
: $oldStock - $validated['jumlah'];
|
||||
|
||||
if ($newStock < 0) {
|
||||
throw ValidationException::withMessages([
|
||||
|
|
@ -154,25 +210,80 @@ private function saveMovement(Request $request, string $type, bool $isLost = fal
|
|||
]);
|
||||
}
|
||||
|
||||
StockMovement::create([
|
||||
$movement = StockMovement::create([
|
||||
'id_barang' => $product->id,
|
||||
'tipe' => $type,
|
||||
'kategori' => $isLost
|
||||
? StockMovement::KATEGORI_HILANG
|
||||
: ($type === 'masuk' ? StockMovement::KATEGORI_MASUK : StockMovement::KATEGORI_KELUAR),
|
||||
'jumlah' => $validated['jumlah'],
|
||||
'keterangan' => $isLost
|
||||
? ($validated['keterangan'] ?? null)
|
||||
: ($validated['keterangan'] ?? null),
|
||||
'keterangan' => $description,
|
||||
'id_pengguna' => Auth::id(),
|
||||
]);
|
||||
|
||||
$activityModel = $this->activityModel($type, $isLost);
|
||||
|
||||
$activityModel::create([
|
||||
'id_barang' => $product->id,
|
||||
'id_mutasi_stok' => $movement->id,
|
||||
'waktu' => $waktu,
|
||||
'barang' => $product->nama,
|
||||
'jumlah' => $validated['jumlah'],
|
||||
'keterangan' => $description,
|
||||
'id_pengguna' => $user?->id,
|
||||
'input_oleh' => $user?->name,
|
||||
]);
|
||||
|
||||
$product->update(['stok' => $newStock]);
|
||||
|
||||
$shouldSendMinimumStockAlert = $type === 'keluar'
|
||||
&& $newStock <= (int) $product->stok_minimum;
|
||||
|
||||
if ($shouldSendMinimumStockAlert) {
|
||||
$product->stok = $newStock;
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
if ($minimumStockAlertProduct !== null) {
|
||||
return $this->whatsAppStockNotifier->sendMinimumStockAlert(
|
||||
$minimumStockAlertProduct,
|
||||
(int) $validated['jumlah']
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function movementDescription(array $validated, string $type, bool $isLost): ?string
|
||||
{
|
||||
return $validated['keterangan'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<StockActivity>
|
||||
*/
|
||||
private function activityModel(string $type, bool $isLost): string
|
||||
{
|
||||
if ($isLost) {
|
||||
return LostStock::class;
|
||||
}
|
||||
|
||||
return $type === 'masuk'
|
||||
? IncomingStock::class
|
||||
: OutgoingStock::class;
|
||||
}
|
||||
|
||||
private function ensureAdmin(): void
|
||||
{
|
||||
abort_unless(auth()->user()?->role === 'admin', 403);
|
||||
}
|
||||
|
||||
private function ensureAdminOrOwner(): void
|
||||
{
|
||||
abort_unless(in_array(auth()->user()?->role, ['admin', 'owner'], true), 403);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
||||
trait HasIndonesianTimestamps
|
||||
{
|
||||
public function getCreatedAtColumn(): ?string
|
||||
{
|
||||
return self::CREATED_AT;
|
||||
}
|
||||
|
||||
public function getUpdatedAtColumn(): ?string
|
||||
{
|
||||
return self::UPDATED_AT;
|
||||
}
|
||||
|
||||
protected function createdAt(): Attribute
|
||||
{
|
||||
return Attribute::get(function () {
|
||||
$raw = $this->attributes[self::CREATED_AT] ?? null;
|
||||
|
||||
if ($raw === null || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->asDateTime($raw);
|
||||
});
|
||||
}
|
||||
|
||||
protected function updatedAt(): Attribute
|
||||
{
|
||||
return Attribute::get(function () {
|
||||
$raw = $this->attributes[self::UPDATED_AT] ?? null;
|
||||
|
||||
if ($raw === null || $raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->asDateTime($raw);
|
||||
});
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'waktu' => 'datetime',
|
||||
self::CREATED_AT => 'datetime',
|
||||
self::UPDATED_AT => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class IncomingStock extends StockActivity
|
||||
{
|
||||
protected $table = 'barang_masuk';
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class LostStock extends StockActivity
|
||||
{
|
||||
protected $table = 'barang_hilang';
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class OutgoingStock extends StockActivity
|
||||
{
|
||||
protected $table = 'barang_keluar';
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ class Product extends Model
|
|||
'kategori',
|
||||
'stok',
|
||||
'stok_minimum',
|
||||
'deskripsi',
|
||||
'keterangan',
|
||||
];
|
||||
|
||||
public function stockMovements(): HasMany
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasIndonesianTimestamps;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
abstract class StockActivity extends Model
|
||||
{
|
||||
use HasIndonesianTimestamps;
|
||||
|
||||
public const CREATED_AT = 'dibuat_pada';
|
||||
|
||||
public const UPDATED_AT = 'diperbarui_pada';
|
||||
|
||||
protected $fillable = [
|
||||
'id_barang',
|
||||
'id_mutasi_stok',
|
||||
'waktu',
|
||||
'barang',
|
||||
'jumlah',
|
||||
'keterangan',
|
||||
'id_pengguna',
|
||||
'input_oleh',
|
||||
];
|
||||
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class, 'id_barang');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'id_pengguna');
|
||||
}
|
||||
|
||||
public function stockMovement(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(StockMovement::class, 'id_mutasi_stok');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WhatsAppStockNotifier
|
||||
{
|
||||
public function sendMinimumStockAlert(Product $product, ?int $outgoingQuantity = null): bool
|
||||
{
|
||||
$targets = config('services.whatsapp.target_numbers', []);
|
||||
$token = config('services.whatsapp.token');
|
||||
$endpoint = config('services.whatsapp.endpoint');
|
||||
|
||||
if ($targets === [] || !$token || !$endpoint) {
|
||||
Log::info('Notifikasi stok minimum WhatsApp belum dikirim karena konfigurasi belum lengkap.', [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->nama,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$message = $this->minimumStockMessage($product, $outgoingQuantity);
|
||||
$allSent = true;
|
||||
|
||||
foreach ($targets as $target) {
|
||||
try {
|
||||
$response = Http::timeout(10)->withHeaders([
|
||||
'Authorization' => $token,
|
||||
])->asForm()->post($endpoint, [
|
||||
'target' => $target,
|
||||
'message' => $message,
|
||||
]);
|
||||
$responseData = $response->json();
|
||||
|
||||
if (!$response->successful() || ($responseData !== null && ($responseData['status'] ?? true) === false)) {
|
||||
Log::warning('Notifikasi stok minimum WhatsApp gagal dikirim.', [
|
||||
'product_id' => $product->id,
|
||||
'target' => $target,
|
||||
'status' => $response->status(),
|
||||
'response' => $response->body(),
|
||||
]);
|
||||
|
||||
$allSent = false;
|
||||
} else {
|
||||
Log::info('Notifikasi stok minimum WhatsApp berhasil dikirim.', [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->nama,
|
||||
'target' => $target,
|
||||
'outgoing_quantity' => $outgoingQuantity,
|
||||
'stock' => $product->stok,
|
||||
'minimum_stock' => $product->stok_minimum,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
Log::warning('Notifikasi stok minimum WhatsApp gagal diproses.', [
|
||||
'product_id' => $product->id,
|
||||
'target' => $target,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
$allSent = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $allSent;
|
||||
}
|
||||
|
||||
public function minimumStockMessage(Product $product, ?int $outgoingQuantity = null): string
|
||||
{
|
||||
$lines = [
|
||||
'Peringatan Stok Minimum',
|
||||
'',
|
||||
"Barang: {$product->nama}",
|
||||
];
|
||||
|
||||
if ($outgoingQuantity !== null) {
|
||||
$lines[] = "Stok keluar: {$outgoingQuantity} unit";
|
||||
}
|
||||
|
||||
return implode("\n", [
|
||||
...$lines,
|
||||
"Stok saat ini: {$product->stok} unit",
|
||||
"Stok minimum: {$product->stok_minimum} unit",
|
||||
'',
|
||||
'Mohon segera lakukan restock barang.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -35,4 +35,14 @@
|
|||
],
|
||||
],
|
||||
|
||||
'whatsapp' => [
|
||||
'endpoint' => env('WHATSAPP_GATEWAY_ENDPOINT', 'https://api.fonnte.com/send'),
|
||||
'token' => env('WHATSAPP_GATEWAY_TOKEN'),
|
||||
'target_number' => env('WHATSAPP_TARGET_NUMBER'),
|
||||
'target_numbers' => array_values(array_filter(array_map(
|
||||
'trim',
|
||||
explode(',', (string) env('WHATSAPP_TARGET_NUMBERS', env('WHATSAPP_TARGET_NUMBER', '')))
|
||||
))),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public function up(): void
|
|||
$table->string('kategori')->nullable();
|
||||
$table->unsignedInteger('stok')->default(0);
|
||||
$table->unsignedInteger('stok_minimum')->default(0);
|
||||
$table->text('deskripsi')->nullable();
|
||||
$table->text('keterangan')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement('DROP VIEW IF EXISTS barang_masuk');
|
||||
DB::statement('DROP VIEW IF EXISTS barang_keluar');
|
||||
DB::statement('DROP VIEW IF EXISTS barang_hilang');
|
||||
|
||||
$this->createActivityTable('barang_masuk');
|
||||
$this->createActivityTable('barang_keluar');
|
||||
$this->createActivityTable('barang_hilang');
|
||||
|
||||
$this->copyMutasiToActivityTable('barang_masuk', 'masuk');
|
||||
$this->copyMutasiToActivityTable('barang_keluar', 'keluar');
|
||||
$this->copyMutasiToActivityTable('barang_hilang', 'hilang');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('barang_hilang');
|
||||
Schema::dropIfExists('barang_keluar');
|
||||
Schema::dropIfExists('barang_masuk');
|
||||
|
||||
$this->createCategoryView('barang_masuk', 'masuk');
|
||||
$this->createCategoryView('barang_keluar', 'keluar');
|
||||
$this->createCategoryView('barang_hilang', 'hilang');
|
||||
}
|
||||
|
||||
private function createActivityTable(string $tableName): void
|
||||
{
|
||||
if (Schema::hasTable($tableName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create($tableName, function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('id_barang')->constrained('barang')->cascadeOnDelete();
|
||||
$table->foreignId('id_mutasi_stok')->nullable()->unique()->constrained('mutasi_stok')->nullOnDelete();
|
||||
$table->timestamp('waktu')->nullable();
|
||||
$table->string('barang')->nullable();
|
||||
$table->unsignedInteger('jumlah');
|
||||
$table->string('keterangan')->nullable();
|
||||
$table->foreignId('id_pengguna')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('input_oleh')->nullable();
|
||||
$table->timestamp('dibuat_pada')->nullable();
|
||||
$table->timestamp('diperbarui_pada')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
private function copyMutasiToActivityTable(string $tableName, string $kategori): void
|
||||
{
|
||||
if (! Schema::hasTable('mutasi_stok') || ! Schema::hasTable($tableName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::statement("
|
||||
INSERT INTO {$tableName}
|
||||
(id_mutasi_stok, id_barang, waktu, barang, jumlah, keterangan, id_pengguna, input_oleh, dibuat_pada, diperbarui_pada)
|
||||
SELECT
|
||||
ms.id,
|
||||
ms.id_barang,
|
||||
ms.dibuat_pada,
|
||||
b.nama,
|
||||
ms.jumlah,
|
||||
ms.keterangan,
|
||||
ms.id_pengguna,
|
||||
u.name,
|
||||
ms.dibuat_pada,
|
||||
ms.diperbarui_pada
|
||||
FROM mutasi_stok ms
|
||||
LEFT JOIN barang b ON b.id = ms.id_barang
|
||||
LEFT JOIN users u ON u.id = ms.id_pengguna
|
||||
WHERE ms.kategori = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM {$tableName} target
|
||||
WHERE target.id_mutasi_stok = ms.id
|
||||
)
|
||||
", [$kategori]);
|
||||
}
|
||||
|
||||
private function createCategoryView(string $viewName, string $category): void
|
||||
{
|
||||
DB::statement("
|
||||
CREATE VIEW {$viewName} AS
|
||||
SELECT
|
||||
ms.id,
|
||||
ms.id_barang,
|
||||
b.kode AS kode_barang,
|
||||
b.nama AS nama_barang,
|
||||
ms.tipe,
|
||||
ms.kategori,
|
||||
ms.jumlah,
|
||||
ms.keterangan,
|
||||
ms.id_pengguna,
|
||||
u.name AS nama_pengguna,
|
||||
ms.dibuat_pada,
|
||||
ms.diperbarui_pada
|
||||
FROM mutasi_stok ms
|
||||
LEFT JOIN barang b ON b.id = ms.id_barang
|
||||
LEFT JOIN users u ON u.id = ms.id_pengguna
|
||||
WHERE ms.kategori = '{$category}'
|
||||
");
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('barang')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('barang', 'deskripsi') && ! Schema::hasColumn('barang', 'keterangan')) {
|
||||
Schema::table('barang', function (Blueprint $table) {
|
||||
$table->renameColumn('deskripsi', 'keterangan');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('barang', 'keterangan')) {
|
||||
Schema::table('barang', function (Blueprint $table) {
|
||||
$table->text('keterangan')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('barang', 'deskripsi') && Schema::hasColumn('barang', 'keterangan')) {
|
||||
DB::table('barang')
|
||||
->whereNull('keterangan')
|
||||
->whereNotNull('deskripsi')
|
||||
->update(['keterangan' => DB::raw('deskripsi')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('barang')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('barang', 'keterangan') && ! Schema::hasColumn('barang', 'deskripsi')) {
|
||||
Schema::table('barang', function (Blueprint $table) {
|
||||
$table->renameColumn('keterangan', 'deskripsi');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private array $activityTables = [
|
||||
'barang_masuk',
|
||||
'barang_keluar',
|
||||
'barang_hilang',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach ($this->activityTables as $tableName) {
|
||||
if (! Schema::hasTable($tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
|
||||
if (! Schema::hasColumn($tableName, 'waktu')) {
|
||||
$table->timestamp('waktu')->nullable()->after('id_mutasi_stok');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn($tableName, 'barang')) {
|
||||
$table->string('barang')->nullable()->after('waktu');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn($tableName, 'input_oleh')) {
|
||||
$table->string('input_oleh')->nullable()->after('id_pengguna');
|
||||
}
|
||||
});
|
||||
|
||||
$this->backfillDisplayColumns($tableName);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach ($this->activityTables as $tableName) {
|
||||
if (! Schema::hasTable($tableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
|
||||
if (Schema::hasColumn($tableName, 'input_oleh')) {
|
||||
$table->dropColumn('input_oleh');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn($tableName, 'barang')) {
|
||||
$table->dropColumn('barang');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn($tableName, 'waktu')) {
|
||||
$table->dropColumn('waktu');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function backfillDisplayColumns(string $tableName): void
|
||||
{
|
||||
DB::statement("
|
||||
UPDATE {$tableName} aktivitas
|
||||
LEFT JOIN barang b ON b.id = aktivitas.id_barang
|
||||
LEFT JOIN users u ON u.id = aktivitas.id_pengguna
|
||||
SET
|
||||
aktivitas.waktu = COALESCE(aktivitas.waktu, aktivitas.dibuat_pada),
|
||||
aktivitas.barang = COALESCE(aktivitas.barang, b.nama),
|
||||
aktivitas.input_oleh = COALESCE(aktivitas.input_oleh, u.name)
|
||||
");
|
||||
}
|
||||
};
|
||||
|
|
@ -17,17 +17,17 @@
|
|||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-width: 450px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 16px;
|
||||
border: 1px solid #dbe2ea;
|
||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
|
||||
padding: 30px 28px;
|
||||
padding: 30px 45px;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
font-size: 28px;
|
||||
font-size: 40px;
|
||||
color: #14532d;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
border-radius: 10px;
|
||||
padding: 11px 12px;
|
||||
margin-bottom: 14px;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
input:focus {
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.16s ease;
|
||||
|
|
@ -93,8 +93,8 @@
|
|||
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1><center>SIM Meubel</center></h1>
|
||||
<p class="hint" style="text-align:center; font-size:16px; color:grey; font-weight:700;">Login untuk akses web</p>
|
||||
<h1><center>SIM Mebel</center></h1>
|
||||
<p class="hint" style="text-align:center; font-size:16px; color:grey; font-weight:700;"></p>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="error">{{ $errors->first() }}</div>
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" required>
|
||||
|
||||
<button type="submit">Masuk</button>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
|
||||
.stat-value {
|
||||
margin-top: 4px;
|
||||
font-size: 34px;
|
||||
font-size: 35px;
|
||||
line-height: 1.1;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
|
|
@ -132,38 +132,6 @@
|
|||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
/* .bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 20px;
|
||||
height: 300px;
|
||||
} */
|
||||
|
||||
/* .bar-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 60px;
|
||||
} */
|
||||
|
||||
/* .bar-track {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
background: #eef2f7;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
} */
|
||||
|
||||
/* .bar-fill {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60%;
|
||||
background: linear-gradient(to top, forestgreen, #34d399);
|
||||
} */
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
|
|
@ -433,6 +401,92 @@
|
|||
font-weight: 600;
|
||||
color: #713f12;
|
||||
}
|
||||
|
||||
.wa-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 14px 0;
|
||||
}
|
||||
|
||||
.wa-metric {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
padding: 12px 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wa-metric span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
.wa-metric strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #0f172a;
|
||||
font-size: 24px;
|
||||
line-height: 1.1;
|
||||
font-family: var(--font-heading);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.wa-auto-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 32px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
.wa-auto-badge.pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.wa-message-preview {
|
||||
margin-top: 14px;
|
||||
border: 1px solid #dbe2ea;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
padding: 12px 14px;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.wa-note {
|
||||
margin: 10px 0 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.wa-summary,
|
||||
.compare-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.yearly-chart {
|
||||
overflow-x: auto;
|
||||
grid-template-columns: repeat(12, 54px);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="hero">
|
||||
|
|
@ -507,6 +561,63 @@
|
|||
</article>
|
||||
</div>
|
||||
|
||||
<section class="panel" style="margin-top:14px;">
|
||||
<div class="yearly-header">
|
||||
<div>
|
||||
<h3 style="margin:0;">Notifikasi Stok Minimum via WhatsApp</h3>
|
||||
<div style="font-size:13px; color:#64748b; margin-top:4px;">
|
||||
Sistem mengirim pesan otomatis ke nomor penerima saat stok barang mencapai batas minimum.
|
||||
</div>
|
||||
</div>
|
||||
<span class="wa-auto-badge {{ $nomorPenerimaWhatsApp->isNotEmpty() ? '' : 'pending' }}">
|
||||
{{ $nomorPenerimaWhatsApp->isNotEmpty() ? 'Otomatis aktif' : 'Nomor belum diatur' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="wa-summary">
|
||||
<div class="wa-metric">
|
||||
<span>Barang Stok Minimum</span>
|
||||
<strong>{{ number_format($stokMenipis) }}</strong>
|
||||
</div>
|
||||
<div class="wa-metric">
|
||||
<span>Persentase Stok Aman</span>
|
||||
<strong>{{ $persentaseStokAman }}%</strong>
|
||||
</div>
|
||||
<div class="wa-metric">
|
||||
<span>Nomor Penerima</span>
|
||||
<strong>{{ $nomorPenerimaWhatsApp->isNotEmpty() ? $nomorPenerimaWhatsApp->implode(', ') : '-' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th>Stok</th>
|
||||
<th>Stok Minimum</th>
|
||||
<th>Kondisi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($produkStokMenipis as $produk)
|
||||
<tr>
|
||||
<td><strong>{{ $produk->nama }}</strong></td>
|
||||
<td>{{ number_format((int) $produk->stok) }} unit</td>
|
||||
<td>{{ number_format((int) $produk->stok_minimum) }} unit</td>
|
||||
<td>Perlu restock</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4">Semua stok barang masih berada di atas stok minimum.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="wa-message-preview">{{ $pesanWhatsAppStokMinimum }}</div>
|
||||
<p class="wa-note">Pesan dikirim otomatis oleh sistem setelah transaksi barang keluar atau barang hilang membuat stok mencapai batas minimum.</p>
|
||||
</section>
|
||||
|
||||
<!-- <div class="stock-alert">
|
||||
Stok Menipis: <strong>{{ $stokMenipis }}</strong> produk perlu perhatian.
|
||||
Persentase stok aman saat ini <strong>{{ $persentaseStokAman }}%</strong>.
|
||||
|
|
@ -727,4 +838,4 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@
|
|||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
font-size: 14px;
|
||||
font-size: 18px;
|
||||
line-height: 1.55;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: 74px;
|
||||
height: 75px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -51,8 +51,8 @@
|
|||
}
|
||||
|
||||
.brand {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
font-size: 30px;
|
||||
font-weight: 850;
|
||||
color: #166534;
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
border-radius: 8px;
|
||||
border: 1px solid #cde9d2;
|
||||
background: #f3fff4;
|
||||
font-size: 13px;
|
||||
font-size: 28px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
}
|
||||
|
||||
.main {
|
||||
padding: 18px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.flash {
|
||||
|
|
@ -120,10 +120,10 @@
|
|||
button,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-size: 19px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 9px 10px;
|
||||
padding: 10px 10px;
|
||||
transition: all 0.18s ease;
|
||||
background: #fff;
|
||||
color: var(--text-main);
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
color: #fff;
|
||||
border-color: var(--primary);
|
||||
font-weight: 600;
|
||||
padding: 9px 14px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
|
|
@ -181,7 +181,8 @@
|
|||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.app-table {
|
||||
/* lebar kolom */
|
||||
.app-table {
|
||||
width: 100%;
|
||||
min-width: 680px;
|
||||
border-collapse: separate;
|
||||
|
|
@ -190,17 +191,17 @@
|
|||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
}
|
||||
|
||||
.app-table thead th {
|
||||
background: var(--bg-soft);
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
} /* Table Daftar User */
|
||||
|
||||
.app-table th,
|
||||
.app-table td {
|
||||
|
|
@ -245,11 +246,11 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-size: 19px;
|
||||
line-height: 1.25;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
} /* Sidebar Font*/
|
||||
|
||||
aside a:hover {
|
||||
background: rgba(22, 101, 52, 0.95);
|
||||
|
|
@ -282,7 +283,7 @@
|
|||
}
|
||||
|
||||
.layout-sidebar {
|
||||
width: 280px;
|
||||
width: 300px;
|
||||
background: linear-gradient(180deg, #14532d 0%, #166534 100%);
|
||||
color: #fff;
|
||||
padding: 16px;
|
||||
|
|
@ -365,6 +366,11 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-table th,
|
||||
.app-table td {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -406,7 +412,7 @@
|
|||
|
||||
@media (max-width: 1024px) {
|
||||
.layout-sidebar {
|
||||
width: 236px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
aside a {
|
||||
|
|
@ -466,7 +472,7 @@
|
|||
}
|
||||
|
||||
.table-card {
|
||||
padding: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
|
@ -538,7 +544,7 @@
|
|||
.app-table th,
|
||||
.app-table td {
|
||||
padding: 8px 9px;
|
||||
font-size: 13px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -551,7 +557,8 @@
|
|||
|
||||
<!-- SIDEBAR -->
|
||||
<aside class="layout-sidebar">
|
||||
<h2 style="margin-top:0; margin-bottom:20px; color:#ffffff; font-size:26px; letter-spacing:-0.01em;">SIM Meubel</h2>
|
||||
<h2 style="margin-top:0; margin-bottom:20px; color:#ffffff; font-size:26px; letter-spacing:-0.01em;">SIM
|
||||
Meubel</h2>
|
||||
|
||||
<a href="{{ route('dashboard') }}" class="{{ $activeMenu === 'dashboard' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-speedometer2" aria-hidden="true"></i></span>
|
||||
|
|
@ -565,14 +572,14 @@
|
|||
</a>
|
||||
@endif
|
||||
|
||||
@if (auth()->user()?->role === 'admin')
|
||||
@if (in_array(auth()->user()?->role, ['admin', 'owner'], true))
|
||||
<a href="{{ route('products.index') }}" class="{{ $activeMenu === 'barang' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-box-seam" aria-hidden="true"></i></span>
|
||||
<span>Data Barang</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (auth()->user()?->role === 'admin')
|
||||
@if (in_array(auth()->user()?->role, ['admin', 'owner'], true))
|
||||
<a href="{{ route('stock-movements.incoming') }}" class="{{ $activeMenu === 'masuk' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-box-arrow-in-down" aria-hidden="true"></i></span>
|
||||
<span>Barang Masuk</span>
|
||||
|
|
@ -582,7 +589,9 @@
|
|||
<span class="menu-icon"><i class="bi bi-box-arrow-up-right" aria-hidden="true"></i></span>
|
||||
<span>Barang Keluar</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (auth()->user()?->role === 'admin')
|
||||
<a href="{{ route('stock-movements.lost') }}" class="{{ $activeMenu === 'hilang' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-exclamation-triangle" aria-hidden="true"></i></span>
|
||||
<span>Input Barang Hilang</span>
|
||||
|
|
@ -601,16 +610,16 @@ class="{{ $activeMenu === 'reports-pembelian' ? 'active' : '' }}">
|
|||
<span class="menu-icon"><i class="bi bi-receipt" aria-hidden="true"></i></span>
|
||||
<span>Laporan Pembelian</span>
|
||||
</a>
|
||||
<a href="{{ route('reports.penjualan') }}"
|
||||
class="{{ $activeMenu === 'reports-penjualan' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-cash-coin" aria-hidden="true"></i></span>
|
||||
<span>Laporan Penjualan</span>
|
||||
</a>
|
||||
<a href="{{ route('reports.rekap-pembelian') }}"
|
||||
class="{{ $activeMenu === 'reports-rekap-pembelian' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-bar-chart-line" aria-hidden="true"></i></span>
|
||||
<span>Rekap Pembelian</span>
|
||||
</a>
|
||||
<a href="{{ route('reports.penjualan') }}"
|
||||
class="{{ $activeMenu === 'reports-penjualan' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-cash-coin" aria-hidden="true"></i></span>
|
||||
<span>Laporan Penjualan</span>
|
||||
</a>
|
||||
<a href="{{ route('reports.rekap-penjualan') }}"
|
||||
class="{{ $activeMenu === 'reports-rekap-penjualan' ? 'active' : '' }}">
|
||||
<span class="menu-icon"><i class="bi bi-graph-up-arrow" aria-hidden="true"></i></span>
|
||||
|
|
@ -640,6 +649,10 @@ class="{{ $activeMenu === 'reports-rekap-penjualan' ? 'active' : '' }}">
|
|||
<div class="flash success">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
@if (session('warning'))
|
||||
<div class="flash error">{{ session('warning') }}</div>
|
||||
@endif
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="flash error">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
|
@ -651,4 +664,4 @@ class="{{ $activeMenu === 'reports-rekap-penjualan' ? 'active' : '' }}">
|
|||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
@ -4,10 +4,22 @@
|
|||
@section('active_menu', 'hilang')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
@php($keteranganKehilangan = ['Hilang'])
|
||||
|
||||
<!-- <style>
|
||||
.panel { margin-bottom: 14px; }
|
||||
.inline { display: grid; grid-template-columns: 1fr 220px 1fr auto; gap: 10px; }
|
||||
</style>
|
||||
</style> -->
|
||||
<style>
|
||||
.panel { margin-bottom: 14px; }
|
||||
.inline { display: grid; grid-template-columns: 1fr 220px 1fr auto; gap: 10px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inline {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="table-card panel">
|
||||
<h2>Input Barang Hilang</h2>
|
||||
|
|
@ -20,7 +32,12 @@
|
|||
@endforeach
|
||||
</select>
|
||||
<input type="number" name="jumlah" min="1" placeholder="Jumlah hilang" required>
|
||||
<input type="text" name="keterangan" placeholder="Keterangan kehilangan">
|
||||
<select name="keterangan" required>
|
||||
<option value="">Pilih keterangan</option>
|
||||
@foreach ($keteranganKehilangan as $keterangan)
|
||||
<option value="{{ $keterangan }}">{{ $keterangan }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" style="background:#b91c1c; border-color:#b91c1c;">Simpan</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -40,11 +57,11 @@
|
|||
<tbody>
|
||||
@forelse ($movements as $movement)
|
||||
<tr>
|
||||
<td>{{ $movement->created_at?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $movement->product->nama }}</td>
|
||||
<td>{{ ($movement->waktu ?? $movement->created_at)?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $movement->barang ?? $movement->product?->nama ?? '-' }}</td>
|
||||
<td>{{ $movement->jumlah }}</td>
|
||||
<td>{{ $movement->keterangan ?? '-' }}</td>
|
||||
<td>{{ $movement->user?->name ?? '-' }}</td>
|
||||
<td>{{ $movement->input_oleh ?? $movement->user?->name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="5">Belum ada data barang hilang.</td></tr>
|
||||
|
|
|
|||
|
|
@ -4,33 +4,66 @@
|
|||
@section('active_menu', 'keluar')
|
||||
|
||||
@section('content')
|
||||
@php($isAdmin = auth()->user()?->role === 'admin')
|
||||
@php($kategoriBarang = ['Meja', 'Kursi', 'Lemari', 'Kasur', 'Sofa', 'Rak', 'Bufet'])
|
||||
@php($keteranganBarangKeluar = ['Terjual'])
|
||||
|
||||
<style>
|
||||
.panel {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.inline {
|
||||
form.outgoing-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 220px 1fr auto;
|
||||
grid-template-columns: minmax(150px, 190px) minmax(230px, 1fr) minmax(170px, 220px) minmax(150px, 180px) max-content !important;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inline button {
|
||||
min-width: 72px;
|
||||
height: 38px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
form.outgoing-form {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="table-card panel">
|
||||
<h2>Input Barang Keluar</h2>
|
||||
<form action="{{ route('stock-movements.outgoing.store') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
<select name="id_barang" required>
|
||||
<option value="">Pilih barang</option>
|
||||
@foreach ($products as $product)
|
||||
<option value="{{ $product->id }}">{{ $product->nama }} (stok: {{ $product->stok }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" name="jumlah" min="1" placeholder="Jumlah keluar" required>
|
||||
<input type="text" name="keterangan" placeholder="Keterangan">
|
||||
<button type="submit">Simpan</button>
|
||||
</form>
|
||||
</div>
|
||||
@if ($isAdmin)
|
||||
<div class="table-card panel">
|
||||
<h2>Input Barang Keluar</h2>
|
||||
<form action="{{ route('stock-movements.outgoing.store') }}" method="POST" class="inline outgoing-form">
|
||||
@csrf
|
||||
<select name="kategori" class="category-filter" data-target="outgoing-product-select" required
|
||||
oninvalid="this.setCustomValidity('Pilih kategori terlebih dahulu.')"
|
||||
onchange="this.setCustomValidity('')">
|
||||
<option value="">Pilih kategori</option>
|
||||
@foreach ($kategoriBarang as $kategori)
|
||||
<option value="{{ strtolower($kategori) }}">{{ ucfirst($kategori) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="id_barang" id="outgoing-product-select" required>
|
||||
<option value="">Pilih barang</option>
|
||||
@foreach ($products as $product)
|
||||
<option value="{{ $product->id }}" data-kategori="{{ strtolower($product->kategori ?? '') }}">
|
||||
{{ $product->nama }} (stok: {{ $product->stok }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" name="jumlah" min="1" placeholder="Jumlah keluar" required>
|
||||
<select name="keterangan" required>
|
||||
<option value="">Pilih keterangan</option>
|
||||
@foreach ($keteranganBarangKeluar as $keterangan)
|
||||
<option value="{{ $keterangan }}" @selected($keterangan === 'Terjual')>{{ $keterangan }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit">Simpan</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="table-card panel">
|
||||
<h2>Filter Barang Keluar</h2>
|
||||
|
|
@ -65,11 +98,11 @@
|
|||
<tbody>
|
||||
@forelse ($movements as $movement)
|
||||
<tr>
|
||||
<td>{{ $movement->created_at?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $movement->product->nama }}</td>
|
||||
<td>{{ ($movement->waktu ?? $movement->created_at)?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $movement->barang ?? $movement->product?->nama ?? '-' }}</td>
|
||||
<td>{{ $movement->jumlah }}</td>
|
||||
<td>{{ $movement->keterangan ?? '-' }}</td>
|
||||
<td>{{ $movement->user?->name ?? '-' }}</td>
|
||||
<td>{{ $movement->input_oleh ?? $movement->user?->name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
|
|
@ -79,4 +112,30 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($isAdmin)
|
||||
<script>
|
||||
document.querySelectorAll('.category-filter').forEach((categorySelect) => {
|
||||
categorySelect.addEventListener('change', () => {
|
||||
const productSelect = document.getElementById(categorySelect.dataset.target);
|
||||
const selectedCategory = categorySelect.value;
|
||||
|
||||
if (!productSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
productSelect.value = '';
|
||||
|
||||
productSelect.querySelectorAll('option').forEach((option) => {
|
||||
if (!option.value) {
|
||||
option.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
option.hidden = selectedCategory !== '' && option.dataset.kategori !== selectedCategory;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -4,26 +4,50 @@
|
|||
@section('active_menu', 'masuk')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.panel {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@php($isAdmin = auth()->user()?->role === 'admin')
|
||||
@php($kategoriBarang = ['Meja', 'Kursi', 'Lemari', 'Kasur', 'Sofa', 'Rak', 'Bufet'])
|
||||
<style>
|
||||
.panel {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr 220px 1fr auto;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inline button {
|
||||
min-width: 72px;
|
||||
height: 38px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inline {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 220px 1fr auto;
|
||||
gap: 10px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
|
||||
@if ($isAdmin)
|
||||
<div class="table-card panel">
|
||||
<h2>Input Barang Masuk</h2>
|
||||
<form action="{{ route('stock-movements.incoming.store') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
<select name="id_barang" required>
|
||||
<select name="kategori" class="category-filter" data-target="incoming-product-select" required
|
||||
oninvalid="this.setCustomValidity('Pilih kategori terlebih dahulu.')" onchange="this.setCustomValidity('')">
|
||||
<option value="">Pilih kategori</option>
|
||||
@foreach ($kategoriBarang as $kategori)
|
||||
<option value="{{ strtolower($kategori) }}">{{ ucfirst($kategori) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="id_barang" id="incoming-product-select" required>
|
||||
<option value="">Pilih barang</option>
|
||||
@foreach ($products as $product)
|
||||
<option value="{{ $product->id }}">{{ $product->nama }} (stok: {{ $product->stok }})</option>
|
||||
<option value="{{ $product->id }}" data-kategori="{{ strtolower($product->kategori ?? '') }}">
|
||||
{{ $product->nama }} (stok: {{ $product->stok }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" name="jumlah" min="1" placeholder="Jumlah masuk" required>
|
||||
|
|
@ -31,52 +55,80 @@
|
|||
<button type="submit">Simpan</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="table-card panel">
|
||||
<h2>Filter Barang Masuk</h2>
|
||||
<form method="GET" action="{{ route('stock-movements.incoming') }}" class="report-filter-form">
|
||||
<div>
|
||||
<label for="tanggal_mulai">Tanggal Mulai</label>
|
||||
<input id="tanggal_mulai" type="date" name="tanggal_mulai" value="{{ request('tanggal_mulai') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="tanggal_selesai">Tanggal Selesai</label>
|
||||
<input id="tanggal_selesai" type="date" name="tanggal_selesai" value="{{ request('tanggal_selesai') }}">
|
||||
</div>
|
||||
<button type="submit">Terapkan</button>
|
||||
<a href="{{ route('stock-movements.incoming') }}" class="btn-link">
|
||||
Reset
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-card panel">
|
||||
<h2>Filter Barang Masuk</h2>
|
||||
<form method="GET" action="{{ route('stock-movements.incoming') }}" class="report-filter-form">
|
||||
<div>
|
||||
<label for="tanggal_mulai">Tanggal Mulai</label>
|
||||
<input id="tanggal_mulai" type="date" name="tanggal_mulai" value="{{ request('tanggal_mulai') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="tanggal_selesai">Tanggal Selesai</label>
|
||||
<input id="tanggal_selesai" type="date" name="tanggal_selesai" value="{{ request('tanggal_selesai') }}">
|
||||
</div>
|
||||
<button type="submit">Terapkan</button>
|
||||
<a href="{{ route('stock-movements.incoming') }}" class="btn-link">
|
||||
Reset
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-card panel">
|
||||
<h3>Riwayat Barang Masuk</h3>
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<div class="table-card panel">
|
||||
<h3>Riwayat Barang Masuk</h3>
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Waktu</th>
|
||||
<th>Barang</th>
|
||||
<th>Jumlah</th>
|
||||
<th>Keterangan</th>
|
||||
<th>Input Oleh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($movements as $movement)
|
||||
<tr>
|
||||
<th>Waktu</th>
|
||||
<th>Barang</th>
|
||||
<th>Jumlah</th>
|
||||
<th>Keterangan</th>
|
||||
<th>Input Oleh</th>
|
||||
<td>{{ ($movement->waktu ?? $movement->created_at)?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '—' }}
|
||||
</td>
|
||||
<td>{{ $movement->barang ?? $movement->product?->nama ?? '-' }}</td>
|
||||
<td>{{ $movement->jumlah }}</td>
|
||||
<td>{{ $movement->keterangan ?? '-' }}</td>
|
||||
<td>{{ $movement->input_oleh ?? $movement->user?->name ?? '-' }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($movements as $movement)
|
||||
<tr>
|
||||
<td>{{ $movement->created_at?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $movement->product->nama }}</td>
|
||||
<td>{{ $movement->jumlah }}</td>
|
||||
<td>{{ $movement->keterangan ?? '-' }}</td>
|
||||
<td>{{ $movement->user?->name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Belum ada data barang masuk.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endsection
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Belum ada data barang masuk.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($isAdmin)
|
||||
<script>
|
||||
document.querySelectorAll('.category-filter').forEach((categorySelect) => {
|
||||
categorySelect.addEventListener('change', () => {
|
||||
const productSelect = document.getElementById(categorySelect.dataset.target);
|
||||
const selectedCategory = categorySelect.value;
|
||||
|
||||
if (!productSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
productSelect.value = '';
|
||||
|
||||
productSelect.querySelectorAll('option').forEach((option) => {
|
||||
if (!option.value) {
|
||||
option.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
option.hidden = selectedCategory !== '' && option.dataset.kategori !== selectedCategory;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@endsection
|
||||
|
|
@ -4,31 +4,71 @@
|
|||
@section('active_menu', 'barang')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.panel {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.inline-form { display: grid; grid-template-columns: 60px 1fr 1fr 95px 95px 1fr auto; gap: 6px; }
|
||||
.table-toolbar {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.table-toolbar label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@php($isAdmin = auth()->user()?->role === 'admin')
|
||||
@php($kategoriBarang = ['meja', 'kursi', 'lemari', 'kasur', 'sofa', 'rak', 'bufet'])
|
||||
|
||||
<style>
|
||||
.grid-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr 1fr 95px 95px 1fr auto auto;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
min-width: 72px;
|
||||
height: 38px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.edit-row[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-row td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background: #64748b;
|
||||
border-color: #64748b;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-toolbar label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-table th,
|
||||
.app-table td {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if ($isAdmin)
|
||||
<div class="table-card panel">
|
||||
<h2>Data Barang</h2>
|
||||
<form action="{{ route('products.store') }}" method="POST">
|
||||
|
|
@ -36,64 +76,142 @@
|
|||
<div class="grid-form">
|
||||
<input type="text" name="kode" placeholder="Kode barang" value="{{ old('kode') }}" required>
|
||||
<input type="text" name="nama" placeholder="Nama barang" value="{{ old('nama') }}" required>
|
||||
<input type="text" name="kategori" placeholder="Kategori" value="{{ old('kategori') }}">
|
||||
<input type="number" name="stok" placeholder="Stok awal" min="0" value="{{ old('stok', 0) }}" required>
|
||||
<input type="number" name="stok_minimum" placeholder="Stok minimum" min="0" value="{{ old('stok_minimum', 0) }}" required>
|
||||
<input type="text" name="deskripsi" placeholder="Deskripsi" value="{{ old('deskripsi') }}">
|
||||
<select name="kategori">
|
||||
<option value="">Pilih kategori</option>
|
||||
@foreach ($kategoriBarang as $kategori)
|
||||
<option value="{{ $kategori }}" @selected(old('kategori') === $kategori)>{{ ucfirst($kategori) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="number" name="stok" placeholder="Stok awal" min="0" value="{{ old('stok', 0) }}"
|
||||
data-stock-input required>
|
||||
<input type="number" name="stok_minimum" placeholder="Stok minimum" min="0" max="{{ old('stok', 0) }}"
|
||||
value="{{ old('stok_minimum', 0) }}" data-minimum-stock-input required>
|
||||
<input type="text" name="keterangan" placeholder="Keterangan" value="{{ old('keterangan') }}">
|
||||
</div>
|
||||
<button type="submit">Simpan Data Barang</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="table-card panel">
|
||||
<h3>Daftar Barang</h3>
|
||||
<form method="GET" action="{{ route('products.index') }}" class="table-toolbar">
|
||||
<label for="urut">Urutkan</label>
|
||||
<select name="urut" id="urut" onchange="this.form.submit()">
|
||||
<option value="nama_asc" @selected(($opsiUrut ?? 'nama_asc') === 'nama_asc')>Nama (A-Z)</option>
|
||||
<option value="lama_baru" @selected(($opsiUrut ?? 'nama_asc') === 'lama_baru')>Data lama ke terbaru</option>
|
||||
</select>
|
||||
</form>
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kode</th>
|
||||
<th>Nama</th>
|
||||
<th style="padding-left: 80px;">Kategori</th>
|
||||
<th>Stok</th>
|
||||
<th>Stok Min</th>
|
||||
<th>Deskripsi</th>
|
||||
<div class="table-card panel">
|
||||
<h3>Daftar Barang</h3>
|
||||
<form method="GET" action="{{ route('products.index') }}" class="table-toolbar">
|
||||
<label for="urut">Urutkan</label>
|
||||
<select name="urut" id="urut" onchange="this.form.submit()">
|
||||
<option value="nama_asc" @selected(($opsiUrut ?? 'nama_asc') === 'nama_asc')>Nama (A-Z)</option>
|
||||
<option value="lama_baru" @selected(($opsiUrut ?? 'nama_asc') === 'lama_baru')>Data lama ke terbaru</option>
|
||||
</select>
|
||||
</form>
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Kode</th>
|
||||
<th>Nama</th>
|
||||
<!-- <th style="padding-left: 80px;">Kategori</th> -->
|
||||
<th>Kategori</th>
|
||||
<th>Stok</th>
|
||||
<th>Stok Min</th>
|
||||
<th>Keterangan</th>
|
||||
@if ($isAdmin)
|
||||
<th colspan="2">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($products as $product)
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($products as $product)
|
||||
@if ($isAdmin)
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<div style="display:flex; align-items:start; gap:6px;">
|
||||
<form action="{{ route('products.update', $product) }}" method="POST" class="inline-form" style="flex:1;">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="text" name="kode" value="{{ $product->kode }}" required>
|
||||
<input type="text" name="nama" value="{{ $product->nama }}" required>
|
||||
<input type="text" name="kategori" value="{{ $product->kategori }}">
|
||||
<input type="number" name="stok" min="0" value="{{ $product->stok }}" required>
|
||||
<input type="number" name="stok_minimum" min="0" value="{{ $product->stok_minimum }}" required>
|
||||
<input type="text" name="deskripsi" value="{{ $product->deskripsi }}">
|
||||
<button type="submit">Update</button>
|
||||
</form>
|
||||
<form action="{{ route('products.destroy', $product) }}" method="POST">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" onclick="return confirm('Hapus barang ini?')" style="background:#b91c1c; border-color:#b91c1c;">Hapus</button>
|
||||
</form>
|
||||
</div>
|
||||
<td>{{ $product->kode }}</td>
|
||||
<td>{{ $product->nama }}</td>
|
||||
<td>{{ $product->kategori ?? '-' }}</td>
|
||||
<td>{{ $product->stok }}</td>
|
||||
<td>{{ $product->stok_minimum }}</td>
|
||||
<td>{{ $product->keterangan ?? '-' }}</td>
|
||||
<td>
|
||||
<button type="button" class="action-button edit-product-button"
|
||||
data-target="edit-product-{{ $product->id }}">Edit</button>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ route('products.destroy', $product) }}" method="POST" class="action-form">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="action-button" onclick="return confirm('Hapus barang ini?')"
|
||||
style="background:#b91c1c; border-color:#b91c1c;">Hapus</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="8">Belum ada data barang.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endsection
|
||||
<tr id="edit-product-{{ $product->id }}" class="edit-row" hidden>
|
||||
<td colspan="8">
|
||||
<form action="{{ route('products.update', $product) }}" method="POST" class="inline-form">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<input type="text" name="kode" value="{{ $product->kode }}" required>
|
||||
<input type="text" name="nama" value="{{ $product->nama }}" required>
|
||||
<input type="text" name="kategori" value="{{ $product->kategori }}">
|
||||
<input type="number" name="stok" min="0" value="{{ $product->stok }}" data-stock-input required>
|
||||
<input type="number" name="stok_minimum" min="0" max="{{ $product->stok }}"
|
||||
value="{{ $product->stok_minimum }}" data-minimum-stock-input required>
|
||||
<input type="text" name="keterangan" value="{{ $product->keterangan }}">
|
||||
<button type="submit">Update</button>
|
||||
<button type="button" class="button-secondary cancel-edit-button"
|
||||
data-target="edit-product-{{ $product->id }}">Batal</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@else
|
||||
<tr>
|
||||
<td>{{ $product->kode }}</td>
|
||||
<td>{{ $product->nama }}</td>
|
||||
<td>{{ $product->kategori ?? '-' }}</td>
|
||||
<td>{{ $product->stok }}</td>
|
||||
<td>{{ $product->stok_minimum }}</td>
|
||||
<td>{{ $product->keterangan ?? '-' }}</td>
|
||||
</tr>
|
||||
@endif
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ $isAdmin ? 8 : 6 }}">Belum ada data barang.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($isAdmin)
|
||||
<script>
|
||||
document.querySelectorAll('.edit-product-button, .cancel-edit-button').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const editRow = document.getElementById(button.dataset.target);
|
||||
|
||||
if (editRow) {
|
||||
editRow.hidden = !editRow.hidden;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('form').forEach((form) => {
|
||||
const stockInput = form.querySelector('[data-stock-input]');
|
||||
const minimumStockInput = form.querySelector('[data-minimum-stock-input]');
|
||||
|
||||
if (!stockInput || !minimumStockInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncMinimumStockLimit = () => {
|
||||
const stock = Number.parseInt(stockInput.value || '0', 10);
|
||||
const minimumStock = Number.parseInt(minimumStockInput.value || '0', 10);
|
||||
|
||||
minimumStockInput.max = stock;
|
||||
|
||||
if (minimumStock > stock) {
|
||||
minimumStockInput.value = stock;
|
||||
}
|
||||
};
|
||||
|
||||
stockInput.addEventListener('input', syncMinimumStockLimit);
|
||||
minimumStockInput.addEventListener('input', syncMinimumStockLimit);
|
||||
syncMinimumStockLimit();
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@endsection
|
||||
|
|
@ -46,10 +46,10 @@ class="export-link pdf">
|
|||
<tbody>
|
||||
@forelse ($data as $item)
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ ($item->waktu ?? $item->created_at)?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->barang ?? $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->input_oleh ?? $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
|
|
|
|||
|
|
@ -33,21 +33,21 @@ class="export-link pdf">
|
|||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Waktu</th>
|
||||
<th>Barang</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
<th>Input Oleh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($data as $item)
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -33,21 +33,21 @@ class="export-link pdf">
|
|||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Waktu</th>
|
||||
<th>Barang</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
<th>Input Oleh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($data as $item)
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -24,25 +24,45 @@ class="export-link pdf">
|
|||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<h2>Rekap Pembelian per Produk</h2>
|
||||
<h2>Rekap Pembelian per Barang</h2>
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th>Kode</th>
|
||||
<th>Barang</th>
|
||||
<th>Kategori</th>
|
||||
<th>Stok Saat Ini</th>
|
||||
<th>Total Qty</th>
|
||||
<th>Total Transaksi</th>
|
||||
<th>Aktivitas</th>
|
||||
<th>Rata-rata</th>
|
||||
<th>Qty Terkecil</th>
|
||||
<th>Qty Terbesar</th>
|
||||
<th>Aktivitas Pertama</th>
|
||||
<th>Aktivitas Terakhir</th>
|
||||
<th>Input Oleh</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($data as $item)
|
||||
<tr>
|
||||
<td>{{ $item->product->kode ?? '-' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->product->kategori ?? '-' }}</td>
|
||||
<td>{{ (int) ($item->product->stok ?? 0) }}</td>
|
||||
<td>{{ (int) $item->total_jumlah }}</td>
|
||||
<td>{{ (int) $item->total_transaksi }}</td>
|
||||
<td>{{ number_format((float) $item->rata_rata, 2, ',', '.') }}</td>
|
||||
<td>{{ (int) $item->jumlah_terkecil }}</td>
|
||||
<td>{{ (int) $item->jumlah_terbesar }}</td>
|
||||
<td>{{ $item->aktivitas_pertama?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $item->aktivitas_terakhir?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $item->input_oleh }}</td>
|
||||
<td>{{ $item->keterangan }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3">Tidak ada data rekap pembelian.</td>
|
||||
<td colspan="13">Tidak ada data rekap pembelian.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -24,25 +24,45 @@ class="export-link pdf">
|
|||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<h2>Rekap Penjualan per Produk</h2>
|
||||
<h2>Rekap Penjualan per Barang</h2>
|
||||
<table class="app-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th>Kode</th>
|
||||
<th>Barang</th>
|
||||
<th>Kategori</th>
|
||||
<th>Stok Saat Ini</th>
|
||||
<th>Total Qty</th>
|
||||
<th>Total Transaksi</th>
|
||||
<th>Aktivitas</th>
|
||||
<th>Rata-rata</th>
|
||||
<th>Qty Terkecil</th>
|
||||
<th>Qty Terbesar</th>
|
||||
<th>Aktivitas Pertama</th>
|
||||
<th>Aktivitas Terakhir</th>
|
||||
<th>Input Oleh</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($data as $item)
|
||||
<tr>
|
||||
<td>{{ $item->product->kode ?? '-' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->product->kategori ?? '-' }}</td>
|
||||
<td>{{ (int) ($item->product->stok ?? 0) }}</td>
|
||||
<td>{{ (int) $item->total_jumlah }}</td>
|
||||
<td>{{ (int) $item->total_transaksi }}</td>
|
||||
<td>{{ number_format((float) $item->rata_rata, 2, ',', '.') }}</td>
|
||||
<td>{{ (int) $item->jumlah_terkecil }}</td>
|
||||
<td>{{ (int) $item->jumlah_terbesar }}</td>
|
||||
<td>{{ $item->aktivitas_pertama?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $item->aktivitas_terakhir?->timezone('Asia/Jakarta')->format('d-m-Y H:i') ?? '-' }}</td>
|
||||
<td>{{ $item->input_oleh }}</td>
|
||||
<td>{{ $item->keterangan }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3">Tidak ada data rekap penjualan.</td>
|
||||
<td colspan="13">Tidak ada data rekap penjualan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -4,16 +4,53 @@
|
|||
@section('active_menu', 'reports')
|
||||
|
||||
@section('content')
|
||||
<!-- <style>
|
||||
.report-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
</style> -->
|
||||
<style>
|
||||
.report-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.summary-cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border: 1px solid #dbe2ea;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.summary-card .summary-label {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.summary-card .summary-value {
|
||||
font-size: 30px;
|
||||
color: #14532d;
|
||||
font-weight: 800;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.summary-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="table-card">
|
||||
<h2>Filter Laporan</h2>
|
||||
<form method="GET" action="{{ route('reports.stock') }}"
|
||||
style="display:grid; grid-template-columns: 1fr 1fr 1fr auto auto; gap:10px; align-items:end;">
|
||||
<form method="GET" action="{{ route('reports.stock') }}" class="report-filter-form">
|
||||
<div>
|
||||
<label for="jenis_laporan">Jenis Laporan</label>
|
||||
<select id="jenis_laporan" name="jenis_laporan">
|
||||
|
|
@ -33,197 +70,210 @@
|
|||
<input id="tanggal_selesai" type="date" name="tanggal_selesai" value="{{ $tanggalSelesai }}">
|
||||
</div>
|
||||
<button type="submit">Terapkan</button>
|
||||
<a href="{{ route('reports.stock') }}"
|
||||
style="display:inline-block; padding:9px 10px; border-radius:7px; border:1px solid #cbd5e1; text-decoration:none; color:#1f2937; background:#fff;">
|
||||
Reset
|
||||
</a>
|
||||
<a href="{{ route('reports.stock') }}" class="btn-link">Reset</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px;">
|
||||
<div style="border:1px solid #dbe2ea; border-radius:12px; background:#fff; padding:16px;">
|
||||
<div style="font-size:14px; color:#64748b;">Total Pembelian</div>
|
||||
<div style="font-size:30px; color:#14532d; font-weight:800;">{{ $totalPembelian }}</div>
|
||||
<!-- <div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px;">
|
||||
<div style="border:1px solid #dbe2ea; border-radius:12px; background:#fff; padding:16px;">
|
||||
<div style="font-size:14px; color:#64748b;">Total Pembelian</div>
|
||||
<div style="font-size:30px; color:#14532d; font-weight:800;">{{ $totalPembelian }}</div>
|
||||
</div>
|
||||
<div style="border:1px solid #dbe2ea; border-radius:12px; background:#fff; padding:16px;">
|
||||
<div style="font-size:14px; color:#64748b;">Total Penjualan</div>
|
||||
<div style="font-size:30px; color:#14532d; font-weight:800;">{{ $totalPenjualan }}</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="summary-cards">
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">Total Pembelian</div>
|
||||
<div class="summary-value">{{ $totalPembelian }}</div>
|
||||
</div>
|
||||
<div style="border:1px solid #dbe2ea; border-radius:12px; background:#fff; padding:16px;">
|
||||
<div style="font-size:14px; color:#64748b;">Total Penjualan</div>
|
||||
<div style="font-size:30px; color:#14532d; font-weight:800;">{{ $totalPenjualan }}</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">Total Penjualan</div>
|
||||
<div class="summary-value">{{ $totalPenjualan }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($jenisLaporan === 'pembelian')
|
||||
<div class="table-card">
|
||||
<h2>Laporan Pembelian</h2>
|
||||
<table class="app-table report-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%;">
|
||||
<col style="width: 24%;">
|
||||
<col style="width: 12%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 26%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($pembelian as $item)
|
||||
<div class="table-card">
|
||||
<h2>Laporan Pembelian</h2>
|
||||
<table class="app-table report-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%;">
|
||||
<col style="width: 24%;">
|
||||
<col style="width: 12%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 26%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Tidak ada data pembelian.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($pembelian as $item)
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Tidak ada data pembelian.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($jenisLaporan === 'penjualan')
|
||||
<div class="table-card">
|
||||
<h2>Laporan Penjualan</h2>
|
||||
<table class="app-table report-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%;">
|
||||
<col style="width: 24%;">
|
||||
<col style="width: 12%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 26%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($penjualan as $item)
|
||||
<div class="table-card">
|
||||
<h2>Laporan Penjualan</h2>
|
||||
<table class="app-table report-table">
|
||||
<colgroup>
|
||||
<col style="width: 18%;">
|
||||
<col style="width: 24%;">
|
||||
<col style="width: 12%;">
|
||||
<col style="width: 20%;">
|
||||
<col style="width: 26%;">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Tidak ada data penjualan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($penjualan as $item)
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Tidak ada data penjualan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($jenisLaporan === 'rekap_pembelian')
|
||||
<div class="table-card">
|
||||
<h2>Rekap Pembelian per Produk</h2>
|
||||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th>Total Qty</th>
|
||||
<th>Total Transaksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($rekapPembelian as $item)
|
||||
<div class="table-card">
|
||||
<h2>Rekap Pembelian per Produk</h2>
|
||||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ (int) $item->total_jumlah }}</td>
|
||||
<td>{{ (int) $item->total_transaksi }}</td>
|
||||
<th>Produk</th>
|
||||
<th>Total Qty</th>
|
||||
<th>Total Transaksi</th>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3">Tidak ada data rekap pembelian.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($rekapPembelian as $item)
|
||||
<tr>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ (int) $item->total_jumlah }}</td>
|
||||
<td>{{ (int) $item->total_transaksi }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3">Tidak ada data rekap pembelian.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($jenisLaporan === 'rekap_penjualan')
|
||||
<div class="table-card">
|
||||
<h2>Rekap Penjualan per Produk</h2>
|
||||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th>Total Qty</th>
|
||||
<th>Total Transaksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($rekapPenjualan as $item)
|
||||
<div class="table-card">
|
||||
<h2>Rekap Penjualan per Produk</h2>
|
||||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ (int) $item->total_jumlah }}</td>
|
||||
<td>{{ (int) $item->total_transaksi }}</td>
|
||||
<th>Produk</th>
|
||||
<th>Total Qty</th>
|
||||
<th>Total Transaksi</th>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3">Tidak ada data rekap penjualan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($rekapPenjualan as $item)
|
||||
<tr>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ (int) $item->total_jumlah }}</td>
|
||||
<td>{{ (int) $item->total_transaksi }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="3">Tidak ada data rekap penjualan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($jenisLaporan === 'barang_hilang')
|
||||
<div style="display:grid; grid-template-columns:1fr; gap:16px; margin-bottom:16px;">
|
||||
<div style="border:1px solid #dbe2ea; border-radius:12px; background:#fff; padding:16px;">
|
||||
<div style="font-size:14px; color:#64748b;">Total Barang Hilang</div>
|
||||
<div style="font-size:30px; color:#b91c1c; font-weight:800;">{{ $totalHilang }}</div>
|
||||
<!-- <div style="display:grid; grid-template-columns:1fr; gap:16px; margin-bottom:16px;">
|
||||
<div style="border:1px solid #dbe2ea; border-radius:12px; background:#fff; padding:16px;">
|
||||
<div style="font-size:14px; color:#64748b;">Total Barang Hilang</div>
|
||||
<div style="font-size:30px; color:#b91c1c; font-weight:800;">{{ $totalHilang }}</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="summary-cards" style="grid-template-columns: 1fr;">
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">Total Barang Hilang</div>
|
||||
<div class="summary-value" style="color:#b91c1c;">{{ $totalHilang }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<h2>Laporan Barang Hilang</h2>
|
||||
<div style="font-size:13px; color:#64748b; margin-bottom:10px;">
|
||||
Data diambil dari transaksi barang keluar dengan keterangan mengandung kata "hilang" atau "kehilangan".
|
||||
<div class="table-card">
|
||||
<h2>Laporan Barang Hilang</h2>
|
||||
<div style="font-size:13px; color:#64748b; margin-bottom:10px;">
|
||||
Data diambil dari input barang hilang dengan keterangan mengandung kata "hilang" atau "kehilangan".
|
||||
</div>
|
||||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($barangHilang as $item)
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Tidak ada data barang hilang.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<table class="app-table report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Produk</th>
|
||||
<th>Jumlah</th>
|
||||
<th>User</th>
|
||||
<th>Keterangan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($barangHilang as $item)
|
||||
<tr>
|
||||
<td>{{ $item->created_at?->timezone('Asia/Jakarta')->format('d/m/Y H:i') ?? '—' }}</td>
|
||||
<td>{{ $item->product->nama ?? '-' }}</td>
|
||||
<td>{{ $item->jumlah }}</td>
|
||||
<td>{{ $item->user->name ?? '-' }}</td>
|
||||
<td>{{ $item->keterangan ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">Tidak ada data barang hilang.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
Route::get('/admin/laporan/rekap-pembelian/export', [ReportController::class, 'exportRekapPembelian'])->name('reports.rekap-pembelian.export');
|
||||
Route::get('/admin/laporan/rekap-penjualan/export', [ReportController::class, 'exportRekapPenjualan'])->name('reports.rekap-penjualan.export');
|
||||
Route::get('/admin/laporan/barang-hilang/export', [ReportController::class, 'exportBarangHilang'])->name('reports.barang-hilang.export');
|
||||
|
||||
|
||||
// Backward compatibility: route lama laporan barang
|
||||
Route::get('/admin/laporan-barang', function () {
|
||||
return redirect()->route('reports.pembelian');
|
||||
|
|
|
|||
Loading…
Reference in New Issue