Files
uldp-degree-mangement-system/views/admin/service/edit.ejs

440 lines
14 KiB
Plaintext

<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Edit Service: <%= service.name %></h1>
<p class="text-muted mb-0">Update service information and settings</p>
</div>
<div>
<a href="/admin/service" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Services
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/service/<%= service.slug %>/edit" method="POST" id="editServiceForm">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--primary-dark);">
<i class="fas fa-edit me-2"></i>Service Information
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Service Name</label>
<input type="text" class="form-control" id="serviceName" name="name"
value="<%= service.name %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label fw-medium">
Slug
<small class="text-muted">(generated from name)</small>
<span id="slugAutoIndicator" class="badge bg-info ms-1" style="font-size: 0.7em;">EXISTING</span>
</label>
<div class="input-group">
<input type="text" class="form-control" id="serviceSlug" name="slug"
value="<%= service.slug %>" readonly>
<button type="button" class="btn btn-primary" id="generateSlugBtn" title="Generate slug from name">
<i class="fas fa-magic me-1"></i>Generate
</button>
</div>
<small class="form-text text-muted">URL-friendly version of the service name.</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Layout</label>
<select class="form-control" id="serviceLayout" name="layout">
<option value="left" <%= service.layout === 'left' ? 'selected' : '' %>>Left</option>
<option value="right" <%= service.layout === 'right' ? 'selected' : '' %>>Right</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-8">
<label class="form-label fw-medium">Image</label>
<div class="input-group">
<input type="text" class="form-control" id="serviceImage" name="image"
value="<%= service.image || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="serviceImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-4">
<div id="serviceImagePreview">
<% if (service.image) { %>
<img src="<%= getFullImageUrl(service.image) %>" class="img-thumbnail"
style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="serviceDescription" name="description"
rows="3" required><%= service.description %></textarea>
</div>
</div>
</div>
</div>
<!-- Bottom Buttons -->
<div class="bottom-buttons">
<a href="/admin/service" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>Cancel
</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Update Service
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let servicesData = []; // Will be populated for duplicate checking
document.addEventListener('DOMContentLoaded', function() {
// Load existing services data for duplicate checking
loadServicesData();
// Initialize form handlers
initializeFormHandlers();
});
async function loadServicesData() {
try {
const response = await fetch('/api/service');
const data = await response.json();
servicesData = data.services?.items || [];
} catch (error) {
console.error('Error loading services data:', error);
servicesData = [];
}
}
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('editServiceForm');
form.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Updating...';
try {
// Check for duplicate slug before submitting
const slug = document.getElementById('serviceSlug').value.trim();
const currentSlug = '<%= service.slug %>';
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
showError('Service with this slug already exists. Please generate a new slug.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
return;
}
this.submit();
} catch (error) {
console.error('Error updating service:', error);
showError('Failed to update service. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
}
});
// Initialize image upload buttons
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function() {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
// Image preview for service image
const serviceImageInput = document.getElementById('serviceImage');
if (serviceImageInput) {
serviceImageInput.addEventListener('input', function() {
updateImagePreview('serviceImagePreview', this.value);
});
}
// Generate slug from service name
const serviceNameInput = document.getElementById('serviceName');
const serviceSlugInput = document.getElementById('serviceSlug');
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
const generateSlugBtn = document.getElementById('generateSlugBtn');
if (serviceNameInput && serviceSlugInput && generateSlugBtn) {
// Generate slug button
generateSlugBtn.addEventListener('click', async function() {
const serviceName = serviceNameInput.value.trim();
if (serviceName) {
// Show loading state
const originalBtnHtml = this.innerHTML;
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Generating...';
try {
const slug = await generateSlugFromText(serviceName);
const currentSlug = '<%= service.slug %>';
// Check for duplicate slug (excluding current service)
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
const uniqueSlug = generateUniqueSlug(slug);
serviceSlugInput.value = uniqueSlug;
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
} else {
serviceSlugInput.value = slug;
showSuccess('Slug generated successfully!');
}
if (slugAutoIndicator) {
slugAutoIndicator.textContent = 'GENERATED';
slugAutoIndicator.className = 'badge bg-success ms-1';
slugAutoIndicator.style.fontSize = '0.7em';
}
} catch (error) {
console.error('Error generating slug:', error);
showError('Failed to generate slug. Please try again.');
} finally {
// Restore button state
this.disabled = false;
this.innerHTML = originalBtnHtml;
}
} else {
showError('Please enter a service name first.');
}
});
}
}
// Generate slug using backend API
async function generateSlugFromText(text) {
try {
const response = await fetch('/admin/service/generate-slug', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: text })
});
const result = await response.json();
if (result.success) {
return result.slug;
} else {
throw new Error(result.message || 'Failed to generate slug');
}
} catch (error) {
console.error('Error generating slug:', error);
// Fallback to simple slug generation if API fails
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '');
}
}
// Check if slug already exists
function isSlugDuplicate(slug, excludeIndex = -1) {
return servicesData.some((service, index) => {
return service && service.slug === slug && index !== excludeIndex;
});
}
// Generate unique slug by appending number
function generateUniqueSlug(baseSlug) {
let counter = 1;
let uniqueSlug = baseSlug;
while (isSlugDuplicate(uniqueSlug, -1)) {
uniqueSlug = baseSlug + '-' + counter;
counter++;
}
return uniqueSlug;
}
function updateImagePreview(previewId, imagePath) {
const preview = document.getElementById(previewId);
if (imagePath) {
const fullImageUrl = getFullImageUrlJS(imagePath);
preview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">`;
} else {
preview.innerHTML = '';
}
}
// Helper function để tạo full URL cho ảnh
function getFullImageUrlJS(imagePath) {
if (!imagePath) return '';
if (imagePath.startsWith('http')) {
return imagePath;
}
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
let imgSrc = imagePath;
if (!imgSrc.startsWith('/')) {
imgSrc = '/uploads/' + imgSrc;
}
return backendUrl + imgSrc;
}
async function openImageUploader(targetInput, imageType) {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return reject(new Error('No file selected'));
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch(`/admin/upload/image?imageType=service`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
const input = document.getElementById(targetInput);
if (!input) throw new Error('Target input not found');
input.value = result.path;
// Update preview
if (targetInput === 'serviceImage') {
updateImagePreview('serviceImagePreview', result.path);
}
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
resolve(result);
} catch (err) {
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
console.error('Upload error:', err);
showError('Upload failed: ' + (err.message || 'Unknown error'));
reject(err);
} finally {
fileInput.remove();
}
};
fileInput.click();
});
}
function showSuccess(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showWarning(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-warning alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showError(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
</script>
<style>
.bottom-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding: 15px 0;
border-top: 1px solid #dee2e6;
}
.card-header h5 {
color: var(--primary-dark);
}
</style>