Kasir/resources/js/pages/EditProduk.vue
2025-10-16 14:13:32 +07:00

575 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 relative">
<!-- 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.prevent="removeImage(image.id)"
type="button"
: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"
class="relative aspect-square"
>
<div
@drop="handleDrop"
@dragover.prevent
@dragenter.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@click="toggleUploadMenu"
class="w-full h-full 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>
<!-- Dropdown Menu -->
<div
v-if="showUploadMenu"
class="absolute top-full left-0 mt-2 w-60 bg-white border border-gray-200 rounded-lg shadow-lg z-20"
>
<button
@click="triggerFileUpload"
class="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center space-x-3 border-b border-gray-100"
>
<svg
class="w-5 h-5 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
></path>
</svg>
<div>
<div class="font-medium text-gray-900">
Upload dari File
</div>
<div class="text-sm text-gray-500">
Pilih foto dari galeri
</div>
</div>
</button>
<button
@click="openCameraModal"
class="w-full px-4 py-3 text-left hover:bg-gray-50 flex items-center space-x-3"
>
<svg
class="w-5 h-5 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<div>
<div class="font-medium text-gray-900">
Ambil dari Kamera
</div>
<div class="text-sm text-gray-500">
Foto langsung dengan kamera
</div>
</div>
</button>
</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>
<!-- Overlay -->
<div
v-if="showUploadMenu"
@click="showUploadMenu = false"
class="fixed inset-0 z-10"
></div>
<!-- Camera Modal -->
<div
v-if="showCamera"
class="fixed inset-0 bg-black/75 flex items-center justify-center z-[9999]"
>
<div class="bg-white w-[480px] rounded-lg shadow-lg p-4 relative">
<video
ref="video"
autoplay
playsinline
class="w-full h-64 bg-black rounded"
></video>
<canvas ref="canvas" class="hidden"></canvas>
<div class="mt-3 flex justify-between">
<button
@click="closeCamera"
class="px-4 py-2 bg-gray-400 text-white rounded"
>
Batal
</button>
<button
@click="capturePhoto"
class="px-4 py-2 bg-blue-500 text-white rounded"
>
Ambil Foto
</button>
</div>
</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 showUploadMenu = ref(false);
const showCamera = ref(false);
const video = ref(null);
const canvas = ref(null);
let stream = null;
const openItemModal = ref(false);
const editedProduct = ref(null);
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 () => {
try {
const response = await axios.get("/api/kategori", {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
category.value = response.data.map((c) => ({ value: c.id, label: c.nama }));
} catch (error) {
console.error("Error loading categories:", error);
}
};
const loadProduk = async () => {
try {
const response = await axios.get(`/api/produk/edit/${productId}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
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,
};
} catch (error) {
console.error("Error loading product:", error);
}
};
const loadFoto = async () => {
try {
const response = await axios.get(`/api/foto`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
uploadedImages.value = response.data;
} catch (error) {
console.error("Error loading photos:", error);
uploadError.value = "Gagal memuat foto";
}
};
const toggleUploadMenu = () => {
if (!uploadLoading.value && uploadedImages.value.length < 6) {
showUploadMenu.value = !showUploadMenu.value;
}
};
const triggerFileUpload = () => {
showUploadMenu.value = false;
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 = '';
if (uploadedImages.value.length + files.length > 6) {
uploadError.value = 'Maksimal 6 foto yang dapat diupload';
return;
}
const validFiles = files.filter(file => {
const isValidType = ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type);
const isValidSize = file.size <= 2 * 1024 * 1024;
if (!isValidType) {
uploadError.value = 'Format file harus JPG, JPEG, atau PNG';
return false;
}
if (!isValidSize) {
uploadError.value = 'Ukuran file maksimal 2MB';
return false;
}
return true;
});
if (validFiles.length === 0) return;
uploadLoading.value = true;
try {
for (const file of validFiles) {
const formData = new FormData();
formData.append('foto', file);
const response = await axios.post('/api/foto', formData, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
'Content-Type': 'multipart/form-data',
},
});
uploadedImages.value.push(response.data);
}
if (fileInput.value) {
fileInput.value.value = '';
}
} catch (error) {
console.error('Upload error:', error);
uploadError.value = error.response?.data?.message || 'Gagal mengupload foto';
} finally {
uploadLoading.value = false;
}
};
const removeImage = async (id) => {
try {
await axios.delete(`/api/foto/${id}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
uploadedImages.value = uploadedImages.value.filter((i) => i.id !== id);
} catch {
uploadError.value = "Gagal menghapus foto";
}
};
const openCameraModal = async () => {
showUploadMenu.value = false;
showCamera.value = true;
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.value.srcObject = stream;
} catch (err) {
console.error("Gagal akses kamera:", err);
alert("Tidak bisa mengakses kamera, cek izin browser!");
closeCamera();
}
};
const closeCamera = () => {
showCamera.value = false;
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
};
const capturePhoto = () => {
const ctx = canvas.value.getContext("2d");
canvas.value.width = video.value.videoWidth;
canvas.value.height = video.value.videoHeight;
ctx.drawImage(video.value, 0, 0);
canvas.value.toBlob(async (blob) => {
if (!blob) return;
await uploadFiles([new File([blob], "camera_photo.png", { type: "image/png" })]);
closeCamera();
}, "image/png");
};
const submitForm = async () => {
loading.value = true;
try {
await axios.put(`/api/produk/${productId}`, form.value, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
router.push("/produk?message=Produk berhasil diperbarui");
} catch (err) {
console.error("Submit error:", err);
uploadError.value = err.response?.data?.message || "Gagal menyimpan produk";
} finally {
loading.value = false;
}
};
const closeItemModal = () => {
openItemModal.value = false;
editedProduct.value = null;
};
const back = () => {
router.push("/produk");
};
onMounted(async () => {
await loadKategori();
await loadProduk();
await loadFoto();
});
</script>