Compare commits
	
		
			2 Commits
		
	
	
		
			86f3e101c8
			...
			10e666a9ce
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 10e666a9ce | ||
|  | ebb17c2a43 | 
| @ -5,6 +5,8 @@ namespace App\Http\Controllers; | ||||
| use App\Models\ItemTransaksi; | ||||
| use App\Models\Produk; | ||||
| use App\Models\Transaksi; | ||||
| use App\Models\Sales; | ||||
| use App\Models\Nampan; | ||||
| use Carbon\Carbon; | ||||
| use Carbon\CarbonPeriod; | ||||
| use Illuminate\Http\Request; | ||||
| @ -14,6 +16,10 @@ use Illuminate\Support\Facades\DB; | ||||
| 
 | ||||
| class LaporanController extends Controller | ||||
| { | ||||
|     private const CURRENCY_SYMBOL = 'Rp '; | ||||
|     private const WEIGHT_UNIT = ' g'; | ||||
|     private const DEFAULT_DISPLAY = '-'; | ||||
| 
 | ||||
|     public function ringkasan(Request $request) | ||||
|     { | ||||
|         $filter = $request->query('filter', 'bulan'); | ||||
| @ -28,10 +34,122 @@ class LaporanController extends Controller | ||||
|         return $this->laporanBulanan($page, $allSalesNames); | ||||
|     } | ||||
| 
 | ||||
|     public function detailPerProduk(Request $request) | ||||
|     { | ||||
|         $request->validate([ | ||||
|             'tanggal' => 'required|date_format:Y-m-d', | ||||
|             'page' => 'nullable|integer|min:1', | ||||
|             'per_page' => 'nullable|integer|min:1|max:100', | ||||
|         ]); | ||||
| 
 | ||||
|         $tanggal = $request->query('tanggal'); | ||||
|         $salesId = $request->query('sales_id'); | ||||
|         $nampanId = $request->query('nampan_id'); | ||||
|         $namaPembeli = $request->query('nama_pembeli'); | ||||
|         $page = $request->query('page', 1); | ||||
|         $perPage = $request->query('per_page', 15); | ||||
| 
 | ||||
|         $carbonDate = Carbon::parse($tanggal); | ||||
| 
 | ||||
|         $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('SUM(produks.berat) as berat_terjual'), | ||||
|                 DB::raw('SUM(item_transaksis.harga_deal) 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), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     public function detailPerNampan(Request $request) | ||||
|     { | ||||
|         $request->validate([ | ||||
|             'tanggal' => 'required|date_format:Y-m-d', | ||||
|             'page' => 'nullable|integer|min:1', | ||||
|             'per_page' => 'nullable|integer|min:1|max:100', | ||||
|         ]); | ||||
| 
 | ||||
|         $tanggal = $request->query('tanggal'); | ||||
|         $salesId = $request->query('sales_id'); | ||||
|         $produkId = $request->query('produk_id'); | ||||
|         $namaPembeli = $request->query('nama_pembeli'); | ||||
|         $page = $request->query('page', 1); | ||||
|         $perPage = $request->query('per_page', 15); | ||||
| 
 | ||||
|         $carbonDate = Carbon::parse($tanggal); | ||||
| 
 | ||||
|         // Query untuk mendapatkan data penjualan per nampan
 | ||||
|         $nampanTerjualQuery = $this->buildBaseItemQuery($carbonDate); | ||||
|         $this->applyNampanFilters($nampanTerjualQuery, $salesId, $produkId, $namaPembeli); | ||||
| 
 | ||||
|         // Menggunakan COALESCE untuk menggabungkan nampan dan brankas
 | ||||
|         $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('SUM(produks.berat) as berat_terjual'), | ||||
|                 DB::raw('SUM(item_transaksis.harga_deal) as pendapatan') | ||||
|             ) | ||||
|             ->groupBy('id_nampan', 'nama_nampan') | ||||
|             ->get() | ||||
|             ->keyBy('id_nampan'); | ||||
| 
 | ||||
|         $totals = $this->calculateTotals($nampanTerjual); | ||||
| 
 | ||||
|         // Mendapatkan semua nampan + entry untuk brankas
 | ||||
|         $semuaNampan = Nampan::select('id', 'nama')->orderBy('nama')->get(); | ||||
|          | ||||
|         // Tambahkan entry brankas (id = 0)
 | ||||
|         $brankasEntry = (object) ['id' => 0, 'nama' => 'Brankas']; | ||||
|         $semuaNampanCollection = $semuaNampan->prepend($brankasEntry); | ||||
| 
 | ||||
|         // Pagination manual
 | ||||
|         $currentPage = $page; | ||||
|         $offset = ($currentPage - 1) * $perPage; | ||||
|         $itemsForCurrentPage = $semuaNampanCollection->slice($offset, $perPage); | ||||
| 
 | ||||
|         $semuaNampanPaginated = new LengthAwarePaginator( | ||||
|             $itemsForCurrentPage, | ||||
|             $semuaNampanCollection->count(), | ||||
|             $perPage, | ||||
|             $currentPage, | ||||
|             ['path' => request()->url(), 'query' => request()->query()] | ||||
|         ); | ||||
| 
 | ||||
|         $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), | ||||
|         ]); | ||||
|     } | ||||
| 
 | ||||
|     private function laporanHarian(int $page, Collection $allSalesNames) | ||||
|     { | ||||
|         $perPage = 7; | ||||
| 
 | ||||
|         $endDate = Carbon::today()->subDays(($page - 1) * $perPage); | ||||
|         $startDate = $endDate->copy()->subDays($perPage - 1); | ||||
| 
 | ||||
| @ -53,7 +171,6 @@ class LaporanController extends Controller | ||||
| 
 | ||||
|             if (isset($transaksisByDay[$dateString])) { | ||||
|                 $transaksisPerTanggal = $transaksisByDay[$dateString]; | ||||
| 
 | ||||
|                 $salesDataTransaksi = $transaksisPerTanggal->groupBy('nama_sales') | ||||
|                     ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); | ||||
| 
 | ||||
| @ -67,21 +184,22 @@ class LaporanController extends Controller | ||||
| 
 | ||||
|                 $laporan[$dateString] = [ | ||||
|                     'tanggal' => $tanggalFormatted, | ||||
|                     'total_item_terjual' => $totalItem > 0 ? $totalItem : '-', | ||||
|                     'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-', | ||||
|                     'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-', | ||||
|                     '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' => '-', | ||||
|                     'total_berat' => '-', | ||||
|                     'total_pendapatan' => '-', | ||||
|                     'total_item_terjual' => self::DEFAULT_DISPLAY, | ||||
|                     'total_berat' => self::DEFAULT_DISPLAY, | ||||
|                     'total_pendapatan' => self::DEFAULT_DISPLAY, | ||||
|                     'sales' => [], | ||||
|                 ]; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         $totalHariUntukPaginasi = 365; | ||||
|         $paginatedData = new LengthAwarePaginator( | ||||
|             array_reverse(array_values($laporan)), | ||||
| @ -104,9 +222,7 @@ class LaporanController extends Controller | ||||
| 
 | ||||
|         $laporan = $transaksis->groupBy(function ($transaksi) { | ||||
|             return Carbon::parse($transaksi->created_at)->format('F Y'); | ||||
|         }) | ||||
|             ->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) { | ||||
| 
 | ||||
|         })->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) { | ||||
|             $salesDataTransaksi = $transaksisPerTanggal | ||||
|                 ->groupBy('nama_sales') | ||||
|                 ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); | ||||
| @ -121,9 +237,9 @@ class LaporanController extends Controller | ||||
| 
 | ||||
|             return [ | ||||
|                 'tanggal' => $tanggal, | ||||
|                     'total_item_terjual' => $totalItem > 0 ? $totalItem : '-', | ||||
|                     'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-', | ||||
|                     'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-', | ||||
|                 '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(), | ||||
|             ]; | ||||
|         }); | ||||
| @ -139,12 +255,175 @@ class LaporanController extends Controller | ||||
|         return response()->json($paginatedData); | ||||
|     } | ||||
| 
 | ||||
|     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); | ||||
|     } | ||||
| 
 | ||||
|     private function applyFilters($query, $salesId, $nampanId, $namaPembeli) | ||||
|     { | ||||
|         if ($salesId) { | ||||
|             $query->join('sales', 'transaksis.id_sales', '=', 'sales.id') | ||||
|                 ->where('sales.id', $salesId); | ||||
|         } | ||||
| 
 | ||||
|         if ($nampanId) { | ||||
|             if ($nampanId == 0) { | ||||
|                 // Filter untuk brankas (id_nampan = null)
 | ||||
|                 $query->whereNull('items.id_nampan'); | ||||
|             } else { | ||||
|                 // Filter untuk nampan tertentu
 | ||||
|                 $query->where('items.id_nampan', $nampanId); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if ($namaPembeli) { | ||||
|             $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private function applyNampanFilters($query, $salesId, $produkId, $namaPembeli) | ||||
|     { | ||||
|         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}%"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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), | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     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, | ||||
|             ]; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     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, | ||||
|             ]; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     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) { | ||||
|             if ($nampanId == 0) { | ||||
|                 $filterInfo['nampan'] = 'Brankas'; | ||||
|             } else { | ||||
|                 $nampan = Nampan::find($nampanId); | ||||
|                 $filterInfo['nampan'] = $nampan?->nama; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return $filterInfo; | ||||
|     } | ||||
| 
 | ||||
|     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; | ||||
|     } | ||||
| 
 | ||||
|     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(), | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     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) | ||||
|             fn($t) => $t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0) | ||||
|         ); | ||||
|         $pendapatan = $transaksisPerSales->sum('total_harga'); | ||||
| 
 | ||||
| @ -169,108 +448,22 @@ class LaporanController extends Controller | ||||
|     private function formatSalesDataValues(Collection $salesData): Collection | ||||
|     { | ||||
|         return $salesData->map(function ($sale) { | ||||
|             $sale['item_terjual'] = $sale['item_terjual'] > 0 ? $sale['item_terjual'] : '-'; | ||||
|             $sale['berat_terjual'] = $sale['berat_terjual_raw'] > 0 ? number_format($sale['berat_terjual_raw'], 2, ',', '.') . 'g' : '-'; | ||||
|             $sale['pendapatan'] = $sale['pendapatan_raw'] > 0 ? 'Rp' . number_format($sale['pendapatan_raw'], 2, ',', '.') : '-'; | ||||
|             $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; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| public function detail(Request $request) | ||||
|     private function formatCurrency(float $amount): string | ||||
|     { | ||||
|         // 1. VALIDASI DAN PENGAMBILAN PARAMETER FILTER
 | ||||
|         $request->validate([ | ||||
|             'tanggal' => 'required|date_format:Y-m-d', | ||||
|         ]); | ||||
| 
 | ||||
|         $tanggal = $request->query('tanggal'); | ||||
|         $namaSales = $request->query('nama_sales'); | ||||
|         $posisi = $request->query('posisi'); | ||||
|         $namaPembeli = $request->query('nama_pembeli'); // Untuk pencarian
 | ||||
| 
 | ||||
|         $carbonDate = Carbon::parse($tanggal); | ||||
| 
 | ||||
|         // 2. QUERY UTAMA UNTUK MENGAMBIL DATA PRODUK YANG TERJUAL BERDASARKAN FILTER
 | ||||
|         // Query ini hanya akan mengambil produk yang memiliki transaksi sesuai filter.
 | ||||
|         $produkTerjualQuery = 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') | ||||
|             // Filter Wajib: Tanggal
 | ||||
|             ->whereDate('transaksis.created_at', $carbonDate) | ||||
|             // Filter Opsional: Nama Sales
 | ||||
|             ->when($namaSales, function ($query, $namaSales) { | ||||
|                 return $query->where('transaksis.nama_sales', $namaSales); | ||||
|             }) | ||||
|             // Filter Opsional: Posisi Asal Item
 | ||||
|             ->when($posisi, function ($query, $posisi) { | ||||
|                 return $query->where('item_transaksis.posisi_asal', $posisi); | ||||
|             }) | ||||
|             // Filter Opsional: Nama Pembeli (menggunakan LIKE untuk pencarian)
 | ||||
|             ->when($namaPembeli, function ($query, $namaPembeli) { | ||||
|                 return $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%"); | ||||
|             }) | ||||
|             ->select( | ||||
|                 'produks.id as id_produk', | ||||
|                 'produks.nama as nama_produk', | ||||
|                 DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'), | ||||
|                 DB::raw('SUM(produks.berat) as berat_terjual'), | ||||
|                 DB::raw('SUM(item_transaksis.harga_deal) as pendapatan') | ||||
|             ) | ||||
|             ->groupBy('produks.id', 'produks.nama') | ||||
|             ->get() | ||||
|             // Mengubah collection menjadi array asosiatif dengan key id_produk agar mudah dicari
 | ||||
|             ->keyBy('id_produk'); | ||||
| 
 | ||||
| 
 | ||||
|         // 3. MENGAMBIL SEMUA PRODUK DARI DATABASE
 | ||||
|         $semuaProduk = Produk::query()->select('id', 'nama')->get(); | ||||
| 
 | ||||
|         // 4. MENGGABUNGKAN DATA SEMUA PRODUK DENGAN PRODUK YANG TERJUAL
 | ||||
|         $detailItem = $semuaProduk->map(function ($produk) use ($produkTerjualQuery) { | ||||
|             // Cek apakah produk ini ada di dalam daftar produk yang terjual
 | ||||
|             if ($produkTerjualQuery->has($produk->id)) { | ||||
|                 $dataTerjual = $produkTerjualQuery->get($produk->id); | ||||
|                 return [ | ||||
|                     'nama_produk' => $produk->nama, | ||||
|                     'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, | ||||
|                     'berat_terjual' => (float) $dataTerjual->berat_terjual, | ||||
|                     'pendapatan' => (float) $dataTerjual->pendapatan, | ||||
|                 ]; | ||||
|             } else { | ||||
|                 // Jika produk tidak terjual, berikan nilai default "-"
 | ||||
|                 return [ | ||||
|                     'nama_produk' => $produk->nama, | ||||
|                     'jumlah_item_terjual' => '-', | ||||
|                     'berat_terjual' => '-', | ||||
|                     'pendapatan' => '-', | ||||
|                 ]; | ||||
|         return self::CURRENCY_SYMBOL . number_format($amount, 2, ',', '.'); | ||||
|     } | ||||
|         }); | ||||
| 
 | ||||
|         // 5. MENGHITUNG TOTAL REKAP HARIAN DARI DATA YANG SUDAH DIFILTER
 | ||||
|         $totalPendapatan = $produkTerjualQuery->sum('pendapatan'); | ||||
|         $totalItemTerjual = $produkTerjualQuery->sum('jumlah_item_terjual'); | ||||
|         $totalBeratTerjual = $produkTerjualQuery->sum('berat_terjual'); | ||||
| 
 | ||||
|         // 6. MENYUSUN STRUKTUR RESPONSE FINAL
 | ||||
|         $response = [ | ||||
|             'filter' => [ | ||||
|                 'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'), | ||||
|                 'nama_sales' => $namaSales, | ||||
|                 'posisi' => $posisi, | ||||
|                 'nama_pembeli' => $namaPembeli, | ||||
|             ], | ||||
|             'rekap_harian' => [ | ||||
|                 'total_item_terjual' => $totalItemTerjual, | ||||
|                 'total_berat_terjual' => $totalBeratTerjual, | ||||
|                 'total_pendapatan' => $totalPendapatan, | ||||
|             ], | ||||
|             'produk' => $detailItem, | ||||
|         ]; | ||||
| 
 | ||||
|         return response()->json($response); | ||||
|     private function formatWeight(float $weight): string | ||||
|     { | ||||
|         return number_format($weight, 2, ',', '.') . self::WEIGHT_UNIT; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,100 +0,0 @@ | ||||
| <template> | ||||
|   <div class="my-6"> | ||||
|     <hr class="border-B mb-5" /> | ||||
|     <div class="flex flex-row mb-3 overflow-x-auto"> | ||||
|       <input type="date" v-model="tanggalDipilih" | ||||
|         class="mt-1 block rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:outline-none focus:ring-D focus:ring-opacity-50 p-2" /> | ||||
|       <InputSelect class="ml-3" :options="opsiSales" v-model="salesDipilih" /> | ||||
|     </div> | ||||
|     <div class="mt-5 overflow-x-auto"> | ||||
|       <table class="w-full border-collapse border border-C rounded-md"> | ||||
|         <thead> | ||||
|           <tr class="bg-C text-D rounded-t-md"> | ||||
|             <th class="border-x border-C px-3 py-3">Nama Produk</th> | ||||
|             <th class="border-x border-C px-3 py-3">Item Terjual</th> | ||||
|             <th class="border-x border-C px-3 py-3">Total Berat</th> | ||||
|             <th class="border-x border-C px-3 py-3">Total Pendapatan</th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr v-if="loading"> | ||||
|             <td colspan="5" class="p-4"> | ||||
|               <div class="flex items-center justify-center w-full h-30"> | ||||
|                 <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div> | ||||
|                 <span class="ml-2 text-gray-600">Memuat data...</span> | ||||
|               </div> | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr v-else-if="!produk.length"> | ||||
|             <td colspan="5" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td> | ||||
|           </tr> | ||||
|           <template v-else v-for="item in produk" :key="item.nama_produk"> | ||||
|             <tr class="hover:bg-B"> | ||||
|               <td class="border-x border-C px-3 py-2 text-center">{{ item.nama_produk }}</td> | ||||
|               <td class="border-x border-C px-3 py-2 text-center">{{ item.jumlah_item_terjual }}</td> | ||||
|               <td class="border-x border-C px-3 py-2 text-center">{{ item.berat_terjual }} gr</td> | ||||
|               <td class="border-x border-C px-3 py-2 text-center">Rp {{ item.pendapatan }}</td> | ||||
|             </tr> | ||||
|           </template> | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, onMounted, watch, computed } from 'vue'; | ||||
| import InputSelect from './InputSelect.vue'; | ||||
| import axios from 'axios'; | ||||
| 
 | ||||
| const tanggalDipilih = ref(''); | ||||
| const data = ref(null); | ||||
| const loading = ref(false); | ||||
| 
 | ||||
| const produk = computed(() => data.value?.produk || []); | ||||
| 
 | ||||
| const salesDipilih = ref(null); | ||||
| const opsiSales = ref([ | ||||
|   { label: 'Semua Sales', value: null, selected: true }, | ||||
| ]); | ||||
| 
 | ||||
| const fetchSales = async () => { | ||||
|   try { | ||||
|     const response = await axios.get('/api/sales'); | ||||
|     const salesData = response.data; | ||||
|     opsiSales.value = [{ label: 'Semua Sales', value: null }, ...salesData.map(sales => ({ | ||||
|       label: sales.nama, | ||||
|       value: sales.id, | ||||
|     }))]; | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data sales:', error); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const fetchData = async (date) => { | ||||
|   if (!date) return; | ||||
| 
 | ||||
|   loading.value = true; | ||||
| 
 | ||||
|   try { | ||||
|     const response = await axios.get(`/api/detail-laporan?tanggal=${date}`); | ||||
|     data.value = response.data;; | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data laporan:', error); | ||||
|     data.value = null; | ||||
|   } finally { | ||||
|     loading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   const today = new Date().toISOString().split('T')[0]; | ||||
|   tanggalDipilih.value = today; | ||||
| 
 | ||||
|   fetchSales(); | ||||
| }); | ||||
| 
 | ||||
| watch(tanggalDipilih, (newDate) => { | ||||
|   fetchData(newDate); | ||||
| }, { immediate: true }); | ||||
| </script> | ||||
							
								
								
									
										420
									
								
								resources/js/components/DetailPerNampan.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								resources/js/components/DetailPerNampan.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,420 @@ | ||||
| <template> | ||||
|   <div class="my-6"> | ||||
|     <hr class="border-B mb-5" /> | ||||
|      | ||||
|     <!-- Filter Section --> | ||||
|     <div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8"> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label> | ||||
|         <input type="date" v-model="tanggalDipilih" id="pilihTanggal" | ||||
|           class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:outline-none focus:ring-D focus:ring-opacity-50 p-2" /> | ||||
|       </div> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihSales">Filter Sales:</label> | ||||
|         <InputSelect :options="opsiSales" v-model="salesDipilih" id="pilihSales" /> | ||||
|       </div> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihPelanggan">Nama Pembeli:</label> | ||||
|         <InputField placeholder="Nama pelanggan" v-model="namaPembeli" id="pilihPelanggan" /> | ||||
|       </div> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihProduk">Filter Produk:</label> | ||||
|         <InputSelect :options="opsiProduk" v-model="produkDipilih" id="pilihProduk" /> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Export Section --> | ||||
|     <div class="flex flex-row items-center justify-between mt-5 gap-3"> | ||||
|       <!-- Summary Cards --> | ||||
|       <div class="flex gap-4" v-if="data?.rekap_harian"> | ||||
|         <div class="bg-A p-3 rounded-md border border-C"> | ||||
|           <div class="text-xs text-D/60">Total Item</div> | ||||
|           <div class="font-semibold text-D">{{ data.rekap_harian.total_item_terjual }}</div> | ||||
|         </div> | ||||
|         <div class="bg-A p-3 rounded-md border border-C"> | ||||
|           <div class="text-xs text-D/60">Total Berat</div> | ||||
|           <div class="font-semibold text-D">{{ data.rekap_harian.total_berat_terjual }}</div> | ||||
|         </div> | ||||
|         <div class="bg-A p-3 rounded-md border border-C"> | ||||
|           <div class="text-xs text-D/60">Total Pendapatan</div> | ||||
|           <div class="font-semibold text-D">{{ data.rekap_harian.total_pendapatan }}</div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Export Dropdown --> | ||||
|       <div class="relative w-40" ref="exportDropdownRef"> | ||||
|         <button @click="isExportOpen = !isExportOpen" type="button" | ||||
|           class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none"> | ||||
|           <span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span> | ||||
|           <i class="fas fa-chevron-down"></i> | ||||
|         </button> | ||||
|         <div v-if="isExportOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C right-0"> | ||||
|           <ul class="py-1"> | ||||
|             <li v-for="option in exportOptions" :key="option.value" @click="selectExport(option)" | ||||
|               class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B"> | ||||
|               {{ option.label }} | ||||
|             </li> | ||||
|           </ul> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Table Section --> | ||||
|     <div class="mt-5 overflow-x-auto"> | ||||
|       <table class="w-full border-collapse border border-C rounded-md"> | ||||
|         <thead> | ||||
|           <tr class="bg-C text-D rounded-t-md"> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('nama_nampan')"  | ||||
|                       class="flex items-center justify-between w-full text-left hover:text-D/80 transition-colors"> | ||||
|                 <span>Nama Nampan</span> | ||||
|                 <i :class="getSortIcon('nama_nampan')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('jumlah_item_terjual')"  | ||||
|                       class="flex items-center justify-center w-full hover:text-D/80 transition-colors"> | ||||
|                 <span>Item Terjual</span> | ||||
|                 <i :class="getSortIcon('jumlah_item_terjual')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('berat_terjual')"  | ||||
|                       class="flex items-center justify-center w-full hover:text-D/80 transition-colors"> | ||||
|                 <span>Total Berat</span> | ||||
|                 <i :class="getSortIcon('berat_terjual')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('pendapatan')"  | ||||
|                       class="flex items-center justify-center w-full hover:text-D/80 transition-colors"> | ||||
|                 <span>Total Pendapatan</span> | ||||
|                 <i :class="getSortIcon('pendapatan')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr v-if="loading"> | ||||
|             <td colspan="4" class="p-4"> | ||||
|               <div class="flex items-center justify-center w-full h-30"> | ||||
|                 <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div> | ||||
|                 <span class="ml-2 text-gray-600">Memuat data...</span> | ||||
|               </div> | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr v-else-if="!sortedNampan.length"> | ||||
|             <td colspan="4" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td> | ||||
|           </tr> | ||||
|           <template v-else v-for="item in sortedNampan" :key="item.nama_nampan"> | ||||
|             <tr class="text-center border-y border-C hover:bg-A"> | ||||
|               <td class="border-x border-C px-3 py-2 text-left">{{ item.nama_nampan }}</td> | ||||
|               <td class="border-x border-C px-3 py-2">{{ item.jumlah_item_terjual }}</td> | ||||
|               <td class="border-x border-C px-3 py-2">{{ item.berat_terjual }}</td> | ||||
|               <td class="border-x border-C px-3 py-2"> | ||||
|                 <div class="flex justify-center"> | ||||
|                   <div :ref="el => { if (el) pendapatanElements.push(el) }"  | ||||
|                        :style="pendapatanStyle"  | ||||
|                        :class="item.pendapatan == '-' ? 'text-center' : 'text-right'"> | ||||
|                     {{ item.pendapatan }} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </template> | ||||
|         </tbody> | ||||
|       </table> | ||||
| 
 | ||||
|       <!-- Pagination --> | ||||
|       <div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4"> | ||||
|         <button @click="goToPage(pagination.current_page - 1)"  | ||||
|                 :disabled="pagination.current_page === 1 || loading" | ||||
|                 class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80"> | ||||
|           Sebelumnya | ||||
|         </button> | ||||
|         <span class="text-sm text-D"> | ||||
|           Halaman {{ pagination.current_page }} dari {{ pagination.last_page }} | ||||
|         </span> | ||||
|         <button @click="goToPage(pagination.current_page + 1)" | ||||
|                 :disabled="(pagination.current_page === pagination.last_page) || loading" | ||||
|                 class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80"> | ||||
|           Berikutnya | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'; | ||||
| import InputSelect from './InputSelect.vue'; | ||||
| import InputField from './InputField.vue'; | ||||
| import axios from 'axios'; | ||||
| 
 | ||||
| // --- State --- | ||||
| const isExportOpen = ref(false); | ||||
| const exportDropdownRef = ref(null); | ||||
| 
 | ||||
| const exportOptions = ref([ | ||||
|   { value: 'pdf', label: 'Pdf' }, | ||||
|   { value: 'xls', label: 'Excel' }, | ||||
|   { value: 'csv', label: 'Csv' } | ||||
| ]); | ||||
| 
 | ||||
| const exportFormat = ref(null); | ||||
| const tanggalDipilih = ref(''); | ||||
| const data = ref(null); | ||||
| const loading = ref(false); | ||||
| 
 | ||||
| // Sorting state | ||||
| const sortBy = ref(null); | ||||
| const sortOrder = ref('asc'); // 'asc' or 'desc' | ||||
| 
 | ||||
| const pagination = ref({ | ||||
|   current_page: 1, | ||||
|   last_page: 1, | ||||
|   total: 0, | ||||
| }); | ||||
| 
 | ||||
| const pendapatanWidth = ref(0); | ||||
| const pendapatanElements = ref([]); | ||||
| 
 | ||||
| const salesDipilih = ref(null); | ||||
| const opsiSales = ref([ | ||||
|   { label: 'Semua Sales', value: null, selected: true }, | ||||
| ]); | ||||
| 
 | ||||
| const produkDipilih = ref(null); | ||||
| const opsiProduk = ref([ | ||||
|   { label: 'Semua Produk', value: null, selected: true }, | ||||
| ]); | ||||
| 
 | ||||
| const namaPembeli = ref(null); | ||||
| 
 | ||||
| // --- Computed --- | ||||
| const nampan = computed(() => data.value?.nampan || []); | ||||
| 
 | ||||
| const sortedNampan = computed(() => { | ||||
|   if (!sortBy.value || !nampan.value.length) { | ||||
|     return nampan.value; | ||||
|   } | ||||
| 
 | ||||
|   const sorted = [...nampan.value].sort((a, b) => { | ||||
|     let aValue = a[sortBy.value]; | ||||
|     let bValue = b[sortBy.value]; | ||||
| 
 | ||||
|     // Handle different data types | ||||
|     if (sortBy.value === 'nama_nampan') { | ||||
|       // String comparison | ||||
|       aValue = aValue?.toString().toLowerCase() || ''; | ||||
|       bValue = bValue?.toString().toLowerCase() || ''; | ||||
|     } else if (sortBy.value === 'jumlah_item_terjual') { | ||||
|       // Numeric comparison | ||||
|       aValue = parseInt(aValue) || 0; | ||||
|       bValue = parseInt(bValue) || 0; | ||||
|     } else if (sortBy.value === 'berat_terjual') { | ||||
|       // Handle weight values (remove unit if exists) | ||||
|       aValue = parseFloat(aValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|       bValue = parseFloat(bValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|     } else if (sortBy.value === 'pendapatan') { | ||||
|       // Handle currency values (remove currency symbols and commas) | ||||
|       if (aValue === '-') aValue = 0; | ||||
|       if (bValue === '-') bValue = 0; | ||||
|       aValue = parseFloat(aValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|       bValue = parseFloat(bValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|     } | ||||
| 
 | ||||
|     if (sortOrder.value === 'asc') { | ||||
|       if (typeof aValue === 'string') { | ||||
|         return aValue.localeCompare(bValue); | ||||
|       } | ||||
|       return aValue - bValue; | ||||
|     } else { | ||||
|       if (typeof aValue === 'string') { | ||||
|         return bValue.localeCompare(aValue); | ||||
|       } | ||||
|       return bValue - aValue; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   return sorted; | ||||
| }); | ||||
| 
 | ||||
| const selectedExportLabel = computed(() => { | ||||
|   return exportOptions.value.find(opt => opt.value === exportFormat.value)?.label || 'Export Laporan'; | ||||
| }); | ||||
| 
 | ||||
| const pendapatanStyle = computed(() => ({ | ||||
|   minWidth: `${pendapatanWidth.value}px`, | ||||
|   padding: '0.5rem 0.75rem' | ||||
| })); | ||||
| 
 | ||||
| // --- Watchers --- | ||||
| watch(nampan, async (newValue) => { | ||||
|   if (newValue && newValue.length > 0) { | ||||
|     await nextTick(); | ||||
|     pendapatanElements.value = []; | ||||
|     let maxWidth = 0; | ||||
|      | ||||
|     await nextTick(); | ||||
|     pendapatanElements.value.forEach(el => { | ||||
|       if (el && el.scrollWidth > maxWidth) { | ||||
|         maxWidth = el.scrollWidth; | ||||
|       } | ||||
|     }); | ||||
|     pendapatanWidth.value = maxWidth; | ||||
|   } | ||||
| }, { deep: true }); | ||||
| 
 | ||||
| // --- Methods --- | ||||
| const handleSort = (column) => { | ||||
|   if (sortBy.value === column) { | ||||
|     // If same column, toggle sort order | ||||
|     sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'; | ||||
|   } else { | ||||
|     // If different column, set new column and default to ascending | ||||
|     sortBy.value = column; | ||||
|     sortOrder.value = 'asc'; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const getSortIcon = (column) => { | ||||
|   if (sortBy.value !== column) { | ||||
|     return 'fas fa-sort text-D/40'; // Default sort icon | ||||
|   } | ||||
|    | ||||
|   if (sortOrder.value === 'asc') { | ||||
|     return 'fas fa-sort-up text-D'; // Ascending | ||||
|   } else { | ||||
|     return 'fas fa-sort-down text-D'; // Descending | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const fetchSales = async () => { | ||||
|   try { | ||||
|     const response = await axios.get('/api/sales', { | ||||
|       headers: { | ||||
|         Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|       }, | ||||
|     }); | ||||
|     const salesData = response.data; | ||||
|     opsiSales.value = [ | ||||
|       { label: 'Semua Sales', value: null },  | ||||
|       ...salesData.map(sales => ({ | ||||
|         label: sales.nama, | ||||
|         value: sales.id, | ||||
|       })) | ||||
|     ]; | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data sales:', error); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const fetchProduk = async () => { | ||||
|   try { | ||||
|     const response = await axios.get('/api/produk', { | ||||
|       headers: { | ||||
|         Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|       }, | ||||
|     }); | ||||
|     const produkData = response.data; | ||||
|     opsiProduk.value = [ | ||||
|       { label: 'Semua Produk', value: null },  | ||||
|       ...produkData.map(produk => ({ | ||||
|         label: produk.nama, | ||||
|         value: produk.id, | ||||
|       })) | ||||
|     ]; | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data produk:', error); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const fetchData = async (page = 1) => { | ||||
|   if (!tanggalDipilih.value) return; | ||||
| 
 | ||||
|   loading.value = true; | ||||
|   pendapatanElements.value = []; | ||||
| 
 | ||||
|   let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`; | ||||
|   if (salesDipilih.value) queryParams += `&sales_id=${salesDipilih.value}`; | ||||
|   if (produkDipilih.value) queryParams += `&produk_id=${produkDipilih.value}`; | ||||
|   if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`; | ||||
| 
 | ||||
|   try { | ||||
|     const response = await axios.get(`/api/detail-per-nampan?${queryParams}`, { | ||||
|       headers: { | ||||
|         Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     data.value = response.data; | ||||
|      | ||||
|     // Handle pagination data if provided by backend | ||||
|     if (response.data.pagination) { | ||||
|       pagination.value = { | ||||
|         current_page: response.data.pagination.current_page, | ||||
|         last_page: response.data.pagination.last_page, | ||||
|         total: response.data.pagination.total, | ||||
|       }; | ||||
|     } else { | ||||
|       // Reset pagination if no pagination data | ||||
|       pagination.value = { | ||||
|         current_page: 1, | ||||
|         last_page: 1, | ||||
|         total: response.data.nampan ? response.data.nampan.length : 0, | ||||
|       }; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data laporan nampan:', error); | ||||
|     data.value = null; | ||||
|     pagination.value = { | ||||
|       current_page: 1, | ||||
|       last_page: 1, | ||||
|       total: 0, | ||||
|     }; | ||||
|   } finally { | ||||
|     loading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const goToPage = (page) => { | ||||
|   if (page >= 1 && page <= pagination.value.last_page) { | ||||
|     pagination.value.current_page = page; | ||||
|     fetchData(page); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const selectExport = (option) => { | ||||
|   exportFormat.value = option.value; | ||||
|   isExportOpen.value = false; | ||||
|   alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`); | ||||
| }; | ||||
| 
 | ||||
| const closeDropdownsOnClickOutside = (event) => { | ||||
|   if (exportDropdownRef.value && !exportDropdownRef.value.contains(event.target)) { | ||||
|     isExportOpen.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // --- Lifecycle Hooks --- | ||||
| onMounted(() => { | ||||
|   const today = new Date().toISOString().split('T')[0]; | ||||
|   tanggalDipilih.value = today; | ||||
| 
 | ||||
|   fetchSales(); | ||||
|   fetchProduk(); | ||||
|    | ||||
|   document.addEventListener('click', closeDropdownsOnClickOutside); | ||||
| }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
|   document.removeEventListener('click', closeDropdownsOnClickOutside); | ||||
| }); | ||||
| 
 | ||||
| // Watch for filter changes | ||||
| watch([tanggalDipilih, salesDipilih, produkDipilih, namaPembeli], () => { | ||||
|   pagination.value.current_page = 1; // Reset to first page when filters change | ||||
|   fetchData(1); | ||||
| }, { immediate: true }); | ||||
| </script> | ||||
							
								
								
									
										420
									
								
								resources/js/components/DetailPerProduk.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								resources/js/components/DetailPerProduk.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,420 @@ | ||||
| <template> | ||||
|   <div class="my-6"> | ||||
|     <hr class="border-B mb-5" /> | ||||
|      | ||||
|     <!-- Filter Section --> | ||||
|     <div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8"> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label> | ||||
|         <input type="date" v-model="tanggalDipilih" id="pilihTanggal" | ||||
|           class="mt-1 block w-full rounded-md shadow-sm sm:text-sm bg-A text-D border-B focus:border-C focus:ring focus:outline-none focus:ring-D focus:ring-opacity-50 p-2" /> | ||||
|       </div> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihSales">Filter Sales:</label> | ||||
|         <InputSelect :options="opsiSales" v-model="salesDipilih" id="pilihSales" /> | ||||
|       </div> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihPelanggan">Nama Pembeli:</label> | ||||
|         <InputField placeholder="Nama pelanggan" v-model="namaPembeli" id="pilihPelanggan" /> | ||||
|       </div> | ||||
|       <div class="mb-3 w-full"> | ||||
|         <label class="text-D/80" for="pilihNampan">Filter Nampan:</label> | ||||
|         <InputSelect :options="opsiNampan" v-model="nampanDipilih" id="pilihNampan" /> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Export Section --> | ||||
|     <div class="flex flex-row items-center justify-between mt-5 gap-3"> | ||||
|       <!-- Summary Cards --> | ||||
|       <div class="flex gap-4" v-if="data?.rekap_harian"> | ||||
|         <div class="bg-A p-3 rounded-md border border-C"> | ||||
|           <div class="text-xs text-D/60">Total Item</div> | ||||
|           <div class="font-semibold text-D">{{ data.rekap_harian.total_item_terjual }}</div> | ||||
|         </div> | ||||
|         <div class="bg-A p-3 rounded-md border border-C"> | ||||
|           <div class="text-xs text-D/60">Total Berat</div> | ||||
|           <div class="font-semibold text-D">{{ data.rekap_harian.total_berat_terjual }}</div> | ||||
|         </div> | ||||
|         <div class="bg-A p-3 rounded-md border border-C"> | ||||
|           <div class="text-xs text-D/60">Total Pendapatan</div> | ||||
|           <div class="font-semibold text-D">{{ data.rekap_harian.total_pendapatan }}</div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <!-- Export Dropdown --> | ||||
|       <div class="relative w-40" ref="exportDropdownRef"> | ||||
|         <button @click="isExportOpen = !isExportOpen" type="button" | ||||
|           class="flex items-center justify-between w-full px-3 py-2 text-sm text-left bg-C border rounded-md border-C hover:bg-C/80 focus:outline-none"> | ||||
|           <span :class="{ 'text-D': !exportFormat }">{{ selectedExportLabel }}</span> | ||||
|           <i class="fas fa-chevron-down"></i> | ||||
|         </button> | ||||
|         <div v-if="isExportOpen" class="absolute z-10 w-full mt-1 bg-C border rounded-md shadow-lg border-C right-0"> | ||||
|           <ul class="py-1"> | ||||
|             <li v-for="option in exportOptions" :key="option.value" @click="selectExport(option)" | ||||
|               class="px-3 py-2 text-sm cursor-pointer text-D hover:bg-B"> | ||||
|               {{ option.label }} | ||||
|             </li> | ||||
|           </ul> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Table Section --> | ||||
|     <div class="mt-5 overflow-x-auto"> | ||||
|       <table class="w-full border-collapse border border-C rounded-md"> | ||||
|         <thead> | ||||
|           <tr class="bg-C text-D rounded-t-md"> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('nama_produk')"  | ||||
|                       class="flex items-center justify-between w-full text-left hover:text-D/80 transition-colors"> | ||||
|                 <span>Nama Produk</span> | ||||
|                 <i :class="getSortIcon('nama_produk')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('jumlah_item_terjual')"  | ||||
|                       class="flex items-center justify-center w-full hover:text-D/80 transition-colors"> | ||||
|                 <span>Item Terjual</span> | ||||
|                 <i :class="getSortIcon('jumlah_item_terjual')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('berat_terjual')"  | ||||
|                       class="flex items-center justify-center w-full hover:text-D/80 transition-colors"> | ||||
|                 <span>Total Berat</span> | ||||
|                 <i :class="getSortIcon('berat_terjual')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|             <th class="border-x border-C px-3 py-3"> | ||||
|               <button @click="handleSort('pendapatan')"  | ||||
|                       class="flex items-center justify-center w-full hover:text-D/80 transition-colors"> | ||||
|                 <span>Total Pendapatan</span> | ||||
|                 <i :class="getSortIcon('pendapatan')" class="ml-2"></i> | ||||
|               </button> | ||||
|             </th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           <tr v-if="loading"> | ||||
|             <td colspan="4" class="p-4"> | ||||
|               <div class="flex items-center justify-center w-full h-30"> | ||||
|                 <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-D"></div> | ||||
|                 <span class="ml-2 text-gray-600">Memuat data...</span> | ||||
|               </div> | ||||
|             </td> | ||||
|           </tr> | ||||
|           <tr v-else-if="!sortedProduk.length"> | ||||
|             <td colspan="4" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td> | ||||
|           </tr> | ||||
|           <template v-else v-for="item in sortedProduk" :key="item.nama_produk"> | ||||
|             <tr class="text-center border-y border-C hover:bg-A"> | ||||
|               <td class="border-x border-C px-3 py-2 text-left">{{ item.nama_produk }}</td> | ||||
|               <td class="border-x border-C px-3 py-2">{{ item.jumlah_item_terjual }}</td> | ||||
|               <td class="border-x border-C px-3 py-2">{{ item.berat_terjual }}</td> | ||||
|               <td class="border-x border-C px-3 py-2"> | ||||
|                 <div class="flex justify-center"> | ||||
|                   <div :ref="el => { if (el) pendapatanElements.push(el) }"  | ||||
|                        :style="pendapatanStyle"  | ||||
|                        :class="item.pendapatan == '-' ? 'text-center' : 'text-right'"> | ||||
|                     {{ item.pendapatan }} | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </template> | ||||
|         </tbody> | ||||
|       </table> | ||||
| 
 | ||||
|       <!-- Pagination --> | ||||
|       <div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4"> | ||||
|         <button @click="goToPage(pagination.current_page - 1)"  | ||||
|                 :disabled="pagination.current_page === 1 || loading" | ||||
|                 class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80"> | ||||
|           Sebelumnya | ||||
|         </button> | ||||
|         <span class="text-sm text-D"> | ||||
|           Halaman {{ pagination.current_page }} dari {{ pagination.last_page }} | ||||
|         </span> | ||||
|         <button @click="goToPage(pagination.current_page + 1)" | ||||
|                 :disabled="(pagination.current_page === pagination.last_page) || loading" | ||||
|                 class="px-2 py-1 text-sm font-medium border rounded-md bg-C border-C disabled:opacity-50 disabled:cursor-not-allowed hover:bg-C/80"> | ||||
|           Berikutnya | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'; | ||||
| import InputSelect from './InputSelect.vue'; | ||||
| import InputField from './InputField.vue'; | ||||
| import axios from 'axios'; | ||||
| 
 | ||||
| // --- State --- | ||||
| const isExportOpen = ref(false); | ||||
| const exportDropdownRef = ref(null); | ||||
| 
 | ||||
| const exportOptions = ref([ | ||||
|   { value: 'pdf', label: 'Pdf' }, | ||||
|   { value: 'xls', label: 'Excel' }, | ||||
|   { value: 'csv', label: 'Csv' } | ||||
| ]); | ||||
| 
 | ||||
| const exportFormat = ref(null); | ||||
| const tanggalDipilih = ref(''); | ||||
| const data = ref(null); | ||||
| const loading = ref(false); | ||||
| 
 | ||||
| // Sorting state | ||||
| const sortBy = ref(null); | ||||
| const sortOrder = ref('asc'); // 'asc' or 'desc' | ||||
| 
 | ||||
| const pagination = ref({ | ||||
|   current_page: 1, | ||||
|   last_page: 1, | ||||
|   total: 0, | ||||
| }); | ||||
| 
 | ||||
| const pendapatanWidth = ref(0); | ||||
| const pendapatanElements = ref([]); | ||||
| 
 | ||||
| const salesDipilih = ref(null); | ||||
| const opsiSales = ref([ | ||||
|   { label: 'Semua Sales', value: null, selected: true }, | ||||
| ]); | ||||
| 
 | ||||
| const nampanDipilih = ref(null); | ||||
| const opsiNampan = ref([ | ||||
|   { label: 'Semua Nampan', value: null, selected: true }, | ||||
| ]); | ||||
| 
 | ||||
| const namaPembeli = ref(null); | ||||
| 
 | ||||
| // --- Computed --- | ||||
| const produk = computed(() => data.value?.produk || []); | ||||
| 
 | ||||
| const sortedProduk = computed(() => { | ||||
|   if (!sortBy.value || !produk.value.length) { | ||||
|     return produk.value; | ||||
|   } | ||||
| 
 | ||||
|   const sorted = [...produk.value].sort((a, b) => { | ||||
|     let aValue = a[sortBy.value]; | ||||
|     let bValue = b[sortBy.value]; | ||||
| 
 | ||||
|     // Handle different data types | ||||
|     if (sortBy.value === 'nama_produk') { | ||||
|       // String comparison | ||||
|       aValue = aValue?.toString().toLowerCase() || ''; | ||||
|       bValue = bValue?.toString().toLowerCase() || ''; | ||||
|     } else if (sortBy.value === 'jumlah_item_terjual') { | ||||
|       // Numeric comparison | ||||
|       aValue = parseInt(aValue) || 0; | ||||
|       bValue = parseInt(bValue) || 0; | ||||
|     } else if (sortBy.value === 'berat_terjual') { | ||||
|       // Handle weight values (remove unit if exists) | ||||
|       aValue = parseFloat(aValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|       bValue = parseFloat(bValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|     } else if (sortBy.value === 'pendapatan') { | ||||
|       // Handle currency values (remove currency symbols and commas) | ||||
|       if (aValue === '-') aValue = 0; | ||||
|       if (bValue === '-') bValue = 0; | ||||
|       aValue = parseFloat(aValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|       bValue = parseFloat(bValue?.toString().replace(/[^\d.-]/g, '')) || 0; | ||||
|     } | ||||
| 
 | ||||
|     if (sortOrder.value === 'asc') { | ||||
|       if (typeof aValue === 'string') { | ||||
|         return aValue.localeCompare(bValue); | ||||
|       } | ||||
|       return aValue - bValue; | ||||
|     } else { | ||||
|       if (typeof aValue === 'string') { | ||||
|         return bValue.localeCompare(aValue); | ||||
|       } | ||||
|       return bValue - aValue; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   return sorted; | ||||
| }); | ||||
| 
 | ||||
| const selectedExportLabel = computed(() => { | ||||
|   return exportOptions.value.find(opt => opt.value === exportFormat.value)?.label || 'Export Laporan'; | ||||
| }); | ||||
| 
 | ||||
| const pendapatanStyle = computed(() => ({ | ||||
|   minWidth: `${pendapatanWidth.value}px`, | ||||
|   padding: '0.5rem 0.75rem' | ||||
| })); | ||||
| 
 | ||||
| // --- Watchers --- | ||||
| watch(produk, async (newValue) => { | ||||
|   if (newValue && newValue.length > 0) { | ||||
|     await nextTick(); | ||||
|     pendapatanElements.value = []; | ||||
|     let maxWidth = 0; | ||||
|      | ||||
|     await nextTick(); | ||||
|     pendapatanElements.value.forEach(el => { | ||||
|       if (el && el.scrollWidth > maxWidth) { | ||||
|         maxWidth = el.scrollWidth; | ||||
|       } | ||||
|     }); | ||||
|     pendapatanWidth.value = maxWidth; | ||||
|   } | ||||
| }, { deep: true }); | ||||
| 
 | ||||
| // --- Methods --- | ||||
| const handleSort = (column) => { | ||||
|   if (sortBy.value === column) { | ||||
|     // If same column, toggle sort order | ||||
|     sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'; | ||||
|   } else { | ||||
|     // If different column, set new column and default to ascending | ||||
|     sortBy.value = column; | ||||
|     sortOrder.value = 'asc'; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const getSortIcon = (column) => { | ||||
|   if (sortBy.value !== column) { | ||||
|     return 'fas fa-sort text-D/40'; // Default sort icon | ||||
|   } | ||||
|    | ||||
|   if (sortOrder.value === 'asc') { | ||||
|     return 'fas fa-sort-up text-D'; // Ascending | ||||
|   } else { | ||||
|     return 'fas fa-sort-down text-D'; // Descending | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const fetchSales = async () => { | ||||
|   try { | ||||
|     const response = await axios.get('/api/sales', { | ||||
|       headers: { | ||||
|         Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|       }, | ||||
|     }); | ||||
|     const salesData = response.data; | ||||
|     opsiSales.value = [ | ||||
|       { label: 'Semua Sales', value: null },  | ||||
|       ...salesData.map(sales => ({ | ||||
|         label: sales.nama, | ||||
|         value: sales.id, | ||||
|       })) | ||||
|     ]; | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data sales:', error); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const fetchNampan = async () => { | ||||
|   try { | ||||
|     const response = await axios.get('/api/nampan', { | ||||
|       headers: { | ||||
|         Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|       }, | ||||
|     }); | ||||
|     const nampanData = response.data; | ||||
|     opsiNampan.value = [ | ||||
|       { label: 'Semua Nampan', value: null },  | ||||
|       ...nampanData.map(nampan => ({ | ||||
|         label: nampan.nama, | ||||
|         value: nampan.id, | ||||
|       })) | ||||
|     ]; | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data nampan:', error); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const fetchData = async (page = 1) => { | ||||
|   if (!tanggalDipilih.value) return; | ||||
| 
 | ||||
|   loading.value = true; | ||||
|   pendapatanElements.value = []; | ||||
| 
 | ||||
|   let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`; | ||||
|   if (salesDipilih.value) queryParams += `&sales_id=${salesDipilih.value}`; | ||||
|   if (nampanDipilih.value) queryParams += `&nampan_id=${nampanDipilih.value}`; | ||||
|   if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`; | ||||
| 
 | ||||
|   try { | ||||
|     const response = await axios.get(`/api/detail-per-produk?${queryParams}`, { | ||||
|       headers: { | ||||
|         Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     data.value = response.data; | ||||
|      | ||||
|     // Handle pagination data if provided by backend | ||||
|     if (response.data.pagination) { | ||||
|       pagination.value = { | ||||
|         current_page: response.data.pagination.current_page, | ||||
|         last_page: response.data.pagination.last_page, | ||||
|         total: response.data.pagination.total, | ||||
|       }; | ||||
|     } else { | ||||
|       // Reset pagination if no pagination data | ||||
|       pagination.value = { | ||||
|         current_page: 1, | ||||
|         last_page: 1, | ||||
|         total: response.data.produk ? response.data.produk.length : 0, | ||||
|       }; | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('Gagal mengambil data laporan:', error); | ||||
|     data.value = null; | ||||
|     pagination.value = { | ||||
|       current_page: 1, | ||||
|       last_page: 1, | ||||
|       total: 0, | ||||
|     }; | ||||
|   } finally { | ||||
|     loading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const goToPage = (page) => { | ||||
|   if (page >= 1 && page <= pagination.value.last_page) { | ||||
|     pagination.value.current_page = page; | ||||
|     fetchData(page); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const selectExport = (option) => { | ||||
|   exportFormat.value = option.value; | ||||
|   isExportOpen.value = false; | ||||
|   alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`); | ||||
| }; | ||||
| 
 | ||||
| const closeDropdownsOnClickOutside = (event) => { | ||||
|   if (exportDropdownRef.value && !exportDropdownRef.value.contains(event.target)) { | ||||
|     isExportOpen.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // --- Lifecycle Hooks --- | ||||
| onMounted(() => { | ||||
|   const today = new Date().toISOString().split('T')[0]; | ||||
|   tanggalDipilih.value = today; | ||||
| 
 | ||||
|   fetchSales(); | ||||
|   fetchNampan(); | ||||
|    | ||||
|   document.addEventListener('click', closeDropdownsOnClickOutside); | ||||
| }); | ||||
| 
 | ||||
| onUnmounted(() => { | ||||
|   document.removeEventListener('click', closeDropdownsOnClickOutside); | ||||
| }); | ||||
| 
 | ||||
| // Watch for filter changes | ||||
| watch([tanggalDipilih, salesDipilih, nampanDipilih, namaPembeli], () => { | ||||
|   pagination.value.current_page = 1; // Reset to first page when filters change | ||||
|   fetchData(1); | ||||
| }, { immediate: true }); | ||||
| </script> | ||||
| @ -164,8 +164,16 @@ const saveMove = async () => { | ||||
| const refreshData = async () => { | ||||
|   try { | ||||
|     const [nampanRes, itemRes] = await Promise.all([ | ||||
|       axios.get("/api/nampan"), | ||||
|       axios.get("/api/item"), | ||||
|       axios.get("/api/nampan", { | ||||
|         headers: { | ||||
|           Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|         }, | ||||
|       }), | ||||
|       axios.get("/api/item", { | ||||
|         headers: { | ||||
|           Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
|         }, | ||||
|       }), | ||||
|     ]); | ||||
|     const nampans = nampanRes.data; | ||||
|     const items = itemRes.data; | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| <template> | ||||
|   <mainLayout> | ||||
|     <div class="home"> | ||||
|     <div class="home p-6"> | ||||
|       <h1 class="text-3xl font-bold text-D mb-4">Contoh penggunaan halaman</h1> | ||||
| 
 | ||||
|       <div class="message-model"> | ||||
|         <p>{{ message }}</p> | ||||
|       </div> | ||||
|       <!-- Komponen Struk --> | ||||
|       <StrukOverlay :isOpen="true" /> | ||||
| 
 | ||||
|       <hr class="my-6 border-D" /> | ||||
|       <h1 class="text-xl font-bold text-D mb-4">Contoh grid</h1> | ||||
| @ -21,25 +20,8 @@ | ||||
| <script setup> | ||||
| import { ref } from 'vue' | ||||
| import mainLayout from '../layouts/mainLayout.vue' | ||||
| import StrukOverlay from '../components/StrukOverlay.vue' // pastikan path sesuai | ||||
| 
 | ||||
| const message = ref("Style dan message dari script dan style di dalam halaman") | ||||
| 
 | ||||
| const data = ref([1, 2, 3, 4, 5]) | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .message-model { | ||||
|   border: 1px solid yellow; | ||||
|   text-align: center; | ||||
|   border-radius: 10px; | ||||
|   width: 80%; | ||||
|   margin: auto; | ||||
|   background-color: yellow; | ||||
|   padding: 20px; | ||||
| } | ||||
| 
 | ||||
| .message-model p { | ||||
|   font-weight: bold; | ||||
|   font-size: 24px; | ||||
| } | ||||
| </style> | ||||
| @ -1,18 +1,54 @@ | ||||
| <template> | ||||
|   <mainLayout> | ||||
|     <div class="p-6"> | ||||
|       <p class="font-serif italic text-[25px] text-D">Laporan</p> | ||||
|       <p class="font-serif italic text-[25px] text-D mb-4">Laporan</p> | ||||
| 
 | ||||
|       <div class="mb-4"> | ||||
|         <ul class="flex flex-wrap text-center" role="tablist"> | ||||
|           <li v-for="tab in tabs" class="mr-2" role="presentation"> | ||||
|             <button :class="[ | ||||
|               'inline-block p-2 border-b-2 rounded-t-lg', | ||||
|               activeTab === tab.id | ||||
|                 ? 'border-D text-D' | ||||
|                 : 'border-transparent text-D hover:text-D/50 hover:border-D', | ||||
|             ]" @click="activeTab = tab.id" type="button" role="tab" aria-controls="ringkasan-content" | ||||
|               :aria-selected="activeTab === tab.id"> | ||||
|               {{ tab.name }} | ||||
|             </button> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
| 
 | ||||
|       <div> | ||||
|         <div v-if="activeTab === 'ringkasan'" id="ringkasan-content" role="tabpanel"> | ||||
|           <RingkasanLaporanB /> | ||||
|         </div> | ||||
| 
 | ||||
|       <DetailLaporan /> | ||||
|         <div v-if="activeTab === 'detail-nampan'" id="detail-content" role="tabpanel"> | ||||
|           <DetailPerNampan /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div v-if="activeTab === 'detail-produk'" id="detail-content" role="tabpanel"> | ||||
|           <DetailPerProduk /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </mainLayout> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import DetailLaporan from '../components/DetailLaporan.vue'; | ||||
| import { ref } from 'vue'; | ||||
| import RingkasanLaporanA from '../components/RingkasanLaporanA.vue'; | ||||
| import RingkasanLaporanB from '../components/RingkasanLaporanB.vue'; | ||||
| import mainLayout from "../layouts/mainLayout.vue"; | ||||
| import mainLayout from '../layouts/mainLayout.vue'; | ||||
| import DetailPerNampan from '../components/DetailPerNampan.vue'; | ||||
| import DetailPerProduk from '../components/DetailPerProduk.vue'; | ||||
| 
 | ||||
| const activeTab = ref('ringkasan'); | ||||
| 
 | ||||
| const tabs = [ | ||||
|   { name: 'Ringkasan Laporan', id: 'ringkasan' }, | ||||
|   { name: 'Detail per Nampan', id: 'detail-nampan' }, | ||||
|   { name: 'Detail per Produk', id: 'detail-produk' }, | ||||
| ]; | ||||
| </script> | ||||
| @ -11,6 +11,7 @@ import EditProduk from "../pages/EditProduk.vue"; | ||||
| import Laporan from "../pages/Laporan.vue"; | ||||
| import Login from "../pages/Login.vue"; | ||||
| import Akun from "../pages/Akun.vue"; | ||||
| import Home from "../pages/Home.vue"; | ||||
| 
 | ||||
| import auth from "../middlewares/auth"; | ||||
| import guest from "../middlewares/guest"; | ||||
| @ -26,6 +27,11 @@ const routes = [ | ||||
|         component: Login, | ||||
|         meta: { middleware: "guest" }, | ||||
|     }, | ||||
|     { | ||||
|         path: "/test", | ||||
|         name: "Test", | ||||
|         component: Home | ||||
|     }, | ||||
|     { | ||||
|         path: "/produk", | ||||
|         name: "Produk", | ||||
|  | ||||
| @ -30,8 +30,6 @@ Route::prefix('api')->group(function () { | ||||
|         Route::apiResource('kategori', KategoriController::class)->except(['index', 'show']); | ||||
|         Route::apiResource('user', UserController::class); | ||||
| 
 | ||||
|         // Custom Endpoint
 | ||||
| 
 | ||||
|         Route::delete('kosongkan-nampan', [NampanController::class, 'kosongkan']); | ||||
| 
 | ||||
|         // Foto Sementara
 | ||||
| @ -42,7 +40,8 @@ Route::prefix('api')->group(function () { | ||||
| 
 | ||||
|         // Laporan
 | ||||
|         Route::get('laporan', [LaporanController::class, 'ringkasan']); | ||||
|         Route::get('detail-laporan', [LaporanController::class, 'detail']); | ||||
|         Route::get('detail-per-produk', [LaporanController::class, 'detailPerProduk']); | ||||
|         Route::get('detail-per-nampan', [LaporanController::class, 'detailPerNampan']); | ||||
|     }); | ||||
| 
 | ||||
|     Route::middleware(['auth:sanctum', 'role:owner,kasir'])->group(function () { | ||||
| @ -64,7 +63,7 @@ Route::prefix('api')->group(function () { | ||||
| 
 | ||||
| 
 | ||||
| // ============================
 | ||||
| // Frontend SPA (Vue / React dll.)
 | ||||
| // Frontend SPA (Vue)
 | ||||
| // ============================
 | ||||
| Route::get('/{any}', function () { | ||||
|     return view('app'); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user