query('filter', 'bulan'); $page = (int) $request->query('page', 1); // Validasi filter if (!in_array($filter, ['hari', 'bulan'])) { return response()->json(['error' => 'Filter harus "hari" atau "bulan"'], 400); } // Cache key berdasarkan filter dan page $cacheKey = "laporan_ringkasan_{$filter}_page_{$page}"; $data = Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filter, $page) { $allSalesNames = $this->getAllSalesNames(); if ($filter === 'hari') { return $this->processLaporanHarian($allSalesNames, $page, true); } return $this->processLaporanBulanan($allSalesNames, $page, true); }); return response()->json($data); } catch (\Exception $e) { Log::error('Error in ringkasan method: ' . $e->getMessage()); return response()->json(['error' => 'Terjadi kesalahan saat mengambil data'], 500); } } /** * Detail laporan per produk dengan validasi dan error handling yang lebih baik */ public function detailPerProduk(Request $request) { try { $validatedData = $request->validate([ 'tanggal' => 'required|date_format:Y-m-d|before_or_equal:today', 'sales_id' => 'nullable|integer|exists:sales,id', 'nampan_id' => 'nullable|integer', 'nama_pembeli' => 'nullable|string|max:255', 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:' . self::MAX_PER_PAGE, ]); $tanggal = $validatedData['tanggal']; $salesId = $request->query('sales_id'); $nampanId = $request->query('nampan_id'); $namaPembeli = $request->query('nama_pembeli'); $page = (int) $request->query('page', 1); $perPage = (int) $request->query('per_page', self::DEFAULT_PER_PAGE); $carbonDate = Carbon::parse($tanggal); // Validasi nampan_id jika ada if ($nampanId && $nampanId != 0) { if (!Nampan::where('id', $nampanId)->exists()) { return response()->json(['error' => 'Nampan tidak ditemukan'], 404); } } $produkTerjualQuery = $this->buildBaseItemQuery($carbonDate); $this->applyFilters($produkTerjualQuery, $salesId, $nampanId, $namaPembeli); $produkTerjual = $produkTerjualQuery ->select( 'produks.id as id_produk', 'produks.nama as nama_produk', DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'), DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan') ) ->groupBy('produks.id', 'produks.nama') ->get() ->keyBy('id_produk'); $totals = $this->calculateTotals($produkTerjual); $semuaProdukPaginated = Produk::select('id', 'nama') ->orderBy('nama') ->paginate($perPage, ['*'], 'page', $page); $detailItem = $this->mapProductsWithSalesData($semuaProdukPaginated, $produkTerjual); $filterInfo = $this->buildFilterInfo($carbonDate, $salesId, $nampanId, $namaPembeli); return response()->json([ 'filter' => $filterInfo, 'rekap_harian' => $totals, 'produk' => $detailItem->values(), 'pagination' => $this->buildPaginationInfo($semuaProdukPaginated), ]); } catch (\Exception $e) { Log::error('Error in detailPerProduk method: ' . $e->getMessage()); return response()->json(['error' => 'Terjadi kesalahan saat mengambil data produk'], 500); } } /** * Detail laporan per nampan dengan perbaikan validasi dan error handling */ public function detailPerNampan(Request $request) { try { $validatedData = $request->validate([ 'tanggal' => 'required|date_format:Y-m-d|before_or_equal:today', 'sales_id' => 'nullable|integer|exists:sales,id', 'produk_id' => 'nullable|integer|exists:produks,id', 'nama_pembeli' => 'nullable|string|max:255', 'page' => 'nullable|integer|min:1', 'per_page' => 'nullable|integer|min:1|max:' . self::MAX_PER_PAGE, ]); $tanggal = $validatedData['tanggal']; $salesId = $request->query('sales_id'); $produkId = $request->query('produk_id'); $namaPembeli = $request->query('nama_pembeli'); $page = (int) $request->query('page', 1); $perPage = (int) $request->query('per_page', self::DEFAULT_PER_PAGE); $carbonDate = Carbon::parse($tanggal); $nampanTerjualQuery = $this->buildBaseItemQuery($carbonDate); $this->applyNampanFilters($nampanTerjualQuery, $salesId, $produkId, $namaPembeli); $nampanTerjual = $nampanTerjualQuery ->leftJoin('nampans', 'items.id_nampan', '=', 'nampans.id') ->select( DB::raw('COALESCE(items.id_nampan, 0) as id_nampan'), DB::raw('COALESCE(nampans.nama, "Brankas") as nama_nampan'), DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), DB::raw('COALESCE(SUM(produks.berat), 0) as berat_terjual'), DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as pendapatan') ) ->groupBy('id_nampan', 'nama_nampan') ->get() ->keyBy('id_nampan'); $totals = $this->calculateTotals($nampanTerjual); $semuaNampanPaginated = $this->getAllNampanWithPagination($page, $perPage); $detailItem = $this->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual); $filterInfo = $this->buildNampanFilterInfo($carbonDate, $salesId, $produkId, $namaPembeli); return response()->json([ 'filter' => $filterInfo, 'rekap_harian' => $totals, 'nampan' => $detailItem->values(), 'pagination' => $this->buildPaginationInfo($semuaNampanPaginated), ]); } catch (\Exception $e) { Log::error('Error in detailPerNampan method: ' . $e->getMessage()); return response()->json(['error' => 'Terjadi kesalahan saat mengambil data nampan'], 500); } } /** * Export laporan ringkasan dengan validasi format */ public function exportRingkasan(Request $request) { try { $validatedData = $request->validate([ 'filter' => 'required|in:hari,bulan', 'format' => 'required|in:pdf,xlsx,csv', ]); $filter = $validatedData['filter']; $format = $validatedData['format']; $allSalesNames = $this->getAllSalesNames(); if ($filter === 'hari') { $data = $this->processLaporanHarian($allSalesNames, 1, false); } else { $data = $this->processLaporanBulanan($allSalesNames, 1, false); } $fileName = "laporan_ringkasan_{$filter}_" . Carbon::now()->format('Ymd') . ".{$format}"; if ($format === 'pdf') { $pdf = PDF::loadView('exports.ringkasan_pdf', [ 'data' => $data, 'filter' => $filter ]); $pdf->setPaper('a4', 'landscape'); return $pdf->download($fileName); } // Format XLSX atau CSV return Excel::download(new RingkasanExport($data), $fileName); } catch (\Exception $e) { Log::error('Error in exportRingkasan method: ' . $e->getMessage()); return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500); } } /** * Helper method untuk mendapatkan semua nama sales dengan caching */ private function getAllSalesNames(): Collection { return Cache::remember('all_sales_names', self::CACHE_TTL, function () { return Transaksi::select('nama_sales')->distinct()->pluck('nama_sales'); }); } /** * Helper method untuk mendapatkan semua nampan dengan pagination */ private function getAllNampanWithPagination(int $page, int $perPage): LengthAwarePaginator { $semuaNampan = Nampan::select('id', 'nama')->orderBy('nama')->get(); $brankasEntry = (object) ['id' => 0, 'nama' => 'Brankas']; $semuaNampanCollection = $semuaNampan->prepend($brankasEntry); $offset = ($page - 1) * $perPage; $itemsForCurrentPage = $semuaNampanCollection->slice($offset, $perPage); return new LengthAwarePaginator( $itemsForCurrentPage, $semuaNampanCollection->count(), $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); } /** * Logika inti untuk menghasilkan data laporan harian yang sudah dioptimasi */ private function processLaporanHarian(Collection $allSalesNames, int $page = 1, bool $limitPagination = true) { $perPage = self::DAILY_PER_PAGE; if ($limitPagination) { $endDate = Carbon::today()->subDays(($page - 1) * $perPage); $startDate = $endDate->copy()->subDays($perPage - 1); $totalHariUntukPaginasi = self::PAGINATION_DAYS_LIMIT; } else { $endDate = Carbon::today(); $startDate = $endDate->copy()->subYear()->addDay(); $totalHariUntukPaginasi = $endDate->diffInDays($startDate) + 1; } $transaksis = Transaksi::with(['itemTransaksi.item.produk']) ->whereBetween('created_at', [$startDate->startOfDay(), $endDate->endOfDay()]) ->orderBy('created_at', 'desc') ->get(); $transaksisByDay = $transaksis->groupBy(function ($transaksi) { return Carbon::parse($transaksi->created_at)->format('Y-m-d'); }); $period = CarbonPeriod::create($startDate, $endDate); $laporan = []; foreach ($period as $date) { $dateString = $date->format('Y-m-d'); $tanggalFormatted = $date->isoFormat('dddd, D MMMM Y'); if (isset($transaksisByDay[$dateString])) { $transaksisPerTanggal = $transaksisByDay[$dateString]; $salesDataTransaksi = $transaksisPerTanggal->groupBy('nama_sales') ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); $fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) { return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales); }); $totalItem = $fullSalesData->sum('item_terjual'); $totalBerat = $fullSalesData->sum('berat_terjual_raw'); $totalPendapatan = $fullSalesData->sum('pendapatan_raw'); $laporan[$dateString] = [ 'tanggal' => $tanggalFormatted, 'total_item_terjual' => $totalItem > 0 ? $totalItem : self::DEFAULT_DISPLAY, 'total_berat' => $totalBerat > 0 ? $this->formatWeight($totalBerat) : self::DEFAULT_DISPLAY, 'total_pendapatan' => $totalPendapatan > 0 ? $this->formatCurrency($totalPendapatan) : self::DEFAULT_DISPLAY, 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), ]; } else { $laporan[$dateString] = [ 'tanggal' => $tanggalFormatted, 'total_item_terjual' => self::DEFAULT_DISPLAY, 'total_berat' => self::DEFAULT_DISPLAY, 'total_pendapatan' => self::DEFAULT_DISPLAY, 'sales' => [], ]; } } if ($limitPagination) { return new LengthAwarePaginator( array_reverse(array_values($laporan)), $totalHariUntukPaginasi, $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); } return collect(array_reverse(array_values($laporan))); } /** * Logika inti untuk menghasilkan data laporan bulanan yang sudah dioptimasi */ private function processLaporanBulanan(Collection $allSalesNames, int $page = 1, bool $limitPagination = true) { $perPage = self::MONTHLY_PER_PAGE; $transaksis = Transaksi::with(['itemTransaksi.item.produk']) ->orderBy('created_at', 'desc') ->get(); $laporan = $transaksis->groupBy(function ($transaksi) { return Carbon::parse($transaksi->created_at)->format('F Y'); })->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) { $salesDataTransaksi = $transaksisPerTanggal ->groupBy('nama_sales') ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); $fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) { return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales); }); $totalItem = $fullSalesData->sum('item_terjual'); $totalBerat = $fullSalesData->sum('berat_terjual_raw'); $totalPendapatan = $fullSalesData->sum('pendapatan_raw'); return [ 'tanggal' => $tanggal, 'total_item_terjual' => $totalItem > 0 ? $totalItem : self::DEFAULT_DISPLAY, 'total_berat' => $totalBerat > 0 ? $this->formatWeight($totalBerat) : self::DEFAULT_DISPLAY, 'total_pendapatan' => $totalPendapatan > 0 ? $this->formatCurrency($totalPendapatan) : self::DEFAULT_DISPLAY, 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), ]; }); if ($limitPagination) { return new LengthAwarePaginator( $laporan->forPage($page, $perPage)->values(), $laporan->count(), $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); } return $laporan->values(); } /** * Membangun query dasar untuk item transaksi */ private function buildBaseItemQuery(Carbon $carbonDate) { return ItemTransaksi::query() ->join('items', 'item_transaksis.id_item', '=', 'items.id') ->join('produks', 'items.id_produk', '=', 'produks.id') ->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id') ->whereDate('transaksis.created_at', $carbonDate); } /** * Menerapkan filter untuk query produk */ private function applyFilters($query, $salesId, $nampanId, $namaPembeli): void { if ($salesId) { $query->join('sales', 'transaksis.id_sales', '=', 'sales.id') ->where('sales.id', $salesId); } if ($nampanId !== null) { if ($nampanId == 0) { $query->whereNull('items.id_nampan'); } else { $query->where('items.id_nampan', $nampanId); } } if ($namaPembeli) { $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%"); } } /** * Menerapkan filter untuk query nampan */ private function applyNampanFilters($query, $salesId, $produkId, $namaPembeli): void { if ($salesId) { $query->join('sales', 'transaksis.id_sales', '=', 'sales.id') ->where('sales.id', $salesId); } if ($produkId) { $query->where('produks.id', $produkId); } if ($namaPembeli) { $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%"); } } /** * Menghitung total dari data penjualan */ private function calculateTotals(Collection $data): array { $totalPendapatan = $data->sum('pendapatan'); $totalItemTerjual = $data->sum('jumlah_item_terjual'); $totalBeratTerjual = $data->sum('berat_terjual'); return [ 'total_item_terjual' => $totalItemTerjual, 'total_berat_terjual' => $this->formatWeight($totalBeratTerjual), 'total_pendapatan' => $this->formatCurrency($totalPendapatan), ]; } /** * Memetakan produk dengan data penjualan */ private function mapProductsWithSalesData($paginatedData, Collection $salesData): Collection { return $paginatedData->getCollection()->map(function ($item) use ($salesData) { if ($salesData->has($item->id)) { $dataTerjual = $salesData->get($item->id); return [ 'nama_produk' => $item->nama, 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, 'berat_terjual' => $this->formatWeight($dataTerjual->berat_terjual), 'pendapatan' => $this->formatCurrency($dataTerjual->pendapatan), ]; } return [ 'nama_produk' => $item->nama, 'jumlah_item_terjual' => self::DEFAULT_DISPLAY, 'berat_terjual' => self::DEFAULT_DISPLAY, 'pendapatan' => self::DEFAULT_DISPLAY, ]; }); } /** * Memetakan nampan dengan data penjualan */ private function mapNampanWithSalesData($paginatedData, Collection $salesData): Collection { return $paginatedData->getCollection()->map(function ($item) use ($salesData) { if ($salesData->has($item->id)) { $dataTerjual = $salesData->get($item->id); return [ 'nama_nampan' => $item->nama, 'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, 'berat_terjual' => $this->formatWeight($dataTerjual->berat_terjual), 'pendapatan' => $this->formatCurrency($dataTerjual->pendapatan), ]; } return [ 'nama_nampan' => $item->nama, 'jumlah_item_terjual' => self::DEFAULT_DISPLAY, 'berat_terjual' => self::DEFAULT_DISPLAY, 'pendapatan' => self::DEFAULT_DISPLAY, ]; }); } /** * Membangun informasi filter untuk produk */ private function buildFilterInfo(Carbon $carbonDate, $salesId, $nampanId, $namaPembeli): array { $filterInfo = [ 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), 'nama_sales' => null, 'nampan' => null, 'nama_pembeli' => $namaPembeli, ]; if ($salesId) { $sales = Sales::find($salesId); $filterInfo['nama_sales'] = $sales?->nama; } if ($nampanId !== null) { if ($nampanId == 0) { $filterInfo['nampan'] = 'Brankas'; } else { $nampan = Nampan::find($nampanId); $filterInfo['nampan'] = $nampan?->nama; } } return $filterInfo; } /** * Membangun informasi filter untuk nampan */ private function buildNampanFilterInfo(Carbon $carbonDate, $salesId, $produkId, $namaPembeli): array { $filterInfo = [ 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), 'nama_sales' => null, 'produk' => null, 'nama_pembeli' => $namaPembeli, ]; if ($salesId) { $sales = Sales::find($salesId); $filterInfo['nama_sales'] = $sales?->nama; } if ($produkId) { $produk = Produk::find($produkId); $filterInfo['produk'] = $produk?->nama; } return $filterInfo; } /** * Membangun informasi pagination */ private function buildPaginationInfo($paginatedData): array { return [ 'current_page' => $paginatedData->currentPage(), 'last_page' => $paginatedData->lastPage(), 'per_page' => $paginatedData->perPage(), 'total' => $paginatedData->total(), 'from' => $paginatedData->firstItem(), 'to' => $paginatedData->lastItem(), ]; } /** * Menghitung data sales dari transaksi */ private function hitungDataSales(Collection $transaksisPerSales): array { $itemTerjual = $transaksisPerSales->sum(fn($t) => $t->itemTransaksi->count()); $beratTerjual = $transaksisPerSales->sum( fn($t) => $t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0) ); $pendapatan = $transaksisPerSales->sum('total_harga'); return [ 'nama' => $transaksisPerSales->first()->nama_sales, 'item_terjual' => $itemTerjual, 'berat_terjual_raw' => $beratTerjual, 'pendapatan_raw' => $pendapatan, ]; } /** * Default data untuk sales yang tidak ada transaksi */ private function defaultSalesData(string $namaSales): array { return [ 'nama' => $namaSales, 'item_terjual' => 0, 'berat_terjual_raw' => 0, 'pendapatan_raw' => 0, ]; } /** * Format nilai data sales untuk tampilan */ private function formatSalesDataValues(Collection $salesData): Collection { return $salesData->map(function ($sale) { $sale['item_terjual'] = $sale['item_terjual'] > 0 ? $sale['item_terjual'] : self::DEFAULT_DISPLAY; $sale['berat_terjual'] = $sale['berat_terjual_raw'] > 0 ? $this->formatWeight($sale['berat_terjual_raw']) : self::DEFAULT_DISPLAY; $sale['pendapatan'] = $sale['pendapatan_raw'] > 0 ? $this->formatCurrency($sale['pendapatan_raw']) : self::DEFAULT_DISPLAY; unset($sale['berat_terjual_raw'], $sale['pendapatan_raw']); return $sale; }); } /** * Format mata uang */ private function formatCurrency(float $amount): string { return self::CURRENCY_SYMBOL . number_format($amount, 2, ',', '.'); } /** * Format berat */ private function formatWeight(float $weight): string { return number_format($weight, 2, ',', '.') . self::WEIGHT_UNIT; } }