Kasir/resources/js/pages/InputProduk.vue
2025-08-28 13:07:35 +07:00

385 lines
12 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>
<div class="p-6">
<p class="font-serif italic text-[25px] text-D">Produk Baru</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.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 class="mt-6">
<button
@click="submitForm"
:disabled="loading || !isFormValid"
class="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{{ loading ? 'Menyimpan...' : 'Simpan Produk' }}
</button>
</div>
</div>
<!-- Image Upload Section -->
<div class="flex-1">
<label class="block text-D mb-1">Foto</label>
<!-- Image Grid -->
<div class="grid grid-cols-3 gap-3">
<!-- Uploaded 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"
/>
<!-- Delete Button -->
<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-blue-400 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">
<!-- Upload Icon or Loading -->
<div v-if="!uploadLoading" class="w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center mx-auto mb-2 group-hover:bg-blue-700 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-blue-600 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>
<!-- Hidden File Input -->
<input
ref="fileInput"
type="file"
multiple
accept="image/jpeg,image/jpg,image/png"
@change="handleFileSelect"
class="hidden"
/>
<!-- Upload Info -->
<p class="text-xs text-gray-500 mt-2">Format: JPG, JPEG, PNG (Max: 2MB per file, Max: 6 foto)</p>
<!-- Error Message -->
<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>
</mainLayout>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
import axios from "axios";
import mainLayout from "../layouts/mainLayout.vue";
import InputField from "../components/InputField.vue";
import InputSelect from "../components/InputSelect.vue";
const form = ref({
nama: '',
kategori: '',
berat: 0,
kadar: 0,
harga_per_gram: 0,
harga_jual: 0,
});
const category = ref([
{ value: "cincin", label: "Cincin" },
{ value: "gelang", label: "Gelang" },
{ value: "kalung", label: "Kalung" },
{ value: "anting", label: "Anting" },
]);
const loading = ref(false);
const uploadLoading = ref(false);
const uploadedImages = ref([]);
const isDragging = ref(false);
const uploadError = ref('');
const fileInput = ref(null);
const userId = ref(1); // Sesuaikan dengan user yang login
const isFormValid = computed(() => {
return form.value.nama &&
form.value.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 loadExistingPhotos = async () => {
try {
const response = await axios.get(`/api/foto/${userId.value}`);
if (response.data && Array.isArray(response.data)) {
uploadedImages.value = response.data;
}
} catch (error) {
if (error.response?.status !== 404) {
console.error('Error loading existing photos:', error);
}
// 404 is expected when no photos exist yet
}
};
const triggerFileInput = () => {
if (!uploadLoading.value && uploadedImages.value.length < 6) {
fileInput.value?.click();
}
};
const handleFileSelect = (event) => {
const files = Array.from(event.target.files);
uploadFiles(files);
};
const handleDrop = (event) => {
event.preventDefault();
isDragging.value = false;
if (uploadLoading.value || uploadedImages.value.length >= 6) return;
const files = Array.from(event.dataTransfer.files);
uploadFiles(files);
};
const uploadFiles = async (files) => {
uploadError.value = '';
// Validate file count
if (uploadedImages.value.length + files.length > 6) {
uploadError.value = 'Maksimal 6 foto yang dapat diupload';
return;
}
// Validate file types and sizes
const validFiles = files.filter(file => {
const isValidType = ['image/jpeg', 'image/jpg', 'image/png'].includes(file.type);
const isValidSize = file.size <= 2 * 1024 * 1024; // 2MB
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);
formData.append('id_user', userId.value);
const response = await axios.post('/api/foto/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
uploadedImages.value.push(response.data);
}
// Clear file input
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 (imageId) => {
try {
await axios.delete(`/api/foto/hapus/${imageId}`);
uploadedImages.value = uploadedImages.value.filter(img => img.id !== imageId);
uploadError.value = '';
} catch (error) {
console.error('Delete error:', error);
uploadError.value = 'Gagal menghapus foto';
}
};
const submitForm = async () => {
if (!isFormValid.value) {
alert('Mohon lengkapi semua field yang diperlukan');
return;
}
loading.value = true;
try {
const response = await axios.post('/api/produk', {
...form.value,
id_user: userId.value
});
// Reset form
form.value = {
nama: '',
kategori: '',
berat: 0,
kadar: 0,
harga_per_gram: 0,
harga_jual: 0,
};
uploadedImages.value = [];
uploadError.value = '';
if (fileInput.value) {
fileInput.value.value = '';
}
alert('Produk berhasil disimpan!');
} catch (error) {
console.error('Submit error:', error);
if (error.response?.data?.errors) {
const errors = Object.values(error.response.data.errors).flat();
alert('Error: ' + errors.join(', '));
} else {
alert('Gagal menyimpan produk: ' + (error.response?.data?.message || error.message));
}
} finally {
loading.value = false;
}
};
const resetPhotos = async () => {
try {
await axios.delete(`/api/foto/reset/${userId.value}`);
uploadedImages.value = [];
} catch (error) {
console.error('Error resetting photos:', error);
}
};
// Load existing photos on component mount
onMounted(() => {
loadExistingPhotos();
});
// Clean up photos if user leaves without saving
onUnmounted(() => {
// Optional: You might want to clean up temporary photos here
// resetPhotos();
});
</script>