Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into fea/thanh-02022026-news

This commit is contained in:
Wini_Fy
2026-02-04 09:22:24 +07:00
41 changed files with 7863 additions and 570 deletions

View File

@@ -0,0 +1,791 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit content displayed on Appointment page</p>
</div>
<div>
<a href="<%= frontendUrl %>/make-appointment/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Appointment Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="appointmentForm"
action="/admin/appointment/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="visaOptions" id="visaOptionsJson">
<input type="hidden" name="form" id="formJson">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#visaOptions" role="tab">
<i class="fas fa-passport me-2"></i>Visa Options
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#form" role="tab">
<i class="fas fa-envelope me-2"></i>Form
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#submissions" role="tab">
<i class="fas fa-list me-2"></i>Submissions
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Hero Section</h6>
<div class="row g-3">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="appointment">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px;">
<% if (data.hero?.backgroundImage) { %>
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
!heroImgSrc.startsWith('http://') &&
!heroImgSrc.startsWith('https://')) {
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
heroImgSrc; } %>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="heroSubtitle"
name="heroSubtitle" value="<%= data.hero?.subtitle || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="heroHeading"
name="heroHeading" value="<%= data.hero?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="heroDescription"
name="heroDescription"
rows="2"><%= data.hero?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Visa Options Tab -->
<div class="tab-pane fade" id="visaOptions" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Visa Options</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addVisaOption()">
<i class="fas fa-plus"></i> Add Option
</button>
</div>
<p class="text-muted small">These options will appear in the visa type selection
dropdown on the appointment form.</p>
<div id="visaOptionsContainer">
<% if (data.visaOptions && data.visaOptions.length> 0) { %>
<% data.visaOptions.forEach((option, index)=> { %>
<div class="input-group mb-2 visa-option-item">
<span class="input-group-text"><i
class="fas fa-passport"></i></span>
<input type="text" class="form-control visa-option-input"
value="<%= option %>" placeholder="Enter visa option">
<button type="button" class="btn btn-outline-danger"
onclick="removeVisaOption(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Form Tab -->
<div class="tab-pane fade" id="form" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Form Settings</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Form Heading</label>
<input type="text" class="form-control" id="formHeading"
value="<%= data.form?.heading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Submit Button Text</label>
<input type="text" class="form-control" id="formSubmitButtonText"
value="<%= data.form?.submitButton?.text || 'Request Appointment' %>">
</div>
<!-- Hidden fields for submitButton icon and buttonClass -->
<input type="hidden" id="formSubmitButtonIcon"
value="<%= data.form?.submitButton?.icon || 'fa-solid fa-arrow-right' %>">
<input type="hidden" id="formSubmitButtonClass"
value="<%= data.form?.submitButton?.buttonClass || 'theme-btn' %>">
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Form Fields</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addFormField()">
<i class="fas fa-plus"></i> Add Field
</button>
</div>
<div id="formFieldsContainer">
<% if (data.form?.fields && data.form.fields.length> 0) { %>
<% data.form.fields.forEach((field, index)=> { %>
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Field Name</label>
<input type="text"
class="form-control field-name-input"
value="<%= field.name || '' %>"
placeholder="e.g., name">
</div>
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text"
class="form-control field-label-input"
value="<%= field.label || '' %>"
placeholder="e.g., Your Name">
</div>
<div class="col-md-2">
<label class="form-label">Type</label>
<select class="form-select field-type-select">
<option value="text" <%=field.type==='text'
? 'selected' : '' %>>Text</option>
<option value="email" <%=field.type==='email'
? 'selected' : '' %>>Email</option>
<option value="tel" <%=field.type==='tel'
? 'selected' : '' %>>Phone</option>
<option value="textarea"
<%=field.type==='textarea' ? 'selected' : ''
%>>Textarea</option>
<option value="date" <%=field.type==='date'
? 'selected' : '' %>>Date</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Col Class</label>
<select class="form-select field-col-select">
<option value="col-lg-4"
<%=field.colClass==='col-lg-4' ? 'selected'
: '' %>>1/3 Width</option>
<option value="col-lg-6"
<%=field.colClass==='col-lg-6' ? 'selected'
: '' %>>1/2 Width</option>
<option value="col-lg-12"
<%=field.colClass==='col-lg-12' ? 'selected'
: '' %>>Full Width</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input
class="form-check-input field-required-check"
type="checkbox" <%=field.required
? 'checked' : '' %>>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text"
class="form-control field-placeholder-input"
value="<%= field.placeholder || '' %>">
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Submissions Tab -->
<div class="tab-pane fade" id="submissions" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Recent Submissions</h6>
</div>
<!-- Date Filter -->
<div class="row g-2 mb-4 align-items-end" id="filterContainer">
<input type="hidden" id="filterTab" value="submissions">
<div class="col-md-3">
<label class="form-label small text-muted">Start Date</label>
<input type="date" class="form-control form-control-sm"
id="filterStartDate" value="<%= locals.startDate || '' %>">
</div>
<div class="col-md-3">
<label class="form-label small text-muted">End Date</label>
<input type="date" class="form-control form-control-sm"
id="filterEndDate" value="<%= locals.endDate || '' %>">
</div>
<div class="col-md-3">
<button type="button" class="btn btn-sm btn-primary w-100"
onclick="applyDateFilter()">
<i class="fas fa-filter me-1"></i> Filter
</button>
</div>
<div class="col-md-2">
<a href="/admin/appointment?tab=submissions"
class="btn btn-sm btn-outline-secondary w-100">
<i class="fas fa-times me-1"></i> Clear
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Name</th>
<th>Contact</th>
<th>Appt Date</th>
<th>Visa Types</th>
<th>Message</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<% if (locals.submissions && submissions.length> 0) { %>
<% submissions.forEach(submission=> { %>
<tr>
<td>
<%= new
Date(submission.createdAt).toLocaleDateString()
%>
<br>
<small class="text-muted">
<%= new
Date(submission.createdAt).toLocaleTimeString([],
{hour: '2-digit' , minute:'2-digit'}) %>
</small>
</td>
<td>
<%= submission.name %>
</td>
<td>
<div class="d-flex flex-column">
<a href="mailto:<%= submission.email %>"
class="text-decoration-none"><i
class="fas fa-envelope me-1"></i>
<%= submission.email %>
</a>
<% if(submission.phone) { %>
<span class="text-muted small"><i
class="fas fa-phone me-1"></i>
<%= submission.phone %>
</span>
<% } %>
</div>
</td>
<td>
<%= submission.appointmentDate || '-' %>
</td>
<td>
<% if (submission.visaTypes &&
submission.visaTypes.length> 0) { %>
<% submission.visaTypes.forEach(type=> { %>
<span
class="badge bg-light text-dark border me-1">
<%= type %>
</span>
<% }); %>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% if (submission.message) { %>
<div title="<%= submission.message %>"
style="max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<%= submission.message %>
</div>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% let statusClass='bg-secondary' ;
if(submission.status==='pending' )
statusClass='bg-warning text-dark' ;
if(submission.status==='confirmed' )
statusClass='bg-success' ;
if(submission.status==='completed' )
statusClass='bg-info text-dark' ;
if(submission.status==='cancelled' )
statusClass='bg-danger' ; %>
<span
class="badge <%= statusClass %> rounded-pill">
<%= submission.status %>
</span>
</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
onclick="openStatusModal('<%= submission._id %>', '<%= submission.status %>')"
title="Update Status">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="8"
class="text-center py-4 text-muted">No
submissions found</td>
</tr>
<% } %>
</tbody>
</table>
</div>
<div class="mt-3 text-end">
<small class="text-muted">Showing last 50 submissions</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Status Update Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="statusForm">
<input type="hidden" id="statusSubmissionId">
<div class="mb-3">
<label for="statusSelect" class="form-label">Status</label>
<select class="form-select" id="statusSelect">
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="saveStatus()">Save changes</button>
</div>
</div>
</div>
</div>
<script type="application/json" id="appointmentDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
let statusModal = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('appointmentDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = {};
}
// Check for tab parameter in URL
const urlParams = new URLSearchParams(window.location.search);
const tab = urlParams.get('tab');
if (tab) {
const triggerEl = document.querySelector(`a[href="#${tab}"]`);
if (triggerEl) {
const tabInstance = new bootstrap.Tab(triggerEl);
tabInstance.show();
}
}
// Move modal to body to prevent backdrop issues
const statusModalEl = document.getElementById('statusModal');
if (statusModalEl) {
document.body.appendChild(statusModalEl);
}
statusModal = new bootstrap.Modal(statusModalEl);
updateAllJsonInputs();
initializeFormHandlers();
});
function applyDateFilter() {
const startDate = document.getElementById('filterStartDate').value;
const endDate = document.getElementById('filterEndDate').value;
const url = new URL(window.location.href);
url.searchParams.set('tab', 'submissions');
if (startDate) {
url.searchParams.set('startDate', startDate);
} else {
url.searchParams.delete('startDate');
}
if (endDate) {
url.searchParams.set('endDate', endDate);
} else {
url.searchParams.delete('endDate');
}
window.location.href = url.toString();
}
function openStatusModal(id, currentStatus) {
document.getElementById('statusSubmissionId').value = id;
document.getElementById('statusSelect').value = currentStatus;
statusModal.show();
}
async function saveStatus() {
const id = document.getElementById('statusSubmissionId').value;
const status = document.getElementById('statusSelect').value;
try {
const response = await fetch(`/admin/appointments/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ status })
});
const result = await response.json();
if (result.success) {
// Determine CSS class for the notification or badge
// Since this is generic, we'll reload or update UI manually if complex.
// Reload is safest to show updated table state (including sorting/filtering if any)
// But let's try to be smooth:
window.location.reload();
} else {
alert('Failed to update status: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Error updating status:', error);
alert('Error updating status');
}
}
function initializeFormHandlers() {
const form = document.getElementById('appointmentForm');
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>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// 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);
});
});
// Update preview when background image changes
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
updateHeroImagePreview(this.value);
});
}
function updateHeroImagePreview(imagePath) {
const previewContainer = document.getElementById('heroImagePreview');
if (imagePath) {
let imgSrc = imagePath;
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
}
previewContainer.innerHTML = `
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewContainer.innerHTML = `
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
`;
}
}
function updateAllJsonInputs() {
updateJsonData();
}
function updateJsonData() {
// Hero data
const heroData = {
title: document.getElementById('heroTitle').value || '',
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
subtitle: document.getElementById('heroSubtitle').value || '',
heading: document.getElementById('heroHeading').value || '',
description: document.getElementById('heroDescription').value || '',
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Visa options
const visaOptions = [];
document.querySelectorAll('.visa-option-input').forEach(input => {
if (input.value.trim()) {
visaOptions.push(input.value.trim());
}
});
document.getElementById('visaOptionsJson').value = JSON.stringify(visaOptions);
// Form data
const fields = [];
document.querySelectorAll('.form-field-item').forEach(item => {
fields.push({
name: item.querySelector('.field-name-input').value || '',
label: item.querySelector('.field-label-input').value || '',
type: item.querySelector('.field-type-select').value || 'text',
placeholder: item.querySelector('.field-placeholder-input').value || '',
required: item.querySelector('.field-required-check').checked,
colClass: item.querySelector('.field-col-select').value || 'col-lg-12',
});
});
const formData = {
heading: document.getElementById('formHeading').value || '',
fields: fields,
submitButton: {
text: document.getElementById('formSubmitButtonText').value || 'Request Appointment',
icon: document.getElementById('formSubmitButtonIcon').value || 'fa-solid fa-arrow-right',
buttonClass: document.getElementById('formSubmitButtonClass').value || 'theme-btn',
},
};
document.getElementById('formJson').value = JSON.stringify(formData);
}
function addVisaOption() {
const container = document.getElementById('visaOptionsContainer');
const html = `
<div class="input-group mb-2 visa-option-item">
<span class="input-group-text"><i class="fas fa-passport"></i></span>
<input type="text" class="form-control visa-option-input" value="" placeholder="Enter visa option">
<button type="button" class="btn btn-outline-danger" onclick="removeVisaOption(this)">
<i class="fas fa-trash"></i>
</button>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeVisaOption(button) {
button.closest('.visa-option-item').remove();
}
function addFormField() {
const container = document.getElementById('formFieldsContainer');
const html = `
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Field Name</label>
<input type="text" class="form-control field-name-input" value="" placeholder="e.g., name">
</div>
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text" class="form-control field-label-input" value="" placeholder="e.g., Your Name">
</div>
<div class="col-md-2">
<label class="form-label">Type</label>
<select class="form-select field-type-select">
<option value="text" selected>Text</option>
<option value="email">Email</option>
<option value="tel">Phone</option>
<option value="textarea">Textarea</option>
<option value="date">Date</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Col Class</label>
<select class="form-select field-col-select">
<option value="col-lg-4">1/3 Width</option>
<option value="col-lg-6">1/2 Width</option>
<option value="col-lg-12" selected>Full Width</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input class="form-check-input field-required-check" type="checkbox">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text" class="form-control field-placeholder-input" value="">
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeFormField(button) {
button.closest('.form-field-item').remove();
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
// Image uploader function (reuse from shared)
function openImageUploader(targetInput, imageType) {
// Open upload modal or trigger file input
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('/admin/upload/image', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.imagePath) {
document.getElementById(targetInput).value = result.imagePath;
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(result.imagePath);
}
} else {
alert('Upload failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed. Please try again.');
}
};
input.click();
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,14 @@
<!-- Quick Actions -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" style="color: var(--primary-color);">Quick Management</h5>
<h5 class="mb-0" style="color: var(--primary-color)">Quick Management</h5>
</div>
<div class="card-body p-0">
<div class="row g-0">
<div class="col-md-4 border-end">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-home fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -29,7 +29,7 @@
<div class="col-md-4 border-end">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-bars fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -47,7 +47,7 @@
<div class="col-md-4">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-window-minimize fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -62,12 +62,10 @@
</div>
</div>
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-users fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -85,7 +83,7 @@
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-envelope fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -103,7 +101,7 @@
<div class="col-md-4 border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-question-circle fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -121,16 +119,16 @@
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-file-contract fa-lg" style="color: var(--primary-color);"></i>
<i class="fas fa-calendar-check fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Terms & Conditions</h5>
<p class="text-muted mb-0 small">Manage terms</p>
<h5 class="mb-0">Appointment</h5>
<p class="text-muted mb-0 small">Manage appointment page</p>
</div>
</div>
<a href="/admin/terms-conditions" class="btn btn-sm btn-primary w-100 mt-2">
<a href="/admin/appointment" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
@@ -139,7 +137,46 @@
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-tags fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Pricing</h5>
<p class="text-muted mb-0 small">Manage pricing page</p>
</div>
</div>
<a href="/admin/pricing" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-file-contract fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Terms & Conditions</h5>
<p class="text-muted mb-0 small">Manage terms</p>
</div>
</div>
<a
href="/admin/terms-conditions"
class="btn btn-sm btn-primary w-100 mt-2"
>
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-plane fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -157,7 +194,7 @@
<div class="col-md-4 border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-shield-alt fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -175,7 +212,7 @@
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-campground fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -184,7 +221,10 @@
<p class="text-muted mb-0 small">Manage camp location</p>
</div>
</div>
<a href="/admin/camp-location" class="btn btn-sm btn-primary w-100 mt-2">
<a
href="/admin/camp-location"
class="btn btn-sm btn-primary w-100 mt-2"
>
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
@@ -193,7 +233,7 @@
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-running fa-lg" style="color: var(--primary-color);"></i>
</div>
@@ -207,7 +247,32 @@
</a>
</div>
</div>
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div
class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="
width: 50px;
height: 50px;
background-color: rgba(184, 183, 106, 0.1);
"
>
<i
class="fas fa-running fa-lg"
style="color: var(--primary-color)"
></i>
</div>
<div>
<h5 class="mb-0">Services</h5>
<p class="text-muted mb-0 small">Manage services</p>
</div>
</div>
<a href="/admin/service" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
</div>
</div>
</div>
@@ -234,7 +299,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-bars" style="color: var(--primary-color);"></i>
</div>
@@ -242,10 +307,20 @@
</div>
</td>
<td><code>/api/header</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get menu header data</td>
<td>
<a href="/api/header" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/header"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -253,7 +328,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-home" style="color: var(--primary-color);"></i>
</div>
@@ -261,10 +336,20 @@
</div>
</td>
<td><code>/api/home</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get homepage data</td>
<td>
<a href="/api/home" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/home"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -272,7 +357,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
</div>
@@ -280,10 +365,20 @@
</div>
</td>
<td><code>/api/about</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get about page data</td>
<td>
<a href="/api/about" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/about"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -291,7 +386,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-users" style="color: var(--primary-color);"></i>
</div>
@@ -299,10 +394,20 @@
</div>
</td>
<td><code>/api/about-us</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get about us data</td>
<td>
<a href="/api/about-us" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/about-us"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -310,7 +415,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-question-circle" style="color: var(--primary-color);"></i>
</div>
@@ -318,10 +423,20 @@
</div>
</td>
<td><code>/api/faq</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get FAQ data</td>
<td>
<a href="/api/faq" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/faq"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -329,7 +444,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-file-contract" style="color: var(--primary-color);"></i>
</div>
@@ -337,10 +452,20 @@
</div>
</td>
<td><code>/api/terms</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get terms & conditions data</td>
<td>
<a href="/api/terms" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/terms"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -348,7 +473,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-plane" style="color: var(--primary-color);"></i>
</div>
@@ -356,10 +481,20 @@
</div>
</td>
<td><code>/api/travel</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get travel data</td>
<td>
<a href="/api/travel" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/travel"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -367,7 +502,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-shield-alt" style="color: var(--primary-color);"></i>
</div>
@@ -375,10 +510,20 @@
</div>
</td>
<td><code>/api/safety</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get safety data</td>
<td>
<a href="/api/safety" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/safety"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -386,7 +531,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-campground" style="color: var(--primary-color);"></i>
</div>
@@ -394,19 +539,29 @@
</div>
</td>
<td><code>/api/camp-location</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get camp location data</td>
<td>
<a href="/api/camp-location" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/camp-location"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-sitemap" style="color: var(--primary-color);"></i>
</div>
@@ -414,10 +569,20 @@
</div>
</td>
<td><code>/api/menu-tree</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get menu tree data</td>
<td>
<a href="/api/menu-tree" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/menu-tree"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -425,7 +590,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-envelope" style="color: var(--primary-color);"></i>
</div>
@@ -433,10 +598,20 @@
</div>
</td>
<td><code>/api/contact</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get contact data</td>
<td>
<a href="/api/contact" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/contact"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -444,7 +619,7 @@
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-campground" style="color: var(--primary-color);"></i>
</div>
@@ -452,10 +627,20 @@
</div>
</td>
<td><code>/api/camp-location</code></td>
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
<td>
<span
class="badge"
style="background-color: var(--primary-color)"
>GET</span
>
</td>
<td>API to get camp location data</td>
<td>
<a href="/api/camp-location" class="btn btn-sm btn-outline-primary" target="_blank">
<a
href="/api/camp-location"
class="btn btn-sm btn-outline-primary"
target="_blank"
>
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
@@ -468,46 +653,62 @@
<!-- System Info -->
<div class="card">
<div class="card-header" style="background: linear-gradient(135deg, #b8b76a, #9a994a); color: white;">
<div
class="card-header"
style="
background: linear-gradient(135deg, #b8b76a, #9a994a);
color: white;
"
>
<h5 class="mb-0">System Information</h5>
</div>
<div class="card-body" style="background-color: #f8faf8;">
<div class="card-body" style="background-color: #f8faf8">
<div class="row">
<div class="col-md-6">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
</div>
<div>
<div class="text-muted small">Version</div>
<div class="fw-bold" style="color: var(--primary-color);">CMS-SIMS v1.0.0</div>
<div class="fw-bold" style="color: var(--primary-color)">
CMS-SIMS v1.0.0
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-user" style="color: var(--primary-color);"></i>
</div>
<div>
<div class="text-muted small">Logged in as</div>
<div class="fw-bold" style="color: var(--primary-color);"><%= user.username %></div>
<div class="fw-bold" style="color: var(--primary-color);">
<%= user.username %>
</div>
</div>
</div>
</div>
</div>
<div class="alert mt-3 mb-0" style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
<div class="alert mt-3 mb-0"
style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
<div class="d-flex">
<div class="me-3">
<i class="fas fa-lightbulb fa-lg" style="color: var(--primary-color);"></i>
<i
class="fas fa-lightbulb fa-lg"
style="color: var(--primary-color)"
></i>
</div>
<div>
<h6 class="mb-1" style="color: var(--primary-color);">Quick Tip</h6>
<p class="mb-0 text-muted">Click the Edit button to make changes to your data.</p>
<h6 class="mb-1" style="color: var(--primary-color)">Quick Tip</h6>
<p class="mb-0 text-muted">
Click the Edit button to make changes to your data.
</p>
</div>
</div>
</div>
@@ -520,36 +721,36 @@
color: var(--primary-color);
font-weight: 600;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
color: white;
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.card-header h5 {
color: white;
}
.badge {
background-color: var(--primary-color) !important;
color: white;
}
</style>
</style>

View File

@@ -0,0 +1,742 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit content displayed on Pricing page</p>
</div>
<div>
<a href="<%= frontendUrl %>/pricing/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Pricing Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="pricingForm" action="/admin/pricing/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="pricingSection" id="pricingSectionJson">
<input type="hidden" name="plans" id="plansJson">
<input type="hidden" name="testimonials" id="testimonialsJson">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#pricingSection" role="tab">
<i class="fas fa-tags me-2"></i>Pricing Section
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#plans" role="tab">
<i class="fas fa-dollar-sign me-2"></i>Plans
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
<i class="fas fa-quote-right me-2"></i>Testimonials
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Hero Section</h6>
<div class="row g-3">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="pricing">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px;">
<% if (data.hero?.backgroundImage) { %>
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
!heroImgSrc.startsWith('http://') &&
!heroImgSrc.startsWith('https://')) {
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
heroImgSrc; } %>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Pricing Section Tab -->
<div class="tab-pane fade" id="pricingSection" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Pricing Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="pricingSectionSubtitle"
value="<%= data.pricingSection?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="pricingSectionHeading"
value="<%= data.pricingSection?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="pricingSectionDescription"
rows="3"><%= data.pricingSection?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Plans Tab -->
<div class="tab-pane fade" id="plans" role="tabpanel">
<!-- Monthly Plans -->
<div class="card border shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Monthly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('monthly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="monthlyPlansContainer">
<% if (data.plans?.monthly && data.plans.monthly.length> 0) { %>
<% data.plans.monthly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="monthly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
<!-- Yearly Plans -->
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Yearly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('yearly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="yearlyPlansContainer">
<% if (data.plans?.yearly && data.plans.yearly.length> 0) { %>
<% data.plans.yearly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="yearly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Testimonials Tab -->
<div class="tab-pane fade" id="testimonials" role="tabpanel">
<div class="card border shadow-sm mb-4">
<div class="card-body">
<h6 class="fw-medium mb-3">Testimonials Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="testimonialsSubtitle"
value="<%= data.testimonials?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="testimonialsHeading"
value="<%= data.testimonials?.heading || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Text</label>
<input type="text" class="form-control" id="testimonialsButtonText"
value="<%= data.testimonials?.buttonText || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Link</label>
<input type="text" class="form-control" id="testimonialsButtonLink"
value="<%= data.testimonials?.buttonLink || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Section Image</label>
<div class="input-group">
<input type="text" class="form-control" id="testimonialsImage"
value="<%= data.testimonials?.image || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsImage" data-image-type="pricing">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Testimonial Items</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addTestimonial()">
<i class="fas fa-plus"></i> Add Testimonial
</button>
</div>
<div id="testimonialsContainer">
<% if (data.testimonials?.items && data.testimonials.items.length> 0) { %>
<% data.testimonials.items.forEach((item, index)=> { %>
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text"
class="form-control testimonial-name"
value="<%= item.name || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text"
class="form-control testimonial-role"
value="<%= item.role || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1" <%=item.rating===1
? 'selected' : '' %>>1 Star</option>
<option value="2" <%=item.rating===2
? 'selected' : '' %>>2 Stars</option>
<option value="3" <%=item.rating===3
? 'selected' : '' %>>3 Stars</option>
<option value="4" <%=item.rating===4
? 'selected' : '' %>>4 Stars</option>
<option value="5" <%=item.rating===5
? 'selected' : '' %>>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content"
rows="3"><%= item.content || '' %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script type="application/json" id="pricingDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('pricingDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = {};
}
updateAllJsonInputs();
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('pricingForm');
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>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// 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);
});
});
// Update preview when background image changes
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
updateHeroImagePreview(this.value);
});
}
function updateHeroImagePreview(imagePath) {
const previewContainer = document.getElementById('heroImagePreview');
if (imagePath) {
let imgSrc = imagePath;
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
}
previewContainer.innerHTML = `
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewContainer.innerHTML = `
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
`;
}
}
function updateAllJsonInputs() {
updateJsonData();
}
function updateJsonData() {
// Hero data
const heroData = {
title: document.getElementById('heroTitle').value || '',
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
shapeImage: originalFormData?.hero?.shapeImage || '/assets/img/inner-page/shape.png',
breadcrumb: originalFormData?.hero?.breadcrumb || [],
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Pricing Section data
const pricingSectionData = {
subtitle: document.getElementById('pricingSectionSubtitle').value || '',
heading: document.getElementById('pricingSectionHeading').value || '',
description: document.getElementById('pricingSectionDescription').value || '',
};
document.getElementById('pricingSectionJson').value = JSON.stringify(pricingSectionData);
// Plans data
const monthlyPlans = [];
document.querySelectorAll('#monthlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
monthlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
const yearlyPlans = [];
document.querySelectorAll('#yearlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
yearlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
document.getElementById('plansJson').value = JSON.stringify({
monthly: monthlyPlans,
yearly: yearlyPlans,
});
// Testimonials data
const testimonialItems = [];
document.querySelectorAll('#testimonialsContainer .testimonial-item').forEach(item => {
testimonialItems.push({
name: item.querySelector('.testimonial-name').value || '',
role: item.querySelector('.testimonial-role').value || '',
rating: parseInt(item.querySelector('.testimonial-rating').value) || 5,
content: item.querySelector('.testimonial-content').value || '',
});
});
const testimonialsData = {
subtitle: document.getElementById('testimonialsSubtitle').value || '',
heading: document.getElementById('testimonialsHeading').value || '',
buttonText: document.getElementById('testimonialsButtonText').value || '',
buttonLink: document.getElementById('testimonialsButtonLink').value || '',
buttonIcon: originalFormData?.testimonials?.buttonIcon || 'fa-solid fa-arrow-right',
image: document.getElementById('testimonialsImage').value || '',
items: testimonialItems,
};
document.getElementById('testimonialsJson').value = JSON.stringify(testimonialsData);
}
function addPlan(type) {
const container = document.getElementById(type + 'PlansContainer');
const html = `
<div class="card mb-3 plan-item" data-type="${type}">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name" value="">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price" value="">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text" class="form-control plan-currency" value="$">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period" value="mo">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default" selected>Default</option>
<option value="style-2">Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text" class="form-control plan-button-text" value="Get Started Today">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text" class="form-control plan-button-link" value="/pricing">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text" class="form-control plan-button-icon" value="fa-solid fa-arrow-right">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per line)</label>
<textarea class="form-control plan-features" rows="4"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removePlan(button) {
if (confirm('Are you sure you want to remove this plan?')) {
button.closest('.plan-item').remove();
}
}
function addTestimonial() {
const container = document.getElementById('testimonialsContainer');
const html = `
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text" class="form-control testimonial-name" value="">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text" class="form-control testimonial-role" value="">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1">1 Star</option>
<option value="2">2 Stars</option>
<option value="3">3 Stars</option>
<option value="4">4 Stars</option>
<option value="5" selected>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content" rows="3"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeTestimonial(button) {
if (confirm('Are you sure you want to remove this testimonial?')) {
button.closest('.testimonial-item').remove();
}
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
// Image uploader function
function openImageUploader(targetInput, imageType) {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
// Send imageType via query string as controller expects req.query.imageType
const uploadUrl = '/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general');
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.path) {
document.getElementById(targetInput).value = result.path;
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(result.path);
}
} else {
alert('Upload failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed. Please try again.');
}
};
input.click();
}
</script>

View File

@@ -0,0 +1,723 @@
<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);">Service Details: <%= service.name %></h1>
<p class="text-muted mb-0">Edit detailed content for <%= service.name %> service</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 %>/details/update" method="POST" class="content-with-fixed-buttons" id="serviceDetailsForm">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="details" id="detailsJson">
<input type="hidden" name="features" id="featuresJson">
<input type="hidden" name="faq" id="faqJson">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#basic-info" role="tab">
<i class="fas fa-info-circle me-2"></i>Basic Information
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#key-features" role="tab">
<i class="fas fa-star me-2"></i>Key Features
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#faq-section" role="tab">
<i class="fas fa-question-circle me-2"></i>FAQ Section
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Basic Information Tab -->
<div class="tab-pane fade show active" id="basic-info" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-5">
<label class="form-label fw-medium">Main Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="mainImage" name="mainImage"
value="<%= service.details?.mainImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="mainImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="form-text text-muted">Recommended size: 800x600px</small>
</div>
<div class="col-md-7">
<div id="mainImagePreview">
<% if (service.details?.mainImage) { %>
<img src="<%= getFullImageUrl(service.details.mainImage) %>" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="detailsTitle" name="title"
value="<%= service.details?.title || service.name %>">
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="detailsDescription" name="description" rows="3"><%= service.details?.description || service.description %></textarea>
</div>
</div>
<!-- Overview Section -->
<div class="row mt-4">
<div class="col-12">
<div class="card border">
<div class="card-header bg-light">
<h6 class="mb-0">Overview Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Overview Title</label>
<input type="text" class="form-control" id="overviewTitle" name="overviewTitle"
value="<%= service.details?.overviewTitle || 'Service Overview' %>">
</div>
<div class="col-md-12">
<label class="form-label">Overview Description</label>
<textarea class="form-control" id="overviewDescription" name="overviewDescription" rows="4"><%= service.details?.overviewDescription || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label">Additional Description</label>
<textarea class="form-control" id="additionalDescription" name="additionalDescription" rows="3"><%= service.details?.additionalDescription || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Key Features Tab -->
<div class="tab-pane fade" id="key-features" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- Features Header -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Features Title</label>
<input type="text" class="form-control" id="keyFeaturesTitle" name="keyFeaturesTitle"
value="<%= service.details?.keyFeaturesTitle || 'Key Features' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Features Image</label>
<div class="input-group">
<input type="text" class="form-control" id="keyFeaturesImage" name="keyFeaturesImage"
value="<%= service.details?.keyFeaturesImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="keyFeaturesImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<!-- Key Features Image Preview -->
<div class="row mt-3">
<div class="col-12">
<div id="keyFeaturesImagePreview">
<% if (service.details?.keyFeaturesImage) { %>
<img src="<%= getFullImageUrl(service.details.keyFeaturesImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">
<% } %>
</div>
</div>
</div>
<!-- Features List -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Features</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFeature()">
<i class="fas fa-plus"></i> Add Feature
</button>
</div>
<div id="featuresContainer">
<% if (service.details?.features && service.details.features.length > 0) { %>
<% service.details.features.forEach((feature, index) => { %>
<div class="card mb-3 feature-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">Feature <%= index + 1 %></h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Title</label>
<input type="text" class="form-control feature-title"
value="<%= feature.title || '' %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control feature-description" rows="2" required><%= feature.description || '' %></textarea>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FAQ Section Tab -->
<div class="tab-pane fade" id="faq-section" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- FAQ Header -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">FAQ Title</label>
<input type="text" class="form-control" id="faqTitle" name="faqTitle"
value="<%= service.details?.faqTitle || 'Frequently Asked Questions' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">FAQ Image</label>
<div class="input-group">
<input type="text" class="form-control" id="faqImage" name="faqImage"
value="<%= service.details?.faqImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="faqImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<!-- FAQ Image Preview -->
<div class="row mt-3">
<div class="col-12">
<div id="faqImagePreview">
<% if (service.details?.faqImage) { %>
<img src="<%= getFullImageUrl(service.details.faqImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">
<% } %>
</div>
</div>
</div>
<!-- FAQ List -->
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">FAQ Items</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFAQ()">
<i class="fas fa-plus"></i> Add FAQ
</button>
</div>
<div id="faqContainer">
<% if (service.details?.faq && service.details.faq.length > 0) { %>
<% service.details.faq.forEach((faq, index) => { %>
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">FAQ <%= index + 1 %></h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID</label>
<input type="text" class="form-control faq-id"
value="<%= faq.id || 'faq-' + (index + 1) %>" required>
</div>
<div class="col-md-6">
<label class="form-label">Expanded by Default</label>
<select class="form-control faq-expanded">
<option value="false" <%= !faq.isExpanded ? 'selected' : '' %>>No</option>
<option value="true" <%= faq.isExpanded ? 'selected' : '' %>>Yes</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question"
value="<%= faq.question || '' %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer" rows="3" required><%= faq.answer || '' %></textarea>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Buttons -->
<div class="bottom-buttons">
<button type="reset" class="btn btn-secondary">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let originalFormData = null;
let featureIndex = <%= service.details?.features?.length || 0 %>;
let faqIndex = <%= service.details?.faq?.length || 0 %>;
document.addEventListener('DOMContentLoaded', function() {
// Initialize form data
originalFormData = <%- JSON.stringify(service) %>;
// Set initial JSON values
updateAllJsonInputs(originalFormData);
// Initialize form handlers
initializeFormHandlers();
});
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('serviceDetailsForm');
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>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
showError('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// 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);
});
});
// Initialize image input change listeners for manual URL input
const imageInputs = ['mainImage', 'keyFeaturesImage', 'faqImage'];
imageInputs.forEach(inputId => {
const input = document.getElementById(inputId);
if (input) {
input.addEventListener('input', function() {
updateImagePreviewAfterUpload(inputId, this.value);
});
}
});
}
function addFeature() {
const container = document.getElementById('featuresContainer');
const featureHtml = `
<div class="card mb-3 feature-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">Feature ${featureIndex + 1}</h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Title</label>
<input type="text" class="form-control feature-title" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control feature-description" rows="2" required></textarea>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', featureHtml);
featureIndex++;
}
function removeFeature(button) {
const featureItem = button.closest('.feature-item');
if (featureItem) {
featureItem.remove();
}
}
function addFAQ() {
const container = document.getElementById('faqContainer');
const faqHtml = `
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">FAQ ${faqIndex + 1}</h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID</label>
<input type="text" class="form-control faq-id"
value="faq-${faqIndex + 1}" required>
</div>
<div class="col-md-6">
<label class="form-label">Expanded by Default</label>
<select class="form-control faq-expanded">
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer" rows="3" required></textarea>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', faqHtml);
faqIndex++;
}
function removeFAQ(button) {
const faqItem = button.closest('.faq-item');
if (faqItem) {
faqItem.remove();
}
}
function updateAllJsonInputs(data) {
// Collect basic details data
const details = {
title: data.details?.title || data.name,
description: data.details?.description || data.description,
mainImage: data.details?.mainImage || '',
overviewTitle: data.details?.overviewTitle || 'Service Overview',
overviewDescription: data.details?.overviewDescription || '',
additionalDescription: data.details?.additionalDescription || '',
keyFeaturesTitle: data.details?.keyFeaturesTitle || 'Key Features',
keyFeaturesImage: data.details?.keyFeaturesImage || '',
faqTitle: data.details?.faqTitle || 'Frequently Asked Questions',
faqImage: data.details?.faqImage || ''
};
document.getElementById('detailsJson').value = JSON.stringify(details);
document.getElementById('featuresJson').value = JSON.stringify(data.details?.features || []);
document.getElementById('faqJson').value = JSON.stringify(data.details?.faq || []);
}
function updateJsonData() {
// Collect basic details data
const details = {
title: document.getElementById('detailsTitle').value,
description: document.getElementById('detailsDescription').value,
mainImage: document.getElementById('mainImage').value,
overviewTitle: document.getElementById('overviewTitle').value,
overviewDescription: document.getElementById('overviewDescription').value,
additionalDescription: document.getElementById('additionalDescription').value,
keyFeaturesTitle: document.getElementById('keyFeaturesTitle').value,
keyFeaturesImage: document.getElementById('keyFeaturesImage').value,
faqTitle: document.getElementById('faqTitle').value,
faqImage: document.getElementById('faqImage').value
};
// Collect features data
const features = [];
document.querySelectorAll('.feature-item').forEach(item => {
features.push({
title: item.querySelector('.feature-title').value,
description: item.querySelector('.feature-description').value
});
});
// Collect FAQ data
const faq = [];
document.querySelectorAll('.faq-item').forEach(item => {
faq.push({
id: item.querySelector('.faq-id').value,
question: item.querySelector('.faq-question').value,
answer: item.querySelector('.faq-answer').value,
isExpanded: item.querySelector('.faq-expanded').value === 'true'
});
});
document.getElementById('detailsJson').value = JSON.stringify(details);
document.getElementById('featuresJson').value = JSON.stringify(features);
document.getElementById('faqJson').value = JSON.stringify(faq);
}
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
function getFullImageUrlJS(imagePath) {
if (!imagePath) return '';
// Nếu đã là full URL thì return luôn
if (imagePath.startsWith('http')) {
return imagePath;
}
// Lấy backend URL
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
// Xử lý đường dẫn
let imgSrc = imagePath;
if (!imgSrc.startsWith('/')) {
imgSrc = '/uploads/' + imgSrc;
}
return backendUrl + imgSrc;
}
// Function để cập nhật image preview sau khi upload
function updateImagePreviewAfterUpload(targetInput, imagePath) {
const fullImageUrl = getFullImageUrlJS(imagePath);
switch(targetInput) {
case 'mainImage':
const mainPreview = document.getElementById('mainImagePreview');
if (mainPreview) {
mainPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">`;
}
break;
case 'keyFeaturesImage':
const featuresPreview = document.getElementById('keyFeaturesImagePreview');
if (featuresPreview) {
featuresPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">`;
}
break;
case 'faqImage':
const faqPreview = document.getElementById('faqImagePreview');
if (faqPreview) {
faqPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">`;
}
break;
}
}
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=${encodeURIComponent(imageType)}`, {
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) || document.querySelector(`[name="${targetInput}"]`);
if (!input) throw new Error('Target input not found');
input.value = result.path;
// Update image preview based on target input
updateImagePreviewAfterUpload(targetInput, 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) {
// Create and show success alert
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 showError(message) {
// Create and show error alert
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;
}
.content-with-fixed-buttons {
/* Remove bottom padding since buttons are no longer fixed */
}
.btn-group .btn {
margin-right: 2px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
.card-header h6 {
color: var(--primary-dark);
}
.text-decoration-underline {
text-decoration: underline;
color: var(--primary-dark);
}
/* Image Preview Styles */
#mainImagePreview, #keyFeaturesImagePreview, #faqImagePreview {
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 10px;
transition: all 0.3s ease;
}
#mainImagePreview:empty::before,
#keyFeaturesImagePreview:empty::before,
#faqImagePreview:empty::before {
content: "No image selected";
color: #6c757d;
font-style: italic;
}
#mainImagePreview img,
#keyFeaturesImagePreview img,
#faqImagePreview img {
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>

View File

@@ -0,0 +1,440 @@
<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>

View File

@@ -0,0 +1,782 @@
<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);"><%= title %></h1>
<p class="text-muted mb-0">Manage services and their detailed content</p>
</div>
<div>
<a href="<%= frontendUrl %>/admin/service/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Services Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/service/update" method="POST" class="content-with-fixed-buttons" id="serviceForm">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="services" id="servicesJson">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#services-list" role="tab">
<i class="fas fa-list me-2"></i>Services List
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#section-settings" role="tab">
<i class="fas fa-cog me-2"></i>Section Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#add-service" role="tab">
<i class="fas fa-plus me-2"></i>Add Service
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Services List Tab -->
<div class="tab-pane fade show active" id="services-list" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
<i class="fas fa-cogs text-primary"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0">Total Services</h6>
<h3 class="mb-0" id="totalCount">
<%= (data.services && data.services.items) ? data.services.items.length : 0 %>
</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="bg-success bg-opacity-10 rounded-circle p-3">
<i class="fas fa-align-left text-success"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0">Left Layout</h6>
<h3 class="mb-0" id="leftCount">
<%= (data.services && data.services.items) ? data.services.items.filter(s => s && s.layout === 'left').length : 0 %>
</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="bg-info bg-opacity-10 rounded-circle p-3">
<i class="fas fa-align-right text-info"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0">Right Layout</h6>
<h3 class="mb-0" id="rightCount">
<%= (data.services && data.services.items) ? data.services.items.filter(s => s && s.layout === 'right').length : 0 %>
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Services Table -->
<div class="table-responsive">
<table class="table table-hover mb-0" id="servicesTable">
<thead class="table-light">
<tr>
<th style="width: 40px;">#</th>
<th style="width: 80px;">Image</th>
<th>Name</th>
<th>Slug</th>
<th style="width: 100px;">Layout</th>
<th style="width: 200px;">Actions</th>
</tr>
</thead>
<tbody id="servicesTableBody">
<% if (data.services && data.services.items) { %>
<% data.services.items.forEach((service, index) => { %>
<% if (service) { %>
<tr data-index="<%= index %>">
<td class="text-muted"><%= index + 1 %></td>
<td>
<% if (service.image) { %>
<img src="<%= getFullImageUrl(service.image) %>" class="rounded" style="width: 60px; height: 45px; object-fit: cover;" alt="<%= service.name %>" >
<% } else { %>
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 60px; height: 45px;">
<i class="fas fa-image text-muted"></i>
</div>
<% } %>
</td>
<td>
<strong><%= service.name %></strong>
<% if (service.description) { %>
<br><small class="text-muted"><%= service.description.substring(0, 50) %>...</small>
<% } %>
</td>
<td>
<span class="badge bg-light text-dark"><%= service.slug %></span>
</td>
<td>
<span class="badge bg-<%= service.layout === 'left' ? 'primary' : 'success' %>">
<i class="fas fa-align-<%= service.layout %>"></i> <%= service.layout %>
</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/service/<%= service.slug %>/edit" class="btn btn-sm btn-outline-primary" title="Edit Service">
<i class="fas fa-edit"></i>
</a>
<a href="/admin/service/<%= service.slug %>/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
<i class="fas fa-cog"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(<%= index %>)" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<% } %>
<% }) %>
<% } %>
</tbody>
</table>
</div>
<% if (!data.services || !data.services.items || data.services.items.length === 0) { %>
<div class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No services found</h5>
<p class="text-muted">Click "Add Service" tab to create your first service.</p>
</div>
<% } %>
</div>
</div>
</div>
<!-- Section Settings Tab -->
<div class="tab-pane fade" id="section-settings" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Services Section Title</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Sub Title</label>
<input type="text" class="form-control" id="servicesSubTitle"
value="<%= data.services?.title?.subTitle || '' %>"
placeholder="Enter subtitle for services section">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Main Title</label>
<input type="text" class="form-control" id="servicesMainTitle"
value="<%= data.services?.title?.mainTitle || '' %>"
placeholder="Enter main title for services section">
</div>
</div>
</div>
</div>
</div>
<!-- Add Service Tab -->
<div class="tab-pane fade" id="add-service" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Add New Service</h6>
<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="newServiceName" placeholder="Enter service name">
</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-success ms-1" style="font-size: 0.7em;">AUTO</span>
</label>
<div class="input-group">
<input type="text" class="form-control" id="newServiceSlug" placeholder="Click generate to create 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. Generated automatically from the service name.</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Layout</label>
<select class="form-control" id="newServiceLayout">
<option value="left">Left</option>
<option value="right">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="newServiceImage" placeholder="Enter image path">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="newServiceImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-4">
<div id="newServiceImagePreview"></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="newServiceDescription" rows="3" placeholder="Enter service description"></textarea>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-primary" onclick="addService()">
<i class="fas fa-plus me-2"></i>Add Service
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Buttons -->
<div class="bottom-buttons">
<button type="reset" class="btn btn-secondary">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let originalFormData = null;
let servicesData = <%- JSON.stringify(data.services?.items || []) %>;
document.addEventListener('DOMContentLoaded', function() {
// Initialize form data
originalFormData = <%- JSON.stringify(data) %>;
// Set initial JSON values
updateAllJsonInputs(originalFormData);
// Initialize form handlers
initializeFormHandlers();
});
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('serviceForm');
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>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
showError('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// 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 new service
const newServiceImageInput = document.getElementById('newServiceImage');
if (newServiceImageInput) {
newServiceImageInput.addEventListener('input', function() {
updateImagePreview('newServiceImagePreview', this.value);
});
}
// Generate slug from service name
const newServiceNameInput = document.getElementById('newServiceName');
const newServiceSlugInput = document.getElementById('newServiceSlug');
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
const generateSlugBtn = document.getElementById('generateSlugBtn');
if (newServiceNameInput && newServiceSlugInput && generateSlugBtn) {
// Generate slug button
generateSlugBtn.addEventListener('click', async function() {
const serviceName = newServiceNameInput.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);
// Check if we're in edit mode
const addBtn = document.querySelector('#add-service .btn-primary');
const isEditMode = addBtn && addBtn.textContent.includes('Update');
// Get current service index if editing
let currentIndex = -1;
if (isEditMode) {
const onclickAttr = addBtn.getAttribute('onclick');
const match = onclickAttr.match(/updateService\((\d+)\)/);
if (match) {
currentIndex = parseInt(match[1]);
}
}
// Check for duplicate slug
if (isSlugDuplicate(slug, currentIndex)) {
const uniqueSlug = generateUniqueSlug(slug);
newServiceSlugInput.value = uniqueSlug;
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
} else {
newServiceSlugInput.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.');
}
});
}
}
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
function getFullImageUrlJS(imagePath) {
if (!imagePath) return '';
// Nếu đã là full URL thì return luôn
if (imagePath.startsWith('http')) {
return imagePath;
}
// Lấy backend URL
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
// Xử lý đường dẫn
let imgSrc = imagePath;
if (!imgSrc.startsWith('/')) {
imgSrc = '/uploads/' + imgSrc;
}
return backendUrl + imgSrc;
}
// 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 (excluding current service when editing)
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 = '';
}
}
function addService() {
const name = document.getElementById('newServiceName').value.trim();
const slug = document.getElementById('newServiceSlug').value.trim();
const description = document.getElementById('newServiceDescription').value.trim();
const image = document.getElementById('newServiceImage').value.trim();
const layout = document.getElementById('newServiceLayout').value;
if (!name || !slug || !description) {
showError('Please fill in all required fields and generate a slug.');
return;
}
// Check if service with same slug already exists
if (isSlugDuplicate(slug, -1)) {
showError('Service with this slug already exists. Please generate a new slug.');
return;
}
const newService = {
slug: slug,
name: name,
description: description,
image: image,
layout: layout,
details: {
title: name,
description: description,
mainImage: "img/inner-page/service-details/details-1.jpg",
overviewTitle: "Service Overview",
overviewDescription: "Service overview description...",
additionalDescription: "Additional description...",
keyFeaturesTitle: "Key Features",
keyFeaturesImage: "img/inner-page/service-details/details-2.jpg",
features: [],
faqTitle: "Frequently Asked Question",
faqImage: "img/inner-page/service-details/details-3.jpg",
faq: []
}
};
servicesData.push(newService);
updateServicesTable();
updateStatistics();
// Clear form
clearAddServiceForm();
showSuccess('Service added successfully!');
}
function deleteService(index) {
if (confirm('Are you sure you want to delete this service? This action cannot be undone.')) {
servicesData.splice(index, 1);
updateServicesTable();
updateStatistics();
showSuccess('Service deleted successfully!');
}
}
function clearAddServiceForm() {
document.getElementById('newServiceName').value = '';
document.getElementById('newServiceSlug').value = '';
document.getElementById('newServiceDescription').value = '';
document.getElementById('newServiceImage').value = '';
document.getElementById('newServiceLayout').value = 'left';
document.getElementById('newServiceImagePreview').innerHTML = '';
// Reset slug indicator
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
if (slugAutoIndicator) {
slugAutoIndicator.textContent = 'AUTO';
slugAutoIndicator.className = 'badge bg-success ms-1';
slugAutoIndicator.style.fontSize = '0.7em';
}
}
function updateServicesTable() {
const tbody = document.getElementById('servicesTableBody');
tbody.innerHTML = '';
servicesData.forEach((service, index) => {
if (!service) return; // Skip undefined services
let imageHtml = '';
if (service.image) {
const fullImageUrl = getFullImageUrlJS(service.image);
imageHtml = `<img src="${fullImageUrl}" class="rounded" style="width: 60px; height: 45px; object-fit: cover;" alt="${service.name || 'Service'}" onerror="this.onerror=null; this.src='/images/placeholder.png'; this.classList.add('bg-light');">`;
} else {
imageHtml = `<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 60px; height: 45px;"><i class="fas fa-image text-muted"></i></div>`;
}
const row = `
<tr data-index="${index}">
<td class="text-muted">${index + 1}</td>
<td>${imageHtml}</td>
<td>
<strong>${service.name || 'Unnamed Service'}</strong>
${service.description ? `<br><small class="text-muted">${service.description.substring(0, 50)}...</small>` : ''}
</td>
<td><span class="badge bg-light text-dark">${service.slug || 'no-slug'}</span></td>
<td><span class="badge bg-${(service.layout === 'left') ? 'primary' : 'success'}"><i class="fas fa-align-${service.layout || 'left'}"></i> ${service.layout || 'left'}</span></td>
<td>
<div class="btn-group" role="group">
<a href="/admin/service/${service.slug || 'unknown'}/edit" class="btn btn-sm btn-outline-primary" title="Edit Service">
<i class="fas fa-edit"></i>
</a>
<a href="/admin/service/${service.slug || 'unknown'}/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
<i class="fas fa-cog"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(${index})" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
tbody.innerHTML += row;
});
}
function updateStatistics() {
const validServices = servicesData.filter(s => s && s.layout);
document.getElementById('totalCount').textContent = servicesData.length;
document.getElementById('leftCount').textContent = validServices.filter(s => s.layout === 'left').length;
document.getElementById('rightCount').textContent = validServices.filter(s => s.layout === 'right').length;
}
function updateAllJsonInputs(data) {
const servicesSection = {
title: data.services?.title || { subTitle: '', mainTitle: '' },
items: data.services?.items || []
};
document.getElementById('servicesJson').value = JSON.stringify(servicesSection);
}
function updateJsonData() {
const servicesSection = {
title: {
subTitle: document.getElementById('servicesSubTitle').value,
mainTitle: document.getElementById('servicesMainTitle').value,
},
items: servicesData
};
// Gửi toàn bộ services section (bao gồm cả title và items)
document.getElementById('servicesJson').value = JSON.stringify(servicesSection);
}
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...';
}
// Sử dụng imageType=service để upload vào folder service
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) || document.querySelector(`[name="${targetInput}"]`);
if (!input) throw new Error('Target input not found');
input.value = result.path;
// Update preview if it's the new service image
if (targetInput === 'newServiceImage') {
updateImagePreview('newServiceImagePreview', 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) {
// Create and show success alert
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) {
// Create and show warning alert
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) {
// Create and show error alert
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;
}
.content-with-fixed-buttons {
/* Remove bottom padding since buttons are no longer fixed */
}
.btn-group .btn {
margin-right: 2px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
</style>