Merge branch 'production' of https://git.abbauf.com/Magang-2025/Kasir into production
This commit is contained in:
commit
c28be3706e
69
app/Exports/RingkasanExport.php
Normal file
69
app/Exports/RingkasanExport.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\FromArray;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
|
||||
class RingkasanExport implements FromArray, WithHeadings, ShouldAutoSize
|
||||
{
|
||||
protected $data;
|
||||
|
||||
public function __construct(iterable $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function array(): array
|
||||
{
|
||||
$rows = [];
|
||||
|
||||
// Iterasi setiap hari/bulan
|
||||
foreach ($this->data as $item) {
|
||||
// Baris pertama untuk entri sales pertama
|
||||
if (count($item['sales']) > 0) {
|
||||
foreach ($item['sales'] as $index => $sales) {
|
||||
$rows[] = [
|
||||
'Tanggal' => $item['tanggal'],
|
||||
'Nama Sales' => $sales['nama'],
|
||||
'Item Terjual' => $sales['item_terjual'],
|
||||
'Berat Terjual' => $sales['berat_terjual'],
|
||||
'Pendapatan' => $sales['pendapatan'],
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Baris jika tidak ada sales hari itu
|
||||
$rows[] = [
|
||||
'Tanggal' => $item['tanggal'],
|
||||
'Nama Sales' => 'N/A',
|
||||
'Item Terjual' => 0,
|
||||
'Berat Terjual' => 0,
|
||||
'Pendapatan' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// Baris Total Harian/Bulanan
|
||||
$rows[] = [
|
||||
'Tanggal' => $item['tanggal'],
|
||||
'Nama Sales' => '** TOTAL **', // Tandai sebagai baris total
|
||||
'Item Terjual' => $item['total_item_terjual'],
|
||||
'Berat Terjual' => $item['total_berat'],
|
||||
'Pendapatan' => $item['total_pendapatan'],
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'Periode',
|
||||
'Nama Sales/Keterangan',
|
||||
'Item Terjual',
|
||||
'Total Berat Terjual',
|
||||
'Total Pendapatan',
|
||||
];
|
||||
}
|
||||
}
|
@ -5,37 +5,283 @@ 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;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use App\Exports\RingkasanExport;
|
||||
|
||||
class LaporanController extends Controller
|
||||
{
|
||||
private const CURRENCY_SYMBOL = 'Rp ';
|
||||
private const WEIGHT_UNIT = ' g';
|
||||
private const DEFAULT_DISPLAY = '-';
|
||||
private const CACHE_TTL = 300; // 5 menit
|
||||
private const DEFAULT_PER_PAGE = 15;
|
||||
private const MAX_PER_PAGE = 100;
|
||||
private const DAILY_PER_PAGE = 7;
|
||||
private const MONTHLY_PER_PAGE = 12;
|
||||
private const PAGINATION_DAYS_LIMIT = 365;
|
||||
|
||||
/**
|
||||
* Endpoint untuk ringkasan laporan dengan caching
|
||||
*/
|
||||
public function ringkasan(Request $request)
|
||||
{
|
||||
$filter = $request->query('filter', 'bulan');
|
||||
$page = $request->query('page', 1);
|
||||
try {
|
||||
$filter = $request->query('filter', 'bulan');
|
||||
$page = (int) $request->query('page', 1);
|
||||
|
||||
$allSalesNames = Transaksi::select('nama_sales')->distinct()->pluck('nama_sales');
|
||||
// Validasi filter
|
||||
if (!in_array($filter, ['hari', 'bulan'])) {
|
||||
return response()->json(['error' => 'Filter harus "hari" atau "bulan"'], 400);
|
||||
}
|
||||
|
||||
if ($filter === 'hari') {
|
||||
return $this->laporanHarian($page, $allSalesNames);
|
||||
// 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);
|
||||
}
|
||||
|
||||
return $this->laporanBulanan($page, $allSalesNames);
|
||||
}
|
||||
|
||||
private function laporanHarian(int $page, Collection $allSalesNames)
|
||||
/**
|
||||
* Detail laporan per produk dengan validasi dan error handling yang lebih baik
|
||||
*/
|
||||
public function detailPerProduk(Request $request)
|
||||
{
|
||||
$perPage = 7;
|
||||
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,
|
||||
]);
|
||||
|
||||
$endDate = Carbon::today()->subDays(($page - 1) * $perPage);
|
||||
$startDate = $endDate->copy()->subDays($perPage - 1);
|
||||
$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);
|
||||
|
||||
$transaksis = Transaksi::with('itemTransaksi.item.produk')
|
||||
$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();
|
||||
@ -53,7 +299,6 @@ class LaporanController extends Controller
|
||||
|
||||
if (isset($transaksisByDay[$dateString])) {
|
||||
$transaksisPerTanggal = $transaksisByDay[$dateString];
|
||||
|
||||
$salesDataTransaksi = $transaksisPerTanggal->groupBy('nama_sales')
|
||||
->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales));
|
||||
|
||||
@ -67,84 +312,280 @@ 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)),
|
||||
$totalHariUntukPaginasi,
|
||||
$perPage,
|
||||
$page,
|
||||
['path' => request()->url(), 'query' => request()->query()]
|
||||
);
|
||||
|
||||
return response()->json($paginatedData);
|
||||
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)));
|
||||
}
|
||||
|
||||
private function laporanBulanan(int $page, Collection $allSalesNames)
|
||||
/**
|
||||
* Logika inti untuk menghasilkan data laporan bulanan yang sudah dioptimasi
|
||||
*/
|
||||
private function processLaporanBulanan(Collection $allSalesNames, int $page = 1, bool $limitPagination = true)
|
||||
{
|
||||
$perPage = 12;
|
||||
$perPage = self::MONTHLY_PER_PAGE;
|
||||
|
||||
$transaksis = Transaksi::with('itemTransaksi.item.produk')
|
||||
$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) {
|
||||
})->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) {
|
||||
$salesDataTransaksi = $transaksisPerTanggal
|
||||
->groupBy('nama_sales')
|
||||
->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales));
|
||||
|
||||
$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 : '-',
|
||||
'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-',
|
||||
'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-',
|
||||
'sales' => $this->formatSalesDataValues($fullSalesData)->values(),
|
||||
];
|
||||
$fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) {
|
||||
return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales);
|
||||
});
|
||||
|
||||
$paginatedData = new LengthAwarePaginator(
|
||||
$laporan->forPage($page, $perPage)->values(),
|
||||
$laporan->count(),
|
||||
$perPage,
|
||||
$page,
|
||||
['path' => request()->url(), 'query' => request()->query()]
|
||||
);
|
||||
$totalItem = $fullSalesData->sum('item_terjual');
|
||||
$totalBerat = $fullSalesData->sum('berat_terjual_raw');
|
||||
$totalPendapatan = $fullSalesData->sum('pendapatan_raw');
|
||||
|
||||
return response()->json($paginatedData);
|
||||
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)
|
||||
fn($t) => $t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0)
|
||||
);
|
||||
$pendapatan = $transaksisPerSales->sum('total_harga');
|
||||
|
||||
@ -156,6 +597,9 @@ class LaporanController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Default data untuk sales yang tidak ada transaksi
|
||||
*/
|
||||
private function defaultSalesData(string $namaSales): array
|
||||
{
|
||||
return [
|
||||
@ -166,111 +610,36 @@ class LaporanController extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] : '-';
|
||||
$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)
|
||||
/**
|
||||
* Format mata uang
|
||||
*/
|
||||
private function formatCurrency(float $amount): string
|
||||
{
|
||||
// 1. VALIDASI DAN PENGAMBILAN PARAMETER FILTER
|
||||
$request->validate([
|
||||
'tanggal' => 'required|date_format:Y-m-d',
|
||||
]);
|
||||
return self::CURRENCY_SYMBOL . number_format($amount, 2, ',', '.');
|
||||
}
|
||||
|
||||
$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' => '-',
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
/**
|
||||
* Format berat
|
||||
*/
|
||||
private function formatWeight(float $weight): string
|
||||
{
|
||||
return number_format($weight, 2, ',', '.') . self::WEIGHT_UNIT;
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,11 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"barryvdh/laravel-dompdf": "^3.1",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.2",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"maatwebsite/excel": "^3.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
956
composer.lock
generated
956
composer.lock
generated
@ -4,8 +4,85 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6c1db6bb080cbc76da51ad3d02a29077",
|
||||
"content-hash": "9c49b3a92b2742e4eb3dcf6c597b178a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "barryvdh/laravel-dompdf",
|
||||
"version": "v3.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barryvdh/laravel-dompdf.git",
|
||||
"reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d",
|
||||
"reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dompdf/dompdf": "^3.0",
|
||||
"illuminate/support": "^9|^10|^11|^12",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^2.7|^3.0",
|
||||
"orchestra/testbench": "^7|^8|^9|^10",
|
||||
"phpro/grumphp": "^2.5",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
|
||||
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
|
||||
},
|
||||
"providers": [
|
||||
"Barryvdh\\DomPDF\\ServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Barryvdh\\DomPDF\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A DOMPDF Wrapper for Laravel",
|
||||
"keywords": [
|
||||
"dompdf",
|
||||
"laravel",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
|
||||
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://fruitcake.nl",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/barryvdh",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-02-13T15:07:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.13.1",
|
||||
@ -135,6 +212,162 @@
|
||||
],
|
||||
"time": "2024-02-09T16:56:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
"version": "3.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/pcre.git",
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan": "<1.11.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.12 || ^2",
|
||||
"phpstan/phpstan-strict-rules": "^1 || ^2",
|
||||
"phpunit/phpunit": "^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-main": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\Pcre\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "http://seld.be"
|
||||
}
|
||||
],
|
||||
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
|
||||
"keywords": [
|
||||
"PCRE",
|
||||
"preg",
|
||||
"regex",
|
||||
"regular expression"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/pcre/issues",
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://packagist.com",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/composer",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-11-12T16:29:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.4.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/semver.git",
|
||||
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.3.2 || ^7.0 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"symfony/phpunit-bridge": "^3 || ^7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Composer\\Semver\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nils Adermann",
|
||||
"email": "naderman@naderman.de",
|
||||
"homepage": "http://www.naderman.de"
|
||||
},
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "http://seld.be"
|
||||
},
|
||||
{
|
||||
"name": "Rob Bast",
|
||||
"email": "rob.bast@gmail.com",
|
||||
"homepage": "http://robbast.nl"
|
||||
}
|
||||
],
|
||||
"description": "Semver library that offers utilities, version constraint parsing and validation.",
|
||||
"keywords": [
|
||||
"semantic",
|
||||
"semver",
|
||||
"validation",
|
||||
"versioning"
|
||||
],
|
||||
"support": {
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/semver/issues",
|
||||
"source": "https://github.com/composer/semver/tree/3.4.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://packagist.com",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/composer",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-20T19:15:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dflydev/dot-access-data",
|
||||
"version": "v3.0.3",
|
||||
@ -377,6 +610,161 @@
|
||||
],
|
||||
"time": "2024-02-05T11:56:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"version": "v3.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/dompdf.git",
|
||||
"reference": "a51bd7a063a65499446919286fb18b518177155a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/a51bd7a063a65499446919286fb18b518177155a",
|
||||
"reference": "a51bd7a063a65499446919286fb18b518177155a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dompdf/php-font-lib": "^1.0.0",
|
||||
"dompdf/php-svg-lib": "^1.0.0",
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"masterminds/html5": "^2.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"mockery/mockery": "^1.3",
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Needed to process images",
|
||||
"ext-gmagick": "Improves image processing performance",
|
||||
"ext-imagick": "Improves image processing performance",
|
||||
"ext-zlib": "Needed for pdf stream compression"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dompdf\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Dompdf Community",
|
||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/dompdf/issues",
|
||||
"source": "https://github.com/dompdf/dompdf/tree/v3.1.0"
|
||||
},
|
||||
"time": "2025-01-15T14:09:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/php-font-lib",
|
||||
"version": "1.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-font-lib.git",
|
||||
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
|
||||
"reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FontLib\\": "src/FontLib"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The FontLib Community",
|
||||
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
||||
"homepage": "https://github.com/dompdf/php-font-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.1"
|
||||
},
|
||||
"time": "2024-12-02T14:37:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/php-svg-lib",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-svg-lib.git",
|
||||
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
|
||||
"reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabberworm/php-css-parser": "^8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Svg\\": "src/Svg"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The SvgLib Community",
|
||||
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse and export to PDF SVG files.",
|
||||
"homepage": "https://github.com/dompdf/php-svg-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0"
|
||||
},
|
||||
"time": "2024-04-29T13:26:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dragonmantank/cron-expression",
|
||||
"version": "v3.4.0",
|
||||
@ -509,6 +897,67 @@
|
||||
],
|
||||
"time": "2025-03-06T22:45:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ezyang/htmlpurifier",
|
||||
"version": "v4.18.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ezyang/htmlpurifier.git",
|
||||
"reference": "cb56001e54359df7ae76dc522d08845dc741621b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b",
|
||||
"reference": "cb56001e54359df7ae76dc522d08845dc741621b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cerdic/css-tidy": "^1.7 || ^2.0",
|
||||
"simpletest/simpletest": "dev-master"
|
||||
},
|
||||
"suggest": {
|
||||
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
|
||||
"ext-bcmath": "Used for unit conversion and imagecrash protection",
|
||||
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
|
||||
"ext-tidy": "Used for pretty-printing HTML"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"library/HTMLPurifier.composer.php"
|
||||
],
|
||||
"psr-0": {
|
||||
"HTMLPurifier": "library/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/library/HTMLPurifier/Language/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Edward Z. Yang",
|
||||
"email": "admin@htmlpurifier.org",
|
||||
"homepage": "http://ezyang.com"
|
||||
}
|
||||
],
|
||||
"description": "Standards compliant HTML filter written in PHP",
|
||||
"homepage": "http://htmlpurifier.org/",
|
||||
"keywords": [
|
||||
"html"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ezyang/htmlpurifier/issues",
|
||||
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0"
|
||||
},
|
||||
"time": "2024-11-01T03:51:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
"version": "v1.3.0",
|
||||
@ -2071,6 +2520,339 @@
|
||||
],
|
||||
"time": "2024-12-08T08:18:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maatwebsite/excel",
|
||||
"version": "3.1.67",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
|
||||
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d",
|
||||
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/semver": "^3.3",
|
||||
"ext-json": "*",
|
||||
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0",
|
||||
"php": "^7.0||^8.0",
|
||||
"phpoffice/phpspreadsheet": "^1.30.0",
|
||||
"psr/simple-cache": "^1.0||^2.0||^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
|
||||
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0",
|
||||
"predis/predis": "^1.1"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
|
||||
},
|
||||
"providers": [
|
||||
"Maatwebsite\\Excel\\ExcelServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Maatwebsite\\Excel\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Patrick Brouwers",
|
||||
"email": "patrick@spartner.nl"
|
||||
}
|
||||
],
|
||||
"description": "Supercharged Excel exports and imports in Laravel",
|
||||
"keywords": [
|
||||
"PHPExcel",
|
||||
"batch",
|
||||
"csv",
|
||||
"excel",
|
||||
"export",
|
||||
"import",
|
||||
"laravel",
|
||||
"php",
|
||||
"phpspreadsheet"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
|
||||
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://laravel-excel.com/commercial-support",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/patrickbrouwers",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-26T09:13:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
"version": "3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maennchen/ZipStream-PHP.git",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"ext-zlib": "*",
|
||||
"php-64bit": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"brianium/paratest": "^7.7",
|
||||
"ext-zip": "*",
|
||||
"friendsofphp/php-cs-fixer": "^3.16",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"php-coveralls/php-coveralls": "^2.5",
|
||||
"phpunit/phpunit": "^11.0",
|
||||
"vimeo/psalm": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"guzzlehttp/psr7": "^2.4",
|
||||
"psr/http-message": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ZipStream\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paul Duncan",
|
||||
"email": "pabs@pablotron.org"
|
||||
},
|
||||
{
|
||||
"name": "Jonatan Männchen",
|
||||
"email": "jonatan@maennchen.ch"
|
||||
},
|
||||
{
|
||||
"name": "Jesse Donat",
|
||||
"email": "donatj@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "András Kolesár",
|
||||
"email": "kolesar@kolesar.hu"
|
||||
}
|
||||
],
|
||||
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
|
||||
"keywords": [
|
||||
"stream",
|
||||
"zip"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
|
||||
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/maennchen",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-01-27T12:07:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MarkBaker/PHPComplex.git",
|
||||
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Complex\\": "classes/src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"email": "mark@lange.demon.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "PHP Class for working with complex numbers",
|
||||
"homepage": "https://github.com/MarkBaker/PHPComplex",
|
||||
"keywords": [
|
||||
"complex",
|
||||
"mathematics"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
|
||||
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
|
||||
},
|
||||
"time": "2022-12-06T16:21:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/matrix",
|
||||
"version": "3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MarkBaker/PHPMatrix.git",
|
||||
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
|
||||
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpdocumentor/phpdocumentor": "2.*",
|
||||
"phploc/phploc": "^4.0",
|
||||
"phpmd/phpmd": "2.*",
|
||||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||
"sebastian/phpcpd": "^4.0",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Matrix\\": "classes/src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"email": "mark@demon-angel.eu"
|
||||
}
|
||||
],
|
||||
"description": "PHP Class for working with matrices",
|
||||
"homepage": "https://github.com/MarkBaker/PHPMatrix",
|
||||
"keywords": [
|
||||
"mathematics",
|
||||
"matrix",
|
||||
"vector"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
|
||||
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
|
||||
},
|
||||
"time": "2022-12-02T22:17:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Masterminds/html5-php.git",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Masterminds\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matt Butcher",
|
||||
"email": "technosophos@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Matt Farina",
|
||||
"email": "matt@mattfarina.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An HTML5 parser and serializer.",
|
||||
"homepage": "http://masterminds.github.io/html5-php",
|
||||
"keywords": [
|
||||
"HTML5",
|
||||
"dom",
|
||||
"html",
|
||||
"parser",
|
||||
"querypath",
|
||||
"serializer",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
||||
},
|
||||
"time": "2025-07-25T09:04:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.9.0",
|
||||
@ -2575,6 +3357,112 @@
|
||||
],
|
||||
"time": "2025-05-08T08:14:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "1.30.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "2f39286e0136673778b7a142b3f0d141e43d1714"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2f39286e0136673778b7a142b3f0d141e43d1714",
|
||||
"reference": "2f39286e0136673778b7a142b3f0d141e43d1714",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer/pcre": "^1||^2||^3",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-xmlreader": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"ezyang/htmlpurifier": "^4.15",
|
||||
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
"mpdf/mpdf": "^8.1.1",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.1",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^8.5 || ^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
},
|
||||
"suggest": {
|
||||
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||
"ext-intl": "PHP Internationalization Functions",
|
||||
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
|
||||
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
|
||||
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Maarten Balliauw",
|
||||
"homepage": "https://blog.maartenballiauw.be"
|
||||
},
|
||||
{
|
||||
"name": "Mark Baker",
|
||||
"homepage": "https://markbakeruk.net"
|
||||
},
|
||||
{
|
||||
"name": "Franck Lefevre",
|
||||
"homepage": "https://rootslabs.net"
|
||||
},
|
||||
{
|
||||
"name": "Erik Tilt"
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
}
|
||||
],
|
||||
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
|
||||
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
|
||||
"keywords": [
|
||||
"OpenXML",
|
||||
"excel",
|
||||
"gnumeric",
|
||||
"ods",
|
||||
"php",
|
||||
"spreadsheet",
|
||||
"xls",
|
||||
"xlsx"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.0"
|
||||
},
|
||||
"time": "2025-08-10T06:28:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.4",
|
||||
@ -3338,6 +4226,72 @@
|
||||
},
|
||||
"time": "2025-06-25T14:20:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"version": "v8.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
|
||||
"rawr/cross-data-providers": "^2.0.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "9.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Raphael Schweikert"
|
||||
},
|
||||
{
|
||||
"name": "Oliver Klee",
|
||||
"email": "github@oliverklee.de"
|
||||
},
|
||||
{
|
||||
"name": "Jake Hotson",
|
||||
"email": "jake.github@qzdesign.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "Parser for CSS Files written in PHP",
|
||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||
"keywords": [
|
||||
"css",
|
||||
"parser",
|
||||
"stylesheet"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0"
|
||||
},
|
||||
"time": "2025-07-11T13:20:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/clock",
|
||||
"version": "v7.3.0",
|
||||
|
@ -1,33 +1,94 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<!-- Daftar Item -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="item in filteredItems"
|
||||
:key="item.id"
|
||||
class="flex justify-between items-center border rounded-lg p-3 shadow-sm hover:shadow-md transition cursor-pointer"
|
||||
@click="openMovePopup(item)"
|
||||
>
|
||||
<!-- Gambar & Info Produk -->
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
v-if="item.produk.foto?.length"
|
||||
:src="item.produk.foto[0].url"
|
||||
class="size-12 object-contain"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-semibold">{{ item.produk.nama }}</p>
|
||||
<p class="text-sm text-gray-500">ID: {{ item.produk.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Berat -->
|
||||
<span class="font-medium">{{ item.produk.berat }}g</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Pindah Nampan -->
|
||||
<div
|
||||
v-for="item in filteredItems"
|
||||
:key="item.id"
|
||||
class="flex justify-between items-center border rounded-lg p-3 shadow-sm hover:shadow-md transition"
|
||||
v-if="isPopupVisible"
|
||||
class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
|
||||
>
|
||||
<!-- Gambar -->
|
||||
<div class="flex items-center gap-3">
|
||||
<img
|
||||
v-if="item.produk.foto && item.produk.foto.length > 0"
|
||||
:src="item.produk.foto[0].url"
|
||||
class="w-12 h-12 object-contain"
|
||||
/>
|
||||
<!-- Info produk -->
|
||||
<div>
|
||||
<p class="font-semibold">{{ item.produk.nama }}</p>
|
||||
<p class="text-sm text-gray-500">{{ item.produk.id }}</p>
|
||||
<div class="bg-white rounded-xl shadow-lg max-w-sm w-full p-6 relative">
|
||||
<!-- QR Code -->
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="p-2 border rounded-lg">
|
||||
<img :src="qrCodeUrl" alt="QR Code" class="size-36" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Produk -->
|
||||
<div class="text-center text-gray-700 font-medium mb-1">
|
||||
{{ selectedItem?.produk?.nama }}
|
||||
</div>
|
||||
<div class="text-center text-gray-500 text-sm mb-4">
|
||||
{{ selectedItem?.produk?.kategori }}
|
||||
</div>
|
||||
|
||||
<!-- Dropdown pilih nampan -->
|
||||
<div class="mb-4">
|
||||
<label for="tray-select" class="block text-sm font-medium mb-1">
|
||||
Nama Nampan
|
||||
</label>
|
||||
<select
|
||||
id="tray-select"
|
||||
v-model="selectedTrayId"
|
||||
class="w-full rounded-md border shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
>
|
||||
|
||||
<option v-for="tray in trays" :key="tray.id" :value="tray.id">
|
||||
{{ tray.nama }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Tombol -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
@click="closePopup"
|
||||
class="px-4 py-2 rounded border text-gray-700 hover:bg-gray-100 transition"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
<button
|
||||
@click="saveMove"
|
||||
:disabled="!selectedTrayId"
|
||||
class="px-4 py-2 rounded text-white transition"
|
||||
:class="selectedTrayId ? 'bg-orange-500 hover:bg-orange-600' : 'bg-gray-400 cursor-not-allowed'"
|
||||
>
|
||||
Simpan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Berat -->
|
||||
<span class="font-medium">{{ item.produk.berat }}g</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
|
||||
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
@ -39,26 +100,81 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const items = ref([]);
|
||||
const produk = ref([])
|
||||
const trays = ref([]);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get("/api/item",{
|
||||
headers:{
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
}
|
||||
}); // ganti sesuai URL backend
|
||||
items.value = res.data; // pastikan backend return array of items
|
||||
console.log(res.data);
|
||||
// --- state modal
|
||||
const isPopupVisible = ref(false);
|
||||
const selectedItem = ref(null);
|
||||
const selectedTrayId = ref("");
|
||||
|
||||
// QR Code generator
|
||||
const qrCodeUrl = computed(() => {
|
||||
if (selectedItem.value) {
|
||||
const data = `ITM-${selectedItem.value.id}-${selectedItem.value.produk.nama.replace(/\s/g, "")}`;
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(
|
||||
data
|
||||
)}`;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
// --- fungsi modal
|
||||
const openMovePopup = (item) => {
|
||||
selectedItem.value = item;
|
||||
selectedTrayId.value = item.id_nampan;
|
||||
isPopupVisible.value = true;
|
||||
};
|
||||
const closePopup = () => {
|
||||
isPopupVisible.value = false;
|
||||
selectedItem.value = null;
|
||||
selectedTrayId.value = "";
|
||||
};
|
||||
|
||||
const saveMove = async () => {
|
||||
if (!selectedTrayId.value || !selectedItem.value) return;
|
||||
try {
|
||||
await axios.put(
|
||||
`/api/item/${selectedItem.value.id}`,
|
||||
{
|
||||
id_nampan: selectedTrayId.value,
|
||||
id_produk: selectedItem.value.id_produk,
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
}
|
||||
);
|
||||
|
||||
await refreshData();
|
||||
closePopup();
|
||||
} catch (err) {
|
||||
console.error("Gagal memindahkan item:", err.response?.data || err);
|
||||
alert("Gagal memindahkan item. Silakan coba lagi.");
|
||||
}
|
||||
};
|
||||
|
||||
// --- ambil data
|
||||
const refreshData = async () => {
|
||||
try {
|
||||
const [itemRes, trayRes] = await Promise.all([
|
||||
axios.get("/api/item", {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
}),
|
||||
axios.get("/api/nampan", {
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
}),
|
||||
]);
|
||||
items.value = itemRes.data;
|
||||
trays.value = trayRes.data;
|
||||
} catch (err) {
|
||||
error.value = err.message || "Gagal mengambil data";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(refreshData);
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
if (!props.search) return items.value;
|
||||
|
@ -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>
|
@ -1,185 +1,276 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="grid grid-cols-2 h-full gap-4 mb-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-D">Kode Item *</label>
|
||||
<div class="flex flex-row justify-between mt-1 w-full rounded-md bg-A shadow-sm sm:text-sm border-B">
|
||||
<input type="text" v-model="kodeItem" @keyup.enter="inputItem" placeholder="Scan atau masukkan kode item"
|
||||
class=" bg-A focus:outline-none focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full rounded-l-md" />
|
||||
<button v-if="!loadingItem" @click="inputItem" class="px-3 bg-D hover:bg-D/80 text-A rounded-r-md"><i
|
||||
class="fas fa-arrow-right"></i></button>
|
||||
<div v-else class="flex items-center justify-center px-3">
|
||||
<div class="rounded-full h-5 w-5 border-b-2 border-A flex items-center justify-center">
|
||||
<i class="fas fa-spinner"></i>
|
||||
</div>
|
||||
<ConfirmDeleteModal
|
||||
:isOpen="showDeleteModal"
|
||||
title="Konfirmasi"
|
||||
message="Yakin ingin menghapus item ini?"
|
||||
@confirm="hapusPesanan"
|
||||
@cancel="closeDeleteModal"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="grid grid-cols-2 h-full gap-4 mb-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-D"
|
||||
>Kode Item *</label
|
||||
>
|
||||
<div
|
||||
class="flex flex-row justify-between mt-1 w-full rounded-md bg-A shadow-sm sm:text-sm border-B"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="kodeItem"
|
||||
@keyup.enter="inputItem"
|
||||
placeholder="Scan atau masukkan kode item"
|
||||
class="bg-A focus:outline-none focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full rounded-l-md"
|
||||
/>
|
||||
<button
|
||||
v-if="!loadingItem"
|
||||
@click="inputItem"
|
||||
class="px-3 bg-D hover:bg-D/80 text-A rounded-r-md"
|
||||
>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-center px-3"
|
||||
>
|
||||
<div
|
||||
class="rounded-full h-5 w-5 border-b-2 border-A flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-D"
|
||||
>Harga Jual</label
|
||||
>
|
||||
<InputField
|
||||
v-model="hargaJual"
|
||||
type="number"
|
||||
placeholder="Masukkan Harga Jual"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between gap-4">
|
||||
<button
|
||||
@click="tambahItem"
|
||||
class="px-4 py-2 rounded-md bg-C text-D font-medium hover:bg-C/80 transition"
|
||||
>
|
||||
Tambah Item
|
||||
</button>
|
||||
<button
|
||||
@click="konfirmasiPenjualan"
|
||||
class="px-6 py-2 rounded-md bg-D text-A font-semibold hover:bg-D/80 transition"
|
||||
>
|
||||
Lanjut
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex pt-10 justify-center">
|
||||
<div class="text-start">
|
||||
<span class="block text-gray-600 font-medium">Total:</span>
|
||||
<span class="text-3xl font-bold text-D">
|
||||
Rp{{ total.toLocaleString() }},-
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-D">Harga Jual</label>
|
||||
<InputField v-model="hargaJual" type="number" placeholder="Masukkan Harga Jual" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between gap-4">
|
||||
<button @click="tambahItem" class="px-4 py-2 rounded-md bg-C text-D font-medium hover:bg-C/80 transition">
|
||||
Tambah Item
|
||||
</button>
|
||||
<button @click="konfirmasiPenjualan"
|
||||
class="px-6 py-2 rounded-md bg-D text-A font-semibold hover:bg-D/80 transition">
|
||||
Lanjut
|
||||
</button>
|
||||
<div class="mb-4">
|
||||
<p
|
||||
v-if="error"
|
||||
:class="{ 'animate-shake': error }"
|
||||
class="text-sm text-red-600 mt-1"
|
||||
>
|
||||
{{ error }}
|
||||
</p>
|
||||
<p v-if="info" class="text-sm text-C mt-1">{{ info }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex pt-10 justify-center">
|
||||
<div class="text-start">
|
||||
<span class="block text-gray-600 font-medium">Total:</span>
|
||||
<span class="text-3xl font-bold text-D">
|
||||
Rp{{ total.toLocaleString() }},-
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table
|
||||
class="w-full border border-B text-sm rounded-lg overflow-hidden"
|
||||
>
|
||||
<thead class="bg-A text-D">
|
||||
<tr>
|
||||
<th class="border border-B p-2">No</th>
|
||||
<th class="border border-B p-2">Nam Produk</th>
|
||||
<th class="border border-B p-2">Posisi</th>
|
||||
<th class="border border-B p-2">Harga</th>
|
||||
<th class="border border-B p-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="pesanan.length == 0" class="text-center text-D/70">
|
||||
<td colspan="5" class="h-20 border border-B">
|
||||
Belum ada item dipesan
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-else
|
||||
v-for="(item, index) in pesanan"
|
||||
:key="index"
|
||||
class="hover:bg-gray-50 text-center"
|
||||
>
|
||||
<td class="border border-B p-2">{{ index + 1 }}</td>
|
||||
<td class="border border-B p-2 text-left">
|
||||
{{ item.produk.nama }}
|
||||
</td>
|
||||
<td class="border border-B p-2">
|
||||
{{ item.posisi ? item.posisi : "Brankas" }}
|
||||
</td>
|
||||
<td class="border border-B p-2">
|
||||
Rp{{ item.harga_deal.toLocaleString() }}
|
||||
</td>
|
||||
<td class="border border-B p-2 text-center">
|
||||
<button
|
||||
@click="openDeleteModal(index)"
|
||||
class="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p v-if="error" :class="{ 'animate-shake': error }" class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
<p v-if="info" class="text-sm text-C mt-1">{{ info }}</p>
|
||||
</div>
|
||||
|
||||
<table class="w-full border border-B text-sm rounded-lg overflow-hidden">
|
||||
<thead class="bg-A text-D">
|
||||
<tr>
|
||||
<th class="border border-B p-2">No</th>
|
||||
<th class="border border-B p-2">Nam Produk </th>
|
||||
<th class="border border-B p-2">Posisi</th>
|
||||
<th class="border border-B p-2">Harga</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="pesanan.length == 0" class="text-center text-D/70">
|
||||
<td colspan="5" class="h-20 border border-B">Belum ada item dipesan</td>
|
||||
</tr>
|
||||
<tr v-else v-for="(item, index) in pesanan" :key="index" class="hover:bg-gray-50 text-center">
|
||||
<td class="border border-B p-2">{{ index + 1 }}</td>
|
||||
<td class="border border-B p-2 text-left">{{ item.produk.nama }}</td>
|
||||
<td class="border border-B p-2">{{ item.posisi ? item.posisi : 'Brankas' }}</td>
|
||||
<td class="border border-B p-2">Rp{{ item.harga_deal.toLocaleString() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import InputField from './InputField.vue'
|
||||
import axios from 'axios'
|
||||
import { ref, computed } from "vue";
|
||||
import InputField from "./InputField.vue";
|
||||
import axios from "axios";
|
||||
import ConfirmDeleteModal from "./ConfirmDeleteModal.vue";
|
||||
|
||||
const kodeItem = ref('')
|
||||
const info = ref('')
|
||||
const error = ref('')
|
||||
const hargaJual = ref(null)
|
||||
const item = ref(null)
|
||||
const loadingItem = ref(false)
|
||||
const pesanan = ref([])
|
||||
const kodeItem = ref("");
|
||||
const info = ref("");
|
||||
const error = ref("");
|
||||
const hargaJual = ref(null);
|
||||
const item = ref(null);
|
||||
const loadingItem = ref(false);
|
||||
const pesanan = ref([]);
|
||||
const showDeleteModal = ref(false)
|
||||
const deleteIndex = ref(null)
|
||||
|
||||
let errorTimeout = null
|
||||
let infoTimeout = null
|
||||
let errorTimeout = null;
|
||||
let infoTimeout = null;
|
||||
|
||||
const inputItem = async () => {
|
||||
if (!kodeItem.value) return
|
||||
if (!kodeItem.value) return;
|
||||
|
||||
info.value = ''
|
||||
error.value = ''
|
||||
clearTimeout(infoTimeout)
|
||||
clearTimeout(errorTimeout)
|
||||
info.value = "";
|
||||
error.value = "";
|
||||
clearTimeout(infoTimeout);
|
||||
clearTimeout(errorTimeout);
|
||||
|
||||
loadingItem.value = true
|
||||
loadingItem.value = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/api/item/${kodeItem.value}`, {
|
||||
try {
|
||||
const response = await axios.get(`/api/item/${kodeItem.value}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
});;
|
||||
item.value = response.data;
|
||||
hargaJual.value = item.value.produk.harga_jual
|
||||
});
|
||||
item.value = response.data;
|
||||
hargaJual.value = item.value.produk.harga_jual;
|
||||
|
||||
if (item.value.is_sold) {
|
||||
throw new Error('Item sudah terjual')
|
||||
if (item.value.is_sold) {
|
||||
throw new Error("Item sudah terjual");
|
||||
}
|
||||
if (pesanan.value.some((p) => p.id === item.value.id)) {
|
||||
throw new Error("Item sedang dipesan");
|
||||
}
|
||||
info.value = `Item dipilih: ${item.value.produk.nama} dari ${
|
||||
item.value.posisi ? item.value.posisi : "Brankas"
|
||||
}`;
|
||||
|
||||
infoTimeout = setTimeout(() => {
|
||||
info.value = "";
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
if (err == "") {
|
||||
error.value = "Error: Item tidak ditemukan";
|
||||
} else {
|
||||
error.value = err;
|
||||
}
|
||||
info.value = "";
|
||||
hargaJual.value = null;
|
||||
item.value = null;
|
||||
|
||||
errorTimeout = setTimeout(() => {
|
||||
error.value = "";
|
||||
}, 3000);
|
||||
} finally {
|
||||
loadingItem.value = false;
|
||||
}
|
||||
if (pesanan.value.some(p => p.id === item.value.id)) {
|
||||
throw new Error('Item sedang dipesan')
|
||||
}
|
||||
info.value = `Item dipilih: ${item.value.produk.nama} dari ${item.value.posisi ? item.value.posisi : 'Brankas'}`
|
||||
|
||||
infoTimeout = setTimeout(() => {
|
||||
info.value = ''
|
||||
}, 3000)
|
||||
|
||||
} catch (err) {
|
||||
if (err == '') {
|
||||
error.value = 'Error: Item tidak ditemukan'
|
||||
} else {
|
||||
error.value = err
|
||||
}
|
||||
info.value = ''
|
||||
hargaJual.value = null
|
||||
item.value = null
|
||||
|
||||
errorTimeout = setTimeout(() => {
|
||||
error.value = ''
|
||||
}, 3000)
|
||||
} finally {
|
||||
loadingItem.value = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tambahItem = () => {
|
||||
if (!item.value || !hargaJual.value) {
|
||||
error.value = 'Scan atau masukkan kode item untuk dijual.'
|
||||
if (kodeItem.value) {
|
||||
error.value = 'Masukkan harga jual, atau input dari kode item lagi.'
|
||||
if (!item.value || !hargaJual.value) {
|
||||
error.value = "Scan atau masukkan kode item untuk dijual.";
|
||||
if (kodeItem.value) {
|
||||
error.value =
|
||||
"Masukkan harga jual, atau input dari kode item lagi.";
|
||||
}
|
||||
clearTimeout(errorTimeout);
|
||||
errorTimeout = setTimeout(() => {
|
||||
error.value = "";
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
clearTimeout(errorTimeout)
|
||||
errorTimeout = setTimeout(() => {
|
||||
error.value = ''
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
|
||||
// harga deal
|
||||
item.value.harga_deal = hargaJual.value
|
||||
// harga deal
|
||||
item.value.harga_deal = hargaJual.value;
|
||||
|
||||
pesanan.value.push(item.value)
|
||||
pesanan.value.push(item.value);
|
||||
|
||||
// Reset input fields
|
||||
kodeItem.value = ''
|
||||
hargaJual.value = null
|
||||
item.value = null
|
||||
info.value = ''
|
||||
clearTimeout(infoTimeout)
|
||||
// Reset input fields
|
||||
kodeItem.value = "";
|
||||
hargaJual.value = null;
|
||||
item.value = null;
|
||||
info.value = "";
|
||||
clearTimeout(infoTimeout);
|
||||
};
|
||||
|
||||
const openDeleteModal = (index) => {
|
||||
deleteIndex.value = index
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
const closeDeleteModal = () => {
|
||||
showDeleteModal.value = false
|
||||
deleteIndex.value = null
|
||||
}
|
||||
|
||||
const hapusPesanan = () => {
|
||||
if (deleteIndex.value !== null) {
|
||||
pesanan.value.splice(deleteIndex.value, 1)
|
||||
}
|
||||
closeDeleteModal()
|
||||
}
|
||||
|
||||
|
||||
const konfirmasiPenjualan = () => {
|
||||
if (pesanan.value.length === 0) {
|
||||
error.value = 'Belum ada item yang dipesan.'
|
||||
clearTimeout(errorTimeout)
|
||||
errorTimeout = setTimeout(() => {
|
||||
error.value = ''
|
||||
}, 3000)
|
||||
return
|
||||
}
|
||||
if (pesanan.value.length === 0) {
|
||||
error.value = "Belum ada item yang dipesan.";
|
||||
clearTimeout(errorTimeout);
|
||||
errorTimeout = setTimeout(() => {
|
||||
error.value = "";
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Todo: Implementasi konfirmasi penjualan
|
||||
alert('Penjualan dikonfirmasi! (Implementasi lebih lanjut diperlukan)')
|
||||
}
|
||||
// Todo: Implementasi konfirmasi penjualan
|
||||
alert("Penjualan dikonfirmasi! (Implementasi lebih lanjut diperlukan)");
|
||||
};
|
||||
|
||||
const total = computed(() => {
|
||||
let sum = 0;
|
||||
pesanan.value.forEach(item => {
|
||||
sum += item.harga_deal;
|
||||
});
|
||||
return sum;
|
||||
})
|
||||
let sum = 0;
|
||||
pesanan.value.forEach((item) => {
|
||||
sum += item.harga_deal;
|
||||
});
|
||||
return sum;
|
||||
});
|
||||
</script>
|
||||
|
@ -140,6 +140,7 @@ const exportOptions = ref([
|
||||
]);
|
||||
|
||||
const filterRingkasan = ref("bulan");
|
||||
const loadingExport = ref(false);
|
||||
const exportFormat = ref(null);
|
||||
const ringkasanLaporan = ref([]);
|
||||
const loading = ref(false);
|
||||
@ -217,9 +218,43 @@ const selectFilter = (option) => {
|
||||
};
|
||||
|
||||
const selectExport = (option) => {
|
||||
exportFormat.value = option.value;
|
||||
isExportOpen.value = false;
|
||||
alert(`Fitur Belum dikerjakan. Laporan akan diekspor dalam format ${option.label}`);
|
||||
triggerDownload(option.value);
|
||||
};
|
||||
|
||||
const triggerDownload = async (format) => {
|
||||
loadingExport.value = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/laporan/ringkasan/export', {
|
||||
params: {
|
||||
filter: filterRingkasan.value,
|
||||
format: format
|
||||
},
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
}
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
const fileName = `laporan_${filterRingkasan.value}_${new Date().toISOString().split('T')[0]}.${format}`;
|
||||
|
||||
link.href = url;
|
||||
link.setAttribute('download', fileName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Gagal mengunduh laporan:", error);
|
||||
alert("Terjadi kesalahan saat membuat laporan.");
|
||||
} finally {
|
||||
loadingExport.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeDropdownsOnClickOutside = (event) => {
|
||||
@ -231,7 +266,6 @@ const closeDropdownsOnClickOutside = (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- Lifecycle Hooks ---
|
||||
onMounted(() => {
|
||||
fetchRingkasan(pagination.value.current_page);
|
||||
document.addEventListener('click', closeDropdownsOnClickOutside);
|
||||
|
@ -4,36 +4,42 @@
|
||||
|
||||
<div v-else-if="error" class="text-center text-red-500 py-6">{{ error }}</div>
|
||||
|
||||
<div v-else-if="filteredTrays.length === 0" class="text-center text-gray-500 py-30">
|
||||
<div v-else-if="filteredTrays.length === 0" class="text-center text-gray-500 py-[120px]">
|
||||
Nampan tidak ditemukan.
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<div v-for="tray in filteredTrays" :key="tray.id"
|
||||
class="border border-C rounded-lg p-4 shadow-sm hover:shadow-md transition">
|
||||
<div
|
||||
v-for="tray in filteredTrays"
|
||||
:key="tray.id"
|
||||
class="border rounded-xl p-4 shadow-sm hover:shadow-md transition"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h2 class="font-bold text-lg" style="color: #102C57;">{{ tray.nama }}</h2>
|
||||
<h2 class="font-bold text-lg text-[#102C57]">{{ tray.nama }}</h2>
|
||||
<div class="flex gap-2">
|
||||
<button class="p-2 rounded bg-yellow-400 hover:bg-yellow-500" @click="emit('edit', tray)">
|
||||
✏️
|
||||
</button>
|
||||
<button class="bg-red-500 text-white p-1 rounded" @click="emit('delete', tray.id)">
|
||||
🗑️
|
||||
</button>
|
||||
<button class="p-2 rounded bg-yellow-400 hover:bg-yellow-500" @click="emit('edit', tray)">✏️</button>
|
||||
<button class="bg-red-500 text-white p-1 rounded" @click="emit('delete', tray.id)">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tray.items && tray.items.length > 0" class="space-y-2 max-h-64 overflow-y-auto pr-2">
|
||||
<div v-for="item in tray.items" :key="item.id" class="flex justify-between items-center border border-C rounded-lg p-2"
|
||||
@click="openMovePopup(item)">
|
||||
|
||||
<div v-if="tray.items && tray.items.length" class="space-y-2 max-h-64 overflow-y-auto pr-2">
|
||||
<div
|
||||
v-for="item in tray.items"
|
||||
:key="item.id"
|
||||
class="flex justify-between items-center border rounded-lg p-2 cursor-pointer hover:bg-gray-50"
|
||||
@click="openMovePopup(item)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<img v-if="item.produk.foto && item.produk.foto.length > 0" :src="item.produk.foto[0].url"
|
||||
alt="foto produk" class="w-12 h-12 object-cover rounded" />
|
||||
<div>
|
||||
<p class="text-sm" style="color: #102C57;">{{ item.produk.nama }}</p>
|
||||
<p class="text-sm" style="color: #102C57;">{{ item.produk.kategori }}</p>
|
||||
<p class="text-sm" style="color: #102C57;">{{ item.produk.harga_jual.toLocaleString() }}</p>
|
||||
<img
|
||||
v-if="item.produk.foto && item.produk.foto.length > 0"
|
||||
:src="item.produk.foto[0].url"
|
||||
alt="foto produk"
|
||||
class="size-12 object-cover rounded"
|
||||
/>
|
||||
<div class="text-[#102C57]">
|
||||
<p class="text-sm">{{ item.produk.nama }}</p>
|
||||
<p class="text-sm">{{ item.produk.kategori }}</p>
|
||||
<p class="text-sm">{{ item.produk.harga_jual.toLocaleString() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@ -55,37 +61,57 @@
|
||||
</div>
|
||||
|
||||
<!-- Pop-up pindah item -->
|
||||
<div v-if="isPopupVisible" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div class="bg-white rounded-lg shadow-lg max-w-sm w-full p-6 relative">
|
||||
<div
|
||||
v-if="isPopupVisible"
|
||||
class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
|
||||
>
|
||||
<div class="bg-white rounded-xl shadow-lg max-w-sm w-full p-6 relative">
|
||||
<div class="flex justify-center mb-4">
|
||||
<div class="p-2 border border-gray-300 rounded-lg">
|
||||
<img :src="qrCodeUrl" alt="QR Code" class="w-36 h-36" />
|
||||
<div class="p-2 border rounded-lg">
|
||||
<img :src="qrCodeUrl" alt="QR Code" class="size-36" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-gray-700 font-medium mb-1">{{ selectedItem.produk.nama }}</div>
|
||||
<div class="text-center text-gray-500 text-sm mb-4">{{ selectedItem.produk.kategori }}</div>
|
||||
|
||||
<div class="flex justify-center mb-4">
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition">
|
||||
Cetak
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown: langsung pilih Nampan saat ini -->
|
||||
<div class="mb-4">
|
||||
<label for="tray-select" class="block text-sm font-medium text-gray-700 mb-1">Nama Nampan</label>
|
||||
<select id="tray-select" v-model="selectedTrayId"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
|
||||
<option value="" disabled>Pilih Nampan</option>
|
||||
<option v-for="tray in availableTrays" :key="tray.id" :value="tray.id">
|
||||
{{ tray.nama }}
|
||||
<label for="tray-select" class="block text-sm font-medium mb-1">Nama Nampan</label>
|
||||
<select
|
||||
id="tray-select"
|
||||
v-model="selectedTrayId"
|
||||
class="w-full rounded-md border shadow-sm focus:outline-none focus:ring focus:ring-indigo-200"
|
||||
>
|
||||
<option
|
||||
v-for="tray in trays"
|
||||
:key="tray.id"
|
||||
:value="tray.id"
|
||||
>
|
||||
{{ tray.nama }}<span v-if="Number(tray.id) === Number(selectedItem?.id_nampan)"></span>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button @click="closePopup"
|
||||
class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition">
|
||||
<button
|
||||
@click="closePopup"
|
||||
class="px-4 py-2 rounded border text-gray-700 hover:bg-gray-100 transition"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
<button @click="saveMove" :disabled="!selectedTrayId" class="px-4 py-2 rounded text-white transition"
|
||||
:class="selectedTrayId ? 'bg-orange-500 hover:bg-orange-600' : 'bg-gray-400 cursor-not-allowed'">
|
||||
<button
|
||||
@click="saveMove"
|
||||
:disabled="!selectedTrayId"
|
||||
class="px-4 py-2 rounded text-white transition"
|
||||
:class="selectedTrayId ? 'bg-orange-500 hover:bg-orange-600' : 'bg-gray-400 cursor-not-allowed'"
|
||||
>
|
||||
Simpan
|
||||
</button>
|
||||
</div>
|
||||
@ -98,10 +124,7 @@ import { ref, onMounted, computed } from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
const props = defineProps({
|
||||
search: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
search: { type: String, default: "" },
|
||||
});
|
||||
const emit = defineEmits(["edit", "delete"]);
|
||||
const trays = ref([]);
|
||||
@ -116,16 +139,16 @@ const selectedTrayId = ref("");
|
||||
// QR Code generator
|
||||
const qrCodeUrl = computed(() => {
|
||||
if (selectedItem.value) {
|
||||
const data = `ITM-${selectedItem.value.id}-${selectedItem.value.produk.nama.replace(/\s/g, '')}`;
|
||||
const data = `ITM-${selectedItem.value.id}-${selectedItem.value.produk.nama.replace(/\s/g, "")}`;
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(data)}`;
|
||||
}
|
||||
return '';
|
||||
return "";
|
||||
});
|
||||
|
||||
// --- Fungsi Pop-up ---
|
||||
const openMovePopup = (item) => {
|
||||
selectedItem.value = item;
|
||||
selectedTrayId.value = "";
|
||||
selectedTrayId.value = item.id_nampan; // ✅ tampilkan nampan saat ini (mis. A4)
|
||||
isPopupVisible.value = true;
|
||||
};
|
||||
|
||||
@ -138,18 +161,16 @@ const closePopup = () => {
|
||||
const saveMove = async () => {
|
||||
if (!selectedTrayId.value || !selectedItem.value) return;
|
||||
try {
|
||||
await axios.put(`/api/item/${selectedItem.value.id}`,
|
||||
{
|
||||
id_nampan: selectedTrayId.value,
|
||||
id_produk: selectedItem.value.id_produk,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await axios.put(
|
||||
`/api/item/${selectedItem.value.id}`,
|
||||
{
|
||||
id_nampan: selectedTrayId.value,
|
||||
id_produk: selectedItem.value.id_produk,
|
||||
},
|
||||
{
|
||||
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
|
||||
}
|
||||
);
|
||||
|
||||
await refreshData();
|
||||
closePopup();
|
||||
@ -159,24 +180,24 @@ const saveMove = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- Ambil data nampan + item ---
|
||||
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;
|
||||
|
||||
trays.value = nampans.map((tray) => {
|
||||
return {
|
||||
...tray,
|
||||
// pastikan tipe sama (string/number)
|
||||
items: items.filter((item) => Number(item.id_nampan) === Number(tray.id)),
|
||||
};
|
||||
});
|
||||
trays.value = nampans.map((tray) => ({
|
||||
...tray,
|
||||
items: items.filter((item) => Number(item.id_nampan) === Number(tray.id)),
|
||||
}));
|
||||
} catch (err) {
|
||||
error.value = err.message || "Gagal mengambil data";
|
||||
} finally {
|
||||
@ -199,14 +220,6 @@ const filteredTrays = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
// Daftar nampan lain (selain tempat item saat ini)
|
||||
const availableTrays = computed(() => {
|
||||
if (!selectedItem.value || !trays.value) return [];
|
||||
return trays.value.filter(
|
||||
(tray) => Number(tray.id) !== Number(selectedItem.value.id_nampan)
|
||||
);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
refreshData();
|
||||
});
|
||||
|
@ -1,12 +1,11 @@
|
||||
<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>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
@ -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>
|
||||
</script>
|
@ -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>
|
||||
|
||||
<RingkasanLaporanB />
|
||||
<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>
|
||||
|
||||
<DetailLaporan />
|
||||
<div>
|
||||
<div v-if="activeTab === 'ringkasan'" id="ringkasan-content" role="tabpanel">
|
||||
<RingkasanLaporanB />
|
||||
</div>
|
||||
|
||||
<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>
|
@ -238,11 +238,12 @@ const loadKategori = async () => {
|
||||
|
||||
const loadProduk = async () => {
|
||||
try {
|
||||
await axios.delete(`/api/produk/${detail.value.id}`, {
|
||||
const response = await axios.get(`/api/produk`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
products.value = response.data;
|
||||
}
|
||||
|
@ -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",
|
||||
|
63
resources/views/exports/ringkasan_pdf.blade.php
Normal file
63
resources/views/exports/ringkasan_pdf.blade.php
Normal file
@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Laporan Ringkasan</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; font-size: 10px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
|
||||
th, td { border: 1px solid #ccc; padding: 4px; text-align: left; }
|
||||
th { background-color: #f0f0f0; }
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
tr.total-row td { background-color: #f9f9f9; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2 style="text-align: center;">Laporan Ringkasan {{ ucfirst($filter) }}</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tanggal</th>
|
||||
<th>Nama Sales</th>
|
||||
<th>Item Terjual</th>
|
||||
<th>Berat Terjual</th>
|
||||
<th>Pendapatan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($data as $item)
|
||||
@php $rowCount = count($item['sales']) > 0 ? count($item['sales']) : 1; @endphp
|
||||
|
||||
@if(count($item['sales']) > 0)
|
||||
@foreach($item['sales'] as $index => $sales)
|
||||
<tr>
|
||||
@if($index == 0)
|
||||
<td rowspan="{{ $rowCount }}">{{ $item['tanggal'] }}</td>
|
||||
@endif
|
||||
<td>{{ $sales['nama'] }}</td>
|
||||
<td class="text-center">{{ $sales['item_terjual'] }}</td>
|
||||
<td class="text-right">{{ $sales['berat_terjual'] }}</td>
|
||||
<td class="text-right">{{ $sales['pendapatan'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td>{{ $item['tanggal'] }}</td>
|
||||
<td colspan="4" class="text-center" style="font-style: italic;">Tidak ada data transaksi</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
{{-- Baris Total --}}
|
||||
<tr class="total-row">
|
||||
<td colspan="2" class="text-right"><strong>Total Periode Ini</strong></td>
|
||||
<td class="text-center"><strong>{{ $item['total_item_terjual'] }}</strong></td>
|
||||
<td class="text-right"><strong>{{ $item['total_berat'] }}</strong></td>
|
||||
<td class="text-right"><strong>{{ $item['total_pendapatan'] }}</strong></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -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,10 @@ 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::get('/laporan/ringkasan/export', [LaporanController::class, 'exportRingkasan']);
|
||||
});
|
||||
|
||||
Route::middleware(['auth:sanctum', 'role:owner,kasir'])->group(function () {
|
||||
@ -64,7 +65,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