Merge branch 'production' of https://git.abbauf.com/Magang-2025/Kasir into production

This commit is contained in:
timotiabbauftech 2025-09-02 14:14:23 +07:00
commit fcd7719826
10 changed files with 261 additions and 149 deletions

View File

@ -42,7 +42,7 @@ class ItemController extends Controller
*/ */
public function show(int $id) 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); return response()->json($item);
} }

View File

@ -35,6 +35,6 @@ class Item extends Model
public function itemTransaksi() 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_kasir',
'id_sales', 'id_sales',
'nama_sales', 'nama_sales',
'nama_pembeli',
'no_hp', 'no_hp',
'alamat', 'alamat',
'ongkos_bikin', 'ongkos_bikin',

View File

@ -26,6 +26,7 @@ class TransaksiFactory extends Factory
'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 ?? $this->faker->name(),
'nama_pembeli' => $sales?->nama ?? $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' => $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_kasir')->constrained('users');
$table->foreignId('id_sales')->nullable()->constrained('sales'); $table->foreignId('id_sales')->nullable()->constrained('sales');
$table->string('nama_sales', 100); $table->string('nama_sales', 100);
$table->string('nama_pembeli', 100);
$table->string('no_hp', 20); $table->string('no_hp', 20);
$table->string('alamat', 100); $table->string('alamat', 100);
$table->double('ongkos_bikin')->nullable(); $table->double('ongkos_bikin')->nullable();

View File

@ -1,17 +1,17 @@
<template> <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 --> <!-- Left: Logo -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<img :src="logo" alt="Logo" class="h-10"> <img :src="logo" alt="Logo" class="h-10">
</div> </div>
<!-- Center: Copyright --> <!-- 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 Abbauf Tech © 2025 Semua hak dilindungi
</div> </div>
<!-- Right: Social Icons --> <!-- 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-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-twitter"></i></a>
<a href="#" class="hover:text-sky-600"><i class="fab fa-instagram"></i></a> <a href="#" class="hover:text-sky-600"><i class="fab fa-instagram"></i></a>
@ -24,8 +24,3 @@
<script setup> <script setup>
import logo from '@/../images/logo.png' import logo from '@/../images/logo.png'
</script> </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> <template>
<div> <div>
<!-- Input Grid --> <div class="grid grid-cols-2 h-full gap-4 mb-4">
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700">Kode Item *</label> <label class="block text-sm font-medium text-D">Kode Item *</label>
<InputField <div class="flex flex-row justify-between mt-1 w-full rounded-md bg-A shadow-sm sm:text-sm border-B">
v-model="kodeItem" <input type="text" v-model="kodeItem" @keyup.enter="inputItem" placeholder="Scan atau masukkan kode item"
type="text" class=" bg-A focus:border-C focus:ring focus:ring-D focus:ring-opacity-50 p-2 w-full" />
placeholder="Masukkan kode item" <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>
<div> <div>
<label class="block text-sm font-medium text-gray-700">Harga Jual</label> <label class="block text-sm font-medium text-D">Harga Jual</label>
<InputField <InputField v-model="hargaJual" type="number" placeholder="Masukkan Harga Jual" />
v-model="hargaJual" </div>
type="number"
placeholder="Masukkan Harga Jual" <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> </div>
<div class="flex items-center justify-center"> <div class="flex pt-10 justify-center">
<div class="text-center"> <div class="text-start">
<span class="block text-gray-600 font-medium">Total:</span> <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() }},- Rp{{ total.toLocaleString() }},-
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<!-- Buttons --> <div class="mb-4">
<div class="flex gap-4 mb-6"> <p v-if="error" :class="{ 'animate-shake': error }" class="text-sm text-red-600 mt-1">{{ error }}</p>
<button @click="tambahItem" <p v-if="info" class="text-sm text-C mt-1">{{ info }}</p>
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> </div>
<!-- Table --> <table class="w-full border border-B text-sm rounded-lg overflow-hidden">
<table class="w-full border-collapse border border-gray-200 text-sm rounded-lg overflow-hidden"> <thead class="bg-A text-D">
<thead class="bg-gray-100 text-[#0f1d4a]">
<tr> <tr>
<th class="border border-gray-200 p-2">No</th> <th class="border border-B p-2">No</th>
<th class="border border-gray-200 p-2">Item</th> <th class="border border-B p-2">Nam Produk </th>
<th class="border border-gray-200 p-2">Jml</th> <th class="border border-B p-2">Posisi</th>
<th class="border border-gray-200 p-2">Harga</th> <th class="border border-B p-2">Harga</th>
<th class="border border-gray-200 p-2">Total</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(item, index) in pesanan" :key="index" class="hover:bg-gray-50 text-center"> <tr v-if="pesanan.length == 0" class="text-center text-D/70">
<td class="border border-gray-200 p-2">{{ index + 1 }}</td> <td colspan="5" class="h-20 border border-B">Belum ada item dipesan</td>
<td class="border border-gray-200 p-2">{{ item.kode }}</td> </tr>
<td class="border border-gray-200 p-2">{{ item.jumlah }}</td> <tr v-else v-for="(item, index) in pesanan" :key="index" class="hover:bg-gray-50 text-center">
<td class="border border-gray-200 p-2">Rp{{ item.harga.toLocaleString() }}</td> <td class="border border-B p-2">{{ index + 1 }}</td>
<td class="border border-gray-200 p-2">Rp{{ (item.harga * item.jumlah).toLocaleString() }}</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> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import InputField from './InputField.vue' import InputField from './InputField.vue'
import axios from 'axios'
const kodeItem = ref('') const kodeItem = ref('')
const info = ref('')
const error = ref('')
const hargaJual = ref(null) const hargaJual = ref(null)
const item = ref(null)
const loadingItem = ref(false)
const pesanan = ref([]) const pesanan = ref([])
const tambahItem = () => { let errorTimeout = null
if (!kodeItem.value || !hargaJual.value) return let infoTimeout = null
pesanan.value.push({
kode: kodeItem.value, const inputItem = async () => {
jumlah: 1, if (!kodeItem.value) return
harga: parseFloat(hargaJual.value),
}) info.value = ''
kodeItem.value = '' error.value = ''
hargaJual.value = 0 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 total = computed(() => const tambahItem = () => {
pesanan.value.reduce((sum, item) => sum + item.harga * item.jumlah, 0) if (!item.value || !hargaJual.value) {
) error.value = 'Scan atau masukkan kode item untuk dijual.'
</script> 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 = 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,22 +1,22 @@
<template> <template>
<h3 class="text-lg font-semibold mb-4 text-gray-800">Transaksi</h3> <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"> <table class="w-full border border-B rounded-lg text-sm">
<thead class="bg-gray-100"> <thead class="bg-A text-D">
<tr> <tr>
<th class="border border-gray-200 p-2">Tanggal</th> <th class="border border-B p-2">Tanggal</th>
<th class="border border-gray-200 p-2">Kode Transaksi</th> <th class="border border-B p-2">Kode Transaksi</th>
<th class="border border-gray-200 p-2">Pendapatan</th> <th class="border border-B p-2">Pendapatan</th>
<th class="border border-gray-200 p-2">Detail Item</th> <th class="border border-B p-2">Detail Item</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="trx in props.transaksi" :key="trx.id"> <tr v-for="trx in props.transaksi" :key="trx.id" class="hover:bg-A">
<td class="border border-gray-200 p-2">{{ trx.tanggal }}</td> <td class="border border-B p-2">{{ trx.tanggal }}</td>
<td class="border border-gray-200 p-2">{{ trx.kode }}</td> <td class="border border-B p-2">{{ trx.kode }}</td>
<td class="border border-gray-200 p-2">Rp{{ (trx.pendapatan || 0).toLocaleString() }}</td> <td class="border border-B p-2">Rp{{ (trx.pendapatan || 0).toLocaleString() }}</td>
<td class="border border-gray-200 p-2 text-center"> <td class="border border-B p-2 text-center">
<button @click="$emit('detail', trx)" <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> </td>
</tr> </tr>
</tbody> </tbody>
@ -24,8 +24,6 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"
const props = defineProps({ const props = defineProps({
transaksi: { transaksi: {
type: Array, type: Array,
@ -34,9 +32,4 @@
}) })
defineEmits(['detail']) defineEmits(['detail'])
onMounted(() => {
console.log(props.transaksi);
})
</script> </script>

View File

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

View File

@ -1,14 +1,45 @@
<template> <template>
<mainLayout> <mainLayout>
<div class="p-6 grid grid-cols-3 gap-6"> <div class="lg:p-2 pt-6">
<!-- Left Section --> <div class="grid grid-cols-1 lg:grid-cols-5 gap-3 sm:gap-2 max-w-7xl mx-auto">
<div class="col-span-2 bg-white p-4 rounded-lg shadow-md border border-gray-200 flex flex-col"> <!-- 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 /> <KasirForm />
</div> </div>
</div>
</div>
<!-- Right Section --> <!-- Right Section - Transaction List -->
<div class="bg-white p-4 rounded-lg shadow-md border border-gray-200"> <div class="lg:col-span-2">
<KasirTransaksiList :transaksi="transaksi" @detail="lihatDetail" /> <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>
</div> </div>
</mainLayout> </mainLayout>
@ -23,14 +54,18 @@
import KasirTransaksiList from '../components/KasirTransaksiList.vue' import KasirTransaksiList from '../components/KasirTransaksiList.vue'
const transaksi = ref([]) const transaksi = ref([])
const loading = ref(true)
onMounted(async () => { onMounted(async () => {
try { 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 transaksi.value = res.data
} catch (err) { } catch (err) {
console.error("Gagal fetch transaksi:", err) console.error("Gagal fetch transaksi:", err)
} finally {
loading.value = false
} }
}) })