diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..e05cca7 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,51 @@ +validate([ + 'nama' => 'required', + 'password' => 'required', + ]); + + // cari user berdasarkan nama + $user = User::where('nama', $request->nama)->first(); + + if (!$user || !Hash::check($request->password, $user->password)) { + return response()->json([ + 'message' => 'Nama atau password salah' + ], 401); + } + + // buat token Sanctum + $token = $user->createToken('auth_token')->plainTextToken; + + $redirectUrl = $user->role === 'owner' ? '/brankas' : '/kasir'; + + return response()->json([ + 'message' => 'Login berhasil', + 'user' => $user, + 'token' => $token, + 'redirect' => $redirectUrl, + 'role' => $user->role + ]); + } + + public function logout(Request $request) + { + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'message' => 'Logout berhasil' + ]); + } +} diff --git a/app/Http/Controllers/LaporanController.php b/app/Http/Controllers/LaporanController.php new file mode 100644 index 0000000..32199ec --- /dev/null +++ b/app/Http/Controllers/LaporanController.php @@ -0,0 +1,276 @@ +query('filter', 'bulan'); + $page = $request->query('page', 1); + + $allSalesNames = Transaksi::select('nama_sales')->distinct()->pluck('nama_sales'); + + if ($filter === 'hari') { + return $this->laporanHarian($page, $allSalesNames); + } + + return $this->laporanBulanan($page, $allSalesNames); + } + + private function laporanHarian(int $page, Collection $allSalesNames) + { + $perPage = 7; + + $endDate = Carbon::today()->subDays(($page - 1) * $perPage); + $startDate = $endDate->copy()->subDays($perPage - 1); + + $transaksis = Transaksi::with('itemTransaksi.item.produk') + ->whereBetween('created_at', [$startDate->startOfDay(), $endDate->endOfDay()]) + ->orderBy('created_at', 'desc') + ->get(); + + $transaksisByDay = $transaksis->groupBy(function ($transaksi) { + return Carbon::parse($transaksi->created_at)->format('Y-m-d'); + }); + + $period = CarbonPeriod::create($startDate, $endDate); + $laporan = []; + + foreach ($period as $date) { + $dateString = $date->format('Y-m-d'); + $tanggalFormatted = $date->isoFormat('dddd, D MMMM Y'); + + if (isset($transaksisByDay[$dateString])) { + $transaksisPerTanggal = $transaksisByDay[$dateString]; + + $salesDataTransaksi = $transaksisPerTanggal->groupBy('nama_sales') + ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); + + $fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) { + return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales); + }); + + $totalItem = $fullSalesData->sum('item_terjual'); + $totalBerat = $fullSalesData->sum('berat_terjual_raw'); + $totalPendapatan = $fullSalesData->sum('pendapatan_raw'); + + $laporan[$dateString] = [ + 'tanggal' => $tanggalFormatted, + 'total_item_terjual' => $totalItem > 0 ? $totalItem : '-', + 'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-', + 'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-', + 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), + ]; + } else { + $laporan[$dateString] = [ + 'tanggal' => $tanggalFormatted, + 'total_item_terjual' => '-', + 'total_berat' => '-', + 'total_pendapatan' => '-', + 'sales' => [], + ]; + } + } + $totalHariUntukPaginasi = 365; + $paginatedData = new LengthAwarePaginator( + array_reverse(array_values($laporan)), + $totalHariUntukPaginasi, + $perPage, + $page, + ['path' => request()->url(), 'query' => request()->query()] + ); + + return response()->json($paginatedData); + } + + private function laporanBulanan(int $page, Collection $allSalesNames) + { + $perPage = 12; + + $transaksis = Transaksi::with('itemTransaksi.item.produk') + ->orderBy('created_at', 'desc') + ->get(); + + $laporan = $transaksis->groupBy(function ($transaksi) { + return Carbon::parse($transaksi->created_at)->format('F Y'); + }) + ->map(function ($transaksisPerTanggal, $tanggal) use ($allSalesNames) { + + $salesDataTransaksi = $transaksisPerTanggal + ->groupBy('nama_sales') + ->map(fn($transaksisPerSales) => $this->hitungDataSales($transaksisPerSales)); + + $fullSalesData = $allSalesNames->map(function ($namaSales) use ($salesDataTransaksi) { + return $salesDataTransaksi->get($namaSales) ?? $this->defaultSalesData($namaSales); + }); + + $totalItem = $fullSalesData->sum('item_terjual'); + $totalBerat = $fullSalesData->sum('berat_terjual_raw'); + $totalPendapatan = $fullSalesData->sum('pendapatan_raw'); + + return [ + 'tanggal' => $tanggal, + 'total_item_terjual' => $totalItem > 0 ? $totalItem : '-', + 'total_berat' => $totalBerat > 0 ? number_format($totalBerat, 2, ',', '.') . 'g' : '-', + 'total_pendapatan' => $totalPendapatan > 0 ? 'Rp' . number_format($totalPendapatan, 2, ',', '.') : '-', + 'sales' => $this->formatSalesDataValues($fullSalesData)->values(), + ]; + }); + + $paginatedData = new LengthAwarePaginator( + $laporan->forPage($page, $perPage)->values(), + $laporan->count(), + $perPage, + $page, + ['path' => request()->url(), 'query' => request()->query()] + ); + + return response()->json($paginatedData); + } + + private function hitungDataSales(Collection $transaksisPerSales): array + { + $itemTerjual = $transaksisPerSales->sum(fn($t) => $t->itemTransaksi->count()); + $beratTerjual = $transaksisPerSales->sum( + fn($t) => + $t->itemTransaksi->sum(fn($it) => $it->item->produk->berat ?? 0) + ); + $pendapatan = $transaksisPerSales->sum('total_harga'); + + return [ + 'nama' => $transaksisPerSales->first()->nama_sales, + 'item_terjual' => $itemTerjual, + 'berat_terjual_raw' => $beratTerjual, + 'pendapatan_raw' => $pendapatan, + ]; + } + + private function defaultSalesData(string $namaSales): array + { + return [ + 'nama' => $namaSales, + 'item_terjual' => 0, + 'berat_terjual_raw' => 0, + 'pendapatan_raw' => 0, + ]; + } + + private function formatSalesDataValues(Collection $salesData): Collection + { + return $salesData->map(function ($sale) { + $sale['item_terjual'] = $sale['item_terjual'] > 0 ? $sale['item_terjual'] : '-'; + $sale['berat_terjual'] = $sale['berat_terjual_raw'] > 0 ? number_format($sale['berat_terjual_raw'], 2, ',', '.') . 'g' : '-'; + $sale['pendapatan'] = $sale['pendapatan_raw'] > 0 ? 'Rp' . number_format($sale['pendapatan_raw'], 2, ',', '.') : '-'; + + unset($sale['berat_terjual_raw'], $sale['pendapatan_raw']); + 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); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 58a24b8..ed1d8b5 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -26,7 +26,7 @@ class UserController extends Controller User::create([ 'nama' => $request->nama, - 'password' => bcrypt($request->password), + 'password' => $request->password, 'role' => $request->role, ]); @@ -41,22 +41,26 @@ class UserController extends Controller $user = User::findOrFail($id); $request->validate([ - 'nama' => 'required|nama|unique:users,nama,' . $id, - 'password' => 'required|min:6', - 'role' => 'required|in:owner, kasir', + 'nama' => 'required|string|unique:users,nama,' . $id, + 'password' => 'nullable|min:6', + 'role' => 'required|in:owner,kasir', ]); - $user->update([ - 'nama' => $request->nama, - 'password' => $request->password, - 'role' => $request->role, - ]); + $data = [ + 'nama' => $request->nama, + 'role' => $request->role, + ]; - return response()->json([ - 'message' => 'User berhasil diupdate' - ],200); + if ($request->filled('password')) { + $data['password'] = $request->password; + } + + $user->update($data); + + return response()->json(['message' => 'User berhasil diupdate', 'user' => $user], 200); } + public function destroy($id) { $user = User::findOrFail($id); diff --git a/app/Http/Middleware/RoleMiddleware.php b/app/Http/Middleware/RoleMiddleware.php new file mode 100644 index 0000000..45363cc --- /dev/null +++ b/app/Http/Middleware/RoleMiddleware.php @@ -0,0 +1,30 @@ +user()) { + return response()->json(['message' => 'Unauthenticated'], 401); + } + + // cek role user + if (!in_array($request->user()->role, $roles)) { + return response()->json(['message' => 'Forbidden'], 403); + } + + return $next($request); + } +} diff --git a/app/Models/ItemTransaksi.php b/app/Models/ItemTransaksi.php index eb9f4ac..994e6e2 100644 --- a/app/Models/ItemTransaksi.php +++ b/app/Models/ItemTransaksi.php @@ -13,7 +13,8 @@ class ItemTransaksi extends Model protected $fillable = [ 'id_transaksi', 'id_item', - 'harga_deal' + 'harga_deal', + 'posisi_asal' ]; protected $hidden = ['created_at', 'updated_at', 'deleted_at']; diff --git a/app/Models/Transaksi.php b/app/Models/Transaksi.php index 31af4ef..3c49d24 100644 --- a/app/Models/Transaksi.php +++ b/app/Models/Transaksi.php @@ -37,14 +37,4 @@ class Transaksi extends Model { 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'); - } } diff --git a/app/Models/User.php b/app/Models/User.php index 69cb688..bc2848a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,11 +6,13 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; + class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. @@ -45,4 +47,9 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + public function getAuthIdentifierName() + { + return 'id'; + } } diff --git a/bootstrap/app.php b/bootstrap/app.php index b82d06f..9e7edfb 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -15,6 +15,10 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->validateCsrfTokens(except: [ 'api/*' ]); + + $middleware->alias([ + 'role' => \App\Http\Middleware\RoleMiddleware::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/composer.lock b/composer.lock index 5393e18..e575000 100644 --- a/composer.lock +++ b/composer.lock @@ -9377,12 +9377,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/config/auth.php b/config/auth.php index 7d1eb0d..0269b13 100644 --- a/config/auth.php +++ b/config/auth.php @@ -40,6 +40,11 @@ return [ 'driver' => 'session', 'provider' => 'users', ], + + 'api' => [ + 'driver' => 'sanctum', + 'provider' => 'users', + ], ], /* diff --git a/database/factories/ProdukFactory.php b/database/factories/ProdukFactory.php index 59c6fde..1d6901b 100644 --- a/database/factories/ProdukFactory.php +++ b/database/factories/ProdukFactory.php @@ -17,12 +17,16 @@ class ProdukFactory extends Factory */ public function definition(): array { + $kategori = Kategori::inRandomOrder()->first(); + $harga_per_gram = $this->faker->numberBetween(80, 120) * 10000; $berat = $this->faker->randomFloat(2, 1, 10); - $kategoriList = Kategori::all()->pluck('id')->toArray(); + return [ - 'nama' => $this->faker->words(3, true), - 'id_kategori' => $this->faker->randomElement($kategoriList), + 'nama' => $kategori->nama . ' ' . $this->faker->words(mt_rand(1, 2), true), + + 'id_kategori' => $kategori->id, + 'berat' => $berat, 'kadar' => $this->faker->numberBetween(10, 24), 'harga_per_gram' => $harga_per_gram, diff --git a/database/factories/TransaksiFactory.php b/database/factories/TransaksiFactory.php index 81b6cfb..fee977d 100644 --- a/database/factories/TransaksiFactory.php +++ b/database/factories/TransaksiFactory.php @@ -22,16 +22,19 @@ class TransaksiFactory extends Factory $sales = Sales::inRandomOrder()->first(); $kasir = User::inRandomOrder()->first(); + $date = $this->faker->dateTimeBetween('-3 months'); + $ongkos_bikin = $this->faker->numberBetween(8, 12) * 10000; return [ 'id_kasir' => $kasir?->id, 'id_sales' => $sales?->id, - 'nama_sales' => $sales?->nama ?? $this->faker->name(), - 'nama_pembeli' => $sales?->nama ?? $this->faker->name(), + 'nama_sales' => $sales?->nama, + 'nama_pembeli' => $this->faker->name(), 'no_hp' => $this->faker->phoneNumber(), 'alamat' => $this->faker->address(), - 'ongkos_bikin' => $this->faker->randomFloat(2, 0, 1000000), - 'total_harga' => $this->faker->randomFloat(2, 100000, 5000000), - 'created_at' => now(), + 'ongkos_bikin' => $ongkos_bikin, + 'total_harga' => $ongkos_bikin, + 'created_at' => $date, + 'updated_at' => $date, ]; } } diff --git a/database/migrations/2025_08_26_031033_create_item_transaksis_table.php b/database/migrations/2025_08_26_031033_create_item_transaksis_table.php index 25d4629..929e80b 100644 --- a/database/migrations/2025_08_26_031033_create_item_transaksis_table.php +++ b/database/migrations/2025_08_26_031033_create_item_transaksis_table.php @@ -16,6 +16,7 @@ return new class extends Migration $table->foreignId('id_transaksi')->constrained('transaksis')->onDelete('cascade'); $table->foreignId('id_item')->constrained('items'); $table->double('harga_deal'); + $table->string('posisi_asal', 100); $table->timestamps(); }); } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 3f3816c..d276c29 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -20,9 +20,14 @@ class DatabaseSeeder extends Seeder public function run(): void { User::factory()->create([ - 'nama' => 'Test User', + 'nama' => 'Owner', 'role' => 'owner', - 'password' => bcrypt('123123123'), + 'password' => bcrypt('123123'), + ]); + User::factory()->create([ + 'nama' => 'Kasir', + 'role' => 'kasir', + 'password' => bcrypt('123123'), ]); User::factory(2)->create(); @@ -77,17 +82,24 @@ class DatabaseSeeder extends Seeder } } - Transaksi::factory(20)->create()->each(function ($transaksi) { - $jumlah_item = rand(1, 5); + Transaksi::factory(40)->create()->each(function ($transaksi) { + $jumlah_item = rand(1, 2); $items = Item::where('is_sold', false)->inRandomOrder()->limit($jumlah_item)->get(); if ($items->isEmpty()) return; + $total_harga = $transaksi->total_harga; foreach ($items as $item) { $transaksi->itemTransaksi()->create([ 'id_item' => $item->id, '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]); }); } } diff --git a/resources/js/components/BrankasList.vue b/resources/js/components/BrankasList.vue index 5532207..958635f 100644 --- a/resources/js/components/BrankasList.vue +++ b/resources/js/components/BrankasList.vue @@ -45,10 +45,14 @@ const error = ref(null); onMounted(async () => { try { - const res = await axios.get("/api/item"); // ganti sesuai URL backend + const res = await axios.get("/api/item",{ + headers:{ + Authorization: `Bearer ${localStorage.getItem("token")}`, + } + }); // ganti sesuai URL backend items.value = res.data; // pastikan backend return array of items console.log(res.data); - + } catch (err) { error.value = err.message || "Gagal mengambil data"; } finally { diff --git a/resources/js/components/CreateAkun.vue b/resources/js/components/CreateAkun.vue index 9c50474..3876180 100644 --- a/resources/js/components/CreateAkun.vue +++ b/resources/js/components/CreateAkun.vue @@ -1,103 +1,99 @@ + - +}; + diff --git a/resources/js/components/CreateItemModal.vue b/resources/js/components/CreateItemModal.vue index ee35328..701e54a 100644 --- a/resources/js/components/CreateItemModal.vue +++ b/resources/js/components/CreateItemModal.vue @@ -113,7 +113,11 @@ const selectedNampanName = computed(() => { // Methods const loadNampanList = async () => { try { - const response = await axios.get('/api/nampan'); + const response = await axios.get('/api/nampan', { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + });; nampanList.value = response.data; positionListOptions.value = [ { value: '', label: 'Brankas', selected: !selectedNampan.value }, @@ -142,7 +146,11 @@ const createItem = async () => { payload.id_nampan = selectedNampan.value; } - const response = await axios.post('/api/item', payload); + const response = await axios.post('/api/item', payload, { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + });; success.value = true; createdItem.value = response.data.data diff --git a/resources/js/components/CreateKategori.vue b/resources/js/components/CreateKategori.vue index 441a3d2..1f88501 100644 --- a/resources/js/components/CreateKategori.vue +++ b/resources/js/components/CreateKategori.vue @@ -11,18 +11,17 @@ -
+
-
-
+
- + +
@@ -32,27 +49,38 @@ diff --git a/resources/js/components/DetailLaporan.vue b/resources/js/components/DetailLaporan.vue new file mode 100644 index 0000000..a5c113a --- /dev/null +++ b/resources/js/components/DetailLaporan.vue @@ -0,0 +1,100 @@ + + + \ No newline at end of file diff --git a/resources/js/components/EditAkun.vue b/resources/js/components/EditAkun.vue index 824b115..3007cd0 100644 --- a/resources/js/components/EditAkun.vue +++ b/resources/js/components/EditAkun.vue @@ -1,38 +1,54 @@