Compare commits
	
		
			No commits in common. "b2f93c4537c1105158b9949b8bac76fc43a4657f" and "c7812ea0fb8afd3abb27c8708ff43c921b9f314e" have entirely different histories.
		
	
	
		
			b2f93c4537
			...
			c7812ea0fb
		
	
		
| @ -52,8 +52,8 @@ class ItemController extends Controller | |||||||
|     public function update(Request $request, int $id) |     public function update(Request $request, int $id) | ||||||
|     { |     { | ||||||
|         $validated = $request->validate([ |         $validated = $request->validate([ | ||||||
|             'id_produk' => 'required|exists:produks,id', |             'id_produk' => 'required|in:produks.id', | ||||||
|             'id_nampan' => 'nullable|exists:nampans,id' |             'id_nampan' => 'nullable|in:nampans.id' | ||||||
|         ],[ |         ],[ | ||||||
|             'id_produk' => 'Id produk tidak valid.', |             'id_produk' => 'Id produk tidak valid.', | ||||||
|             'id_nampan' => 'Id nampan tidak valid' |             'id_nampan' => 'Id nampan tidak valid' | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ | |||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <!-- Berat --> |       <!-- Berat --> | ||||||
|       <span class="font-medium">{{ item.produk.berat }}g</span> |       <span class="font-medium">{{ item.berat }}g</span> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| @ -39,7 +39,6 @@ const props = defineProps({ | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const items = ref([]); | const items = ref([]); | ||||||
| const produk = ref([]) |  | ||||||
| const loading = ref(true); | const loading = ref(true); | ||||||
| const error = ref(null); | const error = ref(null); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,51 +1,66 @@ | |||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|  |     <!-- Loading --> | ||||||
|     <div v-if="loading" class="text-center py-6">Loading...</div> |     <div v-if="loading" class="text-center py-6">Loading...</div> | ||||||
| 
 | 
 | ||||||
|  |     <!-- Error --> | ||||||
|     <div v-else-if="error" class="text-center text-red-500 py-6">{{ error }}</div> |     <div v-else-if="error" class="text-center text-red-500 py-6">{{ error }}</div> | ||||||
| 
 | 
 | ||||||
|     <div v-else-if="filteredTrays.length === 0" class="text-center text-gray-500 py-6"> |     <!-- Kalau hasil search kosong --> | ||||||
|  |     <div | ||||||
|  |       v-else-if="filteredTrays.length === 0" | ||||||
|  |       class="text-center text-gray-500 py-6" | ||||||
|  |     > | ||||||
|       Nampan tidak ditemukan. |       Nampan tidak ditemukan. | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> |     <!-- Grid nampan --> | ||||||
|  |     <div | ||||||
|  |       v-else | ||||||
|  |       class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" | ||||||
|  |     > | ||||||
|       <div |       <div | ||||||
|         v-for="tray in filteredTrays" |         v-for="tray in filteredTrays" | ||||||
|         :key="tray.id" |         :key="tray.id" | ||||||
|         class="border rounded-lg p-4 shadow-sm hover:shadow-md transition" |         class="border rounded-lg p-4 shadow-sm hover:shadow-md transition" | ||||||
|       > |       > | ||||||
|  |         <!-- Header Nampan --> | ||||||
|         <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"> | ||||||
|             <button |             <button  | ||||||
|               class="p-2 rounded bg-yellow-400 hover:bg-yellow-500" |   class="p-2 rounded bg-yellow-400 hover:bg-yellow-500" | ||||||
|               @click="emit('edit', tray)" |   @click="emit('edit', tray)" | ||||||
|             > | > | ||||||
|               ✏️ |   ✏️ | ||||||
|             </button> | </button> | ||||||
|             <button |             <button  | ||||||
|               class="bg-red-500 text-white p-1 rounded" |   class="bg-red-500 text-white p-1 rounded" | ||||||
|               @click="emit('delete', tray.id)" |   @click="emit('delete', tray.id)" | ||||||
|             > | > | ||||||
|               🗑️ |   🗑️ | ||||||
|             </button> | </button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div v-if="tray.items && tray.items.length > 0" class="space-y-2 max-h-64 overflow-y-auto pr-2"> |         <!-- Isi Nampan --> | ||||||
|           <div |         <div  | ||||||
|   v-for="item in tray.items" |   v-if="tray.items && tray.items.length > 0"  | ||||||
|   :key="item.id" |   class="space-y-2 max-h-64 overflow-y-auto pr-2" | ||||||
|   class="flex justify-between items-center border rounded-lg p-2" |  | ||||||
|   @click="openMovePopup(item)" |  | ||||||
| > | > | ||||||
| 
 |   <div | ||||||
|  |     v-for="item in tray.items" | ||||||
|  |     :key="item.id" | ||||||
|  |     class="flex justify-between items-center border rounded-lg p-2" | ||||||
|  |   > | ||||||
|  |            | ||||||
|  |             <!-- Gambar + Info --> | ||||||
|             <div class="flex items-center gap-3"> |             <div class="flex items-center gap-3"> | ||||||
|               <img |               <img | ||||||
|                 v-if="item.produk.foto && item.produk.foto.length > 0" |                  v-if="item.produk.foto && item.produk.foto.length > 0" | ||||||
|                 :src="item.produk.foto[0].url" |       :src="item.produk.foto[0].url" | ||||||
|                 alt="foto produk" |       alt="foto produk" | ||||||
|                 class="w-12 h-12 object-cover rounded" |       class="w-12 h-12 object-cover rounded" | ||||||
|               /> |               /> | ||||||
|               <div> |               <div> | ||||||
|                 <p class="text-sm" style="color: #102C57;">{{ item.produk.nama }}</p> |                 <p class="text-sm" style="color: #102C57;">{{ item.produk.nama }}</p> | ||||||
| @ -53,179 +68,88 @@ | |||||||
|                 <p class="text-sm" style="color: #102C57;">{{ item.produk.harga_jual.toLocaleString() }}</p> |                 <p class="text-sm" style="color: #102C57;">{{ item.produk.harga_jual.toLocaleString() }}</p> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="flex items-center gap-2"> |             <!-- Berat --> | ||||||
|               <span class="font-medium">{{ item.produk.berat }}g</span> |             <span class="font-medium">{{ item.produk.berat }}g</span> | ||||||
|             </div> |  | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|  |         <!-- Kalau nampan kosong --> | ||||||
|         <div v-else class="text-gray-400 text-center py-4"> |         <div v-else class="text-gray-400 text-center py-4"> | ||||||
|           Nampan kosong.<br /> |           Nampan kosong.<br /> | ||||||
|           Masuk ke menu <b>Brankas</b> untuk memindahkan item ke nampan. |           Masuk ke menu <b>Brankas</b> untuk memindahkan item ke nampan. | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|  |         <!-- Total Berat --> | ||||||
|         <div class="border-t mt-3 pt-2 text-right font-semibold"> |         <div class="border-t mt-3 pt-2 text-right font-semibold"> | ||||||
|           Berat Total: {{ totalWeight(tray) }}g |           Berat Total: {{ totalWeight(tray) }}g | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| 
 |  | ||||||
|   <!-- Pop-up pindah item --> |  | ||||||
|   <div v-if="isPopupVisible" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> |  | ||||||
|     <div class="bg-white rounded-lg shadow-lg max-w-sm w-full p-6 relative"> |  | ||||||
|       <div class="flex justify-center mb-4"> |  | ||||||
|         <div class="p-2 border border-gray-300 rounded-lg"> |  | ||||||
|           <img :src="qrCodeUrl" alt="QR Code" class="w-36 h-36" /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|       <div class="text-center text-gray-700 font-medium mb-1">{{ selectedItem.produk.nama }}</div> |  | ||||||
|       <div class="text-center text-gray-500 text-sm mb-4">{{ selectedItem.produk.kategori }}</div> |  | ||||||
|       <div class="flex justify-center mb-4"> |  | ||||||
|         <button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition"> |  | ||||||
|           Cetak |  | ||||||
|         </button> |  | ||||||
|       </div> |  | ||||||
|       <div class="mb-4"> |  | ||||||
|         <label for="tray-select" class="block text-sm font-medium text-gray-700 mb-1">Nama Nampan</label> |  | ||||||
|         <select |  | ||||||
|           id="tray-select" |  | ||||||
|           v-model="selectedTrayId" |  | ||||||
|           class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" |  | ||||||
|         > |  | ||||||
|           <option value="" disabled>Pilih Nampan</option> |  | ||||||
|           <option v-for="tray in availableTrays" :key="tray.id" :value="tray.id"> |  | ||||||
|             {{ tray.nama }} |  | ||||||
|           </option> |  | ||||||
|         </select> |  | ||||||
|       </div> |  | ||||||
|       <div class="flex justify-end gap-2"> |  | ||||||
|         <button |  | ||||||
|           @click="closePopup" |  | ||||||
|           class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition" |  | ||||||
|         > |  | ||||||
|           Batal |  | ||||||
|         </button> |  | ||||||
|         <button |  | ||||||
|           @click="saveMove" |  | ||||||
|           :disabled="!selectedTrayId" |  | ||||||
|           class="px-4 py-2 rounded text-white transition" |  | ||||||
|           :class="selectedTrayId ? 'bg-orange-500 hover:bg-orange-600' : 'bg-gray-400 cursor-not-allowed'" |  | ||||||
|         > |  | ||||||
|           Simpan |  | ||||||
|         </button> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| <script setup> | <script setup> | ||||||
| import { ref, onMounted, computed } from "vue"; | import { ref, onMounted, computed } from "vue"; | ||||||
| import axios from "axios"; | import axios from "axios"; | ||||||
| 
 | 
 | ||||||
|  | // terima search dari parent | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   search: { |   search: { | ||||||
|     type: String, |     type: String, | ||||||
|     default: "", |     default: "", | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
| const emit = defineEmits(["edit", "delete"]); | 
 | ||||||
|  | const emit = defineEmits(["edit", "delete"]) | ||||||
|  | 
 | ||||||
| const trays = ref([]); | const trays = ref([]); | ||||||
| const loading = ref(true); | const loading = ref(true); | ||||||
| const error = ref(null); | const error = ref(null); | ||||||
| 
 | 
 | ||||||
| // --- State untuk Pop-up --- | // hitung total berat | ||||||
| const isPopupVisible = ref(false); | const totalWeight = (tray) => { | ||||||
| const selectedItem = ref(null); |   if (!tray.items) return 0; | ||||||
| const selectedTrayId = ref(""); |   return tray.items.reduce((sum, item) => sum + (item.produk.berat || 0), 0); | ||||||
| 
 |  | ||||||
| // QR Code generator |  | ||||||
| const qrCodeUrl = computed(() => { |  | ||||||
|   if (selectedItem.value) { |  | ||||||
|     const data = `ITM-${selectedItem.value.id}-${selectedItem.value.produk.nama.replace(/\s/g, '')}`; |  | ||||||
|     return `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(data)}`; |  | ||||||
|   } |  | ||||||
|   return ''; |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| // --- Fungsi Pop-up --- |  | ||||||
| const openMovePopup = (item) => { |  | ||||||
|   selectedItem.value = item; |  | ||||||
|   selectedTrayId.value = ""; |  | ||||||
|   isPopupVisible.value = true; |  | ||||||
| }; | }; | ||||||
|  | // 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" | ||||||
|  | // }; | ||||||
| 
 | 
 | ||||||
| const closePopup = () => { | // ambil data dari backend | ||||||
|   isPopupVisible.value = false; | onMounted(async () => { | ||||||
|   selectedItem.value = null; |  | ||||||
|   selectedTrayId.value = ""; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const saveMove = async () => { |  | ||||||
|   if (!selectedTrayId.value || !selectedItem.value) return; |  | ||||||
|   try { |  | ||||||
|     await axios.put(`/api/item/${selectedItem.value.id}`, { |  | ||||||
|       id_nampan: selectedTrayId.value, |  | ||||||
|       id_produk: selectedItem.value.id_produk, // ikutkan id_produk karena API minta |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     await refreshData(); |  | ||||||
|     closePopup(); |  | ||||||
|   } catch (err) { |  | ||||||
|     console.error("Gagal memindahkan item:", err.response?.data || err); |  | ||||||
|     alert("Gagal memindahkan item. Silakan coba lagi."); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| // --- Ambil data nampan + item --- |  | ||||||
| const refreshData = async () => { |  | ||||||
|   try { |   try { | ||||||
|     const [nampanRes, itemRes] = await Promise.all([ |     const [nampanRes, itemRes] = await Promise.all([ | ||||||
|       axios.get("/api/nampan"), |       axios.get("/api/nampan"), | ||||||
|       axios.get("/api/item"), |       axios.get("/api/item") | ||||||
|     ]); |     ]); | ||||||
|  | 
 | ||||||
|     const nampans = nampanRes.data; |     const nampans = nampanRes.data; | ||||||
|     const items = itemRes.data; |     const items = itemRes.data; | ||||||
| 
 | 
 | ||||||
|     trays.value = nampans.map((tray) => { |     // mapping items ke nampan | ||||||
|  |     trays.value = nampans.map(tray => { | ||||||
|       return { |       return { | ||||||
|         ...tray, |         ...tray, | ||||||
|         // pastikan tipe sama (string/number) |         items: items.filter(item => item.id_nampan === tray.id) | ||||||
|         items: items.filter((item) => Number(item.id_nampan) === Number(tray.id)), |  | ||||||
|       }; |       }; | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     console.log("Nampan dengan items:", trays.value); | ||||||
|   } catch (err) { |   } catch (err) { | ||||||
|     error.value = err.message || "Gagal mengambil data"; |     error.value = err.message || "Gagal mengambil data"; | ||||||
|   } finally { |   } finally { | ||||||
|     loading.value = false; |     loading.value = false; | ||||||
|   } |   } | ||||||
| }; | }); | ||||||
| 
 | 
 | ||||||
| // Hitung total berat | // filter berdasarkan nama nampan | ||||||
| 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); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // Filter nampan berdasarkan pencarian |  | ||||||
| const filteredTrays = computed(() => { | const filteredTrays = computed(() => { | ||||||
|   if (!props.search) return trays.value; |   if (!props.search) return trays.value; | ||||||
|   return trays.value.filter((tray) => |   return trays.value.filter((tray) => | ||||||
|     tray.nama.toLowerCase().includes(props.search.toLowerCase()) |     tray.nama.toLowerCase().includes(props.search.toLowerCase()) | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
| 
 | </script> | ||||||
| // Daftar nampan lain (selain tempat item saat ini) |  | ||||||
| const availableTrays = computed(() => { |  | ||||||
|   if (!selectedItem.value || !trays.value) return []; |  | ||||||
|   return trays.value.filter( |  | ||||||
|     (tray) => Number(tray.id) !== Number(selectedItem.value.id_nampan) |  | ||||||
|   ); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| onMounted(() => { |  | ||||||
|   refreshData(); |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user