Merge branch 'production' of https://git.abbauf.com/Magang-2025/Kasir into production
This commit is contained in:
commit
89cc69d789
@ -18,9 +18,10 @@ class LaporanHelper
|
|||||||
|
|
||||||
public function calculateTotals(Collection $data): array
|
public function calculateTotals(Collection $data): array
|
||||||
{
|
{
|
||||||
$totalPendapatan = $data->sum('pendapatan');
|
// Asumsi $data punya raw numeric (int/float)
|
||||||
$totalItemTerjual = $data->sum('jumlah_item_terjual');
|
$totalPendapatan = $data->sum('pendapatan'); // Raw float
|
||||||
$totalBeratTerjual = $data->sum('berat_terjual');
|
$totalItemTerjual = $data->sum('jumlah_item_terjual'); // Int
|
||||||
|
$totalBeratTerjual = $data->sum('berat_terjual'); // Float
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'total_item_terjual' => $totalItemTerjual,
|
'total_item_terjual' => $totalItemTerjual,
|
||||||
@ -57,15 +58,15 @@ class LaporanHelper
|
|||||||
$dataTerjual = $salesData->get($item->id);
|
$dataTerjual = $salesData->get($item->id);
|
||||||
return [
|
return [
|
||||||
'nama_produk' => $item->nama,
|
'nama_produk' => $item->nama,
|
||||||
'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual,
|
'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual, // Selalu int
|
||||||
'berat_terjual' => $this->formatWeight($dataTerjual->berat_terjual),
|
'berat_terjual' => $this->formatWeight($dataTerjual->berat_terjual),
|
||||||
'pendapatan' => $this->formatCurrency($dataTerjual->pendapatan),
|
'pendapatan' => $this->formatCurrency($dataTerjual->pendapatan),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [ // Untuk kosong, return dengan 0 (akan difilter nanti)
|
||||||
'nama_produk' => $item->nama,
|
'nama_produk' => $item->nama,
|
||||||
'jumlah_item_terjual' => self::DEFAULT_DISPLAY,
|
'jumlah_item_terjual' => 0,
|
||||||
'berat_terjual' => self::DEFAULT_DISPLAY,
|
'berat_terjual' => self::DEFAULT_DISPLAY,
|
||||||
'pendapatan' => self::DEFAULT_DISPLAY,
|
'pendapatan' => self::DEFAULT_DISPLAY,
|
||||||
];
|
];
|
||||||
@ -94,12 +95,12 @@ class LaporanHelper
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildProdukFilterInfo(Carbon $carbonDate, array $params): array
|
public function buildProdukFilterInfo(Carbon $startDate, Carbon $endDate, array $params): array
|
||||||
{
|
{
|
||||||
$filterInfo = [
|
$filterInfo = [
|
||||||
'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'),
|
'periode' => "{$startDate->isoFormat('D MMMM Y')} - {$endDate->isoFormat('D MMMM Y')}",
|
||||||
'nama_sales' => null,
|
'nama_sales' => null,
|
||||||
'nampan' => null,
|
'nampan' => null, // Default null
|
||||||
'nama_pembeli' => $params['nama_pembeli'] ?? null,
|
'nama_pembeli' => $params['nama_pembeli'] ?? null,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -109,21 +110,23 @@ class LaporanHelper
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($params['nampan_id'])) {
|
if (isset($params['nampan_id'])) {
|
||||||
if ($params['nampan_id'] == 0) {
|
if ($params['nampan_id'] === -1) {
|
||||||
$filterInfo['nampan'] = 'Brankas';
|
$filterInfo['nampan'] = 'Brankas';
|
||||||
} else {
|
} elseif ($params['nampan_id'] > 0) {
|
||||||
$nampan = Nampan::find($params['nampan_id']);
|
$nampan = Nampan::find($params['nampan_id']);
|
||||||
$filterInfo['nampan'] = $nampan?->nama;
|
$filterInfo['nampan'] = $nampan?->nama;
|
||||||
|
} else { // 0: Semua
|
||||||
|
$filterInfo['nampan'] = 'Semua Nampan';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $filterInfo;
|
return $filterInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildNampanFilterInfo(Carbon $carbonDate, array $params): array
|
public function buildNampanFilterInfo(Carbon $startDate, Carbon $endDate, array $params): array
|
||||||
{
|
{
|
||||||
$filterInfo = [
|
$filterInfo = [
|
||||||
'tanggal' => $carbonDate->isoFormat('dddd, D MMMM Y'),
|
'periode' => "{$startDate->isoFormat('D MMMM Y')} - {$endDate->isoFormat('D MMMM Y')}", // FIXED: Range
|
||||||
'nama_sales' => null,
|
'nama_sales' => null,
|
||||||
'produk' => null,
|
'produk' => null,
|
||||||
'nama_pembeli' => $params['nama_pembeli'] ?? null,
|
'nama_pembeli' => $params['nama_pembeli'] ?? null,
|
||||||
|
|||||||
@ -88,17 +88,18 @@ class LaporanController extends Controller
|
|||||||
public function exportDetailNampan(Request $request)
|
public function exportDetailNampan(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->laporanService->exportPerNampan($request->validate([
|
$validated = $request->validate([
|
||||||
'tanggal' => 'required|string',
|
'start_date' => 'required|date_format:Y-m-d',
|
||||||
|
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||||
'format' => 'required|string|in:pdf,xlsx,csv',
|
'format' => 'required|string|in:pdf,xlsx,csv',
|
||||||
'page' => 'required|integer|min:1',
|
'page' => 'required|integer|min:1',
|
||||||
'sales_id' => 'nullable|integer|exists:sales,id',
|
'sales_id' => 'nullable|integer|exists:sales,id',
|
||||||
'produk_id' => 'nullable|integer|exists:produks,id',
|
'produk_id' => 'nullable|integer|exists:produks,id',
|
||||||
'nama_pembeli' => 'nullable|string|max:255',
|
'nama_pembeli' => 'nullable|string|max:255',
|
||||||
]));
|
]);
|
||||||
|
return $this->laporanService->exportPerNampan($validated);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error in exprot per nampan method: ' . $e->getMessage());
|
Log::error('Error in export per nampan: ' . $e->getMessage());
|
||||||
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,17 +107,18 @@ class LaporanController extends Controller
|
|||||||
public function exportDetailProduk(Request $request)
|
public function exportDetailProduk(Request $request)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return $this->laporanService->exportPerProduk($request->validate([
|
$validated = $request->validate([
|
||||||
'tanggal' => 'required|string',
|
'start_date' => 'required|date_format:Y-m-d',
|
||||||
|
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||||
'format' => 'required|string|in:pdf,xlsx,csv',
|
'format' => 'required|string|in:pdf,xlsx,csv',
|
||||||
'page' => 'required|integer|min:1',
|
'page' => 'required|integer|min:1',
|
||||||
'sales_id' => 'nullable|integer|exists:sales,id',
|
'sales_id' => 'nullable|integer|exists:sales,id',
|
||||||
'nampan_id' => 'nullable|integer|exists:nampans,id',
|
'nampan_id' => 'nullable|integer',
|
||||||
'nama_pembeli' => 'nullable|string|max:255',
|
'nama_pembeli' => 'nullable|string|max:255',
|
||||||
]));
|
]);
|
||||||
|
return $this->laporanService->exportPerProduk($validated);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error in exprot per nampan method: ' . $e->getMessage());
|
Log::error('Error in export per produk: ' . $e->getMessage());
|
||||||
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
return response()->json(['error' => 'Terjadi kesalahan saat export data'], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class ProdukController extends Controller
|
|||||||
|
|
||||||
$validated = $request->validate(
|
$validated = $request->validate(
|
||||||
[
|
[
|
||||||
'nama' => 'required|string|max:100',
|
'nama' => 'required|string|max:100|unique:produks,nama',
|
||||||
'id_kategori' => 'required|exists:kategoris,id',
|
'id_kategori' => 'required|exists:kategoris,id',
|
||||||
'berat' => 'required|numeric',
|
'berat' => 'required|numeric',
|
||||||
'kadar' => 'required|integer',
|
'kadar' => 'required|integer',
|
||||||
@ -42,6 +42,7 @@ class ProdukController extends Controller
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama.required' => 'Nama produk harus diisi.',
|
'nama.required' => 'Nama produk harus diisi.',
|
||||||
|
'nama.unique' => 'Nama produk sudah digunakan.',
|
||||||
'id_kategori' => 'Kategori tidak valid.',
|
'id_kategori' => 'Kategori tidak valid.',
|
||||||
'berat.required' => 'Berat harus diisi.',
|
'berat.required' => 'Berat harus diisi.',
|
||||||
'kadar.required' => 'Kadar harus diisi.',
|
'kadar.required' => 'Kadar harus diisi.',
|
||||||
@ -122,7 +123,7 @@ class ProdukController extends Controller
|
|||||||
|
|
||||||
$validated = $request->validate(
|
$validated = $request->validate(
|
||||||
[
|
[
|
||||||
'nama' => 'required|string|max:100',
|
'nama' => 'required|string|max:100|unique:produks,nama,' . $id,
|
||||||
'id_kategori' => 'required|exists:kategoris,id',
|
'id_kategori' => 'required|exists:kategoris,id',
|
||||||
'berat' => 'required|numeric',
|
'berat' => 'required|numeric',
|
||||||
'kadar' => 'required|integer',
|
'kadar' => 'required|integer',
|
||||||
@ -131,6 +132,7 @@ class ProdukController extends Controller
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'nama.required' => 'Nama produk harus diisi.',
|
'nama.required' => 'Nama produk harus diisi.',
|
||||||
|
'nama.unique' => 'Nama produk sudah digunakan.',
|
||||||
'id_kategori' => 'Kategori tidak valid.',
|
'id_kategori' => 'Kategori tidak valid.',
|
||||||
'berat.required' => 'Berat harus diisi.',
|
'berat.required' => 'Berat harus diisi.',
|
||||||
'kadar.required' => 'Kadar harus diisi',
|
'kadar.required' => 'Kadar harus diisi',
|
||||||
@ -211,7 +213,7 @@ class ProdukController extends Controller
|
|||||||
$foto->delete();
|
$foto->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hapus produk (soft delete)
|
$produk->items()->delete();
|
||||||
$produk->delete();
|
$produk->delete();
|
||||||
|
|
||||||
DB::commit();
|
DB::commit();
|
||||||
|
|||||||
@ -20,7 +20,8 @@ class DetailLaporanRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'tanggal' => 'required|date_format:Y-m-d|before_or_equal:today',
|
'start_date' => 'required|date_format:Y-m-d',
|
||||||
|
'end_date' => 'required|date_format:Y-m-d|after_or_equal:start_date',
|
||||||
'sales_id' => 'nullable|integer|exists:sales,id',
|
'sales_id' => 'nullable|integer|exists:sales,id',
|
||||||
'nampan_id' => 'nullable|integer',
|
'nampan_id' => 'nullable|integer',
|
||||||
'produk_id' => 'nullable|integer|exists:produks,id',
|
'produk_id' => 'nullable|integer|exists:produks,id',
|
||||||
@ -36,9 +37,7 @@ class DetailLaporanRequest extends FormRequest
|
|||||||
public function messages(): array
|
public function messages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'tanggal.required' => 'Tanggal harus diisi',
|
'end_date.after_or_equal' => 'Tanggal akhir harus sama atau setelah tanggal mulai.',
|
||||||
'tanggal.date_format' => 'Format tanggal harus Y-m-d',
|
|
||||||
'tanggal.before_or_equal' => 'Tanggal tidak boleh lebih dari hari ini',
|
|
||||||
'sales_id.exists' => 'Sales tidak ditemukan',
|
'sales_id.exists' => 'Sales tidak ditemukan',
|
||||||
'produk_id.exists' => 'Produk tidak ditemukan',
|
'produk_id.exists' => 'Produk tidak ditemukan',
|
||||||
'nama_pembeli.max' => 'Nama pembeli maksimal 255 karakter',
|
'nama_pembeli.max' => 'Nama pembeli maksimal 255 karakter',
|
||||||
|
|||||||
@ -24,7 +24,7 @@ class Item extends Model
|
|||||||
parent::boot();
|
parent::boot();
|
||||||
|
|
||||||
static::creating(function ($item) {
|
static::creating(function ($item) {
|
||||||
$prefix = 'ITM';
|
$prefix = 'TMJC';
|
||||||
$date = now()->format('Ymd');
|
$date = now()->format('Ymd');
|
||||||
|
|
||||||
// Cari item terakhir yg dibuat hari ini
|
// Cari item terakhir yg dibuat hari ini
|
||||||
|
|||||||
@ -54,20 +54,28 @@ class LaporanService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get paginated sales detail aggregated by product.
|
* Get sales detail aggregated by product (NO PAGINATION - all data).
|
||||||
*
|
*
|
||||||
* @param array $params Filter parameters (tanggal, sales_id, nampan_id, nama_pembeli, page, per_page)
|
* @param array $params Filter parameters (tanggal, sales_id, nampan_id, nama_pembeli)
|
||||||
* @return array Report data structure
|
* @return array Report data structure
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function getDetailPerProduk(array $params)
|
public function getDetailPerProduk(array $params)
|
||||||
{
|
{
|
||||||
$tanggal = Carbon::parse($params['tanggal']);
|
$startDate = Carbon::parse($params['start_date'])->startOfDay();
|
||||||
|
$endDate = Carbon::parse($params['end_date'])->endOfDay();
|
||||||
|
|
||||||
|
// TAMBAH: Validasi range max 30 hari
|
||||||
|
if ($startDate->diffInDays($endDate) > 30) {
|
||||||
|
throw new \InvalidArgumentException('Interval tanggal maksimal 30 hari.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Skip pagination params untuk data utama
|
||||||
$page = $params['page'] ?? 1;
|
$page = $params['page'] ?? 1;
|
||||||
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
|
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
// --- Step 1: Calculate overall totals for all filtered items ---
|
// --- Step 1: Totals ---
|
||||||
$totalsQuery = $this->buildBaseItemQuery($tanggal);
|
$totalsQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
|
||||||
$this->applyFilters($totalsQuery, $params);
|
$this->applyFilters($totalsQuery, $params);
|
||||||
|
|
||||||
$totalsResult = $totalsQuery->select(
|
$totalsResult = $totalsQuery->select(
|
||||||
@ -76,14 +84,14 @@ class LaporanService
|
|||||||
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as total_pendapatan')
|
DB::raw('COALESCE(SUM(item_transaksis.harga_deal), 0) as total_pendapatan')
|
||||||
)->first();
|
)->first();
|
||||||
|
|
||||||
$rekapHarian = [
|
$rekapInterval = [
|
||||||
'total_item_terjual' => (int) $totalsResult->total_item_terjual,
|
'total_item_terjual' => (int) $totalsResult->total_item_terjual,
|
||||||
'total_berat_terjual' => $this->helper->formatWeight($totalsResult->total_berat_terjual),
|
'total_berat_terjual' => $this->helper->formatWeight($totalsResult->total_berat_terjual),
|
||||||
'total_pendapatan' => $this->helper->formatCurrency($totalsResult->total_pendapatan),
|
'total_pendapatan' => $this->helper->formatCurrency($totalsResult->total_pendapatan),
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- Step 2: Build the filtered sales data subquery ---
|
// --- Step 2: Subquery for all products ---
|
||||||
$salesSubQuery = $this->buildBaseItemQuery($tanggal)
|
$salesSubQuery = $this->buildBaseItemQueryForRange($startDate, $endDate)
|
||||||
->select(
|
->select(
|
||||||
'produks.id as id_produk',
|
'produks.id as id_produk',
|
||||||
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
|
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
|
||||||
@ -94,8 +102,8 @@ class LaporanService
|
|||||||
|
|
||||||
$this->applyFilters($salesSubQuery, $params);
|
$this->applyFilters($salesSubQuery, $params);
|
||||||
|
|
||||||
// --- Step 3: Fetch paginated products and LEFT JOIN the sales data subquery ---
|
// --- Step 3: All products (NO PAGINATION) ---
|
||||||
$semuaProdukPaginated = Produk::select(
|
$semuaProduk = Produk::select(
|
||||||
'produks.id',
|
'produks.id',
|
||||||
'produks.nama as nama_produk',
|
'produks.nama as nama_produk',
|
||||||
'sales_data.jumlah_item_terjual',
|
'sales_data.jumlah_item_terjual',
|
||||||
@ -106,36 +114,70 @@ class LaporanService
|
|||||||
$join->on('produks.id', '=', 'sales_data.id_produk');
|
$join->on('produks.id', '=', 'sales_data.id_produk');
|
||||||
})
|
})
|
||||||
->orderBy('produks.nama')
|
->orderBy('produks.nama')
|
||||||
->paginate($perPage, ['*'], 'page', $page);
|
->get(); // FIXED: get() instead of paginate()
|
||||||
|
|
||||||
// --- Step 4: Map results for final presentation ---
|
// --- Step 4: Map & filter ---
|
||||||
$detailItem = $semuaProdukPaginated->map(function ($item) {
|
$detailItem = $semuaProduk->map(function ($item) {
|
||||||
return [
|
return [
|
||||||
'nama_produk' => $item->nama_produk,
|
'nama_produk' => $item->nama_produk,
|
||||||
'jumlah_item_terjual' => $item->jumlah_item_terjual ? (int) $item->jumlah_item_terjual : 0,
|
'jumlah_item_terjual' => $item->jumlah_item_terjual ? (int) $item->jumlah_item_terjual : 0,
|
||||||
'berat_terjual' => $item->berat_terjual ? $this->helper->formatWeight($item->berat_terjual) : '-',
|
'berat_terjual' => $item->berat_terjual ? $this->helper->formatWeight($item->berat_terjual) : '-',
|
||||||
'pendapatan' => $item->pendapatan ? $this->helper->formatCurrency($item->pendapatan) : '-',
|
'pendapatan' => $item->pendapatan ? $this->helper->formatCurrency($item->pendapatan) : '-',
|
||||||
];
|
];
|
||||||
|
})->filter(function ($item) {
|
||||||
|
return $item['jumlah_item_terjual'] > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Step 5: Assemble final response ---
|
// FIXED: Simple collection without pagination
|
||||||
$filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params);
|
$filteredCollection = $detailItem->values();
|
||||||
|
|
||||||
|
// --- Step 5: Response ---
|
||||||
|
$filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'filter' => $filterInfo,
|
'filter' => $filterInfo,
|
||||||
'rekap_harian' => $rekapHarian,
|
'rekap_interval' => $rekapInterval,
|
||||||
'produk' => $detailItem,
|
'produk' => $filteredCollection,
|
||||||
'pagination' => $this->helper->buildPaginationInfo($semuaProdukPaginated),
|
'pagination' => [
|
||||||
|
'current_page' => 1,
|
||||||
|
'from' => 1,
|
||||||
|
'last_page' => 1,
|
||||||
|
'per_page' => $filteredCollection->count(),
|
||||||
|
'to' => $filteredCollection->count(),
|
||||||
|
'total' => $filteredCollection->count(),
|
||||||
|
'has_more_pages' => false,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildBaseItemQueryForRange(Carbon $startDate, Carbon $endDate)
|
||||||
|
{
|
||||||
|
return ItemTransaksi::query()
|
||||||
|
->join('produks', 'item_transaksis.id_produk', '=', 'produks.id')
|
||||||
|
->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id')
|
||||||
|
->whereBetween('transaksis.created_at', [$startDate, $endDate]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get sales detail aggregated by nampan (NO PAGINATION - all data).
|
||||||
|
*
|
||||||
|
* @param array $params Filter parameters (tanggal, sales_id, produk_id, nama_pembeli)
|
||||||
|
* @return array Report data structure
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
public function getDetailPerNampan(array $params)
|
public function getDetailPerNampan(array $params)
|
||||||
{
|
{
|
||||||
$tanggal = Carbon::parse($params['tanggal']);
|
$startDate = Carbon::parse($params['start_date'])->startOfDay();
|
||||||
|
$endDate = Carbon::parse($params['end_date'])->endOfDay();
|
||||||
|
|
||||||
|
if ($startDate->diffInDays($endDate) > 30) {
|
||||||
|
throw new \InvalidArgumentException('Interval tanggal maksimal 30 hari.');
|
||||||
|
}
|
||||||
|
|
||||||
$page = $params['page'] ?? 1;
|
$page = $params['page'] ?? 1;
|
||||||
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
|
$perPage = $params['per_page'] ?? self::DEFAULT_PER_PAGE;
|
||||||
|
|
||||||
$nampanTerjualQuery = $this->buildBaseItemQuery($tanggal);
|
$nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
|
||||||
$this->applyNampanFilters($nampanTerjualQuery, $params);
|
$this->applyNampanFilters($nampanTerjualQuery, $params);
|
||||||
|
|
||||||
$nampanTerjual = $nampanTerjualQuery
|
$nampanTerjual = $nampanTerjualQuery
|
||||||
@ -149,16 +191,41 @@ class LaporanService
|
|||||||
->get()
|
->get()
|
||||||
->keyBy('nama_nampan');
|
->keyBy('nama_nampan');
|
||||||
|
|
||||||
$totals = $this->helper->calculateTotals($nampanTerjual);
|
// FIXED: calculateTotals sum raw (bukan formatted string)
|
||||||
$semuaNampanPaginated = $this->helper->getAllNampanWithPagination($page, $perPage);
|
$nampanTerjualRaw = $nampanTerjual->map(function ($item) {
|
||||||
$detailItem = $this->helper->mapNampanWithSalesData($semuaNampanPaginated, $nampanTerjual);
|
return [
|
||||||
$filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params);
|
'jumlah_item_terjual' => (int) $item->jumlah_item_terjual,
|
||||||
|
'berat_terjual' => $item->berat_terjual,
|
||||||
|
'pendapatan' => $item->pendapatan,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
$totals = $this->helper->calculateTotals($nampanTerjualRaw);
|
||||||
|
|
||||||
|
// FIXED: Get all nampan without pagination
|
||||||
|
$semuaNampan = $this->helper->getAllNampanWithPagination(1, PHP_INT_MAX); // Skip pagination
|
||||||
|
$detailItem = $this->helper->mapNampanWithSalesData($semuaNampan, $nampanTerjual)
|
||||||
|
->filter(function ($item) {
|
||||||
|
return $item['jumlah_item_terjual'] > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXED: Simple collection without pagination
|
||||||
|
$filteredCollection = $detailItem->values();
|
||||||
|
|
||||||
|
$filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'filter' => $filterInfo,
|
'filter' => $filterInfo,
|
||||||
'rekap_harian' => $totals,
|
'rekap_interval' => $totals,
|
||||||
'nampan' => $detailItem->values(),
|
'nampan' => $filteredCollection,
|
||||||
'pagination' => $this->helper->buildPaginationInfo($semuaNampanPaginated),
|
'pagination' => [
|
||||||
|
'current_page' => 1,
|
||||||
|
'from' => 1,
|
||||||
|
'last_page' => 1,
|
||||||
|
'per_page' => $filteredCollection->count(),
|
||||||
|
'to' => $filteredCollection->count(),
|
||||||
|
'total' => $filteredCollection->count(),
|
||||||
|
'has_more_pages' => false,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,22 +261,22 @@ class LaporanService
|
|||||||
|
|
||||||
public function exportPerProduk(array $params)
|
public function exportPerProduk(array $params)
|
||||||
{
|
{
|
||||||
$tanggal = $params['tanggal'];
|
|
||||||
$format = $params['format'];
|
$format = $params['format'];
|
||||||
|
|
||||||
$allParams = $params;
|
$allParams = $params;
|
||||||
unset($allParams['page'], $allParams['per_page']);
|
unset($allParams['page'], $allParams['per_page']);
|
||||||
|
|
||||||
$data = $this->getDetailPerProdukForExport($allParams);
|
$data = $this->getDetailPerProdukForExport($allParams);
|
||||||
|
|
||||||
$fileName = "laporan_per_produk_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}";
|
$startDate = Carbon::parse($params['start_date'])->format('Ymd');
|
||||||
|
$endDate = Carbon::parse($params['end_date'])->format('Ymd');
|
||||||
|
$fileName = "laporan_per_produk_{$startDate}_to_{$endDate}.{$format}";
|
||||||
|
|
||||||
if ($format === 'pdf') {
|
if ($format === 'pdf') {
|
||||||
$pdf = PDF::loadView('exports.perproduk_pdf', [
|
$pdf = PDF::loadView('exports.perproduk_pdf', [
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
'title' => 'Laporan Detail Per Produk'
|
'title' => 'Laporan Detail Per Produk'
|
||||||
]);
|
]);
|
||||||
$pdf->setPaper('a4', 'potrait');
|
$pdf->setPaper('a4', 'portrait');
|
||||||
return $pdf->download($fileName);
|
return $pdf->download($fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,22 +285,22 @@ class LaporanService
|
|||||||
|
|
||||||
public function exportPerNampan(array $params)
|
public function exportPerNampan(array $params)
|
||||||
{
|
{
|
||||||
$tanggal = $params['tanggal'];
|
|
||||||
$format = $params['format'];
|
$format = $params['format'];
|
||||||
|
|
||||||
$allParams = $params;
|
$allParams = $params;
|
||||||
unset($allParams['page'], $allParams['per_page']);
|
unset($allParams['page'], $allParams['per_page']);
|
||||||
|
|
||||||
$data = $this->getDetailPerNampanForExport($allParams);
|
$data = $this->getDetailPerNampanForExport($allParams);
|
||||||
|
|
||||||
$fileName = "laporan_per_nampan_{$tanggal}_" . Carbon::now()->format('Ymd') . ".{$format}";
|
$startDate = Carbon::parse($params['start_date'])->format('Ymd');
|
||||||
|
$endDate = Carbon::parse($params['end_date'])->format('Ymd');
|
||||||
|
$fileName = "laporan_per_nampan_{$startDate}_to_{$endDate}.{$format}";
|
||||||
|
|
||||||
if ($format === 'pdf') {
|
if ($format === 'pdf') {
|
||||||
$pdf = PDF::loadView('exports.pernampan_pdf', [
|
$pdf = PDF::loadView('exports.pernampan_pdf', [
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
'title' => 'Laporan Detail Per Nampan'
|
'title' => 'Laporan Detail Per Nampan'
|
||||||
]);
|
]);
|
||||||
$pdf->setPaper('a4', 'potrait');
|
$pdf->setPaper('a4', 'portrait');
|
||||||
return $pdf->download($fileName);
|
return $pdf->download($fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,9 +309,10 @@ class LaporanService
|
|||||||
|
|
||||||
private function getDetailPerProdukForExport(array $params)
|
private function getDetailPerProdukForExport(array $params)
|
||||||
{
|
{
|
||||||
$tanggal = Carbon::parse($params['tanggal']);
|
$startDate = Carbon::parse($params['start_date'])->startOfDay();
|
||||||
|
$endDate = Carbon::parse($params['end_date'])->endOfDay();
|
||||||
$produkTerjualQuery = $this->buildBaseItemQuery($tanggal);
|
|
||||||
|
$produkTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
|
||||||
$this->applyFilters($produkTerjualQuery, $params);
|
$this->applyFilters($produkTerjualQuery, $params);
|
||||||
|
|
||||||
$produkTerjual = $produkTerjualQuery
|
$produkTerjual = $produkTerjualQuery
|
||||||
@ -259,7 +327,15 @@ class LaporanService
|
|||||||
->get()
|
->get()
|
||||||
->keyBy('id_produk');
|
->keyBy('id_produk');
|
||||||
|
|
||||||
$totals = $this->helper->calculateTotals($produkTerjual);
|
// FIXED: calculateTotals sum raw
|
||||||
|
$produkTerjualRaw = $produkTerjual->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'jumlah_item_terjual' => (int) $item->jumlah_item_terjual,
|
||||||
|
'berat_terjual' => $item->berat_terjual,
|
||||||
|
'pendapatan' => $item->pendapatan,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
$totals = $this->helper->calculateTotals($produkTerjualRaw);
|
||||||
|
|
||||||
$semuaProduk = Produk::select('id', 'nama')->orderBy('nama')->get();
|
$semuaProduk = Produk::select('id', 'nama')->orderBy('nama')->get();
|
||||||
|
|
||||||
@ -273,29 +349,24 @@ class LaporanService
|
|||||||
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
|
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
})->filter();
|
||||||
|
|
||||||
return [
|
$filterInfo = $this->helper->buildProdukFilterInfo($startDate, $endDate, $params);
|
||||||
'nama_produk' => $item->nama,
|
|
||||||
'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
|
||||||
'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
|
||||||
'pendapatan' => LaporanHelper::DEFAULT_DISPLAY,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$filterInfo = $this->helper->buildProdukFilterInfo($tanggal, $params);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'filter' => $filterInfo,
|
'filter' => $filterInfo,
|
||||||
'rekap_harian' => $totals,
|
'rekap_interval' => $totals,
|
||||||
'produk' => $detailItem->values(),
|
'produk' => $detailItem->values(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getDetailPerNampanForExport(array $params)
|
private function getDetailPerNampanForExport(array $params)
|
||||||
{
|
{
|
||||||
$tanggal = Carbon::parse($params['tanggal']);
|
$startDate = Carbon::parse($params['start_date'])->startOfDay();
|
||||||
|
$endDate = Carbon::parse($params['end_date'])->endOfDay();
|
||||||
|
|
||||||
$nampanTerjualQuery = $this->buildBaseItemQuery($tanggal);
|
$nampanTerjualQuery = $this->buildBaseItemQueryForRange($startDate, $endDate);
|
||||||
$this->applyNampanFilters($nampanTerjualQuery, $params);
|
$this->applyNampanFilters($nampanTerjualQuery, $params);
|
||||||
|
|
||||||
$nampanTerjual = $nampanTerjualQuery
|
$nampanTerjual = $nampanTerjualQuery
|
||||||
@ -309,9 +380,18 @@ class LaporanService
|
|||||||
->get()
|
->get()
|
||||||
->keyBy('posisi_asal');
|
->keyBy('posisi_asal');
|
||||||
|
|
||||||
$totals = $this->helper->calculateTotals($nampanTerjual);
|
// FIXED: Sum raw
|
||||||
|
$nampanTerjualRaw = $nampanTerjual->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'jumlah_item_terjual' => (int) $item->jumlah_item_terjual,
|
||||||
|
'berat_terjual' => $item->berat_terjual,
|
||||||
|
'pendapatan' => $item->pendapatan,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
$totals = $this->helper->calculateTotals($nampanTerjualRaw);
|
||||||
|
|
||||||
$semuaPosisi = DB::table('item_transaksis')
|
$semuaPosisi = DB::table('item_transaksis')
|
||||||
|
->whereBetween('created_at', [$startDate, $endDate])
|
||||||
->select('posisi_asal')
|
->select('posisi_asal')
|
||||||
->distinct()
|
->distinct()
|
||||||
->pluck('posisi_asal')
|
->pluck('posisi_asal')
|
||||||
@ -328,25 +408,18 @@ class LaporanService
|
|||||||
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
|
'pendapatan' => $this->helper->formatCurrency($dataTerjual->pendapatan),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
})->filter();
|
||||||
|
|
||||||
return [
|
$filterInfo = $this->helper->buildNampanFilterInfo($startDate, $endDate, $params);
|
||||||
'nama_nampan' => $posisi,
|
|
||||||
'jumlah_item_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
|
||||||
'berat_terjual' => LaporanHelper::DEFAULT_DISPLAY,
|
|
||||||
'pendapatan' => LaporanHelper::DEFAULT_DISPLAY,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$filterInfo = $this->helper->buildNampanFilterInfo($tanggal, $params);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'filter' => $filterInfo,
|
'filter' => $filterInfo,
|
||||||
'rekap_harian' => $totals,
|
'rekap_interval' => $totals,
|
||||||
'nampan' => $detailItem->values(),
|
'nampan' => $detailItem->values(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private function getAllSalesNames(): Collection
|
private function getAllSalesNames(): Collection
|
||||||
{
|
{
|
||||||
return Cache::remember('all_sales_names', self::CACHE_TTL, function () {
|
return Cache::remember('all_sales_names', self::CACHE_TTL, function () {
|
||||||
@ -364,15 +437,6 @@ class LaporanService
|
|||||||
return $this->transaksiRepo->processLaporanBulanan($allSalesNames, $page, $limitPagination);
|
return $this->transaksiRepo->processLaporanBulanan($allSalesNames, $page, $limitPagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildBaseItemQuery(Carbon $carbonDate)
|
|
||||||
{
|
|
||||||
// UBAH: Menghapus join ke tabel 'items' dan join 'produks' langsung dari 'item_transaksis'
|
|
||||||
return ItemTransaksi::query()
|
|
||||||
->join('produks', 'item_transaksis.id_produk', '=', 'produks.id')
|
|
||||||
->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id')
|
|
||||||
->whereDate('transaksis.created_at', $carbonDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function applyFilters($query, array $params): void
|
private function applyFilters($query, array $params): void
|
||||||
{
|
{
|
||||||
if (!empty($params['sales_id'])) {
|
if (!empty($params['sales_id'])) {
|
||||||
@ -381,11 +445,14 @@ class LaporanService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($params['nampan_id'])) {
|
if (isset($params['nampan_id'])) {
|
||||||
// UBAH: Filter berdasarkan 'item_transaksis.id_nampan'
|
$nampanId = (int) $params['nampan_id'];
|
||||||
if ($params['nampan_id'] == 0) {
|
if ($nampanId === -1) {
|
||||||
$query->whereNull('item_transaksis.id_nampan');
|
$query->where('item_transaksis.posisi_asal', 'Brankas');
|
||||||
} else {
|
} elseif ($nampanId > 0) {
|
||||||
$query->where('item_transaksis.id_nampan', $params['nampan_id']);
|
$query->join('nampans', function ($join) use ($nampanId) {
|
||||||
|
$join->on('item_transaksis.posisi_asal', '=', 'nampans.nama')
|
||||||
|
->where('nampans.id', $nampanId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,11 +33,10 @@ class DatabaseSeeder extends Seeder
|
|||||||
User::factory(2)->create();
|
User::factory(2)->create();
|
||||||
Sales::factory(5)->create();
|
Sales::factory(5)->create();
|
||||||
|
|
||||||
$kodeNampan = ['A', 'B'];
|
for ($i=0; $i < 30; $i++) {
|
||||||
foreach ($kodeNampan as $kode) {
|
if ($i != 12) {
|
||||||
for ($i=0; $i < 4; $i++) {
|
|
||||||
Nampan::factory()->create([
|
Nampan::factory()->create([
|
||||||
'nama' => $kode . ($i + 1) // A1, A2, ... B4
|
'nama' => 'A' . ($i + 1)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/logo.ico
Normal file
BIN
public/logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
@ -340,40 +340,50 @@ const printQR = () => {
|
|||||||
<head>
|
<head>
|
||||||
<title>Print QR Code - ${selectedItem.value.kode_item}</title>
|
<title>Print QR Code - ${selectedItem.value.kode_item}</title>
|
||||||
<style>
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 60mm 50mm;
|
||||||
|
margin: 1mm;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
.qr-container {
|
.qr-container {
|
||||||
border: 2px solid #ccc;
|
text-align: center;
|
||||||
padding: 20px;
|
}
|
||||||
display: inline-block;
|
.qr-img {
|
||||||
margin: 20px;
|
width: 40mm;
|
||||||
|
height: 40mm;
|
||||||
|
margin-bottom: 2mm;
|
||||||
}
|
}
|
||||||
.item-info {
|
.item-info {
|
||||||
margin-top: 10px;
|
font-size: 14pt;
|
||||||
font-size: 14px;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<img src="${qrCodeUrl.value}" alt="QR Code" style="width: 200px; height: 200px;" />
|
<img class="qr-img" src="${qrCodeUrl.value}" alt="QR Code"
|
||||||
|
onload="window.print()" />
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<div style="font-weight: bold; margin-bottom: 5px;">${selectedItem.value.kode_item}</div>
|
${selectedItem.value.kode_item}
|
||||||
<div>${selectedItem.value.produk.nama}</div>
|
|
||||||
<div style="color: #666; margin-top: 5px;">${selectedItem.value.produk.berat}g</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
printWindow.print();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleImageError = (event) => {
|
const handleImageError = (event) => {
|
||||||
event.target.style.display = 'none';
|
event.target.style.display = 'none';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -87,7 +87,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close','itemAdded']);
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const selectedNampan = ref('');
|
const selectedNampan = ref('');
|
||||||
@ -147,15 +147,17 @@ const createItem = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.post('/api/item', payload, {
|
const response = await axios.post('/api/item', payload, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
},
|
},
|
||||||
});;
|
});
|
||||||
|
|
||||||
success.value = true;
|
success.value = true;
|
||||||
createdItem.value = response.data.data
|
createdItem.value = response.data.data;
|
||||||
console.log('Item created:', createdItem);
|
console.log('Item created:', createdItem);
|
||||||
|
|
||||||
|
emit('itemAdded'); // 🔔 penting
|
||||||
|
|
||||||
loadNampanList();
|
loadNampanList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating item:', error);
|
console.error('Error creating item:', error);
|
||||||
@ -165,6 +167,7 @@ const createItem = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const addNewItem = () => {
|
const addNewItem = () => {
|
||||||
success.value = false;
|
success.value = false;
|
||||||
selectedNampan.value = '';
|
selectedNampan.value = '';
|
||||||
|
|||||||
206
resources/js/components/DatePicker.vue
Normal file
206
resources/js/components/DatePicker.vue
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative" ref="datePickerRef">
|
||||||
|
<!-- Input Display -->
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label v-if="label" class="text-D/80 block text-sm font-medium mb-1">{{ label }}</label>
|
||||||
|
<div
|
||||||
|
@click="toggleCalendar"
|
||||||
|
class="w-full px-3 py-2 bg-A text-D border border-B rounded-md cursor-pointer hover:border-C focus-within:border-C focus-within:ring focus-within:ring-D focus-within:ring-opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span v-if="displayText" class="text-sm">{{ displayText }}</span>
|
||||||
|
<span v-else class="text-sm text-D/60">{{ placeholder }}</span>
|
||||||
|
<i class="fas fa-calendar-alt text-D/60"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="errorMessage" class="text-red-500 text-xs mt-1">{{ errorMessage }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Calendar Popup (inline, no teleport) -->
|
||||||
|
<div
|
||||||
|
v-if="showCalendar"
|
||||||
|
ref="popupRef"
|
||||||
|
class="absolute z-[9999] bg-A border border-C rounded-lg shadow-xl p-4 min-w-[300px] mt-2"
|
||||||
|
:class="popupPositionClass"
|
||||||
|
>
|
||||||
|
<!-- Manual Date Inputs -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-sm font-medium text-D mb-2">Pilih Manual</div>
|
||||||
|
<div class="flex gap-3 items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="text-xs text-D/80 block mb-1">Dari</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
v-model="tempStartDate"
|
||||||
|
@input="validateDates"
|
||||||
|
:max="maxDate"
|
||||||
|
class="w-full text-xs px-2 py-2 bg-A text-D border border-B rounded focus:border-C focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="text-D/50 text-sm">s/d</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="text-xs text-D/80 block mb-1">Sampai</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
v-model="tempEndDate"
|
||||||
|
@input="validateDates"
|
||||||
|
:min="tempStartDate"
|
||||||
|
:max="maxDate"
|
||||||
|
class="w-full text-xs px-2 py-2 bg-A text-D border border-B rounded focus:border-C focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Range Info -->
|
||||||
|
<div v-if="tempStartDate && tempEndDate" class="text-xs text-D/60 mt-2">
|
||||||
|
{{ rangeDaysText }} ({{ formatDisplayDate(tempStartDate) }} - {{ formatDisplayDate(tempEndDate) }})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex justify-between items-center pt-3 border-t border-C">
|
||||||
|
<button
|
||||||
|
@click="clearDates"
|
||||||
|
class="px-3 py-1 text-xs text-D/60 hover:text-D transition-colors"
|
||||||
|
>
|
||||||
|
Bersihkan
|
||||||
|
</button>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="cancel"
|
||||||
|
class="px-4 py-1 text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 rounded transition-colors"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="confirm"
|
||||||
|
:disabled="!isValidRange"
|
||||||
|
class="px-4 py-1 text-xs bg-C hover:bg-C/80 text-D rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Terapkan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Object, default: () => ({ start: '', end: '' }) },
|
||||||
|
label: { type: String, default: 'Pilih Periode' },
|
||||||
|
placeholder: { type: String, default: 'Pilih rentang tanggal' },
|
||||||
|
maxDays: { type: Number, default: 31 },
|
||||||
|
position: { type: String, default: 'left', validator: (v) => ['left', 'right'].includes(v) }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
const datePickerRef = ref(null)
|
||||||
|
const showCalendar = ref(false)
|
||||||
|
const tempStartDate = ref('')
|
||||||
|
const tempEndDate = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const maxDate = computed(() => new Date().toISOString().split('T')[0])
|
||||||
|
|
||||||
|
const displayText = computed(() => {
|
||||||
|
if (props.modelValue.start && props.modelValue.end) {
|
||||||
|
const startFormatted = formatDisplayDate(props.modelValue.start)
|
||||||
|
const endFormatted = formatDisplayDate(props.modelValue.end)
|
||||||
|
return props.modelValue.start === props.modelValue.end
|
||||||
|
? startFormatted
|
||||||
|
: `${startFormatted} - ${endFormatted}`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValidRange = computed(() => {
|
||||||
|
if (!tempStartDate.value || !tempEndDate.value) return false
|
||||||
|
const start = new Date(tempStartDate.value)
|
||||||
|
const end = new Date(tempEndDate.value)
|
||||||
|
if (start > end) return false
|
||||||
|
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||||
|
return diffDays <= props.maxDays
|
||||||
|
})
|
||||||
|
|
||||||
|
const rangeDaysText = computed(() => {
|
||||||
|
if (!tempStartDate.value || !tempEndDate.value) return ''
|
||||||
|
const start = new Date(tempStartDate.value)
|
||||||
|
const end = new Date(tempEndDate.value)
|
||||||
|
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||||
|
return diffDays > props.maxDays
|
||||||
|
? `⚠️ Maksimal ${props.maxDays} hari`
|
||||||
|
: `${diffDays} hari`
|
||||||
|
})
|
||||||
|
|
||||||
|
const popupPositionClass = computed(() => props.position === 'right' ? 'right-0' : 'left-0')
|
||||||
|
|
||||||
|
const formatDisplayDate = (dateString) => {
|
||||||
|
const date = new Date(dateString + 'T00:00:00')
|
||||||
|
return date.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleCalendar = () => {
|
||||||
|
showCalendar.value = !showCalendar.value
|
||||||
|
if (showCalendar.value) {
|
||||||
|
tempStartDate.value = props.modelValue.start
|
||||||
|
tempEndDate.value = props.modelValue.end
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateDates = () => {
|
||||||
|
errorMessage.value = ''
|
||||||
|
if (!tempStartDate.value || !tempEndDate.value) return
|
||||||
|
const start = new Date(tempStartDate.value)
|
||||||
|
const end = new Date(tempEndDate.value)
|
||||||
|
if (start > end) {
|
||||||
|
errorMessage.value = 'Tanggal akhir harus setelah tanggal mulai'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1
|
||||||
|
if (diffDays > props.maxDays) {
|
||||||
|
errorMessage.value = `Maksimal ${props.maxDays} hari`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearDates = () => {
|
||||||
|
tempStartDate.value = ''
|
||||||
|
tempEndDate.value = ''
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
showCalendar.value = false
|
||||||
|
errorMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
if (isValidRange.value) {
|
||||||
|
const newValue = { start: tempStartDate.value, end: tempEndDate.value }
|
||||||
|
emit('update:modelValue', newValue)
|
||||||
|
emit('change', newValue)
|
||||||
|
showCalendar.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (datePickerRef.value && !datePickerRef.value.contains(e.target)) {
|
||||||
|
showCalendar.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('click', handleClickOutside))
|
||||||
|
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (newValue.start !== tempStartDate.value || newValue.end !== tempEndDate.value) {
|
||||||
|
tempStartDate.value = newValue.start
|
||||||
|
tempEndDate.value = newValue.end
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
</script>
|
||||||
@ -3,11 +3,15 @@
|
|||||||
<hr class="border-B mb-5" />
|
<hr class="border-B mb-5" />
|
||||||
|
|
||||||
<!-- Filter Section -->
|
<!-- Filter Section -->
|
||||||
<div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8">
|
<div class="flex flex-row my-3 gap-1 md:gap-5 lg:gap-8">
|
||||||
<div class="mb-3 w-full">
|
<div class="mb-3 w-full">
|
||||||
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label>
|
<DatePicker
|
||||||
<input type="date" v-model="tanggalDipilih" id="pilihTanggal"
|
v-model="dateRange"
|
||||||
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" />
|
label="Filter Tanggal"
|
||||||
|
placeholder="Pilih rentang tanggal"
|
||||||
|
:max-days="31"
|
||||||
|
@change="handleDateChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 w-full">
|
<div class="mb-3 w-full">
|
||||||
<label class="text-D/80" for="pilihSales">Filter Sales:</label>
|
<label class="text-D/80" for="pilihSales">Filter Sales:</label>
|
||||||
@ -26,26 +30,27 @@
|
|||||||
<!-- Export Section -->
|
<!-- Export Section -->
|
||||||
<div class="flex flex-row items-center justify-between mt-5 gap-3">
|
<div class="flex flex-row items-center justify-between mt-5 gap-3">
|
||||||
<!-- Summary Cards -->
|
<!-- Summary Cards -->
|
||||||
<div class="flex gap-4" v-if="data?.rekap_harian">
|
<div v-if="loading">
|
||||||
<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>
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex items-center justify-center w-full h-30">
|
<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>
|
<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>
|
<span class="ml-2 text-gray-600">Memuat data...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-4" v-else-if="data?.rekap_interval">
|
||||||
|
<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_interval.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_interval.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_interval.total_pendapatan }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else></div>
|
||||||
|
|
||||||
<!-- Export Dropdown -->
|
<!-- Export Dropdown -->
|
||||||
<div class="relative w-40" ref="exportDropdownRef">
|
<div class="relative w-40" ref="exportDropdownRef">
|
||||||
@ -113,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-else-if="!sortedNampan.length">
|
<tr v-else-if="sortedNampan.length == 0">
|
||||||
<td colspan="4" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td>
|
<td colspan="4" class="p-4 text-center">Tidak ada data untuk ditampilkan.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-else v-for="item in sortedNampan" :key="item.nama_nampan">
|
<template v-else v-for="item in sortedNampan" :key="item.nama_nampan">
|
||||||
@ -157,6 +162,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
|
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
|
||||||
import InputSelect from './InputSelect.vue';
|
import InputSelect from './InputSelect.vue';
|
||||||
import InputField from './InputField.vue';
|
import InputField from './InputField.vue';
|
||||||
|
import DatePicker from './DatePicker.vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
@ -170,7 +176,7 @@ const exportOptions = ref([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const exportFormat = ref(null);
|
const exportFormat = ref(null);
|
||||||
const tanggalDipilih = ref('');
|
const dateRange = ref({ start: '', end: '' });
|
||||||
const data = ref(null);
|
const data = ref(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loadingExport = ref(false);
|
const loadingExport = ref(false);
|
||||||
@ -296,6 +302,13 @@ const getSortIcon = (column) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (newDateRange) => {
|
||||||
|
console.log('Date range changed:', newDateRange);
|
||||||
|
// Reset pagination when date changes
|
||||||
|
pagination.value.current_page = 1;
|
||||||
|
fetchData(1);
|
||||||
|
};
|
||||||
|
|
||||||
const fetchSales = async () => {
|
const fetchSales = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/sales', {
|
const response = await axios.get('/api/sales', {
|
||||||
@ -337,12 +350,12 @@ const fetchProduk = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async (page = 1) => {
|
const fetchData = async (page = 1) => {
|
||||||
if (!tanggalDipilih.value) return;
|
if (!dateRange.value.start || !dateRange.value.end) return;
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
pendapatanElements.value = [];
|
pendapatanElements.value = [];
|
||||||
|
|
||||||
let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`;
|
let queryParams = `start_date=${dateRange.value.start}&end_date=${dateRange.value.end}&page=${page}`;
|
||||||
if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`;
|
if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`;
|
||||||
if (produkDipilih.value != 0) queryParams += `&produk_id=${produkDipilih.value}`;
|
if (produkDipilih.value != 0) queryParams += `&produk_id=${produkDipilih.value}`;
|
||||||
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
||||||
@ -393,6 +406,11 @@ const goToPage = (page) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectExport = async (option) => {
|
const selectExport = async (option) => {
|
||||||
|
if (!dateRange.value.start || !dateRange.value.end) {
|
||||||
|
alert('Silakan pilih rentang tanggal terlebih dahulu');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
exportFormat.value = option.value;
|
exportFormat.value = option.value;
|
||||||
isExportOpen.value = false;
|
isExportOpen.value = false;
|
||||||
loadingExport.value = true;
|
loadingExport.value = true;
|
||||||
@ -400,7 +418,8 @@ const selectExport = async (option) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/laporan/export/detail-pernampan`, {
|
const response = await axios.get(`/api/laporan/export/detail-pernampan`, {
|
||||||
params: {
|
params: {
|
||||||
tanggal: tanggalDipilih.value,
|
start_date: dateRange.value.start,
|
||||||
|
end_date: dateRange.value.end,
|
||||||
format: exportFormat.value,
|
format: exportFormat.value,
|
||||||
page: pagination.value.current_page,
|
page: pagination.value.current_page,
|
||||||
sales_id: salesDipilih.value != 0 ? salesDipilih.value : null,
|
sales_id: salesDipilih.value != 0 ? salesDipilih.value : null,
|
||||||
@ -415,7 +434,7 @@ const selectExport = async (option) => {
|
|||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
const fileName = `laporan_${tanggalDipilih.value}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`;
|
const fileName = `laporan_${dateRange.value.start}_to_${dateRange.value.end}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`;
|
||||||
|
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.setAttribute('download', fileName);
|
link.setAttribute('download', fileName);
|
||||||
@ -426,6 +445,7 @@ const selectExport = async (option) => {
|
|||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Gagal mengekspor laporan:", e);
|
console.error("Gagal mengekspor laporan:", e);
|
||||||
|
alert('Gagal mengekspor laporan. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
loadingExport.value = false;
|
loadingExport.value = false;
|
||||||
}
|
}
|
||||||
@ -439,8 +459,9 @@ const closeDropdownsOnClickOutside = (event) => {
|
|||||||
|
|
||||||
// --- Lifecycle Hooks ---
|
// --- Lifecycle Hooks ---
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// Set default date range to today
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
tanggalDipilih.value = today;
|
dateRange.value = { start: today, end: today };
|
||||||
|
|
||||||
fetchSales();
|
fetchSales();
|
||||||
fetchProduk();
|
fetchProduk();
|
||||||
@ -452,9 +473,19 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for filter changes
|
// Watch for filter changes (except date range which has its own handler)
|
||||||
watch([tanggalDipilih, salesDipilih, produkDipilih, namaPembeli], () => {
|
watch([salesDipilih, produkDipilih, namaPembeli], () => {
|
||||||
pagination.value.current_page = 1; // Reset to first page when filters change
|
if (dateRange.value.start && dateRange.value.end) {
|
||||||
fetchData(1);
|
pagination.value.current_page = 1; // Reset to first page when filters change
|
||||||
}, { immediate: true });
|
fetchData(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for date range changes
|
||||||
|
watch(dateRange, (newDateRange) => {
|
||||||
|
if (newDateRange.start && newDateRange.end) {
|
||||||
|
pagination.value.current_page = 1;
|
||||||
|
fetchData(1);
|
||||||
|
}
|
||||||
|
}, { deep: true, immediate: true });
|
||||||
</script>
|
</script>
|
||||||
@ -2,11 +2,16 @@
|
|||||||
<div class="my-6">
|
<div class="my-6">
|
||||||
<hr class="border-B mb-5" />
|
<hr class="border-B mb-5" />
|
||||||
|
|
||||||
<div class="flex flex-row my-3 overflow-x-auto gap-1 md:gap-5 lg:gap-8">
|
<!-- Filter Section -->
|
||||||
<div class="mb-3 w-full min-w-fit">
|
<div class="flex flex-row my-3 gap-1 md:gap-5 lg:gap-8">
|
||||||
<label class="text-D/80" for="pilihTanggal">Filter Tanggal:</label>
|
<div class="mb-3 w-full">
|
||||||
<input type="date" v-model="tanggalDipilih" id="pilihTanggal"
|
<DatePicker
|
||||||
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" />
|
v-model="dateRange"
|
||||||
|
label="Filter Tanggal"
|
||||||
|
placeholder="Pilih rentang tanggal"
|
||||||
|
:max-days="31"
|
||||||
|
@change="handleDateChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 w-full min-w-fit">
|
<div class="mb-3 w-full min-w-fit">
|
||||||
<label class="text-D/80" for="pilihSales">Filter Sales:</label>
|
<label class="text-D/80" for="pilihSales">Filter Sales:</label>
|
||||||
@ -22,29 +27,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Section -->
|
||||||
<div class="flex flex-row items-center justify-between mt-5 gap-3">
|
<div class="flex flex-row items-center justify-between mt-5 gap-3">
|
||||||
<div class="flex gap-4" v-if="data?.rekap_harian">
|
<!-- Summary Cards -->
|
||||||
<div class="bg-A p-3 rounded-md border border-C">
|
<div v-if="loading">
|
||||||
<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>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex items-center justify-center w-full h-30">
|
<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>
|
<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>
|
<span class="ml-2 text-gray-600">Memuat data...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-4" v-if="data?.rekap_interval">
|
||||||
|
<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_interval.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_interval.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_interval.total_pendapatan }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else></div>
|
||||||
|
|
||||||
|
<!-- Export Dropdown -->
|
||||||
<div class="relative w-40" ref="exportDropdownRef">
|
<div class="relative w-40" ref="exportDropdownRef">
|
||||||
<button v-if="loadingExport" type="button"
|
<button v-if="loadingExport" type="button"
|
||||||
class="flex items-center w-full px-3 py-2 text-sm text-left bg-C/80 border rounded-md border-C/80" disabled>
|
class="flex items-center w-full px-3 py-2 text-sm text-left bg-C/80 border rounded-md border-C/80" disabled>
|
||||||
@ -66,6 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Table Section -->
|
||||||
<div class="mt-5 overflow-x-auto">
|
<div class="mt-5 overflow-x-auto">
|
||||||
<table class="w-full border-collapse border border-C rounded-md">
|
<table class="w-full border-collapse border border-C rounded-md">
|
||||||
<thead>
|
<thead>
|
||||||
@ -130,6 +139,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
<div v-if="pagination.total > 0 && pagination.last_page > 1" class="flex items-center justify-end gap-2 mt-4">
|
<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"
|
<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">
|
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">
|
||||||
@ -152,6 +162,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
|
import { ref, onMounted, onUnmounted, watch, computed, nextTick } from 'vue';
|
||||||
import InputSelect from './InputSelect.vue';
|
import InputSelect from './InputSelect.vue';
|
||||||
import InputField from './InputField.vue';
|
import InputField from './InputField.vue';
|
||||||
|
import DatePicker from './DatePicker.vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
@ -165,14 +176,14 @@ const exportOptions = ref([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const exportFormat = ref(null);
|
const exportFormat = ref(null);
|
||||||
const tanggalDipilih = ref('');
|
const dateRange = ref({ start: '', end: '' });
|
||||||
const data = ref(null);
|
const data = ref(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loadingExport = ref(false);
|
const loadingExport = ref(false);
|
||||||
|
|
||||||
// Sorting state
|
// Sorting state
|
||||||
const sortBy = ref(null);
|
const sortBy = ref(null);
|
||||||
const sortOrder = ref('asc'); // 'asc' or 'desc'
|
const sortOrder = ref('asc');
|
||||||
|
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
@ -185,12 +196,12 @@ const pendapatanElements = ref([]);
|
|||||||
|
|
||||||
const salesDipilih = ref(0);
|
const salesDipilih = ref(0);
|
||||||
const opsiSales = ref([
|
const opsiSales = ref([
|
||||||
{ label: 'Semua Sales', value: 0, selected: true },
|
{ label: 'Semua Sales', value: 0 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const nampanDipilih = ref(0);
|
const nampanDipilih = ref(0);
|
||||||
const opsiNampan = ref([
|
const opsiNampan = ref([
|
||||||
{ label: 'Semua Nampan', value: 0, selected: true },
|
{ label: 'Semua Nampan', value: 0 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const namaPembeli = ref(null);
|
const namaPembeli = ref(null);
|
||||||
@ -292,6 +303,13 @@ const getSortIcon = (column) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDateChange = (newDateRange) => {
|
||||||
|
console.log('Date range changed:', newDateRange);
|
||||||
|
// Reset pagination when date changes
|
||||||
|
pagination.value.current_page = 1;
|
||||||
|
fetchData(1);
|
||||||
|
};
|
||||||
|
|
||||||
const fetchSales = async () => {
|
const fetchSales = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/sales', {
|
const response = await axios.get('/api/sales', {
|
||||||
@ -322,7 +340,7 @@ const fetchNampan = async () => {
|
|||||||
const nampanData = response.data;
|
const nampanData = response.data;
|
||||||
opsiNampan.value = [
|
opsiNampan.value = [
|
||||||
{ label: 'Semua Nampan', value: 0 },
|
{ label: 'Semua Nampan', value: 0 },
|
||||||
{ label: 'Brankas', value: 0 },
|
{ label: 'Brankas', value: -1 },
|
||||||
...nampanData.map(nampan => ({
|
...nampanData.map(nampan => ({
|
||||||
label: nampan.nama,
|
label: nampan.nama,
|
||||||
value: nampan.id,
|
value: nampan.id,
|
||||||
@ -334,12 +352,12 @@ const fetchNampan = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async (page = 1) => {
|
const fetchData = async (page = 1) => {
|
||||||
if (!tanggalDipilih.value) return;
|
if (!dateRange.value.start || !dateRange.value.end) return;
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
pendapatanElements.value = [];
|
pendapatanElements.value = [];
|
||||||
|
|
||||||
let queryParams = `tanggal=${tanggalDipilih.value}&page=${page}`;
|
let queryParams = `start_date=${dateRange.value.start}&end_date=${dateRange.value.end}&page=${page}`;
|
||||||
if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`;
|
if (salesDipilih.value != 0 ) queryParams += `&sales_id=${salesDipilih.value}`;
|
||||||
if (nampanDipilih.value != 0) queryParams += `&nampan_id=${nampanDipilih.value}`;
|
if (nampanDipilih.value != 0) queryParams += `&nampan_id=${nampanDipilih.value}`;
|
||||||
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
if (namaPembeli.value) queryParams += `&nama_pembeli=${encodeURIComponent(namaPembeli.value)}`;
|
||||||
@ -353,6 +371,7 @@ const fetchData = async (page = 1) => {
|
|||||||
|
|
||||||
data.value = response.data;
|
data.value = response.data;
|
||||||
|
|
||||||
|
// Handle pagination data if provided by backend
|
||||||
if (response.data.pagination) {
|
if (response.data.pagination) {
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
current_page: response.data.pagination.current_page,
|
current_page: response.data.pagination.current_page,
|
||||||
@ -360,12 +379,14 @@ const fetchData = async (page = 1) => {
|
|||||||
total: response.data.pagination.total,
|
total: response.data.pagination.total,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
// Reset pagination if no pagination data
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
last_page: 1,
|
last_page: 1,
|
||||||
total: response.data.produk ? response.data.produk.length : 0,
|
total: response.data.produk ? response.data.produk.length : 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
console.log('Data laporan produk berhasil diambil:', data.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Gagal mengambil data laporan produk:', error);
|
console.error('Gagal mengambil data laporan produk:', error);
|
||||||
data.value = null;
|
data.value = null;
|
||||||
@ -387,14 +408,20 @@ const goToPage = (page) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectExport = async (option) => {
|
const selectExport = async (option) => {
|
||||||
|
if (!dateRange.value.start || !dateRange.value.end) {
|
||||||
|
alert('Silakan pilih rentang tanggal terlebih dahulu');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
exportFormat.value = option.value;
|
exportFormat.value = option.value;
|
||||||
isExportOpen.value = false;
|
isExportOpen.value = false;
|
||||||
loadingExport.value = true
|
loadingExport.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/laporan/export/detail-perproduk', {
|
const response = await axios.get('/api/laporan/export/detail-perproduk', {
|
||||||
params: {
|
params: {
|
||||||
tanggal: tanggalDipilih.value,
|
start_date: dateRange.value.start,
|
||||||
|
end_date: dateRange.value.end,
|
||||||
format: exportFormat.value,
|
format: exportFormat.value,
|
||||||
page: pagination.value.current_page,
|
page: pagination.value.current_page,
|
||||||
sales_id: salesDipilih.value != 0 ? salesDipilih.value : null,
|
sales_id: salesDipilih.value != 0 ? salesDipilih.value : null,
|
||||||
@ -409,7 +436,7 @@ const selectExport = async (option) => {
|
|||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
const fileName = `laporan_per_produk_${tanggalDipilih.value}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`;
|
const fileName = `laporan_per_produk_${dateRange.value.start}_to_${dateRange.value.end}_${new Date().toISOString().split('T')[0]}.${exportFormat.value}`;
|
||||||
|
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.setAttribute('download', fileName);
|
link.setAttribute('download', fileName);
|
||||||
@ -420,8 +447,9 @@ const selectExport = async (option) => {
|
|||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Gagal mengekspor laporan per produk:", e);
|
console.error("Gagal mengekspor laporan per produk:", e);
|
||||||
|
alert('Gagal mengekspor laporan. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
loadingExport.value = false
|
loadingExport.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -433,11 +461,12 @@ const closeDropdownsOnClickOutside = (event) => {
|
|||||||
|
|
||||||
// --- Lifecycle Hooks ---
|
// --- Lifecycle Hooks ---
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// Set default date range to today
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
tanggalDipilih.value = today;
|
dateRange.value = { start: today, end: today };
|
||||||
|
|
||||||
fetchSales();
|
fetchSales();
|
||||||
fetchNampan(); // Changed from fetchProduk to fetchNampan
|
fetchNampan();
|
||||||
|
|
||||||
document.addEventListener('click', closeDropdownsOnClickOutside);
|
document.addEventListener('click', closeDropdownsOnClickOutside);
|
||||||
});
|
});
|
||||||
@ -446,9 +475,19 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
document.removeEventListener('click', closeDropdownsOnClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for filter changes
|
// Watch for filter changes (except date range which has its own handler)
|
||||||
watch([tanggalDipilih, salesDipilih, nampanDipilih, namaPembeli], () => { // Changed from produkDipilih to nampanDipilih
|
watch([salesDipilih, nampanDipilih, namaPembeli], () => {
|
||||||
pagination.value.current_page = 1;
|
if (dateRange.value.start && dateRange.value.end) {
|
||||||
fetchData(1);
|
pagination.value.current_page = 1; // Reset to first page when filters change
|
||||||
}, { immediate: true });
|
fetchData(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for date range changes
|
||||||
|
watch(dateRange, (newDateRange) => {
|
||||||
|
if (newDateRange.start && newDateRange.end) {
|
||||||
|
pagination.value.current_page = 1;
|
||||||
|
fetchData(1);
|
||||||
|
}
|
||||||
|
}, { deep: true, immediate: true });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -211,7 +211,7 @@ const inputItem = async () => {
|
|||||||
|
|
||||||
infoTimeout = setTimeout(() => {
|
infoTimeout = setTimeout(() => {
|
||||||
info.value = "";
|
info.value = "";
|
||||||
}, 3000);
|
}, 5000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = "Item tidak ditemukan";
|
error.value = "Item tidak ditemukan";
|
||||||
info.value = "";
|
info.value = "";
|
||||||
@ -221,7 +221,7 @@ const inputItem = async () => {
|
|||||||
|
|
||||||
errorTimeout = setTimeout(() => {
|
errorTimeout = setTimeout(() => {
|
||||||
error.value = "";
|
error.value = "";
|
||||||
}, 3000);
|
}, 5000);
|
||||||
} finally {
|
} finally {
|
||||||
loadingItem.value = false;
|
loadingItem.value = false;
|
||||||
}
|
}
|
||||||
@ -237,7 +237,7 @@ const tambahItem = () => {
|
|||||||
clearTimeout(errorTimeout);
|
clearTimeout(errorTimeout);
|
||||||
errorTimeout = setTimeout(() => {
|
errorTimeout = setTimeout(() => {
|
||||||
error.value = "";
|
error.value = "";
|
||||||
}, 3000);
|
}, 5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ const konfirmasiPenjualan = () => {
|
|||||||
clearTimeout(errorTimeout);
|
clearTimeout(errorTimeout);
|
||||||
errorTimeout = setTimeout(() => {
|
errorTimeout = setTimeout(() => {
|
||||||
error.value = "";
|
error.value = "";
|
||||||
}, 3000);
|
}, 5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -185,40 +185,57 @@ const printQR = () => {
|
|||||||
<head>
|
<head>
|
||||||
<title>Print QR Code - ${selectedItem.value.kode_item}</title>
|
<title>Print QR Code - ${selectedItem.value.kode_item}</title>
|
||||||
<style>
|
<style>
|
||||||
|
@page {
|
||||||
|
size: 60mm 50mm;
|
||||||
|
margin: 1mm;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 20px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
}
|
}
|
||||||
.qr-container {
|
.qr-container {
|
||||||
border: 2px solid #ccc;
|
text-align: center;
|
||||||
padding: 20px;
|
width: 100%;
|
||||||
display: inline-block;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
}
|
||||||
.item-info {
|
.qr-img {
|
||||||
margin-top: 10px;
|
width: 40mm;
|
||||||
font-size: 14px;
|
height: 40mm;
|
||||||
|
margin-bottom: 2mm;
|
||||||
|
}
|
||||||
|
.kode-item {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14pt;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="qr-container">
|
<div class="qr-container">
|
||||||
<img src="${qrCodeUrl.value}" alt="QR Code" style="width: 200px; height: 200px;" />
|
<img id="qr-img" class="qr-img" src="${qrCodeUrl.value}" alt="QR Code" />
|
||||||
<div class="item-info">
|
<div class="kode-item">${selectedItem.value.kode_item}</div>
|
||||||
<div style="font-weight: bold; margin-bottom: 5px;">${selectedItem.value.kode_item}</div>
|
|
||||||
<div>${selectedItem.value.produk.nama}</div>
|
|
||||||
<div style="color: #666; margin-top: 5px;">${selectedItem.value.produk.berat}g</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
printWindow.print();
|
|
||||||
|
const img = printWindow.document.getElementById("qr-img");
|
||||||
|
img.onload = () => {
|
||||||
|
printWindow.focus();
|
||||||
|
printWindow.print();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const showDeleteConfirm = ref(false);
|
const showDeleteConfirm = ref(false);
|
||||||
|
|
||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
|
|||||||
@ -10,7 +10,10 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="block text-D mb-1">Nama Produk</label>
|
<label class="block text-D mb-1">Nama Produk</label>
|
||||||
<InputField v-model="form.nama" type="text" placeholder="Masukkan nama produk" />
|
<InputField v-model="form.nama" type="text" placeholder="Masukkan nama produk" @input="errors.nama = null" />
|
||||||
|
<p v-if="errors.nama" class="text-sm text-red-500 mt-1">
|
||||||
|
{{ errors.nama[0] }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -164,7 +167,7 @@ const uploadedImages = ref([]);
|
|||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
const uploadError = ref('');
|
const uploadError = ref('');
|
||||||
const fileInput = ref(null);
|
const fileInput = ref(null);
|
||||||
|
const errors = ref({});
|
||||||
const openItemModal = ref(false);
|
const openItemModal = ref(false);
|
||||||
const createdProduct = ref(null);
|
const createdProduct = ref(null);
|
||||||
|
|
||||||
@ -350,15 +353,16 @@ const submitForm = async (addItem) => {
|
|||||||
window.location.href = '/produk?message=Produk berhasil disimpan';
|
window.location.href = '/produk?message=Produk berhasil disimpan';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit error:', error);
|
console.error('Submit error:', error);
|
||||||
|
|
||||||
if (error.response?.data?.errors) {
|
if (error.response?.status === 422 && error.response.data?.errors) {
|
||||||
const errors = Object.values(error.response.data.errors).flat();
|
// 🔥 simpan error validasi dari backend
|
||||||
alert('Error: ' + errors.join(', '));
|
errors.value = error.response.data.errors;
|
||||||
} else {
|
} else {
|
||||||
alert('Gagal menyimpan produk: ' + (error.response?.data?.message || error.message));
|
uploadError.value = error.response?.data?.message || 'Gagal menyimpan produk';
|
||||||
}
|
}
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,8 +6,7 @@
|
|||||||
message="Apakah Anda yakin ingin menghapus kategori ini?" @confirm="confirmDelete" @cancel="closeDeleteModal" />
|
message="Apakah Anda yakin ingin menghapus kategori ini?" @confirm="confirmDelete" @cancel="closeDeleteModal" />
|
||||||
<div class="p-6 min-h-[75vh]" >
|
<div class="p-6 min-h-[75vh]" >
|
||||||
<p class="font-serif italic text-[25px] text-D">KATEGORI</p>
|
<p class="font-serif italic text-[25px] text-D">KATEGORI</p>
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-end items-center mb-6">
|
||||||
|
|
||||||
<button @click="tambahKategori"
|
<button @click="tambahKategori"
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="px-4 py-2 bg-C text-black rounded-md hover:bg-B transition duration-200 flex items-center gap-2">
|
class="px-4 py-2 bg-C text-black rounded-md hover:bg-B transition duration-200 flex items-center gap-2">
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<mainLayout>
|
<mainLayout>
|
||||||
<!-- Modal Buat Item -->
|
<!-- Modal Buat Item -->
|
||||||
<CreateItemModal :isOpen="creatingItem" :product="detail" @close="closeItemModal" />
|
<CreateItemModal
|
||||||
|
:isOpen="creatingItem"
|
||||||
|
:product="detail"
|
||||||
|
@close="closeItemModal"
|
||||||
|
@itemAdded="handleItemAdded"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal Konfirmasi Hapus Produk -->
|
<!-- Modal Konfirmasi Hapus Produk -->
|
||||||
<ConfirmDeleteModal :isOpen="deleting" @cancel="deleting = false" @confirm="deleteProduk" title="Hapus Produk"
|
<ConfirmDeleteModal :isOpen="deleting" @cancel="deleting = false" @confirm="deleteProduk" title="Hapus Produk"
|
||||||
@ -268,6 +274,15 @@ function formatNumber(num) {
|
|||||||
return new Intl.NumberFormat().format(num || 0);
|
return new Intl.NumberFormat().format(num || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleItemAdded() {
|
||||||
|
if (detail.value) {
|
||||||
|
detail.value.items_count++;
|
||||||
|
}
|
||||||
|
creatingItem.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Hapus produk
|
// Hapus produk
|
||||||
async function deleteProduk() {
|
async function deleteProduk() {
|
||||||
try {
|
try {
|
||||||
@ -279,10 +294,8 @@ async function deleteProduk() {
|
|||||||
products.value = products.value.filter((p) => p.id !== detail.value.id);
|
products.value = products.value.filter((p) => p.id !== detail.value.id);
|
||||||
deleting.value = false;
|
deleting.value = false;
|
||||||
showOverlay.value = false;
|
showOverlay.value = false;
|
||||||
alert("Produk berhasil dihapus!");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Gagal hapus produk:", err);
|
console.error("Gagal hapus produk:", err);
|
||||||
alert("Gagal menghapus produk!");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
<div class="p-6 min-h-[75vh]">
|
<div class="p-6 min-h-[75vh]">
|
||||||
<p class="font-serif italic text-[25px] text-D">SALES</p>
|
<p class="font-serif italic text-[25px] text-D">SALES</p>
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-end items-center mb-6">
|
||||||
|
|
||||||
<button @click="tambahSales"
|
<button @click="tambahSales"
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
class="px-4 py-2 bg-C text-D rounded-md hover:bg-C/80 transition duration-200 flex items-center gap-2">
|
class="px-4 py-2 bg-C text-D rounded-md hover:bg-C/80 transition duration-200 flex items-center gap-2">
|
||||||
|
|||||||
@ -171,7 +171,7 @@ const handleConfirmAction = async () => {
|
|||||||
alert.value = { error: "Gagal mengosongkan nampan. Silakan coba lagi." };
|
alert.value = { error: "Gagal mengosongkan nampan. Silakan coba lagi." };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
clearTimeout(timer.value);
|
clearTimeout(timer.value);
|
||||||
timer.value = setTimeout(() => { alert.value = null }, 3000);
|
timer.value = setTimeout(() => { alert.value = null }, 3000);
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<meta name="twitter:description" content="@yield('description', 'Deskripsi default untuk aplikasi Abbauf.')">
|
<meta name="twitter:description" content="@yield('description', 'Deskripsi default untuk aplikasi Abbauf.')">
|
||||||
<meta name="twitter:image" content="@yield('og_image', asset('images/default-social-image.jpg'))">
|
<meta name="twitter:image" content="@yield('og_image', asset('images/default-social-image.jpg'))">
|
||||||
|
|
||||||
<link rel="icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
|
<link rel="icon" href="{{ asset('logo.ico') }}" type="image/x-icon">
|
||||||
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
|
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
|
||||||
|
|
||||||
<meta name="theme-color" content="#FFFFFF">
|
<meta name="theme-color" content="#FFFFFF">
|
||||||
|
|||||||
@ -102,7 +102,7 @@
|
|||||||
<div class="filter-info">
|
<div class="filter-info">
|
||||||
<h3>Informasi Filter</h3>
|
<h3>Informasi Filter</h3>
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }}
|
<span class="filter-label">Tanggal:</span> {{ $data['filter']['periode'] }}
|
||||||
</div>
|
</div>
|
||||||
@if($data['filter']['nama_sales'])
|
@if($data['filter']['nama_sales'])
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
|
|||||||
@ -102,7 +102,7 @@
|
|||||||
<div class="filter-info">
|
<div class="filter-info">
|
||||||
<h3>Informasi Filter</h3>
|
<h3>Informasi Filter</h3>
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<span class="filter-label">Tanggal:</span> {{ $data['filter']['tanggal'] }}
|
<span class="filter-label">Tanggal:</span> {{ $data['filter']['periode'] }}
|
||||||
</div>
|
</div>
|
||||||
@if($data['filter']['nama_sales'])
|
@if($data['filter']['nama_sales'])
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user