// Procurement Management Frontend JavaScript
// Debug logging
console.log('Procurement Frontend JS loaded')
console.log('jQuery available:', typeof jQuery !== 'undefined')
console.log('procurement_ajax available:', typeof window.procurement_ajax !== 'undefined')
// Define functions in the global scope to make them accessible from HTML onclick attributes
window.showCreateTenderForm = function() {
const formElement = document.getElementById('create-tender-form')
if (formElement) {
formElement.style.display = 'block'
// Initialize form if needed
if (typeof window.initializeTenderForm === 'function') {
window.initializeTenderForm()
}
} else {
console.error('Create tender form element not found')
}
}
window.hideCreateTenderForm = function() {
const formElement = document.getElementById('create-tender-form')
if (formElement) {
formElement.style.display = 'none'
// Reset form if it exists
const formReset = document.getElementById('tender-creation-form')
if (formReset) {
formReset.reset()
}
}
}
// Also define the initializeTenderForm function if it doesn't exist elsewhere
window.initializeTenderForm = function() {
console.log('Initializing tender form')
// Set default dates
const now = new Date()
const tomorrow = new Date(now.getTime() + (24 * 60 * 60 * 1000))
// Format dates for datetime-local inputs
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day}T${hours}:${minutes}`
}
// Set consultation start date to tomorrow if the element exists
const consultationStartInput = document.getElementById('consultation_start_date')
if (consultationStartInput) {
consultationStartInput.value = formatDate(tomorrow)
}
// Generate random encryption key if the field exists
const encryptionKeyField = document.getElementById('encryption_key')
if (encryptionKeyField) {
const keyLength = 16
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*'
let result = ''
for (let i = 0; i < keyLength; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
encryptionKeyField.value = result
}
}
// Add the validateTenderForm function
window.validateTenderForm = function() {
console.log('Validating tender form')
let isValid = true
const errors = []
// Basic validation - using correct field IDs
const requiredFields = [
'tender_title',
'tender_description',
'tender_requirements',
'tender_category',
'estimated_value',
'deadline'
]
requiredFields.forEach(fieldId => {
const field = document.getElementById(fieldId)
if (field && !field.value.trim()) {
field.style.borderColor = '#dc3232'
// Get the label text for the error message
const label = field.closest('tr').querySelector('label')
const fieldName = label ? label.textContent.replace(':', '') : fieldId
errors.push(fieldName + ' is required')
isValid = false
} else if (field) {
field.style.borderColor = ''
}
})
// Date validation
const deadline = document.getElementById('deadline')
if (deadline && deadline.value) {
const deadlineDate = new Date(deadline.value)
const now = new Date()
if (deadlineDate <= now) {
deadline.style.borderColor = '#dc3232'
errors.push('Submission deadline must be in the future')
isValid = false
}
}
// Show errors if any
if (errors.length > 0) {
// Remove any existing error messages
const existingErrors = document.querySelectorAll('.tender-form-error')
existingErrors.forEach(el => el.remove())
// Create and show error message
const errorDiv = document.createElement('div')
errorDiv.className = 'tender-form-error notice notice-error'
errorDiv.innerHTML = '
Please fix the following errors:
' +
errors.map(err => '- ' + err + '
').join('') + '
'
const form = document.getElementById('tender-creation-form')
if (form) {
form.insertBefore(errorDiv, form.firstChild)
}
}
return isValid
}
jQuery(document).ready(($) => {
console.log('Document ready, initializing procurement functionality')
// Form validation and submission
$(".procurement-form").on("submit", function (e) {
if (!validateForm(this)) {
e.preventDefault()
return false
}
})
// File upload validation
$('input[type="file"]').on("change", function () {
validateFileUpload(this)
})
// Real-time form validation
$(".form-group input, .form-group textarea, .form-group select").on("blur", function () {
validateField(this)
})
// IFU and RCCM number formatting
$("#ifu_number, #rccm_number").on("input", function () {
this.value = this.value.replace(/[^0-9A-Za-z-]/g, "").toUpperCase()
})
// Phone number formatting
$("#contact_phone").on("input", function () {
this.value = this.value.replace(/[^0-9+\-\s()]/g, "")
})
// Tender search and filtering
$("#tender-search").on(
"input",
debounce(function () {
filterTenders($(this).val())
}, 300),
)
$("#tender-category-filter").on("change", function () {
filterTendersByCategory($(this).val())
})
// Bid submission
$(document).on("submit", "#bid-form", function (e) {
e.preventDefault()
console.log('Bid form submitted')
submitBid(this)
})
// PPM document download tracking
$(".ppm-download-link").on("click", function () {
trackPPMDownload($(this).data("ppm-id"))
})
// Plan document download tracking
$(".plan-download-link").on("click", function () {
trackPlanDownload($(this).data("plan-id"))
})
// Auto-save draft functionality
let autoSaveTimer
$(".auto-save-form input, .auto-save-form textarea").on("input", () => {
clearTimeout(autoSaveTimer)
autoSaveTimer = setTimeout(() => {
autoSaveDraft()
}, 2000)
})
// Functions
function validateForm(form) {
let isValid = true
const $form = $(form)
// Clear previous errors
$form.find(".error-message").remove()
$form.find(".error").removeClass("error")
// Validate required fields
$form.find("[required]").each(function () {
if (!validateField(this)) {
isValid = false
}
})
// Validate email format
$form.find('input[type="email"]').each(function () {
if (this.value && !isValidEmail(this.value)) {
showFieldError(this, "Please enter a valid email address")
isValid = false
}
})
// Validate file uploads
$form.find('input[type="file"]').each(function () {
if (!validateFileUpload(this)) {
isValid = false
}
})
return isValid
}
function validateField(field) {
const $field = $(field)
const value = field.value.trim()
// Clear previous error
$field.removeClass("error")
$field.siblings(".error-message").remove()
// Check required fields
if (field.hasAttribute("required") && !value) {
showFieldError(field, "This field is required")
return false
}
// Specific validations
switch (field.type) {
case "email":
if (value && !isValidEmail(value)) {
showFieldError(field, "Please enter a valid email address")
return false
}
break
case "tel":
if (value && !isValidPhone(value)) {
showFieldError(field, "Please enter a valid phone number")
return false
}
break
}
// Custom validations
if (field.id === "ifu_number" && value && !isValidIFU(value)) {
showFieldError(field, "Please enter a valid IFU number")
return false
}
if (field.id === "rccm_number" && value && !isValidRCCM(value)) {
showFieldError(field, "Please enter a valid RCCM number")
return false
}
return true
}
function showFieldError(field, message) {
const $field = $(field)
$field.addClass("error")
$field.after(
'' + message + "
",
)
}
function validateFileUpload(fileInput) {
const files = fileInput.files
const maxSize = 5 * 1024 * 1024 // 5MB
const allowedTypes = [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
]
for (let i = 0; i < files.length; i++) {
const file = files[i]
if (file.size > maxSize) {
showFieldError(fileInput, "File size must be less than 5MB")
return false
}
if (!allowedTypes.includes(file.type)) {
showFieldError(fileInput, "Only PDF and Word documents are allowed")
return false
}
}
return true
}
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
function isValidPhone(phone) {
const phoneRegex = /^[+]?[0-9\s\-$$$$]{8,}$/
return phoneRegex.test(phone)
}
function isValidIFU(ifu) {
// Basic IFU validation - adjust based on your country's format
return ifu.length >= 8 && /^[0-9A-Z-]+$/.test(ifu)
}
function isValidRCCM(rccm) {
// Basic RCCM validation - adjust based on your country's format
return rccm.length >= 8 && /^[0-9A-Z-]+$/.test(rccm)
}
function filterTenders(searchTerm) {
$(".tender-card").each(function () {
const $card = $(this)
const title = $card.find("h4").text().toLowerCase()
const description = $card.find(".tender-description").text().toLowerCase()
if (title.includes(searchTerm.toLowerCase()) || description.includes(searchTerm.toLowerCase())) {
$card.show()
} else {
$card.hide()
}
})
}
function filterTendersByCategory(category) {
if (!category) {
$(".tender-card").show()
return
}
$(".tender-card").each(function () {
const $card = $(this)
const cardCategory = $card.data("category")
if (cardCategory === category) {
$card.show()
} else {
$card.hide()
}
})
}
function submitBid(form) {
console.log('submitBid function called')
const $form = $(form)
const formData = new FormData(form)
formData.append("action", "submit_bid")
formData.append("bid_nonce", window.procurement_ajax.nonce)
console.log('Form data prepared, submitting AJAX request')
console.log('AJAX URL:', window.procurement_ajax.ajax_url)
console.log('Nonce:', window.procurement_ajax.nonce)
$form.addClass("loading")
$form.find('button[type="submit"]').prop('disabled', true).text('Submitting...')
$.ajax({
url: window.procurement_ajax.ajax_url,
type: "POST",
data: formData,
processData: false,
contentType: false,
success: (response) => {
console.log('AJAX success response:', response)
$form.removeClass("loading")
$form.find('button[type="submit"]').prop('disabled', false).text('Submit Bid')
if (response.success) {
showNotification("Bid submitted successfully!", "success")
$form[0].reset()
// Close submission modal
if ($('#bid-modal').length) {
$('#bid-modal').removeClass('show')
$('body').removeClass('modal-open')
}
if ($('#bid-submission-modal').length) {
$('#bid-submission-modal').removeClass('show')
$('body').removeClass('modal-open')
}
// If the server returned an encryption key and QR, show it to the supplier
const enc = response.data && response.data.encryption
if (enc && (enc.key || enc.qr)) {
showEncryptionKey(enc)
}
} else {
showNotification("Error: " + response.data, "error")
}
},
error: (xhr, status, error) => {
console.error('AJAX error:', {xhr, status, error})
$form.removeClass("loading")
$form.find('button[type="submit"]').prop('disabled', false).text('Submit Bid')
showNotification("Network error. Please try again.", "error")
console.error('AJAX Error:', error)
},
})
}
function showEncryptionKey(enc) {
const existing = document.getElementById('encryption-key-modal')
if (existing) existing.remove()
const modal = document.createElement('div')
modal.id = 'encryption-key-modal'
modal.style.position = 'fixed'
modal.style.inset = '0'
modal.style.background = 'rgba(0,0,0,0.5)'
modal.style.zIndex = '9999'
modal.style.display = 'flex'
modal.style.alignItems = 'center'
modal.style.justifyContent = 'center'
const card = document.createElement('div')
card.style.background = '#fff'
card.style.padding = '24px'
card.style.borderRadius = '8px'
card.style.maxWidth = '520px'
card.style.width = '90%'
card.style.boxShadow = '0 10px 30px rgba(0,0,0,0.2)'
const title = document.createElement('h3')
title.textContent = 'Your Submission Encryption Key'
title.style.marginTop = '0'
const note = document.createElement('p')
note.textContent = 'Save this key securely. You will need it at opening time.'
const keyWrap = document.createElement('div')
keyWrap.style.display = 'flex'
keyWrap.style.alignItems = 'center'
keyWrap.style.margin = '12px 0'
const keyBox = document.createElement('input')
keyBox.type = 'text'
keyBox.readOnly = true
keyBox.value = enc.key || ''
keyBox.style.flex = '1'
keyBox.style.padding = '8px'
keyBox.style.fontFamily = 'monospace'
keyBox.style.fontSize = '14px'
keyBox.style.marginRight = '8px'
const copyBtn = document.createElement('button')
copyBtn.textContent = 'Copy Key'
copyBtn.className = 'button'
copyBtn.onclick = () => {
navigator.clipboard.writeText(keyBox.value).then(() => {
showNotification('Key copied to clipboard', 'success')
})
}
keyWrap.appendChild(keyBox)
keyWrap.appendChild(copyBtn)
const qrImg = document.createElement('img')
if (enc.qr) {
qrImg.src = enc.qr
qrImg.alt = 'Encryption Key QR'
qrImg.style.display = 'block'
qrImg.style.margin = '12px auto'
qrImg.style.maxWidth = '220px'
}
const actions = document.createElement('div')
actions.style.display = 'flex'
actions.style.justifyContent = 'space-between'
actions.style.marginTop = '12px'
const downloadBtn = document.createElement('a')
downloadBtn.textContent = 'Download QR'
downloadBtn.className = 'button button-secondary'
downloadBtn.href = enc.qr || '#'
downloadBtn.download = 'tender-key-qr.png'
const closeBtn = document.createElement('button')
closeBtn.textContent = 'Done'
closeBtn.className = 'button button-primary'
closeBtn.onclick = () => {
modal.remove()
}
actions.appendChild(downloadBtn)
actions.appendChild(closeBtn)
card.appendChild(title)
card.appendChild(note)
if (enc.key) card.appendChild(keyWrap)
if (enc.qr) card.appendChild(qrImg)
card.appendChild(actions)
modal.appendChild(card)
document.body.appendChild(modal)
}
function trackPPMDownload(ppmId) {
$.post(window.procurement_ajax.ajax_url, {
action: "track_ppm_download",
ppm_id: ppmId,
nonce: window.procurement_ajax.nonce,
})
}
function trackPlanDownload(planId) {
$.post(window.procurement_ajax.ajax_url, {
action: "track_plan_download",
plan_id: planId,
nonce: window.procurement_ajax.nonce,
})
}
function autoSaveDraft() {
const formData = $(".auto-save-form").serialize()
$.post(window.procurement_ajax.ajax_url, {
action: "auto_save_draft",
form_data: formData,
nonce: window.procurement_ajax.nonce,
})
}
function showNotification(message, type) {
const notification = $('' + message + "
")
$("body").append(notification)
setTimeout(() => {
notification.fadeOut(() => {
notification.remove()
})
}, 5000)
}
function debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
// Modal functionality
function closeModal(modalSelector) {
$(modalSelector).removeClass('show').hide()
$('body').removeClass('modal-open')
}
function openModal(modalSelector) {
$(modalSelector).addClass('show').show()
$('body').addClass('modal-open')
}
// Close modals on escape key
$(document).on('keydown', function(e) {
if (e.key === 'Escape') {
closeModal('.procurement-modal')
closeModal('.tender-modal')
}
})
// Close modal when clicking outside
$(document).on('click', '.procurement-modal, .tender-modal', function(e) {
if (e.target === this) {
closeModal($(this))
}
})
// Close modal when clicking close button
$(document).on('click', '.modal-close, .tender-modal-close', function(e) {
e.preventDefault()
const $modal = $(this).closest('.procurement-modal, .tender-modal')
closeModal($modal)
})
})
// Notification styles
const notificationCSS = `
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 6px;
color: white;
font-weight: 600;
z-index: 9999;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.notification-success {
background-color: #27ae60;
}
.notification-error {
background-color: #e74c3c;
}
.notification-info {
background-color: #3498db;
}
`
// Inject notification styles
if (!document.getElementById("procurement-notification-styles")) {
const style = document.createElement("style")
style.id = "procurement-notification-styles"
style.textContent = notificationCSS
document.head.appendChild(style)
}