- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
413 lines
13 KiB
PHP
413 lines
13 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
|
|
defined('BASEPATH') or exit('No direct script access allowed');
|
|
|
|
/**
|
|
* Client Notification Service
|
|
* Handles notifications for client portal users
|
|
*
|
|
* @package Desk-Moloni
|
|
* @version 3.0.0
|
|
* @author Descomplicar Business Solutions
|
|
*/
|
|
class ClientNotificationService
|
|
{
|
|
private $CI;
|
|
private $notificationsTable = 'desk_moloni_client_notifications';
|
|
|
|
public function __construct()
|
|
{
|
|
$this->CI =& get_instance();
|
|
$this->CI->load->database();
|
|
$this->CI->load->helper('date');
|
|
|
|
// Create notifications table if it doesn't exist
|
|
$this->_ensureNotificationsTableExists();
|
|
}
|
|
|
|
/**
|
|
* Create a new notification for a client
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @param string $type Notification type
|
|
* @param string $title Notification title
|
|
* @param string $message Notification message
|
|
* @param int|null $documentId Related document ID (optional)
|
|
* @param string|null $actionUrl Action URL (optional)
|
|
* @return int|false Notification ID or false on failure
|
|
*/
|
|
public function createNotification($clientId, $type, $title, $message, $documentId = null, $actionUrl = null)
|
|
{
|
|
try {
|
|
$data = [
|
|
'client_id' => (int) $clientId,
|
|
'type' => $type,
|
|
'title' => $title,
|
|
'message' => $message,
|
|
'document_id' => $documentId ? (int) $documentId : null,
|
|
'action_url' => $actionUrl,
|
|
'is_read' => 0,
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
// Validate notification type
|
|
if (!$this->_isValidNotificationType($type)) {
|
|
throw new Exception('Invalid notification type: ' . $type);
|
|
}
|
|
|
|
// Validate client exists
|
|
if (!$this->_clientExists($clientId)) {
|
|
throw new Exception('Client does not exist: ' . $clientId);
|
|
}
|
|
|
|
$result = $this->CI->db->insert($this->notificationsTable, $data);
|
|
|
|
if ($result) {
|
|
$notificationId = $this->CI->db->insert_id();
|
|
|
|
// Log notification creation
|
|
log_message('info', "Notification created: ID {$notificationId} for client {$clientId}");
|
|
|
|
return $notificationId;
|
|
}
|
|
|
|
return false;
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Create notification error: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get notifications for a client
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @param bool $unreadOnly Get only unread notifications
|
|
* @param int $limit Maximum number of notifications
|
|
* @param int $offset Offset for pagination
|
|
* @return array Notifications
|
|
*/
|
|
public function getClientNotifications($clientId, $unreadOnly = false, $limit = 20, $offset = 0)
|
|
{
|
|
try {
|
|
$this->CI->db->where('client_id', $clientId);
|
|
|
|
if ($unreadOnly) {
|
|
$this->CI->db->where('is_read', 0);
|
|
}
|
|
|
|
$query = $this->CI->db->order_by('created_at', 'DESC')
|
|
->limit($limit, $offset)
|
|
->get($this->notificationsTable);
|
|
|
|
$notifications = $query->result_array();
|
|
|
|
// Format notifications
|
|
foreach ($notifications as &$notification) {
|
|
$notification['id'] = (int) $notification['id'];
|
|
$notification['client_id'] = (int) $notification['client_id'];
|
|
$notification['document_id'] = $notification['document_id'] ? (int) $notification['document_id'] : null;
|
|
$notification['is_read'] = (bool) $notification['is_read'];
|
|
}
|
|
|
|
return $notifications;
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Get client notifications error: ' . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get unread notifications count for a client
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @return int Unread count
|
|
*/
|
|
public function getUnreadCount($clientId)
|
|
{
|
|
try {
|
|
return $this->CI->db->where('client_id', $clientId)
|
|
->where('is_read', 0)
|
|
->count_all_results($this->notificationsTable);
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Get unread count error: ' . $e->getMessage());
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark notification as read
|
|
*
|
|
* @param int $notificationId Notification ID
|
|
* @param int $clientId Client ID (for security check)
|
|
* @return bool Success status
|
|
*/
|
|
public function markAsRead($notificationId, $clientId)
|
|
{
|
|
try {
|
|
$this->CI->db->where('id', $notificationId)
|
|
->where('client_id', $clientId);
|
|
|
|
$result = $this->CI->db->update($this->notificationsTable, [
|
|
'is_read' => 1,
|
|
'read_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
return $result && $this->CI->db->affected_rows() > 0;
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Mark notification as read error: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark all notifications as read for a client
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @return bool Success status
|
|
*/
|
|
public function markAllAsRead($clientId)
|
|
{
|
|
try {
|
|
$this->CI->db->where('client_id', $clientId)
|
|
->where('is_read', 0);
|
|
|
|
$result = $this->CI->db->update($this->notificationsTable, [
|
|
'is_read' => 1,
|
|
'read_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
return $result;
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Mark all notifications as read error: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete old notifications
|
|
*
|
|
* @param int $olderThanDays Delete notifications older than X days
|
|
* @return int Number of deleted notifications
|
|
*/
|
|
public function cleanupOldNotifications($olderThanDays = 90)
|
|
{
|
|
try {
|
|
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$olderThanDays} days"));
|
|
|
|
$this->CI->db->where('created_at <', $cutoffDate);
|
|
$result = $this->CI->db->delete($this->notificationsTable);
|
|
|
|
return $this->CI->db->affected_rows();
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Cleanup old notifications error: ' . $e->getMessage());
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create document notification when a new document is available
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @param int $documentId Document ID
|
|
* @param string $documentType Document type (invoice, estimate, etc.)
|
|
* @param string $documentNumber Document number
|
|
* @return int|false Notification ID or false on failure
|
|
*/
|
|
public function notifyDocumentCreated($clientId, $documentId, $documentType, $documentNumber)
|
|
{
|
|
$title = 'New ' . ucfirst($documentType) . ' Available';
|
|
$message = "A new {$documentType} ({$documentNumber}) is now available for viewing.";
|
|
$actionUrl = site_url("clients/desk_moloni/documents/{$documentId}");
|
|
|
|
return $this->createNotification(
|
|
$clientId,
|
|
'document_created',
|
|
$title,
|
|
$message,
|
|
$documentId,
|
|
$actionUrl
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create payment received notification
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @param int $documentId Document ID
|
|
* @param float $amount Payment amount
|
|
* @param string $documentNumber Document number
|
|
* @return int|false Notification ID or false on failure
|
|
*/
|
|
public function notifyPaymentReceived($clientId, $documentId, $amount, $documentNumber)
|
|
{
|
|
$title = 'Payment Received';
|
|
$message = "Payment of " . number_format($amount, 2) . " received for {$documentNumber}.";
|
|
$actionUrl = site_url("clients/desk_moloni/documents/{$documentId}");
|
|
|
|
return $this->createNotification(
|
|
$clientId,
|
|
'payment_received',
|
|
$title,
|
|
$message,
|
|
$documentId,
|
|
$actionUrl
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create overdue notice notification
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @param int $documentId Document ID
|
|
* @param string $documentNumber Document number
|
|
* @param string $dueDate Due date
|
|
* @return int|false Notification ID or false on failure
|
|
*/
|
|
public function notifyOverdue($clientId, $documentId, $documentNumber, $dueDate)
|
|
{
|
|
$title = 'Payment Overdue';
|
|
$message = "Payment for {$documentNumber} was due on {$dueDate}. Please review your account.";
|
|
$actionUrl = site_url("clients/desk_moloni/documents/{$documentId}");
|
|
|
|
return $this->createNotification(
|
|
$clientId,
|
|
'overdue_notice',
|
|
$title,
|
|
$message,
|
|
$documentId,
|
|
$actionUrl
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create system message notification
|
|
*
|
|
* @param int $clientId Client ID
|
|
* @param string $title Message title
|
|
* @param string $message Message content
|
|
* @return int|false Notification ID or false on failure
|
|
*/
|
|
public function notifySystemMessage($clientId, $title, $message)
|
|
{
|
|
return $this->createNotification(
|
|
$clientId,
|
|
'system_message',
|
|
$title,
|
|
$message
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get notification by ID
|
|
*
|
|
* @param int $notificationId Notification ID
|
|
* @param int $clientId Client ID (for security check)
|
|
* @return array|null Notification data or null if not found
|
|
*/
|
|
public function getNotificationById($notificationId, $clientId)
|
|
{
|
|
try {
|
|
$query = $this->CI->db->where('id', $notificationId)
|
|
->where('client_id', $clientId)
|
|
->get($this->notificationsTable);
|
|
|
|
$notification = $query->row_array();
|
|
|
|
if ($notification) {
|
|
$notification['id'] = (int) $notification['id'];
|
|
$notification['client_id'] = (int) $notification['client_id'];
|
|
$notification['document_id'] = $notification['document_id'] ? (int) $notification['document_id'] : null;
|
|
$notification['is_read'] = (bool) $notification['is_read'];
|
|
}
|
|
|
|
return $notification;
|
|
|
|
} catch (Exception $e) {
|
|
log_message('error', 'Get notification by ID error: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Private Methods
|
|
|
|
/**
|
|
* Ensure notifications table exists
|
|
*/
|
|
private function _ensureNotificationsTableExists()
|
|
{
|
|
if (!$this->CI->db->table_exists($this->notificationsTable)) {
|
|
$this->_createNotificationsTable();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create notifications table
|
|
*/
|
|
private function _createNotificationsTable()
|
|
{
|
|
$sql = "
|
|
CREATE TABLE IF NOT EXISTS `{$this->notificationsTable}` (
|
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
`client_id` int(11) NOT NULL,
|
|
`type` enum('document_created','payment_received','overdue_notice','system_message') NOT NULL,
|
|
`title` varchar(255) NOT NULL,
|
|
`message` text NOT NULL,
|
|
`document_id` int(11) DEFAULT NULL,
|
|
`action_url` varchar(500) DEFAULT NULL,
|
|
`is_read` tinyint(1) NOT NULL DEFAULT 0,
|
|
`created_at` datetime NOT NULL,
|
|
`read_at` datetime DEFAULT NULL,
|
|
PRIMARY KEY (`id`),
|
|
KEY `idx_client_id` (`client_id`),
|
|
KEY `idx_client_unread` (`client_id`, `is_read`),
|
|
KEY `idx_created_at` (`created_at`),
|
|
KEY `idx_document_id` (`document_id`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
";
|
|
|
|
$this->CI->db->query($sql);
|
|
|
|
if ($this->CI->db->error()['code'] !== 0) {
|
|
log_message('error', 'Failed to create notifications table: ' . $this->CI->db->error()['message']);
|
|
} else {
|
|
log_message('info', 'Notifications table created successfully');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if notification type is valid
|
|
*/
|
|
private function _isValidNotificationType($type)
|
|
{
|
|
$validTypes = [
|
|
'document_created',
|
|
'payment_received',
|
|
'overdue_notice',
|
|
'system_message'
|
|
];
|
|
|
|
return in_array($type, $validTypes);
|
|
}
|
|
|
|
/**
|
|
* Check if client exists
|
|
*/
|
|
private function _clientExists($clientId)
|
|
{
|
|
$count = $this->CI->db->where('userid', $clientId)
|
|
->count_all_results('tblclients');
|
|
return $count > 0;
|
|
}
|
|
} |