Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
P
phphr
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
phphr
Commits
7d22a012
Commit
7d22a012
authored
Apr 08, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 30 files via Son of Anton
parent
aa7e8d7b
Changes
30
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
30 changed files
with
1479 additions
and
493 deletions
+1479
-493
routes.php
modules/BoardTemplates/routes.php
+12
-6
routes.php
modules/CardTemplates/routes.php
+12
-6
ContractExpiryWarningJob.php
modules/Contracts/Jobs/ContractExpiryWarningJob.php
+27
-13
routes.php
modules/Contracts/routes.php
+28
-13
routes.php
modules/DeductionPresets/routes.php
+12
-6
AutoApplyExpiredDeductionsJob.php
modules/Deductions/Jobs/AutoApplyExpiredDeductionsJob.php
+61
-17
EscalateDeadlineDeductionsJob.php
modules/Deductions/Jobs/EscalateDeadlineDeductionsJob.php
+114
-42
OverallScoreCalculator.php
modules/Evaluations/Calculators/OverallScoreCalculator.php
+32
-14
ProfessionalAutoScoreCalculator.php
...aluations/Calculators/ProfessionalAutoScoreCalculator.php
+76
-25
TechnicalAutoScoreCalculator.php
.../Evaluations/Calculators/TechnicalAutoScoreCalculator.php
+38
-29
CompileEvaluationsJob.php
modules/Evaluations/Jobs/CompileEvaluationsJob.php
+82
-56
EvaluationReminderJob.php
modules/Evaluations/Jobs/EvaluationReminderJob.php
+48
-28
OpenEvaluationCycleJob.php
modules/Evaluations/Jobs/OpenEvaluationCycleJob.php
+79
-36
routes.php
modules/Evaluations/routes.php
+28
-14
routes.php
modules/Holidays/routes.php
+12
-13
LearningGoalReminderJob.php
modules/LearningGoals/Jobs/LearningGoalReminderJob.php
+67
-25
routes.php
modules/LearningGoals/routes.php
+21
-10
MeetingReminderJob.php
modules/Meetings/Jobs/MeetingReminderJob.php
+57
-27
routes.php
modules/Meetings/routes.php
+16
-8
routes.php
modules/Offboarding/routes.php
+8
-4
PIPCheckinReminderJob.php
modules/PIPs/Jobs/PIPCheckinReminderJob.php
+61
-15
routes.php
modules/PIPs/routes.php
+18
-9
CreateRecurringCardsJob.php
modules/RecurringCards/Jobs/CreateRecurringCardsJob.php
+116
-39
routes.php
modules/RecurringCards/routes.php
+12
-6
routes.php
modules/Salary/routes.php
+61
-3
routes.php
modules/SavedFilters/routes.php
+10
-5
routes.php
modules/Schedules/routes.php
+14
-7
routes.php
modules/TeamAvailability/routes.php
+6
-4
routes.php
modules/Unavailability/routes.php
+12
-13
dark-mode.css
public/assets/css/dark-mode.css
+339
-0
No files found.
modules/BoardTemplates/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\BoardTemplates\Controllers\BoardTemplateController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/board-templates'
,
[
BoardTemplateController
::
class
,
'index'
]);
$router
->
post
(
'/board-templates'
,
[
BoardTemplateController
::
class
,
'create'
]);
$router
->
post
(
'/board-templates/from-board/{boardId}'
,
[
BoardTemplateController
::
class
,
'saveFromBoard'
]);
$router
->
delete
(
'/board-templates/{templateId}'
,
[
BoardTemplateController
::
class
,
'delete'
]);
$router
->
get
(
'/board-templates'
,
\Modules\BoardTemplates\Controllers\BoardTemplateController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/board-templates'
,
[
BoardTemplateController
::
class
,
'index'
]);
$router
->
post
(
'/board-templates'
,
\Modules\BoardTemplates\Controllers\BoardTemplateController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/board-templates'
,
[
BoardTemplateController
::
class
,
'create'
]);
$router
->
post
(
'/board-templates/from-board/{boardId}'
,
\Modules\BoardTemplates\Controllers\BoardTemplateController
::
class
,
'saveFromBoard'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/board-templates/from-board/{boardId}'
,
[
BoardTemplateController
::
class
,
'saveFromBoard'
]);
$router
->
delete
(
'/board-templates/{id}'
,
\Modules\BoardTemplates\Controllers\BoardTemplateController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/api/board-templates/{templateId}'
,
[
BoardTemplateController
::
class
,
'delete'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/CardTemplates/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\CardTemplates\Controllers\CardTemplateController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/card-templates'
,
[
CardTemplateController
::
class
,
'index'
]);
$router
->
post
(
'/card-templates'
,
[
CardTemplateController
::
class
,
'create'
]);
$router
->
put
(
'/card-templates/{templateId}'
,
[
CardTemplateController
::
class
,
'update'
]);
$router
->
delete
(
'/card-templates/{templateId}'
,
[
CardTemplateController
::
class
,
'delete'
]);
$router
->
get
(
'/card-templates'
,
\Modules\CardTemplates\Controllers\CardTemplateController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/card-templates'
,
[
CardTemplateController
::
class
,
'index'
]);
$router
->
post
(
'/card-templates'
,
\Modules\CardTemplates\Controllers\CardTemplateController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/card-templates'
,
[
CardTemplateController
::
class
,
'create'
]);
$router
->
put
(
'/card-templates/{id}'
,
\Modules\CardTemplates\Controllers\CardTemplateController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/api/card-templates/{templateId}'
,
[
CardTemplateController
::
class
,
'update'
]);
$router
->
delete
(
'/card-templates/{id}'
,
\Modules\CardTemplates\Controllers\CardTemplateController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/api/card-templates/{templateId}'
,
[
CardTemplateController
::
class
,
'delete'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/Contracts/Jobs/ContractExpiryWarningJob.php
View file @
7d22a012
...
@@ -10,33 +10,47 @@ use Engine\Notifications\NotificationManager;
...
@@ -10,33 +10,47 @@ use Engine\Notifications\NotificationManager;
final
class
ContractExpiryWarningJob
implements
JobInterface
final
class
ContractExpiryWarningJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
private
NotificationManager
$notif
;
{
return
'contract_expiry_warnings'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$c
=
Container
::
getInstance
();
return
true
;
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$warningDays
=
[
90
,
60
,
30
];
$warningDays
=
[
90
,
60
,
30
];
$today
=
date
(
'Y-m-d'
);
foreach
(
$warningDays
as
$days
)
{
foreach
(
$warningDays
as
$days
)
{
$targetDate
=
date
(
'Y-m-d'
,
strtotime
(
"+
{
$days
}
days"
));
$targetDate
=
date
(
'Y-m-d'
,
strtotime
(
"+
{
$days
}
days"
));
$expiring
=
$this
->
db
->
fetchAll
(
$expiring
=
$db
->
fetchAll
(
"SELECT id, full_name_en, contract_end_date FROM users
"SELECT id, full_name_en, contract_end_date FROM users
WHERE contract_end_date = ? AND status = 'active'"
,
WHERE contract_end_date = ? AND status = 'active'
AND is_active = 1
"
,
[
$targetDate
]
[
$targetDate
]
);
);
foreach
(
$expiring
as
$u
)
{
foreach
(
$expiring
as
$user
)
{
$admins
=
$this
->
db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin') AND is_active = 1"
);
$admins
=
$db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin') AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
foreach
(
$admins
as
$a
)
{
$this
->
notif
->
createImportant
(
$a
[
'id'
],
"Contract Expiring in
{
$days
}
Days"
,
$notif
->
createImportant
(
$a
[
'id'
],
"Contract Expiring in
{
$days
}
Days"
,
"
{
$u
[
'full_name_en'
]
}
's contract expires on
{
$u
[
'contract_end_date'
]
}
."
,
"
{
$user
[
'full_name_en'
]
}
's contract expires on
{
$user
[
'contract_end_date'
]
}
."
,
"/users/
{
$u
[
'id'
]
}
"
,
'user'
,
$u
[
'id'
]);
"/users/
{
$user
[
'id'
]
}
"
,
'user'
,
(
int
)
$user
[
'id'
]);
}
if
(
$days
===
30
)
{
$notif
->
createImportant
((
int
)
$user
[
'id'
],
'Contract Expiring Soon'
,
"Your contract expires on
{
$user
[
'contract_end_date'
]
}
. Please contact administration."
,
'/dashboard'
);
}
}
}
}
}
}
...
...
modules/Contracts/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\Contracts\Controllers\ContractController
;
use
Modules\Contracts\Controllers\PolicyController
;
use
Modules\Contracts\Controllers\NoticeController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/contracts'
,
[
ContractController
::
class
,
'index'
]);
$router
->
get
(
'/contracts/{contractId}'
,
[
ContractController
::
class
,
'show'
]);
$router
->
get
(
'/contracts'
,
\Modules\Contracts\Controllers\ContractController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/policies'
,
[
PolicyController
::
class
,
'index'
]);
$router
->
get
(
'/contracts/{id}'
,
\Modules\Contracts\Controllers\ContractController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/policies/{policyId}'
,
[
PolicyController
::
class
,
'show'
]);
$router
->
post
(
'/policies'
,
[
PolicyController
::
class
,
'create'
]);
$router
->
post
(
'/policies/{policyId}/publish'
,
[
PolicyController
::
class
,
'publish'
]);
$router
->
post
(
'/policies/versions/{versionId}/acknowledge'
,
[
PolicyController
::
class
,
'acknowledge'
]);
$router
->
get
(
'/policies'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/notices'
,
[
NoticeController
::
class
,
'index'
]);
$router
->
get
(
'/policies/{id}'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/notices'
,
[
NoticeController
::
class
,
'create'
]);
$router
->
post
(
'/policies'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/notices/{noticeId}/acknowledge'
,
[
NoticeController
::
class
,
'acknowledgeNotice'
]);
$router
->
post
(
'/policies/{id}/publish'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'publish'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/notices/{noticeId}'
,
[
NoticeController
::
class
,
'delete'
]);
$router
->
post
(
'/policies/versions/{versionId}/acknowledge'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'acknowledge'
,
[
'auth'
]);
$router
->
get
(
'/notices'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/contracts'
,
[
ContractController
::
class
,
'index'
]);
$router
->
post
(
'/notices'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/contracts/{contractId}'
,
[
ContractController
::
class
,
'show'
]);
$router
->
post
(
'/notices/{id}/acknowledge'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'acknowledgeNotice'
,
[
'auth'
]);
$router
->
get
(
'/api/policies'
,
[
PolicyController
::
class
,
'index'
]);
$router
->
delete
(
'/notices/{id}'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/policies/{policyId}'
,
[
PolicyController
::
class
,
'show'
]);
\ No newline at end of file
$router
->
post
(
'/api/policies'
,
[
PolicyController
::
class
,
'create'
]);
$router
->
post
(
'/api/policies/{policyId}/publish'
,
[
PolicyController
::
class
,
'publish'
]);
$router
->
post
(
'/api/policies/versions/{versionId}/acknowledge'
,
[
PolicyController
::
class
,
'acknowledge'
]);
$router
->
get
(
'/api/notices'
,
[
NoticeController
::
class
,
'index'
]);
$router
->
post
(
'/api/notices'
,
[
NoticeController
::
class
,
'create'
]);
$router
->
post
(
'/api/notices/{noticeId}/acknowledge'
,
[
NoticeController
::
class
,
'acknowledgeNotice'
]);
$router
->
delete
(
'/api/notices/{noticeId}'
,
[
NoticeController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/DeductionPresets/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\DeductionPresets\Controllers\DeductionPresetController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/deduction-presets'
,
[
DeductionPresetController
::
class
,
'index'
]);
$router
->
post
(
'/deduction-presets'
,
[
DeductionPresetController
::
class
,
'create'
]);
$router
->
put
(
'/deduction-presets/{presetId}'
,
[
DeductionPresetController
::
class
,
'update'
]);
$router
->
delete
(
'/deduction-presets/{presetId}'
,
[
DeductionPresetController
::
class
,
'delete'
]);
$router
->
get
(
'/deduction-presets'
,
\Modules\DeductionPresets\Controllers\DeductionPresetController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/deduction-presets'
,
[
DeductionPresetController
::
class
,
'index'
]);
$router
->
post
(
'/deduction-presets'
,
\Modules\DeductionPresets\Controllers\DeductionPresetController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/deduction-presets'
,
[
DeductionPresetController
::
class
,
'create'
]);
$router
->
put
(
'/deduction-presets/{id}'
,
\Modules\DeductionPresets\Controllers\DeductionPresetController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/api/deduction-presets/{presetId}'
,
[
DeductionPresetController
::
class
,
'update'
]);
$router
->
delete
(
'/deduction-presets/{id}'
,
\Modules\DeductionPresets\Controllers\DeductionPresetController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/api/deduction-presets/{presetId}'
,
[
DeductionPresetController
::
class
,
'delete'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/Deductions/Jobs/AutoApplyExpiredDeductionsJob.php
View file @
7d22a012
...
@@ -6,34 +6,78 @@ namespace Modules\Deductions\Jobs;
...
@@ -6,34 +6,78 @@ namespace Modules\Deductions\Jobs;
use
Engine\Scheduler\JobInterface
;
use
Engine\Scheduler\JobInterface
;
use
Engine\Core\Container
;
use
Engine\Core\Container
;
use
Engine\Database\Connection
;
use
Engine\Database\Connection
;
use
Engine\Notifications\NotificationManager
;
final
class
AutoApplyExpiredDeductionsJob
implements
JobInterface
final
class
AutoApplyExpiredDeductionsJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
{
return
'auto_apply_deductions'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
)
;
return
true
;
}
}
public
function
execute
()
:
void
public
function
run
()
:
void
{
{
// Apply deductions where response window has expired with no response
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$this
->
db
->
query
(
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
"UPDATE deductions
SET status = 'applied_no_response',
$now
=
date
(
'Y-m-d H:i:s'
);
final_amount = calculated_amount,
applied_at = NOW(),
$expired
=
$db
->
fetchAll
(
payroll_month = DATE_FORMAT(NOW(), '%Y-%m')
"SELECT d.*, u.full_name_en as contractor_name FROM deductions d
WHERE status = 'acknowledged'
JOIN users u ON u.id = d.contractor_id
AND response_deadline IS NOT NULL
WHERE d.status = 'acknowledged'
AND response_deadline < NOW()
AND d.response_deadline IS NOT NULL
AND deleted_at IS NULL"
AND d.response_deadline < ?
AND d.deleted_at IS NULL"
,
[
$now
]
);
);
foreach
(
$expired
as
$deduction
)
{
$db
->
update
(
'deductions'
,
[
'status'
=>
'applied_no_response'
,
'final_amount'
=>
$deduction
[
'calculated_amount'
],
'applied_at'
=>
$now
,
],
'id = ?'
,
[(
int
)
$deduction
[
'id'
]]);
$notif
->
createImportant
(
$deduction
[
'contractor_id'
],
'Deduction Applied — No Response'
,
"Deduction #
{
$deduction
[
'id'
]
}
(
{
$deduction
[
'category'
]
}{
$deduction
[
'sub_category'
]
}
) of "
.
number_format
(
$deduction
[
'calculated_amount'
],
2
)
.
" EGP has been automatically applied. Response window expired."
,
"/deductions/
{
$deduction
[
'id'
]
}
"
,
'deduction'
,
(
int
)
$deduction
[
'id'
]);
$this
->
checkThreshold
(
$db
,
$notif
,
(
int
)
$deduction
[
'contractor_id'
]);
}
}
}
p
ublic
function
nextRunAt
()
:
string
p
rivate
function
checkThreshold
(
Connection
$db
,
NotificationManager
$notif
,
int
$contractorId
)
:
void
{
{
return
date
(
'Y-m-d H:i:s'
,
strtotime
(
'+1 hour'
));
$month
=
date
(
'Y-m'
);
$contractor
=
$db
->
fetchOne
(
"SELECT actual_salary, full_name_en FROM users WHERE id = ?"
,
[
$contractorId
]);
if
(
!
$contractor
||
!
$contractor
[
'actual_salary'
])
return
;
$totalDeductions
=
(
float
)
$db
->
fetchColumn
(
"SELECT COALESCE(SUM(COALESCE(final_amount, calculated_amount)), 0) FROM deductions
WHERE contractor_id = ? AND payroll_month = ?
AND status IN ('applied','applied_no_response','reduced','accepted') AND deleted_at IS NULL"
,
[
$contractorId
,
$month
]
);
$threshold
=
$contractor
[
'actual_salary'
]
*
0.40
;
if
(
$totalDeductions
>=
$threshold
)
{
$notif
->
createBlocking
(
$contractorId
,
'Critical Deduction Threshold'
,
'Your deductions have reached 40% of your salary. This is critical.'
);
$admins
=
$db
->
fetchAll
(
"SELECT id FROM users WHERE role = 'super_admin' AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
$notif
->
createImportant
(
$a
[
'id'
],
'🚨 40% Deduction Threshold'
,
"
{
$contractor
[
'full_name_en'
]
}
has reached the 40% deduction threshold. PIP recommended."
,
"/users/
{
$contractorId
}
"
,
'user'
,
$contractorId
);
}
}
}
}
}
}
\ No newline at end of file
modules/Deductions/Jobs/EscalateDeadlineDeductionsJob.php
View file @
7d22a012
...
@@ -9,65 +9,137 @@ use Engine\Database\Connection;
...
@@ -9,65 +9,137 @@ use Engine\Database\Connection;
final
class
EscalateDeadlineDeductionsJob
implements
JobInterface
final
class
EscalateDeadlineDeductionsJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
{
return
'escalate_deadline_deductions'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
)
;
return
true
;
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$overdueCards
=
$this
->
db
->
fetchAll
(
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
"SELECT c.id, c.card_key, c.deadline, c.board_id,
DATEDIFF(NOW(), c.deadline) as days_late,
$today
=
date
(
'Y-m-d'
);
ca.user_id as assignee_id
$overdueCards
=
$db
->
fetchAll
(
"SELECT c.id as card_id, c.card_key, c.deadline, c.board_id,
DATEDIFF(?, DATE(c.deadline)) as days_late
FROM cards c
FROM cards c
JOIN card_assignments ca ON ca.card_id = c.id
WHERE c.deadline IS NOT NULL AND c.deadline < ?
WHERE c.deadline IS NOT NULL AND c.deadline < NOW()
AND c.done_at IS NULL AND c.is_archived = 0"
,
AND c.done_at IS NULL AND c.is_archived = 0"
[
$today
,
$today
.
' 00:00:00'
]
);
);
foreach
(
$overdueCards
as
$card
)
{
foreach
(
$overdueCards
as
$card
)
{
$daysLate
=
(
int
)
$card
[
'days_late'
];
$daysLate
=
(
int
)
$card
[
'days_late'
];
if
(
$daysLate
<=
0
)
continue
;
if
(
$daysLate
<=
0
)
continue
;
$subCategory
=
match
(
true
)
{
$newSub
=
$this
->
determineSubCategory
(
$daysLate
);
$daysLate
>=
15
=>
'A4'
,
if
(
!
$newSub
)
continue
;
$daysLate
>=
8
=>
'A3'
,
$daysLate
>=
4
=>
'A2'
,
$assignees
=
$db
->
fetchAll
(
default
=>
'A1'
,
"SELECT user_id FROM card_assignments WHERE card_id = ?"
,
};
[(
int
)
$card
[
'card_id'
]]
);
$existing
=
$this
->
db
->
fetchOne
(
foreach
(
$assignees
as
$assignee
)
{
$existingDeduction
=
$db
->
fetchOne
(
"SELECT id, sub_category FROM deductions
"SELECT id, sub_category FROM deductions
WHERE related_card_id = ? AND contractor
_id = ? AND category = 'A'
WHERE contractor_id = ? AND related_card
_id = ? AND category = 'A'
AND status NOT IN ('dismissed','applied','applied_no_response','reduced','accepted')
AND status NOT IN ('dismissed','applied','applied_no_response','reduced','accepted')
AND deleted_at IS NULL"
,
AND deleted_at IS NULL
[
$card
[
'id'
],
$card
[
'assignee_id'
]]
ORDER BY created_at DESC LIMIT 1"
,
[
$assignee
[
'user_id'
],
(
int
)
$card
[
'card_id'
]]
);
);
if
(
$existing
)
{
if
(
$existingDeduction
)
{
if
(
$existing
[
'sub_category'
]
!==
$subCategory
)
{
$existingSub
=
$existingDeduction
[
'sub_category'
];
$contractor
=
$this
->
db
->
fetchOne
(
"SELECT actual_salary FROM users WHERE id = ?"
,
[
$card
[
'assignee_id'
]]);
if
(
$this
->
subCategoryLevel
(
$newSub
)
>
$this
->
subCategoryLevel
(
$existingSub
))
{
$contractor
=
$db
->
fetchOne
(
"SELECT actual_salary FROM users WHERE id = ?"
,
[
$assignee
[
'user_id'
]]);
$actualSalary
=
(
float
)(
$contractor
[
'actual_salary'
]
??
0
);
$actualSalary
=
(
float
)(
$contractor
[
'actual_salary'
]
??
0
);
$dailyRate
=
$actualSalary
>
0
?
round
(
$actualSalary
/
22
,
2
)
:
0
;
$newAmount
=
match
(
$subCategory
)
{
$expectedDays
=
$this
->
getExpectedWorkingDays
(
$db
,
$assignee
[
'user_id'
]);
$dailyRate
=
$expectedDays
>
0
?
round
(
$actualSalary
/
$expectedDays
,
2
)
:
0
;
$newAmount
=
$this
->
calculateAmount
(
$newSub
,
$dailyRate
,
$actualSalary
,
$daysLate
);
$db
->
update
(
'deductions'
,
[
'sub_category'
=>
$newSub
,
'calculated_amount'
=>
$newAmount
,
'description'
=>
"Auto-escalated: Card
{
$card
[
'card_key'
]
}
is now
{
$daysLate
}
days overdue (category
{
$newSub
}
)."
,
],
'id = ?'
,
[(
int
)
$existingDeduction
[
'id'
]]);
}
}
}
}
}
private
function
determineSubCategory
(
int
$daysLate
)
:
?
string
{
if
(
$daysLate
>=
15
)
return
'A4'
;
if
(
$daysLate
>=
8
)
return
'A3'
;
if
(
$daysLate
>=
4
)
return
'A2'
;
if
(
$daysLate
>=
1
)
return
'A1'
;
return
null
;
}
private
function
subCategoryLevel
(
string
$sub
)
:
int
{
return
(
int
)
substr
(
$sub
,
1
);
}
private
function
calculateAmount
(
string
$sub
,
float
$dailyRate
,
float
$actualSalary
,
int
$daysLate
)
:
float
{
return
match
(
$sub
)
{
'A1'
=>
round
(
$dailyRate
*
0.05
*
$daysLate
,
2
),
'A1'
=>
round
(
$dailyRate
*
0.05
*
$daysLate
,
2
),
'A2'
=>
round
(
$dailyRate
*
0.10
*
$daysLate
,
2
),
'A2'
=>
round
(
$dailyRate
*
0.10
*
$daysLate
,
2
),
'A3'
=>
round
(
$dailyRate
*
0.15
*
$daysLate
,
2
),
'A3'
=>
round
(
$dailyRate
*
0.15
*
$daysLate
,
2
),
'A4'
=>
round
(
$actualSalary
*
0.25
,
2
),
'A4'
=>
round
(
$actualSalary
*
0.25
,
2
),
default
=>
0
,
default
=>
0
,
};
};
}
$this
->
db
->
update
(
'deductions'
,
[
private
function
getExpectedWorkingDays
(
Connection
$db
,
int
$userId
)
:
int
'sub_category'
=>
$subCategory
,
{
'calculated_amount'
=>
$newAmount
,
$month
=
date
(
'Y-m'
);
'description'
=>
"Auto-escalated: Card
{
$card
[
'card_key'
]
}
is
{
$daysLate
}
days overdue."
,
$startDate
=
$month
.
'-01'
;
],
'id = ?'
,
[
$existing
[
'id'
]]);
$endDate
=
date
(
'Y-m-t'
,
strtotime
(
$startDate
));
$schedule
=
$db
->
fetchAll
(
"SELECT day_of_week FROM user_schedule_days WHERE user_id = ? AND effective_to IS NULL AND work_mode != 'off'"
,
[
$userId
]
);
$workDows
=
array_column
(
$schedule
,
'day_of_week'
);
if
(
empty
(
$workDows
))
return
22
;
$holidays
=
$db
->
fetchAll
(
"SELECT start_date, end_date FROM holidays WHERE start_date <= ? AND end_date >= ?"
,
[
$endDate
,
$startDate
]
);
$holidayDates
=
[];
foreach
(
$holidays
as
$h
)
{
$s
=
strtotime
(
$h
[
'start_date'
]);
$e
=
strtotime
(
$h
[
'end_date'
]);
for
(
$d
=
$s
;
$d
<=
$e
;
$d
+=
86400
)
{
$holidayDates
[
date
(
'Y-m-d'
,
$d
)]
=
true
;
}
}
}
$count
=
0
;
$current
=
strtotime
(
$startDate
);
$end
=
strtotime
(
$endDate
);
while
(
$current
<=
$end
)
{
$dow
=
(
int
)
date
(
'w'
,
$current
);
$dateStr
=
date
(
'Y-m-d'
,
$current
);
if
(
in_array
(
$dow
,
$workDows
)
&&
!
isset
(
$holidayDates
[
$dateStr
]))
{
$count
++
;
}
}
$current
+=
86400
;
}
}
return
$count
?:
22
;
}
}
}
}
\ No newline at end of file
modules/Evaluations/Calculators/OverallScoreCalculator.php
View file @
7d22a012
...
@@ -7,29 +7,47 @@ use Engine\Calculation\CalculatorInterface;
...
@@ -7,29 +7,47 @@ use Engine\Calculation\CalculatorInterface;
final
class
OverallScoreCalculator
implements
CalculatorInterface
final
class
OverallScoreCalculator
implements
CalculatorInterface
{
{
public
function
calculate
(
array
$context
)
:
mixed
public
function
calculate
(
array
$context
)
:
array
{
{
$techScore
=
(
float
)(
$context
[
'technical_score'
]
??
0
);
$techScore
=
(
float
)(
$context
[
'technical_score'
]
??
0
);
$profScore
=
(
float
)(
$context
[
'professional_score'
]
??
0
);
$profScore
=
(
float
)(
$context
[
'professional_score'
]
??
0
);
$techWeight
=
(
float
)(
$context
[
'tech_weight'
]
??
0.5
);
$profWeight
=
(
float
)(
$context
[
'prof_weight'
]
??
0.5
);
$criteria
=
require
ROOT_PATH
.
'/config/evaluation_criteria.php'
;
$overallScore
=
round
((
$techScore
*
$techWeight
)
+
(
$profScore
*
$profWeight
),
2
);
$techWeight
=
(
float
)(
$criteria
[
'overall_weights'
][
'technical'
]
??
0.5
);
$profWeight
=
(
float
)(
$criteria
[
'overall_weights'
][
'professional'
]
??
0.5
);
$overall
=
round
((
$techScore
*
$techWeight
)
+
(
$profScore
*
$profWeight
),
2
);
$rating
=
$this
->
determineRating
(
$overallScore
);
return
[
'overall_score'
=>
$overallScore
,
'rating'
=>
$rating
[
'rating'
],
'rating_label'
=>
$rating
[
'label'
],
'technical_score'
=>
$techScore
,
'professional_score'
=>
$profScore
,
];
}
private
function
determineRating
(
float
$score
)
:
array
{
$ratings
=
[
[
'min'
=>
4.5
,
'max'
=>
5.0
,
'rating'
=>
'exceptional'
,
'label'
=>
'⭐ Exceptional'
],
[
'min'
=>
3.5
,
'max'
=>
4.49
,
'rating'
=>
'strong'
,
'label'
=>
'🟢 Strong'
],
[
'min'
=>
2.5
,
'max'
=>
3.49
,
'rating'
=>
'adequate'
,
'label'
=>
'🟡 Adequate'
],
[
'min'
=>
1.5
,
'max'
=>
2.49
,
'rating'
=>
'below_expectations'
,
'label'
=>
'🟠 Below Expectations'
],
[
'min'
=>
1.0
,
'max'
=>
1.49
,
'rating'
=>
'unacceptable'
,
'label'
=>
'🔴 Unacceptable'
],
];
$rating
=
'adequate'
;
$ratings
=
$criteria
[
'ratings'
]
??
[];
foreach
(
$ratings
as
$r
)
{
foreach
(
$ratings
as
$r
)
{
if
(
$overall
>=
$r
[
'min'
]
&&
$overall
<=
$r
[
'max'
])
{
if
(
$score
>=
$r
[
'min'
]
&&
$score
<=
$r
[
'max'
])
{
$rating
=
$r
[
'rating'
];
return
$r
;
break
;
}
}
}
}
return
[
return
[
'rating'
=>
'unacceptable'
,
'label'
=>
'🔴 Unacceptable'
];
'overall_score'
=>
$overall
,
}
'rating'
=>
$rating
,
];
public
function
name
()
:
string
{
return
'overall_eval_score'
;
}
}
}
}
\ No newline at end of file
modules/Evaluations/Calculators/ProfessionalAutoScoreCalculator.php
View file @
7d22a012
...
@@ -9,45 +9,96 @@ use Engine\Database\Connection;
...
@@ -9,45 +9,96 @@ use Engine\Database\Connection;
final
class
ProfessionalAutoScoreCalculator
implements
CalculatorInterface
final
class
ProfessionalAutoScoreCalculator
implements
CalculatorInterface
{
{
private
Connection
$db
;
public
function
calculate
(
array
$context
)
:
array
public
function
__construct
()
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
}
public
function
calculate
(
array
$context
)
:
mixed
{
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$contractorId
=
(
int
)
$context
[
'contractor_id'
];
$contractorId
=
(
int
)
$context
[
'contractor_id'
];
$month
=
$context
[
'month'
];
$month
=
$context
[
'month'
];
// YYYY-MM
$startDate
=
$month
.
'-01'
;
$endDate
=
date
(
'Y-m-t'
,
strtotime
(
$startDate
));
$totalReports
=
(
int
)
$this
->
db
->
fetchColumn
(
// Reporting Compliance: (reports_on_time / expected_reports) * 5
"SELECT COUNT(*) FROM daily_reports WHERE user_id = ? AND report_date LIKE ?"
,
$expectedReports
=
(
int
)
$db
->
fetchColumn
(
[
$contractorId
,
$month
.
'%'
]
"SELECT COUNT(*) FROM daily_reports
WHERE user_id = ? AND report_date >= ? AND report_date <= ?
AND status != 'draft'"
,
[
$contractorId
,
$startDate
,
$endDate
]
);
);
$onTimeReports
=
(
int
)
$this
->
db
->
fetchColumn
(
// Count working days from schedule for a more accurate expected count
"SELECT COUNT(*) FROM daily_reports WHERE user_id = ? AND report_date LIKE ? AND is_on_time = 1"
,
$scheduleDays
=
$db
->
fetchAll
(
[
$contractorId
,
$month
.
'%'
]
"SELECT day_of_week FROM user_schedule_days
WHERE user_id = ? AND effective_to IS NULL AND work_mode != 'off'"
,
[
$contractorId
]
);
);
$workDows
=
array_column
(
$scheduleDays
,
'day_of_week'
);
$reportingCompliance
=
$totalReports
>
0
?
round
((
$onTimeReports
/
$totalReports
)
*
5
,
2
)
:
1.0
;
$holidays
=
$db
->
fetchAll
(
$reportingCompliance
=
min
(
5.0
,
max
(
1.0
,
$reportingCompliance
));
"SELECT start_date, end_date FROM holidays
WHERE (start_date <= ? AND end_date >= ?) OR is_recurring = 1"
,
[
$endDate
,
$startDate
]
);
$holidayDates
=
[];
foreach
(
$holidays
as
$h
)
{
$s
=
strtotime
(
$h
[
'start_date'
]);
$e
=
strtotime
(
$h
[
'end_date'
]);
for
(
$d
=
$s
;
$d
<=
$e
;
$d
+=
86400
)
{
$holidayDates
[
date
(
'Y-m-d'
,
$d
)]
=
true
;
}
}
$violations
=
(
int
)
$this
->
db
->
fetchColumn
(
$totalExpected
=
0
;
"SELECT COUNT(*) FROM deductions WHERE contractor_id = ? AND payroll_month = ?
$current
=
strtotime
(
$startDate
);
AND status IN ('applied','applied_no_response','reduced','accepted') AND deleted_at IS NULL"
,
$end
=
min
(
strtotime
(
$endDate
),
strtotime
(
date
(
'Y-m-d'
)));
while
(
$current
<=
$end
)
{
$dow
=
(
int
)
date
(
'w'
,
$current
);
$dateStr
=
date
(
'Y-m-d'
,
$current
);
if
(
in_array
(
$dow
,
$workDows
)
&&
!
isset
(
$holidayDates
[
$dateStr
]))
{
$unavail
=
$db
->
fetchOne
(
"SELECT id FROM unavailability_records WHERE user_id = ? AND start_date <= ? AND end_date >= ?"
,
[
$contractorId
,
$dateStr
,
$dateStr
]
);
if
(
!
$unavail
)
{
$totalExpected
++
;
}
}
$current
+=
86400
;
}
$reportsOnTime
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(*) FROM daily_reports
WHERE user_id = ? AND report_date >= ? AND report_date <= ?
AND is_on_time = 1 AND status NOT IN ('draft','unreported')"
,
[
$contractorId
,
$startDate
,
$endDate
]
);
$reportingCompliance
=
$totalExpected
>
0
?
min
(
5.0
,
round
((
$reportsOnTime
/
$totalExpected
)
*
5
,
2
))
:
3.0
;
// Policy Compliance: max(1, 5 - (violations * 0.5))
$violations
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(*) FROM deductions
WHERE contractor_id = ? AND payroll_month = ?
AND status IN ('applied','applied_no_response','reduced','accepted')
AND deleted_at IS NULL"
,
[
$contractorId
,
$month
]
[
$contractorId
,
$month
]
);
);
$policyCompliance
=
max
(
1.0
,
min
(
5.0
,
5.0
-
(
$violations
*
0.5
)
));
$policyCompliance
=
max
(
1.0
,
round
(
5.0
-
(
$violations
*
0.5
),
2
));
return
[
return
[
'reporting_compliance'
=>
$reportingCompliance
,
'reporting_compliance'
=>
$reportingCompliance
,
'policy_compliance'
=>
round
(
$policyCompliance
,
2
)
,
'policy_compliance'
=>
$policyCompliance
,
'
total_reports'
=>
$totalReports
,
'
reports_on_time'
=>
$reportsOnTime
,
'
on_time_reports'
=>
$onTimeReports
,
'
expected_reports'
=>
$totalExpected
,
'violations'
=>
$violations
,
'violations'
=>
$violations
,
];
];
}
}
public
function
name
()
:
string
{
return
'professional_auto_score'
;
}
}
}
\ No newline at end of file
modules/Evaluations/Calculators/TechnicalAutoScoreCalculator.php
View file @
7d22a012
...
@@ -9,54 +9,58 @@ use Engine\Database\Connection;
...
@@ -9,54 +9,58 @@ use Engine\Database\Connection;
final
class
TechnicalAutoScoreCalculator
implements
CalculatorInterface
final
class
TechnicalAutoScoreCalculator
implements
CalculatorInterface
{
{
private
Connection
$db
;
public
function
calculate
(
array
$context
)
:
array
public
function
__construct
()
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
}
public
function
calculate
(
array
$context
)
:
mixed
{
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$contractorId
=
(
int
)
$context
[
'contractor_id'
];
$contractorId
=
(
int
)
$context
[
'contractor_id'
];
$month
=
$context
[
'month'
];
$month
=
$context
[
'month'
];
// YYYY-MM
$startDate
=
$month
.
'-01'
;
$startDate
=
$month
.
'-01'
;
$endDate
=
date
(
'Y-m-t'
,
strtotime
(
$startDate
));
$endDate
=
date
(
'Y-m-t'
,
strtotime
(
$startDate
));
$cardsAssigned
=
(
int
)
$this
->
db
->
fetchColumn
(
// Task Completion Rate: (cards_done / cards_assigned) * 5
$cardsAssigned
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(DISTINCT ca.card_id) FROM card_assignments ca
"SELECT COUNT(DISTINCT ca.card_id) FROM card_assignments ca
JOIN cards c ON c.id = ca.card_id
JOIN cards c ON c.id = ca.card_id
WHERE ca.user_id = ? AND ca.created_at
BETWEEN ? AND
?"
,
WHERE ca.user_id = ? AND ca.created_at
<=
?"
,
[
$contractorId
,
$
startDate
.
' 00:00:00'
,
$
endDate
.
' 23:59:59'
]
[
$contractorId
,
$endDate
.
' 23:59:59'
]
);
);
$cardsDone
=
(
int
)
$
this
->
db
->
fetchColumn
(
$cardsDone
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(DISTINCT c.id) FROM cards c
"SELECT COUNT(DISTINCT c.id) FROM cards c
JOIN card_assignments ca ON ca.card_id = c.id
JOIN card_assignments ca ON ca.card_id = c.id
WHERE ca.user_id = ? AND c.done_at BETWEEN ? AND ?"
,
WHERE ca.user_id = ? AND c.done_at IS NOT NULL
AND c.done_at >= ? AND c.done_at <= ?"
,
[
$contractorId
,
$startDate
.
' 00:00:00'
,
$endDate
.
' 23:59:59'
]
[
$contractorId
,
$startDate
.
' 00:00:00'
,
$endDate
.
' 23:59:59'
]
);
);
$cardsWithDeadline
=
(
int
)
$this
->
db
->
fetchColumn
(
$taskCompletionRate
=
$cardsAssigned
>
0
?
min
(
5.0
,
round
((
$cardsDone
/
$cardsAssigned
)
*
5
,
2
))
:
3.0
;
// Deadline Compliance: (cards_on_time / cards_with_deadline) * 5
$cardsWithDeadline
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(DISTINCT c.id) FROM cards c
"SELECT COUNT(DISTINCT c.id) FROM cards c
JOIN card_assignments ca ON ca.card_id = c.id
JOIN card_assignments ca ON ca.card_id = c.id
WHERE ca.user_id = ? AND c.deadline IS NOT NULL AND c.done_at BETWEEN ? AND ?"
,
WHERE ca.user_id = ? AND c.deadline IS NOT NULL
AND c.done_at IS NOT NULL
AND c.done_at >= ? AND c.done_at <= ?"
,
[
$contractorId
,
$startDate
.
' 00:00:00'
,
$endDate
.
' 23:59:59'
]
[
$contractorId
,
$startDate
.
' 00:00:00'
,
$endDate
.
' 23:59:59'
]
);
);
$cardsOnTime
=
(
int
)
$
this
->
db
->
fetchColumn
(
$cardsOnTime
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(DISTINCT c.id) FROM cards c
"SELECT COUNT(DISTINCT c.id) FROM cards c
JOIN card_assignments ca ON ca.card_id = c.id
JOIN card_assignments ca ON ca.card_id = c.id
WHERE ca.user_id = ? AND c.deadline IS NOT NULL AND c.done_at IS NOT NULL
WHERE ca.user_id = ? AND c.deadline IS NOT NULL
AND c.done_at <= c.deadline AND c.done_at BETWEEN ? AND ?"
,
AND c.done_at IS NOT NULL
AND c.done_at <= c.deadline
AND c.done_at >= ? AND c.done_at <= ?"
,
[
$contractorId
,
$startDate
.
' 00:00:00'
,
$endDate
.
' 23:59:59'
]
[
$contractorId
,
$startDate
.
' 00:00:00'
,
$endDate
.
' 23:59:59'
]
);
);
$taskCompletionRate
=
$cardsAssigned
>
0
?
round
((
$cardsDone
/
$cardsAssigned
)
*
5
,
2
)
:
3.0
;
$deadlineCompliance
=
$cardsWithDeadline
>
0
$deadlineCompliance
=
$cardsWithDeadline
>
0
?
round
((
$cardsOnTime
/
$cardsWithDeadline
)
*
5
,
2
)
:
3.0
;
?
min
(
5.0
,
round
((
$cardsOnTime
/
$cardsWithDeadline
)
*
5
,
2
))
:
3.0
;
$taskCompletionRate
=
min
(
5.0
,
max
(
1.0
,
$taskCompletionRate
));
$deadlineCompliance
=
min
(
5.0
,
max
(
1.0
,
$deadlineCompliance
));
return
[
return
[
'task_completion_rate'
=>
$taskCompletionRate
,
'task_completion_rate'
=>
$taskCompletionRate
,
...
@@ -67,4 +71,9 @@ final class TechnicalAutoScoreCalculator implements CalculatorInterface
...
@@ -67,4 +71,9 @@ final class TechnicalAutoScoreCalculator implements CalculatorInterface
'cards_on_time'
=>
$cardsOnTime
,
'cards_on_time'
=>
$cardsOnTime
,
];
];
}
}
public
function
name
()
:
string
{
return
'technical_auto_score'
;
}
}
}
\ No newline at end of file
modules/Evaluations/Jobs/CompileEvaluationsJob.php
View file @
7d22a012
...
@@ -11,98 +11,124 @@ use Engine\Calculation\CalculationEngine;
...
@@ -11,98 +11,124 @@ use Engine\Calculation\CalculationEngine;
final
class
CompileEvaluationsJob
implements
JobInterface
final
class
CompileEvaluationsJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
private
NotificationManager
$notif
;
{
private
CalculationEngine
$calc
;
return
'compile_evaluations'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$c
=
Container
::
getInstance
();
return
true
;
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
$this
->
calc
=
$c
->
resolve
(
CalculationEngine
::
class
);
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$cycle
=
$this
->
db
->
fetchOne
(
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$calc
=
Container
::
getInstance
()
->
resolve
(
CalculationEngine
::
class
);
$activeCycle
=
$db
->
fetchOne
(
"SELECT * FROM evaluation_cycles WHERE status IN ('open','technical_phase','professional_phase','compiling') LIMIT 1"
"SELECT * FROM evaluation_cycles WHERE status IN ('open','technical_phase','professional_phase','compiling') LIMIT 1"
);
);
if
(
!
$cycle
)
return
;
$contractors
=
$this
->
db
->
fetchAll
(
if
(
!
$activeCycle
)
{
return
;
}
$contractors
=
$db
->
fetchAll
(
"SELECT DISTINCT contractor_id FROM evaluations WHERE cycle_id = ?"
,
"SELECT DISTINCT contractor_id FROM evaluations WHERE cycle_id = ?"
,
[
$
c
ycle
[
'id'
]]
[
$
activeC
ycle
[
'id'
]]
);
);
$compiledCount
=
0
;
$allCompiled
=
true
;
foreach
(
$contractors
as
$c
)
{
foreach
(
$contractors
as
$c
)
{
$c
i
d
=
$c
[
'contractor_id'
];
$c
ontractorI
d
=
$c
[
'contractor_id'
];
$
existing
=
$this
->
db
->
fetchOne
(
$
alreadyCompiled
=
$
db
->
fetchOne
(
"SELECT id FROM compiled_evaluations WHERE cycle_id = ? AND contractor_id = ?"
,
"SELECT id FROM compiled_evaluations WHERE cycle_id = ? AND contractor_id = ?"
,
[
$
cycle
[
'id'
],
$ci
d
]
[
$
activeCycle
[
'id'
],
$contractorI
d
]
);
);
if
(
$existing
)
continue
;
if
(
$alreadyCompiled
)
{
continue
;
}
$tech
=
$this
->
db
->
fetchOne
(
$tech
Eval
=
$
db
->
fetchOne
(
"SELECT
total_score FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'technical' AND submitted_at IS NOT NULL
"
,
"SELECT
* FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'technical'
"
,
[
$
cycle
[
'id'
],
$ci
d
]
[
$
activeCycle
[
'id'
],
$contractorI
d
]
);
);
$prof
=
$this
->
db
->
fetchOne
(
$prof
Eval
=
$
db
->
fetchOne
(
"SELECT
total_score FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'professional' AND submitted_at IS NOT NULL
"
,
"SELECT
* FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'professional'
"
,
[
$
cycle
[
'id'
],
$ci
d
]
[
$
activeCycle
[
'id'
],
$contractorI
d
]
);
);
if
(
!
$tech
||
!
$prof
)
continue
;
if
(
!
$techEval
||
!
$techEval
[
'submitted_at'
]
||
!
$profEval
||
!
$profEval
[
'submitted_at'
])
{
$allCompiled
=
false
;
continue
;
}
$techScore
=
(
float
)
$techEval
[
'total_score'
];
$profScore
=
(
float
)
$profEval
[
'total_score'
];
$result
=
$this
->
calc
->
calculate
(
'overall_eval_score'
,
[
$overallResult
=
$calc
->
calculate
(
'overall_eval_score'
,
[
'technical_score'
=>
(
float
)
$tech
[
'total_score'
],
'technical_score'
=>
$techScore
,
'professional_score'
=>
(
float
)
$prof
[
'total_score'
],
'professional_score'
=>
$profScore
,
'tech_weight'
=>
0.5
,
'prof_weight'
=>
0.5
,
]);
]);
$metrics
=
[
$contractor
=
$db
->
fetchOne
(
"SELECT * FROM users WHERE id = ?"
,
[
$contractorId
]);
'technical_score'
=>
(
float
)
$tech
[
'total_score'
],
'professional_score'
=>
(
float
)
$prof
[
'total_score'
],
$systemMetrics
=
[
'month'
=>
$cycle
[
'month'
],
'month'
=>
$activeCycle
[
'month'
],
'actual_salary'
=>
$contractor
[
'actual_salary'
]
??
0
,
'technical_score'
=>
$techScore
,
'professional_score'
=>
$profScore
,
];
];
$this
->
db
->
insert
(
'compiled_evaluations'
,
[
if
(
$calc
->
has
(
'technical_auto_score'
))
{
'cycle_id'
=>
$cycle
[
'id'
],
$systemMetrics
[
'tech_auto'
]
=
$calc
->
calculate
(
'technical_auto_score'
,
[
'contractor_id'
=>
$cid
,
'contractor_id'
=>
$contractorId
,
'technical_score'
=>
(
float
)
$tech
[
'total_score'
],
'month'
=>
$activeCycle
[
'month'
],
'professional_score'
=>
(
float
)
$prof
[
'total_score'
],
]);
'overall_score'
=>
$result
[
'overall_score'
],
}
'rating'
=>
$result
[
'rating'
],
if
(
$calc
->
has
(
'professional_auto_score'
))
{
'system_metrics_json'
=>
json_encode
(
$metrics
),
$systemMetrics
[
'prof_auto'
]
=
$calc
->
calculate
(
'professional_auto_score'
,
[
'contractor_id'
=>
$contractorId
,
'month'
=>
$activeCycle
[
'month'
],
]);
}
$compiledId
=
$db
->
insert
(
'compiled_evaluations'
,
[
'cycle_id'
=>
$activeCycle
[
'id'
],
'contractor_id'
=>
$contractorId
,
'technical_score'
=>
$techScore
,
'professional_score'
=>
$profScore
,
'overall_score'
=>
$overallResult
[
'overall_score'
],
'rating'
=>
$overallResult
[
'rating'
],
'system_metrics_json'
=>
json_encode
(
$systemMetrics
),
'compiled_at'
=>
date
(
'Y-m-d H:i:s'
),
'compiled_at'
=>
date
(
'Y-m-d H:i:s'
),
]);
]);
$
this
->
notif
->
createBlocking
(
$ci
d
,
'Monthly Evaluation Published'
,
$
notif
->
createBlocking
(
$contractorI
d
,
'Monthly Evaluation Published'
,
"Your evaluation for
{
$
cycle
[
'month'
]
}
has been compiled. Overall score:
{
$result
[
'overall_score'
]
}
/5.00 (
{
$result
[
'rating
'
]
}
)"
,
"Your evaluation for
{
$
activeCycle
[
'month'
]
}
has been compiled. Overall score:
{
$overallResult
[
'overall_score'
]
}
(
{
$overallResult
[
'rating_label
'
]
}
)"
,
"/evaluations/compiled/
{
$c
id
}
"
,
'compiled_evaluation'
,
$ci
d
);
"/evaluations/compiled/
{
$c
ompiledId
}
"
,
'compiled_evaluation'
,
$compiledI
d
);
if
(
$
r
esult
[
'overall_score'
]
<
2.5
)
{
if
(
$
overallR
esult
[
'overall_score'
]
<
2.5
)
{
$admins
=
$
this
->
db
->
fetchAll
(
"SELECT id FROM users WHERE role = 'super_admin'
AND is_active = 1"
);
$admins
=
$
db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin')
AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
foreach
(
$admins
as
$a
)
{
$
this
->
notif
->
createImportant
(
$a
[
'id'
],
'Low Evaluation Score Alert
'
,
$
notif
->
createImportant
(
$a
[
'id'
],
'Low Evaluation Score — PIP Recommended
'
,
"Contractor
ID
{
$cid
}
scored
{
$result
[
'overall_score'
]
}
(
{
$result
[
'rating'
]
}
)
. PIP recommended."
,
"Contractor
{
$contractor
[
'full_name_en'
]
}
scored
{
$overallResult
[
'overall_score'
]
}
(
{
$overallResult
[
'rating_label'
]
}
) for
{
$activeCycle
[
'month'
]
}
. PIP recommended."
,
"/users/
{
$c
id
}
"
,
'user'
,
$ci
d
);
"/users/
{
$c
ontractorId
}
"
,
'user'
,
$contractorI
d
);
}
}
}
}
$compiledCount
++
;
}
}
$totalExpected
=
count
(
$contractors
);
if
(
$allCompiled
)
{
$totalCompiled
=
(
int
)
$this
->
db
->
fetchColumn
(
$db
->
update
(
'evaluation_cycles'
,
[
"SELECT COUNT(*) FROM compiled_evaluations WHERE cycle_id = ?"
,
[
$cycle
[
'id'
]]
);
if
(
$totalCompiled
>=
$totalExpected
&&
$totalExpected
>
0
)
{
$this
->
db
->
update
(
'evaluation_cycles'
,
[
'status'
=>
'completed'
,
'status'
=>
'completed'
,
'completed_at'
=>
date
(
'Y-m-d H:i:s'
),
'completed_at'
=>
date
(
'Y-m-d H:i:s'
),
],
'id = ?'
,
[
$
c
ycle
[
'id'
]]);
],
'id = ?'
,
[
$
activeC
ycle
[
'id'
]]);
}
}
}
}
}
}
\ No newline at end of file
modules/Evaluations/Jobs/EvaluationReminderJob.php
View file @
7d22a012
...
@@ -10,51 +10,71 @@ use Engine\Notifications\NotificationManager;
...
@@ -10,51 +10,71 @@ use Engine\Notifications\NotificationManager;
final
class
EvaluationReminderJob
implements
JobInterface
final
class
EvaluationReminderJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
private
NotificationManager
$notif
;
{
return
'evaluation_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$c
=
Container
::
getInstance
();
return
true
;
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$cycle
=
$this
->
db
->
fetchOne
(
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$activeCycle
=
$db
->
fetchOne
(
"SELECT * FROM evaluation_cycles WHERE status IN ('open','technical_phase','professional_phase') LIMIT 1"
"SELECT * FROM evaluation_cycles WHERE status IN ('open','technical_phase','professional_phase') LIMIT 1"
);
);
if
(
!
$cycle
)
return
;
if
(
!
$activeCycle
)
{
return
;
}
$now
=
time
();
$now
=
time
();
$techDeadline
=
strtotime
(
$
c
ycle
[
'tech_deadline'
]);
$techDeadline
=
strtotime
(
$
activeC
ycle
[
'tech_deadline'
]);
$profDeadline
=
strtotime
(
$
c
ycle
[
'prof_deadline'
]);
$profDeadline
=
strtotime
(
$
activeC
ycle
[
'prof_deadline'
]);
$daysT
o
Tech
=
(
int
)
ceil
((
$techDeadline
-
$now
)
/
86400
);
$daysT
il
Tech
=
(
int
)
ceil
((
$techDeadline
-
$now
)
/
86400
);
$daysT
o
Prof
=
(
int
)
ceil
((
$profDeadline
-
$now
)
/
86400
);
$daysT
il
Prof
=
(
int
)
ceil
((
$profDeadline
-
$now
)
/
86400
);
if
(
$daysToTech
<=
2
&&
$daysToTech
>=
0
)
{
if
(
$daysTilTech
<=
2
&&
$daysTilTech
>=
0
)
{
$pending
=
$this
->
db
->
fetchAll
(
$pendingTech
=
$db
->
fetchAll
(
"SELECT DISTINCT evaluator_id FROM evaluations WHERE cycle_id = ? AND type = 'technical' AND submitted_at IS NULL"
,
"SELECT DISTINCT evaluator_id FROM evaluations
[
$cycle
[
'id'
]]
WHERE cycle_id = ? AND type = 'technical' AND submitted_at IS NULL"
,
[
$activeCycle
[
'id'
]]
);
foreach
(
$pendingTech
as
$e
)
{
$count
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(*) FROM evaluations
WHERE cycle_id = ? AND type = 'technical' AND evaluator_id = ? AND submitted_at IS NULL"
,
[
$activeCycle
[
'id'
],
$e
[
'evaluator_id'
]]
);
);
foreach
(
$pending
as
$p
)
{
$urgency
=
$daysTilTech
===
0
?
'🚨 DUE TODAY'
:
"⏰
{
$daysTilTech
}
days remaining"
;
$
this
->
notif
->
createImportant
(
$p
[
'evaluator_id'
],
'Technical Evaluation Due'
,
$
notif
->
createImportant
(
$e
[
'evaluator_id'
],
"Technical Evaluations
{
$urgency
}
"
,
"
Technical evaluations for
{
$cycle
[
'month'
]
}
are due in
{
$daysToTech
}
day(s). Please submit them."
,
"
You have
{
$count
}
technical evaluation(s) pending for
{
$activeCycle
[
'month'
]
}
. Deadline: "
.
date
(
'M j'
,
$techDeadline
)
,
'/evaluations/pending'
,
'evaluation_cycle'
,
$
c
ycle
[
'id'
]);
'/evaluations/pending'
,
'evaluation_cycle'
,
$
activeC
ycle
[
'id'
]);
}
}
}
}
if
(
$daysToProf
<=
2
&&
$daysToProf
>=
0
)
{
if
(
$daysTilProf
<=
2
&&
$daysTilProf
>=
0
)
{
$pending
=
$this
->
db
->
fetchAll
(
$pendingProf
=
$db
->
fetchAll
(
"SELECT DISTINCT evaluator_id FROM evaluations WHERE cycle_id = ? AND type = 'professional' AND submitted_at IS NULL"
,
"SELECT DISTINCT evaluator_id FROM evaluations
[
$cycle
[
'id'
]]
WHERE cycle_id = ? AND type = 'professional' AND submitted_at IS NULL"
,
[
$activeCycle
[
'id'
]]
);
foreach
(
$pendingProf
as
$e
)
{
$count
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(*) FROM evaluations
WHERE cycle_id = ? AND type = 'professional' AND evaluator_id = ? AND submitted_at IS NULL"
,
[
$activeCycle
[
'id'
],
$e
[
'evaluator_id'
]]
);
);
foreach
(
$pending
as
$p
)
{
$urgency
=
$daysTilProf
===
0
?
'🚨 DUE TODAY'
:
"⏰
{
$daysTilProf
}
days remaining"
;
$
this
->
notif
->
createImportant
(
$p
[
'evaluator_id'
],
'Professional Evaluation Due'
,
$
notif
->
createImportant
(
$e
[
'evaluator_id'
],
"Professional Evaluations
{
$urgency
}
"
,
"
Professional evaluations for
{
$cycle
[
'month'
]
}
are due in
{
$daysToProf
}
day(s). Please submit them."
,
"
You have
{
$count
}
professional evaluation(s) pending for
{
$activeCycle
[
'month'
]
}
. Deadline: "
.
date
(
'M j'
,
$profDeadline
)
,
'/evaluations/pending'
,
'evaluation_cycle'
,
$
c
ycle
[
'id'
]);
'/evaluations/pending'
,
'evaluation_cycle'
,
$
activeC
ycle
[
'id'
]);
}
}
}
}
}
}
...
...
modules/Evaluations/Jobs/OpenEvaluationCycleJob.php
View file @
7d22a012
...
@@ -6,56 +6,99 @@ namespace Modules\Evaluations\Jobs;
...
@@ -6,56 +6,99 @@ namespace Modules\Evaluations\Jobs;
use
Engine\Scheduler\JobInterface
;
use
Engine\Scheduler\JobInterface
;
use
Engine\Core\Container
;
use
Engine\Core\Container
;
use
Engine\Database\Connection
;
use
Engine\Database\Connection
;
use
Engine\Notifications\NotificationManager
;
final
class
OpenEvaluationCycleJob
implements
JobInterface
final
class
OpenEvaluationCycleJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
{
return
'open_evaluation_cycle'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
)
;
return
(
int
)
date
(
'j'
)
===
1
;
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
if
((
int
)
date
(
'j'
)
!==
1
)
return
;
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$month
=
date
(
'Y-m'
,
strtotime
(
'-1 month'
));
$month
=
date
(
'Y-m'
,
strtotime
(
'-1 month'
));
$exists
=
$this
->
db
->
fetchOne
(
"SELECT id FROM evaluation_cycles WHERE month = ?"
,
[
$month
]);
if
(
$exists
)
return
;
$exists
=
$db
->
fetchOne
(
"SELECT id FROM evaluation_cycles WHERE month = ?"
,
[
$month
]);
if
(
$exists
)
{
return
;
}
$now
=
date
(
'Y-m-d H:i:s'
);
$now
=
date
(
'Y-m-d H:i:s'
);
$cycleId
=
$this
->
db
->
insert
(
'evaluation_cycles'
,
[
$techDeadline
=
date
(
'Y-m-d H:i:s'
,
strtotime
(
'+5 weekdays'
));
$profDeadline
=
date
(
'Y-m-d H:i:s'
,
strtotime
(
'+7 weekdays'
));
$db
->
beginTransaction
();
try
{
$cycleId
=
$db
->
insert
(
'evaluation_cycles'
,
[
'month'
=>
$month
,
'month'
=>
$month
,
'status'
=>
'open'
,
'status'
=>
'open'
,
'opened_at'
=>
$now
,
'opened_at'
=>
$now
,
'tech_deadline'
=>
date
(
'Y-m-d 23:59:59'
,
strtotime
(
'+5 weekdays'
))
,
'tech_deadline'
=>
$techDeadline
,
'prof_deadline'
=>
date
(
'Y-m-d 23:59:59'
,
strtotime
(
'+7 weekdays'
))
,
'prof_deadline'
=>
$profDeadline
,
]);
]);
$contractors
=
$this
->
db
->
fetchAll
(
$contractors
=
$db
->
fetchAll
(
"SELECT u.id, (SELECT bm.user_id FROM board_members bm
"SELECT id FROM users WHERE role = 'contractor' AND status IN ('active','on_pip') AND is_active = 1"
);
foreach
(
$contractors
as
$contractor
)
{
$pl
=
$db
->
fetchOne
(
"SELECT bm.user_id FROM board_members bm
JOIN board_members bm2 ON bm2.board_id = bm.board_id
JOIN board_members bm2 ON bm2.board_id = bm.board_id
WHERE bm2.user_id = u.id AND bm.role_on_board = 'project_leader' LIMIT 1) as pl_id
WHERE bm2.user_id = ? AND bm.role_on_board = 'project_leader' LIMIT 1"
,
FROM users u WHERE u.role = 'contractor' AND u.status IN ('active','on_pip') AND u.is_active = 1"
[
$contractor
[
'id'
]]
);
);
$defaultAdmin
=
$this
->
db
->
fetchOne
(
"SELECT id FROM users WHERE role IN ('admin','super_admin') AND is_active = 1 LIMIT 1"
);
$plId
=
$pl
?
$pl
[
'user_id'
]
:
null
;
$defaultAdminId
=
$defaultAdmin
?
$defaultAdmin
[
'id'
]
:
1
;
if
(
!
$plId
)
{
$sa
=
$db
->
fetchOne
(
"SELECT id FROM users WHERE role = 'super_admin' AND is_active = 1 LIMIT 1"
);
$plId
=
$sa
?
$sa
[
'id'
]
:
1
;
}
foreach
(
$contractors
as
$c
)
{
$db
->
insert
(
'evaluations'
,
[
$this
->
db
->
insert
(
'evaluations'
,
[
'cycle_id'
=>
$cycleId
,
'cycle_id'
=>
$cycleId
,
'contractor_id'
=>
$c
[
'id'
],
'contractor_id'
=>
$contractor
[
'id'
],
'type'
=>
'technical'
,
'type'
=>
'technical'
,
'evaluator_id'
=>
$c
[
'pl_id'
]
??
$defaultAdmin
Id
,
'evaluator_id'
=>
$pl
Id
,
]);
]);
$this
->
db
->
insert
(
'evaluations'
,
[
$admin
=
$db
->
fetchOne
(
"SELECT id FROM users WHERE role IN ('admin','super_admin') AND is_active = 1 LIMIT 1"
);
$adminId
=
$admin
?
$admin
[
'id'
]
:
$plId
;
$db
->
insert
(
'evaluations'
,
[
'cycle_id'
=>
$cycleId
,
'cycle_id'
=>
$cycleId
,
'contractor_id'
=>
$c
[
'id'
],
'contractor_id'
=>
$contractor
[
'id'
],
'type'
=>
'professional'
,
'type'
=>
'professional'
,
'evaluator_id'
=>
$defaultA
dminId
,
'evaluator_id'
=>
$a
dminId
,
]);
]);
}
}
$db
->
commit
();
$evaluators
=
$db
->
fetchAll
(
"SELECT DISTINCT evaluator_id FROM evaluations WHERE cycle_id = ?"
,
[
$cycleId
]
);
foreach
(
$evaluators
as
$e
)
{
$notif
->
createImportant
(
$e
[
'evaluator_id'
],
'Evaluation Cycle Opened'
,
"The evaluation cycle for
{
$month
}
is now open. Please submit your evaluations."
,
'/evaluations/pending'
,
'evaluation_cycle'
,
$cycleId
);
}
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
throw
$e
;
}
}
}
}
}
\ No newline at end of file
modules/Evaluations/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\Evaluations\Controllers\EvaluationController
;
use
Modules\Evaluations\Controllers\EvaluationCycleController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/evaluations/mine'
,
[
EvaluationController
::
class
,
'myEvaluations'
]);
$router
->
get
(
'/evaluations/pending'
,
[
EvaluationController
::
class
,
'pending'
]);
$router
->
get
(
'/evaluations/compiled/{compiledId}'
,
[
EvaluationController
::
class
,
'showCompiled'
]);
$router
->
get
(
'/evaluations/{evaluationId}/technical'
,
[
EvaluationController
::
class
,
'technicalForm'
]);
$router
->
post
(
'/evaluations/{evaluationId}/technical'
,
[
EvaluationController
::
class
,
'submitTechnical'
]);
$router
->
get
(
'/evaluations/{evaluationId}/professional'
,
[
EvaluationController
::
class
,
'professionalForm'
]);
$router
->
post
(
'/evaluations/{evaluationId}/professional'
,
[
EvaluationController
::
class
,
'submitProfessional'
]);
$router
->
post
(
'/evaluations/compiled/{compiledId}/acknowledge'
,
[
EvaluationController
::
class
,
'acknowledge'
]);
$router
->
post
(
'/evaluations/compiled/{compiledId}/respond'
,
[
EvaluationController
::
class
,
'respond'
]);
$router
->
get
(
'/evaluations'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'myEvaluations'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/evaluations/cycles'
,
[
EvaluationCycleController
::
class
,
'index'
]);
$router
->
get
(
'/evaluations/pending'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'pending'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/evaluations/cycles/{cycleId}'
,
[
EvaluationCycleController
::
class
,
'show'
]);
$router
->
get
(
'/evaluations/compiled/{id}'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'showCompiled'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/evaluations/cycles'
,
[
EvaluationCycleController
::
class
,
'create'
]);
$router
->
post
(
'/evaluations/compiled/{id}/acknowledge'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'acknowledge'
,
[
'auth'
]);
$router
->
post
(
'/evaluations/compiled/{id}/respond'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'respond'
,
[
'auth'
]);
$router
->
get
(
'/evaluations/{id}/technical'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'technicalForm'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/evaluations/{id}/technical'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'submitTechnical'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/evaluations/{id}/professional'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'professionalForm'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/evaluations/{id}/professional'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'submitProfessional'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/evaluations/cycles'
,
\Modules\Evaluations\Controllers\EvaluationCycleController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
// API mirrors
$router
->
get
(
'/evaluations/cycles/{id}'
,
\Modules\Evaluations\Controllers\EvaluationCycleController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/evaluations'
,
[
EvaluationController
::
class
,
'myEvaluations'
]);
$router
->
post
(
'/evaluations/cycles'
,
\Modules\Evaluations\Controllers\EvaluationCycleController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/evaluations/pending'
,
[
EvaluationController
::
class
,
'pending'
]);
\ No newline at end of file
$router
->
get
(
'/api/evaluations/compiled/{compiledId}'
,
[
EvaluationController
::
class
,
'showCompiled'
]);
$router
->
post
(
'/api/evaluations/{evaluationId}/technical'
,
[
EvaluationController
::
class
,
'submitTechnical'
]);
$router
->
post
(
'/api/evaluations/{evaluationId}/professional'
,
[
EvaluationController
::
class
,
'submitProfessional'
]);
$router
->
post
(
'/api/evaluations/compiled/{compiledId}/acknowledge'
,
[
EvaluationController
::
class
,
'acknowledge'
]);
$router
->
post
(
'/api/evaluations/compiled/{compiledId}/respond'
,
[
EvaluationController
::
class
,
'respond'
]);
$router
->
get
(
'/api/evaluations/cycles'
,
[
EvaluationCycleController
::
class
,
'index'
]);
$router
->
get
(
'/api/evaluations/cycles/{cycleId}'
,
[
EvaluationCycleController
::
class
,
'show'
]);
$router
->
post
(
'/api/evaluations/cycles'
,
[
EvaluationCycleController
::
class
,
'create'
]);
};
\ No newline at end of file
modules/Holidays/routes.php
View file @
7d22a012
<?php
<?php
declare
(
strict_types
=
1
);
use
Engine\Core\Router
;
use
Modules\Holidays\Controllers\HolidayController
;
use
Engine\Core\Container
;
return
function
(
Router
$router
)
{
$router
->
get
(
'/holidays'
,
[
HolidayController
::
class
,
'index'
]);
$router
->
post
(
'/holidays'
,
[
HolidayController
::
class
,
'create'
]);
$router
->
put
(
'/holidays/{id}'
,
[
HolidayController
::
class
,
'update'
]);
$router
->
delete
(
'/holidays/{id}'
,
[
HolidayController
::
class
,
'delete'
]);
$router
=
Container
::
getInstance
()
->
resolve
(
Engine\Core\Router
::
class
);
$router
->
get
(
'/api/holidays'
,
[
HolidayController
::
class
,
'index'
]);
$router
->
post
(
'/api/holidays'
,
[
HolidayController
::
class
,
'create'
]);
$router
->
group
([
$router
->
put
(
'/api/holidays/{id}'
,
[
HolidayController
::
class
,
'update'
]);
'prefix'
=>
'/api/holidays'
,
$router
->
delete
(
'/api/holidays/{id}'
,
[
HolidayController
::
class
,
'delete'
]);
'middleware'
=>
[
Middleware\AuthenticationMiddleware
::
class
,
Middleware\CSRFMiddleware
::
class
]
};
],
function
(
$router
)
{
\ No newline at end of file
$router
->
get
(
'/'
,
Modules\Holidays\Controllers\HolidayController
::
class
,
'index'
);
$router
->
post
(
'/'
,
Modules\Holidays\Controllers\HolidayController
::
class
,
'create'
);
$router
->
post
(
'/{id}'
,
Modules\Holidays\Controllers\HolidayController
::
class
,
'update'
);
$router
->
post
(
'/{id}/delete'
,
Modules\Holidays\Controllers\HolidayController
::
class
,
'delete'
);
});
\ No newline at end of file
modules/LearningGoals/Jobs/LearningGoalReminderJob.php
View file @
7d22a012
...
@@ -10,39 +10,81 @@ use Engine\Notifications\NotificationManager;
...
@@ -10,39 +10,81 @@ use Engine\Notifications\NotificationManager;
final
class
LearningGoalReminderJob
implements
JobInterface
final
class
LearningGoalReminderJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
private
NotificationManager
$notif
;
{
return
'learning_goal_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$c
=
Container
::
getInstance
();
return
true
;
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$today
=
date
(
'Y-m-d'
);
$today
=
date
(
'Y-m-d'
);
$reminderDays
=
[
14
,
7
,
2
,
0
];
foreach
(
$reminderDays
as
$days
)
{
$goals
=
$db
->
fetchAll
(
$targetDate
=
date
(
'Y-m-d'
,
strtotime
(
"+
{
$days
}
days"
));
"SELECT lg.*, u.full_name_en as contractor_name, u.assigned_pl_id, ca.name as competency_name
$goals
=
$this
->
db
->
fetchAll
(
FROM learning_goals lg
"SELECT * FROM learning_goals WHERE deadline = ? AND status IN ('active','extended') AND deleted_at IS NULL"
,
JOIN users u ON u.id = lg.contractor_id
[
$targetDate
]
JOIN competency_areas ca ON ca.id = lg.competency_area_id
WHERE lg.status = 'active' AND lg.deleted_at IS NULL"
);
);
foreach
(
$goals
as
$g
)
{
$msg
=
$days
===
0
foreach
(
$goals
as
$goal
)
{
?
"Learning goal
\"
{
$g
[
'title'
]
}
\"
is due TODAY."
$daysRemaining
=
(
int
)((
strtotime
(
$goal
[
'deadline'
])
-
strtotime
(
$today
))
/
86400
);
:
"Learning goal
\"
{
$g
[
'title'
]
}
\"
is due in
{
$days
}
days."
;
$this
->
notif
->
createImportant
(
$g
[
'contractor_id'
],
'⏰ Learning Goal Deadline'
,
$msg
,
if
(
in_array
(
$daysRemaining
,
[
14
,
7
,
2
,
0
]))
{
'/learning-goals'
,
'learning_goal'
,
$g
[
'id'
]);
$urgencyMap
=
[
14
=>
'14 days remaining'
,
7
=>
'7 days remaining'
,
2
=>
'2 days remaining ⚠️'
,
0
=>
'DUE TODAY 🚨'
,
];
$urgency
=
$urgencyMap
[
$daysRemaining
];
$notif
->
createImportant
(
$goal
[
'contractor_id'
],
"Learning Goal:
{
$urgency
}
"
,
"Your learning goal
\"
{
$goal
[
'title'
]
}
\"
(
{
$goal
[
'competency_name'
]
}
) is due
{
$goal
[
'deadline'
]
}
.
{
$urgency
}
"
,
'/learning-goals'
,
'learning_goal'
,
(
int
)
$goal
[
'id'
]);
}
}
if
(
$daysRemaining
<
0
&&
$goal
[
'status'
]
===
'active'
)
{
$db
->
update
(
'learning_goals'
,
[
'status'
=>
'overdue'
],
'id = ?'
,
[(
int
)
$goal
[
'id'
]]);
$notif
->
createImportant
(
$goal
[
'contractor_id'
],
'Learning Goal Overdue'
,
"Your learning goal
\"
{
$goal
[
'title'
]
}
\"
is now overdue."
,
'/learning-goals'
,
'learning_goal'
,
(
int
)
$goal
[
'id'
]);
$recipients
=
[];
if
(
$goal
[
'assigned_pl_id'
])
{
$recipients
[]
=
$goal
[
'assigned_pl_id'
];
}
$admins
=
$db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin') AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
$recipients
[]
=
$a
[
'id'
];
}
}
$this
->
db
->
query
(
foreach
(
array_unique
(
$recipients
)
as
$rid
)
{
"UPDATE learning_goals SET status = 'overdue' WHERE deadline < ? AND status = 'active' AND deleted_at IS NULL"
,
$notif
->
createImportant
(
$rid
,
'Learning Goal Overdue'
,
[
$today
]
"
{
$goal
[
'contractor_name'
]
}
's learning goal
\"
{
$goal
[
'title'
]
}
\"
is overdue."
,
);
"/users/
{
$goal
[
'contractor_id'
]
}
"
,
'learning_goal'
,
(
int
)
$goal
[
'id'
]);
}
$daysOverdue
=
abs
(
$daysRemaining
);
$initialDeadlineDays
=
$goal
[
'is_auto_generated'
]
?
45
:
null
;
if
(
$initialDeadlineDays
&&
$daysOverdue
>=
$initialDeadlineDays
)
{
$admins
=
$db
->
fetchAll
(
"SELECT id FROM users WHERE role = 'super_admin' AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
$notif
->
createImportant
(
$a
[
'id'
],
'🚨 Learning Goal — Double Deadline Exceeded'
,
"
{
$goal
[
'contractor_name'
]
}
's auto-generated learning goal
\"
{
$goal
[
'title'
]
}
\"
has exceeded double the original deadline. Termination review required."
,
"/users/
{
$goal
[
'contractor_id'
]
}
"
,
'user'
,
(
int
)
$goal
[
'contractor_id'
]);
}
}
}
}
}
}
}
}
\ No newline at end of file
modules/LearningGoals/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\LearningGoals\Controllers\LearningGoalController
;
use
Modules\LearningGoals\Controllers\CompetencyController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/learning-goals'
,
[
LearningGoalController
::
class
,
'index'
]);
$router
->
post
(
'/learning-goals'
,
[
LearningGoalController
::
class
,
'create'
]);
$router
->
put
(
'/learning-goals/{goalId}'
,
[
LearningGoalController
::
class
,
'update'
]);
$router
->
post
(
'/learning-goals/{goalId}/assess'
,
[
LearningGoalController
::
class
,
'assess'
]);
$router
->
delete
(
'/learning-goals/{goalId}'
,
[
LearningGoalController
::
class
,
'delete'
]);
$router
->
get
(
'/learning-goals'
,
\Modules\LearningGoals\Controllers\LearningGoalController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/competency/areas'
,
[
CompetencyController
::
class
,
'areas'
]);
$router
->
post
(
'/learning-goals'
,
\Modules\LearningGoals\Controllers\LearningGoalController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/competency/profile/{userId}'
,
[
CompetencyController
::
class
,
'profile'
]);
$router
->
put
(
'/learning-goals/{id}'
,
\Modules\LearningGoals\Controllers\LearningGoalController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/competency/assess/{userId}'
,
[
CompetencyController
::
class
,
'submitAssessment'
]);
$router
->
post
(
'/learning-goals/{id}/assess'
,
\Modules\LearningGoals\Controllers\LearningGoalController
::
class
,
'assess'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/learning-goals/{id}'
,
\Modules\LearningGoals\Controllers\LearningGoalController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/competency/areas'
,
\Modules\LearningGoals\Controllers\CompetencyController
::
class
,
'areas'
,
[
'auth'
]);
$router
->
get
(
'/api/learning-goals'
,
[
LearningGoalController
::
class
,
'index'
]);
$router
->
get
(
'/competency/profile/{userId}'
,
\Modules\LearningGoals\Controllers\CompetencyController
::
class
,
'profile'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/learning-goals'
,
[
LearningGoalController
::
class
,
'create'
]);
$router
->
post
(
'/competency/assess/{userId}'
,
\Modules\LearningGoals\Controllers\CompetencyController
::
class
,
'submitAssessment'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/api/learning-goals/{goalId}'
,
[
LearningGoalController
::
class
,
'update'
]);
\ No newline at end of file
$router
->
put
(
'/api/learning-goals/{goalId}/assess'
,
[
LearningGoalController
::
class
,
'assess'
]);
$router
->
delete
(
'/api/learning-goals/{goalId}'
,
[
LearningGoalController
::
class
,
'delete'
]);
$router
->
get
(
'/api/competency/areas'
,
[
CompetencyController
::
class
,
'areas'
]);
$router
->
get
(
'/api/competency/profile/{userId}'
,
[
CompetencyController
::
class
,
'profile'
]);
$router
->
post
(
'/api/competency/assess/{userId}'
,
[
CompetencyController
::
class
,
'submitAssessment'
]);
};
\ No newline at end of file
modules/Meetings/Jobs/MeetingReminderJob.php
View file @
7d22a012
...
@@ -10,50 +10,80 @@ use Engine\Notifications\NotificationManager;
...
@@ -10,50 +10,80 @@ use Engine\Notifications\NotificationManager;
final
class
MeetingReminderJob
implements
JobInterface
final
class
MeetingReminderJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
private
NotificationManager
$notif
;
{
return
'meeting_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$c
=
Container
::
getInstance
();
return
true
;
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$now
=
time
();
$now
=
time
();
$oneHour
=
date
(
'Y-m-d H:i:s'
,
$now
+
3600
);
$today
=
date
(
'Y-m-d'
);
$oneHourAgo
=
date
(
'Y-m-d H:i:s'
,
$now
+
3000
);
$tomorrow
=
date
(
'Y-m-d'
,
strtotime
(
'+1 day'
));
$currentTime
=
date
(
'H:i:s'
);
$meetings
=
$this
->
db
->
fetchAll
(
// 1-day reminders: meetings tomorrow
"SELECT m.* FROM meetings m
$tomorrowMeetings
=
$db
->
fetchAll
(
WHERE m.status = 'scheduled'
"SELECT m.*, u.full_name_en as creator_name FROM meetings m
AND CONCAT(m.meeting_date, ' ', m.start_time) BETWEEN ? AND ?"
,
JOIN users u ON u.id = m.created_by_id
[
$oneHourAgo
,
$oneHour
]
WHERE m.meeting_date = ? AND m.status = 'scheduled'"
,
[
$tomorrow
]
);
);
foreach
(
$meetings
as
$m
)
{
foreach
(
$tomorrowMeetings
as
$m
)
{
$invitees
=
$this
->
db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]);
$invitees
=
$db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]
);
foreach
(
$invitees
as
$inv
)
{
foreach
(
$invitees
as
$inv
)
{
$this
->
notif
->
createImportant
(
$inv
[
'user_id'
],
'⏰ Meeting in 1 Hour'
,
$alreadySent
=
$db
->
fetchOne
(
"Meeting:
\"
{
$m
[
'title'
]
}
\"
starts in about 1 hour."
,
"SELECT id FROM notifications WHERE user_id = ? AND link_entity_type = 'meeting'
"/meetings/
{
$m
[
'id'
]
}
"
,
'meeting'
,
$m
[
'id'
]);
AND link_entity_id = ? AND title LIKE '%tomorrow%' AND created_at >= ?"
,
[
$inv
[
'user_id'
],
$m
[
'id'
],
date
(
'Y-m-d 00:00:00'
)]
);
if
(
!
$alreadySent
)
{
$notif
->
createImportant
(
$inv
[
'user_id'
],
"Meeting tomorrow:
{
$m
[
'title'
]
}
"
,
"Reminder:
\"
{
$m
[
'title'
]
}
\"
is scheduled for tomorrow at
{
$m
[
'start_time'
]
}
."
.
(
$m
[
'location'
]
?
" Location:
{
$m
[
'location'
]
}
"
:
''
),
"/meetings/
{
$m
[
'id'
]
}
"
,
'meeting'
,
(
int
)
$m
[
'id'
]);
}
}
}
}
}
$tomorrow
=
date
(
'Y-m-d'
,
strtotime
(
'+1 day'
));
// 1-hour reminders: meetings today within the next hour
$tomorrowMeetings
=
$this
->
db
->
fetchAll
(
$oneHourFromNow
=
date
(
'H:i:s'
,
strtotime
(
'+1 hour'
));
"SELECT * FROM meetings WHERE meeting_date = ? AND status = 'scheduled'"
,
$soonMeetings
=
$db
->
fetchAll
(
[
$tomorrow
]
"SELECT m.* FROM meetings m
WHERE m.meeting_date = ? AND m.status = 'scheduled'
AND m.start_time > ? AND m.start_time <= ?"
,
[
$today
,
$currentTime
,
$oneHourFromNow
]
);
);
foreach
(
$tomorrowMeetings
as
$m
)
{
foreach
(
$soonMeetings
as
$m
)
{
$invitees
=
$this
->
db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]);
$invitees
=
$db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]
);
foreach
(
$invitees
as
$inv
)
{
foreach
(
$invitees
as
$inv
)
{
$this
->
notif
->
createImportant
(
$inv
[
'user_id'
],
'Meeting Tomorrow'
,
$alreadySent
=
$db
->
fetchOne
(
"Meeting:
\"
{
$m
[
'title'
]
}
\"
is scheduled for tomorrow at
{
$m
[
'start_time'
]
}
."
,
"SELECT id FROM notifications WHERE user_id = ? AND link_entity_type = 'meeting'
"/meetings/
{
$m
[
'id'
]
}
"
,
'meeting'
,
$m
[
'id'
]);
AND link_entity_id = ? AND title LIKE '%1 hour%' AND created_at >= ?"
,
[
$inv
[
'user_id'
],
$m
[
'id'
],
date
(
'Y-m-d 00:00:00'
)]
);
if
(
!
$alreadySent
)
{
$notif
->
createImportant
(
$inv
[
'user_id'
],
"Meeting in 1 hour:
{
$m
[
'title'
]
}
"
,
"Starting at
{
$m
[
'start_time'
]
}
."
.
(
$m
[
'location'
]
?
" Location:
{
$m
[
'location'
]
}
"
:
''
),
"/meetings/
{
$m
[
'id'
]
}
"
,
'meeting'
,
(
int
)
$m
[
'id'
]);
}
}
}
}
}
}
}
...
...
modules/Meetings/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\Meetings\Controllers\MeetingController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/meetings'
,
[
MeetingController
::
class
,
'index'
]);
$router
->
get
(
'/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'show'
]);
$router
->
post
(
'/meetings'
,
[
MeetingController
::
class
,
'create'
]);
$router
->
put
(
'/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'update'
]);
$router
->
post
(
'/meetings/{meetingId}/notes'
,
[
MeetingController
::
class
,
'addNotes'
]);
$router
->
delete
(
'/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'delete'
]);
$router
->
get
(
'/meetings'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/meetings'
,
[
MeetingController
::
class
,
'index'
]);
$router
->
get
(
'/meetings/{id}'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'show'
]);
$router
->
post
(
'/meetings'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/meetings'
,
[
MeetingController
::
class
,
'create'
]);
$router
->
put
(
'/meetings/{id}'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/api/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'update'
]);
$router
->
post
(
'/meetings/{id}/notes'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'addNotes'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/meetings/{meetingId}/notes'
,
[
MeetingController
::
class
,
'addNotes'
]);
$router
->
delete
(
'/meetings/{id}'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/api/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'delete'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/Offboarding/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\Offboarding\Controllers\OffboardingController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
post
(
'/offboarding/initiate'
,
[
OffboardingController
::
class
,
'initiate'
]);
$router
->
get
(
'/offboarding/settlement/{userId}'
,
[
OffboardingController
::
class
,
'calculateFinalSettlement'
]);
$router
->
post
(
'/offboarding/initiate'
,
\Modules\Offboarding\Controllers\OffboardingController
::
class
,
'initiate'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/offboarding/initiate'
,
[
OffboardingController
::
class
,
'initiate'
]);
$router
->
get
(
'/offboarding/settlement/{userId}'
,
\Modules\Offboarding\Controllers\OffboardingController
::
class
,
'calculateFinalSettlement'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/offboarding/settlement/{userId}'
,
[
OffboardingController
::
class
,
'calculateFinalSettlement'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/PIPs/Jobs/PIPCheckinReminderJob.php
View file @
7d22a012
...
@@ -10,32 +10,78 @@ use Engine\Notifications\NotificationManager;
...
@@ -10,32 +10,78 @@ use Engine\Notifications\NotificationManager;
final
class
PIPCheckinReminderJob
implements
JobInterface
final
class
PIPCheckinReminderJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
private
NotificationManager
$notif
;
{
return
'pip_checkin_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$c
=
Container
::
getInstance
();
return
true
;
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$today
=
date
(
'Y-m-d'
);
$today
=
date
(
'Y-m-d'
);
$checkins
=
$this
->
db
->
fetchAll
(
$tomorrow
=
date
(
'Y-m-d'
,
strtotime
(
'+1 day'
));
"SELECT pc.*, p.contractor_id, p.created_by_id FROM pip_checkins pc
$checkins
=
$db
->
fetchAll
(
"SELECT pc.*, p.contractor_id, p.created_by_id, u.full_name_en as contractor_name,
u.assigned_pl_id
FROM pip_checkins pc
JOIN pips p ON p.id = pc.pip_id
JOIN pips p ON p.id = pc.pip_id
WHERE pc.scheduled_date = ? AND pc.logged_at IS NULL AND p.status = 'active' AND p.deleted_at IS NULL"
,
JOIN users u ON u.id = p.contractor_id
WHERE pc.scheduled_date IN (?, ?)
AND pc.logged_at IS NULL
AND p.status = 'active'
AND p.deleted_at IS NULL"
,
[
$today
,
$tomorrow
]
);
foreach
(
$checkins
as
$checkin
)
{
$isToday
=
$checkin
[
'scheduled_date'
]
===
$today
;
$urgency
=
$isToday
?
'TODAY'
:
'tomorrow'
;
$notif
->
createImportant
(
$checkin
[
'contractor_id'
],
"PIP Check-in
{
$urgency
}
"
,
"You have a PIP check-in scheduled for
{
$urgency
}
."
,
"/pips/
{
$checkin
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$checkin
[
'pip_id'
]);
$notif
->
createImportant
(
$checkin
[
'created_by_id'
],
"PIP Check-in
{
$urgency
}
"
,
"PIP check-in for
{
$checkin
[
'contractor_name'
]
}
is scheduled
{
$urgency
}
."
,
"/pips/
{
$checkin
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$checkin
[
'pip_id'
]);
if
(
$checkin
[
'assigned_pl_id'
]
&&
$checkin
[
'assigned_pl_id'
]
!==
$checkin
[
'created_by_id'
])
{
$notif
->
createImportant
(
$checkin
[
'assigned_pl_id'
],
"PIP Check-in
{
$urgency
}
"
,
"PIP check-in for
{
$checkin
[
'contractor_name'
]
}
is scheduled
{
$urgency
}
."
,
"/pips/
{
$checkin
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$checkin
[
'pip_id'
]);
}
}
$missedCheckins
=
$db
->
fetchAll
(
"SELECT pc.*, p.contractor_id, u.full_name_en as contractor_name
FROM pip_checkins pc
JOIN pips p ON p.id = pc.pip_id
JOIN users u ON u.id = p.contractor_id
WHERE pc.scheduled_date < ?
AND pc.logged_at IS NULL
AND p.status = 'active'
AND p.deleted_at IS NULL"
,
[
$today
]
[
$today
]
);
);
foreach
(
$checkins
as
$ci
)
{
foreach
(
$missedCheckins
as
$missed
)
{
$this
->
notif
->
createImportant
(
$ci
[
'contractor_id'
],
'PIP Check-in Today'
,
$daysSince
=
(
int
)((
strtotime
(
$today
)
-
strtotime
(
$missed
[
'scheduled_date'
]))
/
86400
);
'You have a PIP check-in scheduled for today.'
,
"/pips/
{
$ci
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$ci
[
'pip_id'
]);
if
(
$daysSince
===
2
)
{
$this
->
notif
->
createImportant
(
$ci
[
'created_by_id'
],
'PIP Check-in Due'
,
$admins
=
$db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin') AND is_active = 1"
);
"PIP check-in for contractor ID
{
$ci
[
'contractor_id'
]
}
is scheduled today."
,
foreach
(
$admins
as
$a
)
{
"/pips/
{
$ci
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$ci
[
'pip_id'
]);
$notif
->
createImportant
(
$a
[
'id'
],
'Missed PIP Check-in'
,
"PIP check-in for
{
$missed
[
'contractor_name'
]
}
on
{
$missed
[
'scheduled_date'
]
}
was missed. Notes have not been logged."
,
"/pips/
{
$missed
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$missed
[
'pip_id'
]);
}
}
}
}
}
}
}
}
\ No newline at end of file
modules/PIPs/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\PIPs\Controllers\PIPController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/pips'
,
[
PIPController
::
class
,
'index'
]);
$router
->
get
(
'/pips/{pipId}'
,
[
PIPController
::
class
,
'show'
]);
$router
->
post
(
'/pips'
,
[
PIPController
::
class
,
'create'
]);
$router
->
post
(
'/pips/{pipId}/acknowledge'
,
[
PIPController
::
class
,
'acknowledge'
]);
$router
->
post
(
'/pips/{pipId}/checkins/{checkinId}'
,
[
PIPController
::
class
,
'logCheckin'
]);
$router
->
post
(
'/pips/{pipId}/decide'
,
[
PIPController
::
class
,
'decide'
]);
$router
->
delete
(
'/pips/{pipId}'
,
[
PIPController
::
class
,
'delete'
]);
$router
->
get
(
'/pips'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/pips'
,
[
PIPController
::
class
,
'index'
]);
$router
->
get
(
'/pips/{id}'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/pips/{pipId}'
,
[
PIPController
::
class
,
'show'
]);
$router
->
post
(
'/pips'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/pips'
,
[
PIPController
::
class
,
'create'
]);
$router
->
post
(
'/pips/{id}/acknowledge'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'acknowledge'
,
[
'auth'
]);
$router
->
post
(
'/api/pips/{pipId}/acknowledge'
,
[
PIPController
::
class
,
'acknowledge'
]);
$router
->
post
(
'/pips/{id}/checkins/{checkinId}'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'logCheckin'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/pips/{pipId}/checkin'
,
[
PIPController
::
class
,
'logCheckin'
]);
$router
->
post
(
'/pips/{id}/decide'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'decide'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/api/pips/{pipId}/result'
,
[
PIPController
::
class
,
'decide'
]);
$router
->
delete
(
'/pips/{id}'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/api/pips/{pipId}'
,
[
PIPController
::
class
,
'delete'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/RecurringCards/Jobs/CreateRecurringCardsJob.php
View file @
7d22a012
...
@@ -6,84 +6,161 @@ namespace Modules\RecurringCards\Jobs;
...
@@ -6,84 +6,161 @@ namespace Modules\RecurringCards\Jobs;
use
Engine\Scheduler\JobInterface
;
use
Engine\Scheduler\JobInterface
;
use
Engine\Core\Container
;
use
Engine\Core\Container
;
use
Engine\Database\Connection
;
use
Engine\Database\Connection
;
use
Engine\Notifications\NotificationManager
;
final
class
CreateRecurringCardsJob
implements
JobInterface
final
class
CreateRecurringCardsJob
implements
JobInterface
{
{
private
Connection
$db
;
public
function
key
()
:
string
{
return
'create_recurring_cards'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
)
;
return
true
;
}
}
public
function
run
()
:
void
public
function
run
()
:
void
{
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$now
=
date
(
'Y-m-d H:i:s'
);
$now
=
date
(
'Y-m-d H:i:s'
);
$definitions
=
$this
->
db
->
fetchAll
(
"SELECT * FROM recurring_card_definitions WHERE is_active = 1 AND (next_creation_at IS NULL OR next_creation_at <= ?)"
,
$definitions
=
$db
->
fetchAll
(
"SELECT rcd.*, b.board_key, b.card_sequence, b.is_archived
FROM recurring_card_definitions rcd
JOIN boards b ON b.id = rcd.board_id
WHERE rcd.is_active = 1 AND rcd.next_creation_at IS NOT NULL AND rcd.next_creation_at <= ?
AND b.is_archived = 0"
,
[
$now
]
[
$now
]
);
);
foreach
(
$definitions
as
$def
)
{
foreach
(
$definitions
as
$def
)
{
$template
=
json_decode
(
$def
[
'card_template_json'
],
true
);
$db
->
beginTransaction
();
if
(
!
$template
)
continue
;
try
{
$template
=
json_decode
(
$def
[
'card_template_json'
],
true
)
??
[];
$board
=
$this
->
db
->
fetchOne
(
"SELECT * FROM boards WHERE id = ? AND is_archived = 0"
,
[
$def
[
'board_id'
]]);
$db
->
query
(
"UPDATE boards SET card_sequence = card_sequence + 1 WHERE id = ?"
,
[(
int
)
$def
[
'board_id'
]]);
if
(
!
$board
)
continue
;
$updatedBoard
=
$db
->
fetchOne
(
"SELECT card_sequence FROM boards WHERE id = ?"
,
[(
int
)
$def
[
'board_id'
]]);
$cardKey
=
$def
[
'board_key'
]
.
'-'
.
$updatedBoard
[
'card_sequence'
];
$backlogCol
=
$this
->
db
->
fetchOne
(
$backlogCol
=
$db
->
fetchOne
(
"SELECT id FROM board_columns WHERE board_id = ? AND slug = 'backlog'"
,
[
$board
[
'id'
]]
"SELECT id FROM board_columns WHERE board_id = ? AND slug = 'backlog'"
,
[(
int
)
$def
[
'board_id'
]]
);
);
if
(
!
$backlogCol
)
continue
;
$this
->
db
->
transaction
(
function
()
use
(
$def
,
$template
,
$board
,
$backlogCol
)
{
if
(
!
$backlogCol
)
{
$this
->
db
->
query
(
"UPDATE boards SET card_sequence = card_sequence + 1 WHERE id = ?"
,
[
$board
[
'id'
]]
);
$db
->
rollBack
(
);
$updated
=
$this
->
db
->
fetchOne
(
"SELECT card_sequence FROM boards WHERE id = ?"
,
[
$board
[
'id'
]])
;
continue
;
$cardKey
=
$board
[
'board_key'
]
.
'-'
.
$updated
[
'card_sequence'
];
}
$title
=
(
$template
[
'title'
]
??
'Recurring Task'
)
.
' — '
.
date
(
'M j, Y'
);
$dateStr
=
date
(
'M j, Y'
);
$title
=
(
$template
[
'title'
]
??
'Recurring Task'
)
.
" —
{
$dateStr
}
"
;
$cardId
=
$
this
->
db
->
insert
(
'cards'
,
[
$cardId
=
$db
->
insert
(
'cards'
,
[
'board_id'
=>
$board
[
'
id'
],
'board_id'
=>
(
int
)
$def
[
'board_
id'
],
'column_id'
=>
$backlogCol
[
'id'
],
'column_id'
=>
$backlogCol
[
'id'
],
'card_number'
=>
$updated
[
'card_sequence'
],
'card_number'
=>
$updated
Board
[
'card_sequence'
],
'card_key'
=>
$cardKey
,
'card_key'
=>
$cardKey
,
'title'
=>
$title
,
'title'
=>
$title
,
'description'
=>
$template
[
'description'
]
??
null
,
'description'
=>
$template
[
'description'
]
??
null
,
'priority'
=>
$template
[
'priority'
]
??
'none'
,
'priority'
=>
$template
[
'priority'
]
??
'none'
,
'estimated_hours'
=>
$template
[
'estimated_hours'
]
??
null
,
'estimated_hours'
=>
isset
(
$template
[
'estimated_hours'
])
?
(
float
)
$template
[
'estimated_hours'
]
:
null
,
'deadline'
=>
$template
[
'deadline_offset_days'
]
??
null
?
date
(
'Y-m-d 23:59:00'
,
strtotime
(
"+
{
$template
[
'deadline_offset_days'
]
}
days"
))
:
null
,
'position_in_column'
=>
0
,
'position_in_column'
=>
0
,
'created_by_id'
=>
$def
[
'created_by_id'
],
'created_by_id'
=>
(
int
)
$def
[
'created_by_id'
],
]);
]);
$db
->
insert
(
'card_activity_log'
,
[
'card_id'
=>
$cardId
,
'user_id'
=>
null
,
'action'
=>
'created'
,
'details_json'
=>
json_encode
([
'source'
=>
'recurring'
,
'definition_id'
=>
$def
[
'id'
]]),
]);
if
(
!
empty
(
$template
[
'labels'
]))
{
foreach
(
$template
[
'labels'
]
as
$labelId
)
{
$labelExists
=
$db
->
fetchOne
(
"SELECT id FROM labels WHERE id = ?"
,
[(
int
)
$labelId
]);
if
(
$labelExists
)
{
$db
->
query
(
"INSERT IGNORE INTO card_labels (card_id, label_id) VALUES (?, ?)"
,
[
$cardId
,
(
int
)
$labelId
]);
}
}
}
if
(
!
empty
(
$template
[
'checklists'
]))
{
foreach
(
$template
[
'checklists'
]
as
$ci
=>
$checklist
)
{
$clId
=
$db
->
insert
(
'card_checklists'
,
[
'card_id'
=>
$cardId
,
'name'
=>
$checklist
[
'name'
]
??
'Checklist'
,
'position'
=>
$ci
+
1
,
]);
if
(
!
empty
(
$checklist
[
'items'
]))
{
foreach
(
$checklist
[
'items'
]
as
$ii
=>
$item
)
{
$db
->
insert
(
'card_checklist_items'
,
[
'checklist_id'
=>
$clId
,
'text'
=>
is_string
(
$item
)
?
$item
:
(
$item
[
'text'
]
??
''
),
'position'
=>
$ii
+
1
,
]);
}
}
}
}
$assignees
=
$def
[
'assignees_json'
]
?
json_decode
(
$def
[
'assignees_json'
],
true
)
:
[];
$assignees
=
$def
[
'assignees_json'
]
?
json_decode
(
$def
[
'assignees_json'
],
true
)
:
[];
foreach
(
$assignees
as
$
ui
d
)
{
foreach
(
$assignees
as
$
assigneeI
d
)
{
$
this
->
db
->
insert
(
'card_assignments'
,
[
$db
->
insert
(
'card_assignments'
,
[
'card_id'
=>
$cardId
,
'card_id'
=>
$cardId
,
'user_id'
=>
(
int
)
$
ui
d
,
'user_id'
=>
(
int
)
$
assigneeI
d
,
'assigned_by_id'
=>
$def
[
'created_by_id'
],
'assigned_by_id'
=>
(
int
)
$def
[
'created_by_id'
],
]);
]);
$notif
->
createImportant
((
int
)
$assigneeId
,
'Recurring Task Created'
,
"Recurring card
{
$cardKey
}
:
{
$title
}
has been created."
,
"/cards/
{
$cardId
}
"
,
'card'
,
$cardId
);
}
}
$nextCreation
=
$this
->
calculateNextCreation
(
$def
);
$nextCreation
=
$this
->
calculateNextCreation
(
$def
);
$
this
->
db
->
update
(
'recurring_card_definitions'
,
[
$db
->
update
(
'recurring_card_definitions'
,
[
'last_created_at'
=>
date
(
'Y-m-d H:i:s'
)
,
'last_created_at'
=>
$now
,
'next_creation_at'
=>
$nextCreation
,
'next_creation_at'
=>
$nextCreation
,
],
'id = ?'
,
[
$def
[
'id'
]]);
],
'id = ?'
,
[(
int
)
$def
[
'id'
]]);
});
$db
->
commit
();
}
catch
(
\Throwable
$e
)
{
$db
->
rollBack
();
error_log
(
"CreateRecurringCardsJob error for def
{
$def
[
'id'
]
}
: "
.
$e
->
getMessage
());
}
}
}
}
}
private
function
calculateNextCreation
(
array
$def
)
:
string
private
function
calculateNextCreation
(
array
$def
)
:
string
{
{
$now
=
time
();
$now
=
time
();
return
match
(
$def
[
'frequency'
])
{
switch
(
$def
[
'frequency'
])
{
'daily'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 day'
,
$now
)),
case
'daily'
:
'weekly'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 week'
,
$now
)),
return
date
(
'Y-m-d H:i:s'
,
strtotime
(
'+1 day'
,
$now
));
'biweekly'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+2 weeks'
,
$now
)),
case
'weekly'
:
'monthly'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 month'
,
$now
)),
$dayNames
=
[
'Sunday'
,
'Monday'
,
'Tuesday'
,
'Wednesday'
,
'Thursday'
,
'Friday'
,
'Saturday'
];
'custom'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+'
.
(
int
)
$def
[
'frequency_days'
]
.
' days'
,
$now
)),
$targetDay
=
$dayNames
[
$def
[
'day_of_week'
]
??
1
]
??
'Monday'
;
default
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 week'
,
$now
)),
return
date
(
'Y-m-d H:i:s'
,
strtotime
(
"next
{
$targetDay
}
"
,
$now
));
};
case
'biweekly'
:
return
date
(
'Y-m-d H:i:s'
,
strtotime
(
'+2 weeks'
,
$now
));
case
'monthly'
:
$dom
=
$def
[
'day_of_month'
]
??
1
;
$nextMonth
=
strtotime
(
'+1 month'
,
$now
);
$nextDate
=
date
(
'Y-m'
,
$nextMonth
)
.
'-'
.
str_pad
((
string
)
$dom
,
2
,
'0'
,
STR_PAD_LEFT
);
if
(
!
checkdate
((
int
)
date
(
'm'
,
$nextMonth
),
$dom
,
(
int
)
date
(
'Y'
,
$nextMonth
)))
{
$nextDate
=
date
(
'Y-m-t'
,
$nextMonth
);
}
return
$nextDate
.
' 04:00:00'
;
case
'custom'
:
$days
=
(
int
)(
$def
[
'frequency_days'
]
??
7
);
return
date
(
'Y-m-d H:i:s'
,
strtotime
(
"+
{
$days
}
days"
,
$now
));
default
:
return
date
(
'Y-m-d H:i:s'
,
strtotime
(
'+7 days'
,
$now
));
}
}
}
}
}
\ No newline at end of file
modules/RecurringCards/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\RecurringCards\Controllers\RecurringCardController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/recurring-cards'
,
[
RecurringCardController
::
class
,
'index'
]);
$router
->
post
(
'/recurring-cards'
,
[
RecurringCardController
::
class
,
'create'
]);
$router
->
put
(
'/recurring-cards/{defId}'
,
[
RecurringCardController
::
class
,
'update'
]);
$router
->
delete
(
'/recurring-cards/{defId}'
,
[
RecurringCardController
::
class
,
'delete'
]);
$router
->
get
(
'/recurring-cards'
,
\Modules\RecurringCards\Controllers\RecurringCardController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/recurring-cards'
,
[
RecurringCardController
::
class
,
'index'
]);
$router
->
post
(
'/recurring-cards'
,
\Modules\RecurringCards\Controllers\RecurringCardController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/recurring-cards'
,
[
RecurringCardController
::
class
,
'create'
]);
$router
->
put
(
'/recurring-cards/{id}'
,
\Modules\RecurringCards\Controllers\RecurringCardController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/api/recurring-cards/{defId}'
,
[
RecurringCardController
::
class
,
'update'
]);
$router
->
delete
(
'/recurring-cards/{id}'
,
\Modules\RecurringCards\Controllers\RecurringCardController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/api/recurring-cards/{defId}'
,
[
RecurringCardController
::
class
,
'delete'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/Salary/routes.php
View file @
7d22a012
<?php
<?php
declare
(
strict_types
=
1
);
use
Engine\Core\Router
;
// Salary routes are served via the Dashboard HUD and API endpoints
use
Modules\Dashboard\Controllers\DashboardController
;
// Additional salary-specific routes will be added in Phase 2+
\ No newline at end of file
return
function
(
Router
$router
)
{
// HUD data is served via the Dashboard controller's getHudData
// and SSE stream. Salary module routes for API access:
$router
->
get
(
'/api/users/{userId}/hud'
,
function
(
\Engine\Core\Request
$request
,
string
$userId
)
{
$user
=
$request
->
user
();
$db
=
\Engine\Core\Container
::
getInstance
()
->
resolve
(
\Engine\Database\Connection
::
class
);
$targetId
=
(
int
)
$userId
;
if
(
$user
[
'role'
]
===
'contractor'
&&
$user
[
'id'
]
!==
$targetId
)
{
return
\Engine\Core\Response
::
json
([
'error'
=>
'Forbidden'
],
403
);
}
$target
=
$db
->
fetchOne
(
"SELECT * FROM users WHERE id = ?"
,
[
$targetId
]);
if
(
!
$target
)
{
return
\Engine\Core\Response
::
json
([
'error'
=>
'User not found'
],
404
);
}
$month
=
date
(
'Y-m'
);
$actualSalary
=
(
float
)(
$target
[
'actual_salary'
]
??
0
);
$totalBounties
=
(
float
)
$db
->
fetchColumn
(
"SELECT COALESCE(SUM(amount), 0) FROM bounty_payouts WHERE recipient_id = ? AND payroll_month = ?"
,
[
$targetId
,
$month
]
);
$totalDeductions
=
(
float
)
$db
->
fetchColumn
(
"SELECT COALESCE(SUM(COALESCE(final_amount, calculated_amount)), 0) FROM deductions
WHERE contractor_id = ? AND payroll_month = ? AND status IN ('applied','applied_no_response','reduced','accepted') AND deleted_at IS NULL"
,
[
$targetId
,
$month
]
);
$totalPosAdj
=
(
float
)
$db
->
fetchColumn
(
"SELECT COALESCE(SUM(amount), 0) FROM manual_adjustments
WHERE contractor_id = ? AND effective_month = ? AND type = 'positive' AND status = 'approved' AND deleted_at IS NULL"
,
[
$targetId
,
$month
]
);
$totalNegAdj
=
(
float
)
$db
->
fetchColumn
(
"SELECT COALESCE(SUM(amount), 0) FROM manual_adjustments
WHERE contractor_id = ? AND effective_month = ? AND type = 'negative' AND status = 'approved' AND deleted_at IS NULL"
,
[
$targetId
,
$month
]
);
$liveSalary
=
$actualSalary
+
$totalBounties
+
$totalPosAdj
-
$totalDeductions
-
$totalNegAdj
;
$retentionPct
=
$actualSalary
>
0
?
(
$liveSalary
/
$actualSalary
)
*
100
:
100
;
return
\Engine\Core\Response
::
json
([
'actual_salary'
=>
$actualSalary
,
'live_salary'
=>
round
(
$liveSalary
,
2
),
'total_bounties'
=>
$totalBounties
,
'total_deductions'
=>
$totalDeductions
,
'total_pos_adj'
=>
$totalPosAdj
,
'total_neg_adj'
=>
$totalNegAdj
,
'retention_pct'
=>
round
(
$retentionPct
,
2
),
'month'
=>
$month
,
]);
});
};
\ No newline at end of file
modules/SavedFilters/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\SavedFilters\Controllers\SavedFilterController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/saved-filters'
,
[
SavedFilterController
::
class
,
'index'
]);
$router
->
post
(
'/saved-filters'
,
[
SavedFilterController
::
class
,
'create'
]);
$router
->
delete
(
'/saved-filters/{filterId}'
,
[
SavedFilterController
::
class
,
'delete'
]);
$router
->
get
(
'/saved-filters'
,
\Modules\SavedFilters\Controllers\SavedFilterController
::
class
,
'index'
,
[
'auth'
]);
$router
->
get
(
'/api/saved-filters'
,
[
SavedFilterController
::
class
,
'index'
]);
$router
->
post
(
'/saved-filters'
,
\Modules\SavedFilters\Controllers\SavedFilterController
::
class
,
'create'
,
[
'auth'
]);
$router
->
post
(
'/api/saved-filters'
,
[
SavedFilterController
::
class
,
'create'
]);
$router
->
delete
(
'/saved-filters/{id}'
,
\Modules\SavedFilters\Controllers\SavedFilterController
::
class
,
'delete'
,
[
'auth'
]);
$router
->
delete
(
'/api/saved-filters/{filterId}'
,
[
SavedFilterController
::
class
,
'delete'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/Schedules/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\Schedules\Controllers\ScheduleController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/schedules/user/{userId}'
,
[
ScheduleController
::
class
,
'currentSchedule'
]);
$router
->
get
(
'/schedules/requests'
,
[
ScheduleController
::
class
,
'requests'
]);
$router
->
post
(
'/schedules/requests'
,
[
ScheduleController
::
class
,
'submitRequest'
]);
$router
->
post
(
'/schedules/requests/{requestId}/review'
,
[
ScheduleController
::
class
,
'reviewRequest'
]);
$router
->
put
(
'/schedules/user/{userId}/direct'
,
[
ScheduleController
::
class
,
'directEdit'
]);
$router
->
get
(
'/schedules/users/{userId}'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'currentSchedule'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/users/{userId}/schedule'
,
[
ScheduleController
::
class
,
'currentSchedule'
]);
$router
->
get
(
'/schedules/requests'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'requests'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/schedule-requests'
,
[
ScheduleController
::
class
,
'requests'
]);
$router
->
post
(
'/schedules/requests'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'submitRequest'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/schedule-requests'
,
[
ScheduleController
::
class
,
'submitRequest'
]);
$router
->
post
(
'/schedules/requests/{id}/review'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'reviewRequest'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/api/schedule-requests/{requestId}/review'
,
[
ScheduleController
::
class
,
'reviewRequest'
]);
$router
->
post
(
'/schedules/users/{userId}/edit'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'directEdit'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/api/users/{userId}/schedule'
,
[
ScheduleController
::
class
,
'directEdit'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/TeamAvailability/routes.php
View file @
7d22a012
<?php
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\TeamAvailability\Controllers\TeamAvailabilityController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
return
function
(
Router
$router
)
{
$router
->
get
(
'/team-availability'
,
[
TeamAvailabilityController
::
class
,
'index'
]);
$router
->
get
(
'/team-availability'
,
\Modules\TeamAvailability\Controllers\TeamAvailabilityController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/api/team-availability'
,
[
TeamAvailabilityController
::
class
,
'index'
]);
\ No newline at end of file
};
\ No newline at end of file
modules/Unavailability/routes.php
View file @
7d22a012
<?php
<?php
declare
(
strict_types
=
1
);
use
Engine\Core\Router
;
use
Modules\Unavailability\Controllers\UnavailabilityController
;
use
Engine\Core\Container
;
return
function
(
Router
$router
)
{
$router
->
get
(
'/unavailability'
,
[
UnavailabilityController
::
class
,
'index'
]);
$router
->
post
(
'/unavailability'
,
[
UnavailabilityController
::
class
,
'create'
]);
$router
->
put
(
'/unavailability/{id}'
,
[
UnavailabilityController
::
class
,
'update'
]);
$router
->
delete
(
'/unavailability/{id}'
,
[
UnavailabilityController
::
class
,
'delete'
]);
$router
=
Container
::
getInstance
()
->
resolve
(
Engine\Core\Router
::
class
);
$router
->
get
(
'/api/unavailability'
,
[
UnavailabilityController
::
class
,
'index'
]);
$router
->
post
(
'/api/unavailability'
,
[
UnavailabilityController
::
class
,
'create'
]);
$router
->
group
([
$router
->
put
(
'/api/unavailability/{id}'
,
[
UnavailabilityController
::
class
,
'update'
]);
'prefix'
=>
'/api/unavailability'
,
$router
->
delete
(
'/api/unavailability/{id}'
,
[
UnavailabilityController
::
class
,
'delete'
]);
'middleware'
=>
[
Middleware\AuthenticationMiddleware
::
class
,
Middleware\CSRFMiddleware
::
class
]
};
],
function
(
$router
)
{
\ No newline at end of file
$router
->
get
(
'/'
,
Modules\Unavailability\Controllers\UnavailabilityController
::
class
,
'index'
);
$router
->
post
(
'/'
,
Modules\Unavailability\Controllers\UnavailabilityController
::
class
,
'create'
);
$router
->
post
(
'/{id}'
,
Modules\Unavailability\Controllers\UnavailabilityController
::
class
,
'update'
);
$router
->
post
(
'/{id}/delete'
,
Modules\Unavailability\Controllers\UnavailabilityController
::
class
,
'delete'
);
});
\ No newline at end of file
public/assets/css/dark-mode.css
0 → 100644
View file @
7d22a012
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment