Implementation of Notification System
This commit is contained in:
@@ -161,7 +161,7 @@ RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
|||||||
|
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
php_flag display_errors Off
|
php_flag display_errors On
|
||||||
# php_value error_reporting -1
|
# php_value error_reporting -1
|
||||||
RedirectMatch 403 ^/\.well-known
|
RedirectMatch 403 ^/\.well-known
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|||||||
215
.htaccess copy
215
.htaccess copy
@@ -1,215 +0,0 @@
|
|||||||
# URL Rewrite Rules - Maps old URLs to new directory structure during migration
|
|
||||||
<IfModule mod_rewrite.c>
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteBase /
|
|
||||||
|
|
||||||
# Don't rewrite existing files or directories
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
|
|
||||||
# === STRIP .PHP EXTENSION ===
|
|
||||||
# Redirect /page.php to /page (301 permanent redirect)
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.+)\.php$ /$1 [R=301,L]
|
|
||||||
# Internally rewrite /page to /page.php if page.php exists
|
|
||||||
RewriteCond %{REQUEST_FILENAME}\.php -f
|
|
||||||
RewriteRule ^(.+)$ $1.php [L]
|
|
||||||
|
|
||||||
# === AUTH PAGES ===
|
|
||||||
RewriteRule ^login$ src/pages/auth/login.php [L]
|
|
||||||
RewriteRule ^register$ src/pages/auth/register.php [L]
|
|
||||||
RewriteRule ^forgot_password$ src/pages/auth/forgot_password.php [L]
|
|
||||||
RewriteRule ^reset_password$ src/pages/auth/reset_password.php [L]
|
|
||||||
RewriteRule ^verify$ src/pages/auth/verify.php [L]
|
|
||||||
RewriteRule ^resend_verification$ src/pages/auth/resend_verification.php [L]
|
|
||||||
RewriteRule ^change_password$ src/pages/auth/change_password.php [L]
|
|
||||||
RewriteRule ^update_password$ src/pages/auth/update_password.php [L]
|
|
||||||
|
|
||||||
# === MEMBERSHIP PAGES ===
|
|
||||||
RewriteRule ^membership$ src/pages/memberships/membership.php [L]
|
|
||||||
RewriteRule ^membership_details$ src/pages/memberships/membership_details.php [L]
|
|
||||||
RewriteRule ^membership_application$ src/pages/memberships/membership_application.php [L]
|
|
||||||
RewriteRule ^membership_payment$ src/pages/memberships/membership_payment.php [L]
|
|
||||||
RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L]
|
|
||||||
RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
|
|
||||||
|
|
||||||
# === BOOKING PAGES ===
|
|
||||||
RewriteRule ^bookings$ src/pages/bookings/bookings.php [L]
|
|
||||||
RewriteRule ^campsites$ src/pages/bookings/campsites.php [L]
|
|
||||||
RewriteRule ^campsite_booking$ src/pages/bookings/campsite_booking.php [L]
|
|
||||||
RewriteRule ^add_campsite$ src/pages/add_campsite.php [L]
|
|
||||||
RewriteRule ^trips$ src/pages/bookings/trips.php [L]
|
|
||||||
RewriteRule ^trip-details$ src/pages/bookings/trip-details.php [L]
|
|
||||||
RewriteRule ^course_details$ src/pages/bookings/course_details.php [L]
|
|
||||||
RewriteRule ^driver_training$ src/pages/bookings/driver_training.php [L]
|
|
||||||
|
|
||||||
# === SHOP PAGES ===
|
|
||||||
RewriteRule ^view_cart$ src/pages/shop/view_cart.php [L]
|
|
||||||
RewriteRule ^add_to_cart$ src/pages/shop/add_to_cart.php [L]
|
|
||||||
RewriteRule ^bar_tabs$ src/pages/shop/bar_tabs.php [L]
|
|
||||||
RewriteRule ^payment_confirmation$ src/pages/shop/payment_confirmation.php [L]
|
|
||||||
RewriteRule ^confirm$ src/pages/shop/confirm.php [L]
|
|
||||||
RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L]
|
|
||||||
|
|
||||||
# === GALLERY PAGES ===
|
|
||||||
RewriteRule ^gallery$ src/pages/gallery/gallery.php [L]
|
|
||||||
RewriteRule ^create_album$ src/pages/gallery/create_album.php [L]
|
|
||||||
RewriteRule ^edit_album$ src/pages/gallery/create_album.php [L]
|
|
||||||
RewriteRule ^view_album$ src/pages/gallery/view_album.php [L]
|
|
||||||
|
|
||||||
# === EVENTS & BLOG PAGES ===
|
|
||||||
RewriteRule ^events$ src/pages/events/events.php [L]
|
|
||||||
RewriteRule ^blog$ src/pages/blog/blog.php [L]
|
|
||||||
RewriteRule ^blog_details$ src/pages/blog/blog_details.php [L]
|
|
||||||
RewriteRule ^best_of_the_eastern_cape_2024$ src/pages/events/best_of_the_eastern_cape_2024.php [L]
|
|
||||||
RewriteRule ^2025_agm_minutes$ src/pages/events/2025_agm_minutes.php [L]
|
|
||||||
RewriteRule ^agm_content$ src/pages/events/agm_content.php [L]
|
|
||||||
RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
|
||||||
|
|
||||||
# === OTHER PAGES ===
|
|
||||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
|
||||||
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
|
||||||
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
|
||||||
RewriteRule ^track-map$ src/pages/track-map.php [L]
|
|
||||||
RewriteRule ^404$ src/pages/other/404.php [L]
|
|
||||||
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
|
|
||||||
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
|
|
||||||
RewriteRule ^bush_mechanics$ src/pages/other/bush_mechanics.php [L]
|
|
||||||
RewriteRule ^indemnity$ src/pages/other/indemnity.php [L]
|
|
||||||
RewriteRule ^indemnity_waiver$ src/pages/other/indemnity_waiver.php [L]
|
|
||||||
RewriteRule ^basic_indemnity$ src/pages/other/basic_indemnity.php [L]
|
|
||||||
RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L]
|
|
||||||
|
|
||||||
# === ADMIN PAGES ===
|
|
||||||
RewriteRule ^admin_members$ src/admin/admin_members.php [L]
|
|
||||||
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
|
|
||||||
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
|
|
||||||
RewriteRule ^admin_events$ src/admin/admin_events.php [L]
|
|
||||||
RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L]
|
|
||||||
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
|
|
||||||
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
|
|
||||||
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
|
|
||||||
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
|
|
||||||
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
|
|
||||||
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
|
|
||||||
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
|
|
||||||
|
|
||||||
# === API/AJAX ENDPOINTS ===
|
|
||||||
RewriteRule ^fetch_users$ src/api/fetch_users.php [L]
|
|
||||||
RewriteRule ^fetch_drinks$ src/api/fetch_drinks.php [L]
|
|
||||||
RewriteRule ^fetch_bar_tabs$ src/api/fetch_bar_tabs.php [L]
|
|
||||||
RewriteRule ^get_campsites$ src/api/get_campsites.php [L]
|
|
||||||
RewriteRule ^get_tab_total$ src/api/get_tab_total.php [L]
|
|
||||||
RewriteRule ^google_validate_login$ src/api/google_validate_login.php [L]
|
|
||||||
|
|
||||||
# === PROCESSORS ===
|
|
||||||
RewriteRule ^validate_login$ src/processors/validate_login.php [L]
|
|
||||||
RewriteRule ^register_user$ src/processors/register_user.php [L]
|
|
||||||
RewriteRule ^process_application$ src/processors/process_application.php [L]
|
|
||||||
RewriteRule ^process_booking$ src/processors/process_booking.php [L]
|
|
||||||
RewriteRule ^process_camp_booking$ src/processors/process_camp_booking.php [L]
|
|
||||||
RewriteRule ^process_course_booking$ src/processors/process_course_booking.php [L]
|
|
||||||
RewriteRule ^process_trip_booking$ src/processors/process_trip_booking.php [L]
|
|
||||||
RewriteRule ^process_membership_payment$ src/processors/process_membership_payment.php [L]
|
|
||||||
RewriteRule ^process_payments$ src/processors/process_payments.php [L]
|
|
||||||
RewriteRule ^process_eft$ src/processors/process_eft.php [L]
|
|
||||||
RewriteRule ^submit_order$ src/processors/submit_order.php [L]
|
|
||||||
RewriteRule ^submit_pop$ src/processors/submit_pop.php [L]
|
|
||||||
RewriteRule ^process_signature$ src/processors/process_signature.php [L]
|
|
||||||
RewriteRule ^create_bar_tab$ src/processors/create_bar_tab.php [L]
|
|
||||||
RewriteRule ^update_application$ src/processors/update_application.php [L]
|
|
||||||
RewriteRule ^update_user$ src/processors/update_user.php [L]
|
|
||||||
RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [L]
|
|
||||||
RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L]
|
|
||||||
RewriteRule ^logout$ src/processors/logout.php [L]
|
|
||||||
RewriteRule ^process_trip$ src/processors/process_trip.php [L]
|
|
||||||
RewriteRule ^process_event$ src/processors/process_event.php [L]
|
|
||||||
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
|
|
||||||
RewriteRule ^toggle_event_published$ src/processors/toggle_event_published.php [L]
|
|
||||||
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
|
|
||||||
RewriteRule ^delete_event$ src/processors/delete_event.php [L]
|
|
||||||
RewriteRule ^save_album$ src/processors/save_album.php [L]
|
|
||||||
RewriteRule ^update_album$ src/processors/update_album.php [L]
|
|
||||||
RewriteRule ^delete_album$ src/processors/delete_album.php [L]
|
|
||||||
RewriteRule ^delete_photo$ src/processors/delete_photo.php [L]
|
|
||||||
RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
|
|
||||||
RewriteRule ^link_membership_user$ src/processors/link_membership_user.php [L]
|
|
||||||
RewriteRule ^unlink_membership_user$ src/processors/unlink_membership_user.php [L]
|
|
||||||
|
|
||||||
# Blog routes
|
|
||||||
RewriteRule ^admin_blogs$ src/pages/blog/admin_blogs.php [L]
|
|
||||||
RewriteRule ^user_blogs$ src/pages/blog/user_blogs.php [L]
|
|
||||||
RewriteRule ^blog_read$ src/pages/blog/blog_read.php [L]
|
|
||||||
RewriteRule ^blog_edit$ src/pages/blog/blog_edit.php [L]
|
|
||||||
RewriteRule ^blog_create$ src/processors/blog/blog_create.php [L]
|
|
||||||
RewriteRule ^blog_delete$ src/processors/blog/blog_delete.php [L]
|
|
||||||
RewriteRule ^publish_blog$ src/processors/blog/publish_blog.php [L]
|
|
||||||
RewriteRule ^blog_unpublish$ src/processors/blog/blog_unpublish.php [L]
|
|
||||||
RewriteRule ^submit_blog$ src/processors/blog/submit_blog.php [L]
|
|
||||||
RewriteRule ^upload_blog_image$ src/processors/blog/upload_blog_image.php [L]
|
|
||||||
RewriteRule ^autosave$ src/processors/blog/autosave.php [L]
|
|
||||||
|
|
||||||
</IfModule>
|
|
||||||
|
|
||||||
php_flag display_errors On
|
|
||||||
# php_value error_reporting -1
|
|
||||||
RedirectMatch 403 ^/\.well-known
|
|
||||||
Options -Indexes
|
|
||||||
|
|
||||||
<FilesMatch "\.(env|sql|bak|zip|tar|gz|ini)$">
|
|
||||||
Require all denied
|
|
||||||
</FilesMatch>
|
|
||||||
|
|
||||||
ErrorDocument 404 /404.php
|
|
||||||
|
|
||||||
<RequireAll>
|
|
||||||
Require all granted
|
|
||||||
Require not ip 4.222.252.98
|
|
||||||
Require not ip 4.222.252.97
|
|
||||||
</RequireAll>
|
|
||||||
|
|
||||||
<Files .env>
|
|
||||||
Order allow,deny
|
|
||||||
Deny from all
|
|
||||||
</Files>
|
|
||||||
|
|
||||||
|
|
||||||
# ALL CUSTOM ENTRIES SHOULD GO ABOVE THIS LINE
|
|
||||||
# BEGIN IWORX header
|
|
||||||
# This file was created by InterWorx-CP
|
|
||||||
# You may modify this file, but any changes made between
|
|
||||||
# BEGIN IWORX and END IWORX tags may be lost on future
|
|
||||||
# updates. Additionally, changes NOT made between these
|
|
||||||
# tags will not be recognized in the SiteWorx interface.
|
|
||||||
# END IWORX header
|
|
||||||
|
|
||||||
# BEGIN IWORX accesscontrol
|
|
||||||
# END IWORX accesscontrol
|
|
||||||
|
|
||||||
# BEGIN IWORX errordocs
|
|
||||||
# END IWORX errordocs
|
|
||||||
|
|
||||||
# BEGIN IWORX mimetypes
|
|
||||||
# END IWORX mimetypes
|
|
||||||
|
|
||||||
# BEGIN IWORX handlers
|
|
||||||
# END IWORX handlers
|
|
||||||
|
|
||||||
# BEGIN IWORX charset
|
|
||||||
# END IWORX charset
|
|
||||||
|
|
||||||
# BEGIN IWORX redirects
|
|
||||||
# END IWORX redirects
|
|
||||||
|
|
||||||
# BEGIN IWORX phpvars
|
|
||||||
# END IWORX phpvars
|
|
||||||
|
|
||||||
# BEGIN IWORX dirindex
|
|
||||||
# END IWORX dirindex
|
|
||||||
|
|
||||||
# BEGIN IWORX hotlink
|
|
||||||
# END IWORX hotlink
|
|
||||||
|
|
||||||
# BEGIN IWORX passwordprotection
|
|
||||||
# END IWORX passwordprotection
|
|
||||||
|
|
||||||
73
assets/css/notifications.css
Normal file
73
assets/css/notifications.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.notif-avatar-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.notif-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: none;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.notif-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 44px;
|
||||||
|
width: 320px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.notif-panel .notif-empty {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.notif-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
|
.notif-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.notif-item-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.notif-item-body {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.notif-item-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.notif-item-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.notif-close {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
103
assets/js/notifications_panel.js
Normal file
103
assets/js/notifications_panel.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/* notifications.js - small admin notification panel
|
||||||
|
Requires jQuery. */
|
||||||
|
(function($){
|
||||||
|
function timeAgo(ts){
|
||||||
|
var seconds = Math.floor((Date.now() - (new Date(ts)).getTime())/1000);
|
||||||
|
if (seconds < 60) return seconds + 's ago';
|
||||||
|
var minutes = Math.floor(seconds/60);
|
||||||
|
if (minutes < 60) return minutes + 'm ago';
|
||||||
|
var hours = Math.floor(minutes/60);
|
||||||
|
if (hours < 24) return hours + 'h ago';
|
||||||
|
var days = Math.floor(hours/24);
|
||||||
|
return days + 'd ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNotifications(list){
|
||||||
|
var $panel = $('#notif-panel');
|
||||||
|
$panel.empty();
|
||||||
|
if (!list || list.length === 0) {
|
||||||
|
$panel.append('<div class="notif-empty">No notifications</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.forEach(function(n){
|
||||||
|
var actorAvatar = (n.data && n.data.actor_avatar) ? n.data.actor_avatar : 'assets/images/icons/user.png';
|
||||||
|
// Prefer the notification payload title (n.data.title). Do NOT fall back to the event string.
|
||||||
|
var title = (n.data && n.data.title) ? n.data.title : 'Notification';
|
||||||
|
var time = n.time_created || new Date().toISOString();
|
||||||
|
var read = (n.read_by && Array.isArray(n.read_by) && n.read_by.length>0);
|
||||||
|
var $item = $('<div class="notif-item" data-id="'+n.id+'">');
|
||||||
|
$item.append('<img class="notif-item-avatar" src="'+actorAvatar+'" alt="avatar">');
|
||||||
|
var $body = $('<div class="notif-item-body">');
|
||||||
|
$body.append('<div class="notif-item-title">'+escapeHtml(title)+'</div>');
|
||||||
|
$body.append('<div class="notif-item-meta">'+timeAgo(time)+'</div>');
|
||||||
|
$item.append($body);
|
||||||
|
$item.append('<button class="notif-close" title="Mark read">×</button>');
|
||||||
|
$panel.append($item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return String(str).replace(/[&<>"'`]/g, function(s){ return {'&':'&','<':'<','>':'>','"':'"',"'":"'",'`':'`'}[s]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchAndRender(adminId){
|
||||||
|
$.getJSON('/src/api/notifications.php', { action: 'fetch' }, function(resp){
|
||||||
|
if (resp && resp.success) {
|
||||||
|
renderNotifications(resp.notifications);
|
||||||
|
if (resp.unread_count && resp.unread_count > 0) {
|
||||||
|
$('#notif-badge').text(resp.unread_count).show();
|
||||||
|
} else {
|
||||||
|
$('#notif-badge').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch only unread count (used on page load so badge shows without opening panel)
|
||||||
|
function fetchUnreadCount(){
|
||||||
|
$.getJSON('/src/api/notifications.php', { action: 'fetch' }, function(resp){
|
||||||
|
if (resp && resp.success) {
|
||||||
|
if (resp.unread_count && resp.unread_count > 0) {
|
||||||
|
$('#notif-badge').text(resp.unread_count).show();
|
||||||
|
} else {
|
||||||
|
$('#notif-badge').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function(){
|
||||||
|
var $container = $('.notif-avatar-container');
|
||||||
|
if (!$container.length) return;
|
||||||
|
var adminId = $container.data('admin-id');
|
||||||
|
// ensure badge is populated on page load
|
||||||
|
fetchUnreadCount();
|
||||||
|
$container.on('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
$('#notif-panel').toggle();
|
||||||
|
if ($('#notif-panel').is(':visible')) fetchAndRender(adminId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.notif-close', function(e){
|
||||||
|
e.stopPropagation();
|
||||||
|
var $it = $(this).closest('.notif-item');
|
||||||
|
var id = $it.data('id');
|
||||||
|
if (!id) return;
|
||||||
|
$.post('/src/api/notifications.php', { action: 'mark_read', id: id }, function(resp){
|
||||||
|
if (resp && resp.success) {
|
||||||
|
$it.remove();
|
||||||
|
// refresh count
|
||||||
|
fetchAndRender(adminId);
|
||||||
|
}
|
||||||
|
}, 'json');
|
||||||
|
});
|
||||||
|
|
||||||
|
// click outside to close
|
||||||
|
$(document).on('click', function(e){
|
||||||
|
if (!$(e.target).closest('#notif-panel, .notif-avatar-container').length) {
|
||||||
|
$('#notif-panel').hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
13
docs/migrations/006b_create_notifications_table.sql
Normal file
13
docs/migrations/006b_create_notifications_table.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- Migration: create notifications table (corrected: `read_by` nullable)
|
||||||
|
CREATE TABLE IF NOT EXISTS `notifications` (
|
||||||
|
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
`user_id` INT DEFAULT NULL,
|
||||||
|
`event` VARCHAR(100) NOT NULL,
|
||||||
|
`sub_feed` VARCHAR(100) DEFAULT NULL,
|
||||||
|
`data` TEXT DEFAULT NULL,
|
||||||
|
`target_url` VARCHAR(1024) DEFAULT NULL,
|
||||||
|
`read_by` TEXT DEFAULT NULL,
|
||||||
|
`time_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX (`user_id`),
|
||||||
|
INDEX (`sub_feed`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
16
header.php
16
header.php
@@ -25,6 +25,7 @@ require_once($rootDir . "/src/config/env.php");
|
|||||||
require_once($rootDir . "/src/config/session.php");
|
require_once($rootDir . "/src/config/session.php");
|
||||||
require_once($rootDir . "/src/config/connection.php");
|
require_once($rootDir . "/src/config/connection.php");
|
||||||
require_once($rootDir . "/src/config/functions.php");
|
require_once($rootDir . "/src/config/functions.php");
|
||||||
|
require_once($rootDir . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
$is_logged_in = isset($_SESSION['user_id']);
|
$is_logged_in = isset($_SESSION['user_id']);
|
||||||
if (isset($_SESSION['user_id'])) {
|
if (isset($_SESSION['user_id'])) {
|
||||||
@@ -348,11 +349,17 @@ if ($headerStyle === 'light') {
|
|||||||
<div class="profile-menu">
|
<div class="profile-menu">
|
||||||
<div class="profile-info">
|
<div class="profile-info">
|
||||||
<span style="color: <?php echo $textColor; ?>;">Welcome, <?php echo $_SESSION['first_name']; ?></span>
|
<span style="color: <?php echo $textColor; ?>;">Welcome, <?php echo $_SESSION['first_name']; ?></span>
|
||||||
<a href="account_settings">
|
<div class="notif-avatar-container" data-admin-id="<?php echo intval($_SESSION['user_id'] ?? 0); ?>">
|
||||||
<img src="<?php echo $_SESSION['profile_pic']; ?>?v=<?php echo time(); ?>" alt="Profile Picture" class="profile-pic">
|
<a href="account_settings">
|
||||||
</a>
|
<img src="<?php echo $_SESSION['profile_pic']; ?>?v=<?php echo time(); ?>" alt="Profile Picture" class="profile-pic">
|
||||||
|
</a>
|
||||||
|
<span id="notif-badge" class="notif-badge"></span>
|
||||||
|
</div>
|
||||||
|
<div id="notif-panel" class="notif-panel" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<link rel="stylesheet" href="assets/css/notifications.css">
|
||||||
|
<script src="assets/js/notifications_panel.js" defer></script>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<a href="login" class="theme-btn style-two bgc-secondary">
|
<a href="login" class="theme-btn style-two bgc-secondary">
|
||||||
<span data-hover="Log In">Log In</span>
|
<span data-hover="Log In">Log In</span>
|
||||||
@@ -372,6 +379,9 @@ if ($headerStyle === 'light') {
|
|||||||
const profileInfo = document.querySelector('.profile-info');
|
const profileInfo = document.querySelector('.profile-info');
|
||||||
if (profileInfo) {
|
if (profileInfo) {
|
||||||
profileInfo.addEventListener('click', function(event) {
|
profileInfo.addEventListener('click', function(event) {
|
||||||
|
// Ignore clicks on the notifications avatar so the notif panel
|
||||||
|
// can handle its own toggle without also toggling the profile dropdown.
|
||||||
|
if (event.target.closest && event.target.closest('.notif-avatar-container')) return;
|
||||||
const dropdownMenu = document.querySelector('.dropdown-menu2');
|
const dropdownMenu = document.querySelector('.dropdown-menu2');
|
||||||
if (dropdownMenu) {
|
if (dropdownMenu) {
|
||||||
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
|
dropdownMenu.style.display = dropdownMenu.style.display === 'block' ? 'none' : 'block';
|
||||||
|
|||||||
@@ -120,3 +120,6 @@
|
|||||||
[2025-12-15 15:48:43] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
[2025-12-15 15:48:43] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
[2025-12-15 15:48:43] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
[2025-12-15 15:48:43] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
[2025-12-15 15:48:43] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
[2025-12-15 15:48:43] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
[2025-12-16 22:38:31] IKFLESZTKFM4HWWS76131L8HK9BYF96P | CONTEXT: IKHOKHA App ID
|
||||||
|
[2025-12-16 22:38:31] /public-api/v1/api/payments/history | CONTEXT: IKHOKHA Payload to Sign
|
||||||
|
[2025-12-16 22:38:31] b3b592829090d2cfd0912ccbdec73db18742088d01a2d2bee9f0eacdf37a6b26 | CONTEXT: IKHOKHA Signature
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ==========================================================
|
* ==========================================================
|
||||||
@@ -272,6 +273,14 @@ if (in_array($normalized, ['PAID', 'SUCCESS', 'COMPLETED', 'SETTLED'], true)) {
|
|||||||
nl2br($message)
|
nl2br($message)
|
||||||
);
|
);
|
||||||
sendAdminNotification($subject, nl2br($message));
|
sendAdminNotification($subject, nl2br($message));
|
||||||
|
$event = 'new_payment_received';
|
||||||
|
$sub_feed = 'payments';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Payment Received for Payment ID: {$localPaymentId}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
41
src/api/notifications.php
Normal file
41
src/api/notifications.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
// Ensure environment is loaded before attempting DB connection
|
||||||
|
require_once __DIR__ . '/../config/env.php';
|
||||||
|
require_once __DIR__ . '/../config/connection.php';
|
||||||
|
// helper filename uses singular in this repo: notification_helper.php
|
||||||
|
require_once __DIR__ . '/../helpers/notification_helper.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$admin_id = $_SESSION['user_id'] ?? null;
|
||||||
|
$action = $_REQUEST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'fetch') {
|
||||||
|
$subs = getAdminSubscriptions($admin_id);
|
||||||
|
$notes = fetchNotifications($admin_id, $subs, 50);
|
||||||
|
echo json_encode(['success' => true, 'notifications' => $notes, 'unread_count' => getUnreadCount($admin_id, $subs)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'mark_read') {
|
||||||
|
if (!$admin_id) { echo json_encode(['success' => false, 'error' => 'unauthenticated']); exit; }
|
||||||
|
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||||
|
if (!$id) { echo json_encode(['success' => false, 'error' => 'missing_id']); exit; }
|
||||||
|
$ok = markNotificationRead($id, $admin_id);
|
||||||
|
echo json_encode(['success' => (bool)$ok]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'add') {
|
||||||
|
// internal use: create a notification
|
||||||
|
$target = isset($_POST['user_id']) ? intval($_POST['user_id']) : null;
|
||||||
|
$event = $_POST['event'] ?? '';
|
||||||
|
$sub_feed = $_POST['sub_feed'] ?? null;
|
||||||
|
$data = isset($_POST['data']) ? json_decode($_POST['data'], true) : [];
|
||||||
|
$target_url = $_POST['target_url'] ?? null;
|
||||||
|
$id = addNotification($target, $event, $sub_feed, $data, $target_url);
|
||||||
|
echo json_encode(['success' => (bool)$id, 'id' => $id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => false, 'error' => 'invalid_action']);
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
// Disable mysqli exceptions so we can handle connection errors gracefully
|
// Disable mysqli exceptions so we can handle connection errors gracefully
|
||||||
mysqli_report(MYSQLI_REPORT_OFF);
|
mysqli_report(MYSQLI_REPORT_OFF);
|
||||||
|
|
||||||
$dbhost = $_ENV['DB_HOST'];
|
// Read from environment or fallback to getenv; keep empty string if not set to avoid PHP warnings
|
||||||
$dbuser = $_ENV['DB_USER'];
|
$dbhost = $_ENV['DB_HOST'] ?? getenv('DB_HOST') ?? '';
|
||||||
$dbpass = $_ENV['DB_PASS'];
|
$dbuser = $_ENV['DB_USER'] ?? getenv('DB_USER') ?? '';
|
||||||
$dbname = $_ENV['DB_NAME'];
|
$dbpass = $_ENV['DB_PASS'] ?? getenv('DB_PASS') ?? '';
|
||||||
$salt = $_ENV['SALT'];
|
$dbname = $_ENV['DB_NAME'] ?? getenv('DB_NAME') ?? '';
|
||||||
|
$salt = $_ENV['SALT'] ?? getenv('SALT') ?? '';
|
||||||
|
|
||||||
// echo "hello. ". $dbhost;
|
// echo "hello. ". $dbhost;
|
||||||
|
|
||||||
|
|||||||
151
src/helpers/notification_helper.php
Normal file
151
src/helpers/notification_helper.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
// Notifications helper
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config/env.php';
|
||||||
|
require_once __DIR__ . '/../config/connection.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../classes/DatabaseService.php';
|
||||||
|
|
||||||
|
function addNotification($target_user_id, $event, $sub_feed = null, $data = [], $target_url = null) {
|
||||||
|
global $db, $conn;
|
||||||
|
if (!isset($db) && isset($conn)) {
|
||||||
|
$db = new DatabaseService($conn);
|
||||||
|
}
|
||||||
|
$ds = $db;
|
||||||
|
if (!$ds) {
|
||||||
|
// Try to initialize DatabaseService if connection exists
|
||||||
|
if (isset($conn) && $conn) {
|
||||||
|
$ds = new DatabaseService($conn);
|
||||||
|
} else {
|
||||||
|
// No DB available
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$data_json = $data ? json_encode($data) : null;
|
||||||
|
$read_by_json = json_encode([]);
|
||||||
|
$query = "INSERT INTO notifications (user_id, event, sub_feed, data, target_url, read_by) VALUES (?,?,?,?,?,?)";
|
||||||
|
return $ds->insert($query, [$target_user_id, $event, $sub_feed, $data_json, $target_url, $read_by_json], "isssss");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchNotifications($admin_user_id = null, $subscriptions = [], $limit = 50) {
|
||||||
|
global $db, $conn;
|
||||||
|
if (!isset($db) && isset($conn)) {
|
||||||
|
$db = new DatabaseService($conn);
|
||||||
|
}
|
||||||
|
$ds = $db;
|
||||||
|
if (!$ds) {
|
||||||
|
if (isset($conn) && $conn) {
|
||||||
|
$ds = new DatabaseService($conn);
|
||||||
|
} else {
|
||||||
|
// No DB available — return empty list to avoid fatal error
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$params = [];
|
||||||
|
$types = "";
|
||||||
|
$sql = "SELECT * FROM notifications";
|
||||||
|
$where = [];
|
||||||
|
// Admin-only: fetch notifications targeted to admins (user_id IS NULL) or global, or specifically to an admin
|
||||||
|
if ($admin_user_id) {
|
||||||
|
$where[] = "(user_id IS NULL OR user_id = ?)";
|
||||||
|
$params[] = $admin_user_id;
|
||||||
|
$types .= "i";
|
||||||
|
}
|
||||||
|
if (!empty($subscriptions)) {
|
||||||
|
// build IN (...) list safely by placeholders
|
||||||
|
$placeholders = implode(',', array_fill(0, count($subscriptions), '?'));
|
||||||
|
$where[] = "(sub_feed IN ($placeholders))";
|
||||||
|
foreach ($subscriptions as $s) { $params[] = $s; $types .= "s"; }
|
||||||
|
}
|
||||||
|
if (!empty($where)) {
|
||||||
|
$sql .= " WHERE " . implode(' AND ', $where);
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY time_created DESC LIMIT ?";
|
||||||
|
$params[] = $limit; $types .= "i";
|
||||||
|
|
||||||
|
$results = $ds->select($sql, $params, $types);
|
||||||
|
if ($results === false) {
|
||||||
|
// Query error - return empty list so UI doesn't break
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// decode data JSON and include read_by as array
|
||||||
|
$filtered = [];
|
||||||
|
foreach ($results as $r) {
|
||||||
|
$r['data'] = $r['data'] ? json_decode($r['data'], true) : null;
|
||||||
|
$r['read_by'] = $r['read_by'] ? json_decode($r['read_by'], true) : [];
|
||||||
|
if (!is_array($r['read_by'])) $r['read_by'] = [];
|
||||||
|
// If admin_user_id is provided, skip notifications this admin already read
|
||||||
|
if ($admin_user_id && in_array((int)$admin_user_id, $r['read_by'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$filtered[] = $r;
|
||||||
|
}
|
||||||
|
return $filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markNotificationRead($id, $admin_user_id) {
|
||||||
|
global $conn;
|
||||||
|
if (!$id || !$admin_user_id) return false;
|
||||||
|
if (!isset($conn) || !$conn) return false;
|
||||||
|
$stmt = $conn->prepare("SELECT read_by FROM notifications WHERE id = ? LIMIT 1");
|
||||||
|
$stmt->bind_param("i", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->bind_result($read_by_json);
|
||||||
|
$found = $stmt->fetch();
|
||||||
|
$stmt->close();
|
||||||
|
if (!$found) return false;
|
||||||
|
$read_by = $read_by_json ? json_decode($read_by_json, true) : [];
|
||||||
|
if (!is_array($read_by)) $read_by = [];
|
||||||
|
if (!in_array($admin_user_id, $read_by)) {
|
||||||
|
$read_by[] = (int)$admin_user_id;
|
||||||
|
$new_json = json_encode(array_values($read_by));
|
||||||
|
$u = $conn->prepare("UPDATE notifications SET read_by = ? WHERE id = ?");
|
||||||
|
$u->bind_param("si", $new_json, $id);
|
||||||
|
$res = $u->execute();
|
||||||
|
$u->close();
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUnreadCount($admin_user_id, $subscriptions = []) {
|
||||||
|
global $conn;
|
||||||
|
if (!isset($conn) || !$conn) return 0;
|
||||||
|
$sql = "SELECT id, read_by, sub_feed FROM notifications";
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
$types = "";
|
||||||
|
if ($admin_user_id) {
|
||||||
|
$where[] = "(user_id IS NULL OR user_id = ?)"; $params[] = $admin_user_id; $types .= "i";
|
||||||
|
}
|
||||||
|
if (!empty($subscriptions)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($subscriptions), '?'));
|
||||||
|
$where[] = "(sub_feed IN ($placeholders))";
|
||||||
|
foreach ($subscriptions as $s) { $params[] = $s; $types .= "s"; }
|
||||||
|
}
|
||||||
|
if (!empty($where)) $sql .= " WHERE " . implode(' AND ', $where);
|
||||||
|
$sql .= " ORDER BY time_created DESC";
|
||||||
|
$stmt = $conn->prepare($sql);
|
||||||
|
if ($types) {
|
||||||
|
// bind params dynamically
|
||||||
|
$refs = [];
|
||||||
|
$refs[] = &$types;
|
||||||
|
foreach ($params as $k => $v) { $refs[] = &$params[$k]; }
|
||||||
|
call_user_func_array([$stmt, 'bind_param'], $refs);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
$res = $stmt->get_result();
|
||||||
|
$count = 0;
|
||||||
|
while ($row = $res->fetch_assoc()) {
|
||||||
|
$read_by = $row['read_by'] ? json_decode($row['read_by'], true) : [];
|
||||||
|
if (!is_array($read_by)) $read_by = [];
|
||||||
|
if (!in_array((int)$admin_user_id, $read_by)) $count++;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdminSubscriptions($admin_user_id) {
|
||||||
|
// Placeholder: by default return empty array (all sub_feeds). Implement subscription table later.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -6,11 +6,14 @@ include_once($rootPath . '/header.php');
|
|||||||
$is_logged_in = isset($_SESSION['user_id']);
|
$is_logged_in = isset($_SESSION['user_id']);
|
||||||
if (isset($_SESSION['user_id'])) {
|
if (isset($_SESSION['user_id'])) {
|
||||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
header('Location: login.php');
|
header('Location: login.php');
|
||||||
exit(); // Stop further script execution
|
exit(); // Stop further script execution
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$full_name = getFullName($user_id);
|
||||||
|
|
||||||
//if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
|
//if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
|
||||||
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||||
$stmt->bind_param("i", $user_id);
|
$stmt->bind_param("i", $user_id);
|
||||||
@@ -46,6 +49,16 @@ if ($stmt->execute()) {
|
|||||||
auditLog($user_id, 'MEMBERSHIP_RENEWAL_INITIATED', 'membership_fees', null, ['payment_id' => $payment_id, 'amount' => $payment_amount]);
|
auditLog($user_id, 'MEMBERSHIP_RENEWAL_INITIATED', 'membership_fees', null, ['payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send Notification
|
||||||
|
$event = 'membership_renewal_initiated';
|
||||||
|
$sub_feed = 'membership_renewal';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "Membership Renewal Initiated by {$full_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
|
$checkP = $conn->prepare("SELECT COUNT(*) AS cnt FROM payments WHERE payment_id = ? LIMIT 1");
|
||||||
if ($checkP) {
|
if ($checkP) {
|
||||||
$checkP->bind_param('s', $payment_id);
|
$checkP->bind_param('s', $payment_id);
|
||||||
|
|||||||
@@ -224,6 +224,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
// Audit: membership application submitted
|
// Audit: membership application submitted
|
||||||
if (function_exists('auditLog')) {
|
if (function_exists('auditLog')) {
|
||||||
auditLog($user_id, 'MEMBERSHIP_APPLICATION_SUBMITTED', 'membership_application', null, ['payment_id' => $payment_id, 'amount' => $payment_amount ?? null]);
|
auditLog($user_id, 'MEMBERSHIP_APPLICATION_SUBMITTED', 'membership_application', null, ['payment_id' => $payment_id, 'amount' => $payment_amount ?? null]);
|
||||||
|
$event = 'new_application_submitted';
|
||||||
|
$sub_feed = 'membership_applications';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "New Membership Application from {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
}
|
}
|
||||||
header("Location: indemnity");
|
header("Location: indemnity");
|
||||||
// Success message
|
// Success message
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
// Start session to retrieve the logged-in user's ID
|
// Start session to retrieve the logged-in user's ID
|
||||||
session_start();
|
session_start();
|
||||||
@@ -84,6 +85,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (function_exists('auditLog')) {
|
if (function_exists('auditLog')) {
|
||||||
auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]);
|
auditLog($user_id, 'BOOKING_CREATED', 'bookings', $booking_id, ['total_amount' => $total_amount, 'from' => $from_date, 'to' => $to_date]);
|
||||||
}
|
}
|
||||||
|
$event = 'new_booking_created';
|
||||||
|
$sub_feed = 'bookings';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Booking Created with Booking ID: {$booking_id}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
// Redirect to success page or display success message
|
// Redirect to success page or display success message
|
||||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
|
echo "<script>alert('Booking successfully created!'); window.location.href = 'booking.php';</script>";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
|
|
||||||
@@ -122,6 +123,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (function_exists('auditLog')) {
|
if (function_exists('auditLog')) {
|
||||||
auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
auditLog($user_id, 'COURSE_BOOKING_CREATED', 'bookings', $booking_id, ['course_id' => $course_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||||
}
|
}
|
||||||
|
$event = 'new_course_booking_created';
|
||||||
|
$sub_feed = 'bookings';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Course Booking Created : {$payment_id}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
if ($payment_amount < 1) {
|
if ($payment_amount < 1) {
|
||||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||||
@@ -142,8 +151,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
// Create iKhokha payment link
|
// Create iKhokha payment link
|
||||||
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
$resp = createIkhokhaPayment($payment_id, $payment_amount, $description, $publicRef);
|
||||||
|
|
||||||
// Send invoice and admin notification (keep for records)
|
// Send invoice and admin notification (keep for records)
|
||||||
// sendInvoice(getEmail($user_id), getFullName($user_id), $eft_id, formatCurrency($payment_amount), $description);
|
|
||||||
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
sendAdminNotification('New Course Booking - '.getFullName($user_id), getFullName($user_id).' has booked for '.$description);
|
||||||
|
|
||||||
// Redirect user to payment link if available
|
// Redirect user to payment link if available
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ $rootPath = dirname(dirname(__DIR__));
|
|||||||
require_once($rootPath . "/src/config/env.php");
|
require_once($rootPath . "/src/config/env.php");
|
||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// Get the trip_id from the request (ensure it's sanitized)
|
// Get the trip_id from the request (ensure it's sanitized)
|
||||||
@@ -131,6 +132,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
auditLog($user_id, 'TRIP_BOOKING_CREATED', 'bookings', $booking_id, ['trip_id' => $trip_id, 'payment_id' => $payment_id, 'amount' => $payment_amount]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create notification for new booking
|
||||||
|
$event = 'new_trip_booking_created';
|
||||||
|
$sub_feed = 'bookings';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'] ?? null,
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'] ?? null, // used by UI to show avatar
|
||||||
|
'title' => "New Trip Booking Created: {$payment_id}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
if ($payment_amount < 1) {
|
if ($payment_amount < 1) {
|
||||||
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
if (processZeroPayment($payment_id, $payment_amount, $description)) {
|
||||||
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
echo "<script>alert('Booking successfully created!'); window.location.href = 'bookings.php';</script>";
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ require_once($rootPath . "/src/config/session.php");
|
|||||||
require_once($rootPath . "/src/config/connection.php");
|
require_once($rootPath . "/src/config/connection.php");
|
||||||
require_once($rootPath . "/src/config/functions.php");
|
require_once($rootPath . "/src/config/functions.php");
|
||||||
require_once($rootPath . '/google-client/vendor/autoload.php'); // Add this line for Google Client
|
require_once($rootPath . '/google-client/vendor/autoload.php'); // Add this line for Google Client
|
||||||
|
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||||
|
|
||||||
|
|
||||||
// Check if connection is established
|
// Check if connection is established
|
||||||
if (!$conn) {
|
if (!$conn) {
|
||||||
@@ -37,6 +39,8 @@ if (isset($_GET['code'])) {
|
|||||||
$last_name = $google_account_info->family_name;
|
$last_name = $google_account_info->family_name;
|
||||||
$picture = $google_account_info->picture;
|
$picture = $google_account_info->picture;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check if the user exists in the database
|
// Check if the user exists in the database
|
||||||
$query = "SELECT * FROM users WHERE email = ?";
|
$query = "SELECT * FROM users WHERE email = ?";
|
||||||
$stmt = $conn->prepare($query);
|
$stmt = $conn->prepare($query);
|
||||||
@@ -53,12 +57,20 @@ if (isset($_GET['code'])) {
|
|||||||
$stmt->bind_param("sssssi", $email, $first_name, $last_name, $picture, $password, $is_verified);
|
$stmt->bind_param("sssssi", $email, $first_name, $last_name, $picture, $password, $is_verified);
|
||||||
if ($stmt->execute()) {
|
if ($stmt->execute()) {
|
||||||
// User successfully registered, set session and redirect
|
// User successfully registered, set session and redirect
|
||||||
sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just created an account using Google Login.');
|
|
||||||
$_SESSION['user_id'] = $conn->insert_id;
|
$_SESSION['user_id'] = $conn->insert_id;
|
||||||
$_SESSION['first_name'] = $first_name;
|
$_SESSION['first_name'] = $first_name;
|
||||||
$_SESSION['profile_pic'] = $picture;
|
$_SESSION['profile_pic'] = $picture;
|
||||||
processLegacyMembership($_SESSION['user_id']);
|
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// Send Notification
|
||||||
|
$event = 'user_login';
|
||||||
|
$sub_feed = 'logins';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $conn->insert_id,
|
||||||
|
'actor_avatar' => $picture, // used by UI to show avatar
|
||||||
|
'title' => "User Login by {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
} else {
|
} else {
|
||||||
@@ -72,8 +84,17 @@ if (isset($_GET['code'])) {
|
|||||||
$_SESSION['user_id'] = $row['user_id'];
|
$_SESSION['user_id'] = $row['user_id'];
|
||||||
$_SESSION['first_name'] = $row['first_name'];
|
$_SESSION['first_name'] = $row['first_name'];
|
||||||
$_SESSION['profile_pic'] = $row['profile_pic'];
|
$_SESSION['profile_pic'] = $row['profile_pic'];
|
||||||
sendEmail('chrispintoza@gmail.com', '4WDCSA: New User Login', $name.' has just logged in using Google Login.');
|
|
||||||
// echo json_encode(['status' => 'success', 'message' => 'Google login successful']);
|
// Send Notification
|
||||||
|
$event = 'user_login';
|
||||||
|
$sub_feed = 'logins';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "User Login by {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
header("Location: index.php");
|
header("Location: index.php");
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -181,6 +202,16 @@ if (isset($_POST['email']) && isset($_POST['password'])) {
|
|||||||
// Set session timeout (30 minutes)
|
// Set session timeout (30 minutes)
|
||||||
$_SESSION['login_time'] = time();
|
$_SESSION['login_time'] = time();
|
||||||
$_SESSION['session_timeout'] = 1800; // 30 minutes in seconds
|
$_SESSION['session_timeout'] = 1800; // 30 minutes in seconds
|
||||||
|
|
||||||
|
// Send Notification
|
||||||
|
$event = 'user_login';
|
||||||
|
$sub_feed = 'logins';
|
||||||
|
$data = [
|
||||||
|
'actor_id' => $_SESSION['user_id'],
|
||||||
|
'actor_avatar' => $_SESSION['profile_pic'], // used by UI to show avatar
|
||||||
|
'title' => "User Login by {$first_name} {$last_name}"
|
||||||
|
];
|
||||||
|
addNotification(null, $event, $sub_feed, $data, null);
|
||||||
|
|
||||||
auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']);
|
auditLog($row['user_id'], 'LOGIN_SUCCESS', 'users', $row['user_id']);
|
||||||
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
echo json_encode(['status' => 'success', 'message' => 'Successful Login']);
|
||||||
|
|||||||
Reference in New Issue
Block a user