Compare commits

...

2 Commits

Author SHA1 Message Date
Baghaztra
b2b34a5f76 Merge branch 'production' of https://git.abbauf.com/Magang-2025/Kasir into production 2025-09-03 17:04:40 +07:00
Baghaztra
644d6fb222 [Feat] detail laporan 2025-09-03 17:04:37 +07:00
11 changed files with 272 additions and 63 deletions

View File

@ -2,12 +2,15 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\ItemTransaksi;
use App\Models\Produk;
use App\Models\Transaksi; use App\Models\Transaksi;
use Carbon\Carbon; use Carbon\Carbon;
use Carbon\CarbonPeriod; use Carbon\CarbonPeriod;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class LaporanController extends Controller class LaporanController extends Controller
{ {
@ -139,7 +142,8 @@ class LaporanController extends Controller
private function hitungDataSales(Collection $transaksisPerSales): array private function hitungDataSales(Collection $transaksisPerSales): array
{ {
$itemTerjual = $transaksisPerSales->sum(fn($t) => $t->itemTransaksi->count()); $itemTerjual = $transaksisPerSales->sum(fn($t) => $t->itemTransaksi->count());
$beratTerjual = $transaksisPerSales->sum(fn ($t) => $beratTerjual = $transaksisPerSales->sum(
fn($t) =>
$t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0) $t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0)
); );
$pendapatan = $transaksisPerSales->sum('total_harga'); $pendapatan = $transaksisPerSales->sum('total_harga');
@ -173,4 +177,100 @@ class LaporanController extends Controller
return $sale; return $sale;
}); });
} }
public function detail(Request $request)
{
// 1. VALIDASI DAN PENGAMBILAN PARAMETER FILTER
$request->validate([
'tanggal' => 'required|date_format:Y-m-d',
]);
$tanggal = $request->query('tanggal');
$namaSales = $request->query('nama_sales');
$posisi = $request->query('posisi');
$namaPembeli = $request->query('nama_pembeli'); // Untuk pencarian
$carbonDate = Carbon::parse($tanggal);
// 2. QUERY UTAMA UNTUK MENGAMBIL DATA PRODUK YANG TERJUAL BERDASARKAN FILTER
// Query ini hanya akan mengambil produk yang memiliki transaksi sesuai filter.
$produkTerjualQuery = ItemTransaksi::query()
->join('items', 'item_transaksis.id_item', '=', 'items.id')
->join('produks', 'items.id_produk', '=', 'produks.id')
->join('transaksis', 'item_transaksis.id_transaksi', '=', 'transaksis.id')
// Filter Wajib: Tanggal
->whereDate('transaksis.created_at', $carbonDate)
// Filter Opsional: Nama Sales
->when($namaSales, function ($query, $namaSales) {
return $query->where('transaksis.nama_sales', $namaSales);
})
// Filter Opsional: Posisi Asal Item
->when($posisi, function ($query, $posisi) {
return $query->where('item_transaksis.posisi_asal', $posisi);
})
// Filter Opsional: Nama Pembeli (menggunakan LIKE untuk pencarian)
->when($namaPembeli, function ($query, $namaPembeli) {
return $query->where('transaksis.nama_pembeli', 'like', "%{$namaPembeli}%");
})
->select(
'produks.id as id_produk',
'produks.nama as nama_produk',
DB::raw('COUNT(item_transaksis.id) as jumlah_item_terjual'),
DB::raw('SUM(produks.berat) as berat_terjual'),
DB::raw('SUM(item_transaksis.harga_deal) as pendapatan')
)
->groupBy('produks.id', 'produks.nama')
->get()
// Mengubah collection menjadi array asosiatif dengan key id_produk agar mudah dicari
->keyBy('id_produk');
// 3. MENGAMBIL SEMUA PRODUK DARI DATABASE
$semuaProduk = Produk::query()->select('id', 'nama')->get();
// 4. MENGGABUNGKAN DATA SEMUA PRODUK DENGAN PRODUK YANG TERJUAL
$detailItem = $semuaProduk->map(function ($produk) use ($produkTerjualQuery) {
// Cek apakah produk ini ada di dalam daftar produk yang terjual
if ($produkTerjualQuery->has($produk->id)) {
$dataTerjual = $produkTerjualQuery->get($produk->id);
return [
'nama_produk' => $produk->nama,
'jumlah_item_terjual' => (int) $dataTerjual->jumlah_item_terjual,
'berat_terjual' => (float) $dataTerjual->berat_terjual,
'pendapatan' => (float) $dataTerjual->pendapatan,
];
} else {
// Jika produk tidak terjual, berikan nilai default "-"
return [
'nama_produk' => $produk->nama,
'jumlah_item_terjual' => '-',
'berat_terjual' => '-',
'pendapatan' => '-',
];
}
});
// 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);
}
} }

View File

@ -13,7 +13,8 @@ class ItemTransaksi extends Model
protected $fillable = [ protected $fillable = [
'id_transaksi', 'id_transaksi',
'id_item', 'id_item',
'harga_deal' 'harga_deal',
'posisi_asal'
]; ];
protected $hidden = ['created_at', 'updated_at', 'deleted_at']; protected $hidden = ['created_at', 'updated_at', 'deleted_at'];

View File

@ -37,14 +37,4 @@ class Transaksi extends Model
{ {
return $this->hasMany(ItemTransaksi::class, 'id_transaksi'); return $this->hasMany(ItemTransaksi::class, 'id_transaksi');
} }
public function items()
{
return $this->hasMany(ItemTransaksi::class, 'id_transaksi');
}
public function foto ()
{
return $this->hasMany(Foto::class, 'id_produk');
}
} }

View File

@ -17,12 +17,16 @@ class ProdukFactory extends Factory
*/ */
public function definition(): array public function definition(): array
{ {
$kategori = Kategori::inRandomOrder()->first();
$harga_per_gram = $this->faker->numberBetween(80, 120) * 10000; $harga_per_gram = $this->faker->numberBetween(80, 120) * 10000;
$berat = $this->faker->randomFloat(2, 1, 10); $berat = $this->faker->randomFloat(2, 1, 10);
$kategoriList = Kategori::all()->pluck('id')->toArray();
return [ return [
'nama' => $this->faker->words(3, true), 'nama' => $kategori->nama . ' ' . $this->faker->words(mt_rand(1, 2), true),
'id_kategori' => $this->faker->randomElement($kategoriList),
'id_kategori' => $kategori->id,
'berat' => $berat, 'berat' => $berat,
'kadar' => $this->faker->numberBetween(10, 24), 'kadar' => $this->faker->numberBetween(10, 24),
'harga_per_gram' => $harga_per_gram, 'harga_per_gram' => $harga_per_gram,

View File

@ -23,15 +23,16 @@ class TransaksiFactory extends Factory
$kasir = User::inRandomOrder()->first(); $kasir = User::inRandomOrder()->first();
$date = $this->faker->dateTimeBetween('-3 months'); $date = $this->faker->dateTimeBetween('-3 months');
$ongkos_bikin = $this->faker->numberBetween(8, 12) * 10000;
return [ return [
'id_kasir' => $kasir?->id, 'id_kasir' => $kasir?->id,
'id_sales' => $sales?->id, 'id_sales' => $sales?->id,
'nama_sales' => $sales?->nama ?? $this->faker->name(), 'nama_sales' => $sales?->nama,
'nama_pembeli' => $sales?->nama ?? $this->faker->name(), 'nama_pembeli' => $this->faker->name(),
'no_hp' => $this->faker->phoneNumber(), 'no_hp' => $this->faker->phoneNumber(),
'alamat' => $this->faker->address(), 'alamat' => $this->faker->address(),
'ongkos_bikin' => $this->faker->randomFloat(2, 0, 1000000), 'ongkos_bikin' => $ongkos_bikin,
'total_harga' => $this->faker->randomFloat(2, 100000, 5000000), 'total_harga' => $ongkos_bikin,
'created_at' => $date, 'created_at' => $date,
'updated_at' => $date, 'updated_at' => $date,
]; ];

View File

@ -16,6 +16,7 @@ return new class extends Migration
$table->foreignId('id_transaksi')->constrained('transaksis')->onDelete('cascade'); $table->foreignId('id_transaksi')->constrained('transaksis')->onDelete('cascade');
$table->foreignId('id_item')->constrained('items'); $table->foreignId('id_item')->constrained('items');
$table->double('harga_deal'); $table->double('harga_deal');
$table->string('posisi_asal', 100);
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -77,17 +77,24 @@ class DatabaseSeeder extends Seeder
} }
} }
Transaksi::factory(20)->create()->each(function ($transaksi) { Transaksi::factory(40)->create()->each(function ($transaksi) {
$jumlah_item = rand(1, 5); $jumlah_item = rand(1, 2);
$items = Item::where('is_sold', false)->inRandomOrder()->limit($jumlah_item)->get(); $items = Item::where('is_sold', false)->inRandomOrder()->limit($jumlah_item)->get();
if ($items->isEmpty()) return; if ($items->isEmpty()) return;
$total_harga = $transaksi->total_harga;
foreach ($items as $item) { foreach ($items as $item) {
$transaksi->itemTransaksi()->create([ $transaksi->itemTransaksi()->create([
'id_item' => $item->id, 'id_item' => $item->id,
'harga_deal' => $item->produk->harga_jual, 'harga_deal' => $item->produk->harga_jual,
'posisi_asal' => $item->id_nampan ? 'Nampan ' . $item->nampan->nama : 'Brankas',
]); ]);
$item->update(['is_sold' => true]); $item->update([
'id_nampan' => null,
'is_sold' => true,
]);
$total_harga += $item->produk->harga_jual;
} }
$transaksi->update(['total_harga' => $total_harga]);
}); });
} }
} }

View File

@ -0,0 +1,79 @@
<template>
<div class="my-6">
<hr class="border-B mb-5" />
<div class="flez flex-row mb-3">
<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" />
</div>
<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>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import axios from 'axios';
const tanggalDipilih = ref('');
const data = ref(null);
const loading = ref(false);
const produk = computed(() => data.value?.produk || []);
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;
// console.log("Data berhasil diambil:", data.value);
} 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;
});
watch(tanggalDipilih, (newDate) => {
fetchData(newDate);
}, { immediate: true });
</script>

View File

@ -4,11 +4,14 @@
<p class="font-serif italic text-[25px] text-D">Laporan</p> <p class="font-serif italic text-[25px] text-D">Laporan</p>
<RingkasanLaporanB /> <RingkasanLaporanB />
<DetailLaporan />
</div> </div>
</mainLayout> </mainLayout>
</template> </template>
<script setup> <script setup>
import DetailLaporan from '../components/DetailLaporan.vue';
import RingkasanLaporanA from '../components/RingkasanLaporanA.vue'; import RingkasanLaporanA from '../components/RingkasanLaporanA.vue';
import RingkasanLaporanB from '../components/RingkasanLaporanB.vue'; import RingkasanLaporanB from '../components/RingkasanLaporanB.vue';
import mainLayout from "../layouts/mainLayout.vue"; import mainLayout from "../layouts/mainLayout.vue";

View File

@ -1,12 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable-no" />
<title>Abbauf App</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@yield('title', config('app.name', 'Abbauf App'))</title>
<meta name="description" content="@yield('description', 'Deskripsi default untuk aplikasi Abbauf.')">
<meta name="author" content="Nama Anda atau Perusahaan Anda">
<meta property="og:title" content="@yield('title', config('app.name', 'Abbauf App'))" />
<meta property="og:description" content="@yield('description', 'Deskripsi default untuk aplikasi Abbauf.')" />
<meta property="og:type" content="website" />
<meta property="og:url" content="{{ url()->current() }}" />
<meta property="og:image" content="@yield('og_image', asset('images/default-social-image.jpg'))" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="@yield('title', config('app.name', 'Abbauf App'))">
<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'))">
<link rel="icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
<link rel="apple-touch-icon" href="{{ asset('apple-touch-icon.png') }}">
<meta name="theme-color" content="#FFFFFF">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
@vite(['resources/js/app.js', 'resources/css/app.css'])
</html> </html>

View File

@ -42,6 +42,7 @@ Route::prefix('api')->group(function () {
// Laporan // Laporan
Route::get('laporan', [LaporanController::class, 'ringkasan']); Route::get('laporan', [LaporanController::class, 'ringkasan']);
Route::get('detail-laporan', [LaporanController::class, 'detail']);
}); });
Route::get('brankas', [ItemController::class, 'brankasItem']); Route::get('brankas', [ItemController::class, 'brankasItem']);