From e1205cf14650a0b9e3238a2b4a5dca1b20f313ed Mon Sep 17 00:00:00 2001 From: Baghaztra Date: Fri, 12 Sep 2025 10:11:47 +0700 Subject: [PATCH 1/4] [feat] Docker (belum jalan v:) --- .dockerignore | 7 +++++++ Dockerfile | 29 +++++++++++++++++++++++++++++ docker-compose.yml | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bb72ff1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +vendor +.env +Dockerfile +docker-compose.yml +.git +.gitignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d65ea2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Stage 1: Build Vue +FROM node:20 as node_builder +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Stage 2: Laravel +FROM php:8.3-fpm + +RUN apt-get update && apt-get install -y \ + git unzip libzip-dev libpng-dev libonig-dev libxml2-dev curl \ + && docker-php-ext-install pdo_mysql zip gd mbstring exif pcntl bcmath + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www/html + +# Copy source code KECUALI public (biar ga ketiban build Vue) +COPY . . +# Copy hasil build Vue dari stage 1 +COPY --from=node_builder /app/dist /var/www/html/public + +RUN composer install --no-dev --optimize-autoloader +RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache + +EXPOSE 9000 +CMD ["php-fpm"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0acbbfb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + laravel: + build: + context: . + dockerfile: Dockerfile + container_name: laravel_app + volumes: + - .:/var/www/html + command: php artisan serve --host=0.0.0.0 --port=8000 + ports: + - "8000:8000" + depends_on: + - mysql + environment: + APP_ENV: local + APP_KEY: ${APP_KEY} + DB_CONNECTION: mysql + DB_HOST: mysql + DB_PORT: 3306 + DB_DATABASE: ${DB_DATABASE} + DB_USERNAME: ${DB_USERNAME} + DB_PASSWORD: ${DB_PASSWORD} + + mysql: + image: mysql:8 + container_name: mysql_db + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: laravel + MYSQL_USER: laravel + MYSQL_PASSWORD: laravel + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: From eaa331850671f7a592ced15b56b04bcf5acbeeff Mon Sep 17 00:00:00 2001 From: Baghaztra Date: Fri, 12 Sep 2025 11:10:13 +0700 Subject: [PATCH 2/4] [Fix] transaksi --- app/Http/Controllers/TransaksiController.php | 141 +++---- app/Models/Transaksi.php | 28 +- resources/js/components/KasirForm.vue | 354 +++++++----------- .../js/components/KasirTransaksiList.vue | 2 +- resources/js/components/StrukOverlay.vue | 37 +- resources/js/pages/Kasir.vue | 3 + 6 files changed, 253 insertions(+), 312 deletions(-) diff --git a/app/Http/Controllers/TransaksiController.php b/app/Http/Controllers/TransaksiController.php index 2628dbb..bdae3f3 100644 --- a/app/Http/Controllers/TransaksiController.php +++ b/app/Http/Controllers/TransaksiController.php @@ -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); diff --git a/app/Models/Transaksi.php b/app/Models/Transaksi.php index ec1c6bf..9130f08 100644 --- a/app/Models/Transaksi.php +++ b/app/Models/Transaksi.php @@ -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() { diff --git a/resources/js/components/KasirForm.vue b/resources/js/components/KasirForm.vue index ddb81ac..f29cc6a 100644 --- a/resources/js/components/KasirForm.vue +++ b/resources/js/components/KasirForm.vue @@ -1,22 +1,9 @@ @@ -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; }); diff --git a/resources/js/components/KasirTransaksiList.vue b/resources/js/components/KasirTransaksiList.vue index f5f0770..be70dd2 100644 --- a/resources/js/components/KasirTransaksiList.vue +++ b/resources/js/components/KasirTransaksiList.vue @@ -14,7 +14,7 @@ {{ trx.tanggal }} {{ trx.kode }} - Rp{{ (trx.pendapatan || 0).toLocaleString() }} + Rp{{ (trx.total_harga || 0).toLocaleString() }}