Compare commits

...

242 Commits

Author SHA1 Message Date
Admin f0bed712e6 update vite build 2026-05-21 09:54:39 +07:00
Admin f1de8cb627 update build UI AU 2026-05-21 09:15:09 +07:00
Admin b50dc96ce2 update build UI AU 2026-05-20 16:49:59 +07:00
Admin 744c206743 update build UI 2026-05-20 15:30:39 +07:00
Joseph a23f2155dc update auto detect face 2026-05-20 15:23:31 +07:00
zelda 25918fcb62 Merge pull request 'update UI tablet and mobile' (#167) from zelda.fix-ui-responsive-client into master
Reviewed-on: #167
2026-05-20 17:13:36 +10:00
Admin 607882343a update UI tablet and mobile 2026-05-20 14:11:05 +07:00
Admin 8cd93c578e update(ui) add responsive for tracking 2026-05-18 10:10:58 +07:00
Admin 755297e5b0 update(ui) add responsive for tracking 2026-05-18 10:06:59 +07:00
zelda a3ec46db0a Merge pull request 'update(ui) add responsive for tracking' (#165) from zelda.checkin-for-au into master
Reviewed-on: #165
2026-05-18 12:51:15 +10:00
Admin ab7ccbe194 update(ui) add responsive for tracking 2026-05-18 09:49:56 +07:00
Joseph Le 4eb3dda0e1 update readme for trackin AU 2026-05-15 16:15:56 +07:00
Joseph Le 1cd6b3a108 clear venv folder 2026-05-15 16:14:20 +07:00
zelda 935e88bb72 Merge pull request 'update(server): connect api to erp and add func remove user' (#164) from zelda.checkin-for-au into master
Reviewed-on: #164
2026-05-15 19:06:48 +10:00
Admin 2675a60ffc update(server): connect api to erp and add func remove user 2026-05-15 16:06:33 +07:00
zelda 95e4e833b5 Merge pull request 'update(server): connect api to erp and add func remove user' (#163) from zelda.checkin-for-au into master
Reviewed-on: #163
2026-05-15 18:45:34 +10:00
Admin 8ad0ba9c25 update(server): connect api to erp and add func remove user 2026-05-15 15:43:54 +07:00
zelda 574e92bd23 Merge pull request 'zelda.checkin-for-au' (#162) from zelda.checkin-for-au into master
Reviewed-on: #162
2026-05-15 18:04:49 +10:00
Admin 925823c525 update(server): connect api to erp and add func remove user 2026-05-15 15:03:50 +07:00
Admin a594ba5ba2 update(server): connect api to erp and add func remove user 2026-05-15 15:00:50 +07:00
zelda 683f85efd4 Merge pull request 'feat(au-checkin): upload checkin version for au' (#161) from zelda.checkin-for-au into master
Reviewed-on: #161
2026-05-15 14:07:10 +10:00
Admin 6a1e19ec7f feat(au-checkin): upload checkin version for au 2026-05-15 11:06:36 +07:00
Joseph Le 8e54ca0ceb edit 2026-05-14 14:54:23 +07:00
Your Name 0f2d5db8a3 feature(office-support): add equipment management and electricity bill attachment
Equipment management (new):
- Equipment CRUD with code, name, note, optional photo (max 10MB)
- Barcode scanner using @zxing/browser:
  - Defaults to back camera on mobile, autofocus + 2x zoom
  - Reads any orientation (rotates 12 angles per cycle for arbitrary tilt)
  - Manual "Capture & decode" fallback (18 angles)
  - Horizontal aiming line + haptic feedback on success
- Assign workflow with full history (user <-> warehouse, every state recorded)
- Statistics view: equipments grouped by current holder (Accordion + KPIs)
- Image lightbox: view original at any zoom, open-in-new-tab, pinch-zoom on mobile
- Client-side resize for >10MB uploads to fit PHP upload limit

Electricity bill:
- Attachment (PDF/image) upload, view, replace, remove

Routes, API endpoints, Mantine tab integration, and DB migrations included.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:36:42 +07:00
root 980676fe16 update leave day 2026-05-13 10:27:54 +07:00
zelda 9d536955e4 Merge pull request 'update(checkin tool web): update status when status from server and tool are wrong' (#160) from zelda.refactor-tracking-tool-web into master
Reviewed-on: #160
2026-05-13 13:26:41 +10:00
Admin a5fa0cbcda update(checkin tool web): update status when status from server and tool are wrong 2026-05-13 10:24:34 +07:00
zelda 0a4332729d Merge pull request 'update(tracking-tool-web): refactor to speed checkin' (#159) from zelda.refactor-tracking-tool-web into master
Reviewed-on: #159
2026-05-12 11:46:05 +10:00
Admin 2f1e26d3f6 update(tracking-tool-web): refactor to speed checkin 2026-05-12 08:43:12 +07:00
andrew.ng 3d1b43e200 Merge pull request 'that-bill' (#158) from that-bill into master
Reviewed-on: #158
2026-04-29 17:29:11 +10:00
nguyentrungthat 8d86fc2182 Update OfficeSupport.tsx 2026-04-29 14:28:56 +07:00
nguyentrungthat 54b8b9737b Merge branch 'master' into that-bill 2026-04-29 14:13:55 +07:00
andrew.ng 42c5791da8 Merge pull request 'Update' (#157) from that-bill into master
Reviewed-on: #157
2026-04-29 17:13:48 +10:00
nguyentrungthat 8d5199342d Update 2026-04-29 14:13:19 +07:00
andrew.ng beccf5796c Merge pull request 'Add electricity bills module (API, model, PDF)' (#156) from that-bill into master
Reviewed-on: #156
2026-04-29 16:49:41 +10:00
nguyentrungthat baa3216c69 Add electricity bills module (API, model, PDF)
Introduce an Electricity Bills feature: adds ElectricityBill model, controller with CRUD + PDF export, Blade PDF template, and a migration to create the electricity_bills table. Registers routes (with admin permission middleware) and persists generated PDFs to public storage. Adds ResultSuccess/ResultError helpers to base Controller and updates composer to include dompdf and DBAL dependencies. Also includes frontend updates (Admin API, DataTable, Navbar, new OfficeSupport page and styles, route and CRUD helper adjustments) to support the new functionality.
2026-04-29 13:44:34 +07:00
joseph de351f0877 Merge pull request 'fix(setting): update hr permission' (#155) from vi into master
Reviewed-on: #155
2026-04-18 11:20:57 +10:00
Hoang Vi 2c94e91668 fix(setting): update hr permission 2026-04-18 07:52:39 +07:00
joseph b22c9b9cc5 Merge pull request 'feat(setting): add setting saturday work' (#154) from vi into master
Reviewed-on: #154
2026-04-09 19:24:31 +10:00
Hoang Vi f494367683 feat(setting): add setting saturday work 2026-04-09 16:11:18 +07:00
joseph 61124cd9bb Merge pull request 'fix run cron job initialize leaveDays' (#153) from vi into master
Reviewed-on: #153
2026-01-06 20:16:40 +11:00
Nguyễn Hoàng Vĩ 78066f624e fix run cron job initialize leaveDays 2026-01-06 16:02:24 +07:00
Nguyễn Hoàng Vĩ d7b7a61511 Merge branch 'master' of https://gitea.nswteam.net/joseph/ManagementSystem into vi 2026-01-06 15:59:54 +07:00
Truong Vo 8f30081e55 fix ngày phép 2026-01-05 08:20:01 +07:00
Truong Vo a0bfb19a34 Bổ sung + thêm phép đặc biệt khi chuyển phép từ năm cũ 2026-01-02 08:18:29 +07:00
Truong Vo 7e369785d5 fix 2025-12-30 10:08:18 +07:00
Nguyễn Hoàng Vĩ 54046ac676 Merge branch 'master' of https://gitea.nswteam.net/joseph/ManagementSystem into vi 2025-12-23 14:58:56 +07:00
zelda 9f921750fc Merge pull request 'fix(noti): Fix notification' (#152) from zelda.update-checkin-api into master
Reviewed-on: #152
2025-12-23 11:43:55 +11:00
Admin bd0d4fa13b fix(noti): Fix notification 2025-12-23 07:43:27 +07:00
Nguyễn Hoàng Vĩ f5362e54ef Merge branch 'master' of https://gitea.nswteam.net/joseph/ManagementSystem into vi 2025-12-22 08:20:52 +07:00
zelda 17dc0c3374 Merge pull request 'update(fe): remove speak check in and check out' (#151) from zelda.update-checkin-api into master
Reviewed-on: #151
2025-12-19 11:39:13 +11:00
Admin 29dc787c65 update(fe): remove speak check in and check out 2025-12-19 07:37:59 +07:00
zelda 6ccf5e9e8a Merge pull request 'update(fe): speak name' (#150) from zelda.update-checkin-api into master
Reviewed-on: #150
2025-12-15 17:41:55 +11:00
Admin 5736166da3 update(fe): speak name 2025-12-15 13:41:19 +07:00
zelda 7be6a12edc Merge pull request 'update(fe): Move quantity variable to env' (#149) from zelda.update-checkin-api into master
Reviewed-on: #149
2025-12-12 14:39:41 +11:00
Admin 70436f737f update(fe): Move quantity variable to env 2025-12-12 10:38:39 +07:00
zelda 25162be83d Merge pull request 'update(ttw): update auto sync user' (#148) from zelda.update-checkin-api into master
Reviewed-on: #148
2025-12-12 14:17:33 +11:00
Admin 06027e8f23 update(ttw): update auto sync user 2025-12-12 10:16:23 +07:00
zelda 8c7a24d31c Merge pull request 'update(ttw): run send image in backgound' (#147) from zelda.update-checkin-api into master
Reviewed-on: #147
2025-12-11 18:56:14 +11:00
Admin 1f08e2b503 update(ttw): run send image in backgound 2025-12-11 14:55:09 +07:00
zelda 9d77dfc5d6 Merge pull request 'feat(tkw): update ui' (#146) from zelda.update-static-file-tracking-tool-web into master
Reviewed-on: #146
2025-12-11 14:34:48 +11:00
Admin e7aa80f14b feat(tkw): update ui 2025-12-11 10:34:26 +07:00
zelda 24bd605b7c Merge pull request 'feat(tkw): update file main models' (#145) from zelda.update-static-file-tracking-tool-web into master
Reviewed-on: #145
2025-12-11 14:04:53 +11:00
Admin 2a5f7b0b25 feat(tkw): update file main models 2025-12-11 10:04:29 +07:00
zelda 9183540976 Merge pull request 'feat(tkw): update file main code' (#144) from zelda.update-static-file-tracking-tool-web into master
Reviewed-on: #144
2025-12-11 13:35:33 +11:00
Admin cdce726ff1 feat(tkw): update file main code 2025-12-11 09:35:01 +07:00
zelda 790cab255d Merge pull request 'feat(tkw): Update static file' (#143) from zelda.update-static-file-tracking-tool-web into master
Reviewed-on: #143
2025-12-11 13:31:37 +11:00
Admin 647fc455d5 feat(tkw): Update static file 2025-12-11 09:30:39 +07:00
zelda b5f5fa1748 Merge pull request 'update client for server' (#142) from zelda.push-tracking-tool-web into master
Reviewed-on: #142
2025-12-11 13:11:33 +11:00
Admin 5a564e3703 update client for server 2025-12-11 09:10:42 +07:00
joseph 0af2357bc2 Merge pull request 'feat(tkw): Tracking tool web' (#141) from zelda.push-tracking-tool-web into master
Reviewed-on: #141
2025-12-10 20:29:24 +11:00
Admin a623659b52 feat(tkw): Tracking tool web 2025-12-10 16:23:45 +07:00
joseph b35d4d7ca5 Merge pull request 'dev' (#140) from dev into master
Reviewed-on: #140
2025-08-05 19:13:38 +10:00
joseph 522034ab76 Merge pull request 'update timekeeping, leave UI' (#139) from vi into dev
Reviewed-on: #139
2025-08-05 19:13:12 +10:00
dbdbd9 487f4227d8 Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi 2025-08-05 16:10:32 +07:00
dbdbd9 56d2889297 update timekeeping, leave UI 2025-08-05 16:08:48 +07:00
joseph 7c36add579 Merge pull request 'dev' (#138) from dev into master
Reviewed-on: #138
2025-08-05 18:27:42 +10:00
joseph 6c61819f2e Merge pull request 'update handle for separated user' (#137) from vi into dev
Reviewed-on: #137
2025-08-05 18:26:09 +10:00
dbdbd9 f649b2a6ac Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi 2025-08-05 15:19:15 +07:00
dbdbd9 d335803cf8 update handle for separated user 2025-08-05 15:14:50 +07:00
joseph da0bb9ad0d Merge pull request 'dev' (#136) from dev into master
Reviewed-on: #136
2025-07-31 12:47:25 +10:00
joseph d3d9acb1ce Merge pull request 'fix calculate work time' (#135) from vi.ticket-email into dev
Reviewed-on: #135
2025-07-31 12:32:10 +10:00
dbdbd9 a4751e9d38 Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.ticket-email 2025-07-31 09:15:59 +07:00
dbdbd9 b52537cfe4 fix calculate work time 2025-07-31 09:14:33 +07:00
joseph efaf906f0a Merge pull request 'dev' (#134) from dev into master
Reviewed-on: #134
2025-07-02 12:54:43 +10:00
joseph 3e865e5575 Merge pull request 'update permission display for leave management' (#133) from vi.ticket-email into dev
Reviewed-on: #133
2025-07-02 11:23:51 +10:00
dbdbd9 4f6f3dd803 Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.ticket-email 2025-07-02 07:46:58 +07:00
dbdbd9 ee281986b8 update permission display for leave management 2025-07-01 16:36:21 +07:00
joseph 6e1e452bf1 Merge pull request 'Deploy to prod' (#132) from dev into master
Reviewed-on: #132
2025-07-01 17:54:53 +10:00
joseph 6528b7bc59 Merge pull request 'add update note status' (#131) from vi.ticket-email into dev
Reviewed-on: #131
2025-07-01 17:26:34 +10:00
dbdbd9 fd1be27414 Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.ticket-email 2025-07-01 13:41:05 +07:00
dbdbd9 d064b242e6 add update note status 2025-07-01 13:40:48 +07:00
joseph 8f6977be72 Merge pull request 'dev' (#130) from dev into master
Reviewed-on: #130
2025-06-30 18:37:38 +10:00
joseph af67644416 Merge pull request 'adjust UI, add send mail when delete note/update refuse ticket' (#129) from vi.ticket-email into dev
Reviewed-on: #129
2025-06-30 18:14:26 +10:00
dbdbd9 705e8f9216 adjust UI, add send mail when delete note/update refuse ticket 2025-06-30 14:58:06 +07:00
joseph db4feedde3 Merge pull request 'update handle calculate onleave ticket in next months' (#128) from vi.ticket-email into dev
Reviewed-on: #128
2025-06-30 16:11:00 +10:00
dbdbd9 27b59ae939 update msg 2025-06-30 11:04:44 +07:00
dbdbd9 034b9eee2c update handle calculate onleave ticket in next months 2025-06-30 10:57:02 +07:00
joseph 543b1af58c Merge pull request 'update UI leave management, add func update old data' (#127) from vi.ticket-email into dev
Reviewed-on: #127
2025-06-27 12:51:16 +10:00
dbdbd9 d09fa6d432 comment log 2025-06-27 09:48:15 +07:00
dbdbd9 40f80579dc update UI leave management, add func update old data 2025-06-27 09:45:21 +07:00
joseph b1e7aaa1ea Merge pull request 'fix calculate onleave day' (#126) from vi.ticket-email into dev
Reviewed-on: #126
2025-06-25 13:37:41 +10:00
hoangvi.ng a883662f0b fix calculate onleave day 2025-06-25 10:34:02 +07:00
joseph a11f67f252 Merge pull request 'refactor create, handle ticket' (#125) from vi.ticket-email into dev
Reviewed-on: #125
2025-06-24 18:45:44 +10:00
dbdbd9 fb6c58f1c0 handle onleave new user, adjust background jobs 2025-06-24 15:39:11 +07:00
dbdbd9 c2c9322e7d update delete note, add update ticket 2025-06-21 11:11:02 +07:00
dbdbd9 3263bce784 fix refactor function check leave day, update handle ticket send mail 2025-06-20 14:29:47 +07:00
dbdbd9 500d6ec34d fix warnning on_leave_note_pay message 2025-06-18 15:23:55 +07:00
dbdbd9 1b35f8481b refactor create, handle ticket 2025-06-17 15:44:44 +07:00
vincent.vo 172af363de Merge pull request 'truong-leave-day' (#124) from truong-leave-day into dev
Reviewed-on: #124
2025-06-16 16:34:55 +10:00
Truong Vo cd9c01cdee fix 2025-06-16 13:34:41 +07:00
Truong Vo 2c8bb92775 Bổ sung check ngày phép tối đa 1 tháng mặc dù còn nhiều phép 2025-06-16 13:31:15 +07:00
Truong Vo 5b65415d76 Bổ sung check ticket đang được duyệt k thể tạo 2025-06-16 09:41:54 +07:00
Truong Vo 655d9cd4c6 Hiệu chỉnh ngày phép 2025-06-16 09:29:45 +07:00
Truong Vo e05e92b96b fix lỗi có phép được duyệt trong thời gian được chọn khi tạo ticket 2025-06-16 08:15:14 +07:00
joseph 1328320713 Merge pull request 'add confirm/refuse in ticket mail' (#123) from vi.ticket-email into dev
Reviewed-on: #123
2025-06-12 12:54:55 +10:00
dbdbd9 7217a83e33 add confirm/refuse in ticket mail 2025-06-11 16:44:11 +07:00
dbdbd9 8474bd97a1 Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.document 2025-06-11 14:52:24 +07:00
vincent.vo f8c2af2ec8 Merge pull request 'Hiệu chỉnh hàm tính toán ngày phép' (#122) from truong-leave-day into dev
Reviewed-on: #122
2025-06-11 17:49:54 +10:00
Truong Vo 5f8b03f558 Hiệu chỉnh hàm tính toán ngày phép 2025-06-11 14:49:16 +07:00
dbdbd9 fa47d32b07 Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.document 2025-06-10 10:05:47 +07:00
joseph 160596a7a3 Merge pull request 'truong-leave-day' (#116) from truong-leave-day into dev
Reviewed-on: #116
2025-06-10 12:51:31 +10:00
Truong Vo 177296a324 fix build 2025-05-07 16:49:47 +07:00
Truong Vo d5936087ff fix build 2025-05-07 16:49:15 +07:00
Truong Vo 951c3be613 fix build 2025-05-07 16:48:14 +07:00
Truong Vo b5a7740d9f Bổ sung form xác nhận trước khi xóa note 2025-05-07 15:13:37 +07:00
Truong Vo 2011797dd6 Bổ sung xóa ngày phép 2025-05-07 13:28:14 +07:00
Truong Vo 7dc31bf75b Bổ sung xóa ngày phép 2025-05-07 11:33:52 +07:00
Truong Vo ca766fc293 Bổ sung check trùng ngày phép đã tạo 2025-04-26 10:43:18 +07:00
Truong Vo 32b197969c Bổ sung check trùng ngày phép đã tạo 2025-04-26 09:49:52 +07:00
Truong Vo 462d896e57 Hiệu chỉnh add ticket 2025-04-25 17:03:24 +07:00
Truong Vo 44fa6b55f7 Hiệu chỉnh add ticket 2025-04-25 10:29:35 +07:00
Truong Vo ddcb78ef98 Hiệu chỉnh chức năng ngày phép 2025-04-24 16:37:53 +07:00
Truong Vo d61fd879b7 Hiệu chỉnh chức năng ngày phép 2025-04-24 15:23:25 +07:00
Truong Vo d3c1d9bf60 Hiệu chỉnh chức năng ngày phép 2025-04-24 15:19:12 +07:00
Truong Vo 65c4dbcf88 Hiệu chỉnh chức năng ngày phép 2025-04-24 09:55:03 +07:00
Truong Vo af5721682b [Ngày Phép] Bổ sung chức năng cập nhật ngày nghỉ cho ngày nghỉ đặt biệt (3 ngày nghỉ đám cưới) 2025-04-15 14:07:20 +07:00
Truong Vo 308f5a3a2e [Ngày Phép] Thực hiện Job cập nhật ngày phép bảng mỗi tháng + 1 ngày phép cho mỗi user, thời gian 00:00 ngày 1 hàng tháng 2025-04-15 10:34:54 +07:00
Truong Vo 05cb3f722a Merge branch 'dev' into truong-leave-day 2025-04-04 10:39:11 +07:00
Truong Vo 1f9ab744ae Merge branch 'master' into truong-leave-day 2025-04-04 10:39:00 +07:00
Joseph f807a635bb update logic upload file 2025-03-27 17:18:53 +07:00
Joseph d2c0e0b95d update logic upload file 2025-03-27 17:08:49 +07:00
Joseph 4da48df8d7 update logic upload file 2025-03-27 17:05:32 +07:00
dbdbd9 5cd7071459 Merge branch 'master' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.document 2025-03-27 07:50:25 +07:00
Joseph 6e2a8c2578 update type file profile upload 2025-03-26 09:29:14 +07:00
Joseph c6a9fc28a3 update url 2025-03-26 08:56:06 +07:00
joseph db5c9b09a8 Merge pull request 'master' (#121) from master into dev
Reviewed-on: #121
2025-03-14 19:36:10 +11:00
Joseph 9a9a94e5d4 update download link 2025-03-14 15:32:13 +07:00
Joseph d803d69b72 update download link 2025-03-14 15:29:58 +07:00
Joseph d1f889c996 update search datatable 2025-03-14 15:16:04 +07:00
Joseph e174146678 test 2025-03-14 14:40:37 +07:00
joseph cd2570bc07 Merge pull request 'dev' (#120) from dev into master
Reviewed-on: #120
2025-03-14 18:31:12 +11:00
joseph 230beb4ed7 Merge pull request 'fix search, show file' (#119) from vi.document into dev
Reviewed-on: #119
2025-03-14 18:30:52 +11:00
dbdbd9 60925a83af fix search, show file 2025-03-14 14:26:57 +07:00
joseph b6476feae5 Merge pull request 'dev' (#118) from dev into master
Reviewed-on: #118
2025-03-14 14:52:35 +11:00
joseph 8b9e18bdcb Merge pull request 'adjust Create, Update logic, add permission' (#117) from vi.document into dev
Reviewed-on: #117
2025-03-14 14:47:05 +11:00
dbdbd9 4896461838 Merge branch 'dev' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.document 2025-03-14 10:07:16 +07:00
dbdbd9 7ef2897469 Merge branch 'master' of https://gitea.nswteam.net/joseph/ManagementSystem into vi.document 2025-03-14 10:07:11 +07:00
dbdbd9 88870e075c adjust Create, Update logic, add permission 2025-03-14 10:06:49 +07:00
joseph 008c335b79 Merge pull request 'fix library type' (#115) from vi.document into dev
Reviewed-on: #115
2025-03-13 20:45:38 +11:00
Truong Vo 650cfe1b13 [Ngày Phép] Cập nhật loại phép nộp: WFH, Nghỉ phép năm, Nghỉ không lương 2025-03-13 15:41:43 +07:00
dbdbd9 20745381db fix lib type 2025-03-13 15:09:22 +07:00
Truong Vo 8ce0d957b1 [Ngày Phép] Cập nhật lại tên cột cho bảng ngày nghỉ phép năm 2025-03-13 14:54:37 +07:00
joseph a5ece4d65d Merge pull request 'dev' (#114) from dev into master
Reviewed-on: #114
2025-03-13 18:53:52 +11:00
joseph d8cbb7e01b Merge pull request 'BE, FE Document' (#113) from vi.document into dev
Reviewed-on: #113
2025-03-13 18:53:32 +11:00
Truong Vo 86f2bb12fc [Ngày Phép] Cập nhật lại tên cột cho bảng ngày nghỉ phép năm 2025-03-13 14:50:57 +07:00
dbdbd9 78bdca4e23 update permission 2025-03-13 14:48:26 +07:00
dbdbd9 e026b70e66 fix show file document detail, update document 2025-03-13 14:14:43 +07:00
dbdbd9 0c5f56d30c add BE, FE Document 2025-03-12 16:48:05 +07:00
Joseph 2561d39b4a update ignore project 2025-02-21 15:52:03 +07:00
joseph 12a1447931 Merge pull request 'dev' (#112) from dev into master
Reviewed-on: #112
2025-02-21 19:23:55 +11:00
joseph 35ecaa802d Merge pull request 'truong-frame-drawio' (#111) from truong-frame-drawio into dev
Reviewed-on: #111
2025-02-21 19:23:35 +11:00
Truong Vo 1018172b58 Merge branch 'master' into dev 2025-02-21 11:12:15 +07:00
Truong Vo 0ebe4c772e Bổ sung biểu đồ drawio cho trang allocation 2025-02-21 11:11:37 +07:00
Truong Vo 281c8ad98a Bổ sung biểu đồ drawio cho trang allocation 2025-02-21 10:51:27 +07:00
joseph a2e36501bb Merge pull request 'Update StaffEvaluation.tsx' (#110) from that-fe into master
Reviewed-on: #110
2025-02-17 20:40:24 +11:00
nguentrungthat 04ee5e4081 Merge branch 'master' into that-fe 2025-02-17 16:40:14 +07:00
nguentrungthat 15880f8bd1 Update StaffEvaluation.tsx 2025-02-17 16:39:35 +07:00
joseph 6c5b2d8412 Merge pull request 'Update view detail Working review' (#109) from that-fe into master
Reviewed-on: #109
2025-02-17 20:30:32 +11:00
nguentrungthat f5e4d7b405 Update view detail Working review 2025-02-17 16:14:52 +07:00
joseph 5cfd78d2c0 Merge pull request 'Update check working' (#108) from that-fe into master
Reviewed-on: #108
2025-02-17 13:28:07 +11:00
nguentrungthat 2bda35d8d3 Merge branch 'master' into that-fe 2025-02-14 16:53:26 +07:00
nguentrungthat be6d7be79e Update check working 2025-02-14 16:51:46 +07:00
joseph f60be27969 Merge pull request 'Update select query' (#107) from that-fe into master
Reviewed-on: #107
2025-02-14 19:57:17 +11:00
nguentrungthat 73edecf341 Update select query 2025-02-14 10:49:12 +07:00
nguentrungthat 13a477e883 Update TrackingController.php 2025-02-14 10:44:42 +07:00
joseph 471cc0eb2a Merge pull request 'Update' (#106) from that-fe into master
Reviewed-on: #106
2025-02-14 14:35:08 +11:00
nguentrungthat 1887f2805f Merge branch 'master' into that-fe 2025-02-14 09:43:56 +07:00
nguentrungthat bb12c55ef6 Update TrackingController.php 2025-02-14 09:43:21 +07:00
nguentrungthat 49b681566d fix 2025-02-14 09:42:08 +07:00
joseph 9b472544b6 Merge pull request 'Update Working review' (#105) from that-fe into master
Reviewed-on: #105
2025-02-14 12:22:28 +11:00
nguentrungthat 5cdfaafe03 fic build 2025-02-14 08:03:05 +07:00
nguentrungthat 8107978a42 Merge branch 'master' into that-fe 2025-02-14 07:47:03 +07:00
nguentrungthat 846df81d44 Update Working review 2025-02-13 17:01:52 +07:00
joseph 5165ded25e Merge pull request 'Update' (#104) from that-fe into master
Reviewed-on: #104
2025-02-07 19:32:13 +11:00
nguentrungthat 7ce7251097 Merge branch 'master' of https://gitea.nswteam.net/joseph/ManagementSystem into that-fe 2025-02-07 15:31:40 +07:00
nguentrungthat f05334ea34 Update 2025-02-07 15:23:03 +07:00
joseph 4d94e26bb9 Merge pull request 'master' (#103) from master into dev
Reviewed-on: #103
2025-02-07 19:00:27 +11:00
joseph fc6587bcff Merge pull request 'Update Project Review, export' (#102) from that-fe into master
Reviewed-on: #102
2025-02-07 18:59:52 +11:00
nguentrungthat 174c8889ce Update Project Review, export 2025-02-07 14:42:31 +07:00
nguentrungthat 4fd18b988e Update 2025-02-06 13:16:07 +07:00
Joseph 9532845bc3 update 2025-02-03 16:20:00 +07:00
Joseph 38780c56a8 update 2025-01-21 15:47:13 +07:00
Joseph b908185658 update tracking 2025-01-21 13:35:04 +07:00
root 07d28e24cd add script detect face 2025-01-17 14:50:09 +07:00
Joseph d80e7c0957 update jwt expried 2025-01-16 14:30:04 +07:00
Joseph eb5c6e655f update select month in timekeeping page 2025-01-02 07:36:39 +07:00
Joseph af52952946 update export button 2024-12-06 09:27:06 +07:00
joseph 1443d01685 Merge pull request 'dev' (#101) from dev into master
Reviewed-on: #101
2024-12-06 13:10:35 +11:00
joseph a6b5d83535 Merge pull request 'Bổ sung nút report excel trang timekeeping , leave management' (#100) from truong-report-excel into dev
Reviewed-on: #100
2024-12-06 13:10:11 +11:00
Truong Vo 7fdf6f3e25 Bổ sung nút report excel trang timekeeping , leave management 2024-12-06 09:07:54 +07:00
joseph 824b2fd87c Merge pull request 'dev' (#99) from dev into master
Reviewed-on: #99
2024-12-06 11:36:59 +11:00
joseph cfba5175a6 Merge pull request 'truong-report-excel' (#98) from truong-report-excel into dev
Reviewed-on: #98
2024-12-06 11:36:26 +11:00
Truong Vo 93030b73c7 Bổ sung nút report excel trang timekeeping , leave management 2024-12-05 17:25:07 +07:00
Joseph fcaa2926fd update send mail warning 2024-11-11 16:26:47 +07:00
Joseph 0e875d7cf9 update send mail warning 2024-11-08 16:45:55 +07:00
Joseph 14e9e17273 update accoutant role 2024-11-04 07:44:41 +07:00
Joseph f96f445c7f update accoutant role 2024-11-04 07:39:36 +07:00
Joseph 1d9e428587 update $morning_condition_time 2024-10-24 07:28:41 +07:00
Joseph a830791648 update mail subject 2024-10-23 07:54:50 +07:00
Joseph 8d8985a6be update send mail warning checkin late 2024-10-21 14:23:03 +07:00
Joseph d53b786daf update root folder by enviroment 2024-10-16 16:09:56 +07:00
Joseph 79b3cbe916 update root folder by enviroment 2024-10-16 15:54:58 +07:00
Joseph 2eed0f006b Merge branch 'master' of https://gitea.nswteam.net/joseph/ManagementSystem 2024-10-16 15:38:05 +07:00
Joseph e226ad41d7 create page manage profiles, page add profile user 2024-10-16 15:35:58 +07:00
joseph cc0a62f746 Merge pull request 'BỔ SUNG CHECK THÊM điều kiện user ticket nguyên ngày' (#97) from Truong-CreateAutoTicketKPhep into master
Reviewed-on: #97
2024-10-16 13:47:36 +11:00
Truong Vo 4334f3e73d Bổ sung check điều kiện có user check in check out (ngày hợp lệ) 2024-10-16 09:44:29 +07:00
Truong Vo 1c16b4b164 BỔ SUNG CHECK THÊM điều kiện user ticket nguyên ngày 2024-10-16 09:03:52 +07:00
joseph 2c6a6a81dd Merge pull request 'BỔ SUNG CHECK THÊM điều kiện user ticket nguyên ngày' (#96) from Truong-CreateAutoTicketKPhep into master
Reviewed-on: #96
2024-10-16 11:57:27 +11:00
Truong Vo 34b6698696 BỔ SUNG CHECK THÊM điều kiện user ticket nguyên ngày 2024-10-16 07:54:38 +07:00
joseph 7c8267d6e3 Merge pull request 'Bỏ bắt buộc field Admin notes khi admin duyệt or từ chối ticket' (#95) from Truong-CreateAutoTicketKPhep into master
Reviewed-on: #95
2024-10-14 15:06:15 +11:00
Truong Vo ceb925f571 Bỏ bắt buộc field Admin notes khi admin duyệt or từ chối ticket 2024-10-14 11:04:44 +07:00
vincent.vo bf5dc3d8c1 Merge pull request 'Truong-CreateAutoTicketKPhep' (#94) from Truong-CreateAutoTicketKPhep into master
Reviewed-on: #94
2024-10-14 14:13:38 +11:00
Truong Vo 46d4ffa339 Tạo ticket tự động cho user khi user không check in, check out 2024-10-14 10:11:59 +07:00
Truong Vo d0c1b6894b Tạo ticket tự động cho user khi user không check in, check out 2024-10-14 10:09:04 +07:00
Truong Vo 7fa50478cc Tạo ticket tự động cho user khi user không check in, check out 2024-10-09 10:26:33 +07:00
joseph 0650211f67 Merge pull request 'master' (#93) from master into dev
Reviewed-on: #93
2024-09-25 11:03:07 +10:00
joseph f55ac7e65c Merge pull request 'Sprint-4/MS-36-FE-Technical' (#92) from Sprint-4/MS-36-FE-Technical into master
Reviewed-on: #92
2024-09-25 10:54:42 +10:00
vincent.vo 0b823a226c Merge pull request 'master' (#91) from master into dev
Reviewed-on: #91
2024-09-24 19:23:56 +10:00
vincent.vo 2c2d067e4c Merge pull request 'master' (#89) from master into dev
Reviewed-on: #89
2024-09-24 19:19:13 +10:00
joseph 84d6ee2630 Merge pull request 'master' (#86) from master into dev
Reviewed-on: #86
2024-09-24 14:21:11 +10:00
joseph 6aff6093a6 Merge pull request 'master' (#81) from master into dev
Reviewed-on: #81
2024-09-24 10:36:02 +10:00
273 changed files with 43201 additions and 2975 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -3,6 +3,7 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\Category;
@ -14,7 +15,7 @@ class CategoryController extends Controller
* @param Request $request The HTTP request object.
* @return \Illuminate\Http\JsonResponse The JSON response containing the list of master data.
*/
public function getListMaster(Request $request)
public static function getListMaster(Request $request)
{
$data = Category::where('c_type', '=', $request->type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
return AbstractController::ResultSuccess($data);
@ -24,4 +25,50 @@ class CategoryController extends Controller
$data = Category::where('c_type', '=', $type)->where('c_code', '=', $code)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->first();
return $data;
}
public static function getListMasterByType($type)
{
$data = Category::where('c_type', '=', $type)->where('c_active', '=', 1)->select('id', 'c_code', 'c_name', 'c_value', 'c_type')->get();
return $data;
}
public function workDays()
{
$saturday_work_schedules = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE')
->get();
return response()->json([
'data' => $saturday_work_schedules,
'status' => true
]);
}
public function updateWorkDays(Request $request)
{
$request->validate([
'c_code' => 'required|date_format:d-m-Y',
]);
$schedule = Category::where('c_type', 'SATURDAY_WORK_SCHEDULE')->first();
if (!$schedule) {
$schedule = Category::create([
'c_type' => 'SATURDAY_WORK_SCHEDULE',
'c_name' => "Ngày bắt đầu làm việc thứ 7 trong năm",
'c_code' => $request->c_code,
'c_value' => Carbon::now()->year,
'c_active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
} else {
$schedule->update([
'c_code' => $request->c_code,
]);
}
return response()->json([
'status' => true,
'message' => 'Saturday work schedule updated successfully'
]);
}
}

View File

@ -0,0 +1,161 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Document;
class DocumentController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
public function all(Request $request)
{
$documents = new Document;
// Order by
$this->orderByRequest($documents, $request);
$documents->orderBy('title', 'asc');
// Filter
$this->filterRequest(
builder: $documents,
request: $request,
filterKeys: [
'title' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $documents,
value: $request->get('search'),
fields: [
'title',
]
);
$responseData = $documents->get();
return AbstractController::ResultSuccess($responseData);
}
public function create(Request $request)
{
$request->validate([
'type' => 'required|in:file,link',
'files' => 'nullable|array',
'files.*.title' => 'required|string|max:255',
'files.*.file' => 'required|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
'links' => 'nullable|array',
'links.*.title' => 'required|string|max:255',
'links.*.uri' => 'required|string|url',
'is_active' => 'required|boolean',
]);
$documents = [];
if ($request->type === "file") {
foreach ($request->file('files') as $index => $file) {
$path = $file['file']->store('uploads', 'public');
$documents[] = [
'title' => $request->input("files.$index.title"),
'type' => 'file',
'uri' => "storage/{$path}",
'is_active' => $request->is_active,
'created_at' => now(),
'updated_at' => now(),
];
}
} else {
foreach ($request->links as $link) {
$documents[] = [
'title' => $link['title'],
'type' => 'link',
'uri' => $link['uri'],
'is_active' => $request->is_active,
'created_at' => now(),
'updated_at' => now(),
];
}
}
if (!empty($documents)) {
Document::insert($documents);
}
return AbstractController::ResultSuccess($documents, "Documents created successfully!");
}
public function update(Request $request)
{
$request->validate([
'id' => 'required|exists:documents,id',
'title' => 'required|string|max:255',
'type' => 'required|in:file,link',
'uri' => 'nullable|url',
'file' => 'nullable|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
'existing_file' => 'nullable|string',
'is_active' => 'required|boolean',
]);
$document = Document::find($request->input('id'));
if (!$document) {
return AbstractController::ResultError("Document not found.");
}
if ($request->type === "file") {
$uri = $request->existing_file;
if ($request->hasFile('file')) {
$filePath = str_replace('storage/', 'public/', $request->existing_file);
if (Storage::exists($filePath)) {
Storage::delete($filePath);
}
$path = $request->file('file')->store('uploads', 'public');
$uri = "storage/{$path}";
}
$document->update([
'title' => $request->title,
'type' => $request->type,
'uri' => $uri,
'is_active' => $request->is_active,
]);
return AbstractController::ResultSuccess($document, "Document updated successfully!");
}
$document->update([
'title' => $request->title,
'type' => $request->type,
'uri' => $request->uri,
'is_active' => $request->is_active,
]);
return AbstractController::ResultSuccess($document, "Document updated successfully!");
}
public function delete(Request $request)
{
$id = $request->input('id');
$document = Document::find($id);
if (!$document) {
return AbstractController::ResultError("Document not found");
}
if ($document->type === "file") {
$filePath = str_replace('storage/', 'public/', $document->uri);
if (Storage::exists($filePath)) {
Storage::delete($filePath);
}
}
$document->delete();
return AbstractController::ResultSuccess("Document deleted successfully!");
}
}

View File

@ -0,0 +1,359 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\ElectricityBill;
use Illuminate\Support\Facades\Log;
use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\Carbon;
use Illuminate\Support\Facades\Storage;
class ElectricityBillController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
/**
* Get all electricity bills with pagination
*/
public function index(Request $request)
{
try {
$bills = new ElectricityBill;
// Order by
$this->orderByRequest($bills, $request);
// Filter
$this->filterRequest(
builder: $bills,
request: $request,
filterKeys: [
'billing_date' => self::F_TEXT,
]
);
// Search
$this->searchRequest(
builder: $bills,
value: $request->get('search'),
fields: ['billing_date', 'notes']
);
$responseData = $bills
->leftJoin('users as creator', 'electricity_bills.created_by', '=', 'creator.id')
->leftJoin('users as updater', 'electricity_bills.updated_by', '=', 'updater.id')
->orderBy('electricity_bills.billing_date', 'desc')
->select(
'electricity_bills.*',
'creator.name as creator_name',
'updater.name as updater_name'
)
->paginate($request->get('per_page', 15));
return AbstractController::ResultSuccess($responseData);
} catch (\Exception $e) {
Log::error('Error fetching electricity bills: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Create new electricity bill
*/
public function create(Request $request)
{
try {
$validated = $request->validate([
'billing_date' => 'required|string',
'previous_reading' => 'required|numeric|min:0',
'current_reading' => 'required|numeric|min:0',
'unit_price' => 'required|numeric|min:0',
'notes' => 'nullable|string',
]);
// Check if billing_date already exists
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])->first();
if ($existingBill) {
return AbstractController::ResultError('Bill for this month already exists', 422);
}
// Calculate total amount
$consumption = $validated['current_reading'] - $validated['previous_reading'];
$totalAmount = $consumption * $validated['unit_price'];
$bill = ElectricityBill::create([
'billing_date' => $validated['billing_date'],
'previous_reading' => $validated['previous_reading'],
'current_reading' => $validated['current_reading'],
'unit_price' => $validated['unit_price'],
'total_amount' => $totalAmount,
'notes' => $validated['notes'] ?? null,
'created_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($bill, 'Electricity bill created successfully');
} catch (\Exception $e) {
Log::error('Error creating electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Update electricity bill
*/
public function update(Request $request, $id)
{
try {
$validated = $request->validate([
'billing_date' => 'sometimes|string',
'previous_reading' => 'sometimes|numeric|min:0',
'current_reading' => 'sometimes|numeric|min:0',
'unit_price' => 'sometimes|numeric|min:0',
'notes' => 'nullable|string',
]);
$bill = ElectricityBill::findOrFail($id);
// Check if billing_date already exists (excluding current record)
if (isset($validated['billing_date'])) {
$existingBill = ElectricityBill::where('billing_date', $validated['billing_date'])
->where('id', '!=', $id)
->first();
if ($existingBill) {
return AbstractController::ResultError('Bill for this month already exists', 422);
}
}
// Recalculate total if any reading or price changed
$previousReading = $validated['previous_reading'] ?? $bill->previous_reading;
$currentReading = $validated['current_reading'] ?? $bill->current_reading;
$unitPrice = $validated['unit_price'] ?? $bill->unit_price;
$consumption = $currentReading - $previousReading;
$totalAmount = $consumption * $unitPrice;
$bill->update(array_merge($validated, [
'total_amount' => $totalAmount,
'updated_by' => auth('admins')->user()->id ?? null,
]));
return AbstractController::ResultSuccess($bill, 'Electricity bill updated successfully');
} catch (\Exception $e) {
Log::error('Error updating electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete electricity bill
*/
public function delete(Request $request, $id)
{
try {
$bill = ElectricityBill::findOrFail($id);
$bill->delete();
return AbstractController::ResultSuccess(null, 'Electricity bill deleted successfully');
} catch (\Exception $e) {
Log::error('Error deleting electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Export electricity bill to PDF
*/
public function exportPdf(Request $request, $id)
{
try {
$bill = ElectricityBill::findOrFail($id);
// Get month name from billing_date
$consumption = $bill->current_reading - $bill->previous_reading;
$totalText = $this->numberToVietnamese($bill->total_amount);
$date = Carbon::parse($bill->billing_date);
$dateNow = 'Ngày ' . $date->day .
' tháng ' . $date->month .
' năm ' . $date->year;
// Generate PDF
$pdf = Pdf::loadView('admin::admin.electricity_bills.pdf', [
'bill' => $bill,
'consumption' => $consumption,
'dateNow' => $dateNow,
'totalText' => $totalText
]);
$fileName = 'electricity_bill_' . $bill->billing_date . '.pdf';
$filePath = 'electricity_bills/' . $fileName;
// đảm bảo folder tồn tại
if (!Storage::disk('public')->exists('electricity_bills')) {
Storage::disk('public')->makeDirectory('electricity_bills');
}
// 👇 render 1 lần
$pdfContent = $pdf->output();
// 👇 lưu file
Storage::disk('public')->put($filePath, $pdfContent);
// update DB
$bill->update(['file_path' => $filePath]);
// 👇 trả về đúng file đã tạo
return response($pdfContent)
->header('Content-Type', 'application/pdf')
->header('Content-Disposition', 'attachment; filename="' . $fileName . '"');
} catch (\Exception $e) {
Log::error('Error exporting electricity bill to PDF: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get electricity bill by ID
*/
public function show($id)
{
try {
$bill = ElectricityBill::with(['creator', 'updater'])->findOrFail($id);
return AbstractController::ResultSuccess($bill);
} catch (\Exception $e) {
Log::error('Error fetching electricity bill: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Upload an attachment (PDF or image) for an electricity bill
*/
public function uploadAttachment(Request $request, $id)
{
try {
$request->validate([
'file' => 'required|file|mimes:pdf,jpg,jpeg,png,gif,webp|max:10240',
]);
$bill = ElectricityBill::findOrFail($id);
$file = $request->file('file');
$extension = $file->getClientOriginalExtension();
$filename = 'bill_' . $bill->id . '_' . time() . '.' . $extension;
$directory = 'electricity_bills/attachments';
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory);
}
// Delete old attachment if exists
if ($bill->attachment_path && Storage::disk('public')->exists($bill->attachment_path)) {
Storage::disk('public')->delete($bill->attachment_path);
}
$path = $file->storeAs($directory, $filename, 'public');
$bill->update([
'attachment_path' => $path,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($bill, 'Attachment uploaded successfully');
} catch (\Exception $e) {
Log::error('Error uploading electricity bill attachment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete the attachment of an electricity bill
*/
public function deleteAttachment($id)
{
try {
$bill = ElectricityBill::findOrFail($id);
if ($bill->attachment_path && Storage::disk('public')->exists($bill->attachment_path)) {
Storage::disk('public')->delete($bill->attachment_path);
}
$bill->update([
'attachment_path' => null,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($bill, 'Attachment deleted successfully');
} catch (\Exception $e) {
Log::error('Error deleting electricity bill attachment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
function numberToVietnamese($number)
{
$units = ["", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín"];
$levels = ["", "nghìn", "triệu", "tỷ"];
if ($number == 0) return "không đồng";
$number = (int)$number;
$result = "";
$level = 0;
while ($number > 0) {
$threeDigits = $number % 1000;
if ($threeDigits != 0) {
$result = $this->readThreeDigits($threeDigits, $units) . " " . $levels[$level] . " " . $result;
}
$number = floor($number / 1000);
$level++;
}
return ucfirst(trim(preg_replace('/\s+/', ' ', $result))) . " đồng";
}
function readThreeDigits($number, $units)
{
$hundreds = floor($number / 100);
$tens = floor(($number % 100) / 10);
$ones = $number % 10;
$result = "";
if ($hundreds > 0) {
$result .= $units[$hundreds] . " trăm";
if ($tens == 0 && $ones > 0) {
$result .= " lẻ";
}
}
if ($tens > 1) {
$result .= " " . $units[$tens] . " mươi";
if ($ones == 1) {
$result .= " mốt";
} elseif ($ones == 5) {
$result .= " lăm";
} elseif ($ones > 0) {
$result .= " " . $units[$ones];
}
} elseif ($tens == 1) {
$result .= " mười";
if ($ones == 5) {
$result .= " lăm";
} elseif ($ones > 0) {
$result .= " " . $units[$ones];
}
} elseif ($ones > 0) {
$result .= " " . $units[$ones];
}
return trim($result);
}
}

View File

@ -0,0 +1,517 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Equipment;
use Modules\Admin\app\Models\EquipmentAssignment;
class EquipmentController extends Controller
{
use HasOrderByRequest;
use HasFilterRequest;
use HasSearchRequest;
/**
* Get all equipments with pagination + filter + search.
*/
public function index(Request $request)
{
try {
$equipments = new Equipment;
$this->orderByRequest($equipments, $request);
$this->filterRequest(
builder: $equipments,
request: $request,
filterKeys: [
'code' => self::F_TEXT,
'name' => self::F_TEXT,
'note' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $equipments,
value: $request->get('search'),
fields: ['equipments.code', 'equipments.name', 'equipments.note']
);
// Optional filter: assigned (in use) vs warehouse
$assignedFilter = $request->get('assigned');
if ($assignedFilter === 'in_use') {
$equipments->whereNotNull('equipments.current_user_id');
} elseif ($assignedFilter === 'warehouse') {
$equipments->whereNull('equipments.current_user_id');
}
$responseData = $equipments
->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->orderBy('equipments.updated_at', 'desc')
->select(
'equipments.*',
'holder.name as current_user_name'
)
->paginate($request->get('per_page', 15));
return AbstractController::ResultSuccess($responseData);
} catch (\Exception $e) {
Log::error('Error fetching equipments: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get a single equipment by code (for barcode scan).
*/
public function showByCode(Request $request)
{
try {
$code = trim((string) $request->get('code'));
if ($code === '') {
return AbstractController::ResultError('Code is required', 422);
}
$equipment = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->where('equipments.code', $code)
->select('equipments.*', 'holder.name as current_user_name')
->first();
if (!$equipment) {
return AbstractController::ResultSuccess(null, 'Equipment not found');
}
$history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id')
->where('equipment_assignments.equipment_id', $equipment->id)
->orderBy('equipment_assignments.assigned_at', 'desc')
->select(
'equipment_assignments.*',
'users.name as user_name'
)
->get();
$equipment->history = $history;
return AbstractController::ResultSuccess($equipment);
} catch (\Exception $e) {
Log::error('Error fetching equipment by code: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get assignment history for an equipment.
*/
public function history($id)
{
try {
$history = EquipmentAssignment::leftJoin('users', 'equipment_assignments.user_id', '=', 'users.id')
->where('equipment_assignments.equipment_id', $id)
->orderBy('equipment_assignments.assigned_at', 'desc')
->select(
'equipment_assignments.*',
'users.name as user_name'
)
->get();
return AbstractController::ResultSuccess($history);
} catch (\Exception $e) {
Log::error('Error fetching equipment history: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Get all users for assignment selectbox.
*/
public function userOptions()
{
try {
$users = Admin::orderBy('name', 'asc')
->select('id', 'name', 'email')
->get();
return AbstractController::ResultSuccess($users);
} catch (\Exception $e) {
Log::error('Error fetching user options: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Create new equipment. Accepts multipart with optional image file.
*/
public function create(Request $request)
{
try {
$validated = $request->validate([
'code' => 'required|string|max:255|unique:equipments,code',
'name' => 'required|string|max:255',
'note' => 'nullable|string',
'current_user_id' => 'nullable|integer|exists:users,id',
'image' => 'nullable|file|mimes:jpg,jpeg,png,gif,webp|max:10240',
]);
$authId = auth('admins')->user()->id ?? null;
$imagePath = null;
if ($request->hasFile('image')) {
$directory = 'equipments/images';
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory);
}
$imagePath = $request->file('image')->store($directory, 'public');
}
$equipment = DB::transaction(function () use ($validated, $authId, $imagePath) {
$now = Carbon::now();
$currentUserId = $validated['current_user_id'] ?? null;
$equipment = Equipment::create([
'code' => $validated['code'],
'name' => $validated['name'],
'note' => $validated['note'] ?? null,
'image_path' => $imagePath,
'current_user_id' => $currentUserId,
'assigned_at' => $now,
'created_by' => $authId,
]);
// Always create an initial assignment record so the history
// shows the very first state (warehouse or user).
EquipmentAssignment::create([
'equipment_id' => $equipment->id,
'user_id' => $currentUserId, // null = at warehouse
'assigned_at' => $now,
'note' => $currentUserId ? 'Initial assignment' : 'Initial — at warehouse',
'created_by' => $authId,
]);
return $equipment;
});
return AbstractController::ResultSuccess($equipment, 'Equipment created successfully');
} catch (\Illuminate\Validation\ValidationException $e) {
return AbstractController::ResultError($e->getMessage(), 422);
} catch (\Exception $e) {
Log::error('Error creating equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Upload / replace the equipment image (optional, post-create or edit).
*/
public function uploadImage(Request $request, $id)
{
try {
$request->validate([
'image' => 'required|file|mimes:jpg,jpeg,png,gif,webp|max:10240',
]);
$equipment = Equipment::findOrFail($id);
// Delete old image if present
if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) {
Storage::disk('public')->delete($equipment->image_path);
}
$directory = 'equipments/images';
if (!Storage::disk('public')->exists($directory)) {
Storage::disk('public')->makeDirectory($directory);
}
$path = $request->file('image')->store($directory, 'public');
$equipment->update([
'image_path' => $path,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($equipment, 'Image uploaded');
} catch (\Exception $e) {
Log::error('Error uploading equipment image: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete the equipment image.
*/
public function deleteImage($id)
{
try {
$equipment = Equipment::findOrFail($id);
if ($equipment->image_path && Storage::disk('public')->exists($equipment->image_path)) {
Storage::disk('public')->delete($equipment->image_path);
}
$equipment->update([
'image_path' => null,
'updated_by' => auth('admins')->user()->id ?? null,
]);
return AbstractController::ResultSuccess($equipment, 'Image deleted');
} catch (\Exception $e) {
Log::error('Error deleting equipment image: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Update equipment basic info (code, name, note).
*/
public function update(Request $request, $id)
{
try {
$equipment = Equipment::findOrFail($id);
$validated = $request->validate([
'code' => 'sometimes|string|max:255|unique:equipments,code,' . $id,
'name' => 'sometimes|string|max:255',
'note' => 'nullable|string',
]);
$equipment->update(array_merge($validated, [
'updated_by' => auth('admins')->user()->id ?? null,
]));
return AbstractController::ResultSuccess($equipment, 'Equipment updated successfully');
} catch (\Exception $e) {
Log::error('Error updating equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Assign equipment to a user, or return to warehouse (user_id = null).
* Every state change is recorded: closes the previous open record AND opens
* a new one for the new holder (user or warehouse).
*/
public function assign(Request $request, $id)
{
try {
$validated = $request->validate([
'user_id' => 'nullable|integer|exists:users,id',
'note' => 'nullable|string',
]);
$authId = auth('admins')->user()->id ?? null;
$equipment = DB::transaction(function () use ($id, $validated, $authId) {
$equipment = Equipment::lockForUpdate()->findOrFail($id);
$now = Carbon::now();
$newUserId = $validated['user_id'] ?? null;
// No-op guard: if assigning to the same holder, skip to avoid duplicate history rows
if ($equipment->current_user_id == $newUserId) {
return $equipment;
}
// Close any currently open assignment (records who/Kho was holding it until now)
EquipmentAssignment::where('equipment_id', $equipment->id)
->whereNull('returned_at')
->update([
'returned_at' => $now,
'updated_at' => $now,
]);
// Always open a new assignment row — for a user OR for the warehouse.
// user_id = null means "currently at warehouse".
EquipmentAssignment::create([
'equipment_id' => $equipment->id,
'user_id' => $newUserId,
'assigned_at' => $now,
'note' => $validated['note'] ?? ($newUserId === null ? 'Returned to warehouse' : null),
'created_by' => $authId,
]);
$equipment->update([
'current_user_id' => $newUserId,
'assigned_at' => $now,
'updated_by' => $authId,
]);
return $equipment->fresh();
});
return AbstractController::ResultSuccess($equipment, 'Equipment assignment updated');
} catch (\Exception $e) {
Log::error('Error assigning equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Statistics: list users grouped with the equipments they are currently holding.
*/
public function userStatistics(Request $request)
{
try {
$search = trim((string) $request->get('search', ''));
$usersQuery = DB::table('equipments')
->join('users', 'equipments.current_user_id', '=', 'users.id')
->whereNotNull('equipments.current_user_id');
if ($search !== '') {
$usersQuery->where(function ($q) use ($search) {
$q->where('users.name', 'like', "%{$search}%")
->orWhere('users.email', 'like', "%{$search}%")
->orWhere('equipments.code', 'like', "%{$search}%")
->orWhere('equipments.name', 'like', "%{$search}%");
});
}
$userTotals = (clone $usersQuery)
->select(
'users.id as user_id',
'users.name as user_name',
'users.email as user_email',
DB::raw('COUNT(equipments.id) as equipment_count')
)
->groupBy('users.id', 'users.name', 'users.email')
->orderByDesc('equipment_count')
->orderBy('users.name')
->get();
$userIds = $userTotals->pluck('user_id')->all();
$devices = Equipment::leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->whereIn('equipments.current_user_id', $userIds)
->select(
'equipments.id',
'equipments.code',
'equipments.name',
'equipments.note',
'equipments.image_path',
'equipments.current_user_id',
'equipments.assigned_at'
)
->orderBy('equipments.assigned_at', 'desc')
->get()
->groupBy('current_user_id');
$result = $userTotals->map(function ($u) use ($devices) {
return [
'user_id' => $u->user_id,
'user_name' => $u->user_name,
'user_email' => $u->user_email,
'equipment_count' => (int) $u->equipment_count,
'equipments' => ($devices->get($u->user_id) ?? collect())->values(),
];
});
// Also include warehouse summary
$warehouseCount = Equipment::whereNull('current_user_id')->count();
return AbstractController::ResultSuccess([
'users' => $result,
'warehouse_count' => $warehouseCount,
'total_equipment' => Equipment::count(),
]);
} catch (\Exception $e) {
Log::error('Error fetching equipment user statistics: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Delete equipment (and its history via cascade).
*/
public function delete($id)
{
try {
$equipment = Equipment::findOrFail($id);
$equipment->delete();
return AbstractController::ResultSuccess(null, 'Equipment deleted successfully');
} catch (\Exception $e) {
Log::error('Error deleting equipment: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
/**
* Export the (filtered) equipment list as CSV.
*/
public function export(Request $request)
{
try {
$equipments = new Equipment;
$this->filterRequest(
builder: $equipments,
request: $request,
filterKeys: [
'code' => self::F_TEXT,
'name' => self::F_TEXT,
'note' => self::F_TEXT,
]
);
$this->searchRequest(
builder: $equipments,
value: $request->get('search'),
fields: ['equipments.code', 'equipments.name', 'equipments.note']
);
$assignedFilter = $request->get('assigned');
if ($assignedFilter === 'in_use') {
$equipments->whereNotNull('equipments.current_user_id');
} elseif ($assignedFilter === 'warehouse') {
$equipments->whereNull('equipments.current_user_id');
}
$rows = $equipments
->leftJoin('users as holder', 'equipments.current_user_id', '=', 'holder.id')
->orderBy('equipments.updated_at', 'desc')
->select(
'equipments.id',
'equipments.code',
'equipments.name',
'equipments.note',
'equipments.assigned_at',
'equipments.created_at',
'equipments.updated_at',
'holder.name as current_user_name'
)
->get();
$filename = 'equipments_' . date('Y-m-d_His') . '.csv';
$callback = function () use ($rows) {
$out = fopen('php://output', 'w');
// BOM for Excel UTF-8
fwrite($out, "\xEF\xBB\xBF");
fputcsv($out, ['ID', 'Code', 'Name', 'Note', 'Current Holder', 'Assigned At', 'Created At', 'Updated At']);
foreach ($rows as $r) {
fputcsv($out, [
$r->id,
$r->code,
$r->name,
$r->note,
$r->current_user_name ?: 'Kho',
$r->assigned_at,
$r->created_at,
$r->updated_at,
]);
}
fclose($out);
};
return response()->stream($callback, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
} catch (\Exception $e) {
Log::error('Error exporting equipments: ' . $e->getMessage());
return AbstractController::ResultError($e->getMessage());
}
}
}

View File

@ -8,9 +8,11 @@ use App\Services\JiraService;
use Carbon\Carbon;
use Modules\Admin\app\Models\TechnicalUser;
use Illuminate\Http\Request;
use Modules\Admin\app\Models\ProjectReview;
use Modules\Admin\app\Models\UserCriteria;
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\SimpleType\Jc;
class EvaluationController extends Controller
{
@ -222,6 +224,50 @@ class EvaluationController extends Controller
'spaceAfter' => 600,
]);
}
// **ProjectReview Section**
// Fetch Project Reviews
$projectReviews = ProjectReview::where('user_id', $user->id);
if ($startDate && $endDate) {
$projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
} elseif ($startDate) {
$projectReviews->where('updated_at', '>=', $startDate);
} elseif ($endDate) {
$projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59');
}
if ($projectReviews->get()->count() > 0) {
$section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
$table = $section->addTable([
'borderColor' => '000000',
'borderSize' => 6,
'cellMargin' => 80,
]);
// Table Header
$table->addRow();
$table->addCell(3500)->addText('Project Name', ['bold' => true]);
$table->addCell(2500)->addText('Role', ['bold' => true]);
$table->addCell(5000)->addText('Note', ['bold' => true]);
$table->addCell(2500)->addText('Created At', ['bold' => true]);
$table->addCell(2500)->addText('Updated At', ['bold' => true]);
foreach ($projectReviews->get() as $review) {
$table->addRow();
$table->addCell(3500)->addText($review->name);
$table->addCell(2500)->addText($review->role);
$table->addCell(5000)->addText($review->note);
$table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s'));
$table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s'));
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
if ($technicalData)
$section->addPageBreak();
//Technical
@ -260,4 +306,156 @@ class EvaluationController extends Controller
return response()->download($tempFile, "$user->name.docx")->deleteFileAfterSend(true);
}
public function reportAllUsers(Request $request)
{
$request->validate([
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$users = User::all();
$phpWord = new PhpWord();
$phpWord->setDefaultFontName('Times New Roman');
$phpWord->setDefaultFontSize(12);
$section = $phpWord->addSection();
foreach ($users as $index => $user) {
$userEmail = $user->email;
// Add user heading
$section->addText("Staff Evaluation", ['bold' => true, 'size' => 20, 'color' => '000000'], ['align' => 'center']);
if ($startDate) {
$fromDate = Carbon::parse($startDate)->format('d-m-Y');
$section->addText("From: " . $fromDate, ['size' => 12], ['align' => 'end']);
}
if ($endDate) {
$toDate = Carbon::parse($endDate)->format('d-m-Y');
$section->addText("To: " . $toDate, ['size' => 12], ['align' => 'end']);
}
$section->addText("{$user->name}", ['bold' => true, 'size' => 14, 'color' => '000000'], ['spaceAfter' => 400]);
// **Projects Data**
$projectsData = self::getProjectReviewByParams($startDate, $endDate, $userEmail);
if (!empty($projectsData)) {
foreach ($projectsData as $project) {
$section->addText("Project: {$project['name']}", ['bold' => true, 'size' => 14, 'color' => '000080']);
foreach ($project['sprints'] as $sprint) {
$section->addText("Sprint: {$sprint['name']}", ['bold' => true, 'italic' => true, 'size' => 12]);
$table = $section->addTable(['borderSize' => 6, 'cellMargin' => 80]);
$table->addRow();
$table->addCell(3000)->addText('Criteria', ['bold' => true]);
$table->addCell(3000)->addText('Note', ['bold' => true]);
$table->addCell(2500)->addText('Created By', ['bold' => true]);
$table->addCell(1500)->addText('Point', ['bold' => true]);
foreach ($sprint['criterias'] as $criteria) {
$table->addRow();
$table->addCell(3000)->addText($criteria['criteria']);
$table->addCell(3000)->addText($criteria['note']);
$table->addCell(2500)->addText($criteria['createdBy']);
$table->addCell(1500)->addText($criteria['point'] > 0 ? $criteria['point'] : '');
}
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
}
// **ProjectReview Section**
// Fetch Project Reviews
$projectReviews = ProjectReview::where('user_id', $user->id);
if ($startDate && $endDate) {
$projectReviews->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
} elseif ($startDate) {
$projectReviews->where('updated_at', '>=', $startDate);
} elseif ($endDate) {
$projectReviews->where('updated_at', '<=', $endDate . ' 23:59:59');
}
if ($projectReviews->get()->count() > 0) {
$section->addText("Project Reviews", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
$table = $section->addTable([
'borderColor' => '000000',
'borderSize' => 6,
'cellMargin' => 80,
]);
// Table Header
$table->addRow();
$table->addCell(3500)->addText('Project Name', ['bold' => true]);
$table->addCell(2500)->addText('Role', ['bold' => true]);
$table->addCell(5000)->addText('Note', ['bold' => true]);
$table->addCell(2500)->addText('Created At', ['bold' => true]);
$table->addCell(2500)->addText('Updated At', ['bold' => true]);
foreach ($projectReviews->get() as $review) {
$table->addRow();
$table->addCell(3500)->addText($review->name);
$table->addCell(2500)->addText($review->role);
$table->addCell(5000)->addText($review->note);
$table->addCell(2500)->addText(Carbon::parse($review->created_at)->format('d/m/Y H:i:s'));
$table->addCell(2500)->addText(Carbon::parse($review->updated_at)->format('d/m/Y H:i:s'));
}
$section->addText(' ', [], [
'spaceAfter' => 600,
]);
}
// **Technical Section**
$section->addText("Technicals", ['bold' => true, 'size' => 14, 'color' => '000080'], ['alignment' => Jc::CENTER]);
$table = $section->addTable([
'borderColor' => '000000',
'borderSize' => 6,
'cellMargin' => 80,
]);
$table->addRow();
$table->addCell(1500)->addText('Level', ['bold' => true]);
$table->addCell(3500)->addText('Name', ['bold' => true]);
$table->addCell(2500)->addText('Point', ['bold' => true]);
$table->addCell(2500)->addText('Last Update', ['bold' => true]);
// Fetch Technical Data
$technicalData = TechnicalController::getDataTechnicalsByUserId($user->id);
foreach ($technicalData as $technical) {
$updated_at = $technical['updated_at'] ? Carbon::parse($technical['updated_at'])->format('d/m/Y H:i:s') : null;
$table->addRow();
$table->addCell(1500)->addText($technical['level']);
$table->addCell(3500)->addText($technical['name']);
$table->addCell(2500)->addText($technical['point']);
$table->addCell(2500)->addText($updated_at);
}
// Add page break between users (except last one)
if ($index < count($users) - 1) {
$section->addPageBreak();
}
}
// Save & Download Word File
$tempFile = tempnam(sys_get_temp_dir(), 'word');
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
$objWriter->save($tempFile);
return response()->download($tempFile, "All_Users_Report.docx")->deleteFileAfterSend(true);
}
}

View File

@ -3,7 +3,9 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Mail\WarningLongTask;
use App\Mail\WorklogReport;
use App\Models\User;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
@ -176,6 +178,27 @@ class JiraController extends Controller
}
}
public function getWeeklyReport()
{
try {
$startOfWeek = Carbon::now()->startOfWeek()->format('Y-m-d'); // Mặc định là Thứ Hai
$endOfWeek = Carbon::now()->endOfWeek()->format('Y-m-d'); // Mặc định là Chủ Nhật
// dd($startOfWeek);
$results = [];
$workLogs = $this->jiraService->getAllUserWorkLogs($startOfWeek, $endOfWeek);
foreach($workLogs as $data){
$results[$data['username']] = $data['information']['issues'];
}
return response()->json([
$results
], 200);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
public function sendReport()
{
$dateFormatted = Carbon::yesterday()->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
@ -183,8 +206,8 @@ class JiraController extends Controller
$workLogs = $this->jiraService->getAllUserWorkLogs($dateFormatted, $dateFormatted);
$tasksByUser = $this->formatWorkLogsByUser($workLogs);
Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
// Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
// Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
Mail::to(['joseph@apactech.io', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
// return "Email sent successfully!";
return response()->json([
@ -248,7 +271,6 @@ class JiraController extends Controller
return $tasksByUser;
}
public function getAllUserDoing(Request $request)
{
try {
@ -271,7 +293,7 @@ class JiraController extends Controller
'status' => true
], 200);
}
public function getDetailsProjectsById(Request $request)
{
$id = $request->input('id');
@ -303,6 +325,7 @@ class JiraController extends Controller
'status' => true
], 200);
}
public function getAllIssueByIdSprint(Request $request)
{
$id = $request->input('id');
@ -313,4 +336,42 @@ class JiraController extends Controller
'status' => true
], 200);
}
public function sendWarningMailByAllowcation()
{
$data = $this->jiraService->getAllUserDoing();
$user_info = [];
foreach ($data['projects'] as $project) {
foreach ($project['users'] as $user) {
foreach ($user['issues'] as $issue) {
$targetDate = Carbon::parse($issue['changelog']['histories'][0]['created']); // Target date
$daysRemaining = Carbon::now()->setTimezone(env('TIME_ZONE'))->diffInDays($targetDate);
if ($daysRemaining > 10) {
$issue['daysRemaining'] = $daysRemaining;
$user_info[$user['user']['emailAddress']][] = $issue;
}
}
}
}
foreach ($user_info as $email => $user) {
Mail::to([$email])->cc(['admin@apactech.io', 'joseph@apactech.io'])->send(new WarningLongTask($user_info[$email]));
}
}
public function getUserProjectParticipating(Request $request)
{
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$user = User::find($userID);
$userJira = $this->jiraService->getUserByEmail($user->email);
$projects = $this->jiraService->getUserWorkLogs($userJira[0]['accountId'], $startDate, $endDate);
return response()->json([
'data' => $projects,
"accountId" => $userJira[0]['accountId'],
'status' => true
], 200);
}
}

View File

@ -2,16 +2,23 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Exports\LeaveManagementExport;
use App\Http\Controllers\Controller;
use App\Jobs\InitializeLeaveDays;
use App\Models\LeaveDays;
use App\Models\Notes;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Admin\app\Models\Ticket;
use App\Traits\AnalyzeData;
class LeaveManagementController extends Controller
{
use AnalyzeData;
public function get(Request $request)
{
$yearNow = $request->query('year', now()->year);
@ -33,31 +40,34 @@ class LeaveManagementController extends Controller
})
->leftJoin("categories as reason", function ($join) {
$join->on('n_reason', '=', 'reason.c_code');
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
})
->select(
DB::raw('notes.id as id'),
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_time_type as time_type'),
DB::raw('notes.n_year as year'),
DB::raw('notes.n_month as month'),
DB::raw('categories.c_value as leave_days'),
DB::raw('notes.n_day as day'),
DB::raw('notes.n_reason as reason_code'),
'reason.c_name as reason_name',
'categories.c_name as time_type_name',
// DB::raw('SUM(categories.c_value) as leave_days')
)
// ->where('notes.n_user_id', "1")
->where('notes.n_year', $year)
->where('notes.n_reason', 'ONLEAVE')
->whereIn('notes.n_reason', ['ONLEAVE', 'LEAVE_WITHOUT_PAY', 'TEMPORARY_ONLEAVE'])
// ->groupBy("notes.n_user_id")
->orderBy('notes.n_month')
->orderBy('notes.n_day')
->get()
->map(function ($item) {
return [
"id" => $item->id,
"day" => $item->day,
"n_user_id" => $item->n_user_id,
// "time_type" => $item->time_type,
"reason_code" => $item->reason_code,
"reason_name" => $item->reason_name,
"time_type_name" => $item->time_type_name,
"month" => $item->month,
@ -73,6 +83,7 @@ class LeaveManagementController extends Controller
'users.name as user_name',
'users.email',
'users.avatar',
'users.is_separated',
'users.created_at as user_created_at',
'users.permission',
'users.updated_at as user_updated_at',
@ -94,6 +105,7 @@ class LeaveManagementController extends Controller
'name' => $item->user_name,
'email' => $item->email,
'avatar' => $item->avatar,
'is_separated' => $item->is_separated,
'created_at' => $item->user_created_at,
'permission' => $item->permission,
'updated_at' => $item->user_updated_at,
@ -103,9 +115,10 @@ class LeaveManagementController extends Controller
'leaveDay' => [
'id' => $item->id,
'ld_user_id' => $item->ld_user_id,
'ld_day' => $item->ld_day,
'ld_day_total' => $item->ld_day_total,
'ld_year' => $item->ld_year,
'ld_date_additional' => $item->ld_date_additional,
'ld_additional_day' => $item->ld_additional_day,
'ld_special_leave_day' => $item->ld_special_leave_day,
'ld_note' => $item->ld_note,
'created_at' => $item->created_at,
'updated_at' => $item->updated_at,
@ -131,12 +144,66 @@ class LeaveManagementController extends Controller
$validatedData = $request->all();
$leaveDays = LeaveDays::find($validatedData['id']);
$leaveDays->ld_day = $validatedData['totalLeave'];
$leaveDays->ld_date_additional = $validatedData['dayAdditional']; // Assuming you have this field to store additional days
$leaveDays->ld_day_total = $validatedData['totalLeave'];
$leaveDays->ld_additional_day = $validatedData['dayAdditional'];
$leaveDays->ld_special_leave_day = $validatedData['specialLeave'];
$leaveDays->ld_note = $validatedData['note'];
$leaveDays->save();
return response()->json(['status' => true, 'message' => 'Updated successfully']);
}
public function updateNoteStatus(Request $request)
{
$rules = [
'id' => 'required',
'n_reason' => 'required|in:ONLEAVE,LEAVE_WITHOUT_PAY'
];
// Validate the request
$request->validate($rules);
$id = $request->input('id');
$reason = $request->input('n_reason');
$note = Notes::find($id);
if (!$note) {
return response()->json(['message' => 'Note not found', 'status' => false]);
}
$note->n_reason = $reason;
$note->save();
// Clear Timekeeping cache
$ticket = Ticket::find($note->ticket_id);
if ($ticket) {
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
} else {
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($note->created_at)->month, Carbon::parse($note->created_at)->year);
}
return response()->json(data: ['message' => 'Update success', 'status' => true]);
}
public function export(Request $request)
{
$year = $request->query('year', now()->year);
$leaveDays = $this->getDataByYear($year);
if ($leaveDays->isEmpty()) {
return response()->json(['status' => false, 'message' => 'No data found']);
}
// Lọc chỉ lấy user có permission bao gồm staff
$staffData = $leaveDays->filter(function ($user) {
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
});
$currentDate = date('d_His');
return Excel::download(
new LeaveManagementExport($staffData),
"LeaveManagement_{$year}_{$currentDate}.xlsx"
);
}
}

View File

@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Sprint;
use Modules\Admin\app\Models\UserCriteria;
use App\Models\Files;
use App\DataTransferObjects\FileData;
class ProfileController extends Controller
{
@ -28,6 +33,7 @@ class ProfileController extends Controller
$this->jiraService = $jiraService;
}
public function getProfilesData(Request $request)
{
$user = auth('admins')->user();
@ -115,4 +121,353 @@ class ProfileController extends Controller
$user->save();
return AbstractController::ResultSuccess($path);
}
public function listFiles(Request $request)
{
// Get the root folder from the input URL
$rootFolder = $request->input('root_folder');
// Ensure the root folder is correctly formatted
$rootFolder = rtrim($rootFolder, '/') . '/';
// Get all files and directories in the specified root folder
$fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder);
return response()->json(['data' => $fileList, 'status' => true]);
}
private function getDirectoryTree($dir, $urlRoot)
{
$results = [];
// Scan the directory for files and folders
$files = scandir($dir);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..') {
$filePath = $dir . DIRECTORY_SEPARATOR . $file;
$fileUrl = url($urlRoot . $file);
if (is_dir($filePath)) {
// If it's a directory, recurse into it
$results[] = [
'label' => $file,
'type' => 'directory',
'value' => $fileUrl,
'children' => $this->getDirectoryTree($filePath, $urlRoot . $file . '/')
];
} else {
// If it's a file, add it to the list
$results[] = [
'label' => $file,
'type' => 'file',
'value' => $fileUrl
];
}
}
}
return $results;
}
public function updateProfile(Request $request)
{
$name = $request->input('name') ?? auth('admins')->user()->name;
// Validate the incoming files
$request->validate([
'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx,xlsx,xls,csv|max:5120', // Adjust file types and size limit as needed
]);
$uploadedFiles = [];
$baseDirectory = 'profiles/' . $name;
$othersDirectory = $baseDirectory . '/others';
// Check if the base directory exists, if not create it
if (!Storage::disk('public')->exists($baseDirectory)) {
Storage::disk('public')->makeDirectory($baseDirectory);
}
// Check if the "others" directory exists, if not create it
if (!Storage::disk('public')->exists($othersDirectory)) {
Storage::disk('public')->makeDirectory($othersDirectory);
}
$adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
$currentUser = auth('admins')->user();
if ($request->hasFile('files')) {
foreach ($request->file('files') as $file) {
// Store the file and get its path
$originalFilename = $file->getClientOriginalName();
if (strpos($originalFilename, '__') === 0) {
// Store the file in the "others" directory
$path = $file->storeAs($othersDirectory, $originalFilename, 'public');
} else {
// Store the file in the base directory
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
}
$uploadedFiles[] = $path;
// Tạo URL đầy đủ cho file
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path))
: env('APP_URL') . str_replace('/storage/', '', Storage::url($path));
// // Gửi email thông báo cho admin
// foreach ($adminEmails as $adminEmail) {
// $admin = Admin::where('email', $adminEmail)->first();
// if ($admin) {
// $this->sendFileUploadNotification(
// $admin,
// "File {$originalFilename} đã được tải lên bởi {$currentUser->name}",
// $fileUrl,
// "[APAC Tech] {$currentUser->name} - Đã tải lên file mới"
// );
// }
// }
// // Gửi email xác nhận cho người tải lên
// $this->sendFileUploadNotification(
// $currentUser,
// "Bạn đã tải lên file {$originalFilename} thành công",
// $fileUrl,
// "[APAC Tech] {$currentUser->name} - Tải file thành công"
// );
}
}
return response()->json([
'status' => true,
'message' => 'Files uploaded successfully',
'files' => $uploadedFiles,
]);
}
public function removeFile(Request $request)
{
// Validate that the file URL is provided in the request
$request->validate([
'file_url' => 'required|string',
]);
// Get the full file URL from the request
$fileUrl = $request->input('file_url');
// Parse the file path from the URL (remove the base URL part)
$storagePath = parse_url($fileUrl, PHP_URL_PATH); // Extract the path part of the URL
$filePath = str_replace(env('APP_ENV') === 'local' ? '/storage/' : '/image/storage/', '', $storagePath); // Remove "/storage/" to get the actual file path
// Check if the file exists before attempting to delete it
if (Storage::disk('public')->exists($filePath)) {
// Delete the file
Storage::disk('public')->delete($filePath);
return response()->json([
'status' => true,
'message' => 'File deleted successfully',
]);
}
return response()->json([
'status' => false,
'message' => 'File not found',
], 404);
}
public function sendFileUploadNotification($user, $description, $url, $subject, $note)
{
try {
// Gửi email bất đồng bộ không cần job
dispatch(function() use ($user, $description, $url, $subject, $note) {
Mail::send('emails.file_upload_notification', [
'user' => $user,
'description' => $description,
'url' => $url,
'note' => $note
], function ($message) use ($user, $subject) {
$message->to($user->email)
->subject($subject);
});
})->afterResponse();
return true;
} catch (\Exception $e) {
Log::error('Error dispatching file upload notification email: ' . $e->getMessage());
return false;
}
}
public function uploadFiles(Request $request)
{
try {
$request->validate([
'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120',
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'user_name' => 'required|string|max:255'
]);
$file = $request->file('file');
$user = auth('admins')->user();
// Tạo thư mục cho user nếu chưa tồn tại
$userFolder = 'files/' . $request->user_name;
if (!Storage::disk('public')->exists($userFolder)) {
Storage::disk('public')->makeDirectory($userFolder);
}
$path = $file->store($userFolder, 'public');
$fileRecord = Files::create([
'name' => $request->name,
'url' => $path,
'type' => $this->getFileType($file->getClientOriginalName()),
'description' => $request->description,
'user_id' => Admin::where('name', $request->user_name)->first()->id
]);
$currentUser = Admin::where('name', $request->user_name)->first();
// Gửi email thông báo cho người upload
$fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
? env('APP_URL') . '/image' . Storage::url($path)
: env('APP_URL') . Storage::url($path);
$this->sendFileUploadNotification(
$user,
'Bạn đã tải lên file "' . $request->name . '" thành công',
$fileUrl,
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
$request->description ?? 'No description'
);
// Gửi email thông báo cho tất cả admin khác
$otherAdmins = Admin::where('permission', 'like', '%admin%')->get();
foreach ($otherAdmins as $admin) {
$this->sendFileUploadNotification(
$admin,
'File "' . $request->name . '" đã được tải lên bởi ' . $user->name,
$fileUrl,
"[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
$request->description ?? 'No description'
);
}
return response()->json([
'status' => true,
'message' => 'File uploaded successfully',
'data' => [
'id' => $fileRecord->id,
'name' => $fileRecord->name,
'url' => Storage::url($path),
'type' => $fileRecord->type,
'description' => $fileRecord->description
]
]);
} catch (\Exception $e) {
return response()->json([
'status' => false,
'message' => $e->getMessage()
], 500);
}
}
public function getFiles()
{
try {
// Lấy tất cả users
$users = Admin::all();
// Lấy files và map theo cấu trúc
$files = Files::with('user')->get()
->map(function($file) {
return [
'id' => $file->id,
'name' => $file->name,
'url' => Storage::url($file->url),
'type' => $file->type,
'description' => $file->description,
'created_at' => $file->created_at,
'user_id' => $file->user_id,
'user_name' => $file->user->name
];
});
// Tạo mảng kết quả với tất cả users, không có file thì mảng rỗng
$result = $users->pluck('name')->mapWithKeys(function($userName) use ($files) {
$userFiles = $files->where('user_name', $userName)
->map(function($file) {
return (object)[
'id' => $file['id'],
'name' => $file['name'],
'url' => $file['url'],
'type' => $file['type'],
'description' => $file['description'],
'created_at' => $file['created_at'],
'user_id' => $file['user_id']
];
})->values();
return [$userName => $userFiles];
});
return response()->json([
'status' => true,
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'status' => false,
'message' => $e->getMessage()
], 500);
}
}
public function deleteFile($id)
{
try {
$file = Files::findOrFail($id);
$user = auth('admins')->user();
if ($file->user_id !== $user->id) {
return response()->json([
'status' => false,
'message' => 'Unauthorized'
], 403);
}
Storage::disk('public')->delete($file->url);
$file->delete();
return response()->json([
'status' => true,
'message' => 'File deleted successfully'
]);
} catch (\Exception $e) {
return response()->json([
'status' => false,
'message' => $e->getMessage()
], 500);
}
}
private function getFileType($filename)
{
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$typeMap = [
'pdf' => 'document',
'doc' => 'document',
'docx' => 'document',
'jpg' => 'image',
'jpeg' => 'image',
'png' => 'image',
'xls' => 'spreadsheet',
'xlsx' => 'spreadsheet',
'csv' => 'spreadsheet'
];
return $typeMap[$extension] ?? 'other';
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Admin\app\Http\Controllers\AbstractController;
use Modules\Admin\app\Models\ProjectReview;
use Carbon\Carbon;
use Illuminate\Http\Request;
use DateTime;
class ProjectReviewController extends Controller
{
/**
* Display a listing of the resource.
*/
public function getListReviews(Request $request)
{
$request->validate([
'userID' => 'required|exists:users,id',
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$projectsData = ProjectReview::where('user_id', $userID);
if ($startDate && $endDate) {
$projectsData->whereBetween('updated_at', [$startDate, $endDate . ' 23:59:59']);
} elseif ($startDate) {
$projectsData->where('updated_at', '>=', $startDate);
} elseif ($endDate) {
$projectsData->where('updated_at', '<=', $endDate . ' 23:59:59');
}
return AbstractController::ResultSuccess($projectsData->get());
}
/**
* Store a newly created resource in storage.
*/
public function create(Request $request)
{
$request->validate([
'name' => 'required|string',
'role' => 'required|string',
'note' => 'required|string',
'user_id' => 'required|exists:users,id',
]);
$review = ProjectReview::create($request->all());
return response()->json($review, 201);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request)
{
$request->validate([
'name' => 'sometimes|required|string',
'role' => 'sometimes|required|string',
'note' => 'sometimes|required|string',
'user_id' => 'sometimes|required|exists:users,id',
]);
$id = $request->get('id');
$projectReview = ProjectReview::find($id);
$payload = $request->all();
// if ($request->has('created_at')) {
// $created_at = Carbon::create($request->get('created_at'))->setTimezone(env('TIME_ZONE'));
// $payload['created_at'] = $created_at;
// }
if ($projectReview) {
$projectReview->update($payload);
}
return response()->json([
'data' => $projectReview,
'status' => true
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Request $request)
{
$id = $request->get('id');
ProjectReview::destroy($id);
return response()->json(['message' => 'Deleted successfully', 'status' => true]);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Modules\Admin\app\Http\Controllers;
use App\Traits\AnalyzeData;
class DataAnalyzer
{
use AnalyzeData;
}

File diff suppressed because it is too large Load Diff

View File

@ -11,10 +11,14 @@ use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use App\Mail\TicketMail;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\TimekeepingExport;
use Modules\Admin\app\Models\Ticket;
class TimekeepingController extends Controller
{
@ -74,14 +78,14 @@ class TimekeepingController extends Controller
'user_id' => $user->id,
'status' => 'check in',
'time_string' => $start->format('Y-m-d H:i:s'),
'created_at' => $start->setTimezone('UTC')
'created_at' => $start->setTimezone(env('TIME_ZONE'))
],
[
'name' => $user->name,
'user_id' => $user->id,
'status' => 'check out',
'time_string' => $end->format('Y-m-d H:i:s'),
'created_at' => $end->setTimezone('UTC')
'created_at' => $end->setTimezone(env('TIME_ZONE'))
]
]);
}
@ -149,8 +153,6 @@ class TimekeepingController extends Controller
return response()->json(['status' => true, 'message' => 'Add successfully']);
}
public function updateCacheMonth(Request $request)
{
@ -170,19 +172,97 @@ class TimekeepingController extends Controller
// Validate the request
$request->validate($rules);
$id = $request->input('id');
$month = $request->month;
$year = $request->year;
$note = Notes::find($id);
if ($note) {
$note->delete();
$this->createOrUpdateRecordForCurrentMonth($month, $year);
if (!$note) {
return response()->json(['message' => 'Note not found', 'status' => false]);
}
if ($note->ticket_id != null) {
$ticket = Ticket::find($note->ticket_id);
if (!$ticket) {
return response()->json(['message' => 'Ticket not found, can not delete note', 'status' => false]);
}
$admin = auth('admins')->user();
// Handle send mail
$dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->start_period);
$dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $ticket->end_period);
$dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $ticket->type);
$formattedStartDate = Carbon::createFromFormat('Y-m-d', $ticket->start_date)->format('d/m/Y');
$formattedEndDate = Carbon::createFromFormat('Y-m-d', $ticket->end_date)->format('d/m/Y');
$user = Admin::find($ticket->user_id);
$data = array(
"email_template" => "email.notification_tickets_user",
"user_name" => $user->name,
"email" => $user->email,
"name" => $admin->name, //name admin duyệt
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
"type" => $dataMasterType->c_name,
"note" => $ticket->reason,
"admin_note" => $ticket->admin_note,
"link" => "/tickets", //link đến page admin
"status" => "refused",
"subject" => "[Ticket response] Ticket From " . $admin->name
);
Mail::to($user->email)->send(new TicketMail($data));
// Update
$ticket->updated_by = $admin->name;
$ticket->status = "REFUSED";
$ticket->save();
Notes::where('ticket_id', $ticket->id)->delete();
// Clear Timekeeping cache
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->start_date)->month, Carbon::parse($ticket->start_date)->year);
$this->createOrUpdateRecordForCurrentMonth(Carbon::parse($ticket->end_date)->month, Carbon::parse($ticket->end_date)->year);
return response()->json(['message' => 'Delete success', 'status' => true]);
}
return response()->json(['message' => 'Delete fail', 'status' => false]);
$note->delete();
$this->createOrUpdateRecordForCurrentMonth($month, $year);
return response()->json(['message' => 'Delete success', 'status' => true]);
}
public function export(Request $request)
{
// Validate request
$request->validate([
'month' => 'required|numeric|between:1,12',
'year' => 'required|numeric|min:2000',
'working_days' => 'required|numeric|between:1,31'
]);
// Reuse get() function to fetch data
$response = $this->get($request);
$responseData = json_decode($response->getContent(), true);
if (!$responseData['status']) {
return response()->json(['status' => false, 'message' => 'No data found']);
}
// Lọc chỉ lấy user có permission bao gồm staff
$staffData = array_filter($responseData['data'], function ($user) {
return isset($user['user']['permission']) && strpos($user['user']['permission'], 'staff') !== false;
});
$currentDate = date('d_His');
return Excel::download(
new TimekeepingExport(
array_values($staffData), // Convert to indexed array after filtering
$request->month,
$request->year,
$request->working_days
),
"Timekeeping_{$request->month}_{$request->year}_{$currentDate}.xlsx"
);
}
}

View File

@ -2,8 +2,9 @@
namespace Modules\Admin\app\Http\Controllers;
use App\Helper\Cache\CurrentMonthTimekeeping;
use App\Events\WarningChekinLate;
use App\Http\Controllers\Controller;
use App\Mail\CheckinLateMail;
use App\Traits\AnalyzeData;
use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
@ -11,10 +12,12 @@ use App\Traits\HasSearchRequest;
use Carbon\Carbon;
use DateTime;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\MonthlyTimekeeping;
use Modules\Admin\app\Models\Tracking;
use Illuminate\Support\Facades\DB;
class TrackingController extends Controller
{
@ -22,7 +25,7 @@ class TrackingController extends Controller
use HasFilterRequest;
use HasSearchRequest;
use AnalyzeData;
private $CHECK_IN = 'check in';
private $CHECK_OUT = 'check out';
@ -163,14 +166,9 @@ class TrackingController extends Controller
$id = $request->get('id');
$tracking = Tracking::find($id);
// $id = $request->get('id');
// $tracking = Tracking::find($id);
// $payload = $request->all();
// if ($tracking) {
// $tracking->update($payload);
// }
$user = Admin::find($tracking->user_id);
$this->sendCheckinLateMail($user, $tracking->created_at, $tracking->status);
if ($request->hasFile('image')) {
$file = $request->file('image');
$filename = $request->file_name;
@ -203,4 +201,163 @@ class TrackingController extends Controller
'status' => true
]);
}
// public function sendCheckinLateMail()
private function sendCheckinLateMail($user, $time, $status)
{
// $status = $this->CHECK_IN;
// $lastCheck =Tracking::find(1)->created_at;
// $user = Admin::where('name', 'LE TAN LUAN')->first();
if ($status === $this->CHECK_IN) {
$morning_time = Carbon::createFromTimeString('07:30AM')->setTimezone(env('TIME_ZONE'));
$morning_condition_time = Carbon::createFromTimeString('07:40AM')->setTimezone(env('TIME_ZONE'));
$afternoon_time = Carbon::createFromTimeString('11:30AM')->setTimezone(env('TIME_ZONE'));
$afternoon_condition_time = Carbon::createFromTimeString('01:10PM')->setTimezone(env('TIME_ZONE'));
$admin_mails = Admin::where('permission', 'like', '%admin%')->pluck('email');
if ($time->greaterThan($morning_condition_time) && $time->lessThan($afternoon_time)) {
$period = 'morning';
$minutes_late = $morning_time->diffInMinutes($time);
$data = array(
"email_template" => "email.checkin_late_notification",
"email" => $user->email,
"name" => $user->name,
"admin_mails" => $admin_mails,
"message1" => "Your " . $period . " starts " . $minutes_late . " minutes late",
"message2" => "You checked in at [" . $time . "]",
"url" => env('ADMIN_URL') . "/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=" . $user->name . "&time_string=" . $time->format("Y-m-d H:i") . "&status=check+in",
"subject" => "[Management System] Late warning - " . $user->name
);
Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
}
// if ($time->greaterThan($afternoon_condition_time))
// {
// $period = 'afternoon';
// $minutes_late = $afternoon_time->diffInMinutes($time);
// $data = array(
// "email_template" => "email.checkin_late_notification",
// "email" => $user->email,
// "name" => $user->name,
// "message1" => "Your ". $period ." starts <b>". $minutes_late ."</b> minutes late",
// "message2" => "You checked in at <b>[" . $time ."]</b>",
// "url" => env('ADMIN_URL')."/tracking?search=&per_page=10&page=1&timezone=Asia%2FSaigon&name=".$user->name."&time_string=".$time->format("Y-m-d H:i")."&status=check+in",
// "subject" => "[Management System] Late warning"
// );
// Mail::to($user->email)->cc($admin_mails)->send(new CheckinLateMail($data));
// }
}
}
public function getSummaryTracking(Request $request)
{
$request->validate([
'userID' => 'required|exists:users,id',
'fromDate' => 'nullable|date',
'toDate' => 'nullable|date',
]);
$userID = $request->input('userID');
$startDate = $request->input('fromDate');
$endDate = $request->input('toDate');
$trackingData = Tracking::where('user_id', $userID);
if ($startDate && $endDate) {
$trackingData->whereBetween(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
[$startDate, $endDate . ' 23:59:59']
);
} elseif ($startDate) {
$trackingData->where(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
'>=',
$startDate
);
} elseif ($endDate) {
$trackingData->where(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
'<=',
$endDate . ' 23:59:59'
);
}
$onTimeMorning = 0;
$lateMorning = 0;
$onTimeAfternoon = 0;
$lateAfternoon = 0;
$returnTracking = $trackingData->get();
$listLate = [];
$returnTracking->groupBy(function ($record) {
return Carbon::parse($record->time_string)->toDateString();
})->each(function ($records, $date) use ($userID, &$listLate, &$onTimeMorning, &$lateMorning, &$onTimeAfternoon, &$lateAfternoon, &$datesChecked) {
$morningCheck = $records->filter(function ($record) {
return Carbon::parse($record->time_string)->hour < 12;
})->sortBy('time_string')->first();
$afternoonCheck = $records->filter(function ($record) {
$time = Carbon::parse($record->time_string)->hour;
return $time >= 12 && $time <= 14;
})->sortBy('time_string')->first();
$morningTime = Carbon::parse($date)->setTime(7, 40, 0);
$afternoonTime = Carbon::parse($date)->setTime(13, 10, 0);
$checkOutAfternoonTime = Carbon::parse($date)->setTime(15, 00, 0);
if ($morningCheck) {
$checkInTime = Carbon::parse($morningCheck->time_string);
if ($checkInTime->lessThanOrEqualTo($morningTime)) {
$onTimeMorning++;
} else {
array_push($listLate, $morningCheck->id);
$lateMorning++;
}
}
if ($afternoonCheck) {
$checkInTime = Carbon::parse($afternoonCheck->time_string);
if ($checkInTime->lessThanOrEqualTo($afternoonTime)) {
$onTimeAfternoon++;
} else {
array_push($listLate, $afternoonCheck->id);
$lateAfternoon++;
}
} else {
// print ($date . "\n");
$checkOutAfternoon = Tracking::where('user_id', $userID)->whereBetween(
DB::raw("STR_TO_DATE(time_string, '%Y-%m-%d %H:%i:%s')"),
[$date, $date . ' 23:59:59'],
)->where("status", "check out")->first();
// check if not check-in afternoon but has check out
if ($checkOutAfternoon) {
$timeCheck = Carbon::parse($checkOutAfternoon->time_string);
// print ($timeCheck . "\n");
if ($checkOutAfternoon && $timeCheck->greaterThanOrEqualTo($checkOutAfternoonTime)) {
$onTimeAfternoon++;
}
}
}
});
// ** Add `isLate` flag to each record **
foreach ($returnTracking as $record) {
if (in_array($record->id, $listLate)) {
$record->isLate = true;
}
}
return AbstractController::ResultSuccess([
'on_time_morning' => $onTimeMorning,
'late_morning' => $lateMorning,
'on_time_afternoon' => $onTimeAfternoon,
'late_afternoon' => $lateAfternoon,
'value' => $returnTracking
]);
}
}

View File

@ -8,4 +8,12 @@ use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = [
'c_type',
'c_name',
'c_code',
'c_value',
'c_active',
];
}

View File

@ -0,0 +1,13 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Document extends Model
{
use HasFactory;
protected $fillable = ['title', 'uri', 'type', 'is_active'];
}

View File

@ -0,0 +1,52 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasCacheModel;
class ElectricityBill extends Model
{
use HasFactory;
use HasCacheModel;
public function __construct()
{
$this->table = 'electricity_bills';
$this->guarded = [];
}
/**
* Calculate total amount based on reading difference and unit price
*/
public function calculateTotal(): float
{
$consumption = $this->current_reading - $this->previous_reading;
return round($consumption * $this->unit_price, 2);
}
/**
* Get consumption in kWh
*/
public function getConsumption(): float
{
return $this->current_reading - $this->previous_reading;
}
/**
* Get user who created this record
*/
public function creator()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
/**
* Get user who updated this record
*/
public function updater()
{
return $this->belongsTo(\App\Models\User::class, 'updated_by');
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Equipment extends Model
{
use HasFactory;
public function __construct()
{
$this->table = 'equipments';
$this->guarded = [];
}
public function currentUser()
{
return $this->belongsTo(\App\Models\User::class, 'current_user_id');
}
public function assignments()
{
return $this->hasMany(EquipmentAssignment::class, 'equipment_id')->orderBy('assigned_at', 'desc');
}
public function creator()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
public function updater()
{
return $this->belongsTo(\App\Models\User::class, 'updated_by');
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class EquipmentAssignment extends Model
{
use HasFactory;
public function __construct()
{
$this->table = 'equipment_assignments';
$this->guarded = [];
}
public function equipment()
{
return $this->belongsTo(Equipment::class, 'equipment_id');
}
public function user()
{
return $this->belongsTo(\App\Models\User::class, 'user_id');
}
public function creator()
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Modules\Admin\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectReview extends Model
{
use HasFactory;
protected $fillable = ['name', 'role', 'note', 'user_id'];
// Relationship: A review belongs to a user
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -0,0 +1,98 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Bảng thanh toán tiền điện</title>
<style>
body {
font-family: DejaVu Sans, sans-serif;
font-size: 14px;
}
.text-center {
text-align: center;
}
.mt-10 {
margin-top: 10px;
}
.mt-20 {
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
table, th, td {
border: 1px solid #000;
}
th, td {
padding: 8px;
text-align: center;
}
.no-border {
border: none;
}
.signature {
width: 100%;
margin-top: 50px;
}
.signature td {
border: none;
text-align: center;
}
</style>
</head>
<body>
<h3 class="text-center">BẢNG THANH TOÁN TIỀN ĐIỆN</h3>
<p class="text-center">({{ $dateNow ?? '' }})</p>
<div class="mt-20">
<p>- Tên doanh nghiệp: Công ty TNHH Kỹ Thuật Công Nghệ APAC</p>
<p>- số thuế: 0110038408</p>
<p>- Địa chỉ: Số 219/26/3 đường Lĩnh Nam, Phường Vĩnh Hưng, thành phố Nội, Việt Nam</p>
<p>- Tên chủ sở hữu cho thuê địa điểm sản xuất kinh doanh: Lâm Văn Mười</p>
<p>- Địa chỉ thuê: 50B31 tại Khu dân 91B giai đoạn 3, phường Tân An, thành phố Cần Thơ</p>
</div>
<table>
<thead>
<tr>
<th>Số điện đầu kỳ</th>
<th>Số điện cuối kỳ</th>
<th>Số điện tiêu thụ</th>
<th>Đơn giá</th>
<th>Thành tiền</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ number_format($bill->previous_reading) ?? 0 }}</td>
<td>{{ number_format($bill->current_reading) ?? 0 }}</td>
<td>{{ $consumption ?? 0 }}</td>
<td>{{ number_format($bill->unit_price) ?? '0' }}</td>
<td>{{ number_format($bill->total_amount) ?? '0' }}</td>
</tr>
</tbody>
</table>
<p class="mt-20">
- Tổng tiền thanh toán: <strong>{{ number_format($bill->total_amount) ?? '0' }} VND</strong>
({{ $totalText ?? '' }})
</p>
<table class="signature no-border">
<tr>
<td>
Người lập bảng <br>
(, ghi họ tên)
</td>
<td>
Đại diện doanh nghiệp<br>
(, ghi họ tên)
</td>
</tr>
</table>
</body>
</html>

View File

@ -2,6 +2,7 @@
use App\Http\Middleware\CheckPermission;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Modules\Admin\app\Http\Controllers\AdminController;
use Modules\Admin\app\Http\Controllers\BannerController;
use Modules\Admin\app\Http\Controllers\CategoryController;
@ -9,6 +10,7 @@ use Modules\Admin\app\Http\Controllers\ClientController;
use Modules\Admin\app\Http\Controllers\CountryController;
use Modules\Admin\app\Http\Controllers\CustomThemeController;
use Modules\Admin\app\Http\Controllers\DashboardController;
use Modules\Admin\app\Http\Controllers\DocumentController;
use Modules\Admin\app\Http\Controllers\JiraController;
use Modules\Admin\app\Http\Controllers\LeaveManagementController;
use Modules\Admin\app\Http\Controllers\SettingController;
@ -17,9 +19,12 @@ use Modules\Admin\app\Http\Controllers\TimekeepingController;
use Modules\Admin\app\Http\Controllers\TrackingController;
use Modules\Admin\app\Http\Controllers\CriteriasController;
use Modules\Admin\app\Http\Controllers\EvaluationController;
use Modules\Admin\app\Http\Controllers\ProjectReviewController;
use Modules\Admin\app\Http\Controllers\ProfileController;
use Modules\Admin\app\Http\Controllers\TechnicalController;
use Modules\Admin\app\Http\Controllers\TestCaseForSprintController;
use Modules\Admin\app\Http\Controllers\ElectricityBillController;
use Modules\Admin\app\Http\Controllers\EquipmentController;
use Modules\Admin\app\Http\Middleware\AdminMiddleware;
/*
@ -41,6 +46,7 @@ Route::middleware('api')
Route::post('login', [AdminController::class, 'login']);
Route::post('reset-password', [AdminController::class, 'resetPassword']);
Route::get('forgot-password', [AdminController::class, 'forgotPassword']);
Route::get('/email-handle-ticket', [TicketController::class, 'handleTicketEmail'])->name('email.ticket.handle');
});
// NOTE after login
@ -111,21 +117,25 @@ Route::middleware('api')
Route::get('/get-all-sprint-by-id-board', [JiraController::class, 'getAllSprintByIdBoard'])->middleware('check.permission:admin.tester');
Route::get('/get-all-issue-by-id-sprint', [JiraController::class, 'getAllIssueByIdSprint']);
Route::get('/export-weekly-report', [JiraController::class, 'getWeeklyReport']);
Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
Route::get('/issue/detail', [JiraController::class, 'getDetailIssueById'])->middleware('check.permission:admin.staff');
Route::get('/project-participating', [JiraController::class, 'getUserProjectParticipating'])->middleware('check.permission:admin');
});
Route::group([
'prefix' => 'timekeeping',
], function () {
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff');
Route::get('/', [TimekeepingController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/addMutilple', [TimekeepingController::class, 'addWorkingTimeForMultipleUser'])->middleware('check.permission:admin.hr');
Route::post('/addNote', [TimekeepingController::class, 'addNoteForUser'])->middleware('check.permission:admin.hr');
Route::get('/delete', [TimekeepingController::class, 'deleteNote'])->middleware('check.permission:admin.hr');
Route::post('/update-cache-month', [TimekeepingController::class, 'updateCacheMonth'])->middleware('check.permission:admin');
Route::post('/update-working-days', [TimekeepingController::class, 'saveWorkingDays'])->middleware('check.permission:admin.hr');
Route::get('/export', [TimekeepingController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
@ -134,19 +144,24 @@ Route::middleware('api')
Route::post('/create', [TrackingController::class, 'create'])->middleware('check.permission:admin.hr');
Route::post('/update', [TrackingController::class, 'update'])->middleware('check.permission:admin.hr');
Route::get('/delete', [TrackingController::class, 'delete'])->middleware('check.permission:admin.hr');
Route::get('/summary', [TrackingController::class, 'getSummaryTracking'])->middleware('check.permission:admin');
});
Route::group([
'prefix' => 'category',
], function () {
Route::get('/get-list-master', [CategoryController::class, 'getListMaster']);
Route::get('/work-days', [CategoryController::class, 'workDays'])->middleware('check.permission:admin.hr');
Route::put('/update-work-days', [CategoryController::class, 'updateWorkDays'])->middleware('check.permission:admin.hr');
});
Route::group([
'prefix' => 'leave-management',
], function () {
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff');
Route::get('/', [LeaveManagementController::class, 'get'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export', [LeaveManagementController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/saveNoteLeave', [LeaveManagementController::class, 'saveNoteLeave'])->middleware('check.permission:admin.hr');
Route::post('/updateNoteStatus', [LeaveManagementController::class, 'updateNoteStatus'])->middleware('check.permission:admin.hr');
});
Route::group([
@ -154,11 +169,55 @@ Route::middleware('api')
], function () {
Route::get('/all', [TicketController::class, 'getAll'])->middleware('check.permission:admin.hr');
Route::get('/getByUserId', [TicketController::class, 'getByUserId'])->middleware('check.permission:admin.hr.staff');
Route::post('/update', [TicketController::class, 'updateTicket'])->middleware('check.permission:admin.hr');
Route::post('/create', [TicketController::class, 'createTicket'])->middleware('check.permission:admin.hr.staff');
Route::get('/delete', [TicketController::class, 'deleteTicket'])->middleware('check.permission:admin.hr.staff');
Route::post('/handle-ticket', [TicketController::class, 'handleTicket'])->middleware('check.permission:admin');
});
// Electricity Bills
Route::group([
'prefix' => 'electricity-bill',
], function () {
Route::get('/', [ElectricityBillController::class, 'index'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/{id}', [ElectricityBillController::class, 'show'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/create', [ElectricityBillController::class, 'create'])->middleware('check.permission:admin.hr.staff.accountant');
Route::put('/{id}', [ElectricityBillController::class, 'update'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/delete/{id}', [ElectricityBillController::class, 'delete'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export-pdf/{id}', [ElectricityBillController::class, 'exportPdf'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/upload-attachment/{id}', [ElectricityBillController::class, 'uploadAttachment'])->middleware('check.permission:admin.hr.staff.accountant');
Route::delete('/delete-attachment/{id}', [ElectricityBillController::class, 'deleteAttachment'])->middleware('check.permission:admin.hr.staff.accountant');
});
// Equipments
Route::group([
'prefix' => 'equipment',
], function () {
Route::get('/', [EquipmentController::class, 'index'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/users', [EquipmentController::class, 'userOptions'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/user-statistics', [EquipmentController::class, 'userStatistics'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/export', [EquipmentController::class, 'export'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/by-code', [EquipmentController::class, 'showByCode'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/history/{id}', [EquipmentController::class, 'history'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/create', [EquipmentController::class, 'create'])->middleware('check.permission:admin.hr.staff.accountant');
Route::put('/{id}', [EquipmentController::class, 'update'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/{id}/assign', [EquipmentController::class, 'assign'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/{id}/upload-image', [EquipmentController::class, 'uploadImage'])->middleware('check.permission:admin.hr.staff.accountant');
Route::delete('/{id}/image', [EquipmentController::class, 'deleteImage'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/delete/{id}', [EquipmentController::class, 'delete'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
'prefix' => 'profile',
], function () {
Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant');
Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
'prefix' => 'criterias',
], function () {
@ -182,6 +241,11 @@ Route::middleware('api')
Route::get('/sprint-review', [EvaluationController::class, 'sprintReview'])->middleware('check.permission:admin');
Route::get('/technical', [EvaluationController::class, 'technical'])->middleware('check.permission:admin');
Route::get('/report', [EvaluationController::class, 'report'])->middleware('check.permission:admin');
Route::get('/report-all-users', [EvaluationController::class, 'reportAllUsers'])->middleware('check.permission:admin');
Route::get('/project-review', [ProjectReviewController::class, 'getListReviews'])->middleware('check.permission:admin');
Route::post('/project-review/create', [ProjectReviewController::class, 'create'])->middleware('check.permission:admin');
Route::post('/project-review/update', [ProjectReviewController::class, 'update'])->middleware('check.permission:admin');
Route::get('/project-review/delete', [ProjectReviewController::class, 'destroy'])->middleware('check.permission:admin');
});
Route::group([
@ -196,6 +260,32 @@ Route::middleware('api')
Route::get('/get-list-user-by-tech-id/{technicalId}', [TechnicalController::class, 'getListUserByTechnicalId'])->middleware('check.permission:admin');
Route::post('/technicals-user/update', [TechnicalController::class, 'updateTechnicalsUser']);
});
Route::group([
'prefix' => 'document',
], function () {
Route::get('/all', [DocumentController::class, 'all'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/create', [DocumentController::class, 'create'])->middleware('check.permission:admin');
Route::post('/update', [DocumentController::class, 'update'])->middleware('check.permission:admin');
Route::get('/delete', [DocumentController::class, 'delete'])->middleware('check.permission:admin');
});
Route::get('/download-file/{filename}', function ($filename) {
$path = "uploads/{$filename}";
if (!Storage::disk('public')->exists($path)) {
return response()->json(['error' => 'File not found'], 404);
}
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'GET',
'Access-Control-Allow-Headers' => 'Content-Type',
'Content-Disposition' => 'inline; filename="' . $filename . '"',
];
return response()->file(storage_path("app/public/{$path}"), $headers);
});
});
});
@ -212,4 +302,5 @@ Route::group([
'prefix' => 'v1/admin/jira',
], function () {
Route::get('/send-worklog-report', [JiraController::class, 'sendReport']);
Route::get('/send-warning-mail', [JiraController::class, 'sendWarningMailByAllowcation']);
});

View File

@ -3,18 +3,23 @@
namespace Modules\Auth\app\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\LeaveDays;
use App\Traits\IsAPI;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Modules\Auth\app\Models\User;
use Illuminate\Support\Str;
use Modules\Admin\app\Models\Category;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
use App\Traits\AnalyzeData;
class UserController extends Controller
{
use IsAPI;
use AnalyzeData;
public function __construct()
{
$this->middleware('jwt.auth');
@ -34,9 +39,41 @@ class UserController extends Controller
]);
if ($request->has('id')) {
$payload = $request->only(['name', 'email', 'permission']);
$payload = $request->only(['name', 'email', 'permission', 'is_permanent', 'is_separated']);
$user = User::find($request->id);
// Không cho chuyển từ chính thức thành lại thử việc
if (!$request->is_permanent && $user->is_permanent) {
return response()->json(['status' => false, 'message' => 'You cannot change an employee from permanent to probationary.']);
}
if (!$request->is_separated && $user->is_separated) {
return response()->json(['status' => false, 'message' => 'You cannot change status of separated employee.']);
}
// Thêm ngày phép khi thành nhân viên chính thức
if ($request->is_permanent && !$user->is_permanent) {
$userLeaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', Carbon::now()->year)
->first();
if ($userLeaveDay) {
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
$userLeaveDay->ld_day_total = $permanentDefault;
$newNote = "Cộng ngày phép cho nhân viên chính thức"; // Thêm ghi chú
if (!empty($userLeaveDay->ld_note)) {
$userLeaveDay->ld_note = $userLeaveDay->ld_note . "\n" . $newNote;
} else {
$userLeaveDay->ld_note = $newNote;
}
$userLeaveDay->save();
}
$payload['permanent_date'] = Carbon::now()->toDateString();
}
$user->update($payload);
return response()->json(['data' => $user, 'status' => true, 'message' => 'Update successful']);
} else {
@ -44,9 +81,23 @@ class UserController extends Controller
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt('Work@1234'),
'permission' => $request->permission
'permission' => $request->permission,
'is_permanent' => false,
'is_separated' => false
]);
// Khởi tạo LeaveDays cho nhân viên mới
LeaveDays::insert([
'ld_user_id' => $user->id,
'ld_day_total' => 0,
'ld_year' => Carbon::now()->year,
'ld_additional_day' => 0,
'ld_note' => '',
'created_at' => now(),
'updated_at' => now(),
]);
$this->createOrUpdateRecordForCurrentMonth(Carbon::now()->month, Carbon::now()->year);
$user_res = [
'name' => $user->name,
'email' => $user->email,
@ -98,8 +149,6 @@ class UserController extends Controller
return response()->json(['data' => ['user' => $user_res, 'gitea' => "dev", 'zulip' => "dev"], 'status' => true, 'message' => 'Create successful']);
}
}
return response()->json(['status' => false, 'message' => 'Process fail']);
}
public function delete(Request $request)

View File

@ -25,7 +25,10 @@ class User extends Authenticatable implements JWTSubject
'name',
'email',
'password',
'permission'
'permission',
'is_permanent',
'is_separated',
'permanent_date'
];
/**

View File

@ -0,0 +1,24 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\AddMonthlyLeaveDays;
class AddMonthlyLeaveDaysCommand extends Command
{
protected $signature = 'add:monthly-leavedays {month?} {year?}';
protected $description = 'Cộng 1 ngày phép hàng tháng cho tất cả người dùng';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$month = $this->argument('month');
$year = $this->argument('year');
AddMonthlyLeaveDays::dispatch($month, $year);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Jobs\CheckUserAttendanceJob;
use Illuminate\Console\Command;
class CheckUserAttendanceCommand extends Command
{
// Định nghĩa command signature
protected $signature = 'attendance:check {period?}';
protected $description = 'Kiểm tra check in và check out của người dùng và tạo ticket nếu thiếu';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
// Lấy argument period (Sáng "S" hoặc Chiều "C")
$period = $this->argument('period');
// Dispatch job để kiểm tra check in và check out
CheckUserAttendanceJob::dispatch($period);
}
}

View File

@ -8,7 +8,7 @@ use App\Jobs\InitializeLeaveDays;
class InitializeLeaveDaysCommand extends Command
{
protected $signature = 'initialize:leavedays {year?}';
protected $description = 'Initialize leave days for users';
protected $description = 'Cấp phép năm cho tất cả người dùng';
public function __construct()
{
@ -17,7 +17,6 @@ class InitializeLeaveDaysCommand extends Command
public function handle()
{
$year = $this->argument('year');
InitializeLeaveDays::dispatch($year);
InitializeLeaveDays::dispatch();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\UpdateTemporaryLeaveDays;
class UpdateTemporaryLeaveDaysCommand extends Command
{
protected $signature = 'update:temporary-leavedays {month?} {year?}';
protected $description = 'Tính lại ngày phép cho các note tạm.';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$month = $this->argument('month');
$year = $this->argument('year');
UpdateTemporaryLeaveDays::dispatch($month, $year);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Console;
use App\Jobs\DeductLeaveDays;
use App\Jobs\AddMonthlyLeaveDays;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -23,9 +24,21 @@ class Kernel extends ConsoleKernel
// $schedule->command('daily:api-call')
// ->dailyAt('18:00');
// Chạy command vào ngày 31/12 lúc 23:59:59 mỗi năm
$schedule->command('initialize:leavedays')->yearlyOn(12, 31, '23:59:59');
$schedule->command('leave:deduct')->yearlyOn(3, 31, '23:59:59');
// Chạy command vào ngày 01/01 lúc 00:00 mỗi năm
$schedule->command('initialize:leavedays')->yearlyOn(1, 1, '00:00');
// Chạy command vào ngày 01/04 lúc 00:00 mỗi năm
$schedule->command('leave:deduct')->yearlyOn(4, 1, '00:00');
// Chạy buổi sáng lúc 12:00
$schedule->command('attendance:check S')->dailyAt('12:00');
// Chạy buổi chiều lúc 17:30
$schedule->command('attendance:check C')->dailyAt('17:30');
// Chạy vào ngày đầu tiên của mỗi tháng
$schedule->command('add:monthly-leavedays')->monthlyOn(1, '00:01');
$schedule->command('update:temporary-leavedays')->monthlyOn(1, '00:05');
}
/**

View File

@ -0,0 +1,194 @@
<?php
namespace App\Exports;
use Carbon\Carbon;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class LeaveManagementExport implements FromArray, WithHeadings, WithStyles, WithEvents
{
protected $data;
protected $year;
public function __construct($data)
{
$this->data = $data;
$this->year = Carbon::now()->year;
}
public function headings(): array
{
$months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return array_merge(
['No.', 'User'],
$months,
['Total', 'Off', 'Remaining', 'Notes']
);
}
public function array(): array
{
$headers = $this->headings(); // Lấy tiêu đề
$rows = [];
$stt = 0;
foreach ($this->data as $index => $user) {
$totalDayOff = 0;
$totalDayLeave = $user['leaveDay']['ld_day_total'] + $user['leaveDay']['ld_additional_day'];
// Tính tổng ngày nghỉ theo tháng
$monthlyLeaves = array_fill(1, 12, 0);
foreach ($user['monthlyLeaveDays'] as $leaveDay) {
$monthlyLeaves[$leaveDay['month']] += $leaveDay['leave_days'];
$totalDayOff += $leaveDay['leave_days'];
}
// Tạo dòng dữ liệu
$row = [
$stt + 1,
$user['user']['name']
];
// Thêm dữ liệu các tháng
for ($month = 1; $month <= 12; $month++) {
$row[] = $monthlyLeaves[$month] ?: '';
}
// Thêm tổng số ngày
$row[] = $totalDayLeave;
$row[] = $totalDayOff;
$row[] = $totalDayLeave - $totalDayOff;
$row[] = $user['leaveDay']['ld_note'] ?? '';
$rows[] = $row;
$stt++;
}
return array_merge([$headers], $rows); // Thêm tiêu đề vào đầu mảng
}
public function registerEvents(): array
{
return [
AfterSheet::class => function(AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastRow = count($this->data) + 2;
$noteColumn = 'R'; // Cột Notes
// Xử lý đặc biệt cho cột Notes
$sheet->getStyle("{$noteColumn}3:{$noteColumn}{$lastRow}")
->getAlignment()
->setWrapText(true)
->setVertical(Alignment::VERTICAL_TOP)
->setHorizontal(Alignment::HORIZONTAL_LEFT);
// Tắt auto-size cho cột Notes và set độ rộng cố định
$sheet->getColumnDimension($noteColumn)
->setAutoSize(false)
->setWidth(60);
// Tự động điều chỉnh chiều cao cho từng dòng có nội dung
for ($row = 3; $row <= $lastRow; $row++) {
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
if (!empty($cellValue)) {
// Tính toán chiều cao dựa trên nội dung
$sheet->getRowDimension($row)->setRowHeight(-1);
// Tính toán lại chiều cao dựa trên số dòng trong nội dung
$lineCount = substr_count($cellValue, "\n") + 1;
$minHeight = max(30, $lineCount * 15); // 15 pixels cho mỗi dòng
// Lấy chiều cao hiện tại sau khi auto-size
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
// Nếu chiều cao tự động nhỏ hơn chiều cao tối thiểu, sử dụng chiều cao tối thiểu
if ($currentHeight < $minHeight) {
$sheet->getRowDimension($row)->setRowHeight($minHeight);
}
} else {
$sheet->getRowDimension($row)->setRowHeight(30);
}
}
// Refresh các tính toán của Excel
$sheet->calculateColumnWidths();
},
];
}
public function styles(Worksheet $sheet)
{
$lastRow = count($this->data) + 2;
$lastColumn = 'R';
// Thêm và style title
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', "DANH SÁCH NGÀY NGHỈ NĂM {$this->year}");
$sheet->getStyle("A1:{$lastColumn}1")->applyFromArray([
'font' => [
'bold' => true,
'size' => 14
],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Style cho header (dời xuống row 2)
$sheet->getStyle("A2:{$lastColumn}2")->applyFromArray([
'font' => ['bold' => true],
'alignment' => [
'horizontal' => Alignment::HORIZONTAL_CENTER,
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Style cho toàn bộ bảng (bắt đầu từ row 1)
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray([
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN
]
],
'alignment' => [
'vertical' => Alignment::VERTICAL_CENTER
]
]);
// Căn giữa cho các cột số liệu (điều chỉnh range bắt đầu từ row 3)
for ($col = 'C'; $col <= 'P'; $col++) {
$sheet->getStyle("{$col}3:{$col}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER);
}
// Set độ rộng cột
$sheet->getColumnDimension('A')->setWidth(5); // No.
$sheet->getColumnDimension('B')->setWidth(30); // User
// Các tháng
for ($i = 'C'; $i <= 'N'; $i++) {
$sheet->getColumnDimension($i)->setWidth(8);
}
$sheet->getColumnDimension('O')->setWidth(8); // Total
$sheet->getColumnDimension('P')->setWidth(8); // Off
$sheet->getColumnDimension('Q')->setWidth(12); // Remaining
$sheet->getColumnDimension('R')->setWidth(30); // Notes
// Điều chỉnh style cho cột Notes
$sheet->getStyle("R3:R{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_LEFT)
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
return $sheet;
}
}

View File

@ -0,0 +1,248 @@
<?php
namespace App\Exports;
use Carbon\Carbon;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
class TimekeepingExport implements FromArray, WithHeadings, WithStyles, WithEvents
{
protected $data;
protected $month;
protected $year;
protected $workingDays;
protected $daysInMonth;
public function __construct($data, $month, $year, $workingDays)
{
$this->data = $data;
$this->month = $month;
$this->year = $year;
$this->workingDays = $workingDays;
$this->daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
}
public function registerEvents(): array
{
return [
AfterSheet::class => function(AfterSheet $event) {
$sheet = $event->sheet->getDelegate();
$lastRow = count($this->data) + 4;
$noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
// Xử lý đặc biệt cho cột Notes
$sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
->getAlignment()
->setWrapText(true)
->setVertical(Alignment::VERTICAL_TOP)
->setHorizontal(Alignment::HORIZONTAL_LEFT);
// Tắt auto-size cho cột Notes và set độ rộng cố định
$sheet->getColumnDimension($noteColumn)
->setAutoSize(false)
->setWidth(60);
// Tự động điều chỉnh chiều cao cho từng dòng có nội dung
for ($row = 5; $row <= $lastRow; $row++) {
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
if (!empty($cellValue)) {
// Tính toán chiều cao dựa trên nội dung
$sheet->getRowDimension($row)->setRowHeight(-1);
// Tính toán lại chiều cao dựa trên số dòng trong nội dung
$lineCount = substr_count($cellValue, "\n") + 1;
$minHeight = max(30, $lineCount * 15); // 15 pixels cho mỗi dòng
// Lấy chiều cao hiện tại sau khi auto-size
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
// Nếu chiều cao tự động nhỏ hơn chiều cao tối thiểu, sử dụng chiều cao tối thiểu
if ($currentHeight < $minHeight) {
$sheet->getRowDimension($row)->setRowHeight($minHeight);
}
} else {
$sheet->getRowDimension($row)->setRowHeight(30);
}
}
// Refresh các tính toán của Excel
$sheet->calculateColumnWidths();
},
];
}
public function array(): array
{
// Lấy headers
$headers = $this->headings();
// Lấy dữ liệu người dùng
$userRows = [];
foreach ($this->data as $user) {
$row = [
$user['user']['name'] ?? 'Unknown',
0, // Total days
$this->workingDays, // Off days (initialize with working days)
];
$totalDays = 0;
// Add data for each day in month
for ($day = 1; $day <= $this->daysInMonth; $day++) {
$dayData = '';
if (isset($user['history'])) {
foreach ($user['history'] as $history) {
if ($history['day'] === $day) {
$total = $history['total'] ?? 0;
if ($total >= 7 * 3600) {
$dayData = '1';
$totalDays += 1;
} else if ($total >= 3.5 * 3600) {
$dayData = '0.5';
$totalDays += 0.5;
}
break;
}
}
}
$row[] = $dayData;
}
// Update total and off days
$row[1] = $totalDays;
$row[2] = $this->workingDays - $totalDays;
// Add Notes column with formatted content
$notes = [];
if (isset($user['history'])) {
foreach ($user['history'] as $history) {
if (!empty($history['notes'])) {
$dayNotes = [];
foreach ($history['notes'] as $note) {
$dayNotes[] = "- {$note['reasonName']} ({$note['timeTypeName']}): {$note['note']}";
}
if (!empty($dayNotes)) {
$notes[] = "Day {$history['day']}:\n" . implode("\n", $dayNotes);
}
}
}
}
$row[] = !empty($notes) ? implode("\n\n", $notes) : '';
$userRows[] = $row;
}
return array_merge($headers, $userRows);
}
public function headings(): array
{
$firstRow = ['Day', '', ''];
// Second row: Day of week
$secondRow = ['', '', ''];
$date = Carbon::create($this->year, $this->month, 1);
for ($day = 1; $day <= $this->daysInMonth; $day++) {
$firstRow[] = $day;
$secondRow[] = $date->format('D');
$date->addDay();
}
// Add Notes column
$firstRow[] = 'Notes';
$secondRow[] = '';
return [$firstRow, $secondRow];
}
public function styles(Worksheet $sheet)
{
$lastColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
$lastRow = count($this->data) + 4;
$noteColumn = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($this->daysInMonth + 4);
// Title and working days
$sheet->mergeCells("A1:{$lastColumn}1");
$sheet->setCellValue('A1', "DANH SÁCH CHẤM CÔNG THÁNG {$this->month} NĂM {$this->year}");
$sheet->mergeCells("A2:{$lastColumn}2");
$sheet->setCellValue('A2', "Số ngày làm việc: {$this->workingDays}");
// Merge cells for "Day" title and set value
$sheet->mergeCells("A3:C3");
$sheet->setCellValue('A3', 'Day');
// Set values for A4, B4, C4
$sheet->setCellValue('A4', 'User');
$sheet->setCellValue('B4', 'Total');
$sheet->setCellValue('C4', 'Off');
// Styling
$sheet->getStyle("A1:{$lastColumn}1")->getFont()->setBold(true)->setSize(14);
$sheet->getStyle("A2:{$lastColumn}2")->getFont()->setBold(true);
$sheet->getStyle("A3:{$lastColumn}4")->getFont()->setBold(true);
// Border style
$borderStyle = [
'borders' => [
'allBorders' => [
'borderStyle' => Border::BORDER_THIN,
],
],
];
// Apply borders to the data area
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")->applyFromArray($borderStyle);
// Center align all cells except Notes column
$sheet->getStyle("A1:{$lastColumn}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER);
// Left align Notes column và bật wrap text
$sheet->getStyle("{$noteColumn}5:{$noteColumn}{$lastRow}")
->getAlignment()
->setHorizontal(Alignment::HORIZONTAL_LEFT)
->setVertical(Alignment::VERTICAL_TOP)
->setWrapText(true);
// Set width for Note column - tăng độ rộng để hiển thị tốt hơn
$sheet->getColumnDimension($noteColumn)->setWidth(60);
// Tự động điều chỉnh chiều cao cho các dòng có nội dung Notes
for ($row = 5; $row <= $lastRow; $row++) {
$cellValue = $sheet->getCell($noteColumn . $row)->getValue();
if (!empty($cellValue)) {
// Đết chiều cao tự động
$sheet->getRowDimension($row)->setRowHeight(-1);
// Đảm bảo chiều cao tối thiểu
$currentHeight = $sheet->getRowDimension($row)->getRowHeight();
if ($currentHeight < 30) {
$sheet->getRowDimension($row)->setRowHeight(30);
}
} else {
// Chiều cao mặc định cho các dòng không có note
$sheet->getRowDimension($row)->setRowHeight(30);
}
}
// Set column widths
$sheet->getColumnDimension('A')->setWidth(30);
$sheet->getColumnDimension('B')->setWidth(10);
$sheet->getColumnDimension('C')->setWidth(10);
for ($i = 4; $i <= $this->daysInMonth + 3; $i++) {
$sheet->getColumnDimension(\PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i))->setWidth(5);
}
return [];
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace App\Jobs;
use App\Models\LeaveDays;
use App\Models\Notes;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Modules\Admin\app\Models\Category;
use Illuminate\Support\Facades\DB;
class AddMonthlyLeaveDays implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $month;
protected $year;
private const ONLEAVE_PER_MONTH = 1; // Ngày phép cộng mỗi tháng
public function __construct($month = null, $year = null)
{
$this->month = $month ?? Carbon::now()->month;
$this->year = $year ?? Carbon::now()->year;
}
public function handle(): void
{
$users = User::get();
foreach ($users as $user) {
// Nếu là nhân viên chưa chính thức, ko cộng phép
if (!$user->is_permanent) {
continue;
}
// Nếu là nhân viên nghỉ việc, ko cộng phép
if ($user->is_separated) {
continue;
}
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
if (!$leaveDay && $this->month > 1) {
// Nếu chưa có dữ liệu năm hiện tại, tạo mới
$previousYearData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year - 1)
->first();
$ld_additional_day = 0;
$ld_note = '';
if ($previousYearData) {
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day + $previousYearData->ld_special_leave_day;
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->select(
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_year as year'),
DB::raw('SUM(categories.c_value) as leave_days')
)
->where('notes.n_year', $this->year - 1)
->where('notes.n_user_id', $user->id)
->where('notes.n_reason', 'ONLEAVE')
->groupBy(DB::raw('notes.n_year'))
->first();
if ($totalLeaveDaysByMonth) {
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
if ($ld_additional_day < 0) {
$ld_additional_day = 0;
}
}
if ($ld_additional_day > 0) {
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
}
}
$leaveDay = new LeaveDays([
'ld_user_id' => $user->id,
'ld_day_total' => $this->month, // Số ngày phép bằng tháng hiện tại
'ld_year' => $this->year,
'ld_additional_day' => $ld_additional_day,
'ld_note' => $ld_note,
'ld_special_leave_day' => 0,
]);
$leaveDay->save();
} else {
// Check có phải là nhân viên chính thức trong năm nay (Nhân viên mới)
if ($user->permanent_date && $user->permanent_date !== '0000-00-00') {
$permenantYear = Carbon::parse($user->permanent_date)->year;
if ($permenantYear === $this->year) {
$permanentCategory = Category::where('c_type', 'PERMANENT_ONLEAVE')->where('c_code', "PERMANENT")->first();
$permanentDefault = (int) $permanentCategory->c_value; // Ngày phép khi thành nv chính thức
$permanentMonth = Carbon::parse($user->permanent_date)->month;
if ($this->month > $leaveDay->ld_day_total - ($permanentDefault - $permanentMonth)) {
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
$leaveDay->save();
}
}
}
// Kiểm tra nếu số ngày phép hiện tại nhỏ hơn tháng hiện tại (Nhân viên cũ)
if ($leaveDay->ld_day_total < $this->month) {
// Cộng mỗi tháng 1 ngày phép cho nhân viên
$leaveDay->ld_day_total += self::ONLEAVE_PER_MONTH;
$leaveDay->save();
}
}
}
}
}

View File

@ -0,0 +1,207 @@
<?php
namespace App\Jobs;
use App\Mail\TicketMail;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Modules\Admin\app\Http\Controllers\CategoryController;
use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Ticket;
use Modules\Admin\app\Models\Tracking;
class CheckUserAttendanceJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $period;
/**
* Create a new job instance.
* @param string|null $period
*/
public function __construct($period = null)
{
$this->period = $period;
}
/**
* Execute the job.
*/
public function handle()
{
// Lấy tất cả người dùng
$users = User::where('permission', 'not like', '%admin%')
->where('permission', 'not like', '%accountant%')->get();
foreach ($users as $key => $user) {
// Check nhân viên nghỉ việc
if ($user->is_separated) {
continue;
}
// Kiểm tra dựa trên period (Sáng 'S' hoặc Chiều 'C')
if ($this->period === 'S') {
$this->checkMorning($user);
} elseif ($this->period === 'C') {
$this->checkAfternoon($user);
} else {
if ($this->period == null) {
// Nếu không có period, kiểm tra cả sáng và chiều
$this->checkMorning($user);
$this->checkAfternoon($user);
}
}
}
}
private function checkAttendance($user, $periodCode, $startTime, $endTime)
{
$today = Carbon::today();
$userId = $user->id;
// Lấy tất cả tracking của user trong khoảng thời gian được chỉ định
$records = Tracking::where('user_id', $userId)
->whereBetween('time_string', [$startTime, $endTime])
->get();
$hasCheckIn = $records->where('status', 'check in')->isNotEmpty();
$hasCheckOut = $records->where('status', 'check out')->isNotEmpty();
// Kiểm tra nếu đã có ticket chờ xử lý hoặc đã được xác nhận, cho cả sáng và chiều
$existingTicket = Ticket::where('user_id', $userId)
->where(function ($query) use ($today, $periodCode) {
// Check for tickets that exactly cover the current day and period
$query->where(function ($subQuery) use ($today, $periodCode) {
$subQuery->where('start_date', $today->format('Y-m-d'))
->where('end_date', $today->format('Y-m-d'))
->where(function ($periodQuery) use ($periodCode) {
// Check if the ticket covers the current period
$periodQuery->where('start_period', $periodCode)
->orWhere(function ($query) {
// Check for a full-day ticket (S -> C)
$query->where('start_period', 'S')
->where('end_period', 'C');
});
});
})
// Check for tickets that span multiple days
->orWhere(function ($subQuery) use ($today, $periodCode) {
$subQuery->where('start_date', '<=', $today->format('Y-m-d'))
->where('end_date', '>=', $today->format('Y-m-d'))
->where(function ($periodQuery) use ($periodCode, $today) {
$periodQuery->where(function ($query) use ($today) {
$query->where('start_date', '<', $today->format('Y-m-d'))
->where('end_date', '>', $today->format('Y-m-d'));
})
->orWhere(function ($query) use ($today, $periodCode) {
$query->where(function ($queryDateStart) use ($today, $periodCode) {
$queryDateStart->where('start_date', '=', $today->format('Y-m-d'))
->where('end_date', '>', $today->format('Y-m-d'))
->where(function ($queryDateStartChild) use ($periodCode) {
$queryDateStartChild->where('start_period', $periodCode)
->orWhere(function ($query) {
$query->where('start_period', 'S');
});
});
})
->orWhere(function ($queryDateEnd) use ($today, $periodCode) {
$queryDateEnd->where('end_date', '=', $today->format('Y-m-d'))
->where('start_date', '<', $today->format('Y-m-d'))
->where(function ($queryDateStartChild) use ($periodCode) {
$queryDateStartChild->where('end_period', $periodCode)
->orWhere(function ($query) {
$query->where('end_period', 'C');
});
});
});
});
});
});
})
->whereIn('status', ['WAITING', 'CONFIRMED'])
->first();
$type = 'ONLEAVE';
$reason = 'KHONG PHEP';
//Check ngày hợp lệ nếu có check và check out của user khác
$dateNow = Tracking::whereBetween('time_string', [$startTime, $endTime])->exists();
// Nếu không có check in/out, không gửi ticket và có check in/out của người khác => tạo ticket không phép
if ($hasCheckIn || $hasCheckOut) {
return;
}
if ($existingTicket) {
return;
}
if ($dateNow) {
$ticket = Ticket::create([
'user_id' => $userId,
'start_date' => $today->format('Y-m-d'),
'start_period' => $periodCode,
'end_date' => $today->format('Y-m-d'),
'end_period' => $periodCode,
'type' => $type,
'reason' => $reason,
'status' => 'WAITING',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
//Send mail
$dataMasterStartPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
$dataMasterEndPeriod = CategoryController::getListMasterByCodeAndType("TIME_TYPE", $periodCode);
$dataMasterType = CategoryController::getListMasterByCodeAndType("REASON", $type);
$formattedStartDate = $today->format('d/m/Y');
$formattedEndDate = $today->format('d/m/Y');
$admins = Admin::where('permission', 'like', '%admin%')->get();
foreach ($admins as $key => $value) {
$data = array(
"ticket_id" => $ticket->id,
"email_template" => "email.notification_tickets",
"email" => $user->email,
"admin_email" => $value->email,
"name" => $user->name,
"date" => $dataMasterStartPeriod->c_name . " (" . $formattedStartDate . ") - " . $dataMasterEndPeriod->c_name . " (" . $formattedEndDate . ")",
"type" => $dataMasterType->c_name,
"note" => $reason,
"link" => "/tickets-management", //link đến page admin
"subject" => "[Ticket request] Ticket From " . $user->name
);
Mail::to($value->email)->send(new TicketMail($data));
}
}
}
/**
* Kiểm tra check-in/check-out buổi sáng tạo ticket nếu thiếu.
* @param int $userId
*/
public function checkMorning($user)
{
$startTime = Carbon::today()->setTime(6, 0); // Thời gian bắt đầu buổi sáng
$endTime = Carbon::createFromTime(12, 0); // Thời gian kết thúc buổi sáng
$this->checkAttendance($user, 'S', $startTime, $endTime);
}
/**
* Kiểm tra check-in/check-out buổi chiều tạo ticket nếu thiếu.
* @param int $userId
*/
public function checkAfternoon($user)
{
$startTime = Carbon::today()->setTime(12, 0); // Thời gian bắt đầu buổi chiều
$endTime = Carbon::createFromTime(17, 30); // Thời gian kết thúc buổi chiều
$this->checkAttendance($user, 'C', $startTime, $endTime);
}
}

View File

@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class DeductLeaveDays implements ShouldQueue
@ -36,44 +35,36 @@ class DeductLeaveDays implements ShouldQueue
foreach ($users as $user) {
$existingData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->where('ld_date_additional', ">", 0)
->where('ld_additional_day', ">", 0)
->first();
if (!$existingData) {
continue;
}
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
// Lấy tổng ngày nghỉ phép 3 tháng đầu trong năm
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->select(
DB::raw('notes.n_user_id as n_user_id'),
DB::raw('notes.n_year as year'),
DB::raw('SUM(categories.c_value) as leave_days')
)
->where('notes.n_year', $this->year)
->where('notes.n_user_id', $user->id)
->where('notes.n_reason', 'ONLEAVE')
->groupBy(DB::raw('notes.n_year'))
->first();
if ($totalLeaveDaysByMonth) {
//Nếu ngày phép thừa năm trước chưa sử dụng hết => cập nhật lại ngày đó (Ngày tồn đọng - ngày sử dụng)
if ($existingData->ld_date_additional > $totalLeaveDaysByMonth->leave_days) {
LeaveDays::where('ld_year', $this->year)
->where('ld_user_id', $user->id)
->update([
'ld_date_additional' => $totalLeaveDaysByMonth->leave_days,
]);
->where('n_user_id', $user->id)
->where('n_year', $this->year)
->where('n_month', "<=", 3)
->where('n_reason', 'ONLEAVE')
->sum('categories.c_value');
if ($usedOnleaveDaysTotal) {
if ($existingData->ld_additional_day > $usedOnleaveDaysTotal) {
$ld_note = "Trừ " . $existingData->ld_additional_day - $usedOnleaveDaysTotal . " ngày phép tồn năm trước. \n";
$existingData->ld_note = $existingData->ld_note . $ld_note;
$existingData->ld_additional_day = $usedOnleaveDaysTotal;
}
} else {
//Nếu không sử dụng ngày nghỉ còn lại ở nằm rồi thì xóa => theo luật ld
LeaveDays::where('ld_year', $this->year)
->where('ld_user_id', $user->id)
->update([
'ld_date_additional' => "0",
]);
$ld_note = "Trừ " . $existingData->ld_additional_day . " ngày phép tồn năm trước. \n";
$existingData->ld_note = $existingData->ld_note . $ld_note;
$existingData->ld_additional_day = 0;
}
$existingData->save();
}
}
}

View File

@ -34,15 +34,11 @@ class InitializeLeaveDays implements ShouldQueue
public function handle(): void
{
$users = User::get();
$ld_day = 12;
foreach ($users as $user) {
// Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa
$existingData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
$ld_day_total = Carbon::now()->month; // Khởi tạo phép hiện có bằng tháng hiện tại
if ($existingData) {
// Nếu dữ liệu đã tồn tại, bỏ qua user này
foreach ($users as $user) {
// Check nhân viên nghỉ việc
if ($user->is_separated) {
continue;
}
@ -51,11 +47,11 @@ class InitializeLeaveDays implements ShouldQueue
->where('ld_year', $this->year - 1)
->first();
$ld_date_additional = 0;
$ld_additional_day = 0;
$ld_note = '';
if ($previousYearData) {
$ld_date_additional = $previousYearData->ld_day + $previousYearData->ld_date_additional;
$ld_additional_day = $previousYearData->ld_day_total + $previousYearData->ld_additional_day + $previousYearData->ld_special_leave_day;
$totalLeaveDaysByMonth = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
@ -71,20 +67,36 @@ class InitializeLeaveDays implements ShouldQueue
->groupBy(DB::raw('notes.n_year'))
->first();
if ($totalLeaveDaysByMonth) {
$ld_date_additional = $ld_date_additional - $totalLeaveDaysByMonth->leave_days;
if ($ld_date_additional < 0) {
$ld_date_additional = 0;
$ld_additional_day = $ld_additional_day - $totalLeaveDaysByMonth->leave_days;
if ($ld_additional_day < 0) {
$ld_additional_day = 0;
}
}
$ld_note = 'Cộng dồn ngày phép năm cũ';
if ($ld_additional_day > 0) {
$ld_note = "Cộng " . $ld_additional_day . " ngày phép tồn năm trước. \n";
}
}
// Kiểm tra xem dữ liệu của user này đã tồn tại cho năm hiện tại chưa
$existingData = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
if ($existingData) {
// Nếu dữ liệu đã tồn tại, update lại phép tồn
$existingData->ld_note = $ld_note;
$existingData->ld_additional_day = $ld_additional_day;
$existingData->save();
continue;
}
// Tạo dữ liệu cho năm hiện tại
LeaveDays::insert([
'ld_user_id' => $user->id,
'ld_day' => $ld_day,
'ld_day_total' => $user->is_permanent ? $ld_day_total : 0, // Nếu là nhân viên mới, ko cấp phép
'ld_year' => $this->year,
'ld_date_additional' => $ld_date_additional,
'ld_additional_day' => $ld_additional_day,
'ld_note' => $ld_note,
'created_at' => now(),
'updated_at' => now(),

View File

@ -0,0 +1,220 @@
<?php
namespace App\Jobs;
use App\Models\LeaveDays;
use App\Models\Notes;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Modules\Admin\app\Models\Category;
class UpdateTemporaryLeaveDays implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $month;
protected $year;
public function __construct($month = null, $year = null)
{
$this->month = $month ?? Carbon::now()->month;
$this->year = $year ?? Carbon::now()->year;
}
/**
* Execute the job.
*/
public function handle()
{
$users = User::get();
foreach ($users as $user) {
$leaveDay = LeaveDays::where('ld_user_id', $user->id)
->where('ld_year', $this->year)
->first();
$notes = Notes::where('n_reason', 'TEMPORARY_ONLEAVE')
->where('n_user_id', $user->id)
->where('n_year', $this->year)
->where('n_month', $this->month)
->whereExists(function ($query) use ($user) {
$query->select(DB::raw(1))
->from('tickets')
->where('tickets.user_id', $user->id)
->where('tickets.status', 'CONFIRMED')
->where('tickets.type', 'ONLEAVE');
})
->get();
$maxDaysPerMonth = $this->getMaxLeaveDaysPerMonth();
// Tổng ngày nghỉ sẽ dùng trong tháng
$willUsedDaysInMonth = 0;
foreach ($notes as $note) {
$willUsedDaysInMonth += $note->n_time_type == 'ALL' ? 1.0 : 0.5;
}
// Tổng phép đang có
$onleaveDaysTotal = $leaveDay->ld_day_total + $leaveDay->ld_additional_day + $leaveDay->ld_special_leave_day;
// Phép đã sử dụng tới tháng hiện tại
$usedOnleaveDaysTotal = Notes::join('categories', function ($join) {
$join->on('notes.n_time_type', '=', 'categories.c_code')
->where('categories.c_type', 'TIME_TYPE');
})
->where('n_user_id', $user->id)
->where('n_year', $this->year)
->where('n_month', "<=", $this->month)
->where('n_reason', 'ONLEAVE')
->sum('categories.c_value');
// Phép còn lại
$remainingOnleaveDays = $onleaveDaysTotal - $usedOnleaveDaysTotal;
// Log::debug("User {$user->name}\n");
// Log::debug(
// "📊 Thống kê ngày phép:\n" .
// " - Tháng: {$this->month}\n" .
// " - Tổng ngày nghỉ sẽ dùng trong tháng: $willUsedDaysInMonth\n" .
// " - Tổng ngày phép: $onleaveDaysTotal\n" .
// " - Tổng ngày phép đã nghỉ: $usedOnleaveDaysTotal\n" .
// " - Tổng ngày phép còn lại: $remainingOnleaveDays\n"
// );
$onleave_days_will_use = 0; // Ngày phép sẽ dùng
$nopay_days_will_use = 0; // Ngày ko phép sẽ dùng
// Ngày phép còn lại <= 0 (Hết phép)
if ($remainingOnleaveDays <= 0) {
$onleave_days_will_use = 0;
$nopay_days_will_use = $willUsedDaysInMonth;
Log::debug("--- Hết phép trong tháng ---");
}
// Ngày phép còn lại < ngày yêu cầu (Không đủ phép)
else if ($remainingOnleaveDays < $willUsedDaysInMonth) {
// Vượt limit
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
// Phép còn lại > limit
if ($remainingOnleaveDays > $maxDaysPerMonth) {
$onleave_days_will_use = $maxDaysPerMonth;
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
}
// Phép còn lại < limit
else {
$onleave_days_will_use = $remainingOnleaveDays;
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
}
Log::debug("--- Không đủ phép trong tháng, vượt quá limit ---",);
}
// Không vượt limit
else {
$onleave_days_will_use = $remainingOnleaveDays;
$nopay_days_will_use = $willUsedDaysInMonth - $remainingOnleaveDays;
Log::debug("--- Không đủ phép trong tháng, ko vượt limit ---");
}
}
// Ngày phép còn lại >= ngày yêu cầu (Đủ phép)
else {
// Vượt limit
if ($willUsedDaysInMonth > $maxDaysPerMonth) {
$onleave_days_will_use = $maxDaysPerMonth;
$nopay_days_will_use = $willUsedDaysInMonth - $maxDaysPerMonth;
Log::debug("--- Đủ phép, vượt limit ---");
}
// Không vượt limit
else {
$onleave_days_will_use = $willUsedDaysInMonth;
$nopay_days_will_use = 0;
Log::debug("--- Đủ phép ---");
}
}
Log::debug("", [
"Phep" => $onleave_days_will_use,
"Khong Phep" => $nopay_days_will_use
]);
// Có nghỉ không phép
if ($nopay_days_will_use > 0) {
foreach ($notes as $note) {
$value = ($note->n_time_type === 'ALL') ? 1.0 : 0.5;
if ($note->n_time_type === 'ALL' && $onleave_days_will_use == 0.5) {
// Chỉ còn 0.5 phép, chia thành 2 bản ghi: 1 phép, 1 không phép
// Ưu tiên phép cho buổi sáng (S), không phép cho buổi chiều (C)
Notes::create([
'n_user_id' => $user->id,
'n_day' => $note->n_day,
'n_month' => $note->n_month,
'n_year' => $note->n_year,
'n_time_type' => 'S',
'n_reason' => 'ONLEAVE',
'n_note' => $note->n_note,
'ticket_id' => $note->ticket_id
]);
Notes::create([
'n_user_id' => $user->id,
'n_day' => $note->n_day,
'n_month' => $note->n_month,
'n_year' => $note->n_year,
'n_time_type' => 'C',
'n_reason' => 'LEAVE_WITHOUT_PAY',
'n_note' => $note->n_note,
'ticket_id' => $note->ticket_id
]);
$note->delete();
$onleave_days_will_use = 0;
$nopay_days_will_use -= 0.5;
} elseif ($onleave_days_will_use > 0) {
// Dùng ngày phép trước
$use = min($onleave_days_will_use, $value);
$note->update([
'n_reason' => "ONLEAVE"
]);
$onleave_days_will_use -= $use;
} elseif ($nopay_days_will_use > 0) {
// Hết phép, chuyển sang không phép
$use = min($nopay_days_will_use, $value);
$note->update([
'n_reason' => "LEAVE_WITHOUT_PAY"
]);
$nopay_days_will_use -= $use;
}
}
}
// Đủ phép
else {
foreach ($notes as $note) {
$note->update([
'n_reason' => "ONLEAVE"
]);
}
}
}
}
private function getMaxLeaveDaysPerMonth(): int
{
$limitLeaveMonth = Category::where('c_type', 'LIMIT_LEAVE_MONTH')->where('c_code', "LIMIT")->first();
if ($limitLeaveMonth) {
$maxDaysPerMonth = (int)$limitLeaveMonth->c_value;
} else {
$maxDaysPerMonth = 3; // default nếu k có setting
}
return $maxDaysPerMonth;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use Illuminate\Mail\Mailables\Address;
class CheckinLateMail extends Mailable
{
use Queueable, SerializesModels;
public $data;
/**
* Create a new message instance.
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
// replyTo: [
// new Address($this->data["email"], $this->data["name"]),
// ],
subject: $this->data["subject"],
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: ($this->data["email_template"])
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Mail;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WarningLongTask extends Mailable
{
use Queueable, SerializesModels;
public $data;
/**
* Create a new message instance.
*/
public function __construct($data)
{
$this->data = $data;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '[SYSTEM] - Task are forgotten - '.$this->data[0]['fields']['assignee']['displayName'],
);
}
public function build(): self
{
return $this->view('email.warningLongTask', ['data'=> $this->data]);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Files extends Model
{
use HasFactory;
protected $fillable = [
'name',
'url',
'type',
'description',
'user_id'
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -10,7 +10,7 @@ class LeaveDays extends Model
use HasFactory;
protected $fillable = [
'id', 'ld_user_id', 'ld_day', 'ld_year', 'ld_date_additional', 'ld_note'
'id', 'ld_user_id', 'ld_day_total', 'ld_year', 'ld_additional_day', 'ld_note'
];
protected $table = 'leave_days';

View File

@ -11,7 +11,14 @@ class Notes extends Model
use HasFactory;
protected $fillable = [
'n_user_id', 'n_day', 'n_month', 'n_year', 'n_time_type', 'n_reason', 'n_note',
'n_user_id',
'n_day',
'n_month',
'n_year',
'n_time_type',
'n_reason',
'n_note',
'ticket_id'
];
/**
@ -25,7 +32,7 @@ class Notes extends Model
{
return self::leftJoin("categories as reason", function ($join) {
$join->on('n_reason', '=', 'reason.c_code');
$join->on('reason.c_type', DB::raw("CONCAT('REASON')"));
$join->on('reason.c_type', DB::raw("CONCAT('REASON_NOTES')"));
})
->leftJoin("categories as timeTypes", function ($join) {
$join->on('n_time_type', '=', 'timeTypes.c_code');
@ -47,4 +54,18 @@ class Notes extends Model
)
->get();
}
public static function getNotesByMonthAndYearAndUserId($month, $year, $userId, $idNote)
{
return self::where('n_reason', 'ONLEAVE')->where('n_month', $month)->where('n_year', $year)
->where('n_user_id', $userId)
->where('id', '!=', $idNote)->get();
}
public static function getNotesByMonthAndYearAndUserIdAndReason($month, $year, $userId, $reason)
{
return self::where('n_reason', $reason)->where('n_month', $month)->where('n_year', $year)
->where('n_user_id', $userId)
->orderBy('n_day', 'asc')->orderBy('n_time_type', 'desc')->get();
}
}

View File

@ -23,7 +23,9 @@ class JiraService
'Authorization' => $this->authHeader,
'Accept' => 'application/json',
'Content-Type' => 'application/json'
]
],
'timeout' => 60, // Tăng thời gian timeout lên 60 giây
'connect_timeout' => 30 // Tăng thời gian chờ kết nối lên 30 giây
]);
}
@ -230,13 +232,14 @@ class JiraService
$groupedIssues = [];
$users_data = [];
$user_warning = [];
$ignore_projects = ['PJ_tracking'];
foreach ($users as $user) {
$user = (array)$user[0];
$user = (array) $user[0];
$users_data[$user['displayName']]['user'] = $user;
$users_data[$user['displayName']]['total_spent'] = 0;
$users_data[$user['displayName']]['total_est'] = 0;
$body = [
'expand' => ['names', 'schema','changelog'],
'expand' => ['names', 'schema', 'changelog'],
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'updated'],
'jql' => sprintf(
"assignee = '%s' AND status IN ('to do', 'todo', 'in progress') ORDER BY updated DESC",
@ -252,32 +255,41 @@ class JiraService
$issues = json_decode($response->getBody()->getContents(), true);
// Lọc các issue không thuộc các project bị ignore
$filtered_issues = array_filter($issues['issues'], function ($issue) use ($ignore_projects) {
return !in_array($issue['fields']['project']['name'], $ignore_projects);
});
$issues['issues'] = $filtered_issues;
if (count($issues['issues']) == 0) {
$user_warning[] = $user;
}
foreach ($issues['issues'] as $issue) {
$projectName = $issue['fields']['project']['name'];
$username = $issue['fields']['assignee']['displayName'];
if (!in_array($projectName, $ignore_projects)) {
$username = $issue['fields']['assignee']['displayName'];
$issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
if (!isset($groupedIssues[$projectName])) {
$groupedIssues[$projectName] = [];
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
}
if (!isset($groupedIssues[$projectName])) {
$groupedIssues[$projectName] = [];
$groupedIssues[$projectName]['project'] = $issue['fields']['project'];
if (!isset($groupedIssues[$projectName]['users'][$username])) {
$groupedIssues[$projectName]['users'][$username] = [];
$groupedIssues[$projectName]['users'][$username]['user'] = $issue['fields']['assignee'];
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = 0;
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = 0;
}
$groupedIssues[$projectName]['users'][$username]['issues'][] = $issue;
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = $groupedIssues[$projectName]['users'][$username]['p_total_spent'] + $issue['fields']['timespent'];
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = $groupedIssues[$projectName]['users'][$username]['p_total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
$users_data[$user['displayName']]['total_spent'] = $users_data[$user['displayName']]['total_spent'] + $issue['fields']['timespent'];
$users_data[$user['displayName']]['total_est'] = $users_data[$user['displayName']]['total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
}
if (!isset($groupedIssues[$projectName]['users'][$username])) {
$groupedIssues[$projectName]['users'][$username] = [];
$groupedIssues[$projectName]['users'][$username]['user'] = $issue['fields']['assignee'];
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = 0;
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = 0;
}
$groupedIssues[$projectName]['users'][$username]['issues'][] = $issue;
$groupedIssues[$projectName]['users'][$username]['p_total_spent'] = $groupedIssues[$projectName]['users'][$username]['p_total_spent'] + $issue['fields']['timespent'];
$groupedIssues[$projectName]['users'][$username]['p_total_est'] = $groupedIssues[$projectName]['users'][$username]['p_total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
$users_data[$user['displayName']]['total_spent'] = $users_data[$user['displayName']]['total_spent'] + $issue['fields']['timespent'];
$users_data[$user['displayName']]['total_est'] = $users_data[$user['displayName']]['total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
}
}
@ -314,4 +326,18 @@ class JiraService
$response = $this->client->get('/rest/agile/1.0/sprint/' . $id . '/issue');
return json_decode($response->getBody()->getContents(), true);
}
public function getWeeklyReport()
{
$body = [
'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'worklog'],
'jql' => 'worklogDate >= startOfWeek() AND worklogDate < startOfWeek(1) order by created DESC',
'maxResults' => 1000
];
$response = $this->client->post('/rest/api/3/search', [
'body' => json_encode($body)
]);
return json_decode($response->getBody()->getContents(), true);
}
}

View File

@ -2,11 +2,16 @@
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.2",
"barryvdh/laravel-debugbar": "^3.9",
"barryvdh/laravel-dompdf": "^2.0",
"doctrine/dbal": "^3.10",
"drnxloc/laravel-simple-html-dom": "^1.9",
"guzzlehttp/guzzle": "^7.8",
"laravel/framework": "^10.10",

3089
BACKEND/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,8 @@ return [
'asset_url' => env('ASSET_URL'),
'client_url' => env('ADMIN_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone

View File

@ -15,7 +15,7 @@ return [
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'paths' => ['api/*', 'sanctum/csrf-cookie', 'storage/*'],
'allowed_methods' => ['*'],

View File

@ -101,7 +101,7 @@ return [
|
*/
'ttl' => env('JWT_TTL', 60*24),
'ttl' => env('JWT_TTL', 60*24*365),
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('project_reviews', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('role');
$table->longText('note');
$table->foreignId('user_id')->constrained('users')->onDelete('cascade'); // Khóa ngoại tới bảng users
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('project_reviews');
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('documents', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('uri');
$table->enum('type', ['file', 'link'])->default("file");
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('documents');
}
};

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenameLdDayToLdDayTotalInLeaveDaysTable extends Migration
{
public function up()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_day', 'ld_day_total');
});
}
public function down()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_day_total', 'ld_day');
});
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddLdSpecialLeaveDayToLeaveDaysTable extends Migration
{
public function up()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->float('ld_special_leave_day')->default(0); // Adding the new field
});
}
public function down()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->dropColumn('ld_special_leave_day'); // Dropping the field if needed
});
}
}

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenameLdDateAdditionalToLdAdditionalDayInLeaveDaysTable extends Migration
{
public function up()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_date_additional', 'ld_additional_day');
});
}
public function down()
{
Schema::table('leave_days', function (Blueprint $table) {
$table->renameColumn('ld_date_additional', 'ld_additional_day');
});
}
}

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('categories')->insert([
[
'c_code' => 'LEAVE_WITHOUT_PAY',
'c_name' => 'Nghỉ không hưởng lương',
'c_type' => 'REASON',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
DB::table('categories')
->where('c_name', 'Nghỉ phép')
->update(['c_name' => 'Nghỉ phép năm']);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('categories')
->where('c_name', 'Nghỉ phép năm')
->update(['c_name' => 'Nghỉ phép']);
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('files', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('url');
$table->string('type');
$table->text('description')->nullable();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('files');
}
};

View File

@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class UpdateLeaveCategories extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Xóa item với type REASON và code LEAVE_WITHOUT_PAY
DB::table('categories')
->where('c_type', 'REASON')
->where('c_code', 'LEAVE_WITHOUT_PAY')
->delete();
// Cập nhật tên "Nghỉ phép năm" thành "Nghỉ phép"
DB::table('categories')
->where('c_name', 'Nghỉ phép năm')
->update(['c_name' => 'Nghỉ phép']);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Khôi phục item đã xóa
DB::table('categories')->insert([
'c_code' => 'LEAVE_WITHOUT_PAY',
'c_name' => 'Không phép',
'c_type' => 'REASON',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
// Khôi phục tên cũ
DB::table('categories')
->where('c_name', 'Nghỉ phép')
->update(['c_name' => 'Nghỉ phép năm']);
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddLimitLeaveMonthCategory extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
'c_code' => 'LIMIT',
'c_name' => 'Giới hạn số ngày nghỉ có phép/tháng',
'c_type' => 'LIMIT_LEAVE_MONTH',
'c_value' => '3',
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->where('c_code', 'LIMIT')
->where('c_type', 'LIMIT_LEAVE_MONTH')
->delete();
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddSaturdayWorkScheduleCategory extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
'c_code' => '10-05-2025',
'c_name' => 'Ngày bắt đầu làm việc thứ 7 trong năm',
'c_type' => 'SATURDAY_WORK_SCHEDULE',
'c_value' => '2025',
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->where('c_code', '10-05-2025')
->where('c_type', 'SATURDAY_WORK_SCHEDULE')
->delete();
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddDayWorkSpecialCategory extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
'c_code' => '17-05-2025',
'c_name' => 'Ngày làm việc đặc biệt',
'c_type' => 'DAY_WORK_SPECIAL',
'c_value' => '2025',
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->where('c_code', '17-05-2025')
->where('c_type', 'DAY_WORK_SPECIAL')
->delete();
}
}

View File

@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddLeaveCategories extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('categories')->insert([
[
'c_code' => 'LEAVE_WITHOUT_PAY',
'c_name' => 'Không phép',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'c_code' => 'WFH',
'c_name' => 'Work From Home',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'c_code' => 'ONLEAVE',
'c_name' => 'Nghỉ phép',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('categories')
->whereIn('c_code', ['LEAVE_WITHOUT_PAY', 'WFH', 'ONLEAVE'])
->where('c_type', 'REASON_NOTES')
->delete();
}
}

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->foreignId('ticket_id')
->nullable()
->constrained('tickets')
->onDelete('cascade');
});
}
public function down(): void
{
Schema::table('notes', function (Blueprint $table) {
$table->dropForeign(['ticket_id']);
$table->dropColumn('ticket_id');
});
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_permanent')->default(true);
$table->date('permanent_date');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_permanent');
$table->dropColumn('permanent_date');
});
}
};

View File

@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
DB::table('categories')->insert([
[
'c_code' => 'PERMANENT',
'c_name' => 'Phép cộng nhân viên chính thức',
'c_type' => 'PERMANENT_ONLEAVE',
'c_value' => 1,
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'c_code' => 'TEMPORARY_ONLEAVE',
'c_name' => 'Nghỉ dự kiến',
'c_type' => 'REASON_NOTES',
'c_value' => "",
'c_active' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::table('categories')->where('c_code', 'PERMANENT')->where('c_type', 'PERMANENT_ONLEAVE')->delete();
DB::table('categories')->where('c_code', 'TEMPORARY_ONLEAVE')->where('c_type', 'REASON_NOTES')->delete();
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_separated')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_separated');
});
}
};

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('electricity_bills', function (Blueprint $table) {
$table->id();
$table->date('billing_date')->comment('Ngày lập hóa đơn');
$table->decimal('previous_reading', 12, 2)->comment('Số điện kỳ trước');
$table->decimal('current_reading', 12, 2)->comment('Số điện kỳ này');
$table->decimal('unit_price', 12, 2)->comment('Đơn giá điện');
$table->decimal('total_amount', 12, 2)->comment('Tổng tiền điện');
$table->string('notes')->nullable()->comment('Ghi chú');
$table->string('file_path')->nullable()->comment('Đường dẫn file PDF');
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->timestamps();
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
$table->foreign('updated_by')->references('id')->on('users')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('electricity_bills');
}
};

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('electricity_bills', function (Blueprint $table) {
$table->string('attachment_path')->nullable()->after('file_path')
->comment('User-uploaded attachment (PDF or image)');
});
}
public function down(): void
{
Schema::table('electricity_bills', function (Blueprint $table) {
$table->dropColumn('attachment_path');
});
}
};

View File

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('equipments', function (Blueprint $table) {
$table->id();
$table->string('code')->unique()->comment('Mã thiết bị (dùng để scan barcode)');
$table->string('name')->comment('Tên thiết bị');
$table->text('note')->nullable()->comment('Ghi chú');
$table->unsignedBigInteger('current_user_id')->nullable()->comment('Người đang sử dụng, null = đang ở kho');
$table->timestamp('assigned_at')->nullable()->comment('Thời điểm giao thiết bị hiện tại');
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
$table->timestamps();
$table->foreign('current_user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
$table->foreign('updated_by')->references('id')->on('users')->onDelete('set null');
});
Schema::create('equipment_assignments', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('equipment_id');
$table->unsignedBigInteger('user_id')->nullable()->comment('null = trả về kho');
$table->timestamp('assigned_at')->comment('Bắt đầu sử dụng');
$table->timestamp('returned_at')->nullable()->comment('Trả thiết bị');
$table->text('note')->nullable();
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
$table->foreign('equipment_id')->references('id')->on('equipments')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('created_by')->references('id')->on('users')->onDelete('set null');
$table->index(['equipment_id', 'returned_at']);
});
}
public function down(): void
{
Schema::dropIfExists('equipment_assignments');
Schema::dropIfExists('equipments');
}
};

View File

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('equipments', function (Blueprint $table) {
$table->string('image_path')->nullable()->after('note')
->comment('Ảnh thiết bị (tùy chọn)');
});
}
public function down(): void
{
Schema::table('equipments', function (Blueprint $table) {
$table->dropColumn('image_path');
});
}
};

View File

@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
$this->call([
UserSeeder::class,
FileSeeder::class,
]);
}
}

View File

@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
font-family: "Arial", sans-serif;
}
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f7fafc;
}
.content {
background-color: #fffefe;
padding: 2rem 0;
border-radius: 0.5rem;
width: 100vw;
}
.my-0 {
margin-bottom: 0;
margin-top: 0;
}
</style>
<title>{{ $data['subject'] }}</title>
</head>
<body>
<body style="
font-family: Arial, Helvetica, sans-serif;
background-color: #edf2f7;
">
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
background-color: #ffffff;
width: 768px;
padding: 24px;
">
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Hi {{ $data['name'] }},
</h3>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
{{ $data['message1'] }}
</p>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
{{ $data['message2'] }}
</p>
</td>
</tr>
<tr>
<td>
<p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
<a href="{{ $data['url'] }}"
style="
color: #fff;
border-radius: 10px;
background-color: rgba(68,115,196);
background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(0,0,0,.2) 30%,rgba(0,0,0,0));
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Check now</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td>
<h5
style="
color: #222222;
text-align: center;
padding: 10px 36px;
margin: 0;
">
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</body>
</html>

View File

@ -1,4 +1,3 @@
<!DOCTYPE html>
<html lang="en">
@ -37,119 +36,167 @@
<title>{{ $data['subject'] }}</title>
</head>
<body>
<body style="
<body style="
font-family: Arial, Helvetica, sans-serif;
background-color: #edf2f7;
">
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
background-color: #ffffff;
width: 768px;
padding: 24px;
">
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Dear Admin,
</h3>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a request ticket, the specific content is as follows:
</p>
</td>
</tr>
<tr>
<td>
<div style="padding-left: 10px;color: #696969; margin-bottom: 15px">
<p style="padding: 3px;">Name: <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span></p>
<p style="padding: 3px;">Date: <span style="color: #222222;font-weight: bold;">{{ $data['date'] }}</span></p>
<p style="padding: 3px;">Type: <span style="color: #222222;font-weight: bold;">{{ $data['type'] }}</span></p>
<p style="padding: 3px;">Note: <span style="color: #222222;font-weight: bold;">{{ $data['note'] }}</span></p>
</div>
</td>
</tr>
<tr>
<td>
<p style="margin:0 0 16px;padding:5px;margin: 5px;text-align: center;">
<a href='{{ config('app.url') . $data['link'] }}'
style="
color: #fff;
border-radius: 10px;
background-color: rgba(68,115,196);
background-image: linear-gradient(to top left,rgba(0,0,0,.2),rgba(0,0,0,.2) 30%,rgba(0,0,0,0));
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Check now</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Dear Admin,
</h3>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
Employee <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span> has sent a <a href="{{ config('app.client_url') . $data['link'] }}"> request ticket</a>, the specific content is as follows:
</p>
</td>
</tr>
<tr>
<td>
<div style="padding-left: 10px;color: #696969; margin-bottom: 15px">
<p style="padding: 3px;">Name: <span style="color: #222222;font-weight: bold;">{{ $data['name'] }}</span></p>
<p style="padding: 3px;">Date: <span style="color: #222222;font-weight: bold;">{{ $data['date'] }}</span></p>
<p style="padding: 3px;">Type: <span style="color: #222222;font-weight: bold;">{{ $data['type'] }}</span></p>
<p style="padding: 3px;">Note: <span style="color: #222222;font-weight: bold;">{{ $data['note'] }}</span></p>
</div>
</td>
</tr>
<tr>
<td>
<p style="text-align: center">
You can quick
<span style="font-weight: bold">Confirm</span> or
<span style="font-weight: bold">Refuse</span> here:
</p>
<div
style="
display: flex;
justify-content: center;
gap: 10px;
margin-top: 10px;
">
<a
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'confirm', 'admin_email' => $data['admin_email']]) }}"
style="
color: #fff;
border-radius: 10px;
background-color: #12b886;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Confirm</a>
<a
href="{{ route('email.ticket.handle', ['ticket_id' => $data['ticket_id'], 'action' => 'refuse', 'admin_email' => $data['admin_email']]) }}"
style="
color: #fff;
border-radius: 10px;
background-color: #f03e3e;
background-image: linear-gradient(
to top left,
rgba(0, 0, 0, 0.2),
rgba(0, 0, 0, 0.2) 30%,
rgba(0, 0, 0, 0)
);
text-decoration: none;
display: inline-block;
font-weight: 600;
font-size: 16px;
line-height: 150%;
text-align: center;
margin: 0;
padding: 10px 12px;
">
Refuse</a>
</div>
</td>
</tr>
<tr>
<td style="color: #222222;">
<div style="margin-top: 3rem">
<p><span style="font-weight: bold">Note</span>: If you are redirected to a <span style="font-weight: bold">404 page</span>, it means:</p>
<p>1. The ticket has already been approved by another admin.</p>
<p>2. The ticket has been deleted.</p>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td>
<h5
style="
<tr>
<td>
<h5
style="
color: #222222;
text-align: center;
padding: 10px 36px;
margin: 0;
">
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
</html>

View File

@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
font-family: "Arial", sans-serif;
}
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f7fafc;
}
.content {
background-color: #fffefe;
padding: 2rem 0;
border-radius: 0.5rem;
width: 100vw;
}
.my-0 {
margin-bottom: 0;
margin-top: 0;
}
</style>
<title>[SYSTEM] - Task are forgotten</title>
</head>
<body>
<body style="
font-family: Arial, Helvetica, sans-serif;
background-color: #edf2f7;
">
<table style="margin: 0 auto">
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td align="center" valign="top" style="padding: 36px 24px;">
<a href="{{ config('app.url') }}" target="_blank" style="display: inline-block;">
<img src="https://apactech.io/wp-content/uploads/2022/12/APAC-TECH_side-e1670975093601-190x78.png"
alt="Logo" border="0" width="100"
style="display: block; width: 100px; max-width: 100px; min-width: 48px;">
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
background-color: #ffffff;
width: 768px;
padding: 24px;
">
<tr>
<td>
<h3 style="color: #222222; margin: 5px 0 0 0; font-weight: bold">
Hi {{ $data[0]['fields']['assignee']['displayName'] }},
</h3>
</td>
</tr>
<tr>
<td>
<p style=" white-space:pre-line; margin: 0; margin-bottom: 5px">
You currently have some tasks that have not been updated for a long time. Please check and update them!
</p>
</td>
</tr>
<tr>
<td>
@foreach($data as $iss)
<a href="https://apactechvn.atlassian.net/browse/{{$iss['key']}}">{{$iss['fields']['summary']}} - <b>haven't updated in {{$iss['daysRemaining']}} days</b></a><br>
@endforeach
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
style="
margin: 0 auto;
width: 768px;
">
<tr>
<td>
<h5
style="
color: #222222;
text-align: center;
padding: 10px 36px;
margin: 0;
">
<p>© 2024 APAC Tech.</p>
</h5>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</body>
</html>

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Thông báo tải lên file mới</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #1a73e8;
color: white;
padding: 20px;
text-align: center;
}
.content {
padding: 20px;
background-color: #f9f9f9;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #1a73e8;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}
.footer {
text-align: center;
padding: 20px;
background-color: #f5f5f5;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Thông báo tải lên file mới</h1>
</div>
<div class="content">
<p>Xin chào {{ $user->name }},</p>
<p>{{ $description }} hệ thống APAC Tech.</p>
<p>Note: {{ $note }}</p>
<p>Vui lòng kiểm tra ngay thông tin bằng cách nhấn nút bên dưới:</p>
<div style="text-align: center;">
<a href="{{ $url }}" class="button">Kiểm tra ngay</a>
</div>
<p>Trân trọng,<br>Đội ngũ APAC Tech</p>
</div>
<div class="footer">
<p>Email này được gửi tự động từ hệ thống APAC Tech. Vui lòng không trả lời email này.</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
<?php
use Carbon\Carbon;
require_once __DIR__ . "/../vendor/autoload.php";
$app = include_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
// Cập nhật lại data cho tới tháng hiện tại
$currentMonth = Carbon::now()->month;
$tmpClass = $app->make('Modules\Admin\app\Http\Controllers\TicketController');
$tmpClass->updateOldData($currentMonth, 2025); // Params: month, year

2
DetectFace/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dataset
test

28
DetectFace/collectData.py Normal file
View File

@ -0,0 +1,28 @@
import os
import shutil
def organize_files_by_username(folder_path, dest_folder_path):
# Lấy danh sách các tệp trong thư mục
files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
for file in files:
# Kiểm tra định dạng tên tệp: <username>_checkin_date.png
if "_" in file and file.endswith(".png"):
username = file.split("_")[0] # Lấy phần username từ tên tệp
# Tạo đường dẫn thư mục con
subfolder_path = os.path.join(folder_path, username)
# Tạo thư mục con nếu chưa tồn tại
if not os.path.exists(subfolder_path):
os.makedirs(subfolder_path)
# Di chuyển tệp vào thư mục con
shutil.move(os.path.join(folder_path, file), os.path.join(subfolder_path, file))
print("Hoàn thành sắp xếp tệp theo username.")
# Đường dẫn tới thư mục chứa các tệp
folder_path = "/home/joseph/screenshot"
dest_folder_path = "/home/joseph/DetectFace/dataset"
organize_files_by_username(folder_path, dest_folder_path)

77
DetectFace/detect.py Normal file
View File

@ -0,0 +1,77 @@
import cv2
import face_recognition
import os
import numpy as np
import pickle
datasetPath = "dataset"
images = []
classNames = []
lisFileTrain = os.listdir(datasetPath)
for file in lisFileTrain:
currentImg = cv2.imread(f"{datasetPath}/{file}")
images.append(currentImg)
classNames.append(os.path.splitext(file)[0].split('_')[0])
print(len(images))
def encodeImgs(images, save_path="encodings.pkl"):
if os.path.exists(save_path):
print(f"Loading encodings from {save_path}...")
with open(save_path, "rb") as f:
return pickle.load(f)
encodeList = []
for i, img in enumerate(images):
print(i+1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
encode = face_recognition.face_encodings(img)
if encode: # Check if encodings list is not empty
encodeList.append(encode[0])
else:
print("No face detected in an image. Skipping...")
os.remove(f"{datasetPath}/{lisFileTrain[i]}")
# Lưu encodeList vào file
print(f"Saving encodings to {save_path}...")
with open(save_path, "wb") as f:
pickle.dump(encodeList, f)
return encodeList
encodeListKnow = encodeImgs(images)
print("Load data success")
print(len(encodeListKnow))
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
frameS = cv2.resize(frame, (0,0), None, fx=1, fy=1)
frameS = cv2.cvtColor(frameS, cv2.COLOR_BGR2RGB)
faceCurFrame = face_recognition.face_locations(frameS)
encodeCurFrame = face_recognition.face_encodings(frameS)
for encodeFace, faceLoc in zip(encodeCurFrame, faceCurFrame):
matches = face_recognition.compare_faces(encodeListKnow, encodeFace)
faceDis = face_recognition.face_distance(encodeListKnow, encodeFace)
print(faceDis)
matchIndex = np.argmin(faceDis)
if faceDis[matchIndex] < 0.3:
name = classNames[matchIndex].upper()
else:
name = "Unknow"
y1, x2, y2, x1 = faceLoc
y1, x2, y2, x1 = y1, x2, y2, x1
cv2.rectangle(frame, (x1,y1), (x2,y2), (0,255,0), 2)
cv2.putText(frame, name + f"({(1 - round(faceDis[matchIndex], 2))*100}%)", (x2, y2), cv2.FONT_HERSHEY_COMPLEX, 1, (255,255,255), 2)
cv2.imshow('Face decting', frame)
if cv2.waitKey(1) == ord("q"):
break
cap.release()
cv2.destroyAllWindows()

BIN
DetectFace/encodings.pkl Normal file

Binary file not shown.

BIN
DetectFace/listFiles.pkl Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

View File

@ -1 +1,3 @@
VITE_BACKEND_URL=http://localhost:8000/
VITE_BACKEND_URL=http://localhost:8000/
VITE_URL_DRAWIO="https://viewer.diagrams.net/?tags=%7B%7D&lightbox=1&highlight=0000ff&edit=_blank&layers=1&nav=1&title=Test%20Draw.drawio&dark=auto#Uhttps%3A%2F%2Fdrive.google.com%2Fuc%3Fid%3D1LmB9wCac9DonQPFU-53g1nhI9SfvWuWK%26export%3Ddownload"

425
FRONTEND/install_nvm.sh Normal file
View File

@ -0,0 +1,425 @@
#!/usr/bin/env bash
{ # this ensures the entire script is downloaded #
nvm_has() {
type "$1" > /dev/null 2>&1
}
nvm_default_install_dir() {
[ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm"
}
nvm_install_dir() {
if [ -n "$NVM_DIR" ]; then
printf %s "${NVM_DIR}"
else
nvm_default_install_dir
fi
}
nvm_latest_version() {
echo "v0.35.0"
}
nvm_profile_is_bash_or_zsh() {
local TEST_PROFILE
TEST_PROFILE="${1-}"
case "${TEST_PROFILE-}" in
*"/.bashrc" | *"/.bash_profile" | *"/.zshrc")
return
;;
*)
return 1
;;
esac
}
#
# Outputs the location to NVM depending on:
# * The availability of $NVM_SOURCE
# * The method used ("script" or "git" in the script, defaults to "git")
# NVM_SOURCE always takes precedence unless the method is "script-nvm-exec"
#
nvm_source() {
local NVM_METHOD
NVM_METHOD="$1"
local NVM_SOURCE_URL
NVM_SOURCE_URL="$NVM_SOURCE"
if [ "_$NVM_METHOD" = "_script-nvm-exec" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm-exec"
elif [ "_$NVM_METHOD" = "_script-nvm-bash-completion" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/bash_completion"
elif [ -z "$NVM_SOURCE_URL" ]; then
if [ "_$NVM_METHOD" = "_script" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm.sh"
elif [ "_$NVM_METHOD" = "_git" ] || [ -z "$NVM_METHOD" ]; then
NVM_SOURCE_URL="https://github.com/nvm-sh/nvm.git"
else
echo >&2 "Unexpected value \"$NVM_METHOD\" for \$NVM_METHOD"
return 1
fi
fi
echo "$NVM_SOURCE_URL"
}
#
# Node.js version to install
#
nvm_node_version() {
echo "$NODE_VERSION"
}
nvm_download() {
if nvm_has "curl"; then
curl --compressed -q "$@"
elif nvm_has "wget"; then
# Emulate curl with wget
ARGS=$(echo "$*" | command sed -e 's/--progress-bar /--progress=bar /' \
-e 's/-L //' \
-e 's/--compressed //' \
-e 's/-I /--server-response /' \
-e 's/-s /-q /' \
-e 's/-o /-O /' \
-e 's/-C - /-c /')
# shellcheck disable=SC2086
eval wget $ARGS
fi
}
install_nvm_from_git() {
local INSTALL_DIR
INSTALL_DIR="$(nvm_install_dir)"
if [ -d "$INSTALL_DIR/.git" ]; then
echo "=> nvm is already installed in $INSTALL_DIR, trying to update using git"
command printf '\r=> '
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch origin tag "$(nvm_latest_version)" --depth=1 2> /dev/null || {
echo >&2 "Failed to update nvm, run 'git fetch' in $INSTALL_DIR yourself."
exit 1
}
else
# Cloning to $INSTALL_DIR
echo "=> Downloading nvm from git to '$INSTALL_DIR'"
command printf '\r=> '
mkdir -p "${INSTALL_DIR}"
if [ "$(ls -A "${INSTALL_DIR}")" ]; then
command git init "${INSTALL_DIR}" || {
echo >&2 'Failed to initialize nvm repo. Please report this!'
exit 2
}
command git --git-dir="${INSTALL_DIR}/.git" remote add origin "$(nvm_source)" 2> /dev/null \
|| command git --git-dir="${INSTALL_DIR}/.git" remote set-url origin "$(nvm_source)" || {
echo >&2 'Failed to add remote "origin" (or set the URL). Please report this!'
exit 2
}
command git --git-dir="${INSTALL_DIR}/.git" fetch origin tag "$(nvm_latest_version)" --depth=1 || {
echo >&2 'Failed to fetch origin with tags. Please report this!'
exit 2
}
else
command git -c advice.detachedHead=false clone "$(nvm_source)" -b "$(nvm_latest_version)" --depth=1 "${INSTALL_DIR}" || {
echo >&2 'Failed to clone nvm repo. Please report this!'
exit 2
}
fi
fi
command git -c advice.detachedHead=false --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" checkout -f --quiet "$(nvm_latest_version)"
if [ -n "$(command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" show-ref refs/heads/master)" ]; then
if command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet 2>/dev/null; then
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet -D master >/dev/null 2>&1
else
echo >&2 "Your version of git is out of date. Please update it!"
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch -D master >/dev/null 2>&1
fi
fi
echo "=> Compressing and cleaning up git repository"
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" reflog expire --expire=now --all; then
echo >&2 "Your version of git is out of date. Please update it!"
fi
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" gc --auto --aggressive --prune=now ; then
echo >&2 "Your version of git is out of date. Please update it!"
fi
return
}
#
# Automatically install Node.js
#
nvm_install_node() {
local NODE_VERSION_LOCAL
NODE_VERSION_LOCAL="$(nvm_node_version)"
if [ -z "$NODE_VERSION_LOCAL" ]; then
return 0
fi
echo "=> Installing Node.js version $NODE_VERSION_LOCAL"
nvm install "$NODE_VERSION_LOCAL"
local CURRENT_NVM_NODE
CURRENT_NVM_NODE="$(nvm_version current)"
if [ "$(nvm_version "$NODE_VERSION_LOCAL")" == "$CURRENT_NVM_NODE" ]; then
echo "=> Node.js version $NODE_VERSION_LOCAL has been successfully installed"
else
echo >&2 "Failed to install Node.js $NODE_VERSION_LOCAL"
fi
}
install_nvm_as_script() {
local INSTALL_DIR
INSTALL_DIR="$(nvm_install_dir)"
local NVM_SOURCE_LOCAL
NVM_SOURCE_LOCAL="$(nvm_source script)"
local NVM_EXEC_SOURCE
NVM_EXEC_SOURCE="$(nvm_source script-nvm-exec)"
local NVM_BASH_COMPLETION_SOURCE
NVM_BASH_COMPLETION_SOURCE="$(nvm_source script-nvm-bash-completion)"
# Downloading to $INSTALL_DIR
mkdir -p "$INSTALL_DIR"
if [ -f "$INSTALL_DIR/nvm.sh" ]; then
echo "=> nvm is already installed in $INSTALL_DIR, trying to update the script"
else
echo "=> Downloading nvm as script to '$INSTALL_DIR'"
fi
nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh" || {
echo >&2 "Failed to download '$NVM_SOURCE_LOCAL'"
return 1
} &
nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec" || {
echo >&2 "Failed to download '$NVM_EXEC_SOURCE'"
return 2
} &
nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion" || {
echo >&2 "Failed to download '$NVM_BASH_COMPLETION_SOURCE'"
return 2
} &
for job in $(jobs -p | command sort)
do
wait "$job" || return $?
done
chmod a+x "$INSTALL_DIR/nvm-exec" || {
echo >&2 "Failed to mark '$INSTALL_DIR/nvm-exec' as executable"
return 3
}
}
nvm_try_profile() {
if [ -z "${1-}" ] || [ ! -f "${1}" ]; then
return 1
fi
echo "${1}"
}
#
# Detect profile file if not specified as environment variable
# (eg: PROFILE=~/.myprofile)
# The echo'ed path is guaranteed to be an existing file
# Otherwise, an empty string is returned
#
nvm_detect_profile() {
if [ "${PROFILE-}" = '/dev/null' ]; then
# the user has specifically requested NOT to have nvm touch their profile
return
fi
if [ -n "${PROFILE}" ] && [ -f "${PROFILE}" ]; then
echo "${PROFILE}"
return
fi
local DETECTED_PROFILE
DETECTED_PROFILE=''
if [ -n "${BASH_VERSION-}" ]; then
if [ -f "$HOME/.bashrc" ]; then
DETECTED_PROFILE="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
DETECTED_PROFILE="$HOME/.bash_profile"
fi
elif [ -n "${ZSH_VERSION-}" ]; then
DETECTED_PROFILE="$HOME/.zshrc"
fi
if [ -z "$DETECTED_PROFILE" ]; then
for EACH_PROFILE in ".profile" ".bashrc" ".bash_profile" ".zshrc"
do
if DETECTED_PROFILE="$(nvm_try_profile "${HOME}/${EACH_PROFILE}")"; then
break
fi
done
fi
if [ -n "$DETECTED_PROFILE" ]; then
echo "$DETECTED_PROFILE"
fi
}
#
# Check whether the user has any globally-installed npm modules in their system
# Node, and warn them if so.
#
nvm_check_global_modules() {
command -v npm >/dev/null 2>&1 || return 0
local NPM_VERSION
NPM_VERSION="$(npm --version)"
NPM_VERSION="${NPM_VERSION:--1}"
[ "${NPM_VERSION%%[!-0-9]*}" -gt 0 ] || return 0
local NPM_GLOBAL_MODULES
NPM_GLOBAL_MODULES="$(
npm list -g --depth=0 |
command sed -e '/ npm@/d' -e '/ (empty)$/d'
)"
local MODULE_COUNT
MODULE_COUNT="$(
command printf %s\\n "$NPM_GLOBAL_MODULES" |
command sed -ne '1!p' | # Remove the first line
wc -l | command tr -d ' ' # Count entries
)"
if [ "${MODULE_COUNT}" != '0' ]; then
# shellcheck disable=SC2016
echo '=> You currently have modules installed globally with `npm`. These will no'
# shellcheck disable=SC2016
echo '=> longer be linked to the active version of Node when you install a new node'
# shellcheck disable=SC2016
echo '=> with `nvm`; and they may (depending on how you construct your `$PATH`)'
# shellcheck disable=SC2016
echo '=> override the binaries of modules installed with `nvm`:'
echo
command printf %s\\n "$NPM_GLOBAL_MODULES"
echo '=> If you wish to uninstall them at a later point (or re-install them under your'
# shellcheck disable=SC2016
echo '=> `nvm` Nodes), you can remove them from the system Node as follows:'
echo
echo ' $ nvm use system'
echo ' $ npm uninstall -g a_module'
echo
fi
}
nvm_do_install() {
if [ -n "${NVM_DIR-}" ] && ! [ -d "${NVM_DIR}" ]; then
if [ -e "${NVM_DIR}" ]; then
echo >&2 "File \"${NVM_DIR}\" has the same name as installation directory."
exit 1
fi
if [ "${NVM_DIR}" = "$(nvm_default_install_dir)" ]; then
mkdir "${NVM_DIR}"
else
echo >&2 "You have \$NVM_DIR set to \"${NVM_DIR}\", but that directory does not exist. Check your profile files and environment."
exit 1
fi
fi
if [ -z "${METHOD}" ]; then
# Autodetect install method
if nvm_has git; then
install_nvm_from_git
elif nvm_has nvm_download; then
install_nvm_as_script
else
echo >&2 'You need git, curl, or wget to install nvm'
exit 1
fi
elif [ "${METHOD}" = 'git' ]; then
if ! nvm_has git; then
echo >&2 "You need git to install nvm"
exit 1
fi
install_nvm_from_git
elif [ "${METHOD}" = 'script' ]; then
if ! nvm_has nvm_download; then
echo >&2 "You need curl or wget to install nvm"
exit 1
fi
install_nvm_as_script
else
echo >&2 "The environment variable \$METHOD is set to \"${METHOD}\", which is not recognized as a valid installation method."
exit 1
fi
echo
local NVM_PROFILE
NVM_PROFILE="$(nvm_detect_profile)"
local PROFILE_INSTALL_DIR
PROFILE_INSTALL_DIR="$(nvm_install_dir | command sed "s:^$HOME:\$HOME:")"
SOURCE_STR="\\nexport NVM_DIR=\"${PROFILE_INSTALL_DIR}\"\\n[ -s \"\$NVM_DIR/nvm.sh\" ] && \\. \"\$NVM_DIR/nvm.sh\" # This loads nvm\\n"
# shellcheck disable=SC2016
COMPLETION_STR='[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion\n'
BASH_OR_ZSH=false
if [ -z "${NVM_PROFILE-}" ] ; then
local TRIED_PROFILE
if [ -n "${PROFILE}" ]; then
TRIED_PROFILE="${NVM_PROFILE} (as defined in \$PROFILE), "
fi
echo "=> Profile not found. Tried ${TRIED_PROFILE-}~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile."
echo "=> Create one of them and run this script again"
echo " OR"
echo "=> Append the following lines to the correct file yourself:"
command printf "${SOURCE_STR}"
echo
else
if nvm_profile_is_bash_or_zsh "${NVM_PROFILE-}"; then
BASH_OR_ZSH=true
fi
if ! command grep -qc '/nvm.sh' "$NVM_PROFILE"; then
echo "=> Appending nvm source string to $NVM_PROFILE"
command printf "${SOURCE_STR}" >> "$NVM_PROFILE"
else
echo "=> nvm source string already in ${NVM_PROFILE}"
fi
# shellcheck disable=SC2016
if ${BASH_OR_ZSH} && ! command grep -qc '$NVM_DIR/bash_completion' "$NVM_PROFILE"; then
echo "=> Appending bash_completion source string to $NVM_PROFILE"
command printf "$COMPLETION_STR" >> "$NVM_PROFILE"
else
echo "=> bash_completion source string already in ${NVM_PROFILE}"
fi
fi
if ${BASH_OR_ZSH} && [ -z "${NVM_PROFILE-}" ] ; then
echo "=> Please also append the following lines to the if you are using bash/zsh shell:"
command printf "${COMPLETION_STR}"
fi
# Source nvm
# shellcheck source=/dev/null
\. "$(nvm_install_dir)/nvm.sh"
nvm_check_global_modules
nvm_install_node
nvm_reset
echo "=> Close and reopen your terminal to start using nvm or run the following to use it now:"
command printf "${SOURCE_STR}"
if ${BASH_OR_ZSH} ; then
command printf "${COMPLETION_STR}"
fi
}
#
# Unsets the various functions defined
# during the execution of the install script
#
nvm_reset() {
unset -f nvm_has nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \
nvm_source nvm_node_version nvm_download install_nvm_from_git nvm_install_node \
install_nvm_as_script nvm_try_profile nvm_detect_profile nvm_check_global_modules \
nvm_do_install nvm_reset nvm_default_install_dir
}
[ "_$NVM_ENV" = "_testing" ] || nvm_do_install
} # this ensures the entire script is downloaded #

File diff suppressed because it is too large Load Diff

View File

@ -11,19 +11,20 @@
"test": "npm run jest"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.1",
"@mantine/charts": "^7.4.1",
"@mantine/core": "7.2.1",
"@mantine/dates": "^7.3.2",
"@mantine/ds": "^7.2.2",
"@mantine/form": "^7.2.2",
"@mantine/hooks": "7.2.1",
"@mantine/notifications": "^7.2.2",
"@mantine/vanilla-extract": "7.2.1",
"@reduxjs/toolkit": "^1.9.7",
"@tabler/icons-react": "^2.40.0",
"@uiw/react-codemirror": "^4.21.21",
"@vanilla-extract/css": "^1.13.0",
"@codemirror/lang-javascript": "^6.2.2",
"@mantine/charts": "^7.16.3",
"@mantine/core": "^7.13.2",
"@mantine/dates": "^7.13.2",
"@mantine/form": "^7.13.2",
"@mantine/hooks": "^7.13.2",
"@mantine/notifications": "^7.13.2",
"@mantine/vanilla-extract": "^7.13.2",
"@reduxjs/toolkit": "^2.3.0",
"@tabler/icons-react": "^3.19.0",
"@uiw/react-codemirror": "^4.23.5",
"@vanilla-extract/css": "^1.16.0",
"@zxing/browser": "^0.2.0",
"@zxing/library": "^0.23.0",
"axios": "^1.6.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.10",
@ -33,11 +34,13 @@
"jwt-decode": "^4.0.0",
"moment": "^2.30.1",
"react": "^18.2.0",
"react-doc-viewer": "^0.1.14",
"react-dom": "^18.2.0",
"react-file-viewer": "^1.2.1",
"react-redux": "^8.1.3",
"react-router-dom": "^6.19.0",
"reactstrap": "^9.2.2",
"recharts": "^2.11.0",
"recharts": "^2.15.1",
"tailwind-merge": "^2.0.0",
"tests": "^0.4.2"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -11,6 +11,7 @@ import { BrowserRouter, useRoutes } from 'react-router-dom'
import mainRoutes from '@/routes/main'
import classes from '@/App.module.css'
import '@mantine/dates/styles.css'
import '@mantine/charts/styles.css'
export const App = () => {
const element = useRoutes(mainRoutes)

View File

@ -7,6 +7,7 @@ export const getListTracking = API_URL + 'v1/admin/tracking'
export const addTracking = API_URL + 'v1/admin/tracking/create'
export const updateTracking = API_URL + 'v1/admin/tracking/update'
export const deleteTracking = API_URL + 'v1/admin/tracking/delete'
export const getListTrackingSummary = API_URL + 'v1/admin/tracking/summary'
// Worklogs
export const fetchAllIssues = API_URL + 'v1/admin/jira/fetch-issues'
@ -17,9 +18,12 @@ export const getAllIssuesByProject =
export const getAllUserWorklogs = API_URL + 'v1/admin/jira/worklogs'
export const getAllUserDoing = API_URL + 'v1/admin/jira/allocation'
export const getDetailIssByKey = API_URL + 'v1/admin/jira/issue/detail'
export const getPJParticipating =
API_URL + 'v1/admin/jira/project-participating'
//Timekeeping
export const getTheTimesheet = API_URL + 'v1/admin/timekeeping'
export const exportTimekeeping = API_URL + 'v1/admin/timekeeping/export'
export const updateMultipleUserWorkingTime =
API_URL + 'v1/admin/timekeeping/addMutilple'
export const updateNote = API_URL + 'v1/admin/timekeeping/addNote'
@ -31,17 +35,24 @@ export const updateWorkingDays =
//Category
export const getListMaster = API_URL + 'v1/admin/category/get-list-master'
export const getWorkDay = API_URL + 'v1/admin/category/work-days'
export const updateWorkDay = API_URL + 'v1/admin/category/update-work-days'
//LeaveManagement
export const getLeaveManagement = API_URL + 'v1/admin/leave-management'
export const updateNoteLeave =
API_URL + 'v1/admin/leave-management/saveNoteLeave'
export const updateNoteStatus =
API_URL + 'v1/admin/leave-management/updateNoteStatus'
export const exportLeaveManagement =
API_URL + 'v1/admin/leave-management/export'
//Tickets
export const getTickets = API_URL + 'v1/admin/ticket/all'
export const getTicketsOfUser = API_URL + 'v1/admin/ticket/getByUserId'
export const deleteTicket = API_URL + 'v1/admin/ticket/delete'
export const addTicket = API_URL + 'v1/admin/ticket/create'
export const updateTicket = API_URL + 'v1/admin/ticket/update'
export const handleTicket = API_URL + 'v1/admin/ticket/handle-ticket'
//Users
@ -77,16 +88,82 @@ export const updateProfilesData =
API_URL + 'v1/admin/criterias/profiles-data/update'
export const listUserTechnical = API_URL + 'v1/admin/technical/get-tech-of-user'
export const updateUserTechnical = API_URL + 'v1/admin/technical/technicals-user/update'
export const updateUserTechnical =
API_URL + 'v1/admin/technical/technicals-user/update'
export const getAllUser = API_URL + 'v1/admin/technical/get-all-user'
export const getAllTechByUserId =
API_URL + 'v1/admin/technical/get-tech-by-user-id'
export const evaluation = API_URL + 'v1/admin/evaluation/report'
export const evaluationReportAllUsers =
API_URL + 'v1/admin/evaluation/report-all-users'
export const sprintReview = API_URL + 'v1/admin/evaluation/sprint-review'
export const projectReview = API_URL + 'v1/admin/evaluation/project-review'
export const projectReviewAdd =
API_URL + 'v1/admin/evaluation/project-review/create'
export const projectReviewUpdate =
API_URL + 'v1/admin/evaluation/project-review/update'
export const projectReviewDelete =
API_URL + 'v1/admin/evaluation/project-review/delete'
export const getAllFilesInProfiles = API_URL + 'v1/admin/profile/all-files'
export const updateProfileFolder = API_URL + 'v1/admin/profile/update-profile'
export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
//Technical
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
export const createTechnical = API_URL + 'v1/admin/technical/create'
export const deleteTechnical = API_URL + 'v1/admin/technical/delete'
// Document
export const listDocument = API_URL + 'v1/admin/document/all'
export const createDocument = API_URL + 'v1/admin/document/create'
export const updateDocument = API_URL + 'v1/admin/document/update'
export const deleteDocument = API_URL + 'v1/admin/document/delete'
// Download File
export const downloadFile = API_URL + 'v1/admin/download-file'
// Electricity Bills
export const getElectricityBills = API_URL + 'v1/admin/electricity-bill'
export const getElectricityBillById = (id: number) =>
API_URL + `v1/admin/electricity-bill/${id}`
export const createElectricityBill =
API_URL + 'v1/admin/electricity-bill/create'
export const updateElectricityBill = (id: number) =>
API_URL + `v1/admin/electricity-bill/${id}`
export const deleteElectricityBill = (id: number) =>
API_URL + `v1/admin/electricity-bill/delete/${id}`
export const exportElectricityBillPdf = (id: number) =>
API_URL + `v1/admin/electricity-bill/export-pdf/${id}`
export const uploadElectricityBillAttachment = (id: number) =>
API_URL + `v1/admin/electricity-bill/upload-attachment/${id}`
export const deleteElectricityBillAttachment = (id: number) =>
API_URL + `v1/admin/electricity-bill/delete-attachment/${id}`
// Equipments
export const getEquipments = API_URL + 'v1/admin/equipment'
export const getEquipmentUserOptions = API_URL + 'v1/admin/equipment/users'
export const getEquipmentUserStatistics =
API_URL + 'v1/admin/equipment/user-statistics'
export const exportEquipments = API_URL + 'v1/admin/equipment/export'
export const getEquipmentByCode = API_URL + 'v1/admin/equipment/by-code'
export const getEquipmentHistory = (id: number) =>
API_URL + `v1/admin/equipment/history/${id}`
export const createEquipment = API_URL + 'v1/admin/equipment/create'
export const updateEquipment = (id: number) =>
API_URL + `v1/admin/equipment/${id}`
export const assignEquipment = (id: number) =>
API_URL + `v1/admin/equipment/${id}/assign`
export const uploadEquipmentImage = (id: number) =>
API_URL + `v1/admin/equipment/${id}/upload-image`
export const deleteEquipmentImage = (id: number) =>
API_URL + `v1/admin/equipment/${id}/image`
export const deleteEquipment = (id: number) =>
API_URL + `v1/admin/equipment/delete/${id}`
// Files APIs
export const getFiles = API_URL + 'v1/admin/profile/files'
export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
export const deleteFileById = API_URL + 'v1/admin/profile/files'

View File

@ -11,6 +11,7 @@ import {
MultiSelect,
Pagination,
RadioGroup,
ScrollArea,
Select,
Skeleton,
Table,
@ -84,6 +85,9 @@ export const DataTableAll = ({
checkBox,
size,
infoTotal,
componentRight,
height = 600,
keyHighlight = '',
}: {
data: any[]
columns: Column[]
@ -92,6 +96,9 @@ export const DataTableAll = ({
checkBox?: boolean
size: string
infoTotal?: React.ReactNode // Set the type to ReactNode to allow JSX elements
componentRight?: React.ReactNode
height?: number
keyHighlight?: string
}) => {
const [Tdata, setTData] = useState<any[]>(data)
// const [tempData, setTempData] = useState<any[]>([])
@ -204,6 +211,9 @@ export const DataTableAll = ({
? 'var(--mantine-color-blue-light)'
: undefined
}
style={{
backgroundColor: element[keyHighlight] ? '#ff70704d' : 'transparent',
}}
>
<Table.Td display={checkBox ? 'block' : 'none'}>
<Checkbox
@ -240,10 +250,12 @@ export const DataTableAll = ({
if (query !== '') {
setTData(
data.filter((obj) =>
Object.values(obj).some(
(value: any) =>
value !== null &&
value.toString().toLowerCase().includes(query.toLowerCase()),
Object.values(obj)?.find((c: any) =>
c
.toString()
.normalize('NFC')
.toLowerCase()
.includes(query.normalize('NFC').toLowerCase()),
),
),
)
@ -325,10 +337,7 @@ export const DataTableAll = ({
</Text>
)}
</Box>
<Box
className={classes.totalBox}
display={infoTotal ? 'flex' : 'none'}
>
<Box className={classes.totalBox} display={infoTotal ? 'flex' : 'none'}>
<Text fz={'sm'} ta={'right'}>
{infoTotal}
</Text>
@ -368,44 +377,47 @@ export const DataTableAll = ({
}}
/>
</Box>
{componentRight}
</Box>
<Box className={classes.box}>
<Table
stickyHeader
stickyHeaderOffset={-1}
striped
highlightOnHover
withTableBorder
withColumnBorders
>
<Table.Thead className={classes.headers}>
<Table.Tr>
<Table.Th display={checkBox ? 'block' : 'none'}>
<Checkbox
aria-label="Select row"
checked={
checkSubArray(Tdata, selectedRows) &&
Tdata.length === selectedRows.length
}
onChange={(event) =>
setSelectedRows(
event.currentTarget.checked
? (pre) => [...pre, ...Tdata]
: selectedRows.filter(
(item) =>
!Tdata.some((removeItem) =>
areObjectsEqual(item, removeItem),
),
),
)
}
/>
</Table.Th>
{headers}
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
<Box className={classes.box} style={{ height: height + 50 }}>
<ScrollArea h={height}>
<Table
stickyHeader
stickyHeaderOffset={-1}
striped
highlightOnHover
withTableBorder
withColumnBorders
>
<Table.Thead className={classes.headers}>
<Table.Tr>
<Table.Th display={checkBox ? 'block' : 'none'}>
<Checkbox
aria-label="Select row"
checked={
checkSubArray(Tdata, selectedRows) &&
Tdata.length === selectedRows.length
}
onChange={(event) =>
setSelectedRows(
event.currentTarget.checked
? (pre) => [...pre, ...Tdata]
: selectedRows.filter(
(item) =>
!Tdata.some((removeItem) =>
areObjectsEqual(item, removeItem),
),
),
)
}
/>
</Table.Th>
{headers}
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</ScrollArea>
</Box>
</Container>
)
@ -451,7 +463,7 @@ export const DataTablePagination = ({
})
const [selectedRows, setSelectedRows] = useState<any[]>([])
const navigate = useNavigate()
const urlParams = new URLSearchParams(location.search)
let urlParams = new URLSearchParams(location.search)
// Render headers
const headers = columns.map((col) => (
@ -591,7 +603,7 @@ export const DataTablePagination = ({
// Remove specific parameters
params.delete(name)
urlParams.delete(name)
// Update the URL without reloading the page
window.history.replaceState({}, document.title, url.toString())
}
@ -623,9 +635,9 @@ export const DataTablePagination = ({
Array.isArray(dataFilter[key])
? dataFilter[key]
: key === 'to_date'
? Math.floor(dataFilter[key].getTime() / 1000) +
(60 * 60 * 23 + 60 * 59 + 59)
: Math.floor(dataFilter[key].getTime() / 1000),
? Math.floor(dataFilter[key].getTime() / 1000) +
(60 * 60 * 23 + 60 * 59 + 59)
: Math.floor(dataFilter[key].getTime() / 1000),
})
}
})
@ -655,9 +667,8 @@ export const DataTablePagination = ({
date_used_to: date_used,
})
}
// Add all attributes in 'params' to URL params
Object.entries(params).forEach((param) => urlParams.set(...param))
urlParams = new URLSearchParams(Object.entries(params))
Object.entries(dataFilter).forEach(([key, value]) => {
const typeFilter = filterInfo.find((o) => o.key === key).type
const hasType = {
@ -674,17 +685,16 @@ export const DataTablePagination = ({
if (hasType.Date) {
value = value ? Date.parse(String(value)) / 1000 : '' // to unix timestamp
}
console.log(String(value))
String(value).length
? urlParams.set(key, String(value))
: urlParams.delete(key)
})
// Request to get data API
const res = await get(url, Object.fromEntries(urlParams.entries()))
if (res.status) {
setBaseData(res)
setTData(res.data)
setBaseData(res.data?.links ? res?.data : res)
setTData(res.data?.data ? res.data?.data : res.data)
setSkeletion(false)
navigate({
pathname: location.pathname,
@ -760,7 +770,7 @@ export const DataTablePagination = ({
if (order_by_) {
const sortParam = {
name: order_by_.split('=')[0].split('_')[2],
name: order_by_.split('=')[0].split('_').slice(2).join('_'),
status: order_by_.split('=')[1],
}
if (JSON.stringify(sortParam) !== JSON.stringify(statusSort)) {

View File

@ -0,0 +1,25 @@
import DocViewer, { DocViewerRenderers } from "react-doc-viewer";
export default function FilePreview() {
const docs = [
// {
// uri:
// "http://localhost:9000/uploads/ULRYB3ATJ56B/Screenshot%202021-04-28%20at%2014.04.23.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T142426Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=761187860be22801088ab8c212733f7f52af8f62d638f1341ee2ae4c18944251"
// // "http://localhost:9000/uploads/6QK5HJ84MAEM/RAS-118_CompanyCodes__SalesOffices.xlsx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20210507%2F%2Fs3%2Faws4_request&X-Amz-Date=20210507T110429Z&X-Amz-Expires=432000&X-Amz-SignedHeaders=host&X-Amz-Signature=c20f9b77ffdc1a15910cea5acd3420b6583a1d4d38ce5716da30f1d0ea4315d5"
// // "https://res.cloudinary.com/cloudinaryforme/image/upload/v1618339571/workplace-1245776_1920_i9ayae.jpg"
// },
// {
// uri:
// "https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf"
// },
{ uri: "https://s28.q4cdn.com/392171258/files/doc_downloads/test.pdf" },
];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<DocViewer pluginRenderers={DocViewerRenderers} documents={docs} />
</div>
);
}

View File

@ -1,9 +0,0 @@
// import { render } from 'tests'
// import Footer from './Footer'
// describe('Footer component', () => {
// it('renders "Footer"', () => {
// render(<Footer />)
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
// })
// })

View File

@ -1,107 +0,0 @@
import { Text, Container, ActionIcon, Group, rem } from '@mantine/core'
import {
IconBrandTwitter,
IconBrandYoutube,
IconBrandInstagram,
} from '@tabler/icons-react'
import { MantineLogo } from '@mantine/ds'
import * as classes from './FooterLinks.module.css.ts'
const data = [
{
title: 'About',
links: [
{ label: 'Features', link: '#' },
{ label: 'Pricing', link: '#' },
{ label: 'Support', link: '#' },
{ label: 'Forums', link: '#' },
],
},
{
title: 'Project',
links: [
{ label: 'Contribute', link: '#' },
{ label: 'Media assets', link: '#' },
{ label: 'Changelog', link: '#' },
{ label: 'Releases', link: '#' },
],
},
{
title: 'Community',
links: [
{ label: 'Join Discord', link: '#' },
{ label: 'Follow on Twitter', link: '#' },
{ label: 'Email newsletter', link: '#' },
{ label: 'GitHub discussions', link: '#' },
],
},
]
const Footer = () => {
const groups = data.map((group) => {
const links = group.links.map((link, index) => (
<Text<'a'>
key={index}
className={classes.link}
component="a"
href={link.link}
onClick={(event) => event.preventDefault()}
>
{link.label}
</Text>
))
return (
<div className={classes.wrapper} key={group.title}>
<Text className={classes.title}>{group.title}</Text>
{links}
</div>
)
})
return (
<footer className={classes.footer}>
<Container className={classes.inner}>
<div className={classes.logo}>
<MantineLogo size={30} />
<Text size="xs" c="dimmed" className={classes.description}>
Build fully functional accessible web applications faster than ever
</Text>
</div>
<div className={classes.groups}>{groups}</div>
</Container>
<Container className={classes.afterFooter}>
<Text c="dimmed" size="sm">
© 2023 APACTECH.dev. All rights reserved.
</Text>
<Group
gap={0}
className={classes.social}
justify="flex-end"
wrap="nowrap"
>
<ActionIcon size="lg" color="gray" variant="subtle">
<IconBrandTwitter
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
</ActionIcon>
<ActionIcon size="lg" color="gray" variant="subtle">
<IconBrandYoutube
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
</ActionIcon>
<ActionIcon size="lg" color="gray" variant="subtle">
<IconBrandInstagram
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
</ActionIcon>
</Group>
</Container>
</footer>
)
}
export default Footer

View File

@ -1,104 +0,0 @@
import { rem } from '@mantine/core'
import { vars } from '@/theme'
import { style } from '@vanilla-extract/css'
export const footer = style({
paddingTop: rem(vars.spacing.xl),
paddingBottom: rem(vars.spacing.xl),
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
borderTop: `${rem(1)} solid light-dark(${vars.colors.gray[2]}, transparent)`,
})
export const logo = style({
maxWidth: rem(200),
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
},
})
export const description = style({
marginTop: rem(5),
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
marginTop: rem(vars.spacing.xs),
textAlign: 'center',
},
},
})
export const inner = style({
display: 'flex',
justifyContent: 'space-between',
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
flexDirection: 'column',
alignItems: 'center',
},
},
})
export const groups = style({
display: 'flex',
flexWrap: 'wrap',
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
display: 'none',
},
},
})
export const wrapper = style({
width: rem(160),
})
export const link = style({
display: 'block',
color: `light-dark(${vars.colors.gray[6]}, ${vars.colors.dark[1]})`,
fontSize: vars.fontSizes.sm,
paddingTop: rem(3),
paddingBottom: rem(3),
':hover': {
textDecoration: 'underline',
},
})
export const title = style({
fontSize: vars.fontSizes.lg,
fontWeight: 700,
fontFamily: `Greycliff CF, ${vars.fontFamily}`,
marginBottom: Number(vars.spacing.xs) / 2,
color: `light-dark(${vars.colors.black}, ${vars.colors.white})`,
})
export const afterFooter = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: vars.spacing.xl,
paddingTop: vars.spacing.xl,
paddingBottom: vars.spacing.xl,
borderTop: `rem(1) solid light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[4]})`,
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
flexDirection: 'column',
},
},
})
export const social = style({
'@media': {
[`(max-width: ${vars.breakpoints.sm})`]: {
marginTop: vars.spacing.xs,
},
},
})

View File

@ -1,62 +0,0 @@
import { style, styleVariants } from '@vanilla-extract/css'
import { vars } from '../../theme'
import { rem } from '@mantine/core'
export const header = style({
paddingTop: rem(vars.spacing.sm),
backgroundColor: `light-dark(${vars.colors.gray[0]}, ${vars.colors.dark[6]})`,
borderBottom: `${rem(1)} solid light-dark(${
vars.colors.gray[2]
}, transparent)`,
marginBottom: rem(120),
})
export const mainSection = style({
paddingBottom: rem(vars.spacing.sm),
})
export const user = style({
color: `light-dark(${vars.colors.black}, ${vars.colors.dark[0]})`,
padding: `${rem(vars.spacing.xs)} ${rem(vars.spacing.sm)}`,
borderRadius: rem(vars.radius.sm),
transition: 'background-color 100ms ease',
':hover': {
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
},
'@media': {
[`(max-width: ${vars.breakpoints.xs})`]: {
display: 'none',
},
},
})
export const userActive = style({
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[8]})`,
})
export const tabsList = style({
'::before': {
display: 'none',
},
})
export const tab = styleVariants({
default: {
fontWeight: 500,
height: rem(38),
backgroundColor: 'transparent',
position: 'relative',
bottom: '-1px',
':hover': {
backgroundColor: `light-dark(${vars.colors.gray[1]}, ${vars.colors.dark[5]})`,
},
},
active: {
backgroundColor: `light-dark(${vars.colors.white}, ${vars.colors.dark[7]})`,
borderColor: `light-dark(${vars.colors.gray[2]}, ${vars.colors.dark[7]})`,
borderBottomColor: 'transparent',
},
})

View File

@ -1,11 +0,0 @@
// import { render } from '@tests'
// import Header from './Header'
// describe('Header component', () => {
// it('renders "Header"', () => {
// render(<Header header={''} setHeader={function(): void {
// throw new Error('Function not implemented.')
// } } />)
// // expect(getByRole('divider', { name: 'Login' })).toBeInTheDocument()
// })
// })

View File

@ -1,210 +0,0 @@
import { logout } from '@/rtk/dispatches/auth'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
import {
Avatar,
Burger,
Container,
Group,
Menu,
Tabs,
Text,
UnstyledButton,
rem,
} from '@mantine/core'
import { MantineLogo } from '@mantine/ds'
import { useDisclosure } from '@mantine/hooks'
import {
IconChevronDown,
IconLogout,
IconSettings,
IconSwitchHorizontal,
} from '@tabler/icons-react'
import cx from 'clsx'
import { useCallback, useState } from 'react'
import { ColorSchemeToggle } from '../ColorSchemeToggle/ColorSchemeToggle'
import LanguagePicker from '../LanguagePicker/LanguagePicker'
import * as classes from './DoubleHeader.module.css.ts'
import { useNavigate } from 'react-router-dom'
// const user = {
// name: 'Jane Spoonfighter',
// email: 'janspoon@fighter.dev',
// image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png',
// };
const tabs = ['Home', 'Page 1', 'Page 2', 'Page 3', 'Page 4', 'Page 5']
const Header = ({
header,
setHeader,
}: {
header: string
setHeader: (newHeader: string) => void
}) => {
const [opened, { toggle }] = useDisclosure(false)
const [userMenuOpened, setUserMenuOpened] = useState(false)
const dispatch = useAppDispatch()
const navigate = useNavigate()
const user = useAppSelector((state) => state.authentication.user.user)
const items = tabs.map((tab) => (
<Tabs.Tab value={tab} key={tab} onClick={() => setHeader(tab)}>
{tab}
</Tabs.Tab>
))
const handleLogout = useCallback(() => {
dispatch(logout(navigate))
}, [dispatch, navigate])
return (
<div className={classes.header}>
<Container className={classes.mainSection} size="xl">
<Group justify="space-between">
<MantineLogo size={28} />
<Burger opened={opened} onClick={toggle} hiddenFrom="xs" size="sm" />
<Group>
<LanguagePicker />
<ColorSchemeToggle />
<Menu
width={260}
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
onClose={() => setUserMenuOpened(false)}
onOpen={() => setUserMenuOpened(true)}
withinPortal
>
<Menu.Target>
<UnstyledButton
className={cx(classes.user, {
[classes.userActive]: userMenuOpened,
})}
>
<Group gap={7}>
<Avatar
src={
'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-5.png'
}
alt={user.name}
radius="xl"
size={25}
/>
<Text fw={500} size="sm" lh={1} mr={3}>
{user.name}
</Text>
<IconChevronDown
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
{/* <Menu.Item
leftSection={
<IconHeart
style={{ width: rem(16), height: rem(16) }}
color={theme.colors.red[6]}
stroke={1.5}
/>
}
>
Liked posts
</Menu.Item>
<Menu.Item
leftSection={
<IconStar
style={{ width: rem(16), height: rem(16) }}
color={theme.colors.yellow[6]}
stroke={1.5}
/>
}
>
Saved posts
</Menu.Item>
<Menu.Item
leftSection={
<IconMessage
style={{ width: rem(16), height: rem(16) }}
color={theme.colors.blue[6]}
stroke={1.5}
/>
}
>
Your comments
</Menu.Item> */}
<Menu.Label>Settings</Menu.Label>
<Menu.Item
leftSection={
<IconSettings
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Account settings
</Menu.Item>
<Menu.Item
leftSection={
<IconSwitchHorizontal
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Change account
</Menu.Item>
<Menu.Item
leftSection={
<IconLogout
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
onClick={handleLogout}
>
Logout
</Menu.Item>
{/* <Menu.Divider />
<Menu.Label>Danger zone</Menu.Label>
<Menu.Item
leftSection={
<IconPlayerPause style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
>
Pause subscription
</Menu.Item>
<Menu.Item
color="red"
leftSection={<IconTrash style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
>
Delete account
</Menu.Item> */}
</Menu.Dropdown>
</Menu>
</Group>
</Group>
</Container>
<Container size="md">
<Tabs
defaultValue={header}
variant="outline"
visibleFrom="sm"
classNames={{
// root: classes.tab.active,
list: classes.tabsList,
tab: classes.tab.default,
}}
>
<Tabs.List justify="center">{items}</Tabs.List>
</Tabs>
</Container>
</div>
)
}
export default Header

View File

@ -4,6 +4,7 @@ import { logout } from '@/rtk/dispatches/auth'
import { get, post } from '@/rtk/helpers/apiService'
import { requirementsPassword } from '@/rtk/helpers/variables'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
import { checkPermissions } from '@/utils/checkRoles'
import {
Avatar,
Box,
@ -26,6 +27,8 @@ import {
IconCalendarClock,
IconChartDots2,
IconDevices,
IconFileInvoice,
IconFolders,
IconLayoutSidebarLeftExpand,
IconLayoutSidebarRightExpand,
IconListCheck,
@ -36,6 +39,7 @@ import {
IconReport,
IconScan,
IconSettings,
IconShredder,
IconSun,
IconTicket,
IconUsersGroup,
@ -45,7 +49,6 @@ import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import PasswordRequirementInput from '../PasswordRequirementInput/PasswordRequirementInput'
import classes from './NavbarSimpleColored.module.css'
import { checkPermissions } from '@/utils/checkRoles'
const data = [
// { link: '/dashboard', label: 'Dashboard', icon: IconHome },
@ -53,15 +56,15 @@ const data = [
link: '/timekeeping',
label: 'Timekeeping',
icon: IconCalendar,
permissions: 'admin,hr,staff,tester',
permissions: 'admin,hr,staff,tester,accountant',
group: 'staff',
},
{
link: '/tracking',
label: 'Check in/out',
icon: IconScan,
permissions: 'hr,admin',
group: 'other',
permissions: 'staff,hr,admin',
group: 'staff',
},
{
link: '/worklogs',
@ -70,11 +73,18 @@ const data = [
permissions: 'admin,hr,staff,tester',
group: 'staff',
},
{
link: '/documents',
label: 'Documents',
icon: IconFileInvoice,
permissions: 'admin,hr,staff,tester,accountant',
group: 'staff',
},
{
link: '/leave-management',
label: 'Leave Management',
icon: IconCalendarClock,
permissions: 'admin,hr,staff,tester',
permissions: 'admin,hr,staff,tester,accountant',
group: 'staff',
},
{
@ -137,8 +147,22 @@ const data = [
link: '/organization-settings',
label: 'Organization Settings',
icon: IconSettings,
group: 'other',
permissions: 'admin,hr',
},
{
link: '/profiles',
label: 'Profiles',
icon: IconFolders,
group: 'admin',
permissions: 'admin',
permissions: 'admin,accountant',
},
{
link: '/office-support',
label: 'Office Support',
icon: IconShredder,
group: 'other',
permissions: 'admin,hr,accountant',
},
// { link: '/jira', label: 'Jira', icon: IconSubtask },
// { link: '/custom-theme', label: 'Custom Theme', icon: IconBrush },
@ -183,7 +207,7 @@ const Navbar = ({
confirm_password: '',
})
const [countSpam, setCountSpam] = useState(0)
const [avatar, setAvatar] = useState(user.user.avatar)
const [avatar, setAvatar] = useState(user?.user?.avatar)
const navigate = useNavigate()
const dispatch = useAppDispatch()
@ -231,7 +255,11 @@ const Navbar = ({
// })
const group = [
{ name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester' },
{
name: 'staff',
label: 'General',
permissions: 'admin,hr,staff,tester,accountant',
},
{ name: 'admin', label: 'Admin', permissions: 'admin' },
{ name: 'other', label: 'Other', permissions: 'admin,hr' },
{ name: 'test', label: 'Test', permissions: 'admin,tester' },
@ -301,7 +329,7 @@ const Navbar = ({
const res = await post(
changePassword,
{
email: user.user.email,
email: user?.user?.email,
password: dataChange.password,
new_password: dataChange.new_password,
confirm_password: dataChange.confirm_password,
@ -417,7 +445,7 @@ const Navbar = ({
cursor: 'pointer',
}}
>
{user.user.name}
{user?.user?.name}
</Code>
</Box>
<Box
@ -569,7 +597,7 @@ const Navbar = ({
<Box p="sm">
<TextInput
label="E-mail"
value={user.user.email}
value={user?.user?.email}
disabled
mb={'md'}
></TextInput>

View File

@ -3,3 +3,8 @@ declare module '*.css'
declare module '@codemirror/lang-javascript'
declare const __VITE_BACKEND_URL__: string
declare module 'react-file-viewer' {
const FileViewer: any
export default FileViewer
}

View File

@ -0,0 +1,28 @@
.root {
/* font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; */
font-size: 14px;
}
.label[data-hovered] {
background-color: var(--mantine-color-gray-0);
}
/* Add the light and dark logic using classes, for example */
.label[data-hovered].light {
background-color: var(--mantine-color-gray-0);
}
.label[data-hovered].dark {
background-color: var(--mantine-color-dark-6);
color: var(--mantine-color-white);
}
.label[data-selected] {
font-weight: 700;
}
.label[data-selected].light {
background-color: var(--mantine-color-blue-0);
color: var(--mantine-color-black);
}
.label[data-selected].dark {
background-color: rgba(var(--mantine-color-blue-8), 0.35);
color: var(--mantine-color-blue-0);
}

View File

@ -0,0 +1,335 @@
import { getFiles, uploadFiles } from '@/api/Admin'
import { get } from '@/rtk/helpers/apiService'
import { getAccessToken } from '@/rtk/localStorage'
import {
Box,
Button,
Card,
Collapse,
Group,
Modal,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core'
import { notifications } from '@mantine/notifications'
import {
IconChevronDown,
IconDownload,
IconFileTypeDocx,
IconFileTypePdf,
IconFolder,
IconListCheck,
IconPhoto,
IconSearch,
IconTrash,
} from '@tabler/icons-react'
import axios from 'axios'
import { useEffect, useState } from 'react'
import FileUploadForm from '../Profile/components/FileUploadForm'
import classes from './AllProfiles.module.css'
interface FileData {
id: number
name: string
url: string
type: string
description?: string
created_at: string
}
interface GroupedFiles {
[key: string]: FileData[]
}
const AllProfiles = () => {
const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
const [currentUser, setCurrentUser] = useState<string>('')
const [openedProfile, setOpenedProfile] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [expandedFolders, setExpandedFolders] = useState<{
[key: string]: boolean
}>({})
const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
const toggleFolder = (userName: string) => {
setExpandedFolders((prev) => ({
...prev,
[userName]: !prev[userName],
}))
}
const getFileIcon = (type: string) => {
switch (type) {
case 'document':
return <IconFileTypeDocx size={16} />
case 'image':
return <IconPhoto size={16} />
default:
return <IconFileTypePdf size={16} />
}
}
const handleSubmit = async (
e: React.FormEvent,
fileName: string,
description: string,
currentUser: string
) => {
e.preventDefault()
setIsLoading(true)
const formData = new FormData()
if (selectedFile) {
formData.append('file', selectedFile)
formData.append('name', fileName)
formData.append('description', description)
formData.append('user_name', currentUser)
try {
const token = await getAccessToken()
const response = await axios.post(uploadFiles, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
})
if (response.status === 200) {
setSelectedFile(null)
await getAllFiles()
return true
}
return false
} catch (error) {
console.error('Error uploading file:', error)
throw error
} finally {
setIsLoading(false)
}
}
return false
}
const getAllFiles = async () => {
try {
const res = await get(getFiles)
if (res.status === true) {
setGroupedFiles(res.data)
}
} catch (error) {
console.log(error)
}
}
const removeFile = async (id: number) => {
try {
const token = await getAccessToken();
const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (response.status === 200) {
notifications.show({
title: 'Thành công',
message: 'Xóa file thành công',
color: 'green',
});
await getAllFiles();
}
} catch (error) {
console.log(error);
notifications.show({
title: 'Lỗi',
message: 'Không thể xóa file',
color: 'red',
});
}
}
useEffect(() => {
getAllFiles()
}, [])
const filterFiles = (files: FileData[], searchTerm: string) => {
return files.filter(
(file) =>
file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(file.description &&
file.description.toLowerCase().includes(searchTerm.toLowerCase())),
)
}
return (
<div>
<div className={classes.title}>
<h3>
<Text size="sm">Admin/</Text>
Files Management
</h3>
</div>
<Box ml={'md'}>
<Stack gap="xs">
{Object.entries(groupedFiles).map(([userName, files]) => (
<Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
<Group
justify="space-between"
mb="xs"
gap="xs"
onClick={() => toggleFolder(userName)}
style={{ cursor: 'pointer' }}
>
<Group gap="xs">
<IconFolder size={18} color="var(--mantine-color-yellow-9)" />
<Title order={5}>{userName}</Title>
</Group>
<Group gap="xs">
<Button
size="xs"
variant="subtle"
color="gray"
onClick={() => toggleFolder(userName)}
leftSection={
<IconChevronDown
size={14}
style={{
transform: expandedFolders[userName]
? 'rotate(180deg)'
: 'none',
transition: 'transform 0.2s',
}}
/>
}
>
{expandedFolders[userName] ? 'Collapse' : 'Expand'}
</Button>
<Button
size="xs"
variant="light"
color="blue"
onClick={() => {
setCurrentUser(userName)
setOpenedProfile(true)
}}
>
<Group gap={2}>
<IconListCheck size={14} />
<Text size="xs">Upload Files</Text>
</Group>
</Button>
</Group>
</Group>
<Collapse in={expandedFolders[userName]}>
<Stack gap="xs">
<TextInput
placeholder="Search files by name or description..."
size="xs"
leftSection={<IconSearch size={14} />}
value={searchTerms[userName] || ''}
onChange={(e) =>
setSearchTerms((prev) => ({
...prev,
[userName]: e.target.value,
}))
}
onClick={(e) => e.stopPropagation()}
/>
{filterFiles(files, searchTerms[userName] || '')
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.map((file: FileData) => (
<Card
key={file.id}
shadow="xs"
padding="xs"
radius="sm"
withBorder
>
<Group justify="space-between" gap="xs">
<Group gap="xs">
{getFileIcon(file.type)}
<Box>
<Text size="xs" fw={500}>
{file.name}
</Text>
{file.description && (
<Text size="xs" c="dimmed">
{file.description}
</Text>
)}
<Text size="xs" c="dimmed">
Uploaded:{' '}
{new Date(file.created_at).toLocaleDateString()}
</Text>
</Box>
</Group>
<Group gap="xs">
<Button
size="xs"
variant="light"
color="blue"
component='a'
href={`${import.meta.env.VITE_BACKEND_URL}${
import.meta.env.VITE_BACKEND_URL?.includes(
'localhost',
)
? ''
: 'image/'
}${file.url.slice(1)}`}
target="_blank"
>
<Group gap={2}>
<IconDownload size={12} />
<Text size="xs">Download</Text>
</Group>
</Button>
<Button
size="xs"
variant="light"
color="red"
onClick={() => removeFile(file.id)}
>
<Group gap={2}>
<IconTrash size={12} />
<Text size="xs">Delete</Text>
</Group>
</Button>
</Group>
</Group>
</Card>
),
)}
</Stack>
</Collapse>
</Card>
))}
</Stack>
<Modal
size="lg"
opened={openedProfile}
onClose={() => {
setOpenedProfile(false)
setCurrentUser('')
setSelectedFile(null)
}}
>
<Box>
<FileUploadForm
data={groupedFiles[currentUser] || []}
handleSubmit={handleSubmit}
handleFileChange={(file) => file && setSelectedFile(file)}
removeFile={removeFile}
isLoading={isLoading}
currentUser={currentUser}
/>
</Box>
</Modal>
</Box>
</div>
)
}
export default AllProfiles

View File

@ -10,6 +10,7 @@ import {
Popover,
Text,
Tooltip,
Switch,
} from '@mantine/core'
import { IconInnerShadowTopRightFilled } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
@ -80,6 +81,31 @@ const Allocation = () => {
const [opened, setOpened] = useState(false)
const [issDetail, setIssDetail] = useState('')
const [data, setData] = useState<any>({})
const [showDrawio, setShowDrawio] = useState(true)
useEffect(() => {
// Check if iframe already exists
const existingIframe = document.querySelector('#drawio iframe')
if (existingIframe) return
// Add iframe only if it doesn't exist
const iframe = document.createElement('iframe')
iframe.src = import.meta.env.VITE_URL_DRAWIO ?? ''
iframe.style.width = '100%'
iframe.style.height = '500px'
const drawioDiv = document.getElementById('drawio')
if (drawioDiv) {
drawioDiv.appendChild(iframe)
}
return () => {
const iframe = document.querySelector('#drawio iframe')
if (iframe) {
iframe.remove()
}
}
}, [])
const getAll = async () => {
try {
const res = await get(getAllUserDoing)
@ -117,6 +143,14 @@ const Allocation = () => {
}, [])
return (
<div>
<Box style={{ display: 'flex', alignItems: 'center', margin: '10px' }}>
<Switch
label="Show Diagram"
checked={showDrawio}
onChange={(event) => setShowDrawio(event.currentTarget.checked)}
/>
</Box>
<div id="drawio" style={{ display: showDrawio ? 'block' : 'none' }}></div>
<div>
<Box
style={{
@ -138,12 +172,12 @@ const Allocation = () => {
}}
>
<Box>
<Text fw={600} fz={'md'}>
{/* <Text fw={600} fz={'md'}>
Admin/
</Text>
<Text fw={700} fz={'lg'}>
Personnel Allocation
</Text>
</Text> */}
<Text fw={600} fz={'sm'} c={'gray'} fs={'italic'} ml={'md'}>
"P:" is the timspent/timeestimate number within the project itself
</Text>
@ -224,27 +258,38 @@ const Allocation = () => {
alignItems: 'center',
}}
>
<Box w='10%'>
<IconInnerShadowTopRightFilled
style={{ color: 'orange' }}
height={20}
width={20}
display={userData.issues?.filter(
(iss: Issue) =>
iss.fields.status.name === 'In Progress' &&
((Date.now() - (new Date(iss.changelog?.histories[0]?.created)).getTime()) > 172800000)
).length > 0 ? 'block' :'none'}
/>
<Box w="10%">
<IconInnerShadowTopRightFilled
style={{ color: 'orange' }}
height={20}
width={20}
display={
userData.issues?.filter(
(iss: Issue) =>
iss.fields.status.name ===
'In Progress' &&
Date.now() -
new Date(
iss.changelog?.histories[0]?.created,
).getTime() >
172800000,
).length > 0
? 'block'
: 'none'
}
/>
</Box>
<Box display={'flex'}>
<Avatar
size={'sm'}
ml={'5px'}
src={userData.user.avatarUrls['48x48']}
/>
<Text ml={'md'} fw={600}>
{user}
</Text>
<Avatar
size={'sm'}
ml={'5px'}
src={
userData.user.avatarUrls['48x48']
}
/>
<Text ml={'md'} fw={600}>
{user}
</Text>
</Box>
</Box>
<Box
@ -260,9 +305,12 @@ const Allocation = () => {
}}
ml={'md'}
p="0 20px"
>
<Text ml={'md'} fw={700} fz={'sm'}>{`P: `}</Text>
<Text
ml={'md'}
fw={700}
fz={'sm'}
>{`P: `}</Text>
<Text fw={700} c="green" fz={'sm'}>{`${
userData.p_total_spent / 60 / 60
}h/`}</Text>
@ -316,7 +364,7 @@ const Allocation = () => {
<Box
className={
Date.now() - date.getTime() >
172800000 * 5
172800000 * 5 // 10 days
? classes['blinking-background']
: ''
}
@ -326,7 +374,7 @@ const Allocation = () => {
borderRadius: '10px',
cursor: 'pointer',
backgroundColor: issStatus
? issLastHistory < 172800000
? issLastHistory < 172800000 // 2 days
? '#d1f3d1'
: '#ffff8a'
: '',

View File

@ -0,0 +1,37 @@
.title {
background-color: light-dark(var(white), var(--mantine-color-dark-7));
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
var(--mantine-spacing-sm);
border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
}
.optionIcon {
display: flex;
justify-content: space-evenly;
}
.deleteIcon {
color: red;
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.editIcon {
color: rgb(9, 132, 132);
cursor: pointer;
padding: 2px;
border-radius: 25%;
}
.editIcon:hover {
background-color: rgba(203, 203, 203, 0.809);
}
.deleteIcon:hover {
background-color: rgba(203, 203, 203, 0.809);
}

Some files were not shown because too many files have changed in this diff Show More