Update
This commit is contained in:
244
public/admin/assets/css/password-validator.css
Normal file
244
public/admin/assets/css/password-validator.css
Normal file
@@ -0,0 +1,244 @@
|
||||
/* Enhanced Password Validation Styles */
|
||||
|
||||
.password-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: none;
|
||||
background: transparent;
|
||||
z-index: 10;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.password-toggle:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
/* Password Strength Indicator */
|
||||
.password-strength-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.password-strength-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.strength-indicator {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.strength-indicator[data-strength="0"] {
|
||||
width: 0%;
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.strength-indicator[data-strength="1"] {
|
||||
width: 20%;
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.strength-indicator[data-strength="2"] {
|
||||
width: 40%;
|
||||
background-color: #fd7e14;
|
||||
}
|
||||
|
||||
.strength-indicator[data-strength="3"] {
|
||||
width: 60%;
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
.strength-indicator[data-strength="4"] {
|
||||
width: 80%;
|
||||
background-color: #20c997;
|
||||
}
|
||||
|
||||
.strength-indicator[data-strength="5"] {
|
||||
width: 100%;
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
/* Password Requirements Checklist */
|
||||
.password-requirements {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.password-checklist {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.password-checklist li.requirement {
|
||||
margin-bottom: 0.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.password-checklist .fa-check {
|
||||
color: #28a745;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.password-checklist .fa-times {
|
||||
color: #dc3545;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.password-checklist .requirement.valid {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.password-checklist .requirement.valid .fa-times {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.password-checklist .requirement.valid::before {
|
||||
content: '\f00c';
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
color: #28a745;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Password Match Indicator */
|
||||
.password-match-indicator {
|
||||
padding: 0.375rem 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.password-match-text.text-success {
|
||||
color: #28a745 !important;
|
||||
}
|
||||
|
||||
.password-match-text.text-danger {
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.password-match-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Animation for invalid password */
|
||||
@keyframes shake {
|
||||
0%, 20%, 40%, 60%, 80%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%, 30%, 50%, 70%, 90% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.shake {
|
||||
animation: shake 0.6s;
|
||||
}
|
||||
|
||||
/* Enhanced form field styles */
|
||||
.form-control.password-field:focus,
|
||||
.form-control.password-confirmation-field:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
.form-control.password-field.is-valid,
|
||||
.form-control.password-confirmation-field.is-valid {
|
||||
border-color: #28a745;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='m2.3 6.73.5-.5L4.5 4.5l1.7 1.73.5-.5L4.5 3.5l2.2-2.23-.5-.5L4.5 2.5 2.3.27l-.5.5L3.5 2.5.27 4.73l.5.5L2.5 3.5l1.8 3.23z'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right calc(0.375em + 0.1875rem) center;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
}
|
||||
|
||||
.form-control.password-field.is-invalid,
|
||||
.form-control.password-confirmation-field.is-invalid {
|
||||
border-color: #dc3545;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath d='m5.8 3.6.4.4m0 0 .4.4m-.4-.4L5.8 4.8m0 0 .4.4'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right calc(0.375em + 0.1875rem) center;
|
||||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.password-requirements {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.password-checklist {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.password-input-container .password-toggle {
|
||||
right: 3px;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.password-strength-bar {
|
||||
background-color: #495057;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.password-checklist .requirement {
|
||||
color: #e9ecef;
|
||||
}
|
||||
|
||||
.password-checklist .requirement.valid {
|
||||
color: #28a745;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.password-strength-bar {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.strength-indicator {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
border: 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus visible for better accessibility */
|
||||
.password-toggle:focus-visible {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.password-toggle,
|
||||
.password-strength-container,
|
||||
.password-requirements {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
595
public/admin/assets/js/messaging/messaging.js
Normal file
595
public/admin/assets/js/messaging/messaging.js
Normal file
@@ -0,0 +1,595 @@
|
||||
/**
|
||||
* Messaging JavaScript Component
|
||||
* Handles message composition, attachments, and interface interactions
|
||||
*/
|
||||
|
||||
// Prevent duplicate declaration
|
||||
if (typeof MessagingManager === 'undefined') {
|
||||
class MessagingManager {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
maxAttachmentSize: options.maxAttachmentSize || 10, // MB
|
||||
allowedFileTypes: options.allowedFileTypes || ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'jpeg', 'png', 'gif'],
|
||||
uploadUrl: options.uploadUrl || '/admin/messages/upload-attachment',
|
||||
removeAttachmentUrl: options.removeAttachmentUrl || '/admin/messages/remove-attachment',
|
||||
sendMessageUrl: options.sendMessageUrl || '/admin/messages/send',
|
||||
...options
|
||||
};
|
||||
|
||||
this.attachments = [];
|
||||
this.tempAttachmentIds = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupElements();
|
||||
this.bindEvents();
|
||||
this.initializeRichEditor();
|
||||
}
|
||||
|
||||
setupElements() {
|
||||
this.messageForm = document.getElementById('message-form');
|
||||
this.recipientsSelect = document.getElementById('recipients');
|
||||
this.subjectInput = document.getElementById('subject');
|
||||
this.contentEditor = document.getElementById('content');
|
||||
this.attachmentInput = document.getElementById('attachment-input');
|
||||
this.attachmentsList = document.getElementById('attachments-list');
|
||||
this.requireAcknowledgment = document.getElementById('require_acknowledgment');
|
||||
this.sendButton = document.getElementById('send-message-btn');
|
||||
this.saveDraftButton = document.getElementById('save-draft-btn');
|
||||
this.attachmentButton = document.getElementById('attachment-btn');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// File attachment
|
||||
if (this.attachmentInput) {
|
||||
this.attachmentInput.addEventListener('change', (e) => {
|
||||
this.handleFileSelection(e.target.files);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.attachmentButton) {
|
||||
this.attachmentButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.attachmentInput?.click();
|
||||
});
|
||||
}
|
||||
|
||||
// Form submission
|
||||
if (this.messageForm) {
|
||||
this.messageForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
});
|
||||
}
|
||||
|
||||
// Save draft
|
||||
if (this.saveDraftButton) {
|
||||
this.saveDraftButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.saveDraft();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-save draft every 30 seconds
|
||||
setInterval(() => {
|
||||
this.autoSaveDraft();
|
||||
}, 30000);
|
||||
|
||||
// Recipients validation
|
||||
if (this.recipientsSelect) {
|
||||
this.recipientsSelect.addEventListener('change', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
}
|
||||
|
||||
// Subject validation
|
||||
if (this.subjectInput) {
|
||||
this.subjectInput.addEventListener('input', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initializeRichEditor() {
|
||||
if (this.contentEditor && typeof tinymce !== 'undefined') {
|
||||
tinymce.init({
|
||||
selector: '#content',
|
||||
height: 300,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist autolink lists link image charmap print preview anchor',
|
||||
'searchreplace visualblocks code fullscreen',
|
||||
'insertdatetime media table paste code help wordcount'
|
||||
],
|
||||
toolbar: 'undo redo | formatselect | bold italic backcolor | \
|
||||
alignleft aligncenter alignright alignjustify | \
|
||||
bullist numlist outdent indent | removeformat | help',
|
||||
setup: (editor) => {
|
||||
editor.on('change', () => {
|
||||
this.validateForm();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleFileSelection(files) {
|
||||
for (let file of files) {
|
||||
if (this.validateFile(file)) {
|
||||
await this.uploadFile(file);
|
||||
}
|
||||
}
|
||||
// Clear the input
|
||||
if (this.attachmentInput) {
|
||||
this.attachmentInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
validateFile(file) {
|
||||
// Check file size
|
||||
const maxSizeBytes = this.options.maxAttachmentSize * 1024 * 1024;
|
||||
if (file.size > maxSizeBytes) {
|
||||
this.showError(trans('admin.ATTACHMENT_TOO_LARGE'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file type
|
||||
const extension = file.name.split('.').pop().toLowerCase();
|
||||
if (!this.options.allowedFileTypes.includes(extension)) {
|
||||
this.showError(trans('admin.INVALID_FILE_TYPE'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
this.showUploadProgress(file.name);
|
||||
|
||||
const response = await fetch(this.options.uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
this.addAttachment(data.attachment);
|
||||
this.tempAttachmentIds.push(data.attachment.id);
|
||||
} else {
|
||||
this.showError(data.message || trans('admin.UPLOAD_FAILED'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
this.showError(trans('admin.UPLOAD_FAILED'));
|
||||
} finally {
|
||||
this.hideUploadProgress();
|
||||
}
|
||||
}
|
||||
|
||||
addAttachment(attachment) {
|
||||
this.attachments.push(attachment);
|
||||
this.renderAttachments();
|
||||
}
|
||||
|
||||
async removeAttachment(attachmentId) {
|
||||
try {
|
||||
const response = await fetch(this.options.removeAttachmentUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify({ id: attachmentId })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.attachments = this.attachments.filter(att => att.id !== attachmentId);
|
||||
this.tempAttachmentIds = this.tempAttachmentIds.filter(id => id !== attachmentId);
|
||||
this.renderAttachments();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Remove attachment error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderAttachments() {
|
||||
if (!this.attachmentsList) return;
|
||||
|
||||
if (this.attachments.length === 0) {
|
||||
this.attachmentsList.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentsHtml = this.attachments.map(attachment => `
|
||||
<div class="attachment-item d-flex align-items-center justify-content-between p-2 border rounded mb-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="${this.getFileIcon(attachment.file_name)} me-2"></i>
|
||||
<span class="file-name">${this.escapeHtml(attachment.original_name)}</span>
|
||||
<span class="file-size text-muted ms-2">(${this.formatFileSize(attachment.file_size)})</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
onclick="messagingManager.removeAttachment(${attachment.id})">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
this.attachmentsList.innerHTML = attachmentsHtml;
|
||||
}
|
||||
|
||||
getFileIcon(fileName) {
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
const icons = {
|
||||
'pdf': 'fas fa-file-pdf text-danger',
|
||||
'doc': 'fas fa-file-word text-primary',
|
||||
'docx': 'fas fa-file-word text-primary',
|
||||
'xls': 'fas fa-file-excel text-success',
|
||||
'xlsx': 'fas fa-file-excel text-success',
|
||||
'jpg': 'fas fa-file-image text-info',
|
||||
'jpeg': 'fas fa-file-image text-info',
|
||||
'png': 'fas fa-file-image text-info',
|
||||
'gif': 'fas fa-file-image text-info'
|
||||
};
|
||||
return icons[extension] || 'fas fa-file text-secondary';
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
if (!this.validateForm()) return;
|
||||
|
||||
const formData = this.getFormData();
|
||||
formData.attachment_ids = this.tempAttachmentIds;
|
||||
|
||||
try {
|
||||
this.setSendingState(true);
|
||||
|
||||
const response = await fetch(this.options.sendMessageUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
this.showSuccess(trans('admin.MESSAGE_SENT'));
|
||||
this.resetForm();
|
||||
// Redirect to inbox or sent messages
|
||||
setTimeout(() => {
|
||||
window.location.href = data.redirect || '/admin/messages';
|
||||
}, 1500);
|
||||
} else {
|
||||
this.showError(data.message || trans('admin.SEND_FAILED'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Send message error:', error);
|
||||
this.showError(trans('admin.SEND_FAILED'));
|
||||
} finally {
|
||||
this.setSendingState(false);
|
||||
}
|
||||
}
|
||||
|
||||
async saveDraft() {
|
||||
const formData = this.getFormData();
|
||||
formData.is_draft = true;
|
||||
formData.attachment_ids = this.tempAttachmentIds;
|
||||
|
||||
try {
|
||||
const response = await fetch(this.options.sendMessageUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
this.showSuccess(trans('admin.DRAFT_SAVED'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Save draft error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
autoSaveDraft() {
|
||||
const hasContent = this.hasFormContent();
|
||||
if (hasContent) {
|
||||
this.saveDraft();
|
||||
}
|
||||
}
|
||||
|
||||
getFormData() {
|
||||
const recipients = Array.from(this.recipientsSelect?.selectedOptions || [])
|
||||
.map(option => option.value);
|
||||
|
||||
const content = this.contentEditor && typeof tinymce !== 'undefined'
|
||||
? tinymce.get('content')?.getContent() || ''
|
||||
: this.contentEditor?.value || '';
|
||||
|
||||
return {
|
||||
recipients: recipients,
|
||||
subject: this.subjectInput?.value || '',
|
||||
content: content,
|
||||
require_acknowledgment: this.requireAcknowledgment?.checked || false,
|
||||
attachment_ids: this.tempAttachmentIds
|
||||
};
|
||||
} validateForm() {
|
||||
const data = this.getFormData();
|
||||
const maxRecipients = window.messagingConfig?.maxRecipients || 20;
|
||||
|
||||
let isValid = data.recipients.length > 0 &&
|
||||
data.subject.trim() !== '' &&
|
||||
data.content.trim() !== '';
|
||||
|
||||
// Check max recipients limit
|
||||
if (data.recipients.length > maxRecipients) {
|
||||
isValid = false;
|
||||
this.showError(`Maximum ${maxRecipients} recipients allowed.`);
|
||||
}
|
||||
|
||||
if (this.sendButton) {
|
||||
this.sendButton.disabled = !isValid;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
hasFormContent() {
|
||||
const data = this.getFormData();
|
||||
return data.recipients.length > 0 ||
|
||||
data.subject.trim() !== '' ||
|
||||
data.content.trim() !== '' ||
|
||||
this.attachments.length > 0;
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
if (this.recipientsSelect) {
|
||||
Array.from(this.recipientsSelect.options).forEach(option => {
|
||||
option.selected = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.subjectInput) this.subjectInput.value = '';
|
||||
|
||||
if (this.contentEditor && typeof tinymce !== 'undefined') {
|
||||
tinymce.get('content')?.setContent('');
|
||||
} else if (this.contentEditor) {
|
||||
this.contentEditor.value = '';
|
||||
}
|
||||
|
||||
if (this.requireAcknowledgment) {
|
||||
this.requireAcknowledgment.checked = false;
|
||||
}
|
||||
|
||||
this.attachments = [];
|
||||
this.tempAttachmentIds = [];
|
||||
this.renderAttachments();
|
||||
this.validateForm();
|
||||
}
|
||||
|
||||
setSendingState(sending) {
|
||||
if (this.sendButton) {
|
||||
this.sendButton.disabled = sending;
|
||||
this.sendButton.innerHTML = sending
|
||||
? `<i class="fas fa-spinner fa-spin"></i> ${trans('admin.SENDING')}`
|
||||
: trans('admin.SEND_MESSAGE');
|
||||
}
|
||||
}
|
||||
|
||||
showUploadProgress(fileName) {
|
||||
// You can implement a progress indicator here
|
||||
console.log(`Uploading ${fileName}...`);
|
||||
}
|
||||
|
||||
hideUploadProgress() {
|
||||
// Hide progress indicator
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
// Implement success notification
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: trans('admin.SUCCESS'),
|
||||
text: message,
|
||||
timer: 3000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
// Implement error notification
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: trans('admin.ERROR'),
|
||||
text: message
|
||||
});
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk actions for message lists
|
||||
class MessageBulkActions {
|
||||
constructor() {
|
||||
this.selectedMessages = new Set();
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Select all checkbox
|
||||
const selectAllCheckbox = document.getElementById('select-all-messages');
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.addEventListener('change', (e) => {
|
||||
this.selectAll(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// Individual message checkboxes
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.matches('.message-checkbox')) {
|
||||
this.toggleMessage(e.target.value, e.target.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// Bulk action buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.matches('#bulk-mark-read')) {
|
||||
e.preventDefault();
|
||||
this.bulkMarkRead();
|
||||
} else if (e.target.matches('#bulk-star')) {
|
||||
e.preventDefault();
|
||||
this.bulkStar();
|
||||
} else if (e.target.matches('#bulk-archive')) {
|
||||
e.preventDefault();
|
||||
this.bulkArchive();
|
||||
} else if (e.target.matches('#bulk-delete')) {
|
||||
e.preventDefault();
|
||||
this.bulkDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(checked) {
|
||||
const checkboxes = document.querySelectorAll('.message-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = checked;
|
||||
this.toggleMessage(checkbox.value, checked);
|
||||
});
|
||||
}
|
||||
|
||||
toggleMessage(messageId, selected) {
|
||||
if (selected) {
|
||||
this.selectedMessages.add(messageId);
|
||||
} else {
|
||||
this.selectedMessages.delete(messageId);
|
||||
}
|
||||
this.updateBulkActionsVisibility();
|
||||
}
|
||||
|
||||
updateBulkActionsVisibility() {
|
||||
const bulkActions = document.getElementById('bulk-actions');
|
||||
const hasSelected = this.selectedMessages.size > 0;
|
||||
|
||||
if (bulkActions) {
|
||||
bulkActions.style.display = hasSelected ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async bulkMarkRead() {
|
||||
await this.performBulkAction('mark-read');
|
||||
}
|
||||
|
||||
async bulkStar() {
|
||||
await this.performBulkAction('star');
|
||||
}
|
||||
|
||||
async bulkArchive() {
|
||||
await this.performBulkAction('archive');
|
||||
}
|
||||
|
||||
async bulkDelete() {
|
||||
if (!confirm(trans('admin.CONFIRM_DELETE_SELECTED'))) return;
|
||||
await this.performBulkAction('delete');
|
||||
}
|
||||
|
||||
async performBulkAction(action) {
|
||||
if (this.selectedMessages.size === 0) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/messages/bulk-action', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: action,
|
||||
message_ids: Array.from(this.selectedMessages)
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
throw new Error('Bulk action failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Bulk action error:', error);
|
||||
alert(trans('admin.BULK_ACTION_FAILED'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global helper function for translations
|
||||
function trans(key, replacements = {}) {
|
||||
const translations = window.translations || {};
|
||||
let translation = translations[key] || key;
|
||||
|
||||
Object.keys(replacements).forEach(search => {
|
||||
translation = translation.replace(`:${search}`, replacements[search]);
|
||||
});
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (document.getElementById('message-form')) {
|
||||
window.messagingManager = new MessagingManager();
|
||||
}
|
||||
|
||||
if (document.querySelector('.message-checkbox')) {
|
||||
window.messageBulkActions = new MessageBulkActions();
|
||||
}
|
||||
});
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { MessagingManager, MessageBulkActions };
|
||||
}
|
||||
|
||||
} // End of MessagingManager class guard
|
||||
453
public/admin/assets/js/messaging/notifications.js
Normal file
453
public/admin/assets/js/messaging/notifications.js
Normal file
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* Real-time Notifications JavaScript Component
|
||||
* Handles notification polling, desktop notifications, and UI updates
|
||||
*/
|
||||
|
||||
class NotificationManager {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
pollInterval: options.pollInterval || 30000, // 30 seconds
|
||||
enableDesktopNotifications: options.enableDesktopNotifications || true,
|
||||
enableSound: options.enableSound || false, // Disable sound by default to avoid 404
|
||||
soundUrl: options.soundUrl || '/admin/sounds/notification.mp3',
|
||||
markReadUrl: options.markReadUrl || '/admin/notifications/mark-read',
|
||||
markAllReadUrl: options.markAllReadUrl || '/admin/notifications/mark-all-read',
|
||||
fetchUrl: options.fetchUrl || '/admin/notifications/fetch',
|
||||
...options
|
||||
};
|
||||
|
||||
this.isPolling = false;
|
||||
this.unreadCount = 0;
|
||||
this.lastNotificationTime = null;
|
||||
this.audio = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupElements();
|
||||
this.requestDesktopPermission();
|
||||
this.setupAudio();
|
||||
this.bindEvents();
|
||||
this.startPolling();
|
||||
this.loadNotifications();
|
||||
}
|
||||
|
||||
setupElements() {
|
||||
this.notificationButton = document.getElementById('notification-button');
|
||||
this.notificationDropdown = document.getElementById('notification-dropdown');
|
||||
this.notificationCount = document.getElementById('notification-count');
|
||||
this.notificationList = document.getElementById('notification-list');
|
||||
this.markAllReadBtn = document.getElementById('mark-all-read');
|
||||
|
||||
if (!this.notificationButton || !this.notificationDropdown) {
|
||||
console.warn('Notification elements not found');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
requestDesktopPermission() {
|
||||
if (this.options.enableDesktopNotifications && 'Notification' in window) {
|
||||
if (Notification.permission === 'default') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
}
|
||||
} setupAudio() {
|
||||
if (this.options.enableSound) {
|
||||
try {
|
||||
this.audio = new Audio(this.options.soundUrl);
|
||||
this.audio.preload = 'auto';
|
||||
|
||||
// Handle loading errors
|
||||
this.audio.addEventListener('error', (e) => {
|
||||
console.warn('Notification sound file not found or could not be loaded:', this.options.soundUrl);
|
||||
this.audio = null; // Disable audio if it can't load
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Could not setup notification audio:', error);
|
||||
this.audio = null;
|
||||
}
|
||||
}
|
||||
} bindEvents() {
|
||||
// Mark all as read
|
||||
if (this.markAllReadBtn) {
|
||||
this.markAllReadBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.markAllAsRead();
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to Bootstrap dropdown events
|
||||
if (this.notificationButton) {
|
||||
// Try multiple event binding approaches for different Bootstrap versions
|
||||
|
||||
// Bootstrap 4/5 events
|
||||
this.notificationButton.addEventListener('show.bs.dropdown', () => {
|
||||
this.loadNotifications();
|
||||
});
|
||||
|
||||
// jQuery-based Bootstrap events (fallback)
|
||||
if (typeof $ !== 'undefined') {
|
||||
$(this.notificationButton).on('show.bs.dropdown', () => {
|
||||
this.loadNotifications();
|
||||
});
|
||||
|
||||
// Also try the older Bootstrap 3 events
|
||||
$(this.notificationButton).on('shown.bs.dropdown', () => {
|
||||
this.loadNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: still handle click events but don't prevent default
|
||||
this.notificationButton.addEventListener('click', () => {
|
||||
// Small delay to ensure dropdown state is updated
|
||||
setTimeout(() => {
|
||||
this.loadNotifications();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle individual notification clicks
|
||||
if (this.notificationList) {
|
||||
this.notificationList.addEventListener('click', (e) => {
|
||||
const notificationItem = e.target.closest('.notification-item');
|
||||
if (notificationItem) {
|
||||
const notificationId = notificationItem.dataset.notificationId;
|
||||
if (notificationId) {
|
||||
this.markAsRead(notificationId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
if (this.isPolling) return;
|
||||
|
||||
this.isPolling = true;
|
||||
this.pollInterval = setInterval(() => {
|
||||
this.loadNotifications();
|
||||
}, this.options.pollInterval);
|
||||
}
|
||||
|
||||
stopPolling() {
|
||||
if (this.pollInterval) {
|
||||
clearInterval(this.pollInterval);
|
||||
this.pollInterval = null;
|
||||
}
|
||||
this.isPolling = false;
|
||||
}
|
||||
|
||||
async loadNotifications() {
|
||||
try {
|
||||
const response = await fetch(this.options.fetchUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.updateNotifications(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load notifications:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateNotifications(data) {
|
||||
const newUnreadCount = data.unread_count || 0;
|
||||
const notifications = data.notifications || [];
|
||||
|
||||
// Check for new notifications
|
||||
const hasNewNotifications = newUnreadCount > this.unreadCount;
|
||||
|
||||
// Update count
|
||||
this.unreadCount = newUnreadCount;
|
||||
this.updateUnreadCount();
|
||||
|
||||
// Update notification list
|
||||
this.updateNotificationList(notifications);
|
||||
|
||||
// Show desktop notifications for new items
|
||||
if (hasNewNotifications && notifications.length > 0) {
|
||||
const latestNotification = notifications[0];
|
||||
if (this.shouldShowDesktopNotification(latestNotification)) {
|
||||
this.showDesktopNotification(latestNotification);
|
||||
this.playSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateUnreadCount() {
|
||||
if (this.notificationCount) {
|
||||
if (this.unreadCount > 0) {
|
||||
// Format count: show 99+ for counts over 99
|
||||
const displayCount = this.unreadCount > 99 ? '99+' : this.unreadCount.toString();
|
||||
this.notificationCount.textContent = displayCount;
|
||||
this.notificationCount.classList.remove('d-none');
|
||||
this.notificationButton?.classList.add('has-notifications');
|
||||
} else {
|
||||
this.notificationCount.classList.add('d-none');
|
||||
this.notificationButton?.classList.remove('has-notifications');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateNotificationList(notifications) {
|
||||
if (!this.notificationList) return;
|
||||
|
||||
if (notifications.length === 0) {
|
||||
this.notificationList.innerHTML = `
|
||||
<div class="dropdown-item text-center text-muted py-3">
|
||||
<i class="fas fa-bell-slash mb-2"></i><br>
|
||||
${trans('admin.NO_NOTIFICATIONS')}
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationHtml = notifications.map(notification =>
|
||||
this.renderNotification(notification)
|
||||
).join('');
|
||||
|
||||
this.notificationList.innerHTML = notificationHtml;
|
||||
}
|
||||
|
||||
renderNotification(notification) {
|
||||
const isRead = notification.read_at;
|
||||
const timeAgo = this.timeAgo(notification.created_at);
|
||||
const iconClass = this.getNotificationIcon(notification.type, notification.data);
|
||||
const typeClass = `type-${notification.type}`;
|
||||
|
||||
// Add special class for tardiness alerts
|
||||
const isTardinessAlert = notification.type === 'system_alert' && notification.data && notification.data.type === 'tardiness_alert';
|
||||
const specialClass = isTardinessAlert ? 'type-tardiness_alert' : typeClass;
|
||||
|
||||
return `
|
||||
<div class="dropdown-item notification-item ${isRead ? 'read' : 'unread'}"
|
||||
data-notification-id="${notification.id}">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="notification-icon ${specialClass} me-3">
|
||||
<i class="${iconClass}"></i>
|
||||
</div>
|
||||
<div class="notification-content flex-grow-1">
|
||||
<div class="notification-title">${this.escapeHtml(notification.title)}</div>
|
||||
<div class="notification-message">${this.escapeHtml(notification.message)}</div>
|
||||
<div class="notification-time text-muted small">
|
||||
<i class="fas fa-clock"></i> ${timeAgo}
|
||||
</div>
|
||||
</div>
|
||||
${!isRead ? '<div class="notification-badge"></div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
getNotificationIcon(type, data = {}) {
|
||||
// Check if it's a system_alert with specific subtype
|
||||
if (type === 'system_alert' && data && data.type === 'tardiness_alert') {
|
||||
return 'fas fa-clock text-danger';
|
||||
}
|
||||
|
||||
const icons = {
|
||||
'message': 'fas fa-envelope',
|
||||
'system_alert': 'fas fa-exclamation-triangle',
|
||||
'plugin_notification': 'fas fa-puzzle-piece',
|
||||
'system': 'fas fa-cog',
|
||||
'warning': 'fas fa-exclamation-triangle',
|
||||
'info': 'fas fa-info-circle',
|
||||
'success': 'fas fa-check-circle',
|
||||
'error': 'fas fa-times-circle',
|
||||
'tardiness_alert': 'fas fa-clock text-danger'
|
||||
};
|
||||
return icons[type] || 'fas fa-bell';
|
||||
}
|
||||
|
||||
shouldShowDesktopNotification(notification) {
|
||||
if (!this.options.enableDesktopNotifications || Notification.permission !== 'granted') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show if notification is older than last check
|
||||
if (this.lastNotificationTime && new Date(notification.created_at) <= this.lastNotificationTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
showDesktopNotification(notification) {
|
||||
if (Notification.permission === 'granted') {
|
||||
const desktopNotification = new Notification(notification.title, {
|
||||
body: notification.message,
|
||||
icon: '/admin/images/notification-icon.png',
|
||||
tag: `notification-${notification.id}`
|
||||
});
|
||||
|
||||
desktopNotification.onclick = () => {
|
||||
window.focus();
|
||||
this.markAsRead(notification.id);
|
||||
desktopNotification.close();
|
||||
};
|
||||
|
||||
// Auto close after 5 seconds
|
||||
setTimeout(() => {
|
||||
desktopNotification.close();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
playSound() {
|
||||
if (this.options.enableSound && this.audio) {
|
||||
this.audio.play().catch(error => {
|
||||
console.warn('Could not play notification sound:', error);
|
||||
});
|
||||
}
|
||||
} async markAsRead(notificationId) {
|
||||
try {
|
||||
// Build the URL with the notification ID parameter
|
||||
const url = this.options.markReadUrl.replace('__ID__', notificationId);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update UI to mark as read
|
||||
const notificationItem = document.querySelector(`[data-notification-id="${notificationId}"]`);
|
||||
if (notificationItem) {
|
||||
notificationItem.classList.remove('unread');
|
||||
notificationItem.classList.add('read');
|
||||
const badge = notificationItem.querySelector('.notification-badge');
|
||||
if (badge) badge.remove();
|
||||
}
|
||||
|
||||
// Decrease unread count
|
||||
if (this.unreadCount > 0) {
|
||||
this.unreadCount--;
|
||||
this.updateUnreadCount();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to mark notification as read:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async markAllAsRead() {
|
||||
try {
|
||||
const response = await fetch(this.options.markAllReadUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Update all notifications to read state
|
||||
const unreadItems = document.querySelectorAll('.notification-item.unread');
|
||||
unreadItems.forEach(item => {
|
||||
item.classList.remove('unread');
|
||||
item.classList.add('read');
|
||||
const badge = item.querySelector('.notification-badge');
|
||||
if (badge) badge.remove();
|
||||
});
|
||||
|
||||
// Reset unread count
|
||||
this.unreadCount = 0;
|
||||
this.updateUnreadCount();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to mark all notifications as read:', error);
|
||||
}
|
||||
}
|
||||
|
||||
toggleDropdown() {
|
||||
// Let Bootstrap handle the dropdown toggle naturally
|
||||
// Just load notifications when needed
|
||||
this.loadNotifications();
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
// Bootstrap handles the visual state, we just ensure data is loaded
|
||||
this.loadNotifications();
|
||||
}
|
||||
|
||||
closeDropdown() {
|
||||
// Bootstrap handles the visual state
|
||||
// No additional action needed
|
||||
}
|
||||
|
||||
timeAgo(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return trans('admin.JUST_NOW');
|
||||
} else if (diffInSeconds < 3600) {
|
||||
const minutes = Math.floor(diffInSeconds / 60);
|
||||
return trans('admin.MINUTES_AGO').replace(':minutes', minutes);
|
||||
} else if (diffInSeconds < 86400) {
|
||||
const hours = Math.floor(diffInSeconds / 3600);
|
||||
return trans('admin.HOURS_AGO').replace(':hours', hours);
|
||||
} else {
|
||||
const days = Math.floor(diffInSeconds / 86400);
|
||||
return trans('admin.DAYS_AGO').replace(':days', days);
|
||||
}
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopPolling();
|
||||
// Remove event listeners if needed
|
||||
}
|
||||
}
|
||||
|
||||
// Global helper function for translations
|
||||
function trans(key, replacements = {}) {
|
||||
// This should be implemented based on your translation system
|
||||
// For now, return the key as fallback
|
||||
const translations = window.translations || {};
|
||||
let translation = translations[key] || key;
|
||||
|
||||
Object.keys(replacements).forEach(search => {
|
||||
translation = translation.replace(`:${search}`, replacements[search]);
|
||||
});
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = NotificationManager;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get configuration from backend (passed via window.notificationConfig)
|
||||
const config = window.notificationConfig || {};
|
||||
|
||||
// Wait for NotificationManager to be available before initializing
|
||||
window.notificationManager = new NotificationManager(config);
|
||||
});
|
||||
320
public/admin/assets/js/password-validator.js
Normal file
320
public/admin/assets/js/password-validator.js
Normal file
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* Enhanced Password Validation Component
|
||||
* Provides real-time password strength validation and feedback
|
||||
*
|
||||
* Usage:
|
||||
* new PasswordValidator({
|
||||
* passwordField: '#password',
|
||||
* confirmField: '#verify_password',
|
||||
* strengthIndicator: '#password-strength',
|
||||
* requirementsContainer: '.password-checklist'
|
||||
* });
|
||||
*/
|
||||
|
||||
class PasswordValidator {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
passwordField: '#password',
|
||||
confirmField: '#verify_password',
|
||||
strengthIndicator: '#password-strength',
|
||||
strengthText: '.password-strength-text',
|
||||
requirementsContainer: '.password-checklist',
|
||||
matchIndicator: '.password-match-indicator',
|
||||
matchText: '.password-match-text',
|
||||
toggleButtons: '.password-toggle',
|
||||
minLength: 8,
|
||||
maxLength: 128,
|
||||
requireUppercase: true,
|
||||
requireLowercase: true,
|
||||
requireNumbers: true,
|
||||
requireSpecialChars: true,
|
||||
checkCommonPasswords: true,
|
||||
...options
|
||||
};
|
||||
|
||||
this.translations = {
|
||||
very_weak: 'Very Weak',
|
||||
weak: 'Weak',
|
||||
fair: 'Fair',
|
||||
good: 'Good',
|
||||
strong: 'Strong',
|
||||
match: 'Passwords match',
|
||||
no_match: 'Passwords do not match',
|
||||
...options.translations
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.initializeToggles();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
const passwordField = $(this.options.passwordField);
|
||||
const confirmField = $(this.options.confirmField);
|
||||
|
||||
// Real-time password validation
|
||||
passwordField.on('input', (e) => {
|
||||
const password = $(e.target).val();
|
||||
this.validatePassword(password);
|
||||
this.checkPasswordMatch();
|
||||
});
|
||||
|
||||
// Real-time confirmation validation
|
||||
confirmField.on('input', () => {
|
||||
this.checkPasswordMatch();
|
||||
});
|
||||
|
||||
// Form submission validation
|
||||
passwordField.closest('form').on('submit', (e) => {
|
||||
if (!this.isFormValid()) {
|
||||
e.preventDefault();
|
||||
this.showValidationErrors();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeToggles() {
|
||||
$(this.options.toggleButtons).on('click', (e) => {
|
||||
const button = $(e.currentTarget);
|
||||
const targetSelector = button.data('target');
|
||||
const target = $(targetSelector);
|
||||
const icon = button.find('i');
|
||||
|
||||
if (target.attr('type') === 'password') {
|
||||
target.attr('type', 'text');
|
||||
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
} else {
|
||||
target.attr('type', 'password');
|
||||
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
validatePassword(password) {
|
||||
const checks = {
|
||||
length: password.length >= this.options.minLength && password.length <= this.options.maxLength,
|
||||
uppercase: this.options.requireUppercase ? /[A-Z]/.test(password) : true,
|
||||
lowercase: this.options.requireLowercase ? /[a-z]/.test(password) : true,
|
||||
number: this.options.requireNumbers ? /[0-9]/.test(password) : true,
|
||||
special: this.options.requireSpecialChars ? /[^A-Za-z0-9]/.test(password) : true,
|
||||
notCommon: this.options.checkCommonPasswords ? !this.isCommonPassword(password) : true
|
||||
};
|
||||
|
||||
this.updateRequirementIndicators(checks);
|
||||
|
||||
const strength = this.calculateStrength(password, checks);
|
||||
this.updateStrengthIndicator(strength);
|
||||
|
||||
return {
|
||||
isValid: Object.values(checks).every(Boolean),
|
||||
strength: strength,
|
||||
checks: checks
|
||||
};
|
||||
}
|
||||
|
||||
updateRequirementIndicators(checks) {
|
||||
const container = $(this.options.requirementsContainer);
|
||||
|
||||
container.find('.requirement').each((index, element) => {
|
||||
const requirement = $(element);
|
||||
const rule = requirement.data('rule');
|
||||
const icon = requirement.find('i');
|
||||
|
||||
if (checks[rule]) {
|
||||
requirement.addClass('valid');
|
||||
icon.removeClass('fa-times text-danger').addClass('fa-check text-success');
|
||||
} else {
|
||||
requirement.removeClass('valid');
|
||||
icon.removeClass('fa-check text-success').addClass('fa-times text-danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
calculateStrength(password, checks) {
|
||||
if (!password) return 0;
|
||||
|
||||
let score = 0;
|
||||
const passedChecks = Object.values(checks).filter(Boolean).length;
|
||||
const totalChecks = Object.keys(checks).length;
|
||||
|
||||
// Base score from requirement checks (60% of total)
|
||||
score += (passedChecks / totalChecks) * 60;
|
||||
|
||||
// Length bonus (20% of total)
|
||||
if (password.length >= 16) score += 20;
|
||||
else if (password.length >= 12) score += 15;
|
||||
else if (password.length >= 10) score += 10;
|
||||
else if (password.length >= 8) score += 5;
|
||||
|
||||
// Character variety bonus (10% of total)
|
||||
const uniqueChars = new Set(password.toLowerCase()).size;
|
||||
const varietyRatio = uniqueChars / password.length;
|
||||
if (varietyRatio > 0.8) score += 10;
|
||||
else if (varietyRatio > 0.6) score += 7;
|
||||
else if (varietyRatio > 0.4) score += 5;
|
||||
|
||||
// Pattern penalties (10% of total)
|
||||
if (!this.hasSequentialChars(password)) score += 5;
|
||||
if (!this.hasRepeatedChars(password)) score += 5;
|
||||
|
||||
// Convert score to 0-5 scale
|
||||
return Math.min(Math.floor(score / 20), 5);
|
||||
}
|
||||
|
||||
updateStrengthIndicator(strength) {
|
||||
const indicator = $(this.options.strengthIndicator);
|
||||
const textElement = $(this.options.strengthText);
|
||||
|
||||
const strengthLevels = [
|
||||
this.translations.very_weak,
|
||||
this.translations.weak,
|
||||
this.translations.fair,
|
||||
this.translations.good,
|
||||
this.translations.strong
|
||||
];
|
||||
|
||||
indicator.attr('data-strength', strength);
|
||||
textElement.text(strengthLevels[strength] || strengthLevels[0]);
|
||||
|
||||
// Update text color
|
||||
const colorClasses = ['text-danger', 'text-danger', 'text-warning', 'text-info', 'text-success'];
|
||||
textElement.removeClass('text-danger text-warning text-info text-success')
|
||||
.addClass(colorClasses[strength] || 'text-danger');
|
||||
}
|
||||
|
||||
checkPasswordMatch() {
|
||||
const password = $(this.options.passwordField).val();
|
||||
const confirmPassword = $(this.options.confirmField).val();
|
||||
const matchIndicator = $(this.options.matchIndicator);
|
||||
const matchText = $(this.options.matchText);
|
||||
|
||||
if (!confirmPassword) {
|
||||
matchIndicator.hide();
|
||||
return false;
|
||||
}
|
||||
|
||||
matchIndicator.show();
|
||||
|
||||
const isMatch = password === confirmPassword;
|
||||
|
||||
if (isMatch) {
|
||||
matchText.removeClass('text-danger').addClass('text-success')
|
||||
.text(this.translations.match);
|
||||
} else {
|
||||
matchText.removeClass('text-success').addClass('text-danger')
|
||||
.text(this.translations.no_match);
|
||||
}
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
const password = $(this.options.passwordField).val();
|
||||
const validation = this.validatePassword(password);
|
||||
const passwordsMatch = this.checkPasswordMatch();
|
||||
|
||||
return validation.isValid && passwordsMatch;
|
||||
}
|
||||
|
||||
showValidationErrors() {
|
||||
const password = $(this.options.passwordField).val();
|
||||
const validation = this.validatePassword(password);
|
||||
|
||||
if (!validation.isValid) {
|
||||
// Scroll to password field and highlight issues
|
||||
$(this.options.passwordField)[0].scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
|
||||
// Add shake animation to password field
|
||||
$(this.options.passwordField).addClass('shake');
|
||||
setTimeout(() => {
|
||||
$(this.options.passwordField).removeClass('shake');
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
isCommonPassword(password) {
|
||||
const commonPasswords = [
|
||||
'password', 'password123', '123456', '123456789', 'qwerty',
|
||||
'abc123', 'password1', 'admin', 'administrator', 'root',
|
||||
'guest', 'test', 'demo', 'welcome', 'login', 'user',
|
||||
'12345678', '1234567890', 'qwerty123', 'letmein',
|
||||
'monkey', 'dragon', 'master', 'shadow', 'superman'
|
||||
];
|
||||
|
||||
return commonPasswords.includes(password.toLowerCase());
|
||||
}
|
||||
|
||||
hasSequentialChars(password) {
|
||||
const lower = password.toLowerCase();
|
||||
for (let i = 0; i < lower.length - 2; i++) {
|
||||
const char1 = lower.charCodeAt(i);
|
||||
const char2 = lower.charCodeAt(i + 1);
|
||||
const char3 = lower.charCodeAt(i + 2);
|
||||
|
||||
if ((char2 === char1 + 1 && char3 === char2 + 1) ||
|
||||
(char2 === char1 - 1 && char3 === char2 - 1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hasRepeatedChars(password) {
|
||||
return /(.)\1{3,}/.test(password);
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
getPasswordStrength() {
|
||||
const password = $(this.options.passwordField).val();
|
||||
return this.calculateStrength(password, this.getChecks(password));
|
||||
}
|
||||
|
||||
getChecks(password) {
|
||||
return {
|
||||
length: password.length >= this.options.minLength && password.length <= this.options.maxLength,
|
||||
uppercase: this.options.requireUppercase ? /[A-Z]/.test(password) : true,
|
||||
lowercase: this.options.requireLowercase ? /[a-z]/.test(password) : true,
|
||||
number: this.options.requireNumbers ? /[0-9]/.test(password) : true,
|
||||
special: this.options.requireSpecialChars ? /[^A-Za-z0-9]/.test(password) : true,
|
||||
notCommon: this.options.checkCommonPasswords ? !this.isCommonPassword(password) : true
|
||||
};
|
||||
}
|
||||
|
||||
reset() {
|
||||
$(this.options.strengthIndicator).attr('data-strength', 0);
|
||||
$(this.options.strengthText).text('');
|
||||
$(this.options.matchIndicator).hide();
|
||||
$(this.options.requirementsContainer + ' .requirement')
|
||||
.removeClass('valid')
|
||||
.find('i')
|
||||
.removeClass('fa-check text-success')
|
||||
.addClass('fa-times text-danger');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize if jQuery is available
|
||||
if (typeof $ !== 'undefined') {
|
||||
$(document).ready(function() {
|
||||
// Auto-initialize on pages with password fields
|
||||
if ($('.password-field, .password-strength-container').length > 0) {
|
||||
window.passwordValidator = new PasswordValidator();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PasswordValidator;
|
||||
}
|
||||
|
||||
// Global fallback
|
||||
if (typeof window !== 'undefined') {
|
||||
window.PasswordValidator = PasswordValidator;
|
||||
}
|
||||
Reference in New Issue
Block a user