diff --git a/README.md b/README.md index 5811a60..436776e 100644 --- a/README.md +++ b/README.md @@ -141,14 +141,17 @@ docker compose --env-file .env.production up -d --build # Siapkan database docker exec -it abbauf_kasir_app php artisan migrate --seed +# Atau import database secara manual +docker exec -i abbauf_kasir_db mysql -u kasir_user -pkasir_password kasir_db < ./toko_emas.sql + # Periksa database (opsional) docker exec -it abbauf_kasir_db bash -mysql -u root -p +mysql -u kasir_user -pkasir_password kasir_db ``` ### 8. Print Label -- Install driver, ada pada folder `./driver/` untuk windows 64bit. +- Install driver, ada pada folder `./driver/NiimbotPrinterDriverInstall_3.0.0.5.exe` untuk windows 64bit. - Pilih `NIIMBOT B3S_P` pada saat install driver. - Sambungkan printer ke komputer via USB. - Nyalakan printer. @@ -157,6 +160,16 @@ mysql -u root -p - Pilih printer `NIIMBOT B3S_P` dan atur kertas ke ukuran kertas `40mm x 30mm`, margin `Default`, scale `Default` - Klik print +### 9. Print Nota + +- Pastikan printer terhubung dengan komputer via USB. +- Nyalakan printer. +- Install driver, ada pada folder `./driver/L120_x64_213UsHomeExportAsiaML.exe`. +- Lakukan transaksi penjualan pada aplikasi, atau pilih nota yang akan diprint di `Laporan > Riwayat transaksi`. +- Klik tombol print pada halaman tersebut + - Pilih ukuran kertas A4, margin `Minimum`, scale `95` + - Klik print + --- ## ๐ŸŒ Akses Aplikasi diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index dbe880f..2027d8d 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -37,17 +37,9 @@ class DatabaseSeeder extends Seeder 'updated_at' => now(), ]); - // Create sales record - Sales::create([ - 'nama' => 'Kasir', - 'no_hp' => '-', - 'alamat' => '-', - 'created_at' => now(), - 'updated_at' => now(), - ]); - + // Call other seeders $this->call(DataSeeder::class); $this->call(DummySeeder::class); } -} \ No newline at end of file +} diff --git a/resources/js/components/PrintBarcode.vue b/resources/js/components/PrintBarcode.vue index 769cad5..454d5ae 100644 --- a/resources/js/components/PrintBarcode.vue +++ b/resources/js/components/PrintBarcode.vue @@ -37,7 +37,9 @@ const printBarcode = () => { const printWindow = window.open('', '_blank'); const kode = props.code || 'N/A'; const nama = props.item.nama || 'N/A'; - const berat = props.item.berat ? `(${props.item.berat} g)` : ''; + const berat = props.item.berat ? `${props.item.berat} g` : ''; + const kadar = props.item.kadar ? `${props.item.kadar} K` : ''; + const harga = props.item.harga_jual ? `Rp${props.item.harga_jual.toLocaleString('id-ID')},00` : ''; printWindow.document.write(` @@ -70,29 +72,47 @@ const printBarcode = () => { height: 38mm; } - .barcode-container { - width: 12mm; - height: 38mm; - display: flex; - align-items: center; - justify-content: center; - } - - .barcode-img { - transform: rotate(90deg); - transform-origin: center; - max-height: 12mm; - max-width: 12mm; - } - - .details-container { - width: 12mm; + .left-side { + width: 12.5mm; + height: 38mm; + display: flex; + align-items: center; + justify-content: center; + position: relative; + } + + .left-content { + position: absolute; + width: fit-content; + height: 12.5mm; + transform: rotate(90deg); + transform-origin: center; + display: flex; + align-items: center; + gap: 2mm; + } + + .barcode-img { + height: 10mm; + width: auto; + flex-shrink: 0; + } + + .info-box { + display: flex; + flex-direction: column; + gap: 0.5mm; + font-size: 5pt; + line-height: 1.2; + } + + .right-side { + width: 12.5mm; height: 38mm; display: flex; align-items: center; justify-content: center; position: relative; - overflow: hidden; } .item-name { @@ -101,25 +121,30 @@ const printBarcode = () => { white-space: normal; word-wrap: break-word; position: absolute; - width: 32mm; + width: 36mm; + text-align: center; transform: rotate(270deg); - top: 50%; - left: 50%; transform-origin: center; - translate: -50% -50%; }
-
- Barcode + +
+
+ Barcode +
+ ${harga ? `
${harga}
` : ''} + ${berat ? `
Berat: ${berat}
` : ''} + ${kadar ? `
Kadar: ${kadar}
` : ''} +
+
-
-
${nama} ${berat}
+
+
${nama}
diff --git a/resources/js/components/StrukOverlay.vue b/resources/js/components/StrukOverlay.vue index 5b849b0..1353d67 100644 --- a/resources/js/components/StrukOverlay.vue +++ b/resources/js/components/StrukOverlay.vue @@ -18,7 +18,7 @@

08158851178

-

{{ generateTransactionCode() }}

+

TRSXXXXXXXXXXXX

@@ -108,7 +108,7 @@

Hormat Kami

-
@@ -189,18 +189,9 @@ import inputSelect from '@/components/InputSelect.vue' import axios from 'axios' const props = defineProps({ - isOpen: { - type: Boolean, - default: false, - }, - pesanan: { - type: Array, - default: () => [] - }, - total: { - type: Number, - default: 0 - } + isOpen: Boolean, + pesanan: Array, + total: Number }) const emit = defineEmits(['close', 'confirm', 'transaksi-saved']) @@ -218,52 +209,31 @@ const showToast = ref(false) const toastType = ref('error') const toastMessage = ref('') +// ๐Ÿงพ kode transaksi tetap +const transactionCode = ref('') + const toastClasses = computed(() => { - const baseClasses = 'text-white' - const typeClasses = { + const base = 'text-white' + const type = { error: 'bg-red-500', success: 'bg-green-500', info: 'bg-blue-500' } - return `${baseClasses} ${typeClasses[toastType.value]}` + return `${base} ${type[toastType.value]}` }) -const grandTotal = computed(() => { - return props.total + (ongkosBikin.value || 0) -}) +const grandTotal = computed(() => props.total + (ongkosBikin.value || 0)) -const getRowStyle = () => { - if (props.pesanan.length === 1) { - return { height: '126px' } - } - return { height: '63px' } -} - -const getImageClass = () => { - if (props.pesanan.length === 1) { - return 'w-25 h-25' - } - return 'w-12 h-12' -} - -const getTextClass = () => { - if (props.pesanan.length === 1) { - return 'text-lg font-medium' - } - return 'text-sm' -} +const getRowStyle = () => props.pesanan.length === 1 ? { height: '126px' } : { height: '63px' } +const getImageClass = () => props.pesanan.length === 1 ? 'w-25 h-25' : 'w-12 h-12' +const getTextClass = () => props.pesanan.length === 1 ? 'text-lg font-medium' : 'text-sm' const getCurrentDate = () => { - const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'] - const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'] - + const days = ['Minggu','Senin','Selasa','Rabu','Kamis','Jumat','Sabtu'] const now = new Date() - const dayName = days[now.getDay()] const day = String(now.getDate()).padStart(2, '0') - const month = months[now.getMonth()] - const year = now.getFullYear() - - return `${dayName}, ${day}-${month}-${year}` + const month = String(now.getMonth() + 1).padStart(2, '0') + return `${days[now.getDay()]}, ${day}-${month}-${now.getFullYear()}` } const generateTransactionCode = () => { @@ -276,55 +246,33 @@ const showSimpleToast = (type, message, duration = 3000) => { toastType.value = type toastMessage.value = message showToast.value = true - - setTimeout(() => { - showToast.value = false - }, duration) + setTimeout(() => (showToast.value = false), duration) } const fetchSales = async () => { try { - const response = await axios.get('/api/sales', { - headers: { - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, + const res = await axios.get('/api/sales', { + headers: { Authorization: `Bearer ${localStorage.getItem("token")}` } }) - - salesOptions.value = response.data.map(sales => ({ - value: sales.id, - label: sales.nama - })) - - if (salesOptions.value.length > 0) { - selectedSales.value = salesOptions.value[0].value - } - } catch (error) { - console.error('Error fetching sales:', error) + salesOptions.value = res.data.map(s => ({ value: s.id, label: s.nama })) + if (salesOptions.value.length > 0) selectedSales.value = salesOptions.value[0].value + } catch (e) { + console.error('Error fetching sales:', e) } } +// ๐ŸŸข Generate kode hanya saat menyimpan transaksi const handleSimpan = () => { - if (!namaPembeli.value.trim()) { - showSimpleToast('error', 'Nama pembeli harus diisi!') - return - } + if (!namaPembeli.value.trim()) return showSimpleToast('error', 'Nama pembeli harus diisi!') + if (!nomorTelepon.value.trim()) return showSimpleToast('error', 'Nomor telepon harus diisi!') + if (!alamat.value.trim()) return showSimpleToast('error', 'Alamat harus diisi!') + if (!selectedSales.value) return showSimpleToast('error', 'Sales harus dipilih!') - if (!nomorTelepon.value.trim()) { - showSimpleToast('error', 'Nomor telepon harus diisi!') - return - } - - if (!alamat.value.trim()) { - showSimpleToast('error', 'Alamat harus diisi!') - return - } - - if (!selectedSales.value) { - showSimpleToast('error', 'Sales harus dipilih!') - return - } + // Kode transaksi dibuat hanya saat simpan + transactionCode.value = generateTransactionCode() simpanTransaksi({ + kode_transaksi: transactionCode.value, id_sales: selectedSales.value, nama_pembeli: namaPembeli.value, no_hp: nomorTelepon.value, @@ -337,48 +285,37 @@ const handleSimpan = () => { const simpanTransaksi = async (dataTransaksi) => { if (isSaving.value) return - isSaving.value = true try { - const response = await axios.post('/api/transaksi', dataTransaksi, { - headers: { - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - }); - + const res = await axios.post('/api/transaksi', dataTransaksi, { + headers: { Authorization: `Bearer ${localStorage.getItem("token")}` }, + }) showSimpleToast('success', 'Transaksi berhasil disimpan!', 2000) - - // Emit event dengan data transaksi yang sudah disimpan setTimeout(() => { - emit('transaksi-saved', response.data); - emit('close'); - }, 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); + emit('transaksi-saved', res.data) + emit('close') + }, 2200) + } catch (err) { + const msg = err.response?.data?.message || err.message || 'Terjadi kesalahan saat menyimpan transaksi' + showSimpleToast('error', `Error: ${msg}`, 4000) } finally { isSaving.value = false } -}; +} onMounted(() => { - if (props.isOpen) { - fetchSales() - } + if (props.isOpen) fetchSales() }) function formatInput(e) { - let value = e.target.value.replace(/\D/g, ""); - ongkosBikin.value = value ? parseInt(value, 10) : null; - ongkosBikinFormatted.value = value - ? new Intl.NumberFormat("id-ID").format(value) - : ""; + let val = e.target.value.replace(/\D/g, "") + ongkosBikin.value = val ? parseInt(val, 10) : null + ongkosBikinFormatted.value = val ? new Intl.NumberFormat("id-ID").format(val) : "" } +