updateweb
This commit is contained in:
@@ -525,11 +525,15 @@ function renderImageVariants() {
|
||||
// Edit product
|
||||
async function editProduct(id) {
|
||||
try {
|
||||
console.log("Loading product for edit, ID:", id);
|
||||
|
||||
const response = await fetch(`/api/admin/products/${id}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Product data loaded:", data);
|
||||
|
||||
if (data.success) {
|
||||
const product = data.product;
|
||||
|
||||
@@ -561,6 +565,7 @@ async function editProduct(id) {
|
||||
|
||||
// Load image variants and extract unique product images
|
||||
imageVariants = product.images || [];
|
||||
console.log("Loaded image variants:", imageVariants);
|
||||
|
||||
// Build productImages array from unique image URLs in variants
|
||||
const uniqueImages = {};
|
||||
@@ -579,33 +584,77 @@ async function editProduct(id) {
|
||||
renderImageVariants();
|
||||
|
||||
productModal.show();
|
||||
} else {
|
||||
showError(data.message || "Failed to load product");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load product:", error);
|
||||
showError("Failed to load product details");
|
||||
showError("Failed to load product details: " + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Save product
|
||||
async function saveProduct() {
|
||||
const id = document.getElementById("productId").value;
|
||||
const saveButton = document.getElementById("btnSaveProduct");
|
||||
|
||||
// Disable button and show loading state
|
||||
saveButton.disabled = true;
|
||||
const originalButtonHTML = saveButton.innerHTML;
|
||||
saveButton.innerHTML = '<i class="bi bi-hourglass-split"></i> Saving...';
|
||||
saveButton.style.opacity = "0.7";
|
||||
|
||||
// Get description from Quill editor
|
||||
const description = quillEditor.root.innerHTML;
|
||||
|
||||
// Prepare images array for backend with all new fields
|
||||
const images = imageVariants.map((variant, index) => ({
|
||||
image_url: variant.image_url,
|
||||
color_variant: variant.color_variant || null,
|
||||
color_code: variant.color_code || null,
|
||||
alt_text: variant.alt_text || document.getElementById("productName").value,
|
||||
display_order: index,
|
||||
is_primary: variant.is_primary || false,
|
||||
variant_price: variant.variant_price
|
||||
? parseFloat(variant.variant_price)
|
||||
: null,
|
||||
variant_stock: parseInt(variant.variant_stock) || 0,
|
||||
}));
|
||||
console.log("=== SAVE PRODUCT DEBUG ===");
|
||||
console.log("Product ID:", id);
|
||||
console.log("Image Variants:", imageVariants);
|
||||
console.log("Product Images:", productImages);
|
||||
|
||||
// Prepare images array - include BOTH product images AND color variants
|
||||
const images = [];
|
||||
|
||||
// First, add all color variants (they have color_variant/color_code)
|
||||
imageVariants.forEach((variant, index) => {
|
||||
images.push({
|
||||
image_url: variant.image_url,
|
||||
color_variant: variant.color_variant || null,
|
||||
color_code: variant.color_code || null,
|
||||
alt_text:
|
||||
variant.alt_text || document.getElementById("productName").value,
|
||||
display_order: index,
|
||||
is_primary:
|
||||
variant.is_primary || (index === 0 && imageVariants.length > 0),
|
||||
variant_price: variant.variant_price
|
||||
? parseFloat(variant.variant_price)
|
||||
: null,
|
||||
variant_stock: parseInt(variant.variant_stock) || 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Then add regular product images (without color variants)
|
||||
// Skip images that are already in imageVariants
|
||||
const variantUrls = imageVariants.map((v) => v.image_url);
|
||||
productImages.forEach((img, index) => {
|
||||
if (!variantUrls.includes(img.url)) {
|
||||
images.push({
|
||||
image_url: img.url,
|
||||
color_variant: null,
|
||||
color_code: null,
|
||||
alt_text:
|
||||
img.alt_text ||
|
||||
img.filename ||
|
||||
document.getElementById("productName").value,
|
||||
display_order: imageVariants.length + index,
|
||||
is_primary: images.length === 0, // First image overall is primary
|
||||
variant_price: null,
|
||||
variant_stock: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Prepared images array (variants + regular):", images);
|
||||
|
||||
const formData = {
|
||||
name: document.getElementById("productName").value,
|
||||
@@ -624,17 +673,41 @@ async function saveProduct() {
|
||||
images: images,
|
||||
};
|
||||
|
||||
console.log("Form Data:", formData);
|
||||
|
||||
// Validation
|
||||
if (!formData.name || !formData.price) {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonHTML;
|
||||
saveButton.style.opacity = "1";
|
||||
showError("Please fill in all required fields (Name and Price)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
imageVariants.length > 0 &&
|
||||
imageVariants.some((v) => !v.image_url || !v.color_variant)
|
||||
) {
|
||||
showError("All color variants must have an image and color name selected");
|
||||
// Validate that we have at least one image (either variant or regular)
|
||||
if (images.length === 0) {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonHTML;
|
||||
saveButton.style.opacity = "1";
|
||||
showError("Please add at least one product image");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only validate image URLs exist
|
||||
if (images.some((img) => !img.image_url)) {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonHTML;
|
||||
saveButton.style.opacity = "1";
|
||||
showError("All images must have a valid image URL");
|
||||
return;
|
||||
}
|
||||
|
||||
// If a color_variant name is provided, require color_code
|
||||
if (images.some((img) => img.color_variant && !img.color_code)) {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonHTML;
|
||||
saveButton.style.opacity = "1";
|
||||
showError("All color variants must have both a color name and color code");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -642,6 +715,8 @@ async function saveProduct() {
|
||||
const url = id ? `/api/admin/products/${id}` : "/api/admin/products";
|
||||
const method = id ? "PUT" : "POST";
|
||||
|
||||
console.log("Saving product:", method, url, formData);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
@@ -651,21 +726,58 @@ async function saveProduct() {
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
console.log("Response status:", response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Response error:", errorText);
|
||||
throw new Error(`Server error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Response data:", data);
|
||||
|
||||
if (data.success) {
|
||||
// Change button to success state briefly
|
||||
saveButton.innerHTML = '<i class="bi bi-check-circle-fill"></i> Saved!';
|
||||
saveButton.style.background = "#10b981";
|
||||
|
||||
// Show success toast
|
||||
showSuccess(
|
||||
id
|
||||
? "Product updated successfully! 🎉"
|
||||
: "Product created successfully! 🎉"
|
||||
? "✅ Product Updated Successfully! Changes are now live on your website."
|
||||
: "✅ Product Created Successfully! Now visible on your shop page."
|
||||
);
|
||||
productModal.hide();
|
||||
loadProducts();
|
||||
|
||||
// Wait a moment then close modal
|
||||
setTimeout(async () => {
|
||||
productModal.hide();
|
||||
|
||||
// Reload products to show updated data
|
||||
await loadProducts();
|
||||
|
||||
// Clear form for next use
|
||||
imageVariants = [];
|
||||
productImages = [];
|
||||
|
||||
// Reset button
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonHTML;
|
||||
saveButton.style.opacity = "1";
|
||||
saveButton.style.background = "";
|
||||
}, 1000);
|
||||
} else {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonHTML;
|
||||
saveButton.style.opacity = "1";
|
||||
showError(data.message || "Failed to save product");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save product:", error);
|
||||
showError("Failed to save product");
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalButtonHTML;
|
||||
saveButton.style.opacity = "1";
|
||||
showError(`Failed to save product: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,14 +953,18 @@ function formatDate(dateString) {
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
console.log("✅ SUCCESS:", message);
|
||||
showToast(message, "success");
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
console.error("❌ ERROR:", message);
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
console.log("📢 Showing toast:", type, message);
|
||||
|
||||
// Create toast container if it doesn't exist
|
||||
let container = document.getElementById("toastContainer");
|
||||
if (!container) {
|
||||
@@ -856,6 +972,7 @@ function showToast(message, type = "info") {
|
||||
container.id = "toastContainer";
|
||||
container.className = "toast-container";
|
||||
document.body.appendChild(container);
|
||||
console.log("Created toast container");
|
||||
}
|
||||
|
||||
// Create toast element
|
||||
@@ -883,6 +1000,7 @@ function showToast(message, type = "info") {
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
console.log("Toast appended to container");
|
||||
|
||||
// Auto remove after 4 seconds
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -84,19 +84,19 @@ function renderUsers(users) {
|
||||
${u.isactive ? "Active" : "Disabled"}</span></td>
|
||||
<td>${formatDate(u.createdat)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-info" onclick="editUser(${
|
||||
<button class="btn btn-sm btn-info" onclick="editUser('${escapeHtml(
|
||||
u.id
|
||||
})" title="Edit User">
|
||||
)}')" title="Edit User">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="showChangePassword(${
|
||||
<button class="btn btn-sm btn-warning" onclick="showChangePassword('${escapeHtml(
|
||||
u.id
|
||||
}, '${escapeHtml(u.name)}')" title="Change Password">
|
||||
)}', '${escapeHtml(u.name)}')" title="Change Password">
|
||||
<i class="bi bi-key"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser(${
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser('${escapeHtml(
|
||||
u.id
|
||||
}, '${escapeHtml(u.name)}')" title="Delete User">
|
||||
)}', '${escapeHtml(u.name)}')" title="Delete User">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
@@ -195,6 +195,8 @@ async function saveUser() {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(id ? "Updating user..." : "Creating user...");
|
||||
|
||||
try {
|
||||
const url = id ? `/api/admin/users/${id}` : "/api/admin/users";
|
||||
const method = id ? "PUT" : "POST";
|
||||
@@ -206,6 +208,8 @@ async function saveUser() {
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
hideLoading();
|
||||
|
||||
if (data.success) {
|
||||
showSuccess(
|
||||
id ? "User updated successfully" : "User created successfully"
|
||||
@@ -217,6 +221,7 @@ async function saveUser() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save user:", error);
|
||||
hideLoading();
|
||||
showError("Failed to save user");
|
||||
}
|
||||
}
|
||||
@@ -249,6 +254,8 @@ async function changePassword() {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading("Changing password...");
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${id}/password`, {
|
||||
method: "PUT",
|
||||
@@ -258,6 +265,8 @@ async function changePassword() {
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
hideLoading();
|
||||
|
||||
if (data.success) {
|
||||
showSuccess("Password changed successfully");
|
||||
passwordModal.hide();
|
||||
@@ -266,6 +275,7 @@ async function changePassword() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to change password:", error);
|
||||
hideLoading();
|
||||
showError("Failed to change password");
|
||||
}
|
||||
}
|
||||
@@ -278,12 +288,16 @@ async function deleteUser(id, name) {
|
||||
)
|
||||
return;
|
||||
|
||||
showLoading("Deleting user...");
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await response.json();
|
||||
hideLoading();
|
||||
|
||||
if (data.success) {
|
||||
showSuccess("User deleted successfully");
|
||||
loadUsers();
|
||||
@@ -292,6 +306,7 @@ async function deleteUser(id, name) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete user:", error);
|
||||
hideLoading();
|
||||
showError("Failed to delete user");
|
||||
}
|
||||
}
|
||||
@@ -332,9 +347,76 @@ function formatDate(dateString) {
|
||||
});
|
||||
}
|
||||
|
||||
// Custom notification system
|
||||
function showNotification(type, title, message) {
|
||||
const container = document.getElementById("notificationContainer");
|
||||
const notification = document.createElement("div");
|
||||
notification.className = `notification notification-${type}`;
|
||||
|
||||
const icon =
|
||||
type === "success"
|
||||
? '<i class="bi bi-check-circle-fill"></i>'
|
||||
: '<i class="bi bi-exclamation-circle-fill"></i>';
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="notification-icon">${icon}</div>
|
||||
<div class="notification-content">
|
||||
<div class="notification-title">${title}</div>
|
||||
<div class="notification-message">${message}</div>
|
||||
</div>
|
||||
<button class="notification-close" onclick="closeNotification(this)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(notification);
|
||||
|
||||
// Auto remove after 3 seconds
|
||||
setTimeout(() => {
|
||||
if (notification.parentElement) {
|
||||
notification.classList.add("removing");
|
||||
setTimeout(() => {
|
||||
if (notification.parentElement) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function closeNotification(button) {
|
||||
const notification = button.closest(".notification");
|
||||
notification.classList.add("removing");
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
alert(message);
|
||||
showNotification("success", "Success!", message);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
alert("Error: " + message);
|
||||
showNotification("error", "Error", message);
|
||||
}
|
||||
|
||||
// Loading overlay functions
|
||||
function showLoading(message = "Saving...") {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "loading-overlay";
|
||||
overlay.id = "loadingOverlay";
|
||||
overlay.innerHTML = `
|
||||
<div class="loading-spinner-container">
|
||||
<div class="loading-spinner-icon"></div>
|
||||
<div class="loading-spinner-text">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
const overlay = document.getElementById("loadingOverlay");
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user