feat(admin): implement unified submission and newsletter management

Introduce a centralized system to manage all website form submissions and newsletter subscriptions.
- Add `Submission` and `NewsletterSubscription` models with MongoDB schema validation
- Implement `submissionController` and `newsletterSubscriptionController` for CRUD operations and filtering
- Create a unified admin UI for reviewing submissions across different sources (home, request, contact, partnership, newsletter)
- Add database migration scripts for creating collections and indexes
- Refactor partnership inquiry forms to use a fixed field structure
- Update admin navigation and server CORS settings to support PATCH requests
This commit is contained in:
Tống Thành Đạt
2026-04-24 17:11:23 +07:00
parent caf38e8750
commit f6f2e0e03b
18 changed files with 1534 additions and 54 deletions
+13 -4
View File
@@ -55,11 +55,20 @@
<%- include("partials/templates", { editorUi }) %>
<script type="application/json" id="partnershipsPageBootstrapJson"><%- JSON.stringify({
data,
backendUrl,
editorConfig,
editorUi,
}) %></script>
<script>
window.partnershipsPageData = <%- JSON.stringify(data) %>;
window.partnershipsBackendUrl = <%- JSON.stringify(backendUrl) %>;
window.partnershipsEditorConfig = <%- JSON.stringify(editorConfig) %>;
window.partnershipsEditorUi = <%- JSON.stringify(editorUi) %>;
const partnershipsPageBootstrap = JSON.parse(
document.getElementById("partnershipsPageBootstrapJson").textContent || "{}",
);
window.partnershipsPageData = partnershipsPageBootstrap.data;
window.partnershipsBackendUrl = partnershipsPageBootstrap.backendUrl;
window.partnershipsEditorConfig = partnershipsPageBootstrap.editorConfig;
window.partnershipsEditorUi = partnershipsPageBootstrap.editorUi;
</script>
<%- include("partials/editor-script") %>
@@ -97,12 +97,6 @@
renderPartners();
});
document.getElementById("addInquiryFieldBtn")?.addEventListener("click", function () {
state.inquiryForm.fields.push(getDefaultInquiryField());
ensurePartnershipIds();
renderInquiryFields();
});
document.getElementById("resetPartnershipsForm")?.addEventListener("click", function () {
window.location.reload();
});
@@ -378,7 +372,7 @@
input.value = field[key] || "";
input.addEventListener("input", function () {
field[key] = input.value;
if (key === "label") {
if (key === "label") {
title.textContent = input.value || `${inquiryFieldFields.label?.label || "Field"} ${index + 1}`;
}
if (key === "type") {
@@ -401,11 +395,6 @@
}
});
node.querySelector("[data-remove-item]").addEventListener("click", function () {
state.inquiryForm.fields.splice(index, 1);
renderInquiryFields();
});
node.querySelector("[data-add-option]").addEventListener("click", function () {
field.options = Array.isArray(field.options) ? field.options : [];
field.options.push("");
@@ -444,7 +433,6 @@
inquiryFieldsList.appendChild(node);
});
initSortable(inquiryFieldsList, state.inquiryForm.fields, renderInquiryFields, '[data-item="inquiry-field"]');
}
function initSortable(container, list, rerender, draggableSelector) {
@@ -16,17 +16,13 @@
<div class="mb-3">
<div>
<label class="form-label fw-semibold mb-1"><%= inquiryUi.fields?.label || "Form fields" %></label>
<div class="form-text mt-0"><%= inquiryUi.fieldsHelpText || inquiryUi.fields?.helpText || "" %></div>
<div class="form-text mt-0">
Field structure is fixed to match the inquiry form: First Name, Last Name, Organization Name, Partnership Type, and Message. You can edit field text, required state, width, and Partnership Type select options.
</div>
</div>
</div>
<div id="inquiryFieldsList"></div>
<button type="button" class="cms-add-button mt-3" id="addInquiryFieldBtn">
<i class="fas fa-plus me-2"></i><%= inquiryUi.fields?.addLabel || "Add Field" %>
</button>
</div>
</div>
</div>
</div>
@@ -100,9 +100,6 @@
<div class="card cms-item-card mb-3" data-item="inquiry-field">
<div class="card-header d-flex justify-content-between align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center gap-2">
<button type="button" class="drag-handle" title="Drag to reorder">
<i class="fas fa-grip-vertical"></i>
</button>
<div>
<div class="fw-semibold" data-title>Field</div>
<div class="small text-muted" data-subtitle></div>
@@ -112,9 +109,6 @@
<button type="button" class="cms-collapse-toggle" data-toggle-item title="Collapse item">
<i class="fas fa-chevron-down"></i>
</button>
<button type="button" class="cms-remove-button" data-remove-item title="Remove item">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="card-body">
@@ -129,7 +123,7 @@
</div>
<div class="col-md-4">
<label class="form-label fw-semibold"><%= inquiryFieldFields.type?.label || "Field type" %></label>
<select class="form-select" data-field="type">
<select class="form-select" data-field="type" disabled>
<% (inquiryFieldFields.type?.options || []).forEach((option) => { %>
<option value="<%= option.value %>"><%= option.label %></option>
<% }) %>
@@ -144,10 +138,10 @@
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" data-field="required">
<label class="form-check-label fw-semibold"><%= inquiryFieldFields.required?.label || "Required field" %></label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" data-field="required">
<label class="form-check-label fw-semibold"><%= inquiryFieldFields.required?.label || "Required field" %></label>
</div>
</div>
<div class="col-12" data-options-wrap>
<div class="cms-editor-group">