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/app/Exports/RingkasanExport.php b/app/Exports/RingkasanExport.php index d8ed659..64da7b5 100644 --- a/app/Exports/RingkasanExport.php +++ b/app/Exports/RingkasanExport.php @@ -2,80 +2,73 @@ namespace App\Exports; +use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\FromCollection; use Maatwebsite\Excel\Concerns\WithHeadings; -use Maatwebsite\Excel\Concerns\WithTitle; use Maatwebsite\Excel\Concerns\WithStyles; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class RingkasanExport implements FromCollection, WithHeadings, WithTitle, WithStyles +class RingkasanExport implements FromCollection, WithHeadings, WithStyles { private $data; - private $page; - public function __construct(iterable $data, $page = 1) + public function __construct(iterable $data) { $this->data = $data; - $this->page = $page; } - - public function collection() + public function collection(): Collection { - $collection = collect(); - $items = method_exists($this->data, 'items') ? $this->data->items() : $this->data; + $rows = collect(); - foreach ($items as $item) { - $collection->push([ - 'Tanggal' => $item['tanggal'] ?? '-', - 'Total Item Terjual' => $item['total_item_terjual'] ?? 0, - 'Total Berat' => $item['total_berat'] ?? 0, - 'Total Pendapatan' => $item['total_pendapatan'] ?? 0, - 'Detail Sales' => $this->formatSalesData($item['sales'] ?? []), + foreach ($this->data as $item) { + $tanggal = $item['tanggal'] ?? '-'; + $totalItem = $item['total_item'] ?? 0; + $totalBerat = $item['total_berat'] ?? '0 g'; + $totalPendapatan = $item['total_pendapatan'] ?? 'Rp 0'; + + // Tambahkan detail sales per baris + foreach ($item['sales'] ?? [] as $sale) { + $rows->push([ + 'Tanggal' => $tanggal, + 'Nama Sales' => $sale['nama'] ?? 'Sales Tidak Dikenal', + 'Item Terjual' => $sale['item_terjual'] ?? 0, + 'Berat' => $sale['berat'] ?? '-', + 'Pendapatan' => $sale['pendapatan'] ?? '-', + ]); + } + + // Tambahkan baris total + $rows->push([ + 'Tanggal' => $tanggal, + 'Nama Sales' => 'TOTAL', + 'Item Terjual' => $totalItem, + 'Berat' => $totalBerat, + 'Pendapatan' => $totalPendapatan, ]); + + // Tambahkan baris kosong biar rapi + $rows->push(['Tanggal' => '', 'Nama Sales' => '', 'Item Terjual' => '', 'Berat' => '', 'Pendapatan' => '']); } - return $collection; + return $rows; } public function headings(): array { return [ 'Tanggal', - 'Total Item Terjual', - 'Total Berat', - 'Total Pendapatan', - 'Detail Sales' + 'Nama Sales', + 'Item Terjual', + 'Berat', + 'Pendapatan', ]; } - public function title(): string - { - return "Ringkasan Halaman {$this->page}"; - } - public function styles(Worksheet $sheet) { return [ - 1 => ['font' => ['bold' => true]], + 1 => ['font' => ['bold' => true]], // Header bold ]; } - - private function formatSalesData($sales): string - { - if (empty($sales)) { - return '-'; - } - - $formatted = []; - foreach ($sales as $sale) { - $nama = $sale['nama'] ?? 'Sales Tidak Dikenal'; - $itemTerjual = $sale['item_terjual'] ?? 0; - $pendapatan = $sale['pendapatan'] ?? '-'; - - $formatted[] = "{$nama}: {$itemTerjual} item, {$pendapatan}"; - } - - return implode('; ', $formatted); - } } diff --git a/app/Http/Controllers/TransaksiController.php b/app/Http/Controllers/TransaksiController.php index 2628dbb..11e47e2 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,12 @@ 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, - ]; + $transaksi->each(function ($transaksi) { + $transaksi->total_items = $transaksi->itemTransaksi->count(); + $transaksi->tanggal = $transaksi->created_at->format('d/m/Y'); }); - - return response()->json($mapped); + return response()->json($transaksi); + } @@ -42,63 +38,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 +116,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/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: 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..e5f76d4 100644 --- a/resources/js/components/KasirTransaksiList.vue +++ b/resources/js/components/KasirTransaksiList.vue @@ -13,8 +13,8 @@ {{ trx.tanggal }} - {{ trx.kode }} - Rp{{ (trx.pendapatan || 0).toLocaleString() }} + {{ trx.kode_transaksi }} + Rp{{ (trx.total_harga || 0).toLocaleString() }}