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

This commit is contained in:
adityaalfarison 2025-09-03 09:59:57 +07:00
commit 396baa6444
26 changed files with 762 additions and 214 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

@ -82,7 +82,9 @@ class SalesController extends Controller
*/ */
public function destroy(int $id) public function destroy(int $id)
{ {
Sales::findOrFail($id)->delete(); $sales = Sales::findOrFail($id);
$sales->transaksi()->update(['id_sales' => null]);
$sales->delete();
return response()->json([ return response()->json([
'message' => 'Sales berhasil dihapus' 'message' => 'Sales berhasil dihapus'
], 200); ], 200);

View File

@ -19,9 +19,9 @@ class UserController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$request->validate([ $request->validate([
'nama' => 'required|nama|unique:users', 'nama' => 'required|string|unique:users',
'password' => 'required|min:6', 'password' => 'required|min:6',
'role' => 'required|in:owner, kasir', 'role' => 'required|in:owner,kasir',
]); ]);
User::create([ User::create([

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,3 +1,4 @@
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
@import 'tailwindcss'; @import 'tailwindcss';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@ -23,3 +24,15 @@
--color-C: #77C7EE; --color-C: #77C7EE;
--color-D: #024768; --color-D: #024768;
} }
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-5px);
}
20%, 40%, 60%, 80% {
transform: translateX(5px);
}
}

View File

@ -0,0 +1,103 @@
<template>
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
<div class="bg-white rounded-lg p-6 w-96 shadow-lg">
<h2 class="text-lg font-bold mb-4">Tambah Akun</h2>
<form @submit.prevent="createAkun">
<!-- Nama -->
<div class="mb-3">
<label class="block font-medium mb-1">Nama</label>
<input
v-model.trim="form.nama"
type="text"
class="border rounded w-full p-2 focus:ring focus:ring-blue-300"
required
/>
</div>
<!-- Password -->
<div class="mb-3">
<label class="block font-medium mb-1">Password</label>
<input
v-model="form.password"
type="password"
class="border rounded w-full p-2 focus:ring focus:ring-blue-300"
required
/>
</div>
<!-- Peran -->
<div class="mb-3">
<label class="block font-medium mb-1">Peran</label>
<select
v-model="form.role"
class="border rounded w-full p-2 focus:ring focus:ring-blue-300"
required
>
<option disabled value="">-- Pilih Peran --</option>
<option value="owner">Owner</option>
<option value="kasir">Kasir</option>
</select>
</div>
<!-- Tombol -->
<div class="flex justify-end gap-2 mt-4">
<button
type="button"
@click="$emit('close')"
class="bg-gray-300 hover:bg-gray-400 px-4 py-2 rounded"
>
Batal
</button>
<button
type="submit"
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded"
>
Simpan
</button>
</div>
</form>
<!-- Error -->
<p v-if="errorMessage" class="text-red-500 text-sm mt-3">
{{ errorMessage }}
</p>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "CreateAkun",
data() {
return {
form: {
nama: "",
password: "",
role: "",
},
errorMessage: "",
};
},
methods: {
async createAkun() {
try {
await axios.post("api/user", this.form);
// reset form
this.form = { nama: "", password: "", role: "" };
// tutup modal dan refresh data
this.$emit("refresh");
this.$emit("close");
} catch (err) {
this.errorMessage =
err.response?.data?.message || "Gagal menambah akun.";
console.error("Gagal tambah akun:", err);
}
},
},
};
</script>

View File

@ -1,5 +1,6 @@
<template> <template>
<div v-if="isOpen" class="fixed inset-0 flex items-center justify-center bg-black/75 z-50">
<div v-if="isOpen" class="fixed inset-0 flex items-center justify-center bg-black/65 z-50">
<div class="bg-white rounded-lg shadow-lg w-96 p-6 relative"> <div class="bg-white rounded-lg shadow-lg w-96 p-6 relative">
<!-- Header --> <!-- Header -->
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
@ -31,7 +32,7 @@
<button <button
@click="saveKategori" @click="saveKategori"
:disabled="!form.nama" :disabled="!form.nama"
class="px-4 py-2 bg-C text-white rounded hover:bg-B disabled:opacity-50" class="px-4 py-2 bg-C text-black rounded hover:bg-B"
> >
Simpan Simpan
</button> </button>

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="isOpen" v-if="isOpen"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" class="fixed inset-0 bg-black/65 flex items-center justify-center z-50"
> >
<div class="bg-white rounded-lg shadow-lg w-full max-w-lg p-6 relative"> <div class="bg-white rounded-lg shadow-lg w-full max-w-lg p-6 relative">
<h2 class="text-xl font-bold mb-4">Tambah Sales</h2> <h2 class="text-xl font-bold mb-4">Tambah Sales</h2>
@ -24,7 +24,7 @@
<div class="flex justify-end gap-2 mt-6"> <div class="flex justify-end gap-2 mt-6">
<button type="button" @click="$emit('close')" class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Batal</button> <button type="button" @click="$emit('close')" class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Batal</button>
<button type="submit" class="px-4 py-2 bg-[#c6a77d] text-white rounded hover:bg-[#b09065]">Simpan</button> <button type="submit" class="px-4 py-2 bg-C text-D rounded hover:bg-C/80">Simpan</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -0,0 +1,95 @@
<template>
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white rounded-lg p-6 w-96">
<h2 class="text-lg font-bold mb-4">Edit Akun</h2>
<form @submit.prevent="updateAkun">
<!-- Nama -->
<div class="mb-3">
<label class="block font-medium">Nama</label>
<input v-model="form.nama" type="text" class="border rounded w-full p-2" required />
</div>
<!-- Password -->
<div class="mb-3">
<label class="block font-medium">Password</label>
<input v-model="form.password" type="password" class="border rounded w-full p-2" />
<small class="text-gray-500">Kosongkan jika tidak ingin mengubah password</small>
</div>
<!-- Peran -->
<div class="mb-3">
<label class="block font-medium">Peran</label>
<select v-model="form.role" class="border rounded w-full p-2" required>
<option value="">-- Pilih Peran --</option>
<option value="owner">Owner</option>
<option value="kasir">Kasir</option>
</select>
</div>
<!-- Tombol -->
<div class="flex justify-end gap-2 mt-4">
<button type="button" @click="$emit('close')" class="bg-gray-300 px-4 py-2 rounded">
Batal
</button>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">
Ubah
</button>
</div>
</form>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
props: {
akun: {
type: Object,
required: true,
},
},
data() {
return {
form: {
nama: this.akun.nama || "",
password: "",
role: this.akun.role || "", // gunakan "role" bukan "peran"
},
};
},
watch: {
akun: {
handler(newVal) {
if (newVal) {
this.form = {
nama: newVal.nama || "",
password: "",
role: newVal.role || "",
};
}
},
deep: true,
immediate: true,
},
},
methods: {
async updateAkun() {
try {
const payload = { ...this.form };
if (!payload.password) delete payload.password;
await axios.put(`api/user/${this.akun.id}`, payload);
this.$emit("refresh");
this.$emit("close");
} catch (err) {
console.error("Gagal update akun:", err.response?.data || err.message);
alert("Update akun gagal. Silakan cek kembali inputan.");
}
},
},
};
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-40 z-50"> <div class="fixed inset-0 flex items-center justify-center bg-black/65 z-50">
<div class="bg-white rounded-lg shadow-lg w-[400px] p-6 relative"> <div class="bg-white rounded-lg shadow-lg w-[400px] p-6 relative">
<!-- Tombol close --> <!-- Tombol close -->
@ -27,8 +27,8 @@
<button @click="$emit('close')" class="px-4 py-2 bg-gray-300 rounded-md hover:bg-gray-400"> <button @click="$emit('close')" class="px-4 py-2 bg-gray-300 rounded-md hover:bg-gray-400">
Batal Batal
</button> </button>
<button @click="updateKategori" class="px-4 py-2 bg-B text-white rounded-md hover:bg-A"> <button @click="updateKategori" class="px-4 py-2 bg-B text-D rounded-md hover:bg-A">
Update Ubah
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="isOpen" v-if="isOpen"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" class="fixed inset-0 bg-black/65 flex items-center justify-center z-50"
> >
<div class="bg-white rounded-lg shadow-lg w-full max-w-lg p-6 relative"> <div class="bg-white rounded-lg shadow-lg w-full max-w-lg p-6 relative">
<h2 class="text-xl font-bold mb-4">Ubah Sales</h2> <h2 class="text-xl font-bold mb-4">Ubah Sales</h2>
@ -24,7 +24,7 @@
<div class="flex justify-end gap-2 mt-6"> <div class="flex justify-end gap-2 mt-6">
<button type="button" @click="$emit('close')" class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Batal</button> <button type="button" @click="$emit('close')" class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">Batal</button>
<button type="submit" class="px-4 py-2 bg-[#c6a77d] text-white rounded hover:bg-[#b09065]">Update</button> <button type="submit" class="px-4 py-2 bg-C text-D rounded hover:bg-C">Ubah</button>
</div> </div>
</form> </form>
</div> </div>
@ -37,7 +37,7 @@
const props = defineProps({ const props = defineProps({
isOpen: Boolean, isOpen: Boolean,
sales: Object, // data sales yang akan di-edit sales: Object,
}); });
const emit = defineEmits(["close"]); const emit = defineEmits(["close"]);
@ -48,7 +48,6 @@
alamat: "", alamat: "",
}); });
// isi form dengan data props.sales
watch( watch(
() => props.sales, () => props.sales,
(val) => { (val) => {
@ -62,11 +61,9 @@
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
await axios.put(`/api/sales/${props.sales.id}`, form.value); await axios.put(`/api/sales/${props.sales.id}`, form.value);
alert("Sales berhasil diubah!");
emit("close"); emit("close");
} catch (error) { } catch (error) {
console.error("Error updating sales:", error); console.error("Error updating sales:", error);
alert("Gagal mengubah sales");
} }
}; };
</script> </script>

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>
import { ref, computed } from 'vue'
<script setup>
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 hargaJual = ref(null) const info = ref('')
const pesanan = ref([]) const error = ref('')
const hargaJual = ref(null)
const item = ref(null)
const loadingItem = ref(false)
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 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
} }
const total = computed(() => // harga deal
pesanan.value.reduce((sum, item) => sum + item.harga * item.jumlah, 0) item.value.harga_deal = hargaJual.value
)
</script>
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,42 +1,35 @@
<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>
</table> </table>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue" const props = defineProps({
const props = defineProps({
transaksi: { transaksi: {
type: Array, type: Array,
default: () => [] default: () => []
} }
}) })
defineEmits(['detail']) defineEmits(['detail'])
</script>
onMounted(() => {
console.log(props.transaksi);
})
</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

@ -10,7 +10,7 @@
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div v-for="tray in filteredTrays" :key="tray.id" <div v-for="tray in filteredTrays" :key="tray.id"
class="border rounded-lg p-4 shadow-sm hover:shadow-md transition"> class="border border-C rounded-lg p-4 shadow-sm hover:shadow-md transition">
<div class="flex justify-between items-center mb-3"> <div class="flex justify-between items-center mb-3">
<h2 class="font-bold text-lg" style="color: #102C57;">{{ tray.nama }}</h2> <h2 class="font-bold text-lg" style="color: #102C57;">{{ tray.nama }}</h2>
<div class="flex gap-2"> <div class="flex gap-2">
@ -24,7 +24,7 @@
</div> </div>
<div v-if="tray.items && tray.items.length > 0" class="space-y-2 max-h-64 overflow-y-auto pr-2"> <div v-if="tray.items && tray.items.length > 0" class="space-y-2 max-h-64 overflow-y-auto pr-2">
<div v-for="item in tray.items" :key="item.id" class="flex justify-between items-center border rounded-lg p-2" <div v-for="item in tray.items" :key="item.id" class="flex justify-between items-center border border-C rounded-lg p-2"
@click="openMovePopup(item)"> @click="openMovePopup(item)">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">

211
resources/js/pages/Akun.vue Normal file
View File

@ -0,0 +1,211 @@
<template>
<mainLayout>
<!-- Modal Create/Edit Akun -->
<CreateAkun
v-if="creatingAkun"
:isOpen="creatingAkun"
:akun="detail"
@close="closeAkun"
/>
<EditAkun
v-if="editingAkun"
:isOpen="editingAkun"
:akun="detail"
@close="closeEditAkun"
/>
<!-- Modal Delete -->
<ConfirmDeleteModal
:isOpen="confirmDeleteOpen"
title="Hapus User"
message="Apakah Anda yakin ingin menghapus user ini?"
@confirm="confirmDelete"
@cancel="closeDeleteModal"
/>
<div class="p-6 min-h-[75vh]">
<!-- Header Section -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-D">Manajemen Akun</h1>
<button
@click="tambahAkun"
class="px-4 py-2 bg-C text-D rounded-md hover:bg-C/80 transition duration-200 flex items-center gap-2"
>
<svg
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
Tambah User
</button>
</div>
<!-- Table Section -->
<div
class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden"
>
<table class="w-full">
<thead>
<tr class="bg-C text-white">
<th class="px-6 py-4 text-center text-D border-r border-[#b09065]">No</th>
<th class="px-6 py-4 text-center text-D border-r border-[#b09065]">Nama</th>
<th class="px-6 py-4 text-center text-D border-r border-[#b09065]">Role</th>
<th class="px-6 py-4 text-center text-D">Aksi</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in akun"
:key="item.id"
class="border-b border-gray-200 hover:bg-gray-50 transition duration-150"
:class="{ 'bg-gray-50': index % 2 === 1 }"
>
<td class="px-6 py-4 border-r border-gray-200 text-center font-medium text-gray-900">
{{ index + 1 }}
</td>
<td class="px-6 py-4 border-r border-gray-200 text-D">
{{ item.nama }}
</td>
<td class="px-6 py-4 border-r border-gray-200 text-gray-800">
{{ item.role }}
</td>
<td class="px-6 py-4 text-center">
<div class="flex justify-center gap-2">
<button
@click="ubahAkun(item)"
class="px-3 py-1 bg-yellow-500 text-white text-sm rounded hover:bg-yellow-600 transition duration-200"
>
Ubah
</button>
<button
@click="hapusAkun(item)"
class="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600 transition duration-200"
>
Hapus
</button>
</div>
</td>
</tr>
<!-- Empty State -->
<tr v-if="akun.length === 0 && !loading">
<td colspan="5" class="px-6 py-8 text-center text-gray-500">
<div class="flex flex-col items-center">
<svg
class="w-12 h-12 text-gray-400 mb-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2 2v-5m16 0h-2M4 13h2"
/>
</svg>
<p>Tidak ada data user</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Loading State -->
<div v-if="loading" class="flex justify-center items-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-[#c6a77d]"></div>
<span class="ml-2 text-gray-600">Memuat data...</span>
</div>
</div>
</mainLayout>
</template>
<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
import mainLayout from "../layouts/mainLayout.vue";
import ConfirmDeleteModal from "../components/ConfirmDeleteModal.vue";
import CreateAkun from "../components/CreateAkun.vue";
import EditAkun from "../components/EditAkun.vue";
// State
const akun = ref([]);
const loading = ref(false);
const creatingAkun = ref(false);
const detail = ref(null);
const editingAkun = ref(false);
const confirmDeleteOpen = ref(false);
const akunToDelete = ref(null);
// Fetch data dari API
const fetchAkun = async () => {
loading.value = true;
try {
const response = await axios.get("/api/user");
akun.value = response.data;
} catch (error) {
console.error("Error fetching akun:", error);
} finally {
loading.value = false;
}
};
// Tambah
const tambahAkun = () => {
detail.value = null;
creatingAkun.value = true;
};
// Ubah
const ubahAkun = (item) => {
detail.value = item;
editingAkun.value = true;
};
// Hapus
const hapusAkun = (item) => {
akunToDelete.value = item;
confirmDeleteOpen.value = true;
};
const confirmDelete = async () => {
try {
await axios.delete(`/api/user/${akunToDelete.value.id}`);
fetchAkun();
confirmDeleteOpen.value = false;
} catch (error) {
console.error("Error deleting akun:", error);
}
};
const closeDeleteModal = () => {
confirmDeleteOpen.value = false;
akunToDelete.value = null;
};
// Tutup modal Create/Edit
const closeAkun = () => {
creatingAkun.value = false;
fetchAkun();
};
const closeEditAkun = () => {
editingAkun.value = false;
fetchAkun();
};
// Lifecycle
onMounted(() => {
fetchAkun();
});
</script>

View File

@ -1,36 +1,71 @@
<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>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue" import { ref, onMounted } from "vue"
import axios from "axios" import axios from "axios"
import mainLayout from '../layouts/mainLayout.vue' import mainLayout from '../layouts/mainLayout.vue'
import KasirForm from '../components/KasirForm.vue' import KasirForm from '../components/KasirForm.vue'
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
} }
}) })

View File

@ -4,7 +4,7 @@
<ConfirmDeleteModal :isOpen="confirmDeleteOpen" :item="kategoriToDelete" title="Hapus Kategori" <ConfirmDeleteModal :isOpen="confirmDeleteOpen" :item="kategoriToDelete" title="Hapus Kategori"
message="Apakah Anda yakin ingin menghapus kategori ini?" @confirm="confirmDelete" @cancel="closeDeleteModal" /> message="Apakah Anda yakin ingin menghapus kategori ini?" @confirm="confirmDelete" @cancel="closeDeleteModal" />
<div class="p-6"> <div class="p-6 min-h-[75vh]" >
<!-- Header Section --> <!-- Header Section -->
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">Kategori</h1> <h1 class="text-2xl font-bold text-gray-800">Kategori</h1>
@ -22,10 +22,10 @@
<table class="w-full"> <table class="w-full">
<thead> <thead>
<tr class="bg-C text-black"> <tr class="bg-C text-black">
<th class="px-6 py-4 text-left font-semibold border-r border-C"> <th class="px-6 py-4 text-center font-semibold border-r border-C">
No No
</th> </th>
<th class="px-6 py-4 text-left font-semibold border-r border-C"> <th class="px-6 py-4 text-center font-semibold border-r border-C">
Nama Kategori Nama Kategori
</th> </th>
<th class="px-6 py-4 text-center font-semibold"> <th class="px-6 py-4 text-center font-semibold">
@ -37,10 +37,10 @@
<tr v-for="(item, index) in kategori" :key="item.id" <tr v-for="(item, index) in kategori" :key="item.id"
class="border-b border-gray-200 hover:bg-A transition duration-150" class="border-b border-gray-200 hover:bg-A transition duration-150"
:class="{ 'bg-gray-50': index % 2 === 1 }"> :class="{ 'bg-gray-50': index % 2 === 1 }">
<td class="px-6 py-4 border-r border-gray-200 font-medium text-gray-900"> <td class="px-6 py-4 border-r border-gray-200 font-medium text-center text-gray-900">
{{ index + 1 }} {{ index + 1 }}
</td> </td>
<td class="px-6 py-4 border-r border-gray-200 text-gray-800"> <td class="px-6 py-4 border-r border-gray-200 text-center text-gray-800">
{{ item.nama }} {{ item.nama }}
</td> </td>
<td class="px-6 py-4 text-center"> <td class="px-6 py-4 text-center">

View File

@ -57,11 +57,11 @@
@click.self="closeOverlay" @click.self="closeOverlay"
> >
<div <div
class="bg-white rounded-lg shadow-lg p-6 w-[450px] border-2 border-[#e6d3b3] relative flex flex-col items-center" class="bg-white rounded-lg shadow-lg p-6 w-[450px] border-2 border-B relative flex flex-col items-center"
> >
<!-- Foto Produk --> <!-- Foto Produk -->
<div <div
class="relative w-72 h-72 border border-[#e6d3b3] flex items-center justify-center mb-3 overflow-hidden rounded" class="relative w-72 h-72 border border-B flex items-center justify-center mb-3 overflow-hidden rounded"
> >
<img <img
v-if="detail.foto && detail.foto.length > 0" v-if="detail.foto && detail.foto.length > 0"

View File

@ -12,7 +12,7 @@
v-if="editingSales" v-if="editingSales"
:isOpen="editingSales" :isOpen="editingSales"
:sales="detail" :sales="detail"
@close="closeSales" @close="closeEditSales"
/> />
<!-- Modal Delete --> <!-- Modal Delete -->
@ -21,16 +21,16 @@
title="Hapus Sales" title="Hapus Sales"
message="Apakah Anda yakin ingin menghapus sales ini?" message="Apakah Anda yakin ingin menghapus sales ini?"
@confirm="confirmDelete" @confirm="confirmDelete"
@close="closeDeleteModal" @cancel="closeDeleteModal"
/> />
<div class="p-6"> <div class="p-6 min-h-[75vh]">
<!-- Header Section --> <!-- Header Section -->
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">Sales</h1> <h1 class="text-2xl font-bold text-D">Sales</h1>
<button <button
@click="tambahSales" @click="tambahSales"
class="px-4 py-2 bg-[#c6a77d] text-white rounded-md hover:bg-[#b09065] transition duration-200 flex items-center gap-2" class="px-4 py-2 bg-C text-D rounded-md hover:bg-C/80 transition duration-200 flex items-center gap-2"
> >
<svg <svg
class="w-4 h-4" class="w-4 h-4"
@ -54,29 +54,29 @@
class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden" class="bg-white rounded-lg shadow-md border border-gray-200 overflow-hidden"
> >
<table class="w-full"> <table class="w-full">
<thead> <thead class="">
<tr class="bg-[#c6a77d] text-white"> <tr class="bg-C text-white">
<th <th
class="px-6 py-4 text-left font-semibold border-r border-[#b09065]" class="px-6 py-4 text-center text-D border-r border-[#b09065]"
> >
No No
</th> </th>
<th <th
class="px-6 py-4 text-left font-semibold border-r border-[#b09065]" class="px-6 py-4 text-center text-D border-r border-[#b09065]"
> >
Nama Sales Nama Sales
</th> </th>
<th <th
class="px-6 py-4 text-left font-semibold border-r border-[#b09065]" class="px-6 py-4 text-center text-D border-r border-[#b09065]"
> >
No HP No HP
</th> </th>
<th <th
class="px-6 py-4 text-left font-semibold border-r border-[#b09065]" class="px-6 py-4 text-center text-D border-r border-[#b09065]"
> >
Alamat Alamat
</th> </th>
<th class="px-6 py-4 text-center font-semibold"> <th class="px-6 py-4 text-center text-D">
Aksi Aksi
</th> </th>
</tr> </tr>
@ -89,12 +89,12 @@
:class="{ 'bg-gray-50': index % 2 === 1 }" :class="{ 'bg-gray-50': index % 2 === 1 }"
> >
<td <td
class="px-6 py-4 border-r border-gray-200 font-medium text-gray-900" class="px-6 py-4 border-r border-gray-200 text-center font-medium text-gray-900"
> >
{{ index + 1 }} {{ index + 1 }}
</td> </td>
<td <td
class="px-6 py-4 border-r border-gray-200 text-gray-800" class="px-6 py-4 border-r border-gray-200 text-D"
> >
{{ item.nama }} {{ item.nama }}
</td> </td>
@ -178,7 +178,7 @@ const sales = ref([]);
const loading = ref(false); const loading = ref(false);
const creatingSales = ref(false); const creatingSales = ref(false);
const detail = ref(null); const detail = ref(null);
const editingSales = ref(false);
const confirmDeleteOpen = ref(false); const confirmDeleteOpen = ref(false);
const salesToDelete = ref(null); const salesToDelete = ref(null);
@ -204,7 +204,7 @@ const tambahSales = () => {
// Ubah // Ubah
const ubahSales = (item) => { const ubahSales = (item) => {
detail.value = item; detail.value = item;
creatingSales.value = true; editingSales.value = true;
}; };
// Hapus // Hapus
@ -234,6 +234,11 @@ const closeSales = () => {
fetchSales(); fetchSales();
}; };
const closeEditSales = () => {
editingSales.value = false;
fetchSales();
};
// Lifecycle // Lifecycle
onMounted(() => { onMounted(() => {
fetchSales(); fetchSales();

View File

@ -42,7 +42,7 @@
<!-- Modal Tambah/Edit Nampan --> <!-- Modal Tambah/Edit Nampan -->
<div <div
v-if="showModal" v-if="showModal"
class="fixed inset-0 bg-black bg-opacity-40 flex justify-center items-center z-50" class="fixed inset-0 bg-black/75 flex justify-center items-center z-50"
> >
<div class="bg-white rounded-lg shadow-lg p-6 w-96"> <div class="bg-white rounded-lg shadow-lg p-6 w-96">
<h2 class="text-lg font-semibold mb-4" style="color: #102C57;">Tambah Nampan</h2> <h2 class="text-lg font-semibold mb-4" style="color: #102C57;">Tambah Nampan</h2>

View File

@ -8,8 +8,12 @@ import InputProduk from '../pages/InputProduk.vue'
import Kategori from '../pages/Kategori.vue' import Kategori from '../pages/Kategori.vue'
import Sales from '../pages/Sales.vue' import Sales from '../pages/Sales.vue'
import EditProduk from '../pages/EditProduk.vue' import EditProduk from '../pages/EditProduk.vue'
import Login from '../pages/Login.vue' import Login from '../pages/Login.vue'
import Akun from '../pages/Akun.vue'
const routes = [ const routes = [
{ {
@ -52,6 +56,11 @@ const routes = [
name: 'Sales', name: 'Sales',
component: Sales component: Sales
}, },
{
path: '/akun',
name: 'Akun',
component: Akun
},
{ {
path: '/produk/:id/edit', // :id = parameter dinamis path: '/produk/:id/edit', // :id = parameter dinamis
name: 'EditProduk', name: 'EditProduk',