[Feat] Sistem logic kasir

This commit is contained in:
Baghaztra 2025-09-02 11:44:05 +07:00
parent 3f654c6c7a
commit a99996940e
10 changed files with 261 additions and 149 deletions

View File

@ -42,7 +42,7 @@ class ItemController extends Controller
*/
public function show(int $id)
{
$item = Item::with('produk.foto','nampan')->findOrFail($id);
$item = Item::with('produk.foto','nampan','itemTransaksi.transaksi')->findOrFail($id);
return response()->json($item);
}

View File

@ -35,6 +35,6 @@ class Item extends Model
public function itemTransaksi()
{
return $this->hasMany(ItemTransaksi::class, 'id_item');
return $this->hasOne(ItemTransaksi::class, 'id_item');
}
}

View File

@ -13,6 +13,7 @@ class Transaksi extends Model
'id_kasir',
'id_sales',
'nama_sales',
'nama_pembeli',
'no_hp',
'alamat',
'ongkos_bikin',

View File

@ -26,6 +26,7 @@ class TransaksiFactory extends Factory
'id_kasir' => $kasir?->id,
'id_sales' => $sales?->id,
'nama_sales' => $sales?->nama ?? $this->faker->name(),
'nama_pembeli' => $sales?->nama ?? $this->faker->name(),
'no_hp' => $this->faker->phoneNumber(),
'alamat' => $this->faker->address(),
'ongkos_bikin' => $this->faker->randomFloat(2, 0, 1000000),

View File

@ -16,6 +16,7 @@ return new class extends Migration
$table->foreignId('id_kasir')->constrained('users');
$table->foreignId('id_sales')->nullable()->constrained('sales');
$table->string('nama_sales', 100);
$table->string('nama_pembeli', 100);
$table->string('no_hp', 20);
$table->string('alamat', 100);
$table->double('ongkos_bikin')->nullable();

View File

@ -1,17 +1,17 @@
<template>
<footer class="bg-B border-t border-D py-4 px-6 flex flex-col md:flex-row items-center justify-between">
<footer class="bg-B py-4 px-6 flex flex-col md:flex-row items-center justify-between">
<!-- Left: Logo -->
<div class="flex items-center gap-2">
<img :src="logo" alt="Logo" class="h-10">
</div>
<!-- Center: Copyright -->
<div class="text-sm text-[#0f1d4a] font-medium text-center">
<div class="text-sm text-D font-medium text-center">
Abbauf Tech © 2025 Semua hak dilindungi
</div>
<!-- Right: Social Icons -->
<div class="flex items-center gap-4 text-[#0f1d4a] mt-2 md:mt-0">
<div class="flex items-center gap-4 text-D mt-2 md:mt-0">
<a href="#" class="hover:text-sky-600"><i class="fab fa-facebook"></i></a>
<a href="#" class="hover:text-sky-600"><i class="fab fa-twitter"></i></a>
<a href="#" class="hover:text-sky-600"><i class="fab fa-instagram"></i></a>
@ -24,8 +24,3 @@
<script setup>
import logo from '@/../images/logo.png'
</script>
<style>
/* Pakai Font Awesome untuk ikon */
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
</style>

View File

@ -1,95 +1,181 @@
<template>
<div>
<!-- Input Grid -->
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="grid grid-cols-2 h-full gap-4 mb-4">
<div class="flex flex-col gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Kode Item *</label>
<InputField
v-model="kodeItem"
type="text"
placeholder="Masukkan kode item"
/>
<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:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full" />
<button v-if="!loadingItem" @click="inputItem" class="px-3 bg-D hover:bg-D/80 text-A rounded-r-md"><i
class="fas fa-arrow-right"></i></button>
<div v-else class="flex items-center justify-center px-3">
<div class="rounded-full h-5 w-5 border-b-2 border-A flex items-center justify-center">
<i class="fas fa-spinner"></i>
</div>
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Harga Jual</label>
<InputField
v-model="hargaJual"
type="number"
placeholder="Masukkan Harga Jual"
/>
<label class="block text-sm font-medium text-D">Harga Jual</label>
<InputField v-model="hargaJual" type="number" placeholder="Masukkan Harga Jual" />
</div>
<div class="flex justify-between gap-4">
<button @click="tambahItem" class="px-4 py-2 rounded-md bg-C text-D font-medium hover:bg-C/80 transition">
Tambah Item
</button>
<button @click="konfirmasiPenjualan"
class="px-6 py-2 rounded-md bg-D text-A font-semibold hover:bg-D/80 transition">
Lanjut
</button>
</div>
</div>
<div class="flex items-center justify-center">
<div class="text-center">
<div class="flex pt-10 justify-center">
<div class="text-start">
<span class="block text-gray-600 font-medium">Total:</span>
<span class="text-3xl font-bold text-[#0f1d4a]">
<span class="text-3xl font-bold text-D">
Rp{{ total.toLocaleString() }},-
</span>
</div>
</div>
</div>
<!-- Buttons -->
<div class="flex gap-4 mb-6">
<button @click="tambahItem"
class="px-4 py-2 rounded-md bg-[#f1ede8] text-[#0f1d4a] font-medium hover:bg-[#e4dfd8] transition">
Tambah Item
</button>
<button
class="px-6 py-2 rounded-md bg-[#c6a77d] text-[#0f1d4a] font-semibold hover:bg-[#b09065] transition">
Lanjut
</button>
<div class="mb-4">
<p v-if="error" :class="{ 'animate-shake': error }" class="text-sm text-red-600 mt-1">{{ error }}</p>
<p v-if="info" class="text-sm text-C mt-1">{{ info }}</p>
</div>
<!-- Table -->
<table class="w-full border-collapse border border-gray-200 text-sm rounded-lg overflow-hidden">
<thead class="bg-gray-100 text-[#0f1d4a]">
<table class="w-full border border-B text-sm rounded-lg overflow-hidden">
<thead class="bg-A text-D">
<tr>
<th class="border border-gray-200 p-2">No</th>
<th class="border border-gray-200 p-2">Item</th>
<th class="border border-gray-200 p-2">Jml</th>
<th class="border border-gray-200 p-2">Harga</th>
<th class="border border-gray-200 p-2">Total</th>
<th class="border border-B p-2">No</th>
<th class="border border-B p-2">Nam Produk </th>
<th class="border border-B p-2">Posisi</th>
<th class="border border-B p-2">Harga</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in pesanan" :key="index" class="hover:bg-gray-50 text-center">
<td class="border border-gray-200 p-2">{{ index + 1 }}</td>
<td class="border border-gray-200 p-2">{{ item.kode }}</td>
<td class="border border-gray-200 p-2">{{ item.jumlah }}</td>
<td class="border border-gray-200 p-2">Rp{{ item.harga.toLocaleString() }}</td>
<td class="border border-gray-200 p-2">Rp{{ (item.harga * item.jumlah).toLocaleString() }}</td>
<tr v-if="pesanan.length == 0" class="text-center text-D/70">
<td colspan="5" class="h-20 border border-B">Belum ada item dipesan</td>
</tr>
<tr v-else v-for="(item, index) in pesanan" :key="index" class="hover:bg-gray-50 text-center">
<td class="border border-B p-2">{{ index + 1 }}</td>
<td class="border border-B p-2 text-left">{{ item.produk.nama }}</td>
<td class="border border-B p-2">{{ item.posisi ? item.posisi : 'Brankas' }}</td>
<td class="border border-B p-2">Rp{{ item.harga_deal.toLocaleString() }}</td>
</tr>
</tbody>
</table>
</div>
</template>
</template>
<script setup>
import { ref, computed } from 'vue'
<script setup>
import { ref, computed } from 'vue'
import InputField from './InputField.vue'
import axios from 'axios'
const kodeItem = ref('')
const hargaJual = ref(null)
const pesanan = ref([])
const kodeItem = ref('')
const info = ref('')
const error = ref('')
const hargaJual = ref(null)
const item = ref(null)
const loadingItem = ref(false)
const pesanan = ref([])
const tambahItem = () => {
if (!kodeItem.value || !hargaJual.value) return
pesanan.value.push({
kode: kodeItem.value,
jumlah: 1,
harga: parseFloat(hargaJual.value),
})
kodeItem.value = ''
hargaJual.value = 0
let errorTimeout = null
let infoTimeout = null
const inputItem = async () => {
if (!kodeItem.value) return
info.value = ''
error.value = ''
clearTimeout(infoTimeout)
clearTimeout(errorTimeout)
loadingItem.value = true
try {
const response = await axios.get(`/api/item/${kodeItem.value}`);
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
}
}
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
}
const total = computed(() =>
pesanan.value.reduce((sum, item) => sum + item.harga * item.jumlah, 0)
)
</script>
// harga deal
item.value.harga_deal = hargaJual.value
pesanan.value.push(item.value)
// Reset input fields
kodeItem.value = ''
hargaJual.value = null
item.value = null
info.value = ''
clearTimeout(infoTimeout)
}
const konfirmasiPenjualan = () => {
if (pesanan.value.length === 0) {
error.value = 'Belum ada item yang dipesan.'
clearTimeout(errorTimeout)
errorTimeout = setTimeout(() => {
error.value = ''
}, 3000)
return
}
// Todo: Implementasi konfirmasi penjualan
alert('Penjualan dikonfirmasi! (Implementasi lebih lanjut diperlukan)')
}
const total = computed(() => {
let sum = 0;
pesanan.value.forEach(item => {
sum += item.harga_deal;
});
return sum;
})
</script>

View File

@ -1,42 +1,35 @@
<template>
<h3 class="text-lg font-semibold mb-4 text-gray-800">Transaksi</h3>
<table class="w-full border-collapse border border-gray-200 text-sm">
<thead class="bg-gray-100">
<table class="w-full border border-B rounded-lg text-sm">
<thead class="bg-A text-D">
<tr>
<th class="border border-gray-200 p-2">Tanggal</th>
<th class="border border-gray-200 p-2">Kode Transaksi</th>
<th class="border border-gray-200 p-2">Pendapatan</th>
<th class="border border-gray-200 p-2">Detail Item</th>
<th class="border border-B p-2">Tanggal</th>
<th class="border border-B p-2">Kode Transaksi</th>
<th class="border border-B p-2">Pendapatan</th>
<th class="border border-B p-2">Detail Item</th>
</tr>
</thead>
<tbody>
<tr v-for="trx in props.transaksi" :key="trx.id">
<td class="border border-gray-200 p-2">{{ trx.tanggal }}</td>
<td class="border border-gray-200 p-2">{{ trx.kode }}</td>
<td class="border border-gray-200 p-2">Rp{{ (trx.pendapatan || 0).toLocaleString() }}</td>
<td class="border border-gray-200 p-2 text-center">
<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 text-center">
<button @click="$emit('detail', trx)"
class="px-3 py-1 rounded-md bg-[#c6a77d] text-white hover:bg-[#b09065] transition">Detail</button>
class="px-3 py-1 rounded-md bg-D text-A hover:bg-D/80 transition">Detail</button>
</td>
</tr>
</tbody>
</table>
</template>
</template>
<script setup>
import { ref, onMounted } from "vue"
const props = defineProps({
<script setup>
const props = defineProps({
transaksi: {
type: Array,
default: () => []
}
})
})
defineEmits(['detail'])
onMounted(() => {
console.log(props.transaksi);
})
</script>
defineEmits(['detail'])
</script>

View File

@ -73,7 +73,7 @@ onBeforeUnmount(() => {
})
</script>
<style>
<style scoped>
.modal-enter-active,
.modal-leave-active {
transition: all 0.3s ease;

View File

@ -1,36 +1,71 @@
<template>
<mainLayout>
<div class="p-6 grid grid-cols-3 gap-6">
<!-- Left Section -->
<div class="col-span-2 bg-white p-4 rounded-lg shadow-md border border-gray-200 flex flex-col">
<div class="lg:p-2 pt-6">
<div class="grid grid-cols-1 lg:grid-cols-5 gap-3 sm:gap-2 max-w-7xl mx-auto">
<!-- Left Section - Form Kasir -->
<div class="lg:col-span-3">
<div class="bg-white rounded-xl shadow-lg border border-B overflow-hidden h-full">
<div class="p-2 md:p-4 h-full">
<KasirForm />
</div>
</div>
</div>
<!-- Right Section -->
<div class="bg-white p-4 rounded-lg shadow-md border border-gray-200">
<KasirTransaksiList :transaksi="transaksi" @detail="lihatDetail" />
<!-- Right Section - Transaction List -->
<div class="lg:col-span-2">
<div class="bg-white rounded-xl shadow-lg border border-B overflow-hidden lg:h-fit sticky top-4">
<!-- Transaction List Content -->
<div class="p-4 sm:p-6 overflow-y-auto">
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-C"></div>
<span class="ml-3 text-D/70">Memuat transaksi...</span>
</div>
<!-- Empty State -->
<div v-else-if="!transaksi.length" class="text-center py-8">
<svg class="w-16 h-16 mx-auto text-B mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p class="text-[var(--color-D)]/60 text-sm">Belum ada transaksi</p>
</div>
<!-- Transaction List -->
<KasirTransaksiList
v-else
:transaksi="transaksi"
@detail="lihatDetail"
/>
</div>
</div>
</div>
</div>
</div>
</mainLayout>
</template>
</template>
<script setup>
import { ref, onMounted } from "vue"
import axios from "axios"
<script setup>
import { ref, onMounted } from "vue"
import axios from "axios"
import mainLayout from '../layouts/mainLayout.vue'
import KasirForm from '../components/KasirForm.vue'
import KasirTransaksiList from '../components/KasirTransaksiList.vue'
import mainLayout from '../layouts/mainLayout.vue'
import KasirForm from '../components/KasirForm.vue'
import KasirTransaksiList from '../components/KasirTransaksiList.vue'
const transaksi = ref([])
const transaksi = ref([])
const loading = ref(true)
onMounted(async () => {
onMounted(async () => {
try {
const res = await axios.get("/api/transaksi?limit=10") // GANTI URL SESUAI API
loading.value = true
const res = await axios.get("/api/transaksi?limit=10")
transaksi.value = res.data
} catch (err) {
console.error("Gagal fetch transaksi:", err)
} finally {
loading.value = false
}
})