[Fix] transaksi

This commit is contained in:
Baghaztra 2025-09-12 11:10:13 +07:00
parent 03d01a1b78
commit eaa3318506
6 changed files with 253 additions and 312 deletions

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Transaksi;
use App\Models\ItemTransaksi;
use App\Models\Item;
use App\Models\Sales;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@ -20,17 +21,7 @@ class TransaksiController extends Controller
}
$transaksi = $query->get();
// Mapping agar sesuai dengan kebutuhan frontend
$mapped = $transaksi->map(function ($trx) {
return [
'id' => $trx->id,
'tanggal' => $trx->created_at->format('d/m/Y'),
'kode' => 'TRX-' . str_pad($trx->id, 6, '0', STR_PAD_LEFT),
'pendapatan'=> $trx->total_harga,
];
});
return response()->json($mapped);
return response()->json($transaksi); // Ubah $mapped menjadi $transaksi jika ingin mengirim data asli
}
@ -42,63 +33,76 @@ class TransaksiController extends Controller
}
// Membuat transaksi baru
public function store(Request $request)
{
// Ambil user yang login via Sanctum
$kasir = $request->user(); // user authenticated
if (!$kasir) {
return response()->json(['error' => 'Unauthorized'], 401);
}
// Validasi request (id_kasir dihapus karena otomatis dari token)
$request->validate([
'id_sales' => 'nullable|exists:sales,id',
'nama_sales' => 'required|string',
'no_hp' => 'required|string',
'alamat' => 'required|string',
'ongkos_bikin' => 'nullable|numeric|min:0',
'total_harga' => 'required|numeric',
'items' => 'required|array',
'items.*.id_item' => 'required|exists:items,id',
'items.*.harga_deal' => 'required|numeric',
]);
DB::beginTransaction();
try {
$transaksi = Transaksi::create([
'id_kasir' => $kasir->id, // ambil dari token
'id_sales' => $request->id_sales,
'nama_sales' => $request->nama_sales,
'no_hp' => $request->no_hp,
'alamat' => $request->alamat,
'ongkos_bikin' => $request->ongkos_bikin ?? 0,
'total_harga' => $request->total_harga,
]);
foreach ($request->items as $it) {
ItemTransaksi::create([
'id_transaksi' => $transaksi->id,
'id_item' => $it['id_item'],
'harga_deal' => $it['harga_deal'],
]);
Item::where('id', $it['id_item'])->update(['is_sold' => true]);
public function store(Request $request)
{
$kasir = $request->user();
if (!$kasir) {
return response()->json(['error' => 'Unauthorized'], 401);
}
DB::commit();
return response()->json(
$transaksi->load(['itemTransaksi.item.produk.foto', 'kasir', 'sales']),
201
);
$request->validate([
'id_sales' => 'required|exists:sales,id',
'nama_pembeli' => 'required|string',
'no_hp' => 'required|string',
'alamat' => 'required|string',
'ongkos_bikin' => 'nullable|numeric|min:0',
'total_harga' => 'required|numeric',
'items' => 'required|array',
'items.*.kode_item' => 'required|exists:items,id',
'items.*.harga_deal' => 'required|numeric',
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'error' => $e->getMessage(),
'trace' => $e->getTrace()
], 500);
}
}
DB::beginTransaction();
try {
$sales = Sales::find($request->id_sales);
$transaksi = Transaksi::create([
'kode_transaksi' => 'belum pak',
'id_kasir' => $kasir->id,
'id_sales' => $request->id_sales,
'nama_sales' => $sales->nama ?? 'N/A',
'nama_pembeli' => $request->nama_pembeli,
'no_hp' => $request->no_hp,
'alamat' => $request->alamat,
'ongkos_bikin' => $request->ongkos_bikin ?? 0,
'total_harga' => $request->total_harga,
]);
foreach ($request->items as $it) {
// TODO: ubah saat transaksi pake kode_item
// $item = Item::where('kode_item', $it['kode_item'])->first();
// if (!$item) {
// throw new \Exception("Item dengan kode_item {$it['kode_item']} tidak ditemukan.");
// }
$item = Item::find($it['kode_item']);
ItemTransaksi::create([
'id_transaksi' => $transaksi->id,
'id_item' => $item->id,
'harga_deal' => $it['harga_deal'],
'posisi_asal' => $item->nampan ? 'Nampan ' . $item->nampan->nama : 'Brankas',
]);
$item->update([
'is_sold' => true,
'id_nampan' => null,
]);
}
DB::commit();
return response()->json(
$transaksi->load(['itemTransaksi.item.produk.foto', 'kasir', 'sales']),
201
);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'error' => $e->getMessage(),
'trace' => $e->getTrace()
], 500);
}
}
// Update transaksi
@ -107,7 +111,12 @@ class TransaksiController extends Controller
$transaksi = Transaksi::findOrFail($id);
$transaksi->update($request->only([
'id_sales', 'nama_sales', 'no_hp', 'alamat', 'ongkos_bikin', 'total_harga'
'id_sales',
'nama_sales',
'no_hp',
'alamat',
'ongkos_bikin',
'total_harga'
]));
return response()->json($transaksi);

View File

@ -11,7 +11,7 @@ class Transaksi extends Model
use HasFactory;
protected $fillable = [
'kode_transaksi', // ✅ Tambahin kolom kode transaksi
'kode_transaksi',
'id_kasir',
'id_sales',
'nama_sales',
@ -25,23 +25,21 @@ class Transaksi extends Model
protected $hidden = ['updated_at', 'deleted_at'];
// ✅ Auto-generate kode_transaksi saat create
protected static function boot()
{
parent::boot();
{
parent::boot();
// Setelah transaksi berhasil dibuat (sudah punya ID)
static::created(function ($transaksi) {
if (!$transaksi->kode_transaksi) {
$prefix = "TRS";
$date = $transaksi->created_at->format('Ymd');
$number = str_pad($transaksi->id, 4, '0', STR_PAD_LEFT);
static::created(function ($transaksi) {
if (!$transaksi->kode_transaksi || $transaksi->kode_transaksi === 'belum pak') {
$prefix = "TRS";
$date = $transaksi->created_at->format('Ymd');
$number = str_pad($transaksi->id, 4, '0', STR_PAD_LEFT);
$transaksi->kode_transaksi = $prefix . $date . $number;
$transaksi->save();
}
});
}
$transaksi->kode_transaksi = $prefix . $date . $number;
$transaksi->save();
}
});
}
public function kasir()
{

View File

@ -1,22 +1,9 @@
<template>
<ConfirmDeleteModal
v-if="showDeleteModal"
:isOpen="showDeleteModal"
title="Konfirmasi"
message="Yakin ingin menghapus item ini?"
@confirm="hapusPesanan"
@cancel="closeDeleteModal"
/>
<ConfirmDeleteModal v-if="showDeleteModal" :isOpen="showDeleteModal" title="Konfirmasi"
message="Yakin ingin menghapus item ini?" @confirm="hapusPesanan" @cancel="closeDeleteModal" />
<!-- ==== TAMBAHAN: Struk Overlay ==== -->
<StrukOverlay
v-if="showStruk"
:isOpen="showStruk"
:pesanan="pesanan"
:total="total"
@close="closeStruk"
@confirm="simpanTransaksi"
/>
<StrukOverlay v-if="showStruk" :isOpen="showStruk" :pesanan="pesanan" :total="total" @close="closeStruk"/>
<!-- ==== END TAMBAHAN ==== -->
<div class="p-2 sm:p-4">
@ -27,27 +14,14 @@
<!-- Input Kode Item -->
<div>
<label class="block text-sm font-medium text-D">Kode Item *</label>
<div
class="flex flex-row justify-between mt-1 w-full rounded-md bg-A shadow-sm sm:text-sm border-B"
>
<input
type="text"
v-model="kodeItem"
@keyup.enter="inputItem"
placeholder="Scan atau masukkan kode item"
class="bg-A focus:outline-none focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full rounded-l-md"
/>
<button
v-if="!loadingItem"
@click="inputItem"
class="px-3 bg-D hover:bg-D/80 text-A rounded-r-md"
>
<div class="flex flex-row justify-between mt-1 w-full rounded-md bg-A shadow-sm sm:text-sm border-B">
<input type="text" v-model="kodeItem" @keyup.enter="inputItem" placeholder="Scan atau masukkan kode item"
class="bg-A focus:outline-none focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full rounded-l-md" />
<button v-if="!loadingItem" @click="inputItem" class="px-3 bg-D hover:bg-D/80 text-A rounded-r-md">
<i class="fas fa-arrow-right"></i>
</button>
<div v-else class="flex items-center justify-center px-3">
<div
class="rounded-full h-5 w-5 border-b-2 border-A flex items-center justify-center"
>
<div class="rounded-full h-5 w-5 border-b-2 border-A flex items-center justify-center">
<i class="fas fa-spinner"></i>
</div>
</div>
@ -57,25 +31,17 @@
<!-- Input Harga Jual -->
<div>
<label class="block text-sm font-medium text-D">Harga Jual</label>
<InputField
v-model="hargaJual"
type="number"
placeholder="Masukkan Harga Jual"
/>
<InputField v-model="hargaJual" type="number" placeholder="Masukkan Harga Jual" />
</div>
<!-- Tombol Aksi -->
<div class="flex flex-col sm:flex-row justify-between gap-2">
<button
@click="tambahItem"
class="w-full sm:w-auto px-4 py-2 rounded-md bg-C text-D font-medium hover:bg-C/80 transition"
>
<button @click="tambahItem"
class="w-full sm:w-auto px-4 py-2 rounded-md bg-C text-D font-medium hover:bg-C/80 transition">
Tambah Item
</button>
<button
@click="konfirmasiPenjualan"
class="w-full sm:w-auto px-6 py-2 rounded-md bg-D text-A font-semibold hover:bg-D/80 transition"
>
<button @click="konfirmasiPenjualan"
class="w-full sm:w-auto px-6 py-2 rounded-md bg-D text-A font-semibold hover:bg-D/80 transition">
Lanjut
</button>
</div>
@ -94,64 +60,50 @@
<!-- Error & Info -->
<div class="mb-4">
<p
v-if="error"
:class="{ 'animate-shake': error }"
class="text-sm text-red-600 mt-1"
>
<p v-if="error" :class="{ 'animate-shake': error }" class="text-sm text-red-600 mt-1">
{{ error }}
</p>
<p v-if="info" class="text-sm text-C mt-1">{{ info }}</p>
</div>
<!-- Table Responsive -->
<div class="overflow-x-auto">
<table
class="w-full border border-B rounded-lg overflow-hidden text-xs sm:text-sm"
>
<thead class="bg-A text-D">
<tr>
<th class="border border-B p-2 w-8">No</th>
<th class="border border-B p-2">Nama Produk</th>
<th class="border border-B p-2">Posisi</th>
<th class="border border-B p-2">Harga</th>
<th class="border border-B p-2 w-10"></th>
</tr>
</thead>
<tbody>
<tr v-if="pesanan.length == 0" class="text-center text-D/70">
<td colspan="5" class="h-16 border border-B text-xs sm:text-sm">
Belum ada item dipesan
</td>
</tr>
<tr
v-else
v-for="(item, index) in pesanan"
:key="index"
class="hover:bg-gray-50 text-center"
>
<td class="border border-B p-2">{{ index + 1 }}</td>
<td class="border border-B p-2 text-left truncate max-w-[120px] sm:max-w-none">
{{ item.produk.nama }}
</td>
<td class="border border-B p-2 truncate max-w-[80px]">
{{ item.posisi ? item.posisi : "Brankas" }}
</td>
<td class="border border-B p-2 whitespace-nowrap">
Rp{{ item.harga_deal.toLocaleString() }}
</td>
<td class="border border-B p-2 text-center">
<button
@click="openDeleteModal(index)"
class="text-red-500 hover:text-red-700"
>
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="overflow-x-auto">
<table class="w-full border border-B rounded-lg overflow-hidden text-xs sm:text-sm">
<thead class="bg-A text-D">
<tr>
<th class="border border-B p-2 w-8">No</th>
<th class="border border-B p-2">Nama Produk</th>
<th class="border border-B p-2">Posisi</th>
<th class="border border-B p-2">Harga</th>
<th class="border border-B p-2 w-10"></th>
</tr>
</thead>
<tbody>
<tr v-if="pesanan.length == 0" class="text-center text-D/70">
<td colspan="5" class="h-16 border border-B text-xs sm:text-sm">
Belum ada item dipesan
</td>
</tr>
<tr v-else v-for="(item, index) in pesanan" :key="index" class="hover:bg-gray-50 text-center">
<td class="border border-B p-2">{{ index + 1 }}</td>
<td class="border border-B p-2 text-left truncate max-w-[120px] sm:max-w-none">
{{ item.produk.nama }}
</td>
<td class="border border-B p-2 truncate max-w-[80px]">
{{ item.posisi ? item.posisi : "Brankas" }}
</td>
<td class="border border-B p-2 whitespace-nowrap">
Rp{{ item.harga_deal.toLocaleString() }}
</td>
<td class="border border-B p-2 text-center">
<button @click="openDeleteModal(index)" class="text-red-500 hover:text-red-700">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
@ -162,9 +114,7 @@ import { ref, computed } from "vue";
import InputField from "./InputField.vue";
import axios from "axios";
import ConfirmDeleteModal from "./ConfirmDeleteModal.vue";
// ==== TAMBAHAN: Import StrukOverlay ====
import StrukOverlay from "./StrukOverlay.vue";
// ==== END TAMBAHAN ====
const kodeItem = ref("");
const info = ref("");
@ -176,92 +126,86 @@ const pesanan = ref([]);
const showDeleteModal = ref(false)
const deleteIndex = ref(null)
// ==== TAMBAHAN: State untuk struk ====
const showStruk = ref(false);
// ==== END TAMBAHAN ====
// ==== TAMBAHAN: Emit untuk parent component ====
const emit = defineEmits(['transaksi-saved']);
// ==== END TAMBAHAN ====
let errorTimeout = null;
let infoTimeout = null;
const inputItem = async () => {
if (!kodeItem.value) return;
if (!kodeItem.value) return;
info.value = "";
error.value = "";
clearTimeout(infoTimeout);
clearTimeout(errorTimeout);
info.value = "";
error.value = "";
clearTimeout(infoTimeout);
clearTimeout(errorTimeout);
loadingItem.value = true;
loadingItem.value = true;
try {
const response = await axios.get(`/api/item/${kodeItem.value}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
item.value = response.data;
hargaJual.value = item.value.produk.harga_jual;
try {
const response = await axios.get(`/api/item/${kodeItem.value}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
item.value = response.data;
hargaJual.value = item.value.produk.harga_jual;
if (item.value.is_sold) {
throw new Error("Item sudah terjual");
}
if (pesanan.value.some((p) => p.id === item.value.id)) {
throw new Error("Item sedang dipesan");
}
info.value = `Item dipilih: ${item.value.produk.nama} dari ${
item.value.posisi ? item.value.posisi : "Brankas"
}`;
infoTimeout = setTimeout(() => {
info.value = "";
}, 3000);
} catch (err) {
if (err == "") {
error.value = "Error: Item tidak ditemukan";
} else {
error.value = err;
}
info.value = "";
hargaJual.value = null;
item.value = null;
errorTimeout = setTimeout(() => {
error.value = "";
}, 3000);
} finally {
loadingItem.value = false;
if (item.value.is_sold) {
throw new Error("Item sudah terjual");
}
if (pesanan.value.some((p) => p.id === item.value.id)) {
throw new Error("Item sedang dipesan");
}
info.value = `Item dipilih: ${item.value.produk.nama} dari ${item.value.posisi ? item.value.posisi : "Brankas"
}`;
infoTimeout = setTimeout(() => {
info.value = "";
}, 3000);
} catch (err) {
if (err == "") {
error.value = "Error: Item tidak ditemukan";
} else {
error.value = err;
}
info.value = "";
hargaJual.value = null;
item.value = null;
errorTimeout = setTimeout(() => {
error.value = "";
}, 3000);
} finally {
loadingItem.value = false;
}
};
const tambahItem = () => {
if (!item.value || !hargaJual.value) {
error.value = "Scan atau masukkan kode item untuk dijual.";
if (kodeItem.value) {
error.value =
"Masukkan harga jual, atau input dari kode item lagi.";
}
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
error.value = "";
}, 3000);
return;
if (!item.value || !hargaJual.value) {
error.value = "Scan atau masukkan kode item untuk dijual.";
if (kodeItem.value) {
error.value =
"Masukkan harga jual, atau input dari kode item lagi.";
}
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
error.value = "";
}, 3000);
return;
}
// harga deal
item.value.harga_deal = Number(hargaJual.value);
// harga deal
item.value.kode_item = kodeItem.value;
item.value.harga_deal = Number(hargaJual.value);
pesanan.value.push(item.value);
pesanan.value.push(item.value);
// Reset input fields
kodeItem.value = "";
hargaJual.value = null;
item.value = null;
info.value = "";
clearTimeout(infoTimeout);
// Reset input fields
kodeItem.value = "";
hargaJual.value = null;
item.value = null;
info.value = "";
clearTimeout(infoTimeout);
};
const openDeleteModal = (index) => {
@ -283,71 +227,31 @@ const hapusPesanan = () => {
// ==== MODIFIKASI: konfirmasiPenjualan sekarang menampilkan struk ====
const konfirmasiPenjualan = () => {
if (pesanan.value.length === 0) {
error.value = "Belum ada item yang dipesan.";
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
error.value = "";
}, 3000);
return;
}
if (pesanan.value.length === 0) {
error.value = "Belum ada item yang dipesan.";
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
error.value = "";
}, 3000);
return;
}
// Tampilkan struk overlay
showStruk.value = true;
// Tampilkan struk overlay
showStruk.value = true;
};
// ==== END MODIFIKASI ====
// ==== TAMBAHAN: Fungsi untuk menutup struk ====
const closeStruk = () => {
showStruk.value = false;
};
// ==== END TAMBAHAN ====
// ==== TAMBAHAN: Fungsi untuk menyimpan transaksi ====
const simpanTransaksi = async (dataTransaksi) => {
try {
// Siapkan data untuk API
const transaksiData = {
id_kasir: localStorage.getItem('user_id'), // Asumsi user_id disimpan di localStorage
id_sales: dataTransaksi.selectedSales?.id || null,
nama_sales: dataTransaksi.namaPembeli,
no_hp: dataTransaksi.nomorTelepon,
alamat: dataTransaksi.alamat,
ongkos_bikin: dataTransaksi.ongkosBikin || 0,
total_harga: total.value + (dataTransaksi.ongkosBikin || 0),
items: pesanan.value.map(item => ({
id_item: item.id,
harga_deal: item.harga_deal
}))
};
const response = await axios.post('/api/transaksi', transaksiData, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
// Reset form setelah berhasil
pesanan.value = [];
showStruk.value = false;
// Emit ke parent untuk refresh data transaksi
emit('transaksi-saved', response.data);
alert('Transaksi berhasil disimpan!');
} catch (error) {
console.error('Error saving transaksi:', error);
alert('Error menyimpan transaksi: ' + (error.response?.data?.message || error.message));
}
showStruk.value = false;
};
// ==== END TAMBAHAN ====
const total = computed(() => {
let sum = 0;
pesanan.value.forEach((item) => {
sum += item.harga_deal;
});
return sum;
let sum = 0;
pesanan.value.forEach((item) => {
sum += item.harga_deal;
});
return sum;
});
</script>

View File

@ -14,7 +14,7 @@
<tr v-for="trx in props.transaksi" :key="trx.id" class="hover:bg-A">
<td class="border border-B p-2">{{ trx.tanggal }}</td>
<td class="border border-B p-2">{{ trx.kode }}</td>
<td class="border border-B p-2">Rp{{ (trx.pendapatan || 0).toLocaleString() }}</td>
<td class="border border-B p-2">Rp{{ (trx.total_harga || 0).toLocaleString() }}</td>
<td class="border border-B p-2 text-center">
<button
@click="$emit('detail', trx)"

View File

@ -260,7 +260,7 @@ const fetchSales = async () => {
})
salesOptions.value = response.data.map(sales => ({
value: sales,
value: sales.id,
label: sales.nama
}))
@ -298,14 +298,41 @@ const handleSimpan = () => {
}
// Emit data ke parent
emit('confirm', {
namaPembeli: namaPembeli.value,
nomorTelepon: nomorTelepon.value,
simpanTransaksi({
id_sales: selectedSales.value,
nama_pembeli: namaPembeli.value,
no_hp: nomorTelepon.value,
alamat: alamat.value,
ongkosBikin: ongkosBikin.value || 0,
selectedSales: selectedSales.value
total_harga: grandTotal.value,
items: props.pesanan
})
}
// ==== TAMBAHAN: Fungsi untuk menyimpan transaksi ====
const simpanTransaksi = async (dataTransaksi) => {
console.log('Data transaksi yang akan disimpan:', dataTransaksi);
try {
const response = await axios.post('/api/transaksi', dataTransaksi, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
// Reset form setelah berhasil
props.pesanan.value = [];
props.isOpen = false;
alert('Transaksi berhasil disimpan!');
window.location.reload();
} catch (error) {
console.error('Error saving transaksi:', error);
alert('Error menyimpan transaksi: ' + (error.response?.data?.message || error.message));
}
};
// ==== END TAMBAHAN ====
// ==== END TAMBAHAN ====
// ==== TAMBAHAN: Fetch sales saat component mounted ====

View File

@ -95,6 +95,9 @@ const fetchTransaksi = async () => {
});
transaksi.value = res.data;
console.log("Fetched transaksi:", transaksi.value);
} catch (err) {
console.error("Gagal fetch transaksi:", err);
} finally {