Add core admin dashboard files (HTML, CSS, JS)
This commit is contained in:
parent
889578f180
commit
4d590fd822
9 changed files with 4663 additions and 0 deletions
543
admin-dashboard/src/js/admin.js
Normal file
543
admin-dashboard/src/js/admin.js
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
// Main Admin Dashboard Controller
|
||||
class AdminDashboard {
|
||||
constructor() {
|
||||
this.currentSection = "categories";
|
||||
this.productManager = null;
|
||||
this.categoryManager = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.initializeManagers();
|
||||
this.loadInitialData();
|
||||
|
||||
// Debug: Check if modal exists
|
||||
const modal = document.getElementById("product-modal");
|
||||
console.log("Product modal element exists:", !!modal);
|
||||
if (modal) {
|
||||
console.log("Modal current display:", modal.style.display);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Navigation
|
||||
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const button = e.currentTarget;
|
||||
this.switchSection(button.dataset.section);
|
||||
});
|
||||
});
|
||||
|
||||
// Header actions
|
||||
document.getElementById("preview-btn").addEventListener("click", () => {
|
||||
this.switchSection("preview");
|
||||
});
|
||||
|
||||
document.getElementById("sync-btn").addEventListener("click", () => {
|
||||
this.syncToMainSite();
|
||||
});
|
||||
|
||||
document.getElementById("quick-sync-btn").addEventListener("click", () => {
|
||||
this.quickSync();
|
||||
});
|
||||
|
||||
document.getElementById("logout-btn").addEventListener("click", () => {
|
||||
this.logout();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("undo-changes-btn")
|
||||
.addEventListener("click", () => {
|
||||
this.undoChanges();
|
||||
});
|
||||
|
||||
// Preview control buttons
|
||||
const catalogBtn = document.getElementById("preview-catalog");
|
||||
|
||||
if (catalogBtn) {
|
||||
catalogBtn.addEventListener("click", () => {
|
||||
this.loadPreview("catalog");
|
||||
});
|
||||
}
|
||||
|
||||
// Modal close handlers
|
||||
document.querySelectorAll(".close").forEach((closeBtn) => {
|
||||
closeBtn.addEventListener("click", (e) => {
|
||||
const modal = e.target.closest(".modal");
|
||||
modal.style.display = "none";
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.addEventListener("click", (e) => {
|
||||
if (
|
||||
e.target.classList.contains("modal") &&
|
||||
e.target === e.currentTarget
|
||||
) {
|
||||
e.target.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
this.closeAllModals();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeManagers() {
|
||||
this.productManager = new ProductManager(this);
|
||||
this.categoryManager = new CategoryManager(this);
|
||||
}
|
||||
|
||||
async loadInitialData() {
|
||||
try {
|
||||
await this.productManager.loadData();
|
||||
await this.categoryManager.loadData();
|
||||
|
||||
// Render categories immediately since Categories is the default active section
|
||||
this.categoryManager.renderCategories();
|
||||
|
||||
this.showMessage("Dashboard loaded successfully", "success");
|
||||
} catch (error) {
|
||||
this.showMessage(
|
||||
"Error loading dashboard data: " + error.message,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
switchSection(sectionName) {
|
||||
// Update navigation
|
||||
document.querySelectorAll(".nav-btn").forEach((btn) => {
|
||||
btn.classList.remove("active");
|
||||
});
|
||||
|
||||
// Only update nav button if it's a regular section
|
||||
if (sectionName !== "category-products") {
|
||||
const navBtn = document.querySelector(`[data-section="${sectionName}"]`);
|
||||
if (navBtn) {
|
||||
navBtn.classList.add("active");
|
||||
}
|
||||
}
|
||||
|
||||
// Update content - hide all sections first
|
||||
document.querySelectorAll(".content-section").forEach((section) => {
|
||||
section.classList.remove("active");
|
||||
section.style.display = "none";
|
||||
});
|
||||
|
||||
// Show the target section
|
||||
const targetSection = document.getElementById(`${sectionName}-section`);
|
||||
if (targetSection) {
|
||||
targetSection.classList.add("active");
|
||||
targetSection.style.display = "block";
|
||||
}
|
||||
|
||||
this.currentSection = sectionName;
|
||||
|
||||
// Load section-specific data
|
||||
this.loadSectionData(sectionName);
|
||||
}
|
||||
|
||||
loadSectionData(sectionName) {
|
||||
switch (sectionName) {
|
||||
case "categories":
|
||||
this.categoryManager.renderCategories();
|
||||
break;
|
||||
case "products":
|
||||
this.productManager.renderProducts();
|
||||
break;
|
||||
case "preview":
|
||||
this.loadPreview();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
loadPreview(type = "catalog") {
|
||||
const iframe = document.getElementById("preview-frame");
|
||||
const timestamp = new Date().getTime();
|
||||
|
||||
if (type === "catalog") {
|
||||
iframe.src = `../preview/catalog.html?t=${timestamp}`;
|
||||
} else if (type === "detail") {
|
||||
iframe.src = `../preview/detail.html?t=${timestamp}`;
|
||||
}
|
||||
}
|
||||
|
||||
async syncToMainSite() {
|
||||
const syncModal = document.getElementById("sync-modal");
|
||||
syncModal.style.display = "block";
|
||||
|
||||
try {
|
||||
// Step 1: Copy admin dashboard data to main website
|
||||
if (this.productManager) {
|
||||
await this.copyDataToMainWebsite();
|
||||
}
|
||||
|
||||
// Step 2: Proceed with main site sync
|
||||
await this.simulateSyncProcess();
|
||||
this.showMessage("Successfully synced to main site!", "success");
|
||||
} catch (error) {
|
||||
this.showMessage("Sync failed: " + error.message, "error");
|
||||
} finally {
|
||||
syncModal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
async quickSync() {
|
||||
try {
|
||||
console.log("⚡ QUICK SYNC STARTED");
|
||||
|
||||
// Get the current data from localStorage backup
|
||||
const backupData = localStorage.getItem("khy_products_backup");
|
||||
if (!backupData) {
|
||||
this.showMessage(
|
||||
"No changes to sync. Save a product first!",
|
||||
"warning"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(backupData);
|
||||
console.log("📦 Syncing data:", data.products.length, "products");
|
||||
|
||||
// Create a simple copy command for the user
|
||||
const dataStr = JSON.stringify(data, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: "application/json" });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
|
||||
// Create download link
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = "products_sync.json";
|
||||
downloadLink.style.display = "none";
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Show instructions
|
||||
this.showMessage(
|
||||
"File downloaded! Copy it to data/products.json",
|
||||
"info"
|
||||
);
|
||||
console.log("📋 QUICK SYNC INSTRUCTIONS:");
|
||||
console.log("1. File 'products_sync.json' has been downloaded");
|
||||
console.log("2. Copy it to replace: data/products.json");
|
||||
console.log("3. Refresh the preview to see changes");
|
||||
} catch (error) {
|
||||
console.error("❌ Quick sync failed:", error);
|
||||
this.showMessage("Quick sync failed: " + error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async copyDataToMainWebsite() {
|
||||
try {
|
||||
console.log("🔄 COPYING DATA TO MAIN WEBSITE");
|
||||
|
||||
// Get the current data from localStorage backup
|
||||
const backupData = localStorage.getItem("khy_products_backup");
|
||||
if (!backupData) {
|
||||
console.log("⚠️ No backup data found in localStorage");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(backupData);
|
||||
console.log("📦 Data to copy:", data.products.length, "products");
|
||||
|
||||
// Create a simple file copy using Node.js script
|
||||
// This will be handled by the sync-to-main.js script
|
||||
console.log("✅ Data ready for sync - will be processed by sync script");
|
||||
} catch (error) {
|
||||
console.error("❌ Error preparing data for sync:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async syncTemporaryChangesToLocal() {
|
||||
try {
|
||||
// Check if there are temporary changes to sync
|
||||
const temporaryChanges =
|
||||
await window.storageManager.getTemporaryChanges();
|
||||
|
||||
if (
|
||||
temporaryChanges.products &&
|
||||
Object.keys(temporaryChanges.products).length > 0
|
||||
) {
|
||||
console.log("🔄 Syncing temporary changes to local files...");
|
||||
|
||||
// Handle image changes first
|
||||
if (temporaryChanges.imageChanges) {
|
||||
console.log("📸 Processing image changes...");
|
||||
await this.processImageChanges(temporaryChanges.imageChanges);
|
||||
}
|
||||
|
||||
// Apply changes to actual products array
|
||||
Object.values(temporaryChanges.products).forEach((tempProduct) => {
|
||||
const existingIndex = this.productManager.products.findIndex(
|
||||
(p) => p.id === tempProduct.id
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing product
|
||||
this.productManager.products[existingIndex] = {
|
||||
...this.productManager.products[existingIndex],
|
||||
...tempProduct,
|
||||
};
|
||||
} else {
|
||||
// Add new product
|
||||
this.productManager.products.push(tempProduct);
|
||||
}
|
||||
});
|
||||
|
||||
// Save to actual JSON file
|
||||
await this.productManager.saveData();
|
||||
|
||||
// Clear temporary storage
|
||||
await window.storageManager.clearAllTemporaryData();
|
||||
|
||||
console.log("✅ Temporary changes synced to local files successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error syncing temporary changes:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async processImageChanges(imageChanges) {
|
||||
console.log("📸 PROCESSING IMAGE CHANGES:");
|
||||
console.log("Image changes:", imageChanges);
|
||||
|
||||
for (const [productId, changes] of Object.entries(imageChanges)) {
|
||||
console.log(`📦 Processing images for product ${productId}:`, changes);
|
||||
|
||||
// Handle new image uploads
|
||||
if (changes.newImages && changes.newImages.length > 0) {
|
||||
console.log("📁 Uploading new images:", changes.newImages);
|
||||
for (const imageRef of changes.newImages) {
|
||||
// Get the actual file from IndexedDB
|
||||
const imageData = await window.storageManager.getImageFile(
|
||||
imageRef.imageId
|
||||
);
|
||||
if (imageData) {
|
||||
await this.uploadImageFile(imageData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deleted images
|
||||
if (changes.deletedImages && changes.deletedImages.length > 0) {
|
||||
console.log("🗑️ Deleting images:", changes.deletedImages);
|
||||
for (const deletedImage of changes.deletedImages) {
|
||||
await this.deleteImageFile(deletedImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async uploadImageFile(imageData) {
|
||||
console.log("📤 UPLOADING IMAGE FILE:", imageData.name);
|
||||
|
||||
// In a real implementation, you would:
|
||||
// 1. Create FormData with the file
|
||||
// 2. Upload to server/assets directory
|
||||
// 3. Return the new file path
|
||||
// 4. Handle file naming conflicts
|
||||
|
||||
// For now, simulate the upload process
|
||||
const newPath = `assets/images/products/uploaded/${Date.now()}_${
|
||||
imageData.name
|
||||
}`;
|
||||
console.log(`📁 File would be uploaded to: ${newPath}`);
|
||||
console.log(`📁 File size: ${imageData.size} bytes`);
|
||||
console.log(`📁 File type: ${imageData.type}`);
|
||||
|
||||
// TODO: Implement actual file upload
|
||||
// The actual file is in imageData.file
|
||||
return newPath;
|
||||
}
|
||||
|
||||
async deleteImageFile(deletedImage) {
|
||||
console.log("🗑️ DELETING IMAGE FILE:", deletedImage.path);
|
||||
|
||||
// In a real implementation, you would:
|
||||
// 1. Check if file exists
|
||||
// 2. Delete the file from assets directory
|
||||
// 3. Handle any errors gracefully
|
||||
|
||||
// For now, simulate the deletion process
|
||||
console.log(`🗑️ File would be deleted: ${deletedImage.path}`);
|
||||
|
||||
// TODO: Implement actual file deletion
|
||||
return true;
|
||||
}
|
||||
|
||||
async simulateSyncProcess() {
|
||||
const steps = [
|
||||
"sync-step-1",
|
||||
"sync-step-2",
|
||||
"sync-step-3",
|
||||
"sync-step-4",
|
||||
"sync-step-5",
|
||||
"sync-step-6",
|
||||
];
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = document.getElementById(steps[i]);
|
||||
step.classList.add("active");
|
||||
|
||||
// Simulate processing time
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
step.classList.remove("active");
|
||||
step.classList.add("completed");
|
||||
}
|
||||
|
||||
// In a real implementation, this would call the actual sync script
|
||||
console.log("Sync process completed");
|
||||
}
|
||||
|
||||
logout() {
|
||||
if (confirm("Are you sure you want to logout?")) {
|
||||
// In a real implementation, this would clear authentication
|
||||
window.location.href = "login.html";
|
||||
}
|
||||
}
|
||||
|
||||
undoChanges() {
|
||||
if (
|
||||
confirm(
|
||||
"Are you sure you want to undo all unsaved changes? This action cannot be undone."
|
||||
)
|
||||
) {
|
||||
// Call the productManager's undo method
|
||||
if (this.productManager) {
|
||||
this.productManager.undoTemporaryChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeAllModals() {
|
||||
document.querySelectorAll(".modal").forEach((modal) => {
|
||||
modal.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
showMessage(message, type = "info") {
|
||||
// Remove existing messages
|
||||
const existingMessages = document.querySelectorAll(".message");
|
||||
existingMessages.forEach((msg) => msg.remove());
|
||||
|
||||
// Create new message
|
||||
const messageDiv = document.createElement("div");
|
||||
messageDiv.className = `message ${type}`;
|
||||
messageDiv.textContent = message;
|
||||
|
||||
// Insert at the top of the main content
|
||||
const main = document.querySelector(".admin-main");
|
||||
main.insertBefore(messageDiv, main.firstChild);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
messageDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Utility method for other managers to use
|
||||
getData() {
|
||||
return {
|
||||
products: this.productManager.products,
|
||||
categories: this.categoryManager.categories,
|
||||
};
|
||||
}
|
||||
|
||||
// Utility method to refresh current section
|
||||
refreshCurrentSection() {
|
||||
this.loadSectionData(this.currentSection);
|
||||
}
|
||||
|
||||
async undoChanges() {
|
||||
const undoStack = JSON.parse(
|
||||
localStorage.getItem("khy_undo_stack") || "[]"
|
||||
);
|
||||
|
||||
if (undoStack.length === 0) {
|
||||
this.showMessage("No changes to undo.", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const lastChange = undoStack[undoStack.length - 1];
|
||||
const confirmMessage = `Undo last change: ${lastChange.action.toUpperCase()} "${
|
||||
lastChange.newData.name
|
||||
}"?\n\nThis action cannot be undone.`;
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
try {
|
||||
await this.performUndo(lastChange);
|
||||
|
||||
// Remove the undone change from the stack
|
||||
undoStack.pop();
|
||||
localStorage.setItem("khy_undo_stack", JSON.stringify(undoStack));
|
||||
|
||||
this.showMessage(
|
||||
`Successfully undid: ${lastChange.action} "${lastChange.newData.name}"`,
|
||||
"success"
|
||||
);
|
||||
|
||||
// Refresh the current view
|
||||
this.productManager.refreshCurrentView();
|
||||
} catch (error) {
|
||||
console.error("❌ ERROR DURING UNDO:", error);
|
||||
this.showMessage("Error during undo: " + error.message, "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async performUndo(changeEntry) {
|
||||
console.log("⏪ PERFORMING UNDO:", changeEntry);
|
||||
|
||||
if (changeEntry.action === "edit") {
|
||||
// Restore the original product data
|
||||
const index = this.productManager.products.findIndex(
|
||||
(p) => p.id === changeEntry.productId
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.productManager.products[index] = changeEntry.originalData;
|
||||
console.log("✅ Restored original product data");
|
||||
}
|
||||
} else if (changeEntry.action === "add") {
|
||||
// Remove the added product
|
||||
const index = this.productManager.products.findIndex(
|
||||
(p) => p.id === changeEntry.productId
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.productManager.products.splice(index, 1);
|
||||
console.log("✅ Removed added product");
|
||||
}
|
||||
}
|
||||
|
||||
// Save the changes
|
||||
await this.productManager.saveData();
|
||||
console.log("💾 Undo changes saved to file");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize dashboard when DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
window.adminDashboard = new AdminDashboard();
|
||||
});
|
||||
|
||||
// Global utility functions
|
||||
window.showMessage = (message, type) => {
|
||||
if (window.adminDashboard) {
|
||||
window.adminDashboard.showMessage(message, type);
|
||||
}
|
||||
};
|
||||
|
||||
window.refreshDashboard = () => {
|
||||
if (window.adminDashboard) {
|
||||
window.adminDashboard.refreshCurrentSection();
|
||||
}
|
||||
};
|
||||
459
admin-dashboard/src/js/categoryManager.js
Normal file
459
admin-dashboard/src/js/categoryManager.js
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
// Category Management Class
|
||||
class CategoryManager {
|
||||
constructor(dashboard) {
|
||||
this.dashboard = dashboard;
|
||||
this.categories = [];
|
||||
this.currentCategory = null;
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Add category button - removed from UI
|
||||
// document
|
||||
// .getElementById("add-category-btn")
|
||||
// .addEventListener("click", () => {
|
||||
// this.showCategoryModal();
|
||||
// });
|
||||
|
||||
// Category form submit
|
||||
document.getElementById("category-form").addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
this.saveCategory();
|
||||
});
|
||||
|
||||
// Cancel button in category modal
|
||||
document.addEventListener("click", (e) => {
|
||||
if (e.target.id === "cancel-category-btn") {
|
||||
e.preventDefault();
|
||||
this.hideCategoryModal();
|
||||
}
|
||||
});
|
||||
|
||||
// View Products buttons - use event delegation
|
||||
document.addEventListener("click", (e) => {
|
||||
if (e.target.matches("[data-category-id]")) {
|
||||
const categoryId = e.target.getAttribute("data-category-id");
|
||||
this.viewCategoryProducts(categoryId);
|
||||
}
|
||||
});
|
||||
|
||||
// Back to Categories button
|
||||
document.addEventListener("click", (e) => {
|
||||
if (e.target.id === "back-to-categories-btn") {
|
||||
this.backToHome();
|
||||
}
|
||||
});
|
||||
|
||||
// Add Product to Category button
|
||||
document.addEventListener("click", (e) => {
|
||||
if (e.target.id === "add-product-to-category-btn") {
|
||||
const categoryId = e.target.getAttribute("data-category-id");
|
||||
this.addProductToCategory(categoryId);
|
||||
}
|
||||
});
|
||||
|
||||
// Edit Product buttons in category view - use event delegation
|
||||
document.addEventListener("click", (e) => {
|
||||
console.log("Click detected on:", e.target);
|
||||
// Check if the clicked element or its parent has the edit-product action
|
||||
const editButton = e.target.closest("[data-action='edit-product']");
|
||||
if (editButton) {
|
||||
console.log("Edit product button clicked!");
|
||||
e.stopPropagation();
|
||||
const productId = editButton.getAttribute("data-product-id");
|
||||
console.log("Product ID:", productId);
|
||||
this.editProduct(productId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
const response = await fetch("../data/products.json");
|
||||
const data = await response.json();
|
||||
this.categories = data.categories || [];
|
||||
} catch (error) {
|
||||
console.error("Error loading category data:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
renderCategories() {
|
||||
const container = document.getElementById("categories-list");
|
||||
|
||||
if (this.categories.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="no-categories">
|
||||
<h3>No categories found</h3>
|
||||
<p>Add your first category to get started!</p>
|
||||
<button class="btn btn-primary" onclick="categoryManager.showCategoryModal()">Add Category</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = "";
|
||||
this.categories.forEach((category) => {
|
||||
const categoryCard = this.createCategoryCard(category);
|
||||
container.appendChild(categoryCard);
|
||||
});
|
||||
}
|
||||
|
||||
createCategoryCard(category) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "category-card";
|
||||
|
||||
// Count products in this category
|
||||
const productCount = this.dashboard.productManager.products.filter(
|
||||
(p) => p.category === category.id
|
||||
).length;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="category-header">
|
||||
<h3>${category.name}</h3>
|
||||
<span class="category-id">${category.id}</span>
|
||||
</div>
|
||||
<p class="category-description">${
|
||||
category.description || "No description"
|
||||
}</p>
|
||||
<div class="category-stats">
|
||||
<span class="product-count">${productCount} product${
|
||||
productCount !== 1 ? "s" : ""
|
||||
}</span>
|
||||
</div>
|
||||
<div class="category-actions">
|
||||
<button class="btn btn-sm btn-secondary" data-category-id="${
|
||||
category.id
|
||||
}">
|
||||
View Products
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
showCategoryModal(category = null) {
|
||||
const modal = document.getElementById("category-modal");
|
||||
const title = document.getElementById("category-modal-title");
|
||||
const form = document.getElementById("category-form");
|
||||
|
||||
this.currentCategory = category;
|
||||
|
||||
if (category) {
|
||||
title.textContent = "Edit Category";
|
||||
this.populateForm(category);
|
||||
} else {
|
||||
title.textContent = "Add Category";
|
||||
form.reset();
|
||||
}
|
||||
|
||||
modal.style.display = "block";
|
||||
}
|
||||
|
||||
hideCategoryModal() {
|
||||
document.getElementById("category-modal").style.display = "none";
|
||||
this.currentCategory = null;
|
||||
}
|
||||
|
||||
populateForm(category) {
|
||||
document.getElementById("category-id").value = category.id;
|
||||
document.getElementById("category-name").value = category.name;
|
||||
document.getElementById("category-description").value =
|
||||
category.description || "";
|
||||
}
|
||||
|
||||
async saveCategory() {
|
||||
const formData = new FormData(document.getElementById("category-form"));
|
||||
const categoryData = {
|
||||
id: formData.get("id"),
|
||||
name: formData.get("name"),
|
||||
description: formData.get("description"),
|
||||
};
|
||||
|
||||
try {
|
||||
// Validate required fields
|
||||
if (!categoryData.id || !categoryData.name) {
|
||||
throw new Error("Category ID and name are required");
|
||||
}
|
||||
|
||||
// Check for duplicate ID (only for new categories)
|
||||
if (!this.currentCategory) {
|
||||
const existingCategory = this.categories.find(
|
||||
(cat) => cat.id === categoryData.id
|
||||
);
|
||||
if (existingCategory) {
|
||||
throw new Error("A category with this ID already exists");
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add category
|
||||
const existingIndex = this.categories.findIndex(
|
||||
(cat) => cat.id === categoryData.id
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
this.categories[existingIndex] = {
|
||||
...this.categories[existingIndex],
|
||||
...categoryData,
|
||||
};
|
||||
} else {
|
||||
this.categories.push(categoryData);
|
||||
}
|
||||
|
||||
await this.saveData();
|
||||
this.renderCategories();
|
||||
this.hideCategoryModal();
|
||||
this.dashboard.showMessage("Category saved successfully!", "success");
|
||||
|
||||
// Update product manager's category select
|
||||
if (this.dashboard.productManager) {
|
||||
this.dashboard.productManager.populateCategorySelect();
|
||||
}
|
||||
} catch (error) {
|
||||
this.dashboard.showMessage(
|
||||
"Error saving category: " + error.message,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
editCategory(id) {
|
||||
const category = this.categories.find((cat) => cat.id === id);
|
||||
if (category) {
|
||||
this.showCategoryModal(category);
|
||||
}
|
||||
}
|
||||
|
||||
viewCategoryProducts(categoryId) {
|
||||
const category = this.categories.find((cat) => cat.id === categoryId);
|
||||
if (!category) return;
|
||||
|
||||
// Get products in this category
|
||||
const productsInCategory = this.dashboard.productManager.products.filter(
|
||||
(p) => p.category === categoryId
|
||||
);
|
||||
|
||||
// Switch to the dedicated category products section
|
||||
this.dashboard.switchSection("category-products");
|
||||
|
||||
// Render the category products
|
||||
this.showCategoryProductsScreen(category, productsInCategory);
|
||||
}
|
||||
|
||||
showCategoryProductsScreen(category, products) {
|
||||
console.log("showCategoryProductsScreen called with category:", category);
|
||||
console.log("Products in category:", products);
|
||||
|
||||
// Update the section title
|
||||
const title = document.getElementById("category-products-title");
|
||||
if (title) {
|
||||
title.textContent = `${category.name} Products`;
|
||||
}
|
||||
|
||||
// Update the product count
|
||||
const productCount = document.getElementById("category-product-count");
|
||||
if (productCount) {
|
||||
productCount.textContent = `${products.length} product${
|
||||
products.length !== 1 ? "s" : ""
|
||||
} in this category`;
|
||||
}
|
||||
|
||||
// Update the add product button
|
||||
const addButton = document.getElementById("add-product-to-category-btn");
|
||||
if (addButton) {
|
||||
addButton.setAttribute("data-category-id", category.id);
|
||||
}
|
||||
|
||||
// Update the products list
|
||||
const productsList = document.getElementById("category-products-list");
|
||||
if (productsList) {
|
||||
productsList.innerHTML =
|
||||
products.length === 0
|
||||
? '<p class="no-products">No products in this category yet. Click "Add Product to this Category" to get started!</p>'
|
||||
: products
|
||||
.map((product) => {
|
||||
console.log(
|
||||
"Rendering product:",
|
||||
product.name,
|
||||
"with ID:",
|
||||
product.id
|
||||
);
|
||||
return `
|
||||
<div class="product-card">
|
||||
<div class="product-info">
|
||||
<h4>${product.name}</h4>
|
||||
<p class="product-description">${
|
||||
product.description || "No description"
|
||||
}</p>
|
||||
</div>
|
||||
<div class="product-actions">
|
||||
<button class="btn btn-sm btn-primary" data-action="edit-product" data-product-id="${
|
||||
product.id
|
||||
}">
|
||||
✏️ Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
}
|
||||
|
||||
backToHome() {
|
||||
// Switch back to home section
|
||||
this.dashboard.switchSection("categories");
|
||||
}
|
||||
|
||||
setupModalEventListeners() {
|
||||
const modal = document.getElementById("products-modal");
|
||||
if (!modal) return;
|
||||
|
||||
// Close modal
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (
|
||||
e.target.matches("[data-action='close-modal']") ||
|
||||
e.target.classList.contains("modal")
|
||||
) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Add product to category
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target.matches("[data-action='add-product']")) {
|
||||
const categoryId = e.target.getAttribute("data-category-id");
|
||||
this.addProductToCategory(categoryId);
|
||||
}
|
||||
});
|
||||
|
||||
// View product details
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target.matches("[data-action='view-product']")) {
|
||||
const productId = e.target.getAttribute("data-product-id");
|
||||
this.viewProductDetails(productId);
|
||||
}
|
||||
});
|
||||
|
||||
// Edit product
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target.matches("[data-action='edit-product']")) {
|
||||
e.stopPropagation();
|
||||
const productId = e.target.getAttribute("data-product-id");
|
||||
this.editProduct(productId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addProductToCategory(categoryId) {
|
||||
// Show the product modal directly without switching sections
|
||||
this.dashboard.productManager.showProductModal();
|
||||
|
||||
// Pre-select the category in the form after the modal is shown
|
||||
setTimeout(() => {
|
||||
const categorySelect = document.getElementById("product-category");
|
||||
if (categorySelect) {
|
||||
categorySelect.value = categoryId;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
viewProductDetails(productId) {
|
||||
// Close the products modal immediately
|
||||
const modal = document.getElementById("products-modal");
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
|
||||
// Switch to products section and show product details
|
||||
this.dashboard.switchSection("products");
|
||||
// You could implement a product details view here
|
||||
this.dashboard.showMessage(
|
||||
`Viewing product details for ID: ${productId}`,
|
||||
"info"
|
||||
);
|
||||
}
|
||||
|
||||
editProduct(productId) {
|
||||
console.log("editProduct called with ID:", productId);
|
||||
console.log("Dashboard productManager:", this.dashboard.productManager);
|
||||
// Edit the product directly (we're already in the products section)
|
||||
this.dashboard.productManager.editProduct(productId);
|
||||
}
|
||||
|
||||
async deleteCategory(id) {
|
||||
const category = this.categories.find((cat) => cat.id === id);
|
||||
if (!category) return;
|
||||
|
||||
// Check if category has products
|
||||
const productsInCategory = this.dashboard.productManager.products.filter(
|
||||
(p) => p.category === id
|
||||
);
|
||||
if (productsInCategory.length > 0) {
|
||||
this.dashboard.showMessage(
|
||||
`Cannot delete category "${category.name}" because it contains ${productsInCategory.length} product(s). Please move or delete the products first.`,
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure you want to delete the category "${category.name}"? This action cannot be undone.`
|
||||
)
|
||||
) {
|
||||
try {
|
||||
this.categories = this.categories.filter((cat) => cat.id !== id);
|
||||
await this.saveData();
|
||||
this.renderCategories();
|
||||
this.dashboard.showMessage("Category deleted successfully!", "success");
|
||||
|
||||
// Update product manager's category select
|
||||
if (this.dashboard.productManager) {
|
||||
this.dashboard.productManager.populateCategorySelect();
|
||||
}
|
||||
} catch (error) {
|
||||
this.dashboard.showMessage(
|
||||
"Error deleting category: " + error.message,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveData() {
|
||||
const data = {
|
||||
products: this.dashboard.productManager.products,
|
||||
categories: this.categories,
|
||||
};
|
||||
|
||||
// In a real implementation, this would save to a server
|
||||
console.log("Saving category data:", data);
|
||||
|
||||
// Simulate API call
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Utility method to get category by ID
|
||||
getCategoryById(id) {
|
||||
return this.categories.find((cat) => cat.id === id);
|
||||
}
|
||||
|
||||
// Utility method to get category name by ID
|
||||
getCategoryName(id) {
|
||||
const category = this.getCategoryById(id);
|
||||
return category ? category.name : id;
|
||||
}
|
||||
|
||||
// Utility method to validate category ID format
|
||||
validateCategoryId(id) {
|
||||
// Category IDs should be lowercase, hyphen-separated
|
||||
return /^[a-z0-9-]+$/.test(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Make CategoryManager available globally
|
||||
window.CategoryManager = CategoryManager;
|
||||
338
admin-dashboard/src/js/imageManager.js
Normal file
338
admin-dashboard/src/js/imageManager.js
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
// Image Management Class
|
||||
class ImageManager {
|
||||
constructor(dashboard) {
|
||||
this.dashboard = dashboard;
|
||||
this.images = [];
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Upload images button
|
||||
document
|
||||
.getElementById("upload-images-btn")
|
||||
.addEventListener("click", () => {
|
||||
this.showUploadModal();
|
||||
});
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
try {
|
||||
// In a real implementation, this would fetch from a server
|
||||
// For now, we'll collect images from products
|
||||
this.collectImagesFromProducts();
|
||||
} catch (error) {
|
||||
console.error("Error loading image data:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
collectImagesFromProducts() {
|
||||
this.images = [];
|
||||
const products = this.dashboard.productManager.products;
|
||||
|
||||
products.forEach((product) => {
|
||||
if (product.images && Array.isArray(product.images)) {
|
||||
product.images.forEach((imagePath) => {
|
||||
if (!this.images.find((img) => img.path === imagePath)) {
|
||||
this.images.push({
|
||||
path: imagePath,
|
||||
name: this.extractFileName(imagePath),
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
size: "Unknown", // Would be fetched from server
|
||||
uploadDate: "Unknown", // Would be fetched from server
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
extractFileName(path) {
|
||||
return path.split("/").pop();
|
||||
}
|
||||
|
||||
renderImages() {
|
||||
const container = document.getElementById("image-manager");
|
||||
|
||||
if (this.images.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="no-images">
|
||||
<h3>No images found</h3>
|
||||
<p>Upload images to get started!</p>
|
||||
<button class="btn btn-primary" onclick="imageManager.showUploadModal()">Upload Images</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = "";
|
||||
this.images.forEach((image) => {
|
||||
const imageItem = this.createImageItem(image);
|
||||
container.appendChild(imageItem);
|
||||
});
|
||||
}
|
||||
|
||||
createImageItem(image) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "image-item";
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="image-preview">
|
||||
<img src="${image.path}" alt="${image.name}" onerror="this.src='data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"200\" height=\"150\" viewBox=\"0 0 200 150\"><rect width=\"200\" height=\"150\" fill=\"%23f0f0f0\"/><text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" fill=\"%23999\">Image Not Found</text></svg>'">
|
||||
</div>
|
||||
<div class="image-item-info">
|
||||
<div class="image-item-name" title="${image.name}">${image.name}</div>
|
||||
<div class="image-meta">
|
||||
<span class="image-product">Product: ${image.productName}</span>
|
||||
<span class="image-size">Size: ${image.size}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-item-actions">
|
||||
<button class="btn btn-sm btn-primary" onclick="imageManager.viewImage('${image.path}')">
|
||||
👁️ View
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="imageManager.renameImage('${image.path}')">
|
||||
✏️ Rename
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="imageManager.deleteImage('${image.path}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return item;
|
||||
}
|
||||
|
||||
showUploadModal() {
|
||||
// Create upload modal if it doesn't exist
|
||||
let uploadModal = document.getElementById("upload-modal");
|
||||
if (!uploadModal) {
|
||||
uploadModal = this.createUploadModal();
|
||||
document.body.appendChild(uploadModal);
|
||||
}
|
||||
|
||||
uploadModal.style.display = "block";
|
||||
}
|
||||
|
||||
createUploadModal() {
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "upload-modal";
|
||||
modal.className = "modal";
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Upload Images</h3>
|
||||
<span class="close" onclick="this.closest('.modal').style.display='none'">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="upload-area" id="upload-area">
|
||||
<div class="upload-content">
|
||||
<div class="upload-icon">📤</div>
|
||||
<h4>Drag & Drop images here</h4>
|
||||
<p>or click to select files</p>
|
||||
<input type="file" id="file-input" multiple accept="image/*" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-preview" id="upload-preview"></div>
|
||||
<div class="upload-actions">
|
||||
<button class="btn btn-secondary" onclick="this.closest('.modal').style.display='none'">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="imageManager.processUpload()">Upload Images</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Setup drag and drop
|
||||
const uploadArea = modal.querySelector("#upload-area");
|
||||
const fileInput = modal.querySelector("#file-input");
|
||||
|
||||
uploadArea.addEventListener("click", () => fileInput.click());
|
||||
uploadArea.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add("drag-over");
|
||||
});
|
||||
uploadArea.addEventListener("dragleave", () => {
|
||||
uploadArea.classList.remove("drag-over");
|
||||
});
|
||||
uploadArea.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove("drag-over");
|
||||
this.handleFileSelect(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
this.handleFileSelect(e.target.files);
|
||||
});
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
handleFileSelect(files) {
|
||||
const preview = document.getElementById("upload-preview");
|
||||
preview.innerHTML = "";
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
if (file.type.startsWith("image/")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const imageDiv = document.createElement("div");
|
||||
imageDiv.className = "upload-preview-item";
|
||||
imageDiv.innerHTML = `
|
||||
<img src="${e.target.result}" alt="${file.name}">
|
||||
<div class="upload-preview-info">
|
||||
<span class="upload-filename">${file.name}</span>
|
||||
<span class="upload-filesize">${this.formatFileSize(
|
||||
file.size
|
||||
)}</span>
|
||||
</div>
|
||||
<button class="remove-upload-btn" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
preview.appendChild(imageDiv);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
}
|
||||
|
||||
async processUpload() {
|
||||
const fileInput = document.getElementById("file-input");
|
||||
const files = fileInput.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
this.dashboard.showMessage("Please select files to upload", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.dashboard.showMessage("Uploading images...", "info");
|
||||
|
||||
const uploadedImages = await this.uploadImages(files);
|
||||
|
||||
// Add to images list
|
||||
uploadedImages.forEach((imagePath) => {
|
||||
const fileName = this.extractFileName(imagePath);
|
||||
this.images.push({
|
||||
path: imagePath,
|
||||
name: fileName,
|
||||
productId: null,
|
||||
productName: "Unassigned",
|
||||
size: "Unknown",
|
||||
uploadDate: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
this.renderImages();
|
||||
document.getElementById("upload-modal").style.display = "none";
|
||||
this.dashboard.showMessage(
|
||||
`Successfully uploaded ${uploadedImages.length} image(s)!`,
|
||||
"success"
|
||||
);
|
||||
} catch (error) {
|
||||
this.dashboard.showMessage(
|
||||
"Error uploading images: " + error.message,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadImages(files) {
|
||||
const uploadedImages = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
// In a real implementation, you'd upload to a server
|
||||
const filename = `uploaded_${Date.now()}_${file.name}`;
|
||||
const imagePath = `assets/images/products/${filename}`;
|
||||
uploadedImages.push(imagePath);
|
||||
|
||||
console.log(`Would upload: ${file.name} to ${imagePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return uploadedImages;
|
||||
}
|
||||
|
||||
viewImage(imagePath) {
|
||||
// Create image viewer modal
|
||||
const viewer = document.createElement("div");
|
||||
viewer.className = "modal image-viewer";
|
||||
viewer.innerHTML = `
|
||||
<div class="modal-content image-viewer-content">
|
||||
<div class="modal-header">
|
||||
<h3>Image Viewer</h3>
|
||||
<span class="close" onclick="this.closest('.modal').remove()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<img src="${imagePath}" alt="Full size image" style="max-width: 100%; max-height: 70vh; object-fit: contain;">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(viewer);
|
||||
viewer.style.display = "block";
|
||||
}
|
||||
|
||||
renameImage(imagePath) {
|
||||
const currentName = this.extractFileName(imagePath);
|
||||
const newName = prompt("Enter new name for the image:", currentName);
|
||||
|
||||
if (newName && newName !== currentName) {
|
||||
// In a real implementation, you'd rename the file on the server
|
||||
console.log(`Would rename ${imagePath} to ${newName}`);
|
||||
this.dashboard.showMessage(
|
||||
"Image rename functionality would be implemented here",
|
||||
"info"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteImage(imagePath) {
|
||||
if (
|
||||
confirm(
|
||||
"Are you sure you want to delete this image? This action cannot be undone."
|
||||
)
|
||||
) {
|
||||
try {
|
||||
// Check if image is used by any products
|
||||
const productsUsingImage =
|
||||
this.dashboard.productManager.products.filter(
|
||||
(product) => product.images && product.images.includes(imagePath)
|
||||
);
|
||||
|
||||
if (productsUsingImage.length > 0) {
|
||||
this.dashboard.showMessage(
|
||||
`Cannot delete image because it's used by ${productsUsingImage.length} product(s). Please remove it from the products first.`,
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from images list
|
||||
this.images = this.images.filter((img) => img.path !== imagePath);
|
||||
|
||||
// In a real implementation, you'd delete from server
|
||||
console.log(`Would delete image: ${imagePath}`);
|
||||
|
||||
this.renderImages();
|
||||
this.dashboard.showMessage("Image deleted successfully!", "success");
|
||||
} catch (error) {
|
||||
this.dashboard.showMessage(
|
||||
"Error deleting image: " + error.message,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make ImageManager available globally
|
||||
window.ImageManager = ImageManager;
|
||||
1490
admin-dashboard/src/js/productManager.js
Normal file
1490
admin-dashboard/src/js/productManager.js
Normal file
File diff suppressed because it is too large
Load diff
245
admin-dashboard/src/js/storageManager.js
Normal file
245
admin-dashboard/src/js/storageManager.js
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
class StorageManager {
|
||||
constructor() {
|
||||
this.dbName = "KHYAdminStorage";
|
||||
this.dbVersion = 1;
|
||||
this.db = null;
|
||||
this.initDB();
|
||||
}
|
||||
|
||||
async initDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Failed to open IndexedDB:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
console.log("✅ IndexedDB initialized successfully");
|
||||
resolve(this.db);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
|
||||
// Create stores for different types of data
|
||||
if (!db.objectStoreNames.contains("temporaryChanges")) {
|
||||
db.createObjectStore("temporaryChanges", { keyPath: "id" });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains("imageFiles")) {
|
||||
db.createObjectStore("imageFiles", { keyPath: "id" });
|
||||
}
|
||||
|
||||
console.log("✅ IndexedDB stores created");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveTemporaryChanges(changes) {
|
||||
if (!this.db) {
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(
|
||||
["temporaryChanges"],
|
||||
"readwrite"
|
||||
);
|
||||
const store = transaction.objectStore("temporaryChanges");
|
||||
|
||||
const changeData = {
|
||||
id: "current",
|
||||
data: changes,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const request = store.put(changeData);
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log("💾 Temporary changes saved to IndexedDB");
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Failed to save temporary changes:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getTemporaryChanges() {
|
||||
if (!this.db) {
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(["temporaryChanges"], "readonly");
|
||||
const store = transaction.objectStore("temporaryChanges");
|
||||
const request = store.get("current");
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result;
|
||||
if (result && result.data) {
|
||||
console.log("📦 Retrieved temporary changes from IndexedDB");
|
||||
resolve(result.data);
|
||||
} else {
|
||||
console.log("📦 No temporary changes found in IndexedDB");
|
||||
resolve({});
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Failed to get temporary changes:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveImageFile(imageId, file) {
|
||||
if (!this.db) {
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(["imageFiles"], "readwrite");
|
||||
const store = transaction.objectStore("imageFiles");
|
||||
|
||||
const imageData = {
|
||||
id: imageId,
|
||||
file: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: file.lastModified,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const request = store.put(imageData);
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log(`📁 Image file saved to IndexedDB: ${file.name}`);
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Failed to save image file:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getImageFile(imageId) {
|
||||
if (!this.db) {
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(["imageFiles"], "readonly");
|
||||
const store = transaction.objectStore("imageFiles");
|
||||
const request = store.get(imageId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result;
|
||||
if (result) {
|
||||
console.log(`📁 Retrieved image file from IndexedDB: ${result.name}`);
|
||||
resolve(result);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Failed to get image file:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async deleteImageFile(imageId) {
|
||||
if (!this.db) {
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(["imageFiles"], "readwrite");
|
||||
const store = transaction.objectStore("imageFiles");
|
||||
const request = store.delete(imageId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log(`🗑️ Image file deleted from IndexedDB: ${imageId}`);
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Failed to delete image file:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async clearAllTemporaryData() {
|
||||
if (!this.db) {
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(
|
||||
["temporaryChanges", "imageFiles"],
|
||||
"readwrite"
|
||||
);
|
||||
|
||||
const changeStore = transaction.objectStore("temporaryChanges");
|
||||
const imageStore = transaction.objectStore("imageFiles");
|
||||
|
||||
const changeRequest = changeStore.clear();
|
||||
const imageRequest = imageStore.clear();
|
||||
|
||||
let completed = 0;
|
||||
const total = 2;
|
||||
|
||||
const checkComplete = () => {
|
||||
completed++;
|
||||
if (completed === total) {
|
||||
console.log("🧹 All temporary data cleared from IndexedDB");
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
changeRequest.onsuccess = checkComplete;
|
||||
imageRequest.onsuccess = checkComplete;
|
||||
|
||||
changeRequest.onerror = () => reject(changeRequest.error);
|
||||
imageRequest.onerror = () => reject(imageRequest.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getAllImageFiles() {
|
||||
if (!this.db) {
|
||||
await this.initDB();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction(["imageFiles"], "readonly");
|
||||
const store = transaction.objectStore("imageFiles");
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log(
|
||||
`📁 Retrieved ${request.result.length} image files from IndexedDB`
|
||||
);
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error("Failed to get all image files:", request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.storageManager = new StorageManager();
|
||||
Loading…
Add table
Add a link
Reference in a new issue