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
Hide 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
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
->
post
(
'/board-templates'
,
\Modules\BoardTemplates\Controllers\BoardTemplateController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/board-templates/from-board/{boardId}'
,
\Modules\BoardTemplates\Controllers\BoardTemplateController
::
class
,
'saveFromBoard'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/board-templates/{id}'
,
\Modules\BoardTemplates\Controllers\BoardTemplateController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/board-templates'
,
[
BoardTemplateController
::
class
,
'index'
]);
$router
->
post
(
'/api/board-templates'
,
[
BoardTemplateController
::
class
,
'create'
]);
$router
->
post
(
'/api/board-templates/from-board/{boardId}'
,
[
BoardTemplateController
::
class
,
'saveFromBoard'
]);
$router
->
delete
(
'/api/board-templates/{templateId}'
,
[
BoardTemplateController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/CardTemplates/routes.php
View file @
7d22a012
<?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
->
post
(
'/card-templates'
,
\Modules\CardTemplates\Controllers\CardTemplateController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/card-templates/{id}'
,
\Modules\CardTemplates\Controllers\CardTemplateController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/card-templates/{id}'
,
\Modules\CardTemplates\Controllers\CardTemplateController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/card-templates'
,
[
CardTemplateController
::
class
,
'index'
]);
$router
->
post
(
'/api/card-templates'
,
[
CardTemplateController
::
class
,
'create'
]);
$router
->
put
(
'/api/card-templates/{templateId}'
,
[
CardTemplateController
::
class
,
'update'
]);
$router
->
delete
(
'/api/card-templates/{templateId}'
,
[
CardTemplateController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/Contracts/Jobs/ContractExpiryWarningJob.php
View file @
7d22a012
...
...
@@ -10,33 +10,47 @@ use Engine\Notifications\NotificationManager;
final
class
ContractExpiryWarningJob
implements
JobInterface
{
private
Connection
$db
;
private
NotificationManager
$notif
;
public
function
key
()
:
string
{
return
'contract_expiry_warnings'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
$c
=
Container
::
getInstance
();
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
return
true
;
}
public
function
run
()
:
void
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$warningDays
=
[
90
,
60
,
30
];
$today
=
date
(
'Y-m-d'
);
foreach
(
$warningDays
as
$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
WHERE contract_end_date = ? AND status = 'active'"
,
WHERE contract_end_date = ? AND status = 'active'
AND is_active = 1
"
,
[
$targetDate
]
);
foreach
(
$expiring
as
$u
)
{
$admins
=
$this
->
db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin') AND is_active = 1"
);
foreach
(
$expiring
as
$user
)
{
$admins
=
$db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin') AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
$this
->
notif
->
createImportant
(
$a
[
'id'
],
"Contract Expiring in
{
$days
}
Days"
,
"
{
$u
[
'full_name_en'
]
}
's contract expires on
{
$u
[
'contract_end_date'
]
}
."
,
"/users/
{
$u
[
'id'
]
}
"
,
'user'
,
$u
[
'id'
]);
$notif
->
createImportant
(
$a
[
'id'
],
"Contract Expiring in
{
$days
}
Days"
,
"
{
$user
[
'full_name_en'
]
}
's contract expires on
{
$user
[
'contract_end_date'
]
}
."
,
"/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
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
(
'/contracts/{id}'
,
\Modules\Contracts\Controllers\ContractController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/policies'
,
[
PolicyController
::
class
,
'index'
]);
$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
(
'/policies/{id}'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/policies'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/policies/{id}/publish'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'publish'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/policies/versions/{versionId}/acknowledge'
,
\Modules\Contracts\Controllers\PolicyController
::
class
,
'acknowledge'
,
[
'auth'
]);
$router
->
get
(
'/notices'
,
[
NoticeController
::
class
,
'index'
]);
$router
->
post
(
'/notices'
,
[
NoticeController
::
class
,
'create'
]);
$router
->
post
(
'/notices/{noticeId}/acknowledge'
,
[
NoticeController
::
class
,
'acknowledgeNotice'
]);
$router
->
delete
(
'/notices/{noticeId}'
,
[
NoticeController
::
class
,
'delete'
]);
$router
->
get
(
'/notices'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/notices'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/notices/{id}/acknowledge'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'acknowledgeNotice'
,
[
'auth'
]);
$router
->
delete
(
'/notices/{id}'
,
\Modules\Contracts\Controllers\NoticeController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/contracts'
,
[
ContractController
::
class
,
'index'
]);
$router
->
get
(
'/api/contracts/{contractId}'
,
[
ContractController
::
class
,
'show'
]);
$router
->
get
(
'/api/policies'
,
[
PolicyController
::
class
,
'index'
]);
$router
->
get
(
'/api/policies/{policyId}'
,
[
PolicyController
::
class
,
'show'
]);
$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
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
->
post
(
'/deduction-presets'
,
\Modules\DeductionPresets\Controllers\DeductionPresetController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/deduction-presets/{id}'
,
\Modules\DeductionPresets\Controllers\DeductionPresetController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/deduction-presets/{id}'
,
\Modules\DeductionPresets\Controllers\DeductionPresetController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/deduction-presets'
,
[
DeductionPresetController
::
class
,
'index'
]);
$router
->
post
(
'/api/deduction-presets'
,
[
DeductionPresetController
::
class
,
'create'
]);
$router
->
put
(
'/api/deduction-presets/{presetId}'
,
[
DeductionPresetController
::
class
,
'update'
]);
$router
->
delete
(
'/api/deduction-presets/{presetId}'
,
[
DeductionPresetController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/Deductions/Jobs/AutoApplyExpiredDeductionsJob.php
View file @
7d22a012
...
...
@@ -6,34 +6,78 @@ namespace Modules\Deductions\Jobs;
use
Engine\Scheduler\JobInterface
;
use
Engine\Core\Container
;
use
Engine\Database\Connection
;
use
Engine\Notifications\NotificationManager
;
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
$this
->
db
->
query
(
"UPDATE deductions
SET status = 'applied_no_response',
final_amount = calculated_amount,
applied_at = NOW(),
payroll_month = DATE_FORMAT(NOW(), '%Y-%m')
WHERE status = 'acknowledged'
AND response_deadline IS NOT NULL
AND response_deadline < NOW()
AND deleted_at IS NULL"
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$now
=
date
(
'Y-m-d H:i:s'
);
$expired
=
$db
->
fetchAll
(
"SELECT d.*, u.full_name_en as contractor_name FROM deductions d
JOIN users u ON u.id = d.contractor_id
WHERE d.status = 'acknowledged'
AND d.response_deadline IS NOT 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;
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
{
$overdueCards
=
$this
->
db
->
fetchAll
(
"SELECT c.id, c.card_key, c.deadline, c.board_id,
DATEDIFF(NOW(), c.deadline) as days_late,
ca.user_id as assignee_id
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$today
=
date
(
'Y-m-d'
);
$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
JOIN card_assignments ca ON ca.card_id = c.id
WHERE c.deadline IS NOT NULL AND c.deadline < NOW()
AND c.done_at IS NULL AND c.is_archived = 0"
WHERE c.deadline IS NOT NULL AND c.deadline < ?
AND c.done_at IS NULL AND c.is_archived = 0"
,
[
$today
,
$today
.
' 00:00:00'
]
);
foreach
(
$overdueCards
as
$card
)
{
$daysLate
=
(
int
)
$card
[
'days_late'
];
if
(
$daysLate
<=
0
)
continue
;
$subCategory
=
match
(
true
)
{
$daysLate
>=
15
=>
'A4'
,
$daysLate
>=
8
=>
'A3'
,
$daysLate
>=
4
=>
'A2'
,
default
=>
'A1'
,
};
$existing
=
$this
->
db
->
fetchOne
(
"SELECT id, sub_category FROM deductions
WHERE related_card_id = ? AND contractor_id = ? AND category = 'A'
AND status NOT IN ('dismissed','applied','applied_no_response','reduced','accepted')
AND deleted_at IS NULL"
,
[
$card
[
'id'
],
$card
[
'assignee_id'
]]
$newSub
=
$this
->
determineSubCategory
(
$daysLate
);
if
(
!
$newSub
)
continue
;
$assignees
=
$db
->
fetchAll
(
"SELECT user_id FROM card_assignments WHERE card_id = ?"
,
[(
int
)
$card
[
'card_id'
]]
);
if
(
$existing
)
{
if
(
$existing
[
'sub_category'
]
!==
$subCategory
)
{
$contractor
=
$this
->
db
->
fetchOne
(
"SELECT actual_salary FROM users WHERE id = ?"
,
[
$card
[
'assignee_id'
]]);
$actualSalary
=
(
float
)(
$contractor
[
'actual_salary'
]
??
0
);
$dailyRate
=
$actualSalary
>
0
?
round
(
$actualSalary
/
22
,
2
)
:
0
;
$newAmount
=
match
(
$subCategory
)
{
'A1'
=>
round
(
$dailyRate
*
0.05
*
$daysLate
,
2
),
'A2'
=>
round
(
$dailyRate
*
0.10
*
$daysLate
,
2
),
'A3'
=>
round
(
$dailyRate
*
0.15
*
$daysLate
,
2
),
'A4'
=>
round
(
$actualSalary
*
0.25
,
2
),
default
=>
0
,
};
$this
->
db
->
update
(
'deductions'
,
[
'sub_category'
=>
$subCategory
,
'calculated_amount'
=>
$newAmount
,
'description'
=>
"Auto-escalated: Card
{
$card
[
'card_key'
]
}
is
{
$daysLate
}
days overdue."
,
],
'id = ?'
,
[
$existing
[
'id'
]]);
foreach
(
$assignees
as
$assignee
)
{
$existingDeduction
=
$db
->
fetchOne
(
"SELECT id, sub_category FROM deductions
WHERE contractor_id = ? AND related_card_id = ? AND category = 'A'
AND status NOT IN ('dismissed','applied','applied_no_response','reduced','accepted')
AND deleted_at IS NULL
ORDER BY created_at DESC LIMIT 1"
,
[
$assignee
[
'user_id'
],
(
int
)
$card
[
'card_id'
]]
);
if
(
$existingDeduction
)
{
$existingSub
=
$existingDeduction
[
'sub_category'
];
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
);
$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
),
'A2'
=>
round
(
$dailyRate
*
0.10
*
$daysLate
,
2
),
'A3'
=>
round
(
$dailyRate
*
0.15
*
$daysLate
,
2
),
'A4'
=>
round
(
$actualSalary
*
0.25
,
2
),
default
=>
0
,
};
}
private
function
getExpectedWorkingDays
(
Connection
$db
,
int
$userId
)
:
int
{
$month
=
date
(
'Y-m'
);
$startDate
=
$month
.
'-01'
;
$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;
final
class
OverallScoreCalculator
implements
CalculatorInterface
{
public
function
calculate
(
array
$context
)
:
mixed
public
function
calculate
(
array
$context
)
:
array
{
$techScore
=
(
float
)(
$context
[
'technical_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'
;
$techWeight
=
(
float
)(
$criteria
[
'overall_weights'
][
'technical'
]
??
0.5
);
$profWeight
=
(
float
)(
$criteria
[
'overall_weights'
][
'professional'
]
??
0.5
);
$overallScore
=
round
((
$techScore
*
$techWeight
)
+
(
$profScore
*
$profWeight
),
2
);
$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
)
{
if
(
$overall
>=
$r
[
'min'
]
&&
$overall
<=
$r
[
'max'
])
{
$rating
=
$r
[
'rating'
];
break
;
if
(
$score
>=
$r
[
'min'
]
&&
$score
<=
$r
[
'max'
])
{
return
$r
;
}
}
return
[
'overall_score'
=>
$overall
,
'rating'
=>
$rating
,
];
return
[
'rating'
=>
'unacceptable'
,
'label'
=>
'🔴 Unacceptable'
];
}
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;
final
class
ProfessionalAutoScoreCalculator
implements
CalculatorInterface
{
private
Connection
$db
;
public
function
__construct
()
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
}
public
function
calculate
(
array
$context
)
:
mixed
public
function
calculate
(
array
$context
)
:
array
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$contractorId
=
(
int
)
$context
[
'contractor_id'
];
$month
=
$context
[
'month'
];
$month
=
$context
[
'month'
];
// YYYY-MM
$startDate
=
$month
.
'-01'
;
$endDate
=
date
(
'Y-m-t'
,
strtotime
(
$startDate
));
// Reporting Compliance: (reports_on_time / expected_reports) * 5
$expectedReports
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(*) FROM daily_reports
WHERE user_id = ? AND report_date >= ? AND report_date <= ?
AND status != 'draft'"
,
[
$contractorId
,
$startDate
,
$endDate
]
);
$totalReports
=
(
int
)
$this
->
db
->
fetchColumn
(
"SELECT COUNT(*) FROM daily_reports WHERE user_id = ? AND report_date LIKE ?"
,
[
$contractorId
,
$month
.
'%'
]
// Count working days from schedule for a more accurate expected count
$scheduleDays
=
$db
->
fetchAll
(
"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'
);
$onTimeReports
=
(
int
)
$this
->
db
->
fetchColumn
(
"SELECT COUNT(*) FROM daily_reports WHERE user_id = ? AND report_date LIKE ? AND is_on_time = 1"
,
[
$contractorId
,
$month
.
'%'
]
$holidays
=
$db
->
fetchAll
(
"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
;
}
}
$reportingCompliance
=
$totalReports
>
0
?
round
((
$onTimeReports
/
$totalReports
)
*
5
,
2
)
:
1.0
;
$reportingCompliance
=
min
(
5.0
,
max
(
1.0
,
$reportingCompliance
));
$totalExpected
=
0
;
$current
=
strtotime
(
$startDate
);
$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
;
}
$violations
=
(
int
)
$this
->
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"
,
$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
]
);
$policyCompliance
=
max
(
1.0
,
min
(
5.0
,
5.0
-
(
$violations
*
0.5
)
));
$policyCompliance
=
max
(
1.0
,
round
(
5.0
-
(
$violations
*
0.5
),
2
));
return
[
'reporting_compliance'
=>
$reportingCompliance
,
'policy_compliance'
=>
round
(
$policyCompliance
,
2
)
,
'
total_reports'
=>
$totalReports
,
'
on_time_reports'
=>
$onTimeReports
,
'violations'
=>
$violations
,
'policy_compliance'
=>
$policyCompliance
,
'
reports_on_time'
=>
$reportsOnTime
,
'
expected_reports'
=>
$totalExpected
,
'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,62 +9,71 @@ use Engine\Database\Connection;
final
class
TechnicalAutoScoreCalculator
implements
CalculatorInterface
{
private
Connection
$db
;
public
function
__construct
()
{
$this
->
db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
}
public
function
calculate
(
array
$context
)
:
mixed
public
function
calculate
(
array
$context
)
:
array
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$contractorId
=
(
int
)
$context
[
'contractor_id'
];
$month
=
$context
[
'month'
];
$month
=
$context
[
'month'
];
// YYYY-MM
$startDate
=
$month
.
'-01'
;
$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
JOIN cards c ON c.id = ca.card_id
WHERE ca.user_id = ? AND ca.created_at
BETWEEN ? AND
?"
,
[
$contractorId
,
$
startDate
.
' 00:00:00'
,
$
endDate
.
' 23:59:59'
]
WHERE ca.user_id = ? AND ca.created_at
<=
?"
,
[
$contractorId
,
$endDate
.
' 23:59:59'
]
);
$cardsDone
=
(
int
)
$
this
->
db
->
fetchColumn
(
$cardsDone
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(DISTINCT c.id) FROM cards c
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'
]
);
$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
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'
]
);
$cardsOnTime
=
(
int
)
$
this
->
db
->
fetchColumn
(
$cardsOnTime
=
(
int
)
$db
->
fetchColumn
(
"SELECT COUNT(DISTINCT c.id) FROM cards c
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
AND c.done_at <= c.deadline 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 <= c.deadline
AND c.done_at >= ? AND c.done_at <= ?"
,
[
$contractorId
,
$startDate
.
' 00:00:00'
,
$endDate
.
' 23:59:59'
]
);
$taskCompletionRate
=
$cardsAssigned
>
0
?
round
((
$cardsDone
/
$cardsAssigned
)
*
5
,
2
)
:
3.0
;
$deadlineCompliance
=
$cardsWithDeadline
>
0
?
round
((
$cardsOnTime
/
$cardsWithDeadline
)
*
5
,
2
)
:
3.0
;
$taskCompletionRate
=
min
(
5.0
,
max
(
1.0
,
$taskCompletionRate
));
$deadlineCompliance
=
min
(
5.0
,
max
(
1.0
,
$deadlineCompliance
));
$deadlineCompliance
=
$cardsWithDeadline
>
0
?
min
(
5.0
,
round
((
$cardsOnTime
/
$cardsWithDeadline
)
*
5
,
2
))
:
3.0
;
return
[
'task_completion_rate'
=>
$taskCompletionRate
,
'deadline_compliance'
=>
$deadlineCompliance
,
'cards_assigned'
=>
$cardsAssigned
,
'cards_done'
=>
$cardsDone
,
'cards_with_deadline'
=>
$cardsWithDeadline
,
'cards_on_time'
=>
$cardsOnTime
,
'deadline_compliance'
=>
$deadlineCompliance
,
'cards_assigned'
=>
$cardsAssigned
,
'cards_done'
=>
$cardsDone
,
'cards_with_deadline'
=>
$cardsWithDeadline
,
'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;
final
class
CompileEvaluationsJob
implements
JobInterface
{
private
Connection
$db
;
private
NotificationManager
$notif
;
private
CalculationEngine
$calc
;
public
function
key
()
:
string
{
return
'compile_evaluations'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
$c
=
Container
::
getInstance
();
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
$this
->
calc
=
$c
->
resolve
(
CalculationEngine
::
class
);
return
true
;
}
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"
);
if
(
!
$cycle
)
return
;
$contractors
=
$this
->
db
->
fetchAll
(
if
(
!
$activeCycle
)
{
return
;
}
$contractors
=
$db
->
fetchAll
(
"SELECT DISTINCT contractor_id FROM evaluations WHERE cycle_id = ?"
,
[
$
c
ycle
[
'id'
]]
[
$
activeC
ycle
[
'id'
]]
);
$compiledCount
=
0
;
$allCompiled
=
true
;
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 = ?"
,
[
$
cycle
[
'id'
],
$ci
d
]
[
$
activeCycle
[
'id'
],
$contractorI
d
]
);
if
(
$existing
)
continue
;
if
(
$alreadyCompiled
)
{
continue
;
}
$tech
=
$this
->
db
->
fetchOne
(
"SELECT
total_score FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'technical' AND submitted_at IS NOT NULL
"
,
[
$
cycle
[
'id'
],
$ci
d
]
$tech
Eval
=
$
db
->
fetchOne
(
"SELECT
* FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'technical'
"
,
[
$
activeCycle
[
'id'
],
$contractorI
d
]
);
$prof
=
$this
->
db
->
fetchOne
(
"SELECT
total_score FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'professional' AND submitted_at IS NOT NULL
"
,
[
$
cycle
[
'id'
],
$ci
d
]
$prof
Eval
=
$
db
->
fetchOne
(
"SELECT
* FROM evaluations WHERE cycle_id = ? AND contractor_id = ? AND type = 'professional'
"
,
[
$
activeCycle
[
'id'
],
$contractorI
d
]
);
if
(
!
$tech
||
!
$prof
)
continue
;
if
(
!
$techEval
||
!
$techEval
[
'submitted_at'
]
||
!
$profEval
||
!
$profEval
[
'submitted_at'
])
{
$allCompiled
=
false
;
continue
;
}
$result
=
$this
->
calc
->
calculate
(
'overall_eval_score'
,
[
'technical_score'
=>
(
float
)
$tech
[
'total_score'
],
'professional_score'
=>
(
float
)
$prof
[
'total_score'
],
$techScore
=
(
float
)
$techEval
[
'total_score'
];
$profScore
=
(
float
)
$profEval
[
'total_score'
];
$overallResult
=
$calc
->
calculate
(
'overall_eval_score'
,
[
'technical_score'
=>
$techScore
,
'professional_score'
=>
$profScore
,
'tech_weight'
=>
0.5
,
'prof_weight'
=>
0.5
,
]);
$metrics
=
[
'technical_score'
=>
(
float
)
$tech
[
'total_score'
],
'professional_score'
=>
(
float
)
$prof
[
'total_score'
],
'month'
=>
$cycle
[
'month'
],
$contractor
=
$db
->
fetchOne
(
"SELECT * FROM users WHERE id = ?"
,
[
$contractorId
]);
$systemMetrics
=
[
'month'
=>
$activeCycle
[
'month'
],
'actual_salary'
=>
$contractor
[
'actual_salary'
]
??
0
,
'technical_score'
=>
$techScore
,
'professional_score'
=>
$profScore
,
];
$this
->
db
->
insert
(
'compiled_evaluations'
,
[
'cycle_id'
=>
$cycle
[
'id'
],
'contractor_id'
=>
$cid
,
'technical_score'
=>
(
float
)
$tech
[
'total_score'
],
'professional_score'
=>
(
float
)
$prof
[
'total_score'
],
'overall_score'
=>
$result
[
'overall_score'
],
'rating'
=>
$result
[
'rating'
],
'system_metrics_json'
=>
json_encode
(
$metrics
),
if
(
$calc
->
has
(
'technical_auto_score'
))
{
$systemMetrics
[
'tech_auto'
]
=
$calc
->
calculate
(
'technical_auto_score'
,
[
'contractor_id'
=>
$contractorId
,
'month'
=>
$activeCycle
[
'month'
],
]);
}
if
(
$calc
->
has
(
'professional_auto_score'
))
{
$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'
),
]);
$
this
->
notif
->
createBlocking
(
$ci
d
,
'Monthly Evaluation Published'
,
"Your evaluation for
{
$
cycle
[
'month'
]
}
has been compiled. Overall score:
{
$result
[
'overall_score'
]
}
/5.00 (
{
$result
[
'rating
'
]
}
)"
,
"/evaluations/compiled/
{
$c
id
}
"
,
'compiled_evaluation'
,
$ci
d
);
$
notif
->
createBlocking
(
$contractorI
d
,
'Monthly Evaluation Published'
,
"Your evaluation for
{
$
activeCycle
[
'month'
]
}
has been compiled. Overall score:
{
$overallResult
[
'overall_score'
]
}
(
{
$overallResult
[
'rating_label
'
]
}
)"
,
"/evaluations/compiled/
{
$c
ompiledId
}
"
,
'compiled_evaluation'
,
$compiledI
d
);
if
(
$
r
esult
[
'overall_score'
]
<
2.5
)
{
$admins
=
$
this
->
db
->
fetchAll
(
"SELECT id FROM users WHERE role = 'super_admin'
AND is_active = 1"
);
if
(
$
overallR
esult
[
'overall_score'
]
<
2.5
)
{
$admins
=
$
db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin')
AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
$
this
->
notif
->
createImportant
(
$a
[
'id'
],
'Low Evaluation Score Alert
'
,
"Contractor
ID
{
$cid
}
scored
{
$result
[
'overall_score'
]
}
(
{
$result
[
'rating'
]
}
)
. PIP recommended."
,
"/users/
{
$c
id
}
"
,
'user'
,
$ci
d
);
$
notif
->
createImportant
(
$a
[
'id'
],
'Low Evaluation Score — PIP Recommended
'
,
"Contractor
{
$contractor
[
'full_name_en'
]
}
scored
{
$overallResult
[
'overall_score'
]
}
(
{
$overallResult
[
'rating_label'
]
}
) for
{
$activeCycle
[
'month'
]
}
. PIP recommended."
,
"/users/
{
$c
ontractorId
}
"
,
'user'
,
$contractorI
d
);
}
}
$compiledCount
++
;
}
$totalExpected
=
count
(
$contractors
);
$totalCompiled
=
(
int
)
$this
->
db
->
fetchColumn
(
"SELECT COUNT(*) FROM compiled_evaluations WHERE cycle_id = ?"
,
[
$cycle
[
'id'
]]
);
if
(
$totalCompiled
>=
$totalExpected
&&
$totalExpected
>
0
)
{
$this
->
db
->
update
(
'evaluation_cycles'
,
[
if
(
$allCompiled
)
{
$db
->
update
(
'evaluation_cycles'
,
[
'status'
=>
'completed'
,
'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;
final
class
EvaluationReminderJob
implements
JobInterface
{
private
Connection
$db
;
private
NotificationManager
$notif
;
public
function
key
()
:
string
{
return
'evaluation_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
$c
=
Container
::
getInstance
();
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
return
true
;
}
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"
);
if
(
!
$cycle
)
return
;
if
(
!
$activeCycle
)
{
return
;
}
$now
=
time
();
$techDeadline
=
strtotime
(
$
c
ycle
[
'tech_deadline'
]);
$profDeadline
=
strtotime
(
$
c
ycle
[
'prof_deadline'
]);
$techDeadline
=
strtotime
(
$
activeC
ycle
[
'tech_deadline'
]);
$profDeadline
=
strtotime
(
$
activeC
ycle
[
'prof_deadline'
]);
$daysT
o
Tech
=
(
int
)
ceil
((
$techDeadline
-
$now
)
/
86400
);
$daysT
o
Prof
=
(
int
)
ceil
((
$profDeadline
-
$now
)
/
86400
);
$daysT
il
Tech
=
(
int
)
ceil
((
$techDeadline
-
$now
)
/
86400
);
$daysT
il
Prof
=
(
int
)
ceil
((
$profDeadline
-
$now
)
/
86400
);
if
(
$daysToTech
<=
2
&&
$daysToTech
>=
0
)
{
$pending
=
$this
->
db
->
fetchAll
(
"SELECT DISTINCT evaluator_id FROM evaluations WHERE cycle_id = ? AND type = 'technical' AND submitted_at IS NULL"
,
[
$cycle
[
'id'
]]
if
(
$daysTilTech
<=
2
&&
$daysTilTech
>=
0
)
{
$pendingTech
=
$db
->
fetchAll
(
"SELECT DISTINCT evaluator_id FROM evaluations
WHERE cycle_id = ? AND type = 'technical' AND submitted_at IS NULL"
,
[
$activeCycle
[
'id'
]]
);
foreach
(
$pending
as
$p
)
{
$this
->
notif
->
createImportant
(
$p
[
'evaluator_id'
],
'Technical Evaluation Due'
,
"Technical evaluations for
{
$cycle
[
'month'
]
}
are due in
{
$daysToTech
}
day(s). Please submit them."
,
'/evaluations/pending'
,
'evaluation_cycle'
,
$cycle
[
'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'
]]
);
$urgency
=
$daysTilTech
===
0
?
'🚨 DUE TODAY'
:
"⏰
{
$daysTilTech
}
days remaining"
;
$notif
->
createImportant
(
$e
[
'evaluator_id'
],
"Technical Evaluations
{
$urgency
}
"
,
"You have
{
$count
}
technical evaluation(s) pending for
{
$activeCycle
[
'month'
]
}
. Deadline: "
.
date
(
'M j'
,
$techDeadline
),
'/evaluations/pending'
,
'evaluation_cycle'
,
$activeCycle
[
'id'
]);
}
}
if
(
$daysToProf
<=
2
&&
$daysToProf
>=
0
)
{
$pending
=
$this
->
db
->
fetchAll
(
"SELECT DISTINCT evaluator_id FROM evaluations WHERE cycle_id = ? AND type = 'professional' AND submitted_at IS NULL"
,
[
$cycle
[
'id'
]]
if
(
$daysTilProf
<=
2
&&
$daysTilProf
>=
0
)
{
$pendingProf
=
$db
->
fetchAll
(
"SELECT DISTINCT evaluator_id FROM evaluations
WHERE cycle_id = ? AND type = 'professional' AND submitted_at IS NULL"
,
[
$activeCycle
[
'id'
]]
);
foreach
(
$pending
as
$p
)
{
$this
->
notif
->
createImportant
(
$p
[
'evaluator_id'
],
'Professional Evaluation Due'
,
"Professional evaluations for
{
$cycle
[
'month'
]
}
are due in
{
$daysToProf
}
day(s). Please submit them."
,
'/evaluations/pending'
,
'evaluation_cycle'
,
$cycle
[
'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'
]]
);
$urgency
=
$daysTilProf
===
0
?
'🚨 DUE TODAY'
:
"⏰
{
$daysTilProf
}
days remaining"
;
$notif
->
createImportant
(
$e
[
'evaluator_id'
],
"Professional Evaluations
{
$urgency
}
"
,
"You have
{
$count
}
professional evaluation(s) pending for
{
$activeCycle
[
'month'
]
}
. Deadline: "
.
date
(
'M j'
,
$profDeadline
),
'/evaluations/pending'
,
'evaluation_cycle'
,
$activeCycle
[
'id'
]);
}
}
}
...
...
modules/Evaluations/Jobs/OpenEvaluationCycleJob.php
View file @
7d22a012
...
...
@@ -6,56 +6,99 @@ namespace Modules\Evaluations\Jobs;
use
Engine\Scheduler\JobInterface
;
use
Engine\Core\Container
;
use
Engine\Database\Connection
;
use
Engine\Notifications\NotificationManager
;
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
{
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'
));
$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'
);
$cycleId
=
$this
->
db
->
insert
(
'evaluation_cycles'
,
[
'month'
=>
$month
,
'status'
=>
'open'
,
'opened_at'
=>
$now
,
'tech_deadline'
=>
date
(
'Y-m-d 23:59:59'
,
strtotime
(
'+5 weekdays'
)),
'prof_deadline'
=>
date
(
'Y-m-d 23:59:59'
,
strtotime
(
'+7 weekdays'
)),
]);
$contractors
=
$this
->
db
->
fetchAll
(
"SELECT u.id, (SELECT bm.user_id FROM board_members bm
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
FROM users u WHERE u.role = 'contractor' AND u.status IN ('active','on_pip') AND u.is_active = 1"
);
$defaultAdmin
=
$this
->
db
->
fetchOne
(
"SELECT id FROM users WHERE role IN ('admin','super_admin') AND is_active = 1 LIMIT 1"
);
$defaultAdminId
=
$defaultAdmin
?
$defaultAdmin
[
'id'
]
:
1
;
foreach
(
$contractors
as
$c
)
{
$this
->
db
->
insert
(
'evaluations'
,
[
'cycle_id'
=>
$cycleId
,
'contractor_id'
=>
$c
[
'id'
],
'type'
=>
'technical'
,
'evaluator_id'
=>
$c
[
'pl_id'
]
??
$defaultAdminId
,
]);
$this
->
db
->
insert
(
'evaluations'
,
[
'cycle_id'
=>
$cycleId
,
'contractor_id'
=>
$c
[
'id'
],
'type'
=>
'professional'
,
'evaluator_id'
=>
$defaultAdminId
,
$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
,
'status'
=>
'open'
,
'opened_at'
=>
$now
,
'tech_deadline'
=>
$techDeadline
,
'prof_deadline'
=>
$profDeadline
,
]);
$contractors
=
$db
->
fetchAll
(
"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
WHERE bm2.user_id = ? AND bm.role_on_board = 'project_leader' LIMIT 1"
,
[
$contractor
[
'id'
]]
);
$plId
=
$pl
?
$pl
[
'user_id'
]
:
null
;
if
(
!
$plId
)
{
$sa
=
$db
->
fetchOne
(
"SELECT id FROM users WHERE role = 'super_admin' AND is_active = 1 LIMIT 1"
);
$plId
=
$sa
?
$sa
[
'id'
]
:
1
;
}
$db
->
insert
(
'evaluations'
,
[
'cycle_id'
=>
$cycleId
,
'contractor_id'
=>
$contractor
[
'id'
],
'type'
=>
'technical'
,
'evaluator_id'
=>
$plId
,
]);
$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
,
'contractor_id'
=>
$contractor
[
'id'
],
'type'
=>
'professional'
,
'evaluator_id'
=>
$adminId
,
]);
}
$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
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/pending'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'pending'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/evaluations/compiled/{id}'
,
\Modules\Evaluations\Controllers\EvaluationController
::
class
,
'showCompiled'
,
[
'auth'
,
'blocking'
]);
$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'
,
[
EvaluationCycleController
::
class
,
'index'
]);
$router
->
get
(
'/evaluations/cycles/{cycleId}'
,
[
EvaluationCycleController
::
class
,
'show'
]);
$router
->
post
(
'/evaluations/cycles'
,
[
EvaluationCycleController
::
class
,
'create'
]);
$router
->
get
(
'/evaluations/cycles'
,
\Modules\Evaluations\Controllers\EvaluationCycleController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
$router
->
get
(
'/evaluations/cycles/{id}'
,
\Modules\Evaluations\Controllers\EvaluationCycleController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/evaluations/cycles'
,
\Modules\Evaluations\Controllers\EvaluationCycleController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
// API mirrors
$router
->
get
(
'/api/evaluations'
,
[
EvaluationController
::
class
,
'myEvaluations'
]);
$router
->
get
(
'/api/evaluations/pending'
,
[
EvaluationController
::
class
,
'pending'
]);
$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
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
->
group
([
'prefix'
=>
'/api/holidays'
,
'middleware'
=>
[
Middleware\AuthenticationMiddleware
::
class
,
Middleware\CSRFMiddleware
::
class
]
],
function
(
$router
)
{
$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
$router
->
get
(
'/api/holidays'
,
[
HolidayController
::
class
,
'index'
]);
$router
->
post
(
'/api/holidays'
,
[
HolidayController
::
class
,
'create'
]);
$router
->
put
(
'/api/holidays/{id}'
,
[
HolidayController
::
class
,
'update'
]);
$router
->
delete
(
'/api/holidays/{id}'
,
[
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;
final
class
LearningGoalReminderJob
implements
JobInterface
{
private
Connection
$db
;
private
NotificationManager
$notif
;
public
function
key
()
:
string
{
return
'learning_goal_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
$c
=
Container
::
getInstance
();
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
return
true
;
}
public
function
run
()
:
void
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$today
=
date
(
'Y-m-d'
);
$reminderDays
=
[
14
,
7
,
2
,
0
];
foreach
(
$reminderDays
as
$days
)
{
$targetDate
=
date
(
'Y-m-d'
,
strtotime
(
"+
{
$days
}
days"
));
$goals
=
$this
->
db
->
fetchAll
(
"SELECT * FROM learning_goals WHERE deadline = ? AND status IN ('active','extended') AND deleted_at IS NULL"
,
[
$targetDate
]
);
foreach
(
$goals
as
$g
)
{
$msg
=
$days
===
0
?
"Learning goal
\"
{
$g
[
'title'
]
}
\"
is due TODAY."
:
"Learning goal
\"
{
$g
[
'title'
]
}
\"
is due in
{
$days
}
days."
;
$this
->
notif
->
createImportant
(
$g
[
'contractor_id'
],
'⏰ Learning Goal Deadline'
,
$msg
,
'/learning-goals'
,
'learning_goal'
,
$g
[
'id'
]);
}
}
$this
->
db
->
query
(
"UPDATE learning_goals SET status = 'overdue' WHERE deadline < ? AND status = 'active' AND deleted_at IS NULL"
,
[
$today
]
$goals
=
$db
->
fetchAll
(
"SELECT lg.*, u.full_name_en as contractor_name, u.assigned_pl_id, ca.name as competency_name
FROM learning_goals lg
JOIN users u ON u.id = lg.contractor_id
JOIN competency_areas ca ON ca.id = lg.competency_area_id
WHERE lg.status = 'active' AND lg.deleted_at IS NULL"
);
foreach
(
$goals
as
$goal
)
{
$daysRemaining
=
(
int
)((
strtotime
(
$goal
[
'deadline'
])
-
strtotime
(
$today
))
/
86400
);
if
(
in_array
(
$daysRemaining
,
[
14
,
7
,
2
,
0
]))
{
$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'
];
}
foreach
(
array_unique
(
$recipients
)
as
$rid
)
{
$notif
->
createImportant
(
$rid
,
'Learning Goal Overdue'
,
"
{
$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
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
->
post
(
'/learning-goals'
,
\Modules\LearningGoals\Controllers\LearningGoalController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/learning-goals/{id}'
,
\Modules\LearningGoals\Controllers\LearningGoalController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$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'
,
[
CompetencyController
::
class
,
'areas'
]);
$router
->
get
(
'/competency/profile/{userId}'
,
[
CompetencyController
::
class
,
'profile'
]);
$router
->
post
(
'/competency/assess/{userId}'
,
[
CompetencyController
::
class
,
'submitAssessment'
]);
$router
->
get
(
'/competency/areas'
,
\Modules\LearningGoals\Controllers\CompetencyController
::
class
,
'areas'
,
[
'auth'
]);
$router
->
get
(
'/competency/profile/{userId}'
,
\Modules\LearningGoals\Controllers\CompetencyController
::
class
,
'profile'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/competency/assess/{userId}'
,
\Modules\LearningGoals\Controllers\CompetencyController
::
class
,
'submitAssessment'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/learning-goals'
,
[
LearningGoalController
::
class
,
'index'
]);
$router
->
post
(
'/api/learning-goals'
,
[
LearningGoalController
::
class
,
'create'
]);
$router
->
put
(
'/api/learning-goals/{goalId}'
,
[
LearningGoalController
::
class
,
'update'
]);
$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;
final
class
MeetingReminderJob
implements
JobInterface
{
private
Connection
$db
;
private
NotificationManager
$notif
;
public
function
key
()
:
string
{
return
'meeting_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
$c
=
Container
::
getInstance
();
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
return
true
;
}
public
function
run
()
:
void
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$now
=
time
();
$oneHour
=
date
(
'Y-m-d H:i:s'
,
$now
+
3600
);
$oneHourAgo
=
date
(
'Y-m-d H:i:s'
,
$now
+
3000
);
$today
=
date
(
'Y-m-d'
);
$tomorrow
=
date
(
'Y-m-d'
,
strtotime
(
'+1 day'
));
$currentTime
=
date
(
'H:i:s'
);
$meetings
=
$this
->
db
->
fetchAll
(
"SELECT m.* FROM meetings m
WHERE m.status = 'scheduled'
AND CONCAT(m.meeting_date, ' ', m.start_time) BETWEEN ? AND ?"
,
[
$oneHourAgo
,
$oneHour
]
// 1-day reminders: meetings tomorrow
$tomorrowMeetings
=
$db
->
fetchAll
(
"SELECT m.*, u.full_name_en as creator_name FROM meetings m
JOIN users u ON u.id = m.created_by_id
WHERE m.meeting_date = ? AND m.status = 'scheduled'"
,
[
$tomorrow
]
);
foreach
(
$meetings
as
$m
)
{
$invitees
=
$this
->
db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]);
foreach
(
$tomorrowMeetings
as
$m
)
{
$invitees
=
$db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]
);
foreach
(
$invitees
as
$inv
)
{
$this
->
notif
->
createImportant
(
$inv
[
'user_id'
],
'⏰ Meeting in 1 Hour'
,
"Meeting:
\"
{
$m
[
'title'
]
}
\"
starts in about 1 hour."
,
"/meetings/
{
$m
[
'id'
]
}
"
,
'meeting'
,
$m
[
'id'
]);
$alreadySent
=
$db
->
fetchOne
(
"SELECT id FROM notifications WHERE user_id = ? AND link_entity_type = 'meeting'
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'
));
$tomorrowMeetings
=
$this
->
db
->
fetchAll
(
"SELECT * FROM meetings WHERE meeting_date = ? AND status = 'scheduled'"
,
[
$tomorrow
]
// 1-hour reminders: meetings today within the next hour
$oneHourFromNow
=
date
(
'H:i:s'
,
strtotime
(
'+1 hour'
));
$soonMeetings
=
$db
->
fetchAll
(
"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
)
{
$invitees
=
$this
->
db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]);
foreach
(
$soonMeetings
as
$m
)
{
$invitees
=
$db
->
fetchAll
(
"SELECT user_id FROM meeting_invitees WHERE meeting_id = ?"
,
[
$m
[
'id'
]]
);
foreach
(
$invitees
as
$inv
)
{
$this
->
notif
->
createImportant
(
$inv
[
'user_id'
],
'Meeting Tomorrow'
,
"Meeting:
\"
{
$m
[
'title'
]
}
\"
is scheduled for tomorrow at
{
$m
[
'start_time'
]
}
."
,
"/meetings/
{
$m
[
'id'
]
}
"
,
'meeting'
,
$m
[
'id'
]);
$alreadySent
=
$db
->
fetchOne
(
"SELECT id FROM notifications WHERE user_id = ? AND link_entity_type = 'meeting'
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
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
(
'/meetings/{id}'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/meetings'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/meetings/{id}'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/meetings/{id}/notes'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'addNotes'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/meetings/{id}'
,
\Modules\Meetings\Controllers\MeetingController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/meetings'
,
[
MeetingController
::
class
,
'index'
]);
$router
->
get
(
'/api/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'show'
]);
$router
->
post
(
'/api/meetings'
,
[
MeetingController
::
class
,
'create'
]);
$router
->
put
(
'/api/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'update'
]);
$router
->
post
(
'/api/meetings/{meetingId}/notes'
,
[
MeetingController
::
class
,
'addNotes'
]);
$router
->
delete
(
'/api/meetings/{meetingId}'
,
[
MeetingController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/Offboarding/routes.php
View file @
7d22a012
<?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
->
get
(
'/offboarding/settlement/{userId}'
,
\Modules\Offboarding\Controllers\OffboardingController
::
class
,
'calculateFinalSettlement'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
post
(
'/api/offboarding/initiate'
,
[
OffboardingController
::
class
,
'initiate'
]);
$router
->
get
(
'/api/offboarding/settlement/{userId}'
,
[
OffboardingController
::
class
,
'calculateFinalSettlement'
]);
};
\ No newline at end of file
modules/PIPs/Jobs/PIPCheckinReminderJob.php
View file @
7d22a012
...
...
@@ -10,32 +10,78 @@ use Engine\Notifications\NotificationManager;
final
class
PIPCheckinReminderJob
implements
JobInterface
{
private
Connection
$db
;
private
NotificationManager
$notif
;
public
function
key
()
:
string
{
return
'pip_checkin_reminders'
;
}
public
function
__construct
()
public
function
shouldRun
()
:
bool
{
$c
=
Container
::
getInstance
();
$this
->
db
=
$c
->
resolve
(
Connection
::
class
);
$this
->
notif
=
$c
->
resolve
(
NotificationManager
::
class
);
return
true
;
}
public
function
run
()
:
void
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$today
=
date
(
'Y-m-d'
);
$checkins
=
$this
->
db
->
fetchAll
(
"SELECT pc.*, p.contractor_id, p.created_by_id FROM pip_checkins pc
$tomorrow
=
date
(
'Y-m-d'
,
strtotime
(
'+1 day'
));
$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 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
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 < ?
AND pc.logged_at IS NULL
AND p.status = 'active'
AND p.deleted_at IS NULL"
,
[
$today
]
);
foreach
(
$checkins
as
$ci
)
{
$this
->
notif
->
createImportant
(
$ci
[
'contractor_id'
],
'PIP Check-in Today'
,
'You have a PIP check-in scheduled for today.'
,
"/pips/
{
$ci
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$ci
[
'pip_id'
]);
$this
->
notif
->
createImportant
(
$ci
[
'created_by_id'
],
'PIP Check-in Due'
,
"PIP check-in for contractor ID
{
$ci
[
'contractor_id'
]
}
is scheduled today."
,
"/pips/
{
$ci
[
'pip_id'
]
}
"
,
'pip'
,
(
int
)
$ci
[
'pip_id'
]);
foreach
(
$missedCheckins
as
$missed
)
{
$daysSince
=
(
int
)((
strtotime
(
$today
)
-
strtotime
(
$missed
[
'scheduled_date'
]))
/
86400
);
if
(
$daysSince
===
2
)
{
$admins
=
$db
->
fetchAll
(
"SELECT id FROM users WHERE role IN ('super_admin','admin') AND is_active = 1"
);
foreach
(
$admins
as
$a
)
{
$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
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
(
'/pips/{id}'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'show'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/pips'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/pips/{id}/acknowledge'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'acknowledge'
,
[
'auth'
]);
$router
->
post
(
'/pips/{id}/checkins/{checkinId}'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'logCheckin'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/pips/{id}/decide'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'decide'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/pips/{id}'
,
\Modules\PIPs\Controllers\PIPController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/pips'
,
[
PIPController
::
class
,
'index'
]);
$router
->
get
(
'/api/pips/{pipId}'
,
[
PIPController
::
class
,
'show'
]);
$router
->
post
(
'/api/pips'
,
[
PIPController
::
class
,
'create'
]);
$router
->
post
(
'/api/pips/{pipId}/acknowledge'
,
[
PIPController
::
class
,
'acknowledge'
]);
$router
->
post
(
'/api/pips/{pipId}/checkin'
,
[
PIPController
::
class
,
'logCheckin'
]);
$router
->
put
(
'/api/pips/{pipId}/result'
,
[
PIPController
::
class
,
'decide'
]);
$router
->
delete
(
'/api/pips/{pipId}'
,
[
PIPController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/RecurringCards/Jobs/CreateRecurringCardsJob.php
View file @
7d22a012
...
...
@@ -6,84 +6,161 @@ namespace Modules\RecurringCards\Jobs;
use
Engine\Scheduler\JobInterface
;
use
Engine\Core\Container
;
use
Engine\Database\Connection
;
use
Engine\Notifications\NotificationManager
;
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
{
$db
=
Container
::
getInstance
()
->
resolve
(
Connection
::
class
);
$notif
=
Container
::
getInstance
()
->
resolve
(
NotificationManager
::
class
);
$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
]
);
foreach
(
$definitions
as
$def
)
{
$template
=
json_decode
(
$def
[
'card_template_json'
],
true
);
if
(
!
$template
)
continue
;
$db
->
beginTransaction
();
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'
]]);
if
(
!
$board
)
continue
;
$db
->
query
(
"UPDATE boards SET card_sequence = card_sequence + 1 WHERE id = ?"
,
[(
int
)
$def
[
'board_id'
]]);
$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
(
"SELECT id FROM board_columns WHERE board_id = ? AND slug = 'backlog'"
,
[
$board
[
'id'
]]
);
if
(
!
$backlogCol
)
continue
;
$backlogCol
=
$
db
->
fetchOne
(
"SELECT id FROM board_columns WHERE board_id = ? AND slug = 'backlog'"
,
[(
int
)
$def
[
'board_id'
]]
)
;
$this
->
db
->
transaction
(
function
()
use
(
$def
,
$template
,
$board
,
$backlogCol
)
{
$this
->
db
->
query
(
"UPDATE boards SET card_sequence = card_sequence + 1 WHERE id = ?"
,
[
$board
[
'id'
]]
);
$updated
=
$this
->
db
->
fetchOne
(
"SELECT card_sequence FROM boards WHERE id = ?"
,
[
$board
[
'id'
]])
;
$cardKey
=
$board
[
'board_key'
]
.
'-'
.
$updated
[
'card_sequence'
];
if
(
!
$backlogCol
)
{
$db
->
rollBack
(
);
continue
;
}
$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'
,
[
'board_id'
=>
$board
[
'
id'
],
$cardId
=
$db
->
insert
(
'cards'
,
[
'board_id'
=>
(
int
)
$def
[
'board_
id'
],
'column_id'
=>
$backlogCol
[
'id'
],
'card_number'
=>
$updated
[
'card_sequence'
],
'card_number'
=>
$updated
Board
[
'card_sequence'
],
'card_key'
=>
$cardKey
,
'title'
=>
$title
,
'description'
=>
$template
[
'description'
]
??
null
,
'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
,
'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
)
:
[];
foreach
(
$assignees
as
$
ui
d
)
{
$
this
->
db
->
insert
(
'card_assignments'
,
[
foreach
(
$assignees
as
$
assigneeI
d
)
{
$db
->
insert
(
'card_assignments'
,
[
'card_id'
=>
$cardId
,
'user_id'
=>
(
int
)
$
ui
d
,
'assigned_by_id'
=>
$def
[
'created_by_id'
],
'user_id'
=>
(
int
)
$
assigneeI
d
,
'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
);
$
this
->
db
->
update
(
'recurring_card_definitions'
,
[
'last_created_at'
=>
date
(
'Y-m-d H:i:s'
)
,
$db
->
update
(
'recurring_card_definitions'
,
[
'last_created_at'
=>
$now
,
'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
{
$now
=
time
();
return
match
(
$def
[
'frequency'
])
{
'daily'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 day'
,
$now
)),
'weekly'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 week'
,
$now
)),
'biweekly'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+2 weeks'
,
$now
)),
'monthly'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 month'
,
$now
)),
'custom'
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+'
.
(
int
)
$def
[
'frequency_days'
]
.
' days'
,
$now
)),
default
=>
date
(
'Y-m-d 04:00:00'
,
strtotime
(
'+1 week'
,
$now
)),
};
switch
(
$def
[
'frequency'
])
{
case
'daily'
:
return
date
(
'Y-m-d H:i:s'
,
strtotime
(
'+1 day'
,
$now
));
case
'weekly'
:
$dayNames
=
[
'Sunday'
,
'Monday'
,
'Tuesday'
,
'Wednesday'
,
'Thursday'
,
'Friday'
,
'Saturday'
];
$targetDay
=
$dayNames
[
$def
[
'day_of_week'
]
??
1
]
??
'Monday'
;
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
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
->
post
(
'/recurring-cards'
,
\Modules\RecurringCards\Controllers\RecurringCardController
::
class
,
'create'
,
[
'auth'
,
'blocking'
]);
$router
->
put
(
'/recurring-cards/{id}'
,
\Modules\RecurringCards\Controllers\RecurringCardController
::
class
,
'update'
,
[
'auth'
,
'blocking'
]);
$router
->
delete
(
'/recurring-cards/{id}'
,
\Modules\RecurringCards\Controllers\RecurringCardController
::
class
,
'delete'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/recurring-cards'
,
[
RecurringCardController
::
class
,
'index'
]);
$router
->
post
(
'/api/recurring-cards'
,
[
RecurringCardController
::
class
,
'create'
]);
$router
->
put
(
'/api/recurring-cards/{defId}'
,
[
RecurringCardController
::
class
,
'update'
]);
$router
->
delete
(
'/api/recurring-cards/{defId}'
,
[
RecurringCardController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/Salary/routes.php
View file @
7d22a012
<?php
declare
(
strict_types
=
1
);
// Salary routes are served via the Dashboard HUD and API endpoints
// Additional salary-specific routes will be added in Phase 2+
\ No newline at end of file
use
Engine\Core\Router
;
use
Modules\Dashboard\Controllers\DashboardController
;
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
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
->
post
(
'/saved-filters'
,
\Modules\SavedFilters\Controllers\SavedFilterController
::
class
,
'create'
,
[
'auth'
]);
$router
->
delete
(
'/saved-filters/{id}'
,
\Modules\SavedFilters\Controllers\SavedFilterController
::
class
,
'delete'
,
[
'auth'
]);
\ No newline at end of file
$router
->
get
(
'/api/saved-filters'
,
[
SavedFilterController
::
class
,
'index'
]);
$router
->
post
(
'/api/saved-filters'
,
[
SavedFilterController
::
class
,
'create'
]);
$router
->
delete
(
'/api/saved-filters/{filterId}'
,
[
SavedFilterController
::
class
,
'delete'
]);
};
\ No newline at end of file
modules/Schedules/routes.php
View file @
7d22a012
<?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
(
'/schedules/requests'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'requests'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/schedules/requests'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'submitRequest'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/schedules/requests/{id}/review'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'reviewRequest'
,
[
'auth'
,
'blocking'
]);
$router
->
post
(
'/schedules/users/{userId}/edit'
,
\Modules\Schedules\Controllers\ScheduleController
::
class
,
'directEdit'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
$router
->
get
(
'/api/users/{userId}/schedule'
,
[
ScheduleController
::
class
,
'currentSchedule'
]);
$router
->
get
(
'/api/schedule-requests'
,
[
ScheduleController
::
class
,
'requests'
]);
$router
->
post
(
'/api/schedule-requests'
,
[
ScheduleController
::
class
,
'submitRequest'
]);
$router
->
post
(
'/api/schedule-requests/{requestId}/review'
,
[
ScheduleController
::
class
,
'reviewRequest'
]);
$router
->
put
(
'/api/users/{userId}/schedule'
,
[
ScheduleController
::
class
,
'directEdit'
]);
};
\ No newline at end of file
modules/TeamAvailability/routes.php
View file @
7d22a012
<?php
use
Engine\Core\Container
;
use
Engine\Core\Router
;
use
Modules\TeamAvailability\Controllers\TeamAvailabilityController
;
$router
=
Container
::
getInstance
()
->
resolve
(
\Engine\Core\Router
::
class
);
$router
->
get
(
'/team-availability'
,
\Modules\TeamAvailability\Controllers\TeamAvailabilityController
::
class
,
'index'
,
[
'auth'
,
'blocking'
]);
\ No newline at end of file
return
function
(
Router
$router
)
{
$router
->
get
(
'/team-availability'
,
[
TeamAvailabilityController
::
class
,
'index'
]);
$router
->
get
(
'/api/team-availability'
,
[
TeamAvailabilityController
::
class
,
'index'
]);
};
\ No newline at end of file
modules/Unavailability/routes.php
View file @
7d22a012
<?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
->
group
([
'prefix'
=>
'/api/unavailability'
,
'middleware'
=>
[
Middleware\AuthenticationMiddleware
::
class
,
Middleware\CSRFMiddleware
::
class
]
],
function
(
$router
)
{
$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
$router
->
get
(
'/api/unavailability'
,
[
UnavailabilityController
::
class
,
'index'
]);
$router
->
post
(
'/api/unavailability'
,
[
UnavailabilityController
::
class
,
'create'
]);
$router
->
put
(
'/api/unavailability/{id}'
,
[
UnavailabilityController
::
class
,
'update'
]);
$router
->
delete
(
'/api/unavailability/{id}'
,
[
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