Merge branch 'development' into production

This commit is contained in:
Baghaztra 2025-10-17 11:16:29 +07:00
commit 06ec582ffb
6 changed files with 107 additions and 98 deletions

View File

@ -75,6 +75,7 @@ class TransaksiController extends Controller
'kasir',
'sales',
'itemTransaksi.produk',
'itemTransaksi.produk.foto',
'itemTransaksi' => function ($query) {
$query->orderBy('created_at', 'asc');
}

Binary file not shown.

View File

@ -2,9 +2,23 @@
<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" />
<!-- ==== END TAMBAHAN ==== -->
<!-- Struk Input Overlay -->
<StrukOverlay
v-if="showStruk"
:isOpen="showStruk"
:pesanan="pesanan"
:total="total"
@close="closeStruk"
@transaksi-saved="handleTransaksiSaved"
/>
<!-- Struk View (Print) Overlay -->
<StrukView
v-if="showStrukView"
:isOpen="showStrukView"
:transaksi="savedTransaksi"
@close="closeStrukView"
/>
<div class="p-2 sm:p-4">
<!-- Grid Form & Total -->
@ -115,13 +129,15 @@
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import InputField from "./InputField.vue";
import axios from "axios";
import ConfirmDeleteModal from "./ConfirmDeleteModal.vue";
import StrukOverlay from "./StrukOverlay.vue";
import StrukView from "./StrukView.vue";
// Emit untuk komunikasi dengan parent
const emit = defineEmits(['transaksi-saved']);
const kodeItem = ref("");
const info = ref("");
@ -131,21 +147,21 @@ const hargaJualFormatted = ref("");
const item = ref(null);
const loadingItem = ref(false);
const pesanan = ref([]);
const showDeleteModal = ref(false)
const deleteIndex = ref(null)
const showDeleteModal = ref(false);
const deleteIndex = ref(null);
const showStruk = ref(false);
const showStrukView = ref(false);
const savedTransaksi = ref(null);
let errorTimeout = null;
let infoTimeout = null;
// Format angka dengan pemisah ribuan
const formatNumber = (num) => {
if (!num) return "";
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
};
// Menghapus format dan mengambil angka asli
const unformatNumber = (str) => {
if (!str) return null;
const cleaned = str.replace(/\./g, "");
@ -153,14 +169,11 @@ const unformatNumber = (str) => {
return isNaN(number) ? null : number;
};
// Handler untuk format input harga
const formatHargaInput = (event) => {
const value = event.target.value;
// Hapus semua karakter selain angka
const cleanValue = value.replace(/\D/g, "");
if (cleanValue) {
// Format dengan pemisah ribuan
const formatted = formatNumber(cleanValue);
hargaJualFormatted.value = formatted;
hargaJual.value = parseInt(cleanValue);
@ -170,7 +183,6 @@ const formatHargaInput = (event) => {
}
};
// Hanya izinkan angka saat mengetik
const onlyNumbers = (event) => {
const char = String.fromCharCode(event.which);
if (!/[0-9]/.test(char)) {
@ -196,11 +208,8 @@ const inputItem = async () => {
});
item.value = response.data;
hargaJual.value = item.value.produk.harga_jual;
// Format harga untuk tampilan
hargaJualFormatted.value = formatNumber(item.value.produk.harga_jual);
// console.log(item.value);
if (item.value.is_sold) {
throw new Error("Item sudah terjual");
}
@ -231,8 +240,7 @@ 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.";
error.value = "Masukkan harga jual, atau input dari kode item lagi.";
}
clearTimeout(errorTimeout);
errorTimeout = setTimeout(() => {
@ -241,14 +249,12 @@ const tambahItem = () => {
return;
}
// harga deal
item.value.kode_item = Number(kodeItem.value);
item.value.harga_deal = Number(hargaJual.value);
item.value.posisi = item.value.nampan ? item.value.nampan.nama : "Brankas";
pesanan.value.push(item.value);
// Reset input fields
kodeItem.value = "";
hargaJual.value = null;
hargaJualFormatted.value = "";
@ -258,23 +264,22 @@ const tambahItem = () => {
};
const openDeleteModal = (index) => {
deleteIndex.value = index
showDeleteModal.value = true
}
deleteIndex.value = index;
showDeleteModal.value = true;
};
const closeDeleteModal = () => {
showDeleteModal.value = false
deleteIndex.value = null
}
showDeleteModal.value = false;
deleteIndex.value = null;
};
const hapusPesanan = () => {
if (deleteIndex.value !== null) {
pesanan.value.splice(deleteIndex.value, 1)
pesanan.value.splice(deleteIndex.value, 1);
}
closeDeleteModal()
}
closeDeleteModal();
};
// ==== MODIFIKASI: konfirmasiPenjualan sekarang menampilkan struk ====
const konfirmasiPenjualan = () => {
if (pesanan.value.length === 0) {
error.value = "Belum ada item yang dipesan.";
@ -284,17 +289,37 @@ const konfirmasiPenjualan = () => {
}, 5000);
return;
}
console.log(pesanan.value)
// Tampilkan struk overlay
showStruk.value = true;
};
// ==== END MODIFIKASI ====
// ==== TAMBAHAN: Fungsi untuk menutup struk ====
const closeStruk = () => {
showStruk.value = false;
};
// ==== END TAMBAHAN ====
const closeStrukView = () => {
showStrukView.value = false;
savedTransaksi.value = null;
// Reset pesanan setelah menutup struk view
pesanan.value = [];
};
// Handler ketika transaksi berhasil disimpan
const handleTransaksiSaved = (transaksiData) => {
// Tutup StrukOverlay
showStruk.value = false;
// Simpan data transaksi
savedTransaksi.value = transaksiData;
// Emit ke parent (Kasir.vue)
emit('transaksi-saved', transaksiData);
// Buka StrukView untuk print
setTimeout(() => {
showStrukView.value = true;
}, 300);
};
const total = computed(() => {
let sum = 0;

View File

@ -64,7 +64,6 @@
</thead>
<tbody>
<!-- Item rows dengan dynamic height -->
<tr v-for="(item, index) in props.pesanan" :key="index"
class="text-center"
:style="getRowStyle()">
@ -95,9 +94,7 @@
</tr>
</tbody>
</table>
<!-- Bagian bawah -->
<div class="flex text-sm mt-2">
<!-- PERHATIAN -->
<div class="w-[40%] p-2 text-left">
<p class="font-semibold">PERHATIAN</p>
<ol class="list-decimal ml-4 text-xs space-y-1">
@ -109,17 +106,14 @@
</ol>
</div>
<!-- SALES -->
<div class="w-[20%] p-2 flex flex-col items-center justify-center">
<p><strong>Hormat Kami</strong></p>
<inputSelect v-model="selectedSales" :options="salesOptions"
class="mt-16 text-sm rounded bg-B cursor-pointer !w-[160px] text-center [option]:text-left" />
</div>
<!-- ONGKOS & TOTAL -->
<div class="ml-auto w-[25%] p-2 flex flex-col justify-between">
<div class="space-y-4">
<!-- Ongkos bikin -->
<div class="flex items-start justify-between ">
<div class="flex flex-col ">
<p class="font-semibold">Ongkos bikin</p>
@ -132,7 +126,6 @@
</div>
</div>
<!-- Total -->
<div class="flex items-center justify-between -mt-4">
<p class="font-semibold">Total Harga</p>
<div class="flex items-center w-40">
@ -144,13 +137,12 @@
</div>
</div>
<!-- Tombol -->
<div class="flex justify-end gap-2 mt-4">
<button @click="$emit('close')" class="bg-gray-400 text-white px-6 py-2 rounded">
Batal
</button>
<button @click="handleSimpan" class="bg-C text-white px-6 py-2 rounded">
Simpan
<button @click="handleSimpan" :disabled="isSaving" class="bg-C text-white px-6 py-2 rounded disabled:opacity-50">
{{ isSaving ? 'Menyimpan...' : 'Simpan' }}
</button>
</div>
</div>
@ -162,13 +154,11 @@
</div>
</div>
<!-- Simple Toast Alert -->
<div v-if="showToast"
class="fixed top-4 left-1/2 transform -translate-x-1/2 z-[10001]
transition-all duration-300 ease-in-out"
:class="toastClasses">
<div class="flex items-center gap-2 px-4 py-3 rounded-lg shadow-lg max-w-sm">
<!-- Icon -->
<div class="flex-shrink-0">
<svg v-if="toastType === 'error'" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
@ -180,8 +170,6 @@
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<!-- Message -->
<p class="text-sm font-medium">{{ toastMessage }}</p>
</div>
</div>
@ -198,7 +186,6 @@ import logo_visa from '@/../images/logo_visa.png'
import logo_mandiri from '@/../images/logo_mandiri.png'
import inputField from '@/components/InputField.vue'
import inputSelect from '@/components/InputSelect.vue'
import axios from 'axios'
const props = defineProps({
@ -216,7 +203,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['close', 'confirm'])
const emit = defineEmits(['close', 'confirm', 'transaksi-saved'])
const namaPembeli = ref('')
const nomorTelepon = ref('')
@ -225,10 +212,10 @@ const ongkosBikin = ref(0)
const selectedSales = ref(null)
const salesOptions = ref([])
const ongkosBikinFormatted = ref("")
const isSaving = ref(false)
// Simple Toast State
const showToast = ref(false)
const toastType = ref('error') // 'error', 'success', 'info'
const toastType = ref('error')
const toastMessage = ref('')
const toastClasses = computed(() => {
@ -245,30 +232,25 @@ const grandTotal = computed(() => {
return props.total + (ongkosBikin.value || 0)
})
// Fungsi untuk menentukan style row berdasarkan jumlah item
const getRowStyle = () => {
if (props.pesanan.length === 1) {
return { height: '126px' } // 2x lipat dari tinggi normal (48px)
return { height: '126px' }
}
return { height: '63px' } // Tinggi normal
return { height: '63px' }
}
// Fungsi untuk menentukan class gambar berdasarkan jumlah item
const getImageClass = () => {
if (props.pesanan.length === 1) {
return 'w-25 h-25' // 2x lipat dari ukuran normal (w-10 h-10)
return 'w-25 h-25'
}
return 'w-12 h-12' // Ukuran normal
return 'w-12 h-12'
}
// Fungsi untuk menentukan class text berdasarkan jumlah item
const getTextClass = () => {
if (props.pesanan.length === 1) {
return 'text-lg font-medium' // Text lebih besar untuk single item
return 'text-lg font-medium'
}
return 'text-sm' // Text normal
return 'text-sm'
}
const getCurrentDate = () => {
@ -290,7 +272,6 @@ const generateTransactionCode = () => {
return `TRS-${timestamp}`
}
// Simple Toast Function
const showSimpleToast = (type, message, duration = 3000) => {
toastType.value = type
toastMessage.value = message
@ -348,14 +329,16 @@ const handleSimpan = () => {
nama_pembeli: namaPembeli.value,
no_hp: nomorTelepon.value,
alamat: alamat.value,
ongkos_bikin: ongkosBikin.value || 0, // Pastikan nama field benar
ongkos_bikin: ongkosBikin.value || 0,
total_harga: grandTotal.value,
items: props.pesanan
})
}
const simpanTransaksi = async (dataTransaksi) => {
// console.log('Data transaksi yang akan disimpan:', dataTransaksi);
if (isSaving.value) return
isSaving.value = true
try {
const response = await axios.post('/api/transaksi', dataTransaksi, {
@ -366,16 +349,18 @@ const simpanTransaksi = async (dataTransaksi) => {
showSimpleToast('success', 'Transaksi berhasil disimpan!', 2000)
// Delay untuk memberikan waktu user membaca notifikasi
// Emit event dengan data transaksi yang sudah disimpan
setTimeout(() => {
emit('transaksi-saved', response.data);
emit('close');
window.location.reload();
}, 2200);
} catch (error) {
console.error('Error saving transaksi:', error);
const errorMessage = error.response?.data?.message || error.message || 'Terjadi kesalahan saat menyimpan transaksi';
showSimpleToast('error', `Error: ${errorMessage}`, 4000);
} finally {
isSaving.value = false
}
};

View File

@ -73,19 +73,16 @@
<span v-if="item.harga_deal && item.harga_deal > 0">1</span>
</td>
<td class="flex items-center gap-2 p-2 border-r border-D" :style="getRowStyle()">
<template v-if="item.produk?.foto?.[0]?.url">
<img :src="item.produk.foto[0].url" :class="getImageClass()" class="object-cover rounded" />
</template>
<template v-else-if="item.produk?.nama">
<div :class="getImageClass() + ' bg-gray-200 rounded flex items-center justify-center'">
<span class="text-xs text-gray-500">IMG</span>
</div>
</template>
<template v-else>
<div :class="getImageClass()"></div>
</template>
<span :class="getTextClass()">{{ item.produk?.nama || '' }}</span>
</td>
<img
:src="item.produk?.foto?.[0]?.url || 'https://via.placeholder.com/50x50?text=No+Img'"
:class="getImageClass()"
class="object-cover rounded"
/>
<span :class="getTextClass()">{{ item.produk?.nama || '' }}</span>
</td>
<td class="border-r border-D">
<span v-if="item.produk?.nama">{{ item.posisi_asal || 'Brankas' }}</span>
</td>
@ -206,7 +203,7 @@ const formatDate = (dateString) => {
const day = String(date.getDate()).padStart(2, '0')
const month = months[date.getMonth()]
const year = date.getFullYear()
return `${dayName}/${day}-${month}-${year}`
return `${dayName}, ${day}-${month}-${year}`
}
const itemsWithMinimal = computed(() => {

View File

@ -4,7 +4,6 @@
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-3 sm:gap-2 mx-auto min-h-[75vh]"
>
<div class="lg:col-span-3">
<div
class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden h-auto lg:h-full"
@ -15,13 +14,11 @@
</div>
</div>
<div class="lg:col-span-2">
<div
class="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden lg:h-fit sticky top-4 max-h-[70vh] overflow-y-auto"
>
<div class="p-3 sm:p-4 md:p-6">
<KasirTransaksiList
:transaksi="transaksi.data || []"
:loading="loading"
@ -34,7 +31,6 @@
</div>
</div>
<ModalConfirm
v-if="showConfirm"
title="Konfirmasi"
@ -66,13 +62,11 @@ const showConfirm = ref(false);
const confirmMessage = ref("Apakah kamu yakin?");
let lastTransaksi = null;
const handleConfirm = () => {
showConfirm.value = false;
console.log("User konfirmasi, cetak struk di sini...", lastTransaksi);
};
const fetchTransaksiHariIni = async (page = 1) => {
try {
loading.value = true;
@ -105,47 +99,54 @@ const fetchTransaksiHariIni = async (page = 1) => {
}
};
const handlePageChange = (page) => {
if (page >= 1 && page <= (transaksi.value.pagination?.last_page || 1)) {
fetchTransaksiHariIni(page);
}
};
const handleTransaksiSaved = async (newTransaksi) => {
// Format data transaksi baru
const formattedNewTransaksi = {
id: newTransaksi.id,
kode_transaksi: newTransaksi.kode_transaksi,
created_at: newTransaksi.created_at,
total_harga: newTransaksi.total_harga || 0,
itemTransaksi: newTransaksi.itemTransaksi || [],
itemTransaksi: newTransaksi.itemTransaksi || newTransaksi.items || [],
pendapatan: newTransaksi.total_harga || 0,
total_items: newTransaksi.itemTransaksi?.length || 0,
tanggal: new Date(newTransaksi.created_at).toLocaleDateString('id-ID')
total_items: (newTransaksi.itemTransaksi || newTransaksi.items || []).length,
tanggal: new Date(newTransaksi.created_at).toLocaleDateString('id-ID'),
nama_pembeli: newTransaksi.nama_pembeli,
alamat: newTransaksi.alamat,
no_hp: newTransaksi.no_hp,
ongkos_bikin: newTransaksi.ongkos_bikin,
nama_sales: newTransaksi.nama_sales
};
// Tambahkan ke awal list
transaksi.value.data.unshift(formattedNewTransaksi);
lastTransaksi = formattedNewTransaksi;
// Update pagination
if (transaksi.value.pagination) {
transaksi.value.pagination.total += 1;
// Hapus item terakhir jika melebihi limit
if (transaksi.value.data.length > limit) {
transaksi.value.data.pop();
}
}
confirmMessage.value = "Transaksi berhasil disimpan. Cetak struk sekarang?";
showConfirm.value = true;
// Tidak perlu modal konfirmasi lagi karena sudah ada StrukView
// confirmMessage.value = "Transaksi berhasil disimpan. Cetak struk sekarang?";
// showConfirm.value = true;
};
let refreshInterval = null;
const startAutoRefresh = () => {
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = setInterval(() => {
fetchTransaksiHariIni(currentPage.value);
}, 10000);
}, 30000); // Refresh setiap 30 detik (sebelumnya 10 detik)
};
const stopAutoRefresh = () => {