Fix admin route access and backend configuration
- Added /admin redirect to login page in nginx config - Fixed backend server.js route ordering for proper admin handling - Updated authentication middleware and routes - Added user management routes - Configured PostgreSQL integration - Updated environment configuration
This commit is contained in:
582
Views/AdminProducts/Form.cshtml
Executable file
582
Views/AdminProducts/Form.cshtml
Executable file
@@ -0,0 +1,582 @@
|
||||
@model Product
|
||||
@{
|
||||
ViewData["Title"] = Model?.Id == null ? "Create Product" : "Edit Product";
|
||||
Layout = "_AdminLayout";
|
||||
}
|
||||
|
||||
<style>
|
||||
.form-check-input[type="checkbox"] {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: white;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-check-input[type="checkbox"]:checked {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.form-check-input[type="checkbox"]:checked::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.form-check-input[type="checkbox"]:hover {
|
||||
border-color: #28a745;
|
||||
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
@await Html.PartialAsync("_ImagePickerModal")
|
||||
@await Html.PartialAsync("_VariantManagerModal")
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">@ViewData["Title"]</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/admin/products/@(Model?.Id == null ? "create" : $"edit/{Model.Id}")">
|
||||
<div asp-validation-summary="All" class="text-danger mb-3"></div>
|
||||
<input type="hidden" name="Id" value="@Model?.Id" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label for="Name" class="form-label">Product Name *</label>
|
||||
<input type="text" class="form-control" id="Name" name="Name" value="@Model?.Name" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="SKU" class="form-label">SKU Code</label>
|
||||
<input type="text" class="form-control" id="SKU" name="SKU" value="@Model?.SKU"
|
||||
placeholder="e.g., AB-001, WASH-2024-01">
|
||||
<small class="form-text text-muted">Unique product identifier (leave empty to
|
||||
auto-generate)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ShortDescription" class="form-label">Short Description</label>
|
||||
<textarea class="form-control" id="ShortDescription" name="ShortDescription" rows="3"
|
||||
placeholder="Brief product description (shown in listings)">@Model?.ShortDescription</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Description" class="form-label">Full Description</label>
|
||||
<textarea class="form-control" id="Description" name="Description"
|
||||
rows="10">@Model?.Description</textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="Price" class="form-label">Selling Price *</label>
|
||||
<input type="number" step="0.01" class="form-control" id="Price" name="Price"
|
||||
value="@Model?.Price" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="CostPrice" class="form-label">Cost Price</label>
|
||||
<input type="number" step="0.01" class="form-control" id="CostPrice" name="CostPrice"
|
||||
value="@Model?.CostPrice" placeholder="Your cost">
|
||||
<small class="form-text text-muted">For profit margin calculation</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="StockQuantity" class="form-label">Stock Quantity</label>
|
||||
<input type="number" class="form-control" id="StockQuantity" name="StockQuantity"
|
||||
value="@(Model?.StockQuantity ?? 0)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="Category" class="form-label">Category</label>
|
||||
<input type="text" class="form-control" id="Category" name="Category"
|
||||
value="@Model?.Category" placeholder="e.g., Washi Tape, Stickers">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Product Color Variants</label>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>Color Variant System (NEW)</strong>
|
||||
<p class="text-muted small mb-0">Link specific images to color variants for better customer experience</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="openVariantManager()">
|
||||
<i class="bi bi-palette"></i> Manage Variants
|
||||
</button>
|
||||
</div>
|
||||
<div id="variantSummary" class="mt-2">
|
||||
<span class="badge bg-secondary">No variants</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="productVariantsData" name="ProductVariantsJson" value="" />
|
||||
</div>
|
||||
|
||||
<!-- Legacy color picker removed - Use "Manage Variants" button above -->
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Product Images</label>
|
||||
<div class="border rounded p-3" style="min-height: 200px;">
|
||||
<div id="imageGallery" class="d-flex flex-wrap gap-2" style="position: relative;">
|
||||
@if (Model?.Images != null && Model.Images.Any())
|
||||
{
|
||||
@for (int i = 0; i < Model.Images.Count; i++)
|
||||
{
|
||||
<div class="image-item position-relative" draggable="true" style="width: 80px; height: 80px; cursor: move;" data-image-url="@Model.Images[i]">
|
||||
<img src="@Model.Images[i]" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover; pointer-events: none;">
|
||||
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0"
|
||||
style="padding: 2px 6px; font-size: 0.7rem; z-index: 10;"
|
||||
onclick="removeImageElement(this)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
@if (i == 0)
|
||||
{
|
||||
<span class="badge bg-primary position-absolute bottom-0 start-0 m-1" style="font-size: 0.65rem;">Main</span>
|
||||
}
|
||||
<input type="hidden" name="Images" value="@Model.Images[i]">
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div id="uploadPlaceholder" class="text-center"
|
||||
style="display: @(Model?.Images == null || !Model.Images.Any() ? "block" : "none"); padding: 40px 0;">
|
||||
<i class="bi bi-image" style="font-size: 48px; color: #ccc;"></i>
|
||||
<p class="text-muted mt-2">No images uploaded</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="ImageUrl" name="ImageUrl" value="@Model?.ImageUrl">
|
||||
<button type="button" class="btn btn-primary btn-sm mt-2 w-100"
|
||||
onclick="openImagePicker(handleImagePickerSelection, 'multiple')">
|
||||
<i class="bi bi-images"></i> Select/Upload Images
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">Drag images to reorder. First image is the main display image.</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> <strong>Product Detail Page:</strong>
|
||||
<ul class="mb-0 mt-2" style="font-size:0.9rem;">
|
||||
<li>Main image and additional images will display in gallery</li>
|
||||
<li>SKU, price, stock, and color show in product info</li>
|
||||
<li>Short description appears below buttons</li>
|
||||
<li>Full description displays in expandable section</li>
|
||||
<li>Related products suggested based on category & views</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Product Settings</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="IsActive" name="IsActive" value="true"
|
||||
@(Model?.IsActive != false ? "checked" : "")>
|
||||
<label class="form-check-label" for="IsActive">
|
||||
<strong>Active</strong> - Product visible in shop
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="IsFeatured" name="IsFeatured" value="true"
|
||||
@(Model?.IsFeatured == true ? "checked" : "")>
|
||||
<label class="form-check-label" for="IsFeatured">
|
||||
<strong>Featured</strong> - Show in featured products section
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="IsTopSeller" name="IsTopSeller" value="true"
|
||||
@(Model?.IsTopSeller == true ? "checked" : "")>
|
||||
<label class="form-check-label" for="IsTopSeller">
|
||||
<strong>Top Seller</strong> - Show in top sellers section
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="/admin/products" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Save Product
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://cdn.ckeditor.com/ckeditor5/40.1.0/classic/ckeditor.js"></script>
|
||||
<script>
|
||||
let descriptionEditor;
|
||||
|
||||
// Initialize CKEditor for Description
|
||||
ClassicEditor
|
||||
.create(document.querySelector('#Description'), {
|
||||
toolbar: [
|
||||
'heading', '|',
|
||||
'bold', 'italic', '|',
|
||||
'link', 'bulletedList', 'numberedList', '|',
|
||||
'indent', 'outdent', '|',
|
||||
'blockQuote', 'insertTable', '|',
|
||||
'undo', 'redo'
|
||||
],
|
||||
heading: {
|
||||
options: [
|
||||
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
|
||||
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
|
||||
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
|
||||
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' }
|
||||
]
|
||||
}
|
||||
})
|
||||
.then(editor => {
|
||||
descriptionEditor = editor;
|
||||
|
||||
// Sync CKEditor data before form submission
|
||||
document.querySelector('form').addEventListener('submit', function (e) {
|
||||
document.querySelector('#Description').value = descriptionEditor.getData();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
let imageIndex = @(Model?.Images?.Count ?? 0);
|
||||
|
||||
// Handle Image Picker Selection
|
||||
function handleImagePickerSelection(selectedUrls) {
|
||||
const gallery = document.getElementById('imageGallery');
|
||||
const placeholder = document.getElementById('uploadPlaceholder');
|
||||
|
||||
if (selectedUrls.length > 0) {
|
||||
placeholder.style.display = 'none';
|
||||
|
||||
selectedUrls.forEach(imageUrl => {
|
||||
const imageDiv = document.createElement('div');
|
||||
imageDiv.className = 'image-item position-relative';
|
||||
imageDiv.draggable = true;
|
||||
imageDiv.style.width = '80px';
|
||||
imageDiv.style.height = '80px';
|
||||
imageDiv.style.cursor = 'move';
|
||||
imageDiv.setAttribute('data-image-url', imageUrl);
|
||||
|
||||
const isFirstImage = gallery.querySelectorAll('.image-item').length === 0;
|
||||
const mainBadge = isFirstImage ? '<span class="badge bg-primary position-absolute bottom-0 start-0 m-1" style="font-size: 0.65rem; z-index: 10;">Main</span>' : '';
|
||||
|
||||
imageDiv.innerHTML = `
|
||||
<img src="${imageUrl}" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover; pointer-events: none;">
|
||||
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0"
|
||||
style="padding: 2px 6px; font-size: 0.7rem; z-index: 10;"
|
||||
onclick="removeImageElement(this)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
${mainBadge}
|
||||
<input type="hidden" name="Images" value="${imageUrl}">
|
||||
`;
|
||||
|
||||
gallery.appendChild(imageDiv);
|
||||
imageIndex++;
|
||||
|
||||
// Set first image as main ImageUrl
|
||||
if (gallery.children.length === 1 || !document.getElementById('ImageUrl').value) {
|
||||
document.getElementById('ImageUrl').value = imageUrl;
|
||||
}
|
||||
});
|
||||
|
||||
// Re-initialize drag and drop for new elements
|
||||
setTimeout(() => initializeDragAndDrop(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImageUpload(event) {
|
||||
const files = event.target.files;
|
||||
const gallery = document.getElementById('imageGallery');
|
||||
const placeholder = document.getElementById('uploadPlaceholder');
|
||||
|
||||
if (files.length > 0) {
|
||||
placeholder.style.display = 'none';
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/upload/image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const imageUrl = result.url;
|
||||
|
||||
const imageDiv = document.createElement('div');
|
||||
imageDiv.className = 'image-item position-relative';
|
||||
imageDiv.draggable = true;
|
||||
imageDiv.style.width = '80px';
|
||||
imageDiv.style.height = '80px';
|
||||
imageDiv.style.cursor = 'move';
|
||||
imageDiv.setAttribute('data-image-url', imageUrl);
|
||||
|
||||
const isFirstImage = gallery.querySelectorAll('.image-item').length === 0;
|
||||
const mainBadge = isFirstImage ? '<span class="badge bg-primary position-absolute bottom-0 start-0 m-1" style="font-size: 0.65rem; z-index: 10;">Main</span>' : '';
|
||||
|
||||
imageDiv.innerHTML = `
|
||||
<img src="${imageUrl}" class="img-thumbnail" style="width: 100%; height: 100%; object-fit: cover; pointer-events: none;">
|
||||
<button type="button" class="btn btn-danger btn-sm position-absolute top-0 end-0"
|
||||
style="padding: 2px 6px; font-size: 0.7rem; z-index: 10;"
|
||||
onclick="removeImageElement(this)">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
${mainBadge}
|
||||
<input type="hidden" name="Images" value="${imageUrl}">
|
||||
`;
|
||||
|
||||
gallery.appendChild(imageDiv);
|
||||
imageIndex++;
|
||||
|
||||
// Set first image as main ImageUrl
|
||||
if (gallery.children.length === 1 || !document.getElementById('ImageUrl').value) {
|
||||
document.getElementById('ImageUrl').value = imageUrl;
|
||||
}
|
||||
} else {
|
||||
alert('Error uploading image: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error uploading image');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset file input
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
function removeImageElement(button) {
|
||||
const imageDiv = button.closest('.position-relative');
|
||||
const gallery = document.getElementById('imageGallery');
|
||||
const placeholder = document.getElementById('uploadPlaceholder');
|
||||
|
||||
imageDiv.remove();
|
||||
|
||||
// Show placeholder if no images left
|
||||
if (gallery.children.length === 0) {
|
||||
placeholder.style.display = 'block';
|
||||
document.getElementById('ImageUrl').value = '';
|
||||
} else {
|
||||
// Update main ImageUrl to first image if removed image was main
|
||||
const firstImage = gallery.querySelector('img');
|
||||
if (firstImage) {
|
||||
const currentMain = document.getElementById('ImageUrl').value;
|
||||
const allImages = Array.from(gallery.querySelectorAll('input[type="hidden"]')).map(input => input.value);
|
||||
if (!allImages.includes(currentMain)) {
|
||||
document.getElementById('ImageUrl').value = allImages[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage(index) {
|
||||
if (confirm('Remove this image?')) {
|
||||
const gallery = document.getElementById('imageGallery');
|
||||
const imageDiv = gallery.children[index];
|
||||
removeImageElement(imageDiv.querySelector('button'));
|
||||
}
|
||||
}
|
||||
|
||||
// Drag and Drop Functionality
|
||||
let draggedElement = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeDragAndDrop();
|
||||
});
|
||||
|
||||
function initializeDragAndDrop() {
|
||||
const gallery = document.getElementById('imageGallery');
|
||||
|
||||
gallery.addEventListener('dragstart', function(e) {
|
||||
if (e.target.classList.contains('image-item')) {
|
||||
draggedElement = e.target;
|
||||
e.target.classList.add('dragging');
|
||||
e.target.style.opacity = '0.5';
|
||||
}
|
||||
});
|
||||
|
||||
gallery.addEventListener('dragend', function(e) {
|
||||
if (e.target.classList.contains('image-item')) {
|
||||
e.target.classList.remove('dragging');
|
||||
e.target.style.opacity = '1';
|
||||
updateMainBadge();
|
||||
updateImageUrl();
|
||||
}
|
||||
});
|
||||
|
||||
gallery.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
const afterElement = getDragAfterElement(gallery, e.clientX, e.clientY);
|
||||
if (draggedElement) {
|
||||
if (afterElement == null) {
|
||||
gallery.appendChild(draggedElement);
|
||||
} else {
|
||||
gallery.insertBefore(draggedElement, afterElement);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
gallery.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function getDragAfterElement(container, x, y) {
|
||||
const draggableElements = [...container.querySelectorAll('.image-item:not(.dragging)')];
|
||||
|
||||
return draggableElements.reduce((closest, child) => {
|
||||
const box = child.getBoundingClientRect();
|
||||
const centerX = box.left + box.width / 2;
|
||||
const centerY = box.top + box.height / 2;
|
||||
|
||||
// Calculate distance from mouse to center of element
|
||||
const offsetX = x - centerX;
|
||||
const offsetY = y - centerY;
|
||||
|
||||
// For horizontal layout, primarily use X offset
|
||||
if (offsetX < 0 && (closest.offset === undefined || offsetX > closest.offset)) {
|
||||
return { offset: offsetX, element: child };
|
||||
} else {
|
||||
return closest;
|
||||
}
|
||||
}, { offset: undefined, element: null }).element;
|
||||
}
|
||||
|
||||
function updateMainBadge() {
|
||||
const gallery = document.getElementById('imageGallery');
|
||||
const images = gallery.querySelectorAll('.image-item');
|
||||
|
||||
images.forEach((item, index) => {
|
||||
// Remove existing main badge
|
||||
const existingBadge = item.querySelector('.badge');
|
||||
if (existingBadge) {
|
||||
existingBadge.remove();
|
||||
}
|
||||
|
||||
// Add main badge to first image
|
||||
if (index === 0) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-primary position-absolute bottom-0 start-0 m-1';
|
||||
badge.style.fontSize = '0.65rem';
|
||||
badge.textContent = 'Main';
|
||||
item.appendChild(badge);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateImageUrl() {
|
||||
const gallery = document.getElementById('imageGallery');
|
||||
const firstImage = gallery.querySelector('.image-item img');
|
||||
if (firstImage) {
|
||||
document.getElementById('ImageUrl').value = firstImage.src;
|
||||
}
|
||||
}
|
||||
|
||||
// Update drag functionality when new images are added
|
||||
const originalHandleImageUpload = handleImageUpload;
|
||||
handleImageUpload = async function(event) {
|
||||
await originalHandleImageUpload(event);
|
||||
setTimeout(() => {
|
||||
const newImages = document.querySelectorAll('.image-item');
|
||||
newImages.forEach(item => {
|
||||
if (!item.draggable) {
|
||||
item.draggable = true;
|
||||
item.style.cursor = 'move';
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Color selection toggle
|
||||
function toggleColorSelection(label) {
|
||||
const checkbox = label.previousElementSibling;
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
||||
// Update visual state
|
||||
if (checkbox.checked) {
|
||||
const color = checkbox.value;
|
||||
const checkIcon = color === "White" || color === "Yellow" ? "black" : "white";
|
||||
label.innerHTML = `<i class="bi bi-check-lg" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: ${checkIcon}; font-size: 1.5rem; font-weight: bold;"></i>`;
|
||||
label.style.transform = "scale(1.1)";
|
||||
} else {
|
||||
label.innerHTML = "";
|
||||
label.style.transform = "scale(1)";
|
||||
}
|
||||
}
|
||||
|
||||
// Open Variant Manager
|
||||
function openVariantManager() {
|
||||
// Get all product images
|
||||
const productImages = Array.from(document.querySelectorAll('#imageGallery .image-item img'))
|
||||
.map(img => img.src);
|
||||
|
||||
// Load existing variants
|
||||
const variantsJson = document.getElementById('productVariantsData').value;
|
||||
const existingVariants = variantsJson ? JSON.parse(variantsJson) : [];
|
||||
|
||||
// Initialize variant manager
|
||||
initVariantManager(existingVariants, productImages);
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('variantManagerModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load existing variants if editing
|
||||
@if (Model?.Variants != null && Model.Variants.Any())
|
||||
{
|
||||
<text>
|
||||
const existingVariants = @Html.Raw(Json.Serialize(Model.Variants));
|
||||
document.getElementById('productVariantsData').value = JSON.stringify(existingVariants);
|
||||
productVariants = existingVariants;
|
||||
updateVariantSummary();
|
||||
</text>
|
||||
}
|
||||
|
||||
// Debug form submission
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
const variantData = document.getElementById('productVariantsData').value;
|
||||
console.log('=== FORM SUBMISSION DEBUG ===');
|
||||
console.log('ProductVariantsJson field value:', variantData);
|
||||
console.log('Is empty?', !variantData || variantData === '');
|
||||
console.log('productVariants array:', productVariants);
|
||||
console.log('============================');
|
||||
|
||||
if (!variantData || variantData === '' || variantData === '[]') {
|
||||
console.warn('WARNING: No variant data in form! Did you click "Apply Changes" in Variant Manager?');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
109
Views/AdminProducts/Index.cshtml
Executable file
109
Views/AdminProducts/Index.cshtml
Executable file
@@ -0,0 +1,109 @@
|
||||
@model List<Product>
|
||||
@{
|
||||
ViewData["Title"] = "Manage Products";
|
||||
Layout = "_AdminLayout";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">All Products (@Model.Count)</h5>
|
||||
<a href="/admin/products/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Add New Product
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Price</th>
|
||||
<th>Stock</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var product in Model.OrderByDescending(p => p.CreatedAt))
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(product.ImageUrl))
|
||||
{
|
||||
<img src="@product.ImageUrl" alt="@product.Name" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="width: 50px; height: 50px; background: #e0e0e0; border-radius: 4px;"></div>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<strong>@product.Name</strong>
|
||||
@if (product.IsFeatured)
|
||||
{
|
||||
<span class="badge bg-warning text-dark ms-1">Featured</span>
|
||||
}
|
||||
@if (product.IsTopSeller)
|
||||
{
|
||||
<span class="badge bg-success ms-1">Top Seller</span>
|
||||
}
|
||||
</td>
|
||||
<td>@product.Category</td>
|
||||
<td>$@product.Price.ToString("F2")</td>
|
||||
<td>@product.StockQuantity</td>
|
||||
<td>
|
||||
@if (product.IsActive)
|
||||
{
|
||||
<span class="badge bg-success"><i class="bi bi-check-circle-fill"></i> Active</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger"><i class="bi bi-x-circle-fill"></i> Inactive</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="/admin/products/edit/@product.Id" class="btn btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button onclick="deleteProduct('@product.Id', '@product.Name')" class="btn btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-center text-muted my-5">No products found. Create your first product!</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function deleteProduct(id, name) {
|
||||
if (confirm(`Are you sure you want to delete "${name}"?`)) {
|
||||
fetch(`/admin/products/delete/${id}`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error deleting product');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user