Compare commits
	
		
			No commits in common. "baff04f6a59d2be3b62228f65a872e1325c80387" and "8ab48b4e7d63aa9b0c67ddb9315bfc277a18fa1c" have entirely different histories.
		
	
	
		
			baff04f6a5
			...
			8ab48b4e7d
		
	
		
| @ -34,7 +34,7 @@ class ProdukController extends Controller | ||||
|             'harga_per_gram' => 'required|numeric', | ||||
|             'harga_jual' => 'required|numeric', | ||||
|             'id_user' => 'nullable|exists:users,id', | ||||
|         ], | ||||
|         ],  | ||||
|         [ | ||||
|             'nama.required' => 'Nama produk harus diisi.', | ||||
|             'id_kategori' => 'Kategori tidak valid.', | ||||
| @ -59,13 +59,13 @@ class ProdukController extends Controller | ||||
|             // Pindahkan foto sementara ke foto permanen jika ada
 | ||||
|             if (isset($validated['id_user'])) { | ||||
|                 $fotoSementara = FotoSementara::where('id_user', $validated['id_user'])->get(); | ||||
| 
 | ||||
|                  | ||||
|                 foreach ($fotoSementara as $fs) { | ||||
|                     Foto::create([ | ||||
|                         'id_produk' => $produk->id, | ||||
|                         'url' => $fs->url | ||||
|                     ]); | ||||
| 
 | ||||
|                      | ||||
|                     // Hapus foto sementara setelah dipindah
 | ||||
|                     $fs->delete(); | ||||
|                 } | ||||
| @ -110,7 +110,7 @@ class ProdukController extends Controller | ||||
|             'harga_jual' => 'required|numeric', | ||||
|             'id_user' => 'nullable|exists:users,id', // untuk mengambil foto sementara baru
 | ||||
|             'hapus_foto_lama' => 'nullable|boolean', // flag untuk menghapus foto lama
 | ||||
|         ], | ||||
|         ],  | ||||
|         [ | ||||
|             'nama.required' => 'Nama produk harus diisi.', | ||||
|             'id_kategori' => 'Kategori tidak valid.', | ||||
| @ -123,11 +123,11 @@ class ProdukController extends Controller | ||||
|         DB::beginTransaction(); | ||||
|         try { | ||||
|             $produk = Produk::findOrFail($id); | ||||
| 
 | ||||
|              | ||||
|             // Update data produk
 | ||||
|             $produk->update([ | ||||
|                 'nama' => $validated['nama'], | ||||
|                 'id_kategori' => $validated['id_kategori'], | ||||
|                 'kategori' => $validated['kategori'], | ||||
|                 'berat' => $validated['berat'], | ||||
|                 'kadar' => $validated['kadar'], | ||||
|                 'harga_per_gram' => $validated['harga_per_gram'], | ||||
| @ -149,13 +149,13 @@ class ProdukController extends Controller | ||||
|             // Tambahkan foto baru dari foto sementara jika ada
 | ||||
|             if (isset($validated['id_user'])) { | ||||
|                 $fotoSementara = FotoSementara::where('id_user', $validated['id_user'])->get(); | ||||
| 
 | ||||
|                  | ||||
|                 foreach ($fotoSementara as $fs) { | ||||
|                     Foto::create([ | ||||
|                         'id_produk' => $produk->id, | ||||
|                         'url' => $fs->url | ||||
|                     ]); | ||||
| 
 | ||||
|                      | ||||
|                     // Hapus foto sementara setelah dipindah
 | ||||
|                     $fs->delete(); | ||||
|                 } | ||||
|  | ||||
| @ -1,266 +0,0 @@ | ||||
| <template> | ||||
|   <mainLayout> | ||||
|     <!-- Modal Buat Item --> | ||||
|     <CreateItemModal | ||||
|       :isOpen="openItemModal" | ||||
|       :product="editedProduct" | ||||
|       @close="closeItemModal" | ||||
|     /> | ||||
| 
 | ||||
|     <div class="p-6"> | ||||
|       <p class="font-serif italic text-[25px] text-D">Edit Produk</p> | ||||
| 
 | ||||
|       <div class="flex flex-col md:flex-row mt-5 gap-6"> | ||||
|         <!-- Form Section --> | ||||
|         <div class="flex-1"> | ||||
|           <div class="mb-3"> | ||||
|             <label class="block text-D mb-1">Nama Produk</label> | ||||
|             <InputField v-model="form.nama" type="text" placeholder="Masukkan nama produk" /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="mb-3"> | ||||
|             <label class="block text-D mb-1">Kategori</label> | ||||
|             <InputSelect v-model="form.id_kategori" :options="category" placeholder="Pilih kategori" /> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="mb-3 flex flex-row w-full gap-3"> | ||||
|             <div class="flex-1"> | ||||
|               <label class="block text-D mb-1">Berat (g)</label> | ||||
|               <InputField v-model="form.berat" type="number" step="0.01" placeholder="Masukkan berat" @input="calculateHargaJual" /> | ||||
|             </div> | ||||
|             <div class="flex-1"> | ||||
|               <label class="block text-D mb-1">Kadar (K)</label> | ||||
|               <InputField v-model="form.kadar" type="number" placeholder="Masukkan kadar" /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="mb-3 flex flex-row w-full gap-3"> | ||||
|             <div class="flex-1"> | ||||
|               <label class="block text-D mb-1">Harga per Gram</label> | ||||
|               <InputField v-model="form.harga_per_gram" type="number" step="0.01" placeholder="Masukkan harga per gram" @input="calculateHargaJual" /> | ||||
|             </div> | ||||
|             <div class="flex-1"> | ||||
|               <label class="block text-D mb-1">Harga Jual</label> | ||||
|               <InputField v-model="form.harga_jual" type="number" step="0.01" placeholder="Masukkan harga jual" /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Image Upload Section --> | ||||
|         <div class="flex-1"> | ||||
|           <label class="block text-D mb-1">Foto</label> | ||||
| 
 | ||||
|           <div class="grid grid-cols-3 gap-3"> | ||||
|             <!-- Existing Images --> | ||||
|             <div v-for="(image, index) in uploadedImages" :key="`img-${image.id}`" class="relative group aspect-square"> | ||||
|               <div class="w-full h-full bg-gray-100 rounded-lg border-2 border-gray-200 overflow-hidden"> | ||||
|                 <img :src="image.url" :alt="`Foto ${index + 1}`" class="w-full h-full object-cover" /> | ||||
|                 <button @click="removeImage(image.id)" :disabled="uploadLoading" | ||||
|                   class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold shadow-lg hover:bg-red-600 transition-colors disabled:bg-gray-400"> | ||||
|                   × | ||||
|                 </button> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- Upload Button --> | ||||
|             <div v-if="uploadedImages.length < 6" @drop="handleDrop" @dragover.prevent | ||||
|               @dragenter.prevent="isDragging = true" @dragleave.prevent="isDragging = false" @click="triggerFileInput" | ||||
|               class="aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-D hover:bg-blue-50 transition-colors group" | ||||
|               :class="{ 'border-blue-400 bg-blue-50': isDragging, 'cursor-not-allowed opacity-50': uploadLoading }"> | ||||
|               <div class="text-center"> | ||||
|                 <div v-if="!uploadLoading" | ||||
|                   class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2 group-hover:bg-D transition-colors"> | ||||
|                   <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||||
|                     <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||
|                       d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path> | ||||
|                   </svg> | ||||
|                 </div> | ||||
|                 <div v-else class="w-12 h-12 bg-D rounded-lg flex items-center justify-center mx-auto mb-2"> | ||||
|                   <svg class="animate-spin w-6 h-6 text-white" fill="none" viewBox="0 0 24 24"> | ||||
|                     <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | ||||
|                     <path class="opacity-75" fill="currentColor" | ||||
|                       d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> | ||||
|                     </path> | ||||
|                   </svg> | ||||
|                 </div> | ||||
|                 <p class="text-xs text-gray-600 font-medium" v-html="uploadLoading ? 'Uploading...' : 'Unggah<br/>Foto'"></p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <input ref="fileInput" type="file" multiple accept="image/jpeg,image/jpg,image/png" @change="handleFileSelect" class="hidden" /> | ||||
| 
 | ||||
|           <p class="text-xs text-gray-500 mt-2">Format: JPG, JPEG, PNG (Max: 2MB per file, Max: 6 foto)</p> | ||||
| 
 | ||||
|           <div v-if="uploadError" class="mt-2 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-600"> | ||||
|             {{ uploadError }} | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="mt-6 flex justify-end flex-row gap-3"> | ||||
|         <button @click="back" class="px-6 py-2 rounded-md bg-gray-400 hover:bg-gray-500 text-white">Batal</button> | ||||
|         <button @click="submitForm" :disabled="loading || !isFormValid" | ||||
|           class="bg-C text-D px-6 py-2 rounded-md hover:bg-B disabled:bg-B disabled:text-white disabled:cursor-not-allowed"> | ||||
|           {{ loading ? 'Menyimpan...' : 'Simpan Perubahan' }} | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </mainLayout> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, computed, onMounted } from "vue"; | ||||
| import { useRoute, useRouter } from "vue-router"; | ||||
| import axios from "axios"; | ||||
| import mainLayout from "../layouts/mainLayout.vue"; | ||||
| import InputField from "../components/InputField.vue"; | ||||
| import InputSelect from "../components/InputSelect.vue"; | ||||
| import CreateItemModal from "../components/CreateItemModal.vue"; | ||||
| 
 | ||||
| const route = useRoute(); | ||||
| const router = useRouter(); | ||||
| 
 | ||||
| const productId = route.params.id; | ||||
| 
 | ||||
| const form = ref({ | ||||
|   nama: "", | ||||
|   id_kategori: null, | ||||
|   berat: 0, | ||||
|   kadar: 0, | ||||
|   harga_per_gram: 0, | ||||
|   harga_jual: 0, | ||||
| }); | ||||
| 
 | ||||
| const category = ref([]); | ||||
| const uploadedImages = ref([]); | ||||
| const loading = ref(false); | ||||
| const uploadLoading = ref(false); | ||||
| const uploadError = ref(""); | ||||
| const isDragging = ref(false); | ||||
| const fileInput = ref(null); | ||||
| 
 | ||||
| const openItemModal = ref(false); | ||||
| const editedProduct = ref(null); | ||||
| const userId = ref(1); // TODO: ambil dari auth | ||||
| 
 | ||||
| const isFormValid = computed(() => { | ||||
|   return ( | ||||
|     form.value.nama && | ||||
|     form.value.id_kategori && | ||||
|     form.value.berat > 0 && | ||||
|     form.value.kadar > 0 && | ||||
|     form.value.harga_per_gram > 0 && | ||||
|     form.value.harga_jual > 0 | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| const calculateHargaJual = () => { | ||||
|   const berat = parseFloat(form.value.berat) || 0; | ||||
|   const hargaPerGram = parseFloat(form.value.harga_per_gram) || 0; | ||||
|   if (berat > 0 && hargaPerGram > 0) { | ||||
|     form.value.harga_jual = berat * hargaPerGram; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const loadKategori = async () => { | ||||
|   const response = await axios.get("/api/kategori"); | ||||
|   category.value = response.data.map((c) => ({ value: c.id, label: c.nama })); | ||||
| }; | ||||
| 
 | ||||
| const loadProduk = async () => { | ||||
|   const response = await axios.get(`/api/produk/${productId}`); | ||||
|   const produk = response.data; | ||||
|   form.value = { | ||||
|     nama: produk.nama, | ||||
|     id_kategori: produk.id_kategori, | ||||
|     berat: produk.berat, | ||||
|     kadar: produk.kadar, | ||||
|     harga_per_gram: produk.harga_per_gram, | ||||
|     harga_jual: produk.harga_jual, | ||||
|   }; | ||||
|   uploadedImages.value = produk.foto || []; | ||||
| }; | ||||
| 
 | ||||
| const triggerFileInput = () => { | ||||
|   if (!uploadLoading.value && uploadedImages.value.length < 6) { | ||||
|     fileInput.value?.click(); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const handleFileSelect = (e) => { | ||||
|   const files = Array.from(e.target.files); | ||||
|   uploadFiles(files); | ||||
| }; | ||||
| 
 | ||||
| const handleDrop = (e) => { | ||||
|   e.preventDefault(); | ||||
|   isDragging.value = false; | ||||
|   if (uploadLoading.value || uploadedImages.value.length >= 6) return; | ||||
|   const files = Array.from(e.dataTransfer.files); | ||||
|   uploadFiles(files); | ||||
| }; | ||||
| 
 | ||||
| const uploadFiles = async (files) => { | ||||
|   uploadError.value = ""; | ||||
|   const validFiles = files.filter( | ||||
|     (file) => | ||||
|       ["image/jpeg", "image/jpg", "image/png"].includes(file.type) && | ||||
|       file.size <= 2 * 1024 * 1024 | ||||
|   ); | ||||
|   if (!validFiles.length) return; | ||||
|   uploadLoading.value = true; | ||||
|   try { | ||||
|     for (const file of validFiles) { | ||||
|       const formData = new FormData(); | ||||
|       formData.append("foto", file); | ||||
|       formData.append("id_user", userId.value); | ||||
|       const res = await axios.post("/api/foto/upload", formData, { | ||||
|         headers: { "Content-Type": "multipart/form-data" }, | ||||
|       }); | ||||
|       uploadedImages.value.push(res.data); | ||||
|     } | ||||
|   } finally { | ||||
|     uploadLoading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const removeImage = async (id) => { | ||||
|   try { | ||||
|     await axios.delete(`/api/foto/hapus/${id}`); | ||||
|     uploadedImages.value = uploadedImages.value.filter((i) => i.id !== id); | ||||
|   } catch { | ||||
|     uploadError.value = "Gagal menghapus foto"; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const submitForm = async () => { | ||||
|   loading.value = true; | ||||
|   try { | ||||
|     await axios.put(`/api/produk/${productId}`, { | ||||
|       ...form.value, | ||||
|       id_user: userId.value, | ||||
|     }); | ||||
|     alert("Produk berhasil diupdate!"); | ||||
|     router.push("/produk"); | ||||
|   } catch (err) { | ||||
|     alert("Gagal update produk!"); | ||||
|     console.error(err); | ||||
|   } finally { | ||||
|     loading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const closeItemModal = () => { | ||||
|   openItemModal.value = false; | ||||
| }; | ||||
| 
 | ||||
| const back = () => { | ||||
|   router.push("/produk"); | ||||
| }; | ||||
| 
 | ||||
| onMounted(() => { | ||||
|   loadKategori(); | ||||
|   loadProduk(); | ||||
| }); | ||||
| </script> | ||||
| @ -123,12 +123,10 @@ | ||||
|         <!-- Tombol Aksi --> | ||||
|         <div class="flex w-full gap-3"> | ||||
|           <button | ||||
|             @click="$router.push(`/produk/${detail.id}/edit`)" | ||||
|             class="flex-1 bg-yellow-400 text-black py-2 rounded font-bold" | ||||
|             > | ||||
|           > | ||||
|             Ubah | ||||
|             </button> | ||||
| 
 | ||||
|           </button> | ||||
|           <button | ||||
|             @click="openItemModal" | ||||
|             class="bg-green-400 text-black px-4 py-2 rounded font-bold" | ||||
|  | ||||
| @ -6,7 +6,6 @@ import Tray from '../pages/Tray.vue' | ||||
| import Kasir from '../pages/Kasir.vue' | ||||
| import InputProduk from '../pages/InputProduk.vue' | ||||
| import Kategori from '../pages/Kategori.vue' | ||||
| import EditProduk from '../pages/EditProduk.vue' | ||||
| 
 | ||||
| 
 | ||||
| const routes = [ | ||||
| @ -45,12 +44,6 @@ const routes = [ | ||||
|     name: 'Kategori', | ||||
|     component: Kategori | ||||
|   }, | ||||
|   { | ||||
|     path: '/produk/:id/edit',   // :id = parameter dinamis
 | ||||
|     name: 'EditProduk', | ||||
|     component: EditProduk, | ||||
|     props: true                 // biar id bisa langsung jadi props di komponen
 | ||||
|   } | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user