diff --git a/.env.example b/.env.example
deleted file mode 100644
index 35db1dd..0000000
--- a/.env.example
+++ /dev/null
@@ -1,65 +0,0 @@
-APP_NAME=Laravel
-APP_ENV=local
-APP_KEY=
-APP_DEBUG=true
-APP_URL=http://localhost
-
-APP_LOCALE=en
-APP_FALLBACK_LOCALE=en
-APP_FAKER_LOCALE=en_US
-
-APP_MAINTENANCE_DRIVER=file
-# APP_MAINTENANCE_STORE=database
-
-PHP_CLI_SERVER_WORKERS=4
-
-BCRYPT_ROUNDS=12
-
-LOG_CHANNEL=stack
-LOG_STACK=single
-LOG_DEPRECATIONS_CHANNEL=null
-LOG_LEVEL=debug
-
-DB_CONNECTION=sqlite
-# DB_HOST=127.0.0.1
-# DB_PORT=3306
-# DB_DATABASE=laravel
-# DB_USERNAME=root
-# DB_PASSWORD=
-
-SESSION_DRIVER=database
-SESSION_LIFETIME=120
-SESSION_ENCRYPT=false
-SESSION_PATH=/
-SESSION_DOMAIN=null
-
-BROADCAST_CONNECTION=log
-FILESYSTEM_DISK=local
-QUEUE_CONNECTION=database
-
-CACHE_STORE=database
-# CACHE_PREFIX=
-
-MEMCACHED_HOST=127.0.0.1
-
-REDIS_CLIENT=phpredis
-REDIS_HOST=127.0.0.1
-REDIS_PASSWORD=null
-REDIS_PORT=6379
-
-MAIL_MAILER=log
-MAIL_SCHEME=null
-MAIL_HOST=127.0.0.1
-MAIL_PORT=2525
-MAIL_USERNAME=null
-MAIL_PASSWORD=null
-MAIL_FROM_ADDRESS="hello@example.com"
-MAIL_FROM_NAME="${APP_NAME}"
-
-AWS_ACCESS_KEY_ID=
-AWS_SECRET_ACCESS_KEY=
-AWS_DEFAULT_REGION=us-east-1
-AWS_BUCKET=
-AWS_USE_PATH_STYLE_ENDPOINT=false
-
-VITE_APP_NAME="${APP_NAME}"
diff --git a/app/Http/Controllers/FotoSementaraController.php b/app/Http/Controllers/FotoSementaraController.php
index 3aa5ecb..339ad6d 100644
--- a/app/Http/Controllers/FotoSementaraController.php
+++ b/app/Http/Controllers/FotoSementaraController.php
@@ -11,7 +11,7 @@ class FotoSementaraController extends Controller
public function upload(Request $request)
{
$request->validate([
- 'id_produk' => 'required|exists:produk,id',
+ 'id_user' => 'required|exists:users,id',
'foto' => 'required|image|mimes:jpg,jpeg,png|max:2048',
]);
@@ -19,11 +19,11 @@ class FotoSementaraController extends Controller
$url = asset('storage/' . $path);
$foto = FotoSementara::create([
- 'id_produk' => $request->id_produk,
+ 'id_user' => $request->id_user,
'url' => $url,
]);
- return response()->json(['message' => 'Foto berhasil disimpan'], 201);
+ return response()->json($foto, 201);
}
public function hapus($id)
@@ -45,6 +45,9 @@ class FotoSementaraController extends Controller
public function getAll($user_id)
{
$data = FotoSementara::where('id_user', $user_id);
+ if (!$data->exists()) {
+ return response()->json(['message' => 'Tidak ada foto ditemukan'], 404);
+ }
return response()->json($data);
}
diff --git a/app/Http/Controllers/NampanController.php b/app/Http/Controllers/NampanController.php
index 396f90a..7bf5049 100644
--- a/app/Http/Controllers/NampanController.php
+++ b/app/Http/Controllers/NampanController.php
@@ -13,7 +13,7 @@ class NampanController extends Controller
public function index()
{
return response()->json(
- Nampan::withCount('items')->get()
+ Nampan::with('items.produk.foto')->withCount('items')->get()
);
}
@@ -43,7 +43,7 @@ class NampanController extends Controller
public function show(int $id)
{
return response()->json(
- Nampan::with('items')->find($id)
+ Nampan::with('items.produk.foto')->find($id)
);
}
diff --git a/app/Http/Controllers/TransaksiController.php b/app/Http/Controllers/TransaksiController.php
index 0eb206a..055a1bb 100644
--- a/app/Http/Controllers/TransaksiController.php
+++ b/app/Http/Controllers/TransaksiController.php
@@ -13,14 +13,20 @@ class TransaksiController extends Controller
// List semua transaksi
public function index()
{
- $transaksi = Transaksi::with(['kasir', 'sales', 'items.item.produk'])->get();
+ $limit = request()->query('limit', null);
+ $query = Transaksi::with(['kasir', 'sales', 'items.item.produk'])->latest();
+ if ($limit) {
+ $query->limit((int)$limit);
+ }
+ $transaksi = $query->get();
+ $transaksi = Transaksi::with(['kasir', 'sales', 'items.item.produk'])->latest()->limit(100)->get();
return response()->json($transaksi);
}
// Detail transaksi by ID
public function show($id)
{
- $transaksi = Transaksi::with(['kasir', 'sales', 'items.item.produk'])->findOrFail($id);
+ $transaksi = Transaksi::with(['kasir', 'sales', 'items.item.produk.foto'])->findOrFail($id);
return response()->json($transaksi);
}
diff --git a/app/Models/Nampan.php b/app/Models/Nampan.php
index 80c8c4f..5044fba 100644
--- a/app/Models/Nampan.php
+++ b/app/Models/Nampan.php
@@ -13,9 +13,18 @@ class Nampan extends Model
protected $fillable = [
'nama'
];
+ protected $appends = ['berat_total'];
+
public function items()
{
return $this->hasMany(Item::class, 'id_nampan');
}
+
+ public function getBeratTotalAttribute()
+ {
+ return $this->items()
+ ->join('produks', 'items.id_produk', '=', 'produks.id')
+ ->sum('produks.berat');
+ }
}
diff --git a/resources/js/components/BrankasList.vue b/resources/js/components/BrankasList.vue
index dcdb2fc..0135531 100644
--- a/resources/js/components/BrankasList.vue
+++ b/resources/js/components/BrankasList.vue
@@ -8,8 +8,8 @@
![Product Image]()
diff --git a/resources/js/components/DropdownNav.vue b/resources/js/components/DropdownNav.vue
new file mode 100644
index 0000000..629ff4a
--- /dev/null
+++ b/resources/js/components/DropdownNav.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/components/Header.vue b/resources/js/components/Header.vue
index 0ba4563..174bc14 100644
--- a/resources/js/components/Header.vue
+++ b/resources/js/components/Header.vue
@@ -1,17 +1,70 @@
-
-
-
-
-
- {{ item }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
-
\ No newline at end of file
+
+
diff --git a/resources/js/components/InputField.vue b/resources/js/components/InputField.vue
new file mode 100644
index 0000000..b82db89
--- /dev/null
+++ b/resources/js/components/InputField.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/resources/js/components/InputSelect.vue b/resources/js/components/InputSelect.vue
new file mode 100644
index 0000000..1a3833e
--- /dev/null
+++ b/resources/js/components/InputSelect.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
diff --git a/resources/js/components/ProductCard.vue b/resources/js/components/ProductCard.vue
index 1253d5b..650784a 100644
--- a/resources/js/components/ProductCard.vue
+++ b/resources/js/components/ProductCard.vue
@@ -1,65 +1,31 @@
-
-
+
+
+
![]()
+
[tidak ada foto]
+
+
-
- {{ product.nama }}
-
-
-
-
-
-
-
-
-
-
-
- Detail Produk
-
-
-
-
-
Nama: {{ product.nama }}
-
Kategori: {{ product.kategori }}
-
Berat: {{ product.berat }} gram
-
Kadar: {{ product.kadar }}%
-
Harga/gram: Rp {{ formatHarga(product.harga_per_gram) }}
-
Harga Jual: Rp {{ formatHarga(product.harga_jual) }}
-
Stok: {{ product.items_count }} pcs
-
-
+ {{ product.nama }}
diff --git a/resources/js/components/TrayList.vue b/resources/js/components/TrayList.vue
index eea9b85..85e8124 100644
--- a/resources/js/components/TrayList.vue
+++ b/resources/js/components/TrayList.vue
@@ -26,34 +26,50 @@
>
-
{{ tray.nama }}
+
{{ tray.nama }}
-
-
+
+
-
-
+
+
+
-
{{ item.produk.nama }}
-
{{ item.produk.id }}
+
{{ item.produk.nama }}
+
{{ item.produk.kategori }}
+
{{ item.produk.harga_jual.toLocaleString() }}
-
{{ item.berat }}g
+
{{ item.produk.berat }}g
@@ -85,6 +101,8 @@ const props = defineProps({
},
});
+const emit = defineEmits(["edit", "delete"])
+
const trays = ref([]);
const loading = ref(true);
const error = ref(null);
@@ -92,15 +110,34 @@ const error = ref(null);
// hitung total berat
const totalWeight = (tray) => {
if (!tray.items) return 0;
- return tray.items.reduce((sum, item) => sum + (item.berat || 0), 0);
+ return tray.items.reduce((sum, item) => sum + (item.produk.berat || 0), 0);
};
+// const totalWeight = (tray) => {
+// if (!tray.items) return 0;
+// const total = tray.items.reduce((sum, item) => sum + (item.produk.berat || 0), 0);
+// return total.toFixed(2); // hasil string "12.34"
+// };
// ambil data dari backend
onMounted(async () => {
try {
- const res = await axios.get("/api/nampan");
- trays.value = res.data; // harus array tray dengan items
- console.log("Data nampan:", res.data);
+ const [nampanRes, itemRes] = await Promise.all([
+ axios.get("/api/nampan"),
+ axios.get("/api/item")
+ ]);
+
+ const nampans = nampanRes.data;
+ const items = itemRes.data;
+
+ // mapping items ke nampan
+ trays.value = nampans.map(tray => {
+ return {
+ ...tray,
+ items: items.filter(item => item.id_nampan === tray.id)
+ };
+ });
+
+ console.log("Nampan dengan items:", trays.value);
} catch (err) {
error.value = err.message || "Gagal mengambil data";
} finally {
diff --git a/resources/js/pages/InputProduk.vue b/resources/js/pages/InputProduk.vue
new file mode 100644
index 0000000..8ecfacd
--- /dev/null
+++ b/resources/js/pages/InputProduk.vue
@@ -0,0 +1,384 @@
+
+
+
+
Produk Baru
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Format: JPG, JPEG, PNG (Max: 2MB per file, Max: 6 foto)
+
+
+
+ {{ uploadError }}
+
+
+
+
+
+
+
+
diff --git a/resources/js/pages/Produk.vue b/resources/js/pages/Produk.vue
index 8fe0318..566dce7 100644
--- a/resources/js/pages/Produk.vue
+++ b/resources/js/pages/Produk.vue
@@ -4,14 +4,27 @@
PRODUK
-
-
+
+
+
+
+
+
+
+
-
@@ -22,78 +35,90 @@
v-for="item in filteredProducts"
:key="item.id"
:product="item"
- @showDetail="openOverlay"
+ @click="openOverlay(item.id)"
/>
-
-
-
-
- ✕
-
+
+
+
+
+
+
![]()
+
[gambar]
-
-
-
![]()
-
[gambar]
-
-
-
-
{{ detail.item_count }} pcs
-
-
-
- {{ detail.nama }}
-
-
-
-
-
Harga Beli : Rp. {{ formatNumber(detail.harga_beli) }}
-
{{ detail.kadar }} K
-
Harga Jual : Rp. {{ formatNumber(detail.harga_jual) }}
-
{{ detail.berat }} gram
-
- Harga/gram : Rp. {{ formatNumber(detail.harga_per_gram) }}
-
-
-
-
-
-
- Ubah
-
-
- Tambah
-
-
- Hapus
-
-
+
+
+ {{ detail.item_count }} pcs
+
+
+
+ {{ detail.nama }}
+
+
+
+
+ ‹
+
+
+
+ ›
+
+
+
+
+
+
Harga Jual : Rp. {{ formatNumber(detail.harga_jual) }}
+
{{ detail.kadar }} K
+
+ Berat : {{ detail.berat }} gram
+
+
+ Harga/gram : Rp. {{ formatNumber(detail.harga_per_gram) }}
+
+
+
+
+
+
+ Ubah
+
+
+ Tambah
+
+
+ Hapus
+
+
+
+
+
@@ -106,10 +131,12 @@ import searchbar from "../components/searchbar.vue";
const products = ref([]);
const searchQuery = ref("");
+const selectedCategory = ref("semua");
// overlay state
const showOverlay = ref(false);
const detail = ref({});
+const currentFotoIndex = ref(0);
// Fetch data awal
onMounted(async () => {
@@ -121,29 +148,60 @@ onMounted(async () => {
}
});
-// Filter
+// Filter gabungan (kategori + search)
const filteredProducts = computed(() => {
- if (!searchQuery.value) return products.value;
- return products.value.filter((p) =>
- p.nama.toLowerCase().includes(searchQuery.value.toLowerCase())
- );
+ let hasil = products.value;
+
+ // filter kategori
+ if (selectedCategory.value !== "semua") {
+ hasil = hasil.filter(
+ (p) => p.kategori.toLowerCase() === selectedCategory.value
+ );
+ }
+
+ // filter search
+ if (searchQuery.value) {
+ hasil = hasil.filter((p) =>
+ p.nama.toLowerCase().includes(searchQuery.value.toLowerCase())
+ );
+ }
+
+ return hasil;
});
-// Fungsi buka overlay
+// buka overlay
async function openOverlay(id) {
try {
const res = await axios.get(`http://127.0.0.1:8000/api/produk/${id}`);
detail.value = res.data;
+ currentFotoIndex.value = 0; // reset ke foto pertama
showOverlay.value = true;
} catch (error) {
console.error("Gagal fetch detail produk:", error);
}
}
-// Fungsi tutup overlay
+// tutup overlay
function closeOverlay() {
showOverlay.value = false;
detail.value = {};
+ currentFotoIndex.value = 0;
+}
+
+// foto navigation
+function nextFoto() {
+ if (detail.value.foto && detail.value.foto.length > 0) {
+ currentFotoIndex.value =
+ (currentFotoIndex.value + 1) % detail.value.foto.length;
+ }
+}
+
+function prevFoto() {
+ if (detail.value.foto && detail.value.foto.length > 0) {
+ currentFotoIndex.value =
+ (currentFotoIndex.value - 1 + detail.value.foto.length) %
+ detail.value.foto.length;
+ }
}
// Format angka
diff --git a/resources/js/pages/Tray.vue b/resources/js/pages/Tray.vue
index fde2d7e..fd7de17 100644
--- a/resources/js/pages/Tray.vue
+++ b/resources/js/pages/Tray.vue
@@ -1,16 +1,181 @@
-
- NAMPAN
-
-
-
-
+
+
+
+
+
+ NAMPAN
+
+
+
+
+
+
+
+
+
+ Tambah Nampan
+
+
+
+
+ Kosongkan
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tambah Nampan
+
+
+
+
+
+
+ Cancel
+
+
+
+ Save
+
+
+
+
+
+
+
+
+
Kosongkan semua nampan?
+
+ Semua item akan dimasukkan ke brankas.
+ Masuk ke menu ‘Brankas’ untuk mengembalikan item ke nampan.
+
+
+
+ Batal
+
+
+ Ya
+
+
+
+
+
+
+
\ No newline at end of file
+import searchbar from '../components/searchbar.vue'
+import TrayList from '../components/TrayList.vue'
+
+const searchQuery = ref("")
+const showModal = ref(false)
+const showConfirmModal = ref(false)
+const trayName = ref("")
+const editingTrayId = ref(null)
+
+// buka modal tambah/edit
+const openModal = () => { showModal.value = true }
+const closeModal = () => {
+ trayName.value = ""
+ editingTrayId.value = null
+ showModal.value = false
+}
+
+// simpan nampan
+const saveTray = async () => {
+ if (!trayName.value.trim()) {
+ alert("Nama Nampan tidak boleh kosong")
+ return
+ }
+ try {
+ if (editingTrayId.value) {
+ await axios.put(`/api/nampan/${editingTrayId.value}`, { nama: trayName.value })
+ alert("Nampan berhasil diupdate")
+ } else {
+ await axios.post("/api/nampan", { nama: trayName.value })
+ alert("Nampan berhasil ditambahkan")
+ }
+ closeModal()
+ location.reload()
+ } catch (error) {
+ console.error(error)
+ alert("Gagal menyimpan nampan")
+ }
+}
+
+// === Konfirmasi kosongkan nampan ===
+const openConfirmModal = () => { showConfirmModal.value = true }
+const closeConfirmModal = () => { showConfirmModal.value = false }
+
+const confirmEmptyTray = async () => {
+ try {
+ await axios.post("/api/brankas", { action: "move_all_from_tray" })
+ alert("Semua item berhasil dipindahkan ke Brankas")
+ closeConfirmModal()
+ location.reload()
+ } catch (error) {
+ console.error(error)
+ alert("Gagal mengosongkan nampan")
+ }
+}
+
+const editTray = (tray) => {
+ trayName.value = tray.nama
+ editingTrayId.value = tray.id
+ showModal.value = true
+}
+
+const deleteTray = async (id) => {
+ if (!confirm("Yakin ingin menghapus nampan ini?")) return
+ try {
+ await axios.delete(`/api/nampan/${id}`)
+ alert("Nampan berhasil dihapus")
+ location.reload()
+ } catch (error) {
+ console.error(error)
+ alert("Gagal menghapus nampan")
+ }
+}
+
diff --git a/resources/js/router/index.js b/resources/js/router/index.js
index f5eaa5d..1856f73 100644
--- a/resources/js/router/index.js
+++ b/resources/js/router/index.js
@@ -4,7 +4,7 @@ import Produk from '../pages/Produk.vue'
import Brankas from '../pages/Brankas.vue'
import Tray from '../pages/Tray.vue'
import Kasir from '../pages/Kasir.vue'
-
+import InputProduk from '../pages/InputProduk.vue'
const routes = [
@@ -18,6 +18,11 @@ const routes = [
name: 'Produk',
component: Produk
},
+ {
+ path: '/produk/baru',
+ name: 'ProdukBaru',
+ component: InputProduk
+ },
{
path: '/brankas',
name: 'Brankas',
diff --git a/routes/web.php b/routes/web.php
index d6cbe28..fcdea34 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -19,10 +19,12 @@ Route::prefix('api')->group(function () {
Route::apiResource('transaksi', TransaksiController::class);
Route::get('brankas', [ItemController::class, 'brankasItem']);
+
+ // Foto Sementara
Route::post('foto/upload', [FotoSementaraController::class, 'upload']);
- Route::delete('foto/hapus/
', [FotoSementaraController::class, 'hapus']);
- Route::get('foto/', [FotoSementaraController::class, 'getAll']);
- Route::delete('foto/reset/', [FotoSementaraController::class, 'reset']);
+ Route::delete('foto/hapus/{id}', [FotoSementaraController::class, 'hapus']);
+ Route::get('foto/{user_id}', [FotoSementaraController::class, 'getAll']);
+ Route::delete('foto/reset/{user_id}', [FotoSementaraController::class, 'reset']);
});
// Frontend SPA