Compare commits

..

497 Commits

Author SHA1 Message Date
Sabda Yagra 9223122e66 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-27 10:48:33 +07:00
Sabda Yagra 9e286ffd06 fix: dev fixing
continuous-integration/drone/push Build is passing Details
2026-02-27 10:47:55 +07:00
Sabda Yagra 18d6f32536 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-20 15:29:26 +07:00
Sabda Yagra 9c2b3612c6 fix: ck editor
continuous-integration/drone/push Build is passing Details
2026-02-20 15:16:23 +07:00
Sabda Yagra 6bd83237c4 fix: install all package and fixing task 2026-02-20 15:13:48 +07:00
Sabda Yagra 61df236e13 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-20 14:09:25 +07:00
Sabda Yagra 7f9166b866 fix: .drone.yml
continuous-integration/drone/push Build is passing Details
2026-02-20 14:04:40 +07:00
Sabda Yagra 2876e4eb30 Merge branch 'dev-sabda-v2' of http://38.47.180.165:3000/mediahub/mediahub-fe
continuous-integration/drone/push Build is passing Details
2026-02-20 13:55:41 +07:00
Sabda Yagra 292cde6ed0 feat: drone.yml
continuous-integration/drone/push Build is passing Details
2026-02-20 11:55:18 +07:00
Sabda Yagra 3c5e644668 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-02-11 22:47:17 +07:00
Sabda Yagra d80adaa133 fix: schedule checkbox 2026-02-11 22:46:47 +07:00
Sabda Yagra eefa43a5b0 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-02-11 18:13:06 +07:00
Sabda Yagra 277f1cc805 fix: api key maps google 2026-02-11 18:10:36 +07:00
Sabda Yagra f357a4e8ce fix: api key maps 2026-02-11 18:09:39 +07:00
Sabda Yagra bb91379058 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-02-11 17:13:44 +07:00
Sabda Yagra 740d73d689 fix: satker section all and maps 2026-02-11 17:12:50 +07:00
Sabda Yagra 76c4ed9238 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-29 18:39:52 +07:00
Sabda Yagra 44c6cc6d9d fix: menus for polres 2026-01-29 18:38:34 +07:00
Sabda Yagra 2b8f7b724e Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-22 14:28:34 +07:00
Sabda Yagra 10694547d1 fix: table task mabes=>koorkurator 2026-01-22 14:24:33 +07:00
Sabda Yagra e5e32496ab Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-22 12:03:33 +07:00
Sabda Yagra 78d1fc0d93 fix: schedule live report 2026-01-22 11:59:48 +07:00
Sabda Yagra c99e1a5c7d fix: penugasan mabes=>koorkurator 2026-01-21 10:06:40 +07:00
Sabda Yagra 274fd022e1 Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-13 09:55:09 +07:00
Sabda Yagra 933a672280 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-13 09:53:07 +07:00
Sabda Yagra ce7c343808 fix: taskTA from mabes to koorkurator 2026-01-13 09:52:29 +07:00
hanif salafi cba94cf0a9 feat: rollback using pnpm 2026-01-13 02:20:57 +07:00
Sabda Yagra 7c721dcc96 fix: form create task-ta 2026-01-12 09:09:52 +07:00
Sabda Yagra 78ddd461df Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2026-01-09 20:33:10 +07:00
Sabda Yagra ec2d7eb203 fixing 2026-01-09 20:32:39 +07:00
Sabda Yagra ad5d048e78 fixing 2026-01-09 20:27:20 +07:00
Sabda Yagra 5614b5ada5 fixing 2026-01-09 20:08:13 +07:00
Sabda Yagra f1b59ee537 fix: add xlsx and file-saver dependency 2026-01-09 18:47:26 +07:00
Sabda Yagra 0ad1acee09 fix: button relevant in media tracking and table 2026-01-09 18:35:20 +07:00
Sabda Yagra 2d09af2e8b fix: local dev error 2026-01-09 14:30:51 +07:00
Sabda Yagra edafc223db fix: baseUrl api 2026-01-09 11:11:55 +07:00
Sabda Yagra 933cdb1100 fix: button validate in detail media tracking 2026-01-09 10:22:43 +07:00
Sabda Yagra 280ea508e9 fix: adjust coloumns media tracking 2026-01-06 11:22:13 +07:00
Sabda Yagra 1a0d53bb11 fix: new content typeId 2025-12-31 13:20:22 +07:00
Sabda Yagra be1c799629 fix: change docker 2025-12-29 08:37:26 +07:00
Sabda Yagra 3d23d75e05 fix: change ip docker image 2025-12-26 16:14:10 +07:00
Sabda Yagra 1ee379df71 fix: change docker and jenkins IP 2025-12-24 12:18:41 +07:00
Sabda Yagra 9a1b41e90b fix: change api url 2025-12-17 15:17:01 +07:00
Sabda Yagra fcfa24b06c fix: change ip jenkins 2025-12-17 11:22:20 +07:00
Sabda Yagra cf91ea33ec fix: adjust gitlab ci 2025-12-15 09:31:24 +07:00
Sabda Yagra 1cc7786dee fix: change gitlab ci 2025-12-15 08:36:49 +07:00
Sabda Yagra f1ecebfb0f fix: adjust the dev code to be the same as prod 2025-12-09 12:01:13 +07:00
Sabda Yagra 76b7b4b86e fix: edit link jenkins 2025-12-08 15:43:21 +07:00
Sabda Yagra 3698996c0c fix: adjust code from prod 2025-11-27 08:25:28 +07:00
Sabda Yagra 3349450cb9 fix: type file in download audio and doc 2025-11-26 12:12:19 +07:00
Sabda Yagra 80d07d358f fix: adjust branch to prod 2025-11-26 07:41:13 +07:00
Sabda Yagra f9417b63ae fix: adjust the dev-sabda branch code with the prod branch 2025-11-25 13:51:52 +07:00
Sabda Yagra 8f494739c6 fix: fixing all eror in excel 2025-11-23 22:37:29 +07:00
Sabda Yagra dd94afc81d fixing: bugs in excel 2025-11-19 23:40:31 +07:00
Sabda Yagra 4c32b76ea3 fix: fixing breadcrumbs admin and button in management-user 2025-11-18 22:52:36 +07:00
hanif salafi d21d377e17 fix: update media tracking table 2025-11-18 00:52:51 +07:00
hanif salafi f90c9f73e1 feat: add media rewrite to email 2025-11-18 00:31:00 +07:00
Sabda Yagra 3c14fd2358 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-11-18 00:08:49 +07:00
Sabda Yagra 6951ccd137 fix: update expert section 2025-11-18 00:08:18 +07:00
Sabda Yagra cd822a7b5d Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-11-17 17:53:53 +07:00
Sabda Yagra 68b9439f23 fix: all filter card 2025-11-17 17:53:16 +07:00
Sabda Yagra 6afd7d1183 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-11-17 12:52:44 +07:00
Sabda Yagra 1f7b912b52 fix: rollback commit in thursday 2025-11-17 12:51:14 +07:00
Sabda Yagra ae61d19a61 fix: link href in category, and add isAdmin in update add-experts 2025-11-17 12:12:33 +07:00
Sabda Yagra 3b0bf64e39 fix: redirect /in and /en 2025-11-14 09:26:50 +07:00
Sabda Yagra 0c5a7f86bb feat: new page for satker-content 2025-11-13 23:59:06 +07:00
Sabda Yagra 15a395ccd2 fix: redirect /in and double // 2025-11-13 17:46:23 +07:00
Sabda Yagra c71532cdd5 fix: rollback file placement in spit 2025-11-12 11:59:42 +07:00
Sabda Yagra eaa9001c98 fix: adjust payloadin escalation forward 2025-11-12 00:52:05 +07:00
Sabda Yagra a08841b44c fix: change gitlab-ci 2025-11-11 21:30:40 +07:00
Sabda Yagra e5704c6bec fix: change clickCount filter to favorite and fix pagination in image admin 2025-11-11 21:11:25 +07:00
Sabda Yagra 5b6ffd0863 fix: input search in all content, add menu sidebar in polda and satker approver 2025-11-10 22:06:43 +07:00
Sabda Yagra eaa1628fde fix: file placements in spit 2025-11-08 23:55:07 +07:00
Sabda Yagra dbabd36687 Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign into dev-sabda-v2 2025-11-06 16:18:39 +07:00
Sabda Yagra 618e5f4edb fix: show createdAdd in new-content 2025-11-06 16:18:33 +07:00
hanif salafi 5c7b8b7a34 fix: change clickCount to favorite in landing 2025-11-06 01:35:30 +07:00
Sabda Yagra 78518f038e fix: fixing region in update section all content 2025-11-06 00:06:21 +07:00
Sabda Yagra 9bbbf4e45e fix: fixing filter in all content, image, video, text, audio and spit 2025-11-05 11:35:04 +07:00
Sabda Yagra d730cc9cf9 feat: page regional content filter 2025-10-27 20:14:20 +07:00
Sabda Yagra 30235c240a fix: dockerfile 2025-10-27 11:31:51 +07:00
Sabda Yagra f7b1b79c08 pull main 2025-10-27 10:13:49 +07:00
Sabda Yagra a69d3afa72 Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign into dev-sabda-v2 2025-10-27 10:13:01 +07:00
Sabda Yagra f5802ff248 fix: button in filter regional, load in create video 2025-10-27 10:12:27 +07:00
hanif salafi 16b5bb9fa0 fix: update dockerfile 2025-10-23 21:51:17 +07:00
hanif salafi 6b72b3e606 fix: update dockerfile 2025-10-23 21:38:54 +07:00
hanif salafi abb5e0370f fix: update dockerfile 2025-10-23 16:03:06 +07:00
hanif salafi 6a76f2c0c2 fix: update dockerfile 2025-10-23 15:24:17 +07:00
hanif salafi 2f7bccfea9 fix: update dockerfile 2025-10-23 15:21:41 +07:00
hanif salafi 83cf910a34 fix: update dockerfile 2025-10-23 11:27:03 +07:00
hanif salafi 587f221fe1 fix: add pnpm-lock 2025-10-23 10:37:53 +07:00
hanif salafi c662f3c866 fix: add pnpm-lock 2025-10-23 10:07:25 +07:00
hanif salafi 0d1f8041a9 feat: update Dockerfile 2025-10-23 10:01:14 +07:00
Sabda Yagra 0757ec6519 fix: input search in all content admin 2025-10-22 21:55:14 +07:00
Sabda Yagra ecf4a64b03 fix: button approve in detail conten admin 2025-10-21 11:19:04 +07:00
Sabda Yagra a60e661a6e fix: change domain disastages 2025-10-20 09:51:49 +07:00
Sabda Yagra 942b00521c fix: edit api in category landing page 2025-10-16 15:19:38 +07:00
Sabda Yagra fd6ffda5a3 fix: api in category approver 2025-10-16 12:22:48 +07:00
Sabda Yagra d7e4200f3c fix: edit url api in categories approver 2025-10-15 17:40:32 +07:00
Sabda Yagra c6e0e123c3 feat: add button translate in all content update and upload 2025-10-15 14:02:41 +07:00
Sabda Yagra 92811802cc fix: add button translate in image upload and edit admin, change url api in all filter content 2025-10-15 10:57:41 +07:00
Sabda Yagra 838b492768 fix: hide menu in sidebar satker 2025-10-08 16:49:26 +07:00
Sabda Yagra 3e920e9108 fix: fixing og page detail video 2025-10-08 11:34:15 +07:00
Sabda Yagra 405455ea1f fix:sort in table media tracking 2025-10-06 16:16:45 +07:00
Sabda Yagra 38897c1cc1 fix: data table in campaign 2025-10-06 08:12:01 +07:00
Sabda Yagra ff44a3b837 fix: category in admin 2025-10-03 21:44:55 +07:00
Sabda Yagra 4a5bce9c24 fix: category in admin, broadcast in admin, and rewrite article 2025-10-03 16:36:48 +07:00
Sabda Yagra 5343e67f7d fix: indeks filter car anf placement in polda spit 2025-09-29 19:17:19 +07:00
Sabda Yagra 399eef865f Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign into dev-sabda-v2 2025-09-29 15:18:24 +07:00
Sabda Yagra 92e300a0cf fix: fixing placement in spit polda 2025-09-29 13:39:08 +07:00
hanif salafi b163084dfe feat: update broadcast conig and dashboard report 2025-09-27 12:25:36 +07:00
hanif salafi 745c0b72f2 Merge branches 'main' and 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-09-26 14:51:00 +07:00
Sabda Yagra c65afe8ced feat: add authorization in report download 2025-09-26 14:46:28 +07:00
hanif salafi 94535fa64e Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-09-26 10:17:22 +07:00
Sabda Yagra a96ab8590a fix: prefixpath and link href in all filter 2025-09-26 10:16:00 +07:00
Sabda Yagra 5e82ee8085 fix: href in card document 2025-09-25 16:58:25 +07:00
hanif salafi b34a3d684b Merge branches 'main' and 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-09-25 15:34:31 +07:00
Sabda Yagra d647025a42 fix: fixing link href in content card filter 2025-09-25 15:33:49 +07:00
Sabda Yagra c661f8bbd2 feat: add translate button in update content 2025-09-24 21:53:34 +07:00
hanif salafi 6a89dd3683 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-09-23 17:59:38 +07:00
Sabda Yagra 20f7b19471 fix: edit notification success in media-tracking 2025-09-23 17:42:27 +07:00
hanif salafi 8b5b6aa64e Merge branch 'dev-sabda-v2' into 'main'
fix:href link in card filter

See merge request hanifsalafi/mediahub_redesign!1
2025-09-19 11:30:20 +00:00
Sabda Yagra 49a257cab4 fix:href link in card filter 2025-09-19 18:15:00 +07:00
Sabda Yagra b7e4258773 fix: href link in filter card 2025-09-19 14:32:29 +07:00
Sabda Yagra 683c4cc465 fix: href link in card filter 2025-09-19 13:57:06 +07:00
Sabda Yagra 9cf5498c88 fix: link hred in image-filter-card 2025-09-19 09:52:32 +07:00
Sabda Yagra f50d37f4de pull main 2025-09-19 08:16:32 +07:00
hanif salafi b72e145227 feat: update fixing error regional filter 2025-09-19 02:50:49 +07:00
Sabda Yagra 73c56e764d fix: translate button in approver-int 2025-09-19 00:01:44 +07:00
hanif salafi 6fc2dd70af feat: update regional list path 2025-09-18 20:53:25 +07:00
Sabda Yagra d9475cc0a9 fix: logic download in image detail 2025-09-16 20:00:48 +07:00
Sabda Yagra d471035246 fix: detail video polda 2025-09-16 08:54:05 +07:00
Sabda Yagra 0d15974117 fixing detail video in polda 2025-09-13 13:08:26 +07:00
Sabda Yagra 6230460148 fix: adjust layout in spv menus 2025-09-12 17:51:33 +07:00
Sabda Yagra 6688345b64 pull main 2025-09-12 14:48:11 +07:00
hanif salafi 6991c6d9b9 feat: update placement on media 2025-09-12 14:45:42 +07:00
Sabda Yagra 037f3f374c fixing 2025-09-11 20:59:17 +07:00
Sabda Yagra fdb2a161f6 fix conflict pull main 2025-09-11 09:04:55 +07:00
hanif salafi 82e8dfafc9 feat: update edit media 2025-09-11 08:15:02 +07:00
Sabda Yagra e43ede341f fix: add indeks content in filter regional 2025-09-11 01:22:35 +07:00
Sabda Yagra bffe869f32 fix: fixing update section image, feat: ticketing in supervisor 2025-09-10 20:39:49 +07:00
Sabda Yagra 2c7b46bcc3 hide menus sidebar in polres 2025-09-10 12:21:34 +07:00
Sabda Yagra 397b30aa75 pull main 2025-09-10 12:17:48 +07:00
hanif salafi 78497bfcb1 feat: update fixing glitch swiper spit 2025-09-10 12:05:12 +07:00
Sabda Yagra 92f8a4b030 fixing 2025-09-10 11:53:27 +07:00
Sabda Yagra 3d4860c7a5 pull main 2025-09-10 08:58:51 +07:00
Sabda Yagra b6054fb3f8 fixing 2025-09-10 08:37:25 +07:00
hanif salafi b7a2b445a9 feat: fixing vendor ckeditor5 2025-09-10 08:20:35 +07:00
hanif salafi c25acbf4c5 feat: fixing error 2025-09-10 07:26:46 +07:00
hanif salafi d0336e9ca0 feat: fixing custom editor 2025-09-10 07:17:49 +07:00
hanif salafi 4f27b0f332 feat: fixing error 2025-09-10 07:01:55 +07:00
Sabda Yagra 53af10753c fixing 2025-09-09 22:58:15 +07:00
Sabda Yagra 5fdfccaeed fixing 2025-09-09 22:34:23 +07:00
Sabda Yagra e1bc9ae6d5 fixing 2025-09-09 22:33:29 +07:00
hanif salafi a4846c7c43 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-09-09 01:13:34 +07:00
hanif salafi 26bbe4c003 feat: minor update detail image 2025-09-09 01:13:18 +07:00
Sabda Yagra ce6ca9e853 fixing 2025-09-09 00:26:30 +07:00
Sabda Yagra aee612f3f5 fixing 2025-09-04 21:58:03 +07:00
Sabda Yagra affe1bce94 fixing 2025-09-04 00:05:14 +07:00
Sabda Yagra a11257ef4e fixing 2025-09-03 22:11:27 +07:00
hanif salafi e873af1f56 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-09-03 08:02:19 +07:00
Sabda Yagra 4b69d1cc17 fix: rewrite in spit, placement in create task, and edit tab in task-ta 2025-09-02 21:32:17 +07:00
hanif salafi bc8676d859 feat: update disestages 2025-09-02 08:16:06 +07:00
Sabda Yagra 7595a5126d fix: ckeditor and Table task-ta 2025-09-01 22:12:21 +07:00
hanif salafi 3fee36390d feat: update media broadcast, update blogs 2025-09-01 03:30:13 +07:00
hanif salafi 902f8de516 feat: update ckeditor 2025-09-01 02:06:19 +07:00
hanif salafi df2546bc08 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-08-31 08:01:48 +07:00
Sabda Yagra e60b452780 fix: adjust broadcast admin 2025-08-30 09:28:28 +07:00
hanif salafi 66b592513e Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-08-29 14:22:18 +07:00
Sabda Yagra e5541c3b35 QUDO-374, QUDO-375, QUDO-377, QUDO-380 2025-08-28 20:59:12 +07:00
hanif salafi 690f046bb3 feat: update report 2025-08-27 09:05:31 +07:00
hanif salafi 4dd34f7144 update approval 2025-08-27 08:48:38 +07:00
Sabda Yagra bbed0c29c1 QUDO-365, QUDO-364, QUDO-362, QUDO-363 2025-08-26 23:13:48 +07:00
Sabda Yagra 8ba5499378 fixing detail form all content 2025-08-25 21:25:15 +07:00
Sabda Yagra 5cdde1a75b pull main and fixing 2025-08-25 16:33:36 +07:00
hanif salafi 0b40c99ce9 update save approval 2025-08-25 15:13:16 +07:00
hanif salafi d9fcd2ec32 update detail iamge 2025-08-25 12:16:34 +07:00
hanif salafi f21a5ae389 feat: update image detail 2025-08-25 12:16:14 +07:00
Sabda Yagra 285bcad1d8 fixing 2025-08-23 22:04:54 +07:00
Sabda Yagra e5a2fc2bf1 fix: placement in approval content 2025-08-22 16:23:37 +07:00
Sabda Yagra bb982482c8 fix conflict when pull main 2025-08-22 00:15:39 +07:00
Sabda Yagra 5c3eb2aefa fixing 2025-08-22 00:12:01 +07:00
hanif salafi 3feb406715 feat: update approval modals 2025-08-21 17:56:58 +07:00
hanif salafi 82bf3a8709 update : image detail approval 2025-08-21 10:38:38 +07:00
Sabda Yagra 0c866f9363 pull main 2025-08-21 09:36:10 +07:00
Sabda Yagra 7a4e7cbefe fixing 2025-08-20 22:43:10 +07:00
hanif salafi 9fc973d151 update : selection box user levels on agenda-settings and task 2025-08-19 10:01:29 +07:00
hanif salafi 78239c5f9d update : selection box user levels 2025-08-19 08:01:20 +07:00
hanif salafi c12dbf1596 update : selection box user levels 2025-08-19 07:54:40 +07:00
Sabda Yagra 24a5e2a5c1 fix: add checkbox wilayah in aprove content, add select date in dashboard generate report 2025-08-19 00:07:26 +07:00
Sabda Yagra 5ae245c123 fix: agenda setting 2025-08-15 22:23:33 +07:00
Sabda Yagra 71f0b3b52e fix: link in broadcast 2025-08-15 14:29:37 +07:00
Sabda Yagra bd5d87c7ce fix: auto scroll in modal landing 2025-08-15 13:50:20 +07:00
Sabda Yagra fb976f01bb fix: styling in hero and add imageblurry 2025-08-14 21:19:12 +07:00
Sabda Yagra 7e1ffe2d2f fixing 2025-08-14 10:38:00 +07:00
Sabda Yagra cd7f7aec86 fix: add /in in link broadcast 2025-08-14 09:35:01 +07:00
Sabda Yagra 8b77700af8 fixing 2025-08-13 18:44:58 +07:00
Sabda Yagra b891591e7a fix: typo in path navbar, and image blury in modal 2025-08-13 13:10:45 +07:00
Sabda Yagra aa7fc8c6d4 fix: video player in content spit 2025-08-12 19:40:07 +07:00
Sabda Yagra 000945c33e fix: path for navbar indeks and schedule 2025-08-12 13:53:27 +07:00
Sabda Yagra eae930552a fix: resend otp 2025-08-11 15:29:27 +07:00
Sabda Yagra 1cbfd35a76 fix: detail vidio admin and ticketing 2025-08-11 11:03:32 +07:00
Sabda Yagra 74cae6134b fix: search all filter toLowerCase 2025-08-08 12:09:06 +07:00
Sabda Yagra 3bf251b53c fixing 2025-08-07 23:22:18 +07:00
Sabda Yagra 74265cb797 fix: spit tags edit 2025-08-07 10:31:35 +07:00
Sabda Yagra 54633d8936 fixing 2025-08-06 22:24:30 +07:00
Sabda Yagra c1aec7f316 fixing 2025-08-05 22:54:26 +07:00
Sabda Yagra 4508f5db37 fix: iklan admin 2025-08-05 12:25:43 +07:00
Sabda Yagra ab8f0c1086 fix: iklan admin 2025-08-05 11:41:42 +07:00
Sabda Yagra ac6f100047 fix: card in filter video and foto 2025-08-04 13:16:32 +07:00
Sabda Yagra 035f33250e fix: download selected image in detail 2025-08-04 12:22:06 +07:00
Sabda Yagra 16c469f72c fix: detail video vertical 2025-08-04 10:38:49 +07:00
Sabda Yagra a1bcadec86 fix: height video player when video vertical 2025-08-01 21:43:39 +07:00
Sabda Yagra 6fa5a05624 fix: hide button edit and delete if status done 2025-08-01 15:19:58 +07:00
Sabda Yagra cfc47a00a8 fix: fixing detail content and update content contributro 2025-08-01 11:50:31 +07:00
Sabda Yagra 5bc8099e11 feat: checkbox delete in content spit 2025-07-31 19:58:16 +07:00
Sabda Yagra 895cef6c75 fixing 2025-07-31 12:36:17 +07:00
Sabda Yagra a6683014c2 pull main 2025-07-30 23:16:54 +07:00
Sabda Yagra f572c8110d fixing update 2025-07-30 23:15:43 +07:00
Sabda Yagra d27806ba40 fixing 2025-07-29 21:57:51 +07:00
Sabda Yagra 9dd9f621ce fix: all filter search index 2025-07-29 21:57:36 +07:00
Sabda Yagra b7dce8c13c feat: add button see All 2025-07-29 10:29:47 +07:00
hanif salafi 12668edd3d Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-29 10:18:53 +07:00
Sabda Yagra e1ac7cc688 fixing 2025-07-28 21:35:05 +07:00
hanif salafi b20610369f Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-28 15:48:07 +07:00
Sabda Yagra 5c340a05a2 fixing 2025-07-28 15:16:03 +07:00
Anang Yusman f151ccecba merge 2025-07-28 14:18:59 +08:00
Anang Yusman f5477357e6 update broadcast 2025-07-28 14:05:59 +08:00
Sabda Yagra 2f7461be46 pull main 2025-07-28 08:32:29 +07:00
Sabda Yagra 76f529a5c9 fix: fixing modal time 2025-07-28 08:24:13 +07:00
Anang Yusman 4f20e4be57 merge 2025-07-25 16:18:16 +08:00
Anang Yusman 232c9f72b5 add indeks filter, fix thumbnail video 2025-07-25 16:17:35 +08:00
hanif salafi 65d1aaf3fe feat: update popup data 2025-07-24 21:15:06 +07:00
hanif salafi 1e4d324963 update ckeditor 2025-07-24 16:22:33 +07:00
Anang Yusman 4def21dcb1 merge 2025-07-24 16:36:54 +08:00
Anang Yusman 5216bcd8a5 update filter status admin, fix button delete 2025-07-24 16:36:05 +08:00
hanif salafi c4c854a37a fixing error 2025-07-24 15:16:02 +07:00
hanif salafi 66cfb09a46 fixing error 2025-07-24 14:52:59 +07:00
hanif salafi 6590743d90 fixing error 2025-07-24 14:37:12 +07:00
hanif salafi 2d6ef04f45 fixing error 2025-07-24 14:19:15 +07:00
Anang Yusman f878ab405e merge 2025-07-24 14:14:03 +08:00
Anang Yusman f434ba21ff update filter 2025-07-24 14:13:15 +08:00
hanif salafi 450c54db11 feat: update docker file & package 2025-07-24 12:51:01 +07:00
hanif salafi 8feeaf7702 feat: update fixing error 2025-07-24 12:17:27 +07:00
hanif salafi c4c3d54a70 feat: update management user 2025-07-24 12:00:40 +07:00
hanif salafi 2597abd40b Merge branches 'dev-landing-v2' and 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-24 11:34:37 +07:00
Rama Priyanto cfd89ea3f8 init locale in 2025-07-24 11:26:45 +07:00
Sabda Yagra 373063e23a fix: spit button 2025-07-23 22:21:43 +07:00
Sabda Yagra 96a3fd91cf fix: spit button 2025-07-23 22:21:11 +07:00
hanif salafi 538376a687 feat: update gitlab ci 2025-07-23 21:25:22 +07:00
hanif salafi 64729c9339 feat: update gitlab ce 2025-07-23 21:24:06 +07:00
Sabda Yagra 13dcd5cd26 feat: auto change in right bottom banner 2025-07-23 21:08:27 +07:00
Sabda Yagra 0873cef3ce fixing banner landing 2025-07-23 17:34:08 +07:00
Sabda Yagra 2c18d422d5 pull main 2025-07-23 16:21:47 +07:00
hanif salafi 8645f913bf update fixing 2025-07-23 16:14:57 +07:00
Sabda Yagra 98025cbab1 fix: banner and popUp 2025-07-23 15:25:46 +07:00
Sabda Yagra 53f2ebab41 fix: create spit 2025-07-23 11:15:19 +07:00
Sabda Yagra a9bba5ced6 pull main 2025-07-22 22:07:51 +07:00
Sabda Yagra 549eec65cc fix: fixing filter nd pagination in spit 2025-07-22 22:07:18 +07:00
hanif salafi 98de288be5 feat: update package-lock 2025-07-22 20:24:21 +07:00
Sabda Yagra 969eceab14 fixing 2025-07-22 19:14:14 +07:00
Sabda Yagra b96f538184 fixing 2025-07-22 19:09:16 +07:00
Sabda Yagra 80e426570e pull main 2025-07-22 14:13:22 +07:00
Sabda Yagra 6650eb4a95 fixing 2025-07-22 14:12:16 +07:00
Rama Priyanto a10a2108df Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-22 14:11:52 +07:00
Rama Priyanto 9563ad5987 fix:content filter query 2025-07-22 14:11:38 +07:00
hanif salafi 994d2a57d0 feat: remove detail accountlist 2025-07-22 12:26:51 +07:00
hanif salafi ae1eba7beb feat: update dockerfile 2025-07-22 12:09:03 +07:00
Rama Priyanto 5807e389b4 skip auth 2025-07-22 10:29:28 +07:00
Rama Priyanto 504101b91c fix:handle empty file placement & target publish spit 2025-07-22 09:29:16 +07:00
Sabda Yagra e0bb77e7af fix: fixing filter and add thumbnail in admin update 2025-07-21 22:23:30 +07:00
hanif salafi 933bbc402b Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-21 20:07:21 +07:00
Sabda Yagra 481cf1a2b4 fix: fix buttun edit in dashboard admin 2025-07-21 16:19:02 +07:00
hanif salafi eeff4cc059 feat: update image opengraph 2025-07-21 15:24:01 +07:00
Rama Priyanto f42f6b8388 fix:tiny to ckeditor 2025-07-21 15:12:11 +07:00
hanif salafi 803960cf62 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-21 14:01:36 +07:00
hanif salafi 3f8ff614f8 feat: fixing detail metadata 2025-07-21 14:01:10 +07:00
Sabda Yagra b03da630ba fix: fixing point 1 -4 2025-07-21 13:55:24 +07:00
hanif salafi 055c39f8d8 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-21 11:07:24 +07:00
hanif salafi d4d094ec95 feat: update form description 2025-07-21 11:07:08 +07:00
Sabda Yagra 6c5bb50661 feat: edit checkbox in create task 2025-07-21 10:43:40 +07:00
hanif salafi fd021426d1 feat: update campaign detail 2025-07-20 23:56:02 +07:00
Sabda Yagra 968f2642cc fixing 2025-07-19 19:51:23 +07:00
Sabda Yagra 978c8b364f fixing 2025-07-19 01:06:24 +07:00
Sabda Yagra 00ddca22b9 fixing 2025-07-18 21:59:02 +07:00
Sabda Yagra 2e75d679b3 pull main 2025-07-18 16:28:57 +07:00
Sabda Yagra 9f32d3dde5 fixing 2025-07-18 16:27:17 +07:00
hanif salafi 008c8e61a8 feat: update user role routing 2025-07-18 16:09:57 +07:00
hanif salafi 38c5b513ff feat: update login feature 2025-07-18 15:24:36 +07:00
hanif salafi 1ef498179c feat: update side home landing 2025-07-18 15:08:27 +07:00
Sabda Yagra 377c22e083 fix: create content imgae 2025-07-18 09:33:32 +07:00
Sabda Yagra b8827ba8a8 fix conflict 2025-07-17 08:34:18 +07:00
Sabda Yagra cabbd2d1da fixing 2025-07-17 08:02:30 +07:00
hanif salafi e6a093bbaa Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-17 02:32:42 +07:00
hanif salafi 1fb792402d feat: fixing report 2025-07-17 02:32:28 +07:00
Sabda Yagra 4675b997f4 fixing 2025-07-17 01:44:37 +07:00
hanif salafi 518cdbffd3 feat: update category selection on update/detail for all media types 2025-07-17 00:26:46 +07:00
Sabda Yagra 78a4a08d4a fixing 2025-07-16 18:01:50 +07:00
Sabda Yagra 76998748b1 fixing 2025-07-16 17:26:23 +07:00
Sabda Yagra 80169209a2 url detail-video 2025-07-16 08:03:50 +07:00
Sabda Yagra ced4fae0da pull main 2025-07-15 16:21:46 +07:00
Sabda Yagra 6c7b32fcc3 fixing 2025-07-15 16:20:59 +07:00
Sabda Yagra 1c0e920b5d fixing 2025-07-15 16:15:34 +07:00
hanif salafi 249de7b595 feat: rollback forgot password 2025-07-15 10:37:30 +07:00
hanif salafi 317c5ebd92 feat: update hero landing 2025-07-15 07:12:44 +07:00
hanif salafi ddc2484cc3 feat: update banner and pop up 2025-07-15 01:08:27 +07:00
Sabda Yagra aea083c0ea fixing 2025-07-14 23:34:52 +07:00
hanif salafi 4250c2ec78 feat: fixing some error 2025-07-14 22:55:51 +07:00
hanif salafi dab5958ac2 feat: fixing error, etc 2025-07-14 17:14:20 +07:00
hanif salafi 538d941976 feat: update refactoring plan 2025-07-14 09:47:25 +07:00
hanif salafi a996c2623d add : starting improvement, add component refactoring 2025-07-13 21:58:07 +07:00
hanif salafi cd80bd07cb feat: change expired library, update registration and login feature 2025-07-13 04:14:12 +07:00
hanif salafi e280a68635 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-07-13 02:14:39 +07:00
hanif salafi 68c251c1f7 feat: delete all unused page / component 2025-07-13 02:12:09 +07:00
Sabda Yagra 2602e831e3 fixing 2025-07-12 18:52:36 +07:00
Sabda Yagra e047b97e58 pull main 2025-07-08 15:35:09 +07:00
Sabda Yagra 7186ecf568 fixing 2025-07-08 15:32:39 +07:00
hanif salafi 1982679255 feat: fixing login error 2025-07-08 13:42:24 +07:00
hanif salafi bee5dcadb6 feat: fixing http service & config 2025-07-08 11:41:50 +07:00
Sabda Yagra 47402ea655 fixing 2025-07-07 22:15:24 +07:00
Sabda Yagra 426a9e5c39 pull main 2025-07-07 15:03:59 +07:00
hanif salafi 92af4a4f6d feat: update hero modal and category 2025-07-07 02:20:22 +07:00
hanif salafi 1bb6363a69 feat: update image priority 2025-07-06 21:57:09 +07:00
hanif salafi d5303c4919 feat: update translation with defaultValue 2025-07-06 16:23:45 +07:00
hanif salafi 0d12b105fb feat: delete documentation change 2025-07-06 12:57:25 +07:00
hanif salafi d5fdc5b4af Edit most-sales.tsx 2025-07-05 03:12:05 +00:00
hanif salafi 381918d85a Edit Dockerfile 2025-07-05 03:04:04 +00:00
hanif salafi fe4ca9052a update: major fixing, remove unused library, remove large library, etc 2025-07-05 02:42:42 +07:00
Sabda Yagra 11eef893f4 pull main 2025-07-04 12:34:07 +07:00
Sabda Yagra 2bf469b721 fixing 2025-07-04 12:32:55 +07:00
hanif salafi 1f85dccc83 feat: update auto play schedule 2025-07-03 10:00:31 +07:00
hanif salafi e365aa8007 feat: update schedule on landing 2025-07-03 02:13:31 +07:00
Sabda Yagra 19f1a59feb QUDO-273 2025-06-30 22:15:39 +07:00
Sabda Yagra 576b129cc9 copy vendor 2025-06-30 15:51:20 +07:00
Sabda Yagra 836a45f364 QUDO-275 2025-06-30 15:41:41 +07:00
Sabda Yagra 9dc4b64066 QUDO-270 2025-06-30 13:25:00 +07:00
Sabda Yagra 0f9c070af1 pull main 2025-06-30 10:52:44 +07:00
Sabda Yagra 9345345e02 pull main 2025-06-26 09:59:32 +07:00
Sabda Yagra fbd5586ae6 fixing 2025-06-25 21:05:02 +07:00
Sabda Yagra 3c6358fa62 merge dev-sabda-v2 2025-06-25 11:10:40 +07:00
Sabda Yagra 82a4233fb1 QUDO-285 2025-06-25 10:28:45 +07:00
Anang Yusman 69bed8b61b merge 2025-06-24 16:25:10 +08:00
Anang Yusman 6f6dea7def fix login 2025-06-24 16:24:37 +08:00
hanif salafi 2e87fb3b14 Merge branch 'dev-anang' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-24 15:22:00 +07:00
hanif salafi 3fa6376f2f Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-24 15:21:32 +07:00
Anang Yusman a98d16008c merge 2025-06-24 16:20:13 +08:00
Anang Yusman 81a4aaee88 feat:contest 2025-06-24 16:19:46 +08:00
hanif salafi af7f5b3226 feat: update banner on landing 2025-06-24 14:09:00 +07:00
hanif salafi ecb869ff51 feat : update fixing banner in landing 2025-06-24 14:07:34 +07:00
hanif salafi 85e72aa17a Merge branch 'dev-rama' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-24 13:53:13 +07:00
hanif salafi b14cc43bbe Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-24 13:52:28 +07:00
Sabda Yagra 484ebd87d3 fixing 2025-06-24 13:34:18 +07:00
hanif salafi 21d06c0e8a feat: fixing thumbnail 2025-06-24 13:34:09 +07:00
Rama Priyanto e3df511890 fix:supervisor, feedback create edit delete, knowledge base creat edit delete 2025-06-24 13:33:38 +07:00
Sabda Yagra 6490679901 fixing 2025-06-24 11:26:58 +07:00
Sabda Yagra 92bba180ef fixing 2025-06-24 11:25:39 +07:00
hanif salafi 19849e6088 feat : fixing dashboard analityc 2025-06-24 09:55:38 +07:00
hanif salafi 89375f00b1 feat: fixing set banner and pop up (minor) 2025-06-24 09:43:08 +07:00
hanif salafi 4ef9d5b513 feat: update visualization, reports, and userway 2025-06-24 02:01:30 +07:00
hanif salafi 199aaf5808 Merge branch 'dev-anang' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-23 23:22:51 +07:00
hanif salafi ce5b84dbd9 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-23 23:22:29 +07:00
Sabda Yagra 74b24e7377 QUDO-265, QUDO-267, QUDO-268, QUDO-277, QUDO-269 2025-06-23 19:15:31 +07:00
Anang Yusman f5fddd3294 ckeditor 2025-06-23 18:26:37 +08:00
Anang Yusman d706a898b8 feat:detail agenda setting, detail edit penugasan Ta 2025-06-23 18:22:15 +08:00
hanif salafi b60ac3a9b4 feat: update category, admin settings, management user 2025-06-22 12:08:08 +07:00
hanif salafi 4d3aaab4e2 feat : update spit convert and setting banner & popup 2025-06-22 09:01:48 +07:00
hanif salafi c187ba7486 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-22 06:02:06 +07:00
Sabda Yagra 1103f5f42f fixing 2025-06-21 20:17:25 +07:00
hanif salafi d98836494e Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-21 11:43:54 +07:00
Sabda Yagra 6cb2838015 fixing 2025-06-20 21:43:19 +07:00
hanif salafi 66b97a9af6 Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-20 16:57:39 +07:00
hanif salafi 571a58c2bd feat: update column content 2025-06-20 16:57:25 +07:00
Sabda Yagra 32f9bf6098 pull 2025-06-20 09:05:30 +07:00
Sabda Yagra 54bc7e36eb fixing run build 2025-06-20 09:01:54 +07:00
hanif salafi 27954102ec Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-19 22:06:25 +07:00
Sabda Yagra 2dbbbe2a4d fixing 2025-06-19 20:31:47 +07:00
Anang Yusman a2086f66f2 merge 2025-06-18 19:13:09 +08:00
Anang Yusman 06371c4782 category spit 2025-06-18 19:12:37 +08:00
Sabda Yagra c64c6c3685 QUDO-257 2025-06-18 13:59:33 +07:00
hanif salafi e9c57fdd75 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-18 13:23:01 +07:00
Anang Yusman 81c3c1e781 feat:update create content rewrite 2025-06-18 14:19:30 +08:00
Sabda Yagra 89c42c0f24 pull main 2025-06-16 17:54:56 +07:00
Sabda Yagra 98b70b0b24 QUDO-246 2025-06-16 17:54:09 +07:00
Anang Yusman d2658b294b merge 2025-06-16 16:10:08 +08:00
Anang Yusman 5875b64069 feat: menu sidebar,emergency issue 2025-06-16 16:07:58 +08:00
hanif salafi 6aaa433379 Merge branch 'dev-anang' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-16 08:06:04 +07:00
hanif salafi 6ca0e05ab6 feat: update url redirect 2025-06-16 08:05:50 +07:00
Anang Yusman e6e9c84468 feat:update popup 2025-06-15 18:21:39 +08:00
Anang Yusman 0c15d97aa4 merge 2025-06-14 09:16:39 +08:00
Anang Yusman 0f2fd15e8c feat:form add expert,list popup 2025-06-14 09:15:59 +08:00
hanif salafi d5be139635 Merge branch 'dev-anang' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-13 16:23:40 +07:00
Anang Yusman 45a7ec116a feat:escalation,video 2025-06-13 16:21:08 +08:00
Anang Yusman cfbc6349cc feat:fix komunikasi,indeks 2025-06-12 14:43:42 +08:00
Sabda Yagra 4ea2519ab0 fix: QUDO-158 2025-06-12 11:21:53 +07:00
Anang Yusman 16c6d2f565 feat:merge 2025-06-11 18:19:30 +08:00
Anang Yusman c9c3297851 feat:task-ta 2025-06-11 18:18:56 +08:00
Sabda Yagra 51142c1ac8 fix: language 2025-06-11 13:11:18 +07:00
Sabda Yagra 5477eca3d0 fix: language 2025-06-11 13:10:40 +07:00
Anang Yusman 173227c35a merge 2025-06-11 14:09:27 +08:00
Anang Yusman 6d9140cdb4 feat:task-ta 2025-06-11 14:08:44 +08:00
Rama Priyanto 73bd371fbc Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign into dev-rama 2025-06-11 12:26:38 +07:00
Rama Priyanto 5418d97ad5 fix:input file validator 2025-06-11 12:26:16 +07:00
Anang Yusman 75f8485dd9 fix validasi otp 2025-06-11 12:44:17 +08:00
Anang Yusman dbc5a396e5 feat:merge 2025-06-11 12:38:48 +08:00
Anang Yusman ddad28f1ea feat:update tenaga ahli 2025-06-11 12:38:13 +08:00
Sabda Yagra 481cc9f930 pull main 2025-06-11 10:53:08 +07:00
Sabda Yagra a96912cbcf pull main 2025-06-11 10:51:46 +07:00
Sabda Yagra 6e353b1fce fix: menghapus liputan wilayah&satker pada halaman polda & satker, memperbaiki kalender dinamis 2025-06-11 10:51:03 +07:00
hanif salafi 13f25f94a7 feat : update mediahub international on landing and approver 2025-06-11 10:47:52 +07:00
Sabda Yagra 6f853d4680 fix: fixing slug polda kalimantan-utara 2025-06-11 09:51:03 +07:00
Anang Yusman f7b3c05a27 feat:fix communication detail 2025-06-10 14:53:39 +08:00
Anang Yusman 8c179ecfc8 merge 2025-06-10 12:56:18 +08:00
Anang Yusman d38bb005a1 feat:emergency issue 2025-06-10 12:55:36 +08:00
hanif salafi 05bf7ab9f8 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-09 22:38:31 +07:00
hanif salafi cbbf440ec8 Merge branch 'dev-rama' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-09 22:37:47 +07:00
Sabda Yagra c3b769c719 fix: latest fix 2025-06-09 16:51:16 +07:00
Sabda Yagra 9df1748d86 fix: latest fix 2025-06-09 16:50:30 +07:00
hanif salafi 7487711086 Merge branch 'dev-anang' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-09 16:06:51 +07:00
hanif salafi c669663dab Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-09 16:06:32 +07:00
hanif salafi 874cc9604d feat : update add-expert & live report table 2025-06-09 16:06:09 +07:00
Rama Priyanto f8829985f6 fix:pagination media traking-tracking berita, setting banner, media tracking result 2025-06-09 16:04:03 +07:00
Anang Yusman c064026779 fix experts 2025-06-09 16:41:30 +08:00
Anang Yusman 901e015142 feat:merge 2025-06-09 15:42:45 +08:00
Anang Yusman a7d2cacd99 fix:tambah tenaga ahli admin 2025-06-09 15:41:13 +08:00
Rama Priyanto 48d24e76b6 Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign into dev-rama 2025-06-09 14:16:54 +07:00
Rama Priyanto 8515a9956e fix:penugasan redirect, approval image 2025-06-09 14:16:37 +07:00
Sabda Yagra 912d728453 fix: tbnews + mediahub 2025-06-09 13:09:47 +07:00
Sabda Yagra 219cfa7312 fix: tbnews 2025-06-09 11:58:36 +07:00
Sabda Yagra 4ab11ea377 QUDO-159 2025-06-09 11:06:15 +07:00
Sabda Yagra aeea40392c QUDO-159 2025-06-09 11:04:59 +07:00
Anang Yusman 30a97afcfb feat:data tablue 2025-06-09 10:01:46 +08:00
Anang Yusman fadb699f2f merge 2025-06-08 23:42:04 +08:00
Anang Yusman 575ecf02d8 fix login form 2025-06-08 23:41:06 +08:00
hanif salafi 0fe0971628 Merge branch 'dev-anang' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-08 22:13:16 +07:00
hanif salafi 0526fca68c feat: fixing management user admin 2025-06-08 22:12:44 +07:00
Anang Yusman 0f9d84c137 merge 2025-06-08 22:56:34 +08:00
Anang Yusman f3abd767ba feat:delete iklan 2025-06-08 22:54:54 +08:00
hanif salafi 6cbd29bfe5 feat: fixing home UI 2025-06-08 21:36:16 +07:00
hanif salafi fd45d0754d feat : fixing article image filter/list in landing 2025-06-08 21:25:35 +07:00
Anang Yusman 1f7c07121a merge 2025-06-08 19:07:16 +08:00
Anang Yusman 1fc21c8d2b fix ads flow 2025-06-08 19:06:17 +08:00
hanif salafi c3dd46c0d6 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-08 16:55:07 +07:00
hanif salafi 2d57cd606a feat : fixing calendar view of agenda-settings 2025-06-08 16:53:46 +07:00
Sabda Yagra d653ac9eee save conflict 2025-06-08 15:01:07 +07:00
Sabda Yagra 8ffacb60d3 pull main 2025-06-08 14:56:39 +07:00
Sabda Yagra 2d84b14d86 fix: fixing logo satker and add polda papua barat daya and papua tengah 2025-06-08 14:49:20 +07:00
hanif salafi d7747da00b feat: disable react lenis in modals & fixing color of calendar agenda-settings 2025-06-08 13:17:11 +07:00
Anang Yusman a241c28a95 feat:pull main 2025-06-07 23:57:14 +08:00
hanif salafi 15d7fe3da4 Merge branch 'dev-sabda-v2' of https://gitlab.com/hanifsalafi/mediahub_redesign 2025-06-07 22:19:45 +07:00
Sabda Yagra 87aa6c3e34 pull main 2025-06-07 20:30:50 +07:00
Sabda Yagra 67c9147eea [QUDO-138, QUDO-149, QUDO-155] 2025-06-07 20:28:41 +07:00
Anang Yusman b5fe86821a merge 2025-06-07 20:37:06 +08:00
Anang Yusman 8980d8d77d feat:fix spit,ui executive 2025-06-07 20:35:53 +08:00
Rama Priyanto 1c33c0cdfe Merge branch 'main' of https://gitlab.com/hanifsalafi/mediahub_redesign into dev-rama 2025-06-07 18:16:57 +07:00
Rama Priyanto b238d5f396 fix:auth enable otp 2025-06-07 18:15:44 +07:00
Rama Priyanto 289e05dcc1 [QUDO-153] fix:upload content polda polres 2025-06-07 17:27:37 +07:00
hanif salafi b2bcbc5ae4 feat: update penugasan TA 2025-06-07 15:28:26 +07:00
Rama Priyanto a8a9a987eb [QUDO-151] feat:approval histori, fix:status content table 2025-06-07 12:36:19 +07:00
hanif salafi 98a65aaff8 feat : update admin form ads & ads on landing 2025-06-07 03:18:18 +07:00
hanif salafi 7fc65833b0 feat: update advertisements and calendars in landing 2025-06-07 02:59:54 +07:00
Anang Yusman 43ae5c480a merge 2025-06-06 00:12:39 +08:00
Anang Yusman d5f697ddfa feat:update tambah Ta,Spit,fix routing 2025-06-06 00:11:10 +08:00
Rama Priyanto 2e6a2beb5c feat:kotak saran 2025-06-05 22:47:52 +07:00
Sabda Yagra 8741b08787 fix: logo satker navbar 2025-06-05 19:37:53 +07:00
Rama Priyanto 9231546780 [QUDO-154] - fix:create live report form & flow, polda sidebar 2025-06-05 18:16:40 +07:00
Sabda Yagra f51469942e fix: image satker 2025-06-05 18:13:32 +07:00
Sabda Yagra cc9a1a7d55 repush logo satker eror 2025-06-05 16:48:11 +07:00
Sabda Yagra 86394cc4b6 repush logo satker 2025-06-05 16:23:46 +07:00
Sabda Yagra f6abd4ebc4 fix: sidebarLeft Fixing 2025-06-05 15:06:02 +07:00
Sabda Yagra caa0c60a8a pull main 2025-06-05 13:54:55 +07:00
Sabda Yagra 17a37e00d0 fix: fixing logo satker and polda in navbar 2025-06-05 13:49:13 +07:00
Rama Priyanto 9703c25ea2 install 2025-06-05 11:51:09 +07:00
Sabda Yagra ea6eacadbe pull main 2025-06-05 10:07:34 +07:00
Sabda Yagra cbc1d02cf4 pull main 2025-06-05 09:58:20 +07:00
Sabda Yagra f8c138f53b fix: navbar, berita satke and polda, content-catergory 2025-06-05 09:48:13 +07:00
Anang Yusman f574d97523 [QUDO-140,QUDO-139] feat:fix image iklan, add carousel prev,next banner 2025-06-05 01:06:44 +08:00
Sabda Yagra a01a839d5e merge 2025-06-04 14:29:57 +07:00
Sabda Yagra 84ef1dc935 feat: logo in satker and polda 2025-06-04 13:38:28 +07:00
Anang Yusman 150bdc339e feat:fix error 2025-06-04 11:42:23 +08:00
Anang Yusman 88501ac84f feat:update calendar-polri 2025-06-03 23:13:51 +08:00
Anang Yusman 5b6ba3e23d merge 2025-06-03 13:47:44 +08:00
Anang Yusman 882c77b96f feat:update content-rewrite 2025-06-03 13:46:06 +08:00
Sabda Yagra 33566e012f pull main 2025-06-03 10:55:09 +07:00
Sabda Yagra 3e4bae16b2 fixing eror 2025-06-03 10:35:09 +07:00
Anang Yusman 649aa4c638 feat:fix content rewrite 2025-06-03 11:34:12 +08:00
Sabda Yagra 35462046ae conflict 2025-06-03 09:54:15 +07:00
Sabda Yagra 853ec99014 run build fixing 2025-06-03 09:43:37 +07:00
Anang Yusman 08e3ca8662 fix media tracking 2025-06-03 10:10:12 +08:00
Anang Yusman 20753e53b1 [QUDO-134,QUDO-133,QUDO-130] feat:ui iklan, ui kalender, change API AI content rewrite 2025-06-03 09:01:25 +08:00
Anang Yusman 7b35a9b3da [QUDO-133,QUDO-130,QUDO-134] feat:ui kalender,ui iklan, change api AI content rewrite 2025-06-03 08:59:48 +08:00
Sabda Yagra 4a716dd637 fixing landing page polda and satker 2025-06-02 13:11:34 +07:00
Sabda Yagra 9ec6319b8f adjust conflict 2025-06-02 08:25:17 +07:00
Sabda Yagra b878e762fb pull main 2025-06-02 08:24:19 +07:00
Sabda Yagra 4657f79556 adjust landing page mabes 2025-06-02 08:00:01 +07:00
Anang Yusman 883abc9402 [QUDO-129,QUDO-122] feat:live report and media tracking 2025-05-30 23:24:39 +08:00
Anang Yusman 76cc8ca099 feat:add live report,kalender polri,popup,iklan 2025-05-29 18:16:54 +08:00
Anang Yusman 6a3bb288c9 [QUDO-108,QUDO-109,QUDO-110,QUDO-111,QUDO-112]feat:add menu media tracking,fix otp,fix banner and popup 2025-05-27 15:22:59 +08:00
Anang Yusman a2cee179d5 feat:status table konten 2025-05-26 20:14:15 +08:00
13128 changed files with 273008 additions and 210751 deletions

45
.drone.yml Normal file
View File

@ -0,0 +1,45 @@
kind: pipeline
type: ssh
name: mediahub-fe-build-deploy
server:
host:
from_secret: ssh_host
user:
from_secret: ssh_user
ssh_key:
from_secret: ssh_key
steps:
- name: prepare repo
when:
branch:
- dev-sabda-v2
- main
- prod
commands:
- rm -rf /opt/build/mediahub-fe
- mkdir -p /opt/build
- cd /opt/build
- git clone http://38.47.180.165:3000/mediahub/mediahub-fe.git
- name: build image
when:
branch:
- dev-sabda-v2
- prod
commands:
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
- cd /opt/build/mediahub-fe
- docker build -t 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH .
- docker push 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH
- name: deploy
when:
branch:
- prod
commands:
- docker pull 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH
- docker stop new-mediahub-fe || true
- docker rm new-mediahub-fe || true
- docker run -dt -p 4200:3000 --restart always --name new-mediahub-fe 38.47.180.165:3000/mediahub/mediahub-fe:$DRONE_BRANCH

5
.env
View File

@ -1,2 +1,3 @@
NEXT_PUBLIC_API=https://netidhub.com/api
NEXT_PUBLIC=https://netidhub.com
NEXT_PUBLIC_API=https://new.netidhub.com/api
NEXT_PUBLIC=https://new.netidhub.com
NEXT_PUBLIC_TINYMCE_API_KEY=bhteuja26yz5p0aubxry9b95hs33amgn65kjv5km0fd5iuev

View File

@ -1,29 +1,30 @@
stages:
- build
- deploy
build-dev:
stage: build
when: on_success
only:
- main
image: docker:stable
- dev-landing-v2
image:
name: docker:25.0.3-cli
services:
- name: docker:dind
command: ["--insecure-registry=103.82.242.92:8900"]
- name: docker:25.0.3-dind
command: ["--insecure-registry=38.47.185.86:8900"]
script:
- docker logout
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
- docker build -t 103.82.242.92:8900/mediahub/new-mediahub-fe:dev .
- docker push 103.82.242.92:8900/mediahub/new-mediahub-fe:dev
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
- docker build -t 38.47.185.86:8900/mediahub/new-mediahub-fe:dev .
- docker push 38.47.185.86:8900/mediahub/new-mediahub-fe:dev
auto-deploy:
stage: deploy
when: on_success
only:
- main
- dev-landing-v2
image: curlimages/curl:latest
services:
- docker:dind
script:
- curl --user admin:$JENKINS_PWD http://38.47.180.165:8080/job/auto-deploy-new-mediahub-fe/build?token=autodeploynewmediahub
- curl --user admin:$JENKINS_PWD http://38.47.185.86:8080/job/auto-deploy-new-mediahub-fe/build?token=autodeploynewmediahub

View File

@ -1,13 +1,14 @@
{
"ci": {
"collect": {
"staticDistDir": "./public"
},
"assert": {
"assertions": {}
},
"upload": {
"target": "temporary-public-storage"
"ci": {
"collect": {
"staticDistDir": "./public"
},
"assert": {
"assertions": {}
},
"upload": {
"target": "temporary-public-storage"
}
}
}
}

View File

@ -1,44 +0,0 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Testing
coverage
# Next.js
.next/
out/
build
dist
# Misc
.DS_Store
*.pem
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Vercel
.vercel
# Typescript
*.tsbuildinfo
next-env.d.ts
# PWA
**/public/sw.js
**/public/workbox-*.js
**/public/worker-*.js
**/public/sw.js.map
**/public/workbox-*.js.map
**/public/worker-*.js.map

View File

@ -1,28 +0,0 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss", "@ianvs/prettier-plugin-sort-imports"],
"importOrder": [
"^(react/(.*)$)|^(react$)",
"^(next/(.*)$)|^(next$)",
"<THIRD_PARTY_MODULES>",
"",
"^@/types/(.*)$",
"^@/config/(.*)$",
"^@/lib/(.*)$",
"^@/hooks/(.*)$",
"^@/components/ui/(.*)$",
"^@/components/(.*)$",
"^@/styles/(.*)$",
"^@/app/(.*)$",
"",
"^[./]"
],
"importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"]
}

View File

@ -10,8 +10,9 @@ RUN npm install -g pnpm
# Membuat direktori aplikasi dan mengatur sebagai working directory
WORKDIR /usr/src/app
# Menyalin file penting terlebih dahulu untuk caching
COPY package.json pnpm-lock.yaml ./
COPY package.json ./
# Menyalin direktori ckeditor5 jika diperlukan
COPY vendor/ckeditor5 ./vendor/ckeditor5
@ -24,10 +25,49 @@ RUN pnpm install
COPY . .
# Build aplikasi
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm run build
# Expose port untuk server
EXPOSE 3000
# Perintah untuk menjalankan aplikasi
CMD ["pnpm", "run", "start"]
# # Gunakan base image Node.js Alpine yang ringan
# FROM node:23.5.0-alpine
# # Atur environment
# ENV PORT=3000
# ENV NODE_ENV=production
# ENV NODE_OPTIONS="--max-old-space-size=4096"
# # Install dependencies global
# RUN npm install -g pnpm pm2
# # Set working directory
# WORKDIR /usr/src/app
# # Salin file penting untuk caching dependencies
# COPY package.json pnpm-lock.yaml* ./
# # Salin vendor jika diperlukan (ckeditor misalnya)
# COPY vendor/ckeditor5 ./vendor/ckeditor5
# # Install dependencies
# RUN pnpm install --frozen-lockfile
# # Salin semua source code
# COPY . .
# # Salin ecosystem config
# COPY ecosystem.config.js ./
# # Build Next.js
# RUN pnpm run build
# # Expose port
# EXPOSE 3000
# # Jalankan Next.js dalam mode cluster
# CMD ["pm2-runtime", "start", "ecosystem.config.js"]

167
__tests__/auth-flow.test.ts Normal file
View File

@ -0,0 +1,167 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { AuthPage } from '../app/[locale]/auth/page';
import { useEmailValidation } from '../hooks/use-auth';
// Mock the hooks
jest.mock('../hooks/use-auth', () => ({
useAuth: () => ({
login: jest.fn(),
logout: jest.fn(),
refreshToken: jest.fn(),
isAuthenticated: false,
user: null,
loading: false,
error: null,
}),
useEmailValidation: jest.fn(),
useEmailSetup: () => ({
setupEmail: jest.fn(),
loading: false,
error: null,
}),
useOTPVerification: () => ({
verifyOTP: jest.fn(),
loading: false,
error: null,
}),
}));
// Mock the components
jest.mock('../components/auth/auth-layout', () => ({
AuthLayout: ({ children }: { children: React.ReactNode }) => <div data-testid="auth-layout">{children}</div>,
}));
jest.mock('../components/auth/login-form', () => ({
LoginForm: ({ onSuccess, onError }: any) => (
<div data-testid="login-form">
<button onClick={() => onSuccess({ username: 'testuser', password: 'testpass' })}>
Login Success
</button>
<button onClick={() => onError('Login failed')}>
Login Error
</button>
</div>
),
}));
jest.mock('../components/auth/email-setup-form', () => ({
EmailSetupForm: ({ onSuccess, onError, onBack }: any) => (
<div data-testid="email-setup-form">
<button onClick={() => onSuccess()}>Email Setup Success</button>
<button onClick={() => onError('Email setup failed')}>Email Setup Error</button>
<button onClick={() => onBack()}>Back</button>
</div>
),
}));
jest.mock('../components/auth/otp-form', () => ({
OTPForm: ({ onSuccess, onError, onResend }: any) => (
<div data-testid="otp-form">
<button onClick={() => onSuccess()}>OTP Success</button>
<button onClick={() => onError('OTP failed')}>OTP Error</button>
<button onClick={() => onResend()}>Resend OTP</button>
</div>
),
}));
// Mock toast
jest.mock('sonner', () => ({
toast: {
error: jest.fn(),
success: jest.fn(),
info: jest.fn(),
},
}));
describe('Auth Flow', () => {
const mockValidateEmail = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useEmailValidation as jest.Mock).mockReturnValue({
validateEmail: mockValidateEmail,
loading: false,
error: null,
});
});
it('should start with login form', () => {
render(<AuthPage params={{ locale: 'en' }} />);
expect(screen.getByTestId('login-form')).toBeInTheDocument();
});
it('should transition to email setup when validation returns "setup"', async () => {
mockValidateEmail.mockResolvedValue('setup');
render(<AuthPage params={{ locale: 'en' }} />);
// Click login success to trigger email validation
fireEvent.click(screen.getByText('Login Success'));
await waitFor(() => {
expect(screen.getByTestId('email-setup-form')).toBeInTheDocument();
});
});
it('should transition to OTP when validation returns "otp"', async () => {
mockValidateEmail.mockResolvedValue('otp');
render(<AuthPage params={{ locale: 'en' }} />);
// Click login success to trigger email validation
fireEvent.click(screen.getByText('Login Success'));
await waitFor(() => {
expect(screen.getByTestId('otp-form')).toBeInTheDocument();
});
});
it('should stay on login when validation returns "success"', async () => {
mockValidateEmail.mockResolvedValue('success');
render(<AuthPage params={{ locale: 'en' }} />);
// Click login success to trigger email validation
fireEvent.click(screen.getByText('Login Success'));
await waitFor(() => {
expect(screen.getByTestId('login-form')).toBeInTheDocument();
});
});
it('should transition from email setup to OTP', async () => {
mockValidateEmail.mockResolvedValue('setup');
render(<AuthPage params={{ locale: 'en' }} />);
// First, go to email setup
fireEvent.click(screen.getByText('Login Success'));
await waitFor(() => {
expect(screen.getByTestId('email-setup-form')).toBeInTheDocument();
});
// Then go to OTP
fireEvent.click(screen.getByText('Email Setup Success'));
await waitFor(() => {
expect(screen.getByTestId('otp-form')).toBeInTheDocument();
});
});
it('should go back from email setup to login', async () => {
mockValidateEmail.mockResolvedValue('setup');
render(<AuthPage params={{ locale: 'en' }} />);
// First, go to email setup
fireEvent.click(screen.getByText('Login Success'));
await waitFor(() => {
expect(screen.getByTestId('email-setup-form')).toBeInTheDocument();
});
// Then go back to login
fireEvent.click(screen.getByText('Back'));
await waitFor(() => {
expect(screen.getByTestId('login-form')).toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,264 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { LoginForm } from "@/components/auth/login-form";
import { useAuth, useEmailValidation } from "@/hooks/use-auth";
// Mock the hooks
jest.mock("@/hooks/use-auth");
jest.mock("@/service/landing/landing", () => ({
listRole: jest.fn().mockResolvedValue({
data: {
data: [
{ id: 1, name: "Admin" },
{ id: 2, name: "User" },
],
},
}),
}));
// Mock next-intl
jest.mock("next-intl", () => ({
useTranslations: () => (key: string, options?: any) => {
const defaults = {
logInPlease: "Log In Please",
acc: "Acc",
register: "Register",
password: "Password",
rememberMe: "Remember Me",
forgotPass: "Forgot Pass",
next: "Next",
categoryReg: "Category Reg",
selectOne: "Select One",
signIn: "Sign In",
};
return options?.defaultValue || defaults[key] || key;
},
}));
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
const mockUseEmailValidation = useEmailValidation as jest.MockedFunction<typeof useEmailValidation>;
describe("LoginForm", () => {
const mockLogin = jest.fn();
const mockValidateEmail = jest.fn();
const mockOnSuccess = jest.fn();
const mockOnError = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
mockUseAuth.mockReturnValue({
login: mockLogin,
logout: jest.fn(),
refreshToken: jest.fn(),
isAuthenticated: false,
user: null,
loading: false,
error: null,
});
mockUseEmailValidation.mockReturnValue({
validateEmail: mockValidateEmail,
loading: false,
error: null,
});
});
it("renders login form with all required fields", () => {
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
expect(screen.getByText("Log In Please")).toBeInTheDocument();
expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /selanjutnya/i })).toBeInTheDocument();
});
it("shows validation errors for invalid input", async () => {
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText("Username is required")).toBeInTheDocument();
expect(screen.getByText("Password is required")).toBeInTheDocument();
});
});
it("handles successful form submission", async () => {
mockValidateEmail.mockResolvedValue("success");
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const usernameInput = screen.getByLabelText(/username/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
fireEvent.change(usernameInput, { target: { value: "testuser" } });
fireEvent.change(passwordInput, { target: { value: "password123" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockValidateEmail).toHaveBeenCalledWith({
username: "testuser",
password: "password123",
});
});
});
it("handles email validation step", async () => {
mockValidateEmail.mockResolvedValue("setup");
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const usernameInput = screen.getByLabelText(/username/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
fireEvent.change(usernameInput, { target: { value: "testuser" } });
fireEvent.change(passwordInput, { target: { value: "password123" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockOnError).toHaveBeenCalledWith("Email setup required");
});
});
it("handles OTP step", async () => {
mockValidateEmail.mockResolvedValue("otp");
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const usernameInput = screen.getByLabelText(/username/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
fireEvent.change(usernameInput, { target: { value: "testuser" } });
fireEvent.change(passwordInput, { target: { value: "password123" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockOnError).toHaveBeenCalledWith("OTP verification required");
});
});
it("toggles password visibility", () => {
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const passwordInput = screen.getByLabelText(/password/i);
const toggleButton = screen.getByLabelText(/show password/i);
expect(passwordInput).toHaveAttribute("type", "password");
fireEvent.click(toggleButton);
expect(passwordInput).toHaveAttribute("type", "text");
expect(screen.getByLabelText(/hide password/i)).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(passwordInput).toHaveAttribute("type", "password");
});
it("handles remember me checkbox", () => {
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const rememberMeCheckbox = screen.getByRole("checkbox", { name: /remember me/i });
expect(rememberMeCheckbox).toBeChecked();
fireEvent.click(rememberMeCheckbox);
expect(rememberMeCheckbox).not.toBeChecked();
});
it("opens registration dialog", () => {
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const registerLink = screen.getByText("Register");
fireEvent.click(registerLink);
expect(screen.getByText("Category Reg")).toBeInTheDocument();
expect(screen.getByText("Select One")).toBeInTheDocument();
expect(screen.getByLabelText("Admin")).toBeInTheDocument();
expect(screen.getByLabelText("User")).toBeInTheDocument();
});
it("handles loading state", async () => {
mockUseEmailValidation.mockReturnValue({
validateEmail: mockValidateEmail,
loading: true,
error: null,
});
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const submitButton = screen.getByRole("button", { name: /processing/i });
expect(submitButton).toBeDisabled();
});
it("handles error state", async () => {
mockValidateEmail.mockRejectedValue(new Error("Validation failed"));
render(
<LoginForm
onSuccess={mockOnSuccess}
onError={mockOnError}
/>
);
const usernameInput = screen.getByLabelText(/username/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole("button", { name: /selanjutnya/i });
fireEvent.change(usernameInput, { target: { value: "testuser" } });
fireEvent.change(passwordInput, { target: { value: "password123" } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockOnError).toHaveBeenCalledWith("Validation failed");
});
});
});

11
__tests__/setup.test.ts Normal file
View File

@ -0,0 +1,11 @@
describe('Testing Setup', () => {
it('should work correctly', () => {
expect(true).toBe(true);
});
it('should have testing library matchers', () => {
const element = document.createElement('div');
element.textContent = 'Hello World';
expect(element).toBeInTheDocument();
});
});

11
action/app-actions.ts Normal file
View File

@ -0,0 +1,11 @@
'use server'
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export const postMessageAction = async (id: string, message: string,) => {
const response = await postMessage(id, message)
revalidatePath("/");
return response;
}

View File

@ -1,8 +1,6 @@
"use server";
import { revalidatePath } from "next/cache";
'use server'
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export const loginUser = async (data: any) => {
try {
const response = undefined;
@ -15,4 +13,4 @@ export const loginUser = async (data: any) => {
} catch (error) {
throw new Error(error as string);
}
};
};

View File

@ -0,0 +1,165 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Link } from "@/components/navigation";
import { useToast } from "@/components/ui/use-toast";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useRouter } from "next/navigation";
import { deleteUser } from "@/service/management-user/management-user";
import { stringify } from "querystring";
const getColumns = ({ onRefresh }: { onRefresh: () => void }): ColumnDef<any>[] => [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "fullname",
header: "Nama",
cell: ({ row }) => <span>{row.getValue("fullname")}</span>,
},
{
accessorKey: "address",
header: "Wilayah",
cell: () => <span>MABES</span>,
},
{
accessorKey: "userRolePlacements",
header: "Posisi",
cell: ({ row }) => {
const placements = row.original.userRolePlacements || [];
const placement = placements.find(
(p: any) => p.roleId === 11 || p.roleId === 12
);
let posisi = "-";
if (placement) {
posisi = placement.roleId === 11 ? "Koorkurator" : "Kurator";
}
return <span>{posisi}</span>;
},
},
{
accessorKey: "role.name",
header: "Bidang Keahlian",
cell: ({ row }) => (
<span>
{row.original.userProfilesAdditional?.userCompetency?.name ?? "-"}
</span>
),
},
{
accessorKey: "userExperienceId",
header: "Pengalaman",
cell: ({ row }) => {
const experienceId =
row.original.userProfilesAdditional?.userExperienceId;
const experienceMap: Record<number, string> = {
1: "Akademisi",
2: "Praktisi",
3: "Akademisi + Praktisi",
};
return <span>{experienceMap[experienceId] ?? "-"}</span>;
},
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const { toast } = useToast();
const MySwal = withReactContent(Swal);
const doDelete = async (id: number) => {
Swal.fire({
title: "Menghapus user...",
text: "Mohon tunggu",
allowOutsideClick: false,
didOpen: () => Swal.showLoading(),
});
const response = await deleteUser(id);
Swal.close();
if (response?.error) {
toast({
title: stringify(response?.message),
variant: "destructive",
});
return;
}
toast({ title: "Berhasil menghapus user" });
// ⬅️ INI YANG PENTING → REFRESH TABLE TANPA RELOAD
onRefresh();
};
const handleDelete = (id: number) => {
MySwal.fire({
title: "Hapus user ini?",
showCancelButton: true,
confirmButtonColor: "#dc3545",
confirmButtonText: "Iya",
cancelButtonText: "Tidak",
}).then((res) => {
if (res.isConfirmed) doDelete(id);
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link href={`/admin/add-experts/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
<Eye className="w-4 h-4 me-1.5" /> View
</DropdownMenuItem>
</Link>
<Link href={`/admin/add-experts/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
<SquarePen className="w-4 h-4 me-1.5" /> Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => handleDelete(row.original.userKeycloakId)}
className="text-red-600 cursor-pointer hover:bg-red-300"
>
<Trash2 className="w-4 h-4 me-1.5" /> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default getColumns;

View File

@ -0,0 +1,275 @@
"use client";
import * as React from "react";
import {
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { UserIcon } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
// import columns from "./column";
import getColumns from "./column";
import { listEnableCategory } from "@/service/content/content";
import { Link } from "@/i18n/routing";
import { listDataExperts } from "@/service/experts/experts";
const AddExpertTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const table = useReactTable({
data: dataTable,
// columns,
columns: getColumns({ onRefresh: fetchData }),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
fetchData();
}
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
setPagination({
pageIndex: 0,
pageSize: Number(showData),
});
}, [page, showData]);
// async function fetchData() {
// try {
// loading();
// const contentData = dummyData;
// contentData.forEach((item: any, index: number) => {
// item.no = (page - 1) * Number(showData) + index + 1;
// });
// setDataTable(contentData);
// setTotalData(contentData?.length);
// setTotalPage(1);
// close();
// } catch (error) {
// console.error("Error fetching tasks:", error);
// }
// }
async function fetchData() {
// const formattedStartDate = startDate
// ? format(new Date(startDate), "yyyy-MM-dd")
// : "";
// const formattedEndDate = endDate
// ? format(new Date(endDate), "yyyy-MM-dd")
// : "";
try {
// const isForSelf = Number(roleId) === 4;
const res = await listDataExperts(limit, page - 1);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * limit + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
return (
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
<div className="flex justify-between mb-10 items-center">
<p className="text-xl font-medium text-default-900">Tenaga Ahli</p>
<Link href="/admin/add-experts/create">
<Button color="primary" size="md" className="text-sm">
<UserIcon />
Tambah Tenaga Ahli
</Button>
</Link>
</div>
<div className="flex justify-between ">
<Input
type="text"
placeholder="Search"
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
className="max-w-[300px]"
/>
<div className="flex flex-row gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
// colSpan={columns.length}
colSpan={table.getAllLeafColumns().length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default AddExpertTable;

View File

@ -0,0 +1,675 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useRouter } from "@/i18n/routing";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useEffect, useState } from "react";
import {
AdministrationLevelList,
checkRolePlacementsAvailability,
getListCompetencies,
getListExperiences,
saveUserInternal,
saveUserRolePlacements,
} from "@/service/management-user/management-user";
import { error, loading } from "@/config/swal";
import { Eye, EyeOff } from "lucide-react";
// const FormSchema = z.object({
// name: z.string({
// required_error: "Required",
// }),
// username: z.string({
// required_error: "Required",
// }),
// password: z.string({
// required_error: "Required",
// }),
// phoneNumber: z.string({
// required_error: "Required",
// }),
// email: z.string({
// required_error: "Required",
// }),
// skills: z.string({
// required_error: "Required",
// }),
// experiences: z.string({
// required_error: "Required",
// }),
// company: z.string({
// required_error: "Required",
// }),
// });
const FormSchema = z.object({
name: z.string({ required_error: "Required" }),
username: z
.string({ required_error: "Required" })
.refine((val) => !/\s/.test(val), {
message: "Username tidak boleh mengandung spasi",
}),
// .transform((val) => val.toLowerCase()),
password: z
.string({ required_error: "Required" })
.min(8, "Minimal 8 karakter")
.regex(/[A-Z]/, "Harus mengandung huruf besar (A-Z)")
.regex(/[0-9]/, "Harus mengandung angka (0-9)")
.regex(/[^A-Za-z0-9]/, "Harus mengandung karakter spesial (!@#$%^&*)"),
// confirmPassword: z.string({ required_error: "Required" }),
phoneNumber: z.string({ required_error: "Required" }),
email: z.string({ required_error: "Required" }),
skills: z.string({ required_error: "Required" }),
experiences: z.string({ required_error: "Required" }),
company: z.string({ required_error: "Required" }),
});
// .refine((data) => data.password === data.confirmPassword, {
// path: ["confirmPassword"],
// message: "Konfirmasi password tidak sama",
// });
export type Placements = {
index: number;
roleId?: string;
userLevelId?: number;
};
export default function AddExpertForm() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});
const [passwordStrength, setPasswordStrength] = useState("");
const [incrementId, setIncrementId] = useState(1);
const [placementRows, setPlacementRows] = useState<Placements[]>([
{ index: 0, roleId: "", userLevelId: 0 },
]);
const [userCompetencies, setUserCompetencies] = useState<any>();
const [userExperiences, setUserExperiences] = useState<any>();
const [userLevels, setUserLevels] = useState<any>();
const [passwordType, setPasswordType] = useState("password");
const [showPassword, setShowPassword] = useState(false);
const togglePasswordType = () => {
setPasswordType((prevType) =>
prevType === "password" ? "text" : "password"
);
};
const roleSelection = [
{
id: "11",
name: "Koor Kurator",
},
{
id: "12",
name: "Kurator",
},
];
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
const save = async (data: z.infer<typeof FormSchema>) => {
console.log("data", data);
const dataReq = {
firstName: data.name,
username: data.username,
email: data.email,
password: data.password,
address: "",
roleId: "EXP-ID",
phoneNumber: data.phoneNumber,
userCompetencyId: data.skills,
userExperienceId: data.experiences,
companyName: data.company,
isAdmin: true,
};
loading();
// check availability first
var placementArr: any[] = [];
placementRows.forEach((row: any) => {
placementArr.push({
roleId: Number(row.roleId),
userLevelId: Number(row.userLevelId),
});
});
const dataReqAvail = {
placements: placementArr,
};
const resAvail = await checkRolePlacementsAvailability(dataReqAvail);
if (resAvail?.error) {
close();
error(resAvail.message);
return false;
}
const res = await saveUserInternal(dataReq);
const resData = res?.data?.data;
const userProfileId = resData?.id;
var placementArr: any[] = [];
placementRows.forEach((row: any) => {
placementArr.push({
roleId: Number(row.roleId),
userLevelId: Number(row.userLevelId),
userProfileId: userProfileId,
});
});
const dataReq2 = {
userId: userProfileId,
placements: placementArr,
};
const res2 = await saveUserRolePlacements(dataReq2);
const resData2 = res2?.data?.data;
success("/admin/add-experts");
};
function success(redirect: string): void {
MySwal.fire({
title: '<p class="text-green-600 font-bold">Sukses</p>',
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: '<span class="text-white">OK</span>',
allowOutsideClick: false,
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
useEffect(() => {
getDataAdditional();
}, []);
async function getDataAdditional() {
const resCompetencies = await getListCompetencies();
setUserCompetencies(resCompetencies?.data?.data);
const resExperiences = await getListExperiences();
setUserExperiences(resExperiences?.data?.data);
const resUserLevels = await AdministrationLevelList();
const data = resUserLevels?.data?.data;
var levelsArr: any[] = [];
data.forEach((levels: any) => {
levelsArr.push({
id: levels.id,
label: levels.name,
name: levels.name,
value: String(levels.id),
levelNumber: levels.levelNumber,
});
});
setUserLevels(levelsArr);
}
function successSubmit() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/add-experts");
}
});
}
const handleSelectionChange = (
index: number,
type: "roleId" | "userLevelId",
value: string
) => {
setPlacementRows((prevRows) =>
prevRows.map((row) =>
row.index === index ? { ...row, [type]: value } : row
)
);
};
const handleRemoveRow = (index: number) => {
console.log(index);
console.log(placementRows);
const newPlacements = placementRows.filter((row) => row.index != index);
console.log(newPlacements);
setPlacementRows(newPlacements);
};
const handleAddRow = () => {
if (placementRows.length < 2) {
setPlacementRows((prevRows) => [
...prevRows,
{ index: incrementId, roleId: "", userLevelId: 0 },
]);
setIncrementId((prevId) => prevId + 1);
}
};
const computeStrength = (password: string) => {
let score = 0;
if (password.length >= 8) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
if (score <= 1) return "weak";
if (score === 2 || score === 3) return "medium";
if (score === 4) return "strong";
return "";
};
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white dark:bg-black rounded-sm p-4"
>
<p className="fonnt-semibold">Campaign</p>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap</FormLabel>
<Input
value={field.value}
placeholder="Masukkan Nama Lengkap"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username (huruf kecil, tanpa spasi)</FormLabel>
<Input
type="text"
value={field.value}
placeholder="masukkan username"
onChange={(e) => {
let value = e.target.value;
// Hapus spasi otomatis
value = value.replace(/\s+/g, "");
// Jadikan lowercase otomatis
value = value.toLowerCase();
field.onChange(value);
}}
/>
{/* Info tambahan */}
<p className="text-xs text-gray-500 mt-1">
Username otomatis menjadi huruf kecil tanpa spasi.
</p>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<Input
type="text"
value={field.value}
placeholder="Masukkan"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>No. HP</FormLabel>
<Input
type="number"
value={field.value}
placeholder="Masukkan No.Hp"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<input
type="text"
name="usernamefake"
autoComplete="username"
hidden
/>
<input
type="password"
name="passwordfake"
autoComplete="new-password"
hidden
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Input
type="email"
value={field.value}
placeholder="Masukkan email"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
value={field.value}
placeholder="Masukkan Password"
onChange={(e) => {
field.onChange(e.target.value);
setPasswordStrength(computeStrength(e.target.value));
}}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<FormLabel className="text-gray-400 text-[12px]">
Password harus memiliki minimal 8 karakter, special karakter,
angka dan huruf kapital
</FormLabel>
{/* Strength meter */}
{field.value && (
<div className="mt-2">
<div
className={`h-2 rounded transition-all ${
passwordStrength === "weak"
? "bg-red-500 w-1/4"
: passwordStrength === "medium"
? "bg-yellow-500 w-2/4"
: "bg-green-500 w-full"
}`}
/>
<p
className={`text-xs mt-1 ${
passwordStrength === "weak"
? "text-red-500"
: passwordStrength === "medium"
? "text-yellow-600"
: "text-green-600"
}`}
>
{passwordStrength === "weak" && "Weak Password"}
{passwordStrength === "medium" && "Medium Password"}
{passwordStrength === "strong" && "Strong Password"}
</p>
</div>
)}
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<div className="relative">
<Input
value={field.value}
type={showPassword ? "text" : "password"}
placeholder="Masukkan Password"
onChange={field.onChange}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500 hover:text-default-700"
tabIndex={-1}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="skills"
render={({ field }) => (
<FormItem>
<FormLabel>Bidang Keahlian</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih Bidang Keahlian" />
</SelectTrigger>
</FormControl>
<SelectContent>
{userCompetencies?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="experiences"
render={({ field }) => (
<FormItem>
<FormLabel>Pengalaman</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih Pengalaman" />
</SelectTrigger>
</FormControl>
<SelectContent>
{userExperiences?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="company"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Institusi/Perusahaan</FormLabel>
<Input
type="text"
value={field.value}
placeholder="Nama Institusi/Perusahaan"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-4">
<FormLabel>Posisi</FormLabel>
{placementRows?.map((row: any) => (
<div key={row.index} className="flex items-center gap-2 my-2">
<Select
onValueChange={(e) =>
handleSelectionChange(row.index, "roleId", e)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih Role" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roleSelection?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* <Select
onValueChange={(e) =>
handleSelectionChange(row.index, "userLevelId", e)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih User Level" />
</SelectTrigger>
</FormControl>
<SelectContent>
{userLevels?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select> */}
<Select
onValueChange={(e) =>
handleSelectionChange(row.index, "userLevelId", e)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih User Level" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="216">MABES POLRI</SelectItem>
</SelectContent>
</Select>
{placementRows.length > 1 && (
<Button
type="button"
size="md"
color="destructive"
onClick={() => handleRemoveRow(row.index)}
>
Hapus
</Button>
)}
</div>
))}
{/* <Button
type="button"
size="md"
onClick={handleAddRow}
disabled={placementRows.length >= 2} // optional: disable button if already 1 row added
>
Tambah
</Button> */}
</div>
<div className="flex flex-row justify-end gap-2 mt-4 pt-4">
<Button
size="md"
type="button"
variant="outline"
color="destructive"
className="text-xs"
>
Cancel
</Button>
<Button size="md" type="submit" color="primary" className="text-xs">
Submit
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,15 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormDetailExperts from "@/components/form/experts/experts-detail";
const ExpertsDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormDetailExperts />
</div>
</div>
);
};
export default ExpertsDetailPage;

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import AddExpertTable from "./component/table";
export default function AddExpert() {
return (
<div>
<SiteBreadcrumb />
<AddExpertTable />
</div>
);
}

View File

@ -0,0 +1,719 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useRouter } from "@/i18n/routing";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useEffect, useState } from "react";
import {
AdministrationLevelList,
getListCompetencies,
getListExperiences,
getUserById,
saveUserInternal,
saveUserRolePlacements,
} from "@/service/management-user/management-user";
import { loading } from "@/config/swal";
import { Eye, EyeOff } from "lucide-react";
import { useParams } from "next/navigation";
const FormSchema = z.object({
name: z.string().optional(),
username: z.string().optional(),
password: z.string().optional(),
phoneNumber: z.string().optional(),
email: z.string().optional(),
skills: z.string().optional(),
experiences: z.string().optional(),
company: z.string().optional(),
});
// const FormSchema = z.object({
// name: z.string({
// required_error: "Required",
// }),
// username: z.string({
// required_error: "Required",
// }),
// password: z.string({
// required_error: "Required",
// }),
// phoneNumber: z.string({
// required_error: "Required",
// }),
// email: z.string({
// required_error: "Required",
// }),
// skills: z.string({
// required_error: "Required",
// }),
// experiences: z.string({
// required_error: "Required",
// }),
// company: z.string({
// required_error: "Required",
// }),
// });
export type Placements = {
index: number;
roleId?: string;
userLevelId?: number;
};
interface Detail {
id: string;
fullname: string;
username: string;
phoneNumber: string;
email: string;
birthPlace: string;
birthDate: string;
education: string;
career: string;
expertise: string;
experience: string;
position: string;
region: string;
cvUrl: string;
photoUrl: string;
isActive: boolean;
userProfilesAdditional?: {
companyName: string;
userExperienceId: any;
userCompetency?: {
id: number;
name: string;
isActive: boolean;
createdAt: string;
};
};
userRolePlacements?: {
roleId: number;
userLevelId: number;
}[];
}
export default function UpdateExpertForm() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const params = useParams();
const id = params?.id;
const [detail, setDetail] = useState<Detail | null>(null);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});
const [incrementId, setIncrementId] = useState(1);
const [placementRows, setPlacementRows] = useState<Placements[]>([
{ index: 0, roleId: "", userLevelId: 0 },
]);
const [userCompetencies, setUserCompetencies] = useState<any>();
const [userExperiences, setUserExperiences] = useState<any>();
const [userLevels, setUserLevels] = useState<any>();
const [passwordType, setPasswordType] = useState("password");
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
getDataAdditional();
}, []);
useEffect(() => {
async function initState() {
if (id) {
const response = await getUserById(String(id));
const details = response?.data?.data;
setDetail(details);
}
if (detail?.userProfilesAdditional?.companyName) {
form.setValue("company", detail.userProfilesAdditional.companyName);
}
if (detail?.userProfilesAdditional?.userCompetency?.id) {
form.setValue(
"skills",
String(detail.userProfilesAdditional.userCompetency.id)
);
}
if (detail?.userProfilesAdditional?.userExperienceId) {
form.setValue(
"experiences",
String(detail.userProfilesAdditional.userExperienceId)
);
}
}
initState();
}, [id]);
useEffect(() => {
if (!detail) return;
// Isi semua form field
form.reset({
name: detail.fullname || "",
username: detail.username || "",
phoneNumber: detail.phoneNumber || "",
email: detail.email || "",
password: "",
skills: detail?.userProfilesAdditional?.userCompetency?.id
? String(detail.userProfilesAdditional.userCompetency.id)
: "",
experiences: detail?.userProfilesAdditional?.userExperienceId
? String(detail.userProfilesAdditional.userExperienceId)
: "",
company: detail?.userProfilesAdditional?.companyName || "",
});
// 🔥 Masukkan posisi existing
if (detail.userRolePlacements && detail.userRolePlacements.length > 0) {
const mapped = detail.userRolePlacements.map(
(item: any, idx: number) => ({
index: idx,
roleId: String(item.roleId),
userLevelId: Number(item.userLevelId),
})
);
setPlacementRows(mapped);
}
}, [detail]);
if (!detail) return <div>Loading...</div>;
const togglePasswordType = () => {
setPasswordType((prevType) =>
prevType === "password" ? "text" : "password"
);
};
const roleSelection = [
{
id: "11",
name: "Koor Kurator",
},
{
id: "12",
name: "Kurator",
},
];
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
const save = async (data: z.infer<typeof FormSchema>) => {
console.log("data", data);
const dataReq = {
id: detail?.id,
firstName: data.name || detail.fullname,
username: data.username || detail.username,
email: data.email || detail.email,
password: data.password || undefined,
address: "",
roleId: "EXP-ID",
phoneNumber: data.phoneNumber || detail.phoneNumber,
userCompetencyId:
data.skills || detail.userProfilesAdditional?.userCompetency?.id,
userExperienceId:
data.experiences || detail.userProfilesAdditional?.userExperienceId,
companyName: data.company || detail.userProfilesAdditional?.companyName,
isAdmin: true,
};
// const dataReq = {
// id: detail?.id,
// firstName: data.name,
// username: data.username,
// email: data.email,
// password: data.password,
// address: "",
// roleId: "EXP-ID",
// phoneNumber: data.phoneNumber,
// userCompetencyId: data.skills,
// userExperienceId: data.experiences,
// companyName: data.company,
// };
loading();
const res = await saveUserInternal(dataReq);
const resData = res?.data?.data;
const userProfileId = resData?.id;
var placementArr: any[] = [];
placementRows.forEach((row: any) => {
placementArr.push({
roleId: Number(row.roleId),
userLevelId: Number(row.userLevelId),
userProfileId: userProfileId,
});
});
const dataReq2 = {
userId: userProfileId,
placements: placementArr,
};
const res2 = await saveUserRolePlacements(dataReq2);
const resData2 = res2?.data?.data;
success("/admin/add-experts");
};
function success(redirect: string): void {
MySwal.fire({
title: '<p class="text-green-600 font-bold">Sukses</p>',
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: '<span class="text-white">OK</span>',
allowOutsideClick: false,
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
async function getDataAdditional() {
const resCompetencies = await getListCompetencies();
setUserCompetencies(resCompetencies?.data?.data);
const resExperiences = await getListExperiences();
setUserExperiences(resExperiences?.data?.data);
console.log("experience", resExperiences?.data?.data);
const resUserLevels = await AdministrationLevelList();
const data = resUserLevels?.data?.data;
var levelsArr: any[] = [];
data.forEach((levels: any) => {
levelsArr.push({
id: levels.id,
label: levels.name,
name: levels.name,
value: String(levels.id),
levelNumber: levels.levelNumber,
});
});
setUserLevels(levelsArr);
}
function successSubmit() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/add-experts");
}
});
}
const handleSelectionChange = (
index: number,
type: "roleId" | "userLevelId",
value: string
) => {
setPlacementRows((prevRows) =>
prevRows.map((row) =>
row.index === index ? { ...row, [type]: value } : row
)
);
};
const handleRemoveRow = (index: number) => {
console.log(index);
console.log(placementRows);
const newPlacements = placementRows.filter((row) => row.index != index);
console.log(newPlacements);
setPlacementRows(newPlacements);
};
const handleAddRow = () => {
if (placementRows.length < 2) {
setPlacementRows((prevRows) => [
...prevRows,
{ index: incrementId, roleId: "", userLevelId: 0 },
]);
setIncrementId((prevId) => prevId + 1);
}
};
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
{detail !== undefined ? (
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm p-4"
>
<p className="fonnt-semibold">Campaign</p>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap</FormLabel>
{/* <Input
defaultValue={detail?.fullname}
placeholder="Masukkan Nama Lengkap"
onChange={field.onChange}
/> */}
<Input
{...field}
defaultValue={detail?.fullname}
placeholder="Masukkan Nama Lengkap"
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
disabled
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
{/* <Input
type="text"
defaultValue={detail?.username}
placeholder="Masukkan"
onChange={field.onChange}
/> */}
<Input
{...field}
type="text"
defaultValue={detail?.username}
placeholder="Masukkan"
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>No. HP</FormLabel>
{/* <Input
type="number"
defaultValue={detail?.phoneNumber}
placeholder="Masukkan No.Hp"
onChange={field.onChange}
/> */}
<Input
{...field}
type="number"
defaultValue={detail?.phoneNumber}
placeholder="Masukkan"
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
{/* <Input
type="email"
defaultValue={detail?.email}
placeholder="Masukkan email"
onChange={field.onChange}
/> */}
<Input
{...field}
type="email"
defaultValue={detail?.email}
placeholder="Masukkan email"
/>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (Opsional)</FormLabel>
<div className="relative">
<Input
{...field}
type={showPassword ? "text" : "password"}
placeholder="Kosongkan jika tidak ingin mengubah password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</FormItem>
)}
/> */}
{/* <FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<div className="relative">
<Input
value={field.value}
type={showPassword ? "text" : "password"}
placeholder="Masukkan Password"
onChange={field.onChange}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500 hover:text-default-700"
tabIndex={-1}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="skills"
render={({ field }) => (
<FormItem>
<FormLabel>Bidang Keahlian</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={
detail?.userProfilesAdditional?.userCompetency?.name
? detail.userProfilesAdditional.userCompetency
.name
: "Pilih Bidang Keahlian"
}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{userCompetencies?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="experiences"
render={({ field }) => (
<FormItem>
<FormLabel>Pengalaman</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={
detail &&
detail.userProfilesAdditional &&
detail.userProfilesAdditional.userExperienceId
? userExperiences?.find(
(item: any) =>
item.id ===
detail.userProfilesAdditional!
.userExperienceId
)?.name
: "Pilih Pengalaman"
}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{userExperiences?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
)}
/>
<FormField
control={form.control}
name="company"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Institusi/Perusahaan</FormLabel>
{/* <Input
type="text"
value={detail?.userProfilesAdditional?.companyName || ""}
placeholder="Nama Institusi/Perusahaan"
onChange={field.onChange}
/> */}
<Input
{...field}
type="text"
defaultValue={
detail?.userProfilesAdditional?.companyName || ""
}
placeholder="Nama Institusi/Perusahaan"
/>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-4">
<FormLabel>Posisi</FormLabel>
{placementRows?.map((row: any) => (
<div key={row.index} className="flex items-center gap-2 my-2">
<Select
value={row.roleId}
onValueChange={(e) =>
handleSelectionChange(row.index, "roleId", e)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih Role" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roleSelection?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* <Select
onValueChange={(e) =>
handleSelectionChange(row.index, "userLevelId", e)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih User Level" />
</SelectTrigger>
</FormControl>
<SelectContent>
{userLevels?.map((item: any) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select> */}
<Select
value={row.userLevelId}
onValueChange={(e) =>
handleSelectionChange(row.index, "userLevelId", e)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Pilih User Level" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="216">MABES POLRI</SelectItem>
</SelectContent>
</Select>
{placementRows.length > 1 && (
<Button
type="button"
size="md"
color="destructive"
onClick={() => handleRemoveRow(row.index)}
>
Hapus
</Button>
)}
</div>
))}
<Button
type="button"
size="md"
onClick={handleAddRow}
disabled={placementRows.length >= 2}
>
Tambah
</Button>
</div>
<div className="flex flex-row justify-end gap-2 mt-4 pt-4">
<Button
size="md"
type="button"
variant="outline"
color="destructive"
className="text-xs"
>
Cancel
</Button>
<Button
size="md"
type="submit"
color="primary"
className="text-xs"
>
Submit
</Button>
</div>
</form>
) : (
""
)}
</Form>
</div>
);
}

View File

@ -0,0 +1,306 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { useEffect, useState } from "react";
import { addDays, format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
import Cookies from "js-cookie";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { generateTicket } from "@/service/tableau/tableau-service";
const users = [
{ id: 1, name: "POLRI" },
{ id: 2, name: "JURNALIS" },
];
export default function ContentManagement() {
const [startDate, setStartDate] = useState<any>(new Date());
const [endDate, setEndDate] = useState<any>(new Date());
const [hasMounted, setHasMounted] = useState(false);
// const t = useTranslations("AnalyticsDashboard");
const levelName = getCookiesDecrypt("ulnae");
const poldaState = Cookies.get("state");
const provState = Cookies.get("state-prov");
const [ticket1, setTicket1] = useState("");
const [ticket2, setTicket2] = useState("");
const [ticket3, setTicket3] = useState("");
const [ticket4, setTicket4] = useState("");
const [ticket5, setTicket5] = useState("");
const [ticket6, setTicket6] = useState("");
const [isInternational, setIsInternational] = useState([false, false, false]);
const baseUrl = "https://analytic.sitani.info/";
const url = "https://analytic.sitani.info/trusted/";
const view1 =
levelName == "MABES POLRI"
? isInternational[0]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-polri?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-polri?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-polri?provinsi-polda=${provState}&`;
const view2 =
levelName == "MABES POLRI"
? isInternational[1]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-jurnalis?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-jurnalis?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-jurnalis?provinsi-polda=${poldaState}&`;
const view3 =
levelName == "MABES POLRI"
? isInternational[2]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-jurnalis-intl?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-jurnalis-intl?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-jurnalis-intl?provinsi-polda=${poldaState}&`;
const view4 =
levelName == "MABES POLRI"
? isInternational[3]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-satker?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-satker?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-satker?provinsi-polda=${poldaState}&`;
const view5 =
levelName == "MABES POLRI"
? isInternational[4]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-jnl?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-jnl?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-indo-jnl?provinsi-polda=${poldaState}&`;
const view6 =
levelName == "MABES POLRI"
? isInternational[5]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-intl?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-intl?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-intl?provinsi-polda=${poldaState}&`;
const param = ":embed=yes&:toolbar=no&:iframeSizedToWindow=true";
useEffect(() => {
async function initState() {
const response1 = await generateTicket();
setTicket1(response1?.data?.data);
const response2 = await generateTicket();
setTicket2(response2?.data?.data);
const response3 = await generateTicket();
setTicket3(response3?.data?.data);
const response4 = await generateTicket();
setTicket4(response4?.data?.data);
const response5 = await generateTicket();
setTicket5(response5?.data?.data);
const response6 = await generateTicket();
setTicket6(response6?.data?.data);
}
initState();
}, [isInternational]);
// Hooks
useEffect(() => {
setHasMounted(true);
}, []);
// Render
if (!hasMounted) return null;
const handleInternational = (index: number, val: boolean) => {
const updatedIsInternational = [...isInternational];
updatedIsInternational[index] = val;
setIsInternational(updatedIsInternational);
};
return (
<div>
<SiteBreadcrumb />
<div className="flex flex-col gap-3">
<Accordion id="polri" type="single" collapsible className="w-full">
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
KONTEN YANG DISIMPAN OLEH PENGGUNA POLRI INDONESIA
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket1 == "" ? (
<iframe
src={`${baseUrl + view1 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket1}/${view1}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion id="2" type="single" collapsible className="w-full">
<AccordionItem value="item-2" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
PENAMBAHAN JUMLAH PENGGUNA JURNALIS INDONESIA
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket2 == "" ? (
<iframe
src={`${baseUrl + view2 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket2}/${view2}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion id="3" type="single" collapsible className="w-full">
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
KONTEN YANG DISIMPAN OLEH PENGGUNA JURNALIS INTERNASIONAL
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket3 == "" ? (
<iframe
src={`${baseUrl + view3 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket3}/${view3}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion id="4" type="single" collapsible className="w-full">
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
PENAMBAHAN JUMLAH PENGGUNA POLRI INDONESIA
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket4 == "" ? (
<iframe
src={`${baseUrl + view4 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket4}/${view4}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion id="5" type="single" collapsible className="w-full">
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
PENAMBAHAN JUMLAH PENGGUNA JURNALIS INDONESIA
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket5 == "" ? (
<iframe
src={`${baseUrl + view5 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket5}/${view5}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion id="6" type="single" collapsible className="w-full">
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
PENAMBAHAN JUMLAH PENGGUNA JURNALIS INTERNASIONAL
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket6 == "" ? (
<iframe
src={`${baseUrl + view6 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket6}/${view6}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
}

View File

@ -0,0 +1,121 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { useEffect, useState } from "react";
import { addDays, format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
import Cookies from "js-cookie";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { generateTicket } from "@/service/tableau/tableau-service";
const users = [
{ id: 1, name: "POLRI" },
{ id: 2, name: "JURNALIS" },
];
export default function EmergencyIssue() {
const [startDate, setStartDate] = useState<any>(new Date());
const [endDate, setEndDate] = useState<any>(new Date());
const [hasMounted, setHasMounted] = useState(false);
// const t = useTranslations("AnalyticsDashboard");
const levelName = getCookiesDecrypt("ulnae");
const poldaState = Cookies.get("state");
const provState = Cookies.get("state-prov");
const [ticket1, setTicket1] = useState("");
const [ticket2, setTicket2] = useState("");
const [ticket3, setTicket3] = useState("");
const [ticket4, setTicket4] = useState("");
const [ticket5, setTicket5] = useState("");
const [ticket6, setTicket6] = useState("");
const [isInternational, setIsInternational] = useState([false, false, false]);
const baseUrl = "https://analytic.sitani.info/";
const url = "https://analytic.sitani.info/trusted/";
const view1 =
levelName == "MABES POLRI"
? isInternational[0]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-emg-issue?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-emg-issue?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-emg-issue?provinsi-polda=${provState}&`;
const param = ":embed=yes&:toolbar=no&:iframeSizedToWindow=true";
useEffect(() => {
async function initState() {
const response1 = await generateTicket();
setTicket1(response1?.data?.data);
}
initState();
}, [isInternational]);
// Hooks
useEffect(() => {
setHasMounted(true);
}, []);
// Render
if (!hasMounted) return null;
const handleInternational = (index: number, val: boolean) => {
const updatedIsInternational = [...isInternational];
updatedIsInternational[index] = val;
setIsInternational(updatedIsInternational);
};
return (
<div>
<SiteBreadcrumb />
<div className="flex flex-col gap-3">
<Accordion
id="feedback-center"
type="single"
collapsible
className="w-full"
>
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
ANALISA BERKAITAN DENGAN AKUN PELAPOR{" "}
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket1 == "" ? (
<iframe
src={`${baseUrl + view1 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket1}/${view1}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
}

View File

@ -0,0 +1,122 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { useEffect, useState } from "react";
import { addDays, format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
import Cookies from "js-cookie";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { generateTicket } from "@/service/tableau/tableau-service";
const users = [
{ id: 1, name: "POLRI" },
{ id: 2, name: "JURNALIS" },
];
export default function FeedbackCenter() {
const [startDate, setStartDate] = useState<any>(new Date());
const [endDate, setEndDate] = useState<any>(new Date());
const [hasMounted, setHasMounted] = useState(false);
// const t = useTranslations("AnalyticsDashboard");
const levelName = getCookiesDecrypt("ulnae");
const poldaState = Cookies.get("state");
const provState = Cookies.get("state-prov");
const [ticket1, setTicket1] = useState("");
const [ticket2, setTicket2] = useState("");
const [ticket3, setTicket3] = useState("");
const [ticket4, setTicket4] = useState("");
const [ticket5, setTicket5] = useState("");
const [ticket6, setTicket6] = useState("");
const [isInternational, setIsInternational] = useState([false, false, false]);
const baseUrl = "https://analytic.sitani.info/";
const url = "https://analytic.sitani.info/trusted/";
const view1 =
levelName == "MABES POLRI"
? isInternational[0]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-tickets?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-tickets?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-tickets?provinsi-polda=${provState}&`;
const param = ":embed=yes&:toolbar=no&:iframeSizedToWindow=true";
useEffect(() => {
async function initState() {
const response1 = await generateTicket();
setTicket1(response1?.data?.data);
}
initState();
}, [isInternational]);
// Hooks
useEffect(() => {
setHasMounted(true);
}, []);
// Render
if (!hasMounted) return null;
const handleInternational = (index: number, val: boolean) => {
const updatedIsInternational = [...isInternational];
updatedIsInternational[index] = val;
setIsInternational(updatedIsInternational);
};
return (
<div>
<SiteBreadcrumb />
<div className="flex flex-col gap-3">
<Accordion
id="feedback-center"
type="single"
collapsible
className="w-full"
>
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
TICKET PADA FEEDBACK CENTER{" "}
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket1 == "" ? (
<iframe
src={`${baseUrl + view1 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket1}/${view1}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
}

View File

@ -0,0 +1,246 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { useEffect, useState } from "react";
import { addDays, format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { DateRange } from "react-day-picker";
import Cookies from "js-cookie";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { generateTicket } from "@/service/tableau/tableau-service";
export default function ContentManagement() {
const [startDate, setStartDate] = useState<any>(new Date());
const [endDate, setEndDate] = useState<any>(new Date());
const [hasMounted, setHasMounted] = useState(false);
// const t = useTranslations("AnalyticsDashboard");
const levelName = getCookiesDecrypt("ulnae");
const poldaState = Cookies.get("state");
const provState = Cookies.get("state-prov");
const [ticket1, setTicket1] = useState("");
const [ticket2, setTicket2] = useState("");
const [ticket3, setTicket3] = useState("");
const [ticket4, setTicket4] = useState("");
const [ticket5, setTicket5] = useState("");
const [ticket6, setTicket6] = useState("");
const [isInternational, setIsInternational] = useState([false, false, false]);
const baseUrl = "https://analytic.sitani.info/";
const url = "https://analytic.sitani.info/trusted/";
const view1 =
levelName == "MABES POLRI"
? isInternational[0]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-rank?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-rank?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-rank?provinsi-polda=${provState}&`;
const view2 =
levelName == "MABES POLRI"
? isInternational[1]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-rank-kat?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-rank-kat?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-rank-kat?provinsi-polda=${poldaState}&`;
const view3 =
levelName == "MABES POLRI"
? isInternational[2]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-act?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-act?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-act?provinsi-polda=${poldaState}&`;
const view4 =
levelName == "MABES POLRI"
? isInternational[3]
? "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-act-jnl?"
: "views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-act-jnl?"
: `views/2023_08_MediaHUB-KtnMgt_Rev100/db-ktn-act-jnl?provinsi-polda=${poldaState}&`;
const param = ":embed=yes&:toolbar=no&:iframeSizedToWindow=true";
useEffect(() => {
async function initState() {
const response1 = await generateTicket();
setTicket1(response1?.data?.data);
const response2 = await generateTicket();
setTicket2(response2?.data?.data);
const response3 = await generateTicket();
setTicket3(response3?.data?.data);
const response4 = await generateTicket();
setTicket4(response4?.data?.data);
}
initState();
}, [isInternational]);
// Hooks
useEffect(() => {
setHasMounted(true);
}, []);
// Render
if (!hasMounted) return null;
const handleInternational = (index: number, val: boolean) => {
const updatedIsInternational = [...isInternational];
updatedIsInternational[index] = val;
setIsInternational(updatedIsInternational);
};
return (
<div>
<SiteBreadcrumb />
<div className="flex flex-col gap-3">
<Accordion
id="schedule-press-conference"
type="single"
collapsible
className="w-full"
>
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
PUBLISH JADWAL PRESS CONFERENCE TERBANYAK
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket1 == "" ? (
<iframe
src={`${baseUrl + view1 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket1}/${view1}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion
id="content-press-conference"
type="single"
collapsible
className="w-full"
>
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
JUMLAH PRODUKSI KONTEN UNTUK KATEGORI PRESS CONFERENCE
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket2 == "" ? (
<iframe
src={`${baseUrl + view2 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket2}/${view2}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion
id="interaction-press-conference"
type="single"
collapsible
className="w-full"
>
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
TINGKAT INTERAKSI KONTEN UNTUK KATEGORI PRESS CONFERENCE
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket3 == "" ? (
<iframe
src={`${baseUrl + view3 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket3}/${view3}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<Accordion
id="press-release"
type="single"
collapsible
className="w-full"
>
<AccordionItem value="item-1" className="bg-white dark:bg-black w-full">
<AccordionTrigger className="bg-white dark:bg-black">
AKTIFITAS MEDIA BERKAITAN DENGAN PERS RILIS
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
<div className="my-5">
{ticket4 == "" ? (
<iframe
src={`${baseUrl + view4 + param}`}
width="100%"
height="750"
frameBorder="0"
/>
) : (
<iframe
src={`${`${url + ticket4}/${view4}${param}`}`}
width="100%"
height="750"
frameBorder="0"
/>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
);
}

View File

@ -0,0 +1,124 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import Swal from "sweetalert2";
import { Link, useRouter } from "@/i18n/routing";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
import { deleteMediaBlastAccount } from "@/service/broadcast/broadcast";
import { useToast } from "@/components/ui/use-toast";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "accountName",
header: "Nama",
cell: ({ row }) => (
<span className="normal-case">{row.original.mediaBlastAccountName}</span>
),
},
{
accessorKey: "accountType",
header: "Tipe Akun",
cell: ({ row }) => (
<span className="normal-case">{row.original.mediaBlastAccountType}</span>
),
},
{
accessorKey: "emailAddress",
header: "Email",
cell: ({ row }) => (
<span className="normal-case">{row.original.mediaBlastAccountEmail}</span>
),
},
{
accessorKey: "whatsappNumber",
header: "Whatsapp",
cell: ({ row }) => <span>{row.original.mediaBlastAccountPhone}</span>,
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
const { toast } = useToast();
const router = useRouter();
const handleDelete = (id: string) => {
MySwal.fire({
title: "Apakah anda ingin menghapus data?",
showCancelButton: true,
confirmButtonColor: "#dc3545",
confirmButtonText: "Iya",
cancelButtonText: "Tidak",
}).then((result) => {
if (result.isConfirmed) {
doDeleteAccount(id);
}
});
};
async function succes() {
toast({
title: "Sukses",
description: "Berhasil Delete",
});
window.location.reload();
}
async function doDeleteAccount(id: string) {
loading();
const response = await deleteMediaBlastAccount(id);
close();
if (response?.error) {
error(response.message);
return false;
}
succes();
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Link
href={`/admin/broadcast/campaign-list/account-list/edit/${row.original.mediaBlastAccountId}`}
>
Edit
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<a onClick={() => handleDelete(row.original.id)}>Delete</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,592 @@
"use client";
import * as React from "react";
import {
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import {
Select as UISelect,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Checkbox } from "@/components/ui/checkbox";
import { Icon } from "@iconify/react";
import { useParams, useSearchParams } from "next/navigation";
import { Badge } from "@/components/ui/badge";
import { X } from "lucide-react";
import ReactSelect from "react-select";
import columns from "./column";
import TablePagination from "@/components/table/table-pagination";
import {
getMediaBlastCampaignAccountList,
deleteMediaBlastCampaignAccount,
saveMediaBlastCampaignAccountBulk,
} from "@/service/broadcast/broadcast";
import {
AdministrationUserList,
getUserListAll,
} from "@/service/management-user/management-user";
import { close, loading, error, success, successCallback } from "@/config/swal";
import { Link } from "@/i18n/routing";
// Mock data for available accounts - replace with actual API call
const availableAccounts = [
{ id: "1", accountName: "Account 1", category: "polri" },
{ id: "2", accountName: "Account 2", category: "jurnalis" },
{ id: "3", accountName: "Account 3", category: "umum" },
{ id: "4", accountName: "Account 4", category: "ksp" },
{ id: "5", accountName: "Account 5", category: "polri" },
];
const AccountListTable = () => {
const params = useParams();
const searchParams = useSearchParams();
const campaignId = params?.id as string;
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [filtered, setFiltered] = React.useState<string[]>([]);
// --- state utk Dialog Pilih Akun ---
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const [accountCategory, setAccountCategory] = React.useState<string>("");
const [selectedAccount, setSelectedAccount] = React.useState<any[]>([]);
const [selectedCategory, setSelectedCategory] = React.useState<string>("");
const [availableAccountsList, setAvailableAccountsList] =
React.useState<any[]>(availableAccounts);
const [usersList, setUsersList] = React.useState<any[]>([]);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) setPage(Number(pageFromUrl));
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page, filtered]);
async function fetchData() {
try {
loading();
const res = await getMediaBlastCampaignAccountList(
page - 1,
filtered ? filtered.join(",") : "",
campaignId
);
const data = res?.data?.data;
const contentData = data?.content || [];
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * 10 + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
close();
} catch (err) {
console.error("Error fetching tasks:", err);
close();
}
}
// --- API helpers ---
async function doDeleteAccount(id: string) {
loading();
const response = await deleteMediaBlastCampaignAccount(id);
close();
if (response?.error) {
error(response.message);
return;
}
fetchData();
}
async function saveCampaignAccount() {
try {
loading();
if (accountCategory === "all-account") {
// Handle all accounts - send only campaignId and category "all"
const request = {
mediaBlastCampaignId: campaignId,
mediaBlastAccountCategory: "all",
};
const response = await saveMediaBlastCampaignAccountBulk(request);
if (response?.error) {
error(response.message);
return;
}
} else if (accountCategory === "kategori" && selectedCategory) {
// Handle category selection - send campaignId and role-based category
let roleId = "";
switch (selectedCategory) {
case "umum":
roleId = "5";
break;
case "jurnalis":
roleId = "6";
break;
case "polri":
roleId = "7";
break;
case "ksp":
roleId = "8";
break;
default:
roleId = "5";
}
const request = {
mediaBlastCampaignId: campaignId,
mediaBlastAccountCategory: `role-${roleId}`,
};
const response = await saveMediaBlastCampaignAccountBulk(request);
if (response?.error) {
error(response.message);
return;
}
} else if (accountCategory === "custom") {
// Handle custom selection - send campaignId and selected user IDs
const request = {
mediaBlastCampaignId: campaignId,
mediaBlastAccountIds: selectedAccount.map((acc) => acc.id),
};
const response = await saveMediaBlastCampaignAccountBulk(request);
if (response?.error) {
error(response.message);
return;
}
}
close();
successCallback("Akun berhasil ditambahkan ke campaign!");
resetDialogState();
fetchData();
} catch (err) {
close();
error("Terjadi kesalahan saat menyimpan akun");
}
}
const resetDialogState = () => {
setAccountCategory("");
setSelectedAccount([]);
setSelectedCategory("");
setUsersList([]);
setIsDialogOpen(false);
};
const fetchUsersList = async () => {
try {
loading();
const response = await getUserListAll();
if (response?.data?.data?.content) {
setUsersList(response.data.data.content);
}
close();
} catch (err) {
close();
error("Terjadi kesalahan saat mengambil daftar user");
}
};
const handleFilter = (id: string, checked: boolean) => {
let temp = [...filtered];
if (checked) temp = [...temp, id];
else temp = temp.filter((a) => a !== id);
setFiltered(temp);
};
const removeSelectedAccount = (accountId: string) => {
setSelectedAccount(selectedAccount.filter((acc) => acc.id !== accountId));
};
const getFilteredAccounts = () => {
if (accountCategory === "kategori" && selectedCategory) {
return availableAccountsList.filter(
(acc) => acc.category === selectedCategory
);
}
return availableAccountsList;
};
return (
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
<div className="flex justify-between mb-3 items-center">
<p className="text-xl font-medium text-default-900">Daftar Akun</p>
<div className="flex flex-row gap-3">
{/* === Dialog Pilih Akun === */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" className="text-sm">
<Icon icon="tdesign:user-add-filled" className="mr-2" />
Pilih Akun
</Button>
</DialogTrigger>
<DialogContent
size="md"
className="max-w-xl max-h-[80vh] overflow-y-auto"
>
<DialogHeader>
<DialogTitle>Pilih Akun Untuk Campaign Ini</DialogTitle>
</DialogHeader>
<div className="space-y-4 my-3">
<RadioGroup
value={accountCategory}
onValueChange={(val) => {
setAccountCategory(val);
setSelectedAccount([]);
setSelectedCategory("");
if (val === "custom") {
fetchUsersList();
}
}}
className="flex space-x-6"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="all-account" id="all-account" />
<Label htmlFor="all-account">Semua Akun</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="kategori" id="kategori" />
<Label htmlFor="kategori">Kategori</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="custom" id="custom" />
<Label htmlFor="custom">Kustom</Label>
</div>
</RadioGroup>
{/* Category Selection */}
{accountCategory === "kategori" && (
<div className="space-y-2">
<Label>Pilih Kategori:</Label>
<UISelect onValueChange={(val) => setSelectedCategory(val)}>
<SelectTrigger>
<SelectValue placeholder="Pilih kategori" />
</SelectTrigger>
<SelectContent>
<SelectItem value="umum">Umum</SelectItem>
<SelectItem value="polri">Polri</SelectItem>
<SelectItem value="ksp">KSP</SelectItem>
<SelectItem value="jurnalis">Jurnalis</SelectItem>
</SelectContent>
</UISelect>
</div>
)}
{/* Custom Account Selection */}
{accountCategory === "custom" && (
<div className="space-y-3">
<Label>Pilih User:</Label>
<ReactSelect
isMulti
options={usersList.map((user: any) => ({
value: user.id,
label: `${user.fullname} (${user.role?.name})`,
user: user,
}))}
value={selectedAccount.map((acc: any) => ({
value: acc.id,
label: `${acc.fullname} (${acc.role?.name})`,
user: acc,
}))}
onChange={(selectedOptions: any) => {
const selectedUsers = selectedOptions
? selectedOptions.map((option: any) => option.user)
: [];
setSelectedAccount(selectedUsers);
}}
placeholder="Cari dan pilih user..."
noOptionsMessage={() => "Tidak ada user ditemukan"}
loadingMessage={() => "Memuat..."}
isSearchable={true}
isClearable={true}
className="react-select"
classNamePrefix="select"
/>
{/* Selected Accounts Display */}
{selectedAccount.length > 0 && (
<div className="space-y-2">
<Label>User Terpilih ({selectedAccount.length}):</Label>
<div className="flex flex-wrap gap-2">
{selectedAccount.map((acc) => (
<Badge
key={acc.id}
className="flex items-center gap-1"
>
{acc.fullname}
<X
className="h-3 w-3 cursor-pointer"
onClick={() => removeSelectedAccount(acc.id)}
/>
</Badge>
))}
</div>
</div>
)}
</div>
)}
{/* All Accounts Info */}
{accountCategory === "all-account" && (
<div className="p-3 bg-blue-50 rounded-md">
<p className="text-sm text-blue-700">
Semua akun akan ditambahkan ke campaign ini.
</p>
</div>
)}
{/* Category Accounts Info */}
{accountCategory === "kategori" && selectedCategory && (
<div className="p-3 bg-green-50 rounded-md">
<p className="text-sm text-green-700">
Semua akun dengan role "{selectedCategory.toUpperCase()}"
akan ditambahkan.
</p>
</div>
)}
{/* Custom Selection Info */}
{accountCategory === "custom" && (
<div className="p-3 bg-purple-50 rounded-md">
<p className="text-sm text-purple-700">
{selectedAccount.length} user terpilih akan ditambahkan ke
campaign ini.
</p>
</div>
)}
</div>
<DialogFooter>
<Button
onClick={saveCampaignAccount}
disabled={
!accountCategory ||
(accountCategory === "custom" &&
selectedAccount.length < 1) ||
(accountCategory === "kategori" && !selectedCategory)
}
>
Simpan
</Button>
<DialogClose asChild>
<Button variant="outline" onClick={resetDialogState}>
Batal
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
{/* === Filter Akun === */}
<div className="flex flex-row justify-end">
{/* <div className="flex flex-row gap-4">
<Link href="/admin/broadcast/campaign-list/account-list/create">
<Button variant="default" className="bg-[#3f37c9] gap-2">
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M17 13h-4v4h-2v-4H7v-2h4V7h2v4h4m2-8H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2"
/>
</svg>
</span>
Tambahkan Akun
</Button>
</Link>
<Button variant="default" className="bg-[#3f37c9] gap-2">
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="none">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M12 2v6.5a1.5 1.5 0 0 0 1.5 1.5H20v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-1h3.414l-1.121 1.121a1 1 0 1 0 1.414 1.415l2.829-2.829a1 1 0 0 0 0-1.414l-2.829-2.828a1 1 0 1 0-1.414 1.414L7.414 17H4V4a2 2 0 0 1 2-2zM4 17v2H3a1 1 0 1 1 0-2zM14 2.043a2 2 0 0 1 1 .543L19.414 7a2 2 0 0 1 .543 1H14z"
/>
</g>
</svg>
</span>
Import Akun
</Button>
</div> */}
<Popover>
<PopoverTrigger asChild>
<Button size="md" variant="outline">
Filter
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 ">
<div className="flex flex-col gap-2 px-2">
<div className="flex justify-between text-sm">
<p>Filter</p>
<a
onClick={() => fetchData()}
className="cursor-pointer text-primary"
>
Simpan
</a>
</div>
<div className="flex flex-col gap-1 overflow-auto max-h-[300px] text-xs custom-scrollbar-table">
{["polri", "jurnalis", "umum", "ksp"].map((cat) => (
<div key={cat} className="flex items-center space-x-2">
<Checkbox
id={cat}
checked={filtered.includes(cat)}
onCheckedChange={(e) => handleFilter(cat, Boolean(e))}
/>
<label
htmlFor={cat}
className="text-xs font-medium leading-none"
>
{cat.toUpperCase()}
</label>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
</div>
{/* === Table Data === */}
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default AccountListTable;

View File

@ -0,0 +1,661 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
getMediaBlastCampaignPage,
saveMediaBlastAccount,
} from "@/service/broadcast/broadcast";
import { error } from "@/config/swal";
import { useRouter } from "@/i18n/routing";
import { Checkbox } from "@/components/ui/checkbox";
import { useEffect, useState } from "react";
// ----------------------------
// ZOD SCHEMA (dinamis)
// ----------------------------
const FormSchema = z.object({
name: z.string({ required_error: "Required" }),
accountType: z
.array(z.string())
.min(1, "Pilih minimal satu tipe akun"),
email: z.string().optional(),
whatsapp: z.string().optional(),
campaignId: z.string({ required_error: "Required" }),
}).refine(
(data) => {
if (data.accountType.includes("email") && !data.email) return false;
return true;
},
{ message: "Email wajib diisi", path: ["email"] }
).refine(
(data) => {
if (data.accountType.includes("wa") && !data.whatsapp) return false;
return true;
},
{ message: "Whatsapp wajib diisi", path: ["whatsapp"] }
);
// ----------------------------
// COMPONENT
// ----------------------------
export default function CreateAccountForBroadcast() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
accountType: [],
email: "",
whatsapp: "",
},
});
const selectedTypes = form.watch("accountType");
const [campaigns, setCampaigns] = useState<any[]>([]);
useEffect(() => {
fetchCampaignList();
}, []);
async function fetchCampaignList() {
try {
const res = await getMediaBlastCampaignPage(0);
setCampaigns(res?.data?.data?.content ?? []);
} catch (e) {
console.log("Error fetch campaign:", e);
}
}
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
function successSubmit() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/broadcast/campaign-list/account-list");
});
}
const save = async (data: z.infer<typeof FormSchema>) => {
const reqData = {
accountName: data.name,
accountType: data.accountType.join(","),
emailAddress: data.email ?? "",
whatsappNumber: data.whatsapp ?? "",
campaignId: data.campaignId,
};
console.log("REQ:", reqData);
const response = await saveMediaBlastAccount(reqData);
if (response?.error) {
error(response.message);
return;
}
successSubmit();
};
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm p-4"
>
<p className="font-semibold">Account</p>
{/* NAMA */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nama</FormLabel>
<Input
value={field.value}
placeholder="Masukkan nama"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
{/* CHECKBOX TIPE AKUN */}
<FormField
control={form.control}
name="accountType"
render={({ field }) => (
<FormItem>
<FormLabel>Tipe Akun</FormLabel>
<div className="flex flex-row gap-4">
{/* WA */}
<div className="flex items-center gap-2">
<Checkbox
checked={field.value.includes("wa")}
onCheckedChange={(checked) =>
checked
? field.onChange([...field.value, "wa"])
: field.onChange(field.value.filter((v) => v !== "wa"))
}
/>
<label>Whatsapp</label>
</div>
{/* EMAIL */}
<div className="flex items-center gap-2">
<Checkbox
checked={field.value.includes("email")}
onCheckedChange={(checked) =>
checked
? field.onChange([...field.value, "email"])
: field.onChange(
field.value.filter((v) => v !== "email")
)
}
/>
<label>Email</label>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* FORM WHATSAPP */}
{selectedTypes.includes("wa") && (
<FormField
control={form.control}
name="whatsapp"
render={({ field }) => (
<FormItem>
<FormLabel>Whatsapp</FormLabel>
<Input
type="number"
placeholder="Masukkan nomor Whatsapp"
{...field}
/>
<FormMessage />
</FormItem>
)}
/>
)}
{/* FORM EMAIL */}
{selectedTypes.includes("email") && (
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Input
type="email"
placeholder="Masukkan email"
{...field}
/>
<FormMessage />
</FormItem>
)}
/>
)}
{/* CAMPAIGN */}
<FormField
control={form.control}
name="campaignId"
render={({ field }) => (
<FormItem>
<FormLabel>Campaign</FormLabel>
<FormControl>
<select
className="w-full border rounded-md p-2 text-sm"
value={field.value}
onChange={field.onChange}
>
<option value="" className="text-slate-400">
Pilih campaign
</option>
{campaigns.map((c: any) => (
<option key={c.id} value={c.id}>
{c.title || `Campaign ${c.id}`}
</option>
))}
</select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* BUTTON */}
<div className="flex flex-row gap-2 mt-4 pt-4">
<Button type="button" variant="outline" color="destructive">
Cancel
</Button>
<Button type="submit" color="primary">
Submit
</Button>
</div>
</form>
</Form>
</div>
);
}
// "use client";
// import SiteBreadcrumb from "@/components/site-breadcrumb";
// import { z } from "zod";
// import { useForm } from "react-hook-form";
// import { zodResolver } from "@hookform/resolvers/zod";
// import {
// Form,
// FormControl,
// FormField,
// FormItem,
// FormLabel,
// FormMessage,
// } from "@/components/ui/form";
// import withReactContent from "sweetalert2-react-content";
// import Swal from "sweetalert2";
// import { Input } from "@/components/ui/input";
// import { Button } from "@/components/ui/button";
// import {
// getMediaBlastCampaignPage,
// saveMediaBlastAccount,
// } from "@/service/broadcast/broadcast";
// import { error } from "@/config/swal";
// import { useRouter } from "@/i18n/routing";
// import { Checkbox } from "@/components/ui/checkbox";
// import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
// import { useEffect, useState } from "react";
// // const FormSchema = z.object({
// // name: z.string({
// // required_error: "Required",
// // }),
// // accountType: z
// // .array(z.string())
// // .refine((value) => value.some((item) => item), {
// // message: "Required",
// // }),
// // accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
// // required_error: "Required",
// // }),
// // email: z.string({
// // required_error: "Required",
// // }),
// // whatsapp: z.string({
// // required_error: "Required",
// // }),
// // campaignId: z.string({ required_error: "Required" }),
// // });
// const FormSchema = z
// .object({
// name: z.string().min(1, "Required"),
// accountType: z.array(z.string()).refine((value) => value.length > 0, {
// message: "Pilih minimal satu tipe akun",
// }),
// accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
// required_error: "Required",
// }),
// email: z.string().optional(),
// whatsapp: z.string().optional(),
// campaignId: z.string().min(1, "Required"),
// })
// .refine(
// (data) => {
// if (data.accountType.includes("email")) {
// return !!data.email && data.email.trim() !== "";
// }
// return true;
// },
// { path: ["email"], message: "Email wajib diisi" }
// )
// .refine(
// (data) => {
// if (data.accountType.includes("wa")) {
// return !!data.whatsapp && data.whatsapp.trim() !== "";
// }
// return true;
// },
// { path: ["whatsapp"], message: "Whatsapp wajib diisi" }
// );
// export default function CreateAccountForBroadcast() {
// const MySwal = withReactContent(Swal);
// const router = useRouter();
// const form = useForm<z.infer<typeof FormSchema>>({
// resolver: zodResolver(FormSchema),
// defaultValues: { accountType: [] },
// });
// const selectedTypes = form.watch("accountType");
// const [campaigns, setCampaigns] = useState<any[]>([]);
// useEffect(() => {
// fetchCampaignList();
// }, []);
// async function fetchCampaignList() {
// try {
// const res = await getMediaBlastCampaignPage(0);
// setCampaigns(res?.data?.data?.content ?? []);
// } catch (e) {
// console.log("Error fetch campaign:", e);
// }
// }
// const onSubmit = async (data: z.infer<typeof FormSchema>) => {
// MySwal.fire({
// title: "Simpan Data",
// text: "Apakah Anda yakin ingin menyimpan data ini?",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#d33",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "Simpan",
// }).then((result) => {
// if (result.isConfirmed) {
// save(data);
// }
// });
// };
// function successSubmit() {
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then((result) => {
// if (result.isConfirmed) {
// router.push("/admin/broadcast/campaign-list/account-list");
// }
// });
// }
// const save = async (data: z.infer<typeof FormSchema>) => {
// const reqData = {
// accountName: data.name,
// accountType: data.accountType.join(","),
// accountCategory: data.accountCategory,
// emailAddress: data.email ?? "",
// whatsappNumber: data.whatsapp ?? "",
// campaignId: data.campaignId,
// };
// console.log("data", data);
// const response = await saveMediaBlastAccount(reqData);
// if (response?.error) {
// error(response.message);
// return false;
// }
// successSubmit();
// };
// return (
// <div>
// <SiteBreadcrumb />
// <Form {...form}>
// <form
// onSubmit={form.handleSubmit(onSubmit)}
// className="space-y-3 bg-white rounded-sm p-4"
// >
// <p className="fonnt-semibold">Account</p>
// <FormField
// control={form.control}
// name="name"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Nama</FormLabel>
// <Input
// value={field.value}
// placeholder="Masukkan nama"
// onChange={field.onChange}
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="accountType"
// render={() => (
// <FormItem>
// <FormLabel>Tipe Akun</FormLabel>
// <div className="flex flex-row gap-2">
// {" "}
// <FormField
// key="wa"
// control={form.control}
// name="accountType"
// render={({ field }) => {
// return (
// <FormItem
// key="wa"
// className="flex flex-row items-start space-x-3 space-y-0"
// >
// <FormControl>
// <Checkbox
// checked={field.value?.includes("wa")}
// onCheckedChange={(checked) => {
// return checked
// ? field.onChange([...field.value, "wa"])
// : field.onChange(
// field.value?.filter(
// (value) => value !== "wa"
// )
// );
// }}
// />
// </FormControl>
// <FormLabel className="font-normal">
// Whatsapp
// </FormLabel>
// </FormItem>
// );
// }}
// />
// <FormField
// key="email"
// control={form.control}
// name="accountType"
// render={({ field }) => {
// return (
// <FormItem
// key="email"
// className="flex flex-row items-start space-x-3 space-y-0"
// >
// <FormControl>
// <Checkbox
// checked={field.value?.includes("email")}
// onCheckedChange={(checked) => {
// return checked
// ? field.onChange([...field.value, "email"])
// : field.onChange(
// field.value?.filter(
// (value) => value !== "email"
// )
// );
// }}
// />
// </FormControl>
// <FormLabel className="font-normal">Email</FormLabel>
// </FormItem>
// );
// }}
// />
// </div>
// <FormMessage />
// </FormItem>
// )}
// />
// {/* <FormField
// control={form.control}
// name="accountCategory"
// render={({ field }) => (
// <FormItem className="space-y-3">
// <FormLabel>Kategori</FormLabel>
// <FormControl>
// <RadioGroup
// onValueChange={field.onChange}
// defaultValue={field.value}
// className="flex flex-row gap-2"
// >
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="polri" />
// </FormControl>
// <FormLabel className="font-normal">POLRI</FormLabel>
// </FormItem>
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="jurnalis" />
// </FormControl>
// <FormLabel className="font-normal">JURNALIS</FormLabel>
// </FormItem>
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="umum" />
// </FormControl>
// <FormLabel className="font-normal">UMUM</FormLabel>
// </FormItem>
// <FormItem className="flex items-center space-x-3 space-y-0">
// <FormControl>
// <RadioGroupItem value="ksp" />
// </FormControl>
// <FormLabel className="font-normal">KSP</FormLabel>
// </FormItem>
// </RadioGroup>
// </FormControl>
// <FormMessage />
// </FormItem>
// )}
// /> */}
// <FormField
// control={form.control}
// name="email"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Email</FormLabel>
// <Input
// type="email"
// value={field.value}
// placeholder="Masukkan email"
// onChange={field.onChange}
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="whatsapp"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Nama</FormLabel>
// <Input
// type="number"
// value={field.value}
// placeholder="Masukkan whatsapp"
// onChange={field.onChange}
// />
// <FormMessage />
// </FormItem>
// )}
// />
// <FormField
// control={form.control}
// name="campaignId"
// render={({ field }) => (
// <FormItem>
// <FormLabel>Campaign</FormLabel>
// <FormControl>
// <select
// className="w-full border rounded-md p-2"
// value={field.value}
// onChange={field.onChange}
// >
// <option value="" className="text-slate-400">
// Pilih campaign
// </option>
// {campaigns.map((c: any) => (
// <option key={c.id} value={c.id}>
// {c.title || `Campaign ${c.id}`}
// </option>
// ))}
// </select>
// </FormControl>
// <FormMessage />
// </FormItem>
// )}
// />
// <div className="flex flex-row gap-2 mt-4 pt-4">
// <Button
// size="md"
// type="button"
// variant="outline"
// color="destructive"
// className="text-xs"
// >
// Cancel
// </Button>
// <Button size="md" type="submit" color="primary" className="text-xs">
// Submit
// </Button>
// </div>
// </form>
// </Form>
// </div>
// );
// }

View File

@ -0,0 +1,362 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { getOnlyDate } from "@/utils/globals";
import {
getMediaBlastAccount,
saveMediaBlastAccount,
saveMediaBlastCampaign,
} from "@/service/broadcast/broadcast";
import { error } from "@/config/swal";
import { useRouter } from "@/i18n/routing";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useParams } from "next/navigation";
import { useEffect } from "react";
import {
getUserById,
saveUserInternal,
} from "@/service/management-user/management-user";
// const FormSchema = z.object({
// fullname: z.string({
// required_error: "Required",
// }),
// // accountType: z
// // .array(z.string())
// // .refine((value) => value.some((item) => item), {
// // message: "Required",
// // }),
// // accountCategory: z.enum(["polri", "jurnalis", "umum", "ksp"], {
// // required_error: "Required",
// // }),
// email: z.string({
// required_error: "Required",
// }),
// phoneNumber: z.string({
// required_error: "Required",
// }),
// });
const FormSchema = z.object({
fullname: z.string({ required_error: "Required" }),
email: z.string({ required_error: "Required" }),
phoneNumber: z.string({ required_error: "Required" }),
username: z.string().optional(),
role: z.string().optional(),
level: z.string().optional(),
nrp: z.string().optional(),
address: z.string().optional(),
password: z.string().optional(),
confirmPassword: z.string().optional(),
});
export default function EditAccountForBroadcast() {
const id = useParams()?.id;
const MySwal = withReactContent(Swal);
const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
// defaultValues: { accountType: [] },
});
useEffect(() => {
async function getDetailData() {
const response = await getUserById(String(id));
const details = response?.data?.data;
console.log("Response full:", response);
form.setValue("fullname", details?.fullname);
form.setValue("username", details?.username);
form.setValue("phoneNumber", details?.phoneNumber);
// form.setValue("nrp", details?.memberIdentity);
// form.setValue("address", details?.address);
form.setValue("email", details?.email);
form.setValue("role", details?.role?.code);
// form.setValue("level", String(details?.userLevelId));
// form.setValue("name", details?.accountName);
// form.setValue("fullname", details?.fullname);
// form.setValue("email", details?.email);
// form.setValue("phoneNumber", details?.phoneNumber);
// form.setValue("whatsapp", details?.whatsappNumber);
// form.setValue("accountCategory", details?.accountCategory);
// form.setValue("accountType", details?.accountType.split(","));
}
getDetailData();
}, [id]);
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
function successSubmit() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/broadcast/campaign-list");
}
});
}
const save = async (data: z.infer<typeof FormSchema>) => {
const reqData = {
id: Number(id),
// accountName: data.fullname,
// accountType: data.accountType.join(","),
// accountCategory: data.accountCategory,
// emailAddress: data.email,
phoneNumber: data.phoneNumber,
firstName: data.fullname,
username: data.username,
roleId: data.role,
// userLevelId: Number(data.level),
// memberIdentity: data.nrp,
// address: data.address,
email: data.email,
isDefault: false,
isAdmin: true,
};
console.log("data", data);
const response = await saveUserInternal(reqData);
if (response?.error) {
error(response.message);
return false;
}
successSubmit();
};
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm p-4"
>
<p className="fonnt-semibold">Account</p>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nama</FormLabel>
<Input
value={field.value}
placeholder="Masukkan nama"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="accountType"
render={() => (
<FormItem>
<FormLabel>Tipe Akun</FormLabel>
<div className="flex flex-row gap-2">
<FormField
key="wa"
control={form.control}
name="accountType"
render={({ field }) => {
return (
<FormItem
key="wa"
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes("wa")}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, "wa"])
: field.onChange(
field.value?.filter(
(value) => value !== "wa"
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
Whatsapp
</FormLabel>
</FormItem>
);
}}
/>
<FormField
key="email"
control={form.control}
name="accountType"
render={({ field }) => {
return (
<FormItem
key="email"
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes("email")}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, "email"])
: field.onChange(
field.value?.filter(
(value) => value !== "email"
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">Email</FormLabel>
</FormItem>
);
}}
/>
</div>
<FormMessage />
</FormItem>
)}
/> */}
{/* <FormField
control={form.control}
name="accountCategory"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Kategori</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-row gap-2"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="polri" />
</FormControl>
<FormLabel className="font-normal">POLRI</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="jurnalis" />
</FormControl>
<FormLabel className="font-normal">JURNALIS</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="umum" />
</FormControl>
<FormLabel className="font-normal">UMUM</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="ksp" />
</FormControl>
<FormLabel className="font-normal">KSP</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<Input
type="email"
value={field.value}
placeholder="Masukkan email"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Nomor Whatsapp</FormLabel>
<Input
type="number"
value={field.value}
placeholder="Masukkan nomor whatsapp"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row gap-2 mt-4 pt-4">
<Button
size="md"
type="button"
variant="outline"
color="destructive"
className="text-xs"
>
Cancel
</Button>
<Button size="md" type="submit" color="primary" className="text-xs">
Submit
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import AccountListTable from "./component/table";
export default function AdminCampaignList() {
return (
<div>
<SiteBreadcrumb />
<AccountListTable />
</div>
);
}

View File

@ -0,0 +1,225 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Link, useRouter } from "@/i18n/routing";
import { close, error, loading, success } from "@/config/swal";
import { deleteMediaBlastCampaign } from "@/service/broadcast/broadcast";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Judul",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("title")}</span>
),
},
{
accessorKey: "sendTime",
header: "Tanggal & Waktu",
cell: ({ row }) => <span>{row.getValue("sendTime")}</span>,
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<Badge
className={`text-center items-center text-white ${
row.getValue("status") == "done"
? "bg-blue-500"
: row.getValue("status") == "waiting"
? "bg-yellow-400"
: "bg-red-600"
}`}
>
{row.getValue("status") == "done"
? "Selesai"
: row.getValue("status") == "waiting"
? "Proses"
: "Gagal"}
</Badge>
),
},
{
id: "actions",
header: "Actions",
cell: ({ row, onDeleteSuccess }: any) => {
const MySwal = withReactContent(Swal);
const handleDelete = (id: number) => {
MySwal.fire({
title: "Apakah anda ingin menghapus data?",
showCancelButton: true,
confirmButtonColor: "#dc3545",
confirmButtonText: "Iya",
cancelButtonText: "Tidak",
}).then((result) => {
if (result.isConfirmed) {
doDeleteAccount(id);
}
});
};
async function doDeleteAccount(id: number) {
loading();
const response = await deleteMediaBlastCampaign(id);
close();
if (response.error) {
error(response.message);
return false;
}
console.log("Delete response:", response);
MySwal.fire({
icon: "success",
title: "Berhasil!",
text: "Data berhasil dihapus.",
confirmButtonColor: "#3085d6",
timer: 2000,
timerProgressBar: true,
});
// ✅ panggil callback dari parent
onDeleteSuccess?.(id);
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link
href={`/admin/broadcast/campaign-list/detail/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Detail
</DropdownMenuItem>
</Link>
<Link
href={`//admin/broadcast/campaign-list/edit/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => handleDelete(row.original.id)}
className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer"
>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
// {
// id: "actions",
// accessorKey: "action",
// header: "Actions",
// enableHiding: false,
// cell: ({ row, onDeleteSuccess }: any) => {
// const MySwal = withReactContent(Swal);
// const handleDelete = (id: any) => {
// MySwal.fire({
// title: "Apakah anda ingin menghapus data?",
// showCancelButton: true,
// confirmButtonColor: "#dc3545",
// confirmButtonText: "Iya",
// cancelButtonText: "Tidak",
// }).then((result: any) => {
// if (result.isConfirmed) {
// doDeleteAccount(id);
// }
// });
// };
// async function doDeleteAccount(id: any) {
// loading();
// const response = await deleteMediaBlastCampaign(id);
// close();
// if (response.error) {
// error(response.message);
// return false;
// }
// console.log("Delete response:", response);
// MySwal.fire({
// icon: "success",
// title: "Berhasil!",
// text: "Data berhasil dihapus.",
// confirmButtonColor: "#3085d6",
// timer: 2000,
// timerProgressBar: true,
// });
// // ✅ langsung hapus dari state
// onDeleteSuccess?.(id);
// }
// return (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button
// size="icon"
// className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
// >
// <MoreVertical className="h-4 w-4 text-default-800" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent className="p-0" align="end">
// <Link
// href={`/admin/broadcast/campaign-list/detail/${row.original.id}`}
// >
// <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
// Detail
// </DropdownMenuItem>
// </Link>
// <Link
// href={`//admin/broadcast/campaign-list/edit/${row.original.id}`}
// >
// <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
// Edit
// </DropdownMenuItem>
// </Link>
// <DropdownMenuItem
// onClick={() => handleDelete(row.original.id)}
// className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer"
// >
// Delete
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// );
// },
// },
];
export default columns;

View File

@ -0,0 +1,227 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
getMediaBlastCampaignPage,
listDataMedia,
} from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing";
import { NewCampaignIcon } from "@/components/icon";
const CampaignListTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
function handleDeleteSuccess(id: number) {
setDataTable((prev) => prev.filter((item) => item.id !== id));
}
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page]);
async function fetchData() {
try {
loading();
const res = await getMediaBlastCampaignPage(page - 1);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * 10 + index + 1;
});
console.log("contentData : ", data);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
close();
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
return (
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
<div className="flex justify-between mb-10 items-center">
<p className="text-xl font-medium text-default-900">Daftar Campaign</p>
<div className="flex flex-row gap-2">
{/* <Link href="/admin/broadcast/campaign-list/account-list">
<Button color="primary" size="md" className="text-sm">
<UserIcon />
Daftar Akun
</Button>
</Link> */}
<Link href="/admin/broadcast/campaign-list/create">
<Button color="primary" size="md" className="text-sm">
<NewCampaignIcon size={23} />
Buat Campaign Baru
</Button>
</Link>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{/* {flexRender(cell.column.columnDef.cell, cell.getContext())} */}
{flexRender(cell.column.columnDef.cell, {
...cell.getContext(),
onDeleteSuccess: (id: number) => {
// setDataTable((prev) => prev.filter((item) => item.id !== id)
fetchData()
},
})}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default CampaignListTable;

View File

@ -0,0 +1,198 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { getOnlyDate } from "@/utils/globals";
import { saveMediaBlastCampaign } from "@/service/broadcast/broadcast";
import { error } from "@/config/swal";
import { useRouter } from "@/i18n/routing";
const FormSchema = z.object({
date: z.date({
required_error: "Required",
}),
title: z.string({
required_error: "Required",
}),
time: z.string({
required_error: "Required",
}),
});
export default function CreateCampaign() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
function successSubmit() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/broadcast/campaign-list");
}
});
}
const save = async (data: z.infer<typeof FormSchema>) => {
const newDate = getOnlyDate(data.date).split("-");
const dates = `${newDate[2]}-${newDate[1]}-${newDate[0]}`;
const reqData = {
title: data.title,
sendTime: `${dates} ${data.time}:00`,
status: "waiting",
};
const response = await saveMediaBlastCampaign(reqData);
if (response?.error) {
error(response.message);
return false;
}
successSubmit();
};
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white dark:bg-black rounded-sm p-4"
>
<p className="fonnt-semibold">Campaign</p>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Judul</FormLabel>
<Input
value={field.value}
placeholder="Masukkan Judul"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<p className="text-sm">Jadwal Kirim</p>
<div className="flex flex-row gap-2">
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
size="md"
className={cn(
"w-[200px] justify-start text-left font-normal border-gray-200",
!field.value && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value ? (
format(field.value, "dd-MM-yyyy")
) : (
<span>Masukkan Bulan</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="time"
render={({ field }) => (
<FormItem>
<Input
type="time"
className="max-w-[100px]"
value={field.value}
placeholder="Masukkan Judul Perencanaan"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row gap-2 mt-4 pt-4">
<Button
size="md"
type="button"
variant="outline"
color="destructive"
className="text-xs"
>
Cancel
</Button>
<Button size="md" type="submit" color="primary" className="text-xs">
Submit
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,406 @@
"use client";
import "react-datepicker/dist/react-datepicker.css";
import { Icon } from "@iconify/react";
import Link from "next/link";
import { useRouter, usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { useMediaQuery } from "react-responsive";
import { close, loading } from "@/config/swal";
import ReactDatePicker from "react-datepicker";
import { getOnlyDate } from "@/utils/globals";
import AccountListTable from "../../account-list/component/table";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import TablePagination from "@/components/table/table-pagination";
import {
getMediaBlastCampaignById,
getMediaBlastBroadcastList,
} from "@/service/broadcast/broadcast";
// Types
interface CampaignData {
id: string;
no: number;
mediaBlastCampaignId: string;
mediaBlastCampaign: {
title: string;
};
subject: string;
type: string;
status: string;
sendDate: string;
}
interface PaginatedResponse {
content: CampaignData[];
totalPages: number;
totalElements: number;
}
interface PageProps {
params: {
id: string;
locale: string;
};
searchParams: {
page?: string;
size?: string;
};
}
export default function BroadcastCampaignDetail({
params,
searchParams,
}: PageProps) {
const router = useRouter();
const pathname = usePathname();
const { id, locale } = params;
const [getData, setGetData] = useState<CampaignData[]>([]);
const [totalPage, setTotalPage] = useState<number>(0);
const [totalData, setTotalData] = useState<number>(0);
const [activeTab, setActiveTab] = useState<
"sent" | "schedule" | "account-list"
>("sent");
const { page, size } = searchParams;
const [calenderState, setCalenderState] = useState<boolean>(false);
const [typeFilter, setTypeFilter] = useState<string>("email");
const [dateRange, setDateRange] = useState<[Date, Date]>([
new Date(),
new Date(),
]);
const [startDate, endDate] = dateRange;
const [startDateString, setStartDateString] = useState<string | undefined>();
const [endDateString, setEndDateString] = useState<string | undefined>();
// Table state
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: parseInt(size || "10"),
});
const pages = page ? parseInt(page) - 1 : 0;
const currentPage = page ? parseInt(page) : 1;
const pageSize = parseInt(size || "10");
const isFHD = useMediaQuery({
minWidth: 1920,
});
const setCurrentPage = (pageNumber: number) => {
const params = new URLSearchParams(searchParams);
params.set("page", pageNumber.toString());
router.push(`${pathname}?${params.toString()}`);
};
async function getListPaginationData() {
loading();
console.log("Type : ", typeFilter);
console.log("Date : ", startDateString, endDateString);
try {
const res = await getMediaBlastBroadcastList(
pages,
activeTab === "schedule",
startDateString || "",
endDateString || "",
typeFilter,
id
);
close();
if (res?.data?.data) {
setupData(res.data.data);
}
} catch (error) {
console.error("Error fetching data:", error);
close();
}
}
useEffect(() => {
getListPaginationData();
}, [
currentPage,
pageSize,
activeTab,
endDateString,
startDateString,
typeFilter,
]);
function setupData(rawData: PaginatedResponse) {
console.log("raw", rawData);
if (rawData !== undefined) {
const dataContent = rawData?.content;
const data: CampaignData[] = [];
dataContent.forEach((element, i) => {
element.no = (currentPage - 1) * pageSize + i + 1;
data.push(element);
});
setGetData(data);
setTotalPage(rawData?.totalPages);
setTotalData(rawData?.totalElements);
}
}
const columns: ColumnDef<CampaignData>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "mediaBlastCampaign.title",
header: "Campaign",
cell: ({ row }) => (
<Link
href={`/${locale}/admin/broadcast/campaign-list/detail/${row.original.mediaBlastCampaignId}`}
className="text-dark"
>
<span className="font-weight-bold">
{row.original.mediaBlastCampaign?.title}
</span>
</Link>
),
},
{
accessorKey: "subject",
header: "Judul",
cell: ({ row }) => (
<Link
href={`/${locale}/admin/broadcast/content/detail/${row.original.id}`}
className="text-dark"
>
<span className="font-weight-bold">{row.getValue("subject")}</span>
</Link>
),
},
{
accessorKey: "type",
header: "Tipe",
cell: ({ row }) => (
<div className="text-right text-black">{row.getValue("type")}</div>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<div className="text-right text-black">{row.getValue("status")}</div>
),
},
{
accessorKey: "sendDate",
header: "Tanggal & Waktu",
cell: ({ row }) => (
<div className="text-black">{row.getValue("sendDate")}</div>
),
},
];
const table = useReactTable({
data: getData,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
useEffect(() => {
function initState() {
if (startDate != null && endDate != null) {
setStartDateString(getOnlyDate(startDate));
setEndDateString(getOnlyDate(endDate));
}
}
console.log("date range", dateRange);
initState();
}, [calenderState, startDate, endDate]);
const handleTypeFilter = (type: string) => {
setTypeFilter(type);
};
return (
<div className="bg-white dark:bg-black container-fluid rounded ">
<div className="mt-1 p-4">
<div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-4">
<Button
onClick={() => setActiveTab("sent")}
size="md"
className={`hover:text-white ${
activeTab === "sent"
? "bg-indigo-600 text-white "
: "bg-white text-black "
}`}
>
Sent
</Button>
<Button
onClick={() => setActiveTab("schedule")}
size="md"
className={`hover:text-white ${
activeTab === "schedule"
? "bg-indigo-600 text-white "
: "bg-white text-black "
}`}
>
Schedule
</Button>
<Button
onClick={() => setActiveTab("account-list")}
size="md"
className={`hover:text-white ${
activeTab === "account-list"
? "bg-indigo-600 text-white "
: "bg-white text-black "
}`}
>
List Akun
</Button>
</div>
{activeTab === "account-list" ? (
<AccountListTable />
) : (
<>
<div className="broadcast-filter flex flex-column gap-3 mb-4">
<div className="flex flex-row gap-1 border-2 rounded-md w-fit h-fit">
<Button
onClick={() => handleTypeFilter("email")}
className={`hover:text-white ${
typeFilter === "email"
? "bg-black text-white "
: "bg-white text-black "
}`}
size="sm"
>
Email Blast
</Button>
<Button
onClick={() => handleTypeFilter("wa")}
className={`hover:text-white ${
typeFilter === "wa"
? "bg-black text-white "
: "bg-white text-black "
}`}
size="sm"
>
WhatsApp Blast
</Button>
</div>
<div className="dashboard-date-picker">
<div className="mx-6 my-1">
<ReactDatePicker
selectsRange
startDate={startDate}
endDate={endDate}
onChange={(update) => {
setDateRange(update as [Date, Date]);
}}
placeholderText="Pilih Tanggal"
onCalendarClose={() => setCalenderState(!calenderState)}
className="form-control rounded-pill"
/>
</div>
</div>
</div>
<div className="w-full overflow-x-auto">
<Table className="overflow-hidden mt-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
</>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,227 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { getOnlyDate } from "@/utils/globals";
import {
getMediaBlastCampaignById,
saveMediaBlastCampaign,
} from "@/service/broadcast/broadcast";
import { error } from "@/config/swal";
import { useRouter } from "@/i18n/routing";
import { useParams } from "next/navigation";
import { useEffect } from "react";
const FormSchema = z.object({
date: z.date({
required_error: "Required",
}),
title: z.string({
required_error: "Required",
}),
time: z.string({
required_error: "Required",
}),
status: z.string({
required_error: "Required",
}),
});
export default function EditCampaign() {
const id = useParams()?.id;
const MySwal = withReactContent(Swal);
const router = useRouter();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});
useEffect(() => {
async function getInitData() {
const response = await getMediaBlastCampaignById(String(id));
const date = response?.data?.data?.sendTime.split(" ");
const dateInput: Date = new Date(date[0]);
const time = date[1].split(":");
form.setValue("title", response?.data?.data?.title);
form.setValue("status", response?.data?.data?.status);
form.setValue("date", new Date(format(dateInput, "dd-MM-yyyy")));
form.setValue("time", `${time[0]}:${time[1]}`);
}
if (id != undefined) {
getInitData();
}
}, [id]);
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
function successSubmit() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/broadcast/campaign-list");
}
});
}
const save = async (data: z.infer<typeof FormSchema>) => {
const newDate = getOnlyDate(data.date).split("-");
const dates = `${newDate[2]}-${newDate[1]}-${newDate[0]}`;
const reqData = {
id: String(id),
title: data.title,
sendTime: `${dates} ${data.time}:00`,
status: data.status,
};
const response = await saveMediaBlastCampaign(reqData);
if (response?.error) {
error(response.message);
return false;
}
successSubmit();
};
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white dark:bg-black rounded-sm p-4"
>
<p className="fonnt-semibold">Campaign</p>
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Judul</FormLabel>
<Input
value={field.value}
placeholder="Masukkan Judul"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<p className="text-sm">Jadwal Kirim</p>
<div className="flex flex-row gap-2">
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
size="md"
className={cn(
"w-[200px] justify-start text-left font-normal border-gray-200",
!field.value && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value ? (
format(field.value, "dd-MM-yyyy")
) : (
<span>Masukkan Bulan</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="time"
render={({ field }) => (
<FormItem>
<Input
type="time"
className="max-w-[100px]"
value={field.value}
placeholder="Masukkan Judul Perencanaan"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row gap-2 mt-4 pt-4">
<Button
size="md"
type="button"
variant="outline"
color="destructive"
className="text-xs"
>
Cancel
</Button>
<Button size="md" type="submit" color="primary" className="text-xs">
Submit
</Button>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import CampaignListTable from "./component/table";
export default function AdminCampaignList() {
return (
<div>
<SiteBreadcrumb />
<CampaignListTable />
</div>
);
}

View File

@ -0,0 +1,11 @@
import DetailContentBlast from "@/components/form/broadcast/content-blast--detail-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
export default function DetailEmailBlast() {
return (
<div>
<SiteBreadcrumb />
<DetailContentBlast />
</div>
);
}

View File

@ -0,0 +1,11 @@
import ContentBlast from "@/components/form/broadcast/content-blast-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
export default function CreateEmailBlast() {
return (
<div>
<SiteBreadcrumb />
<ContentBlast />
</div>
);
}

View File

@ -0,0 +1,110 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Judul",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "fileTypeName",
header: "Konten",
cell: ({ row }) => <span>{row.getValue("fileTypeName")}</span>,
},
{
accessorKey: "categoryName",
header: "Kategori",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
{
accessorKey: "createdAt",
header: "Tanggal Unggah",
cell: ({ row }) => (
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
),
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => <span>{row.getValue("statusName")}</span>,
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/contributor/content/image/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Detail
</DropdownMenuItem>
</Link>
<Link href={`/admin/broadcast/create/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Email & Whatsapp Blast
</DropdownMenuItem>
</Link>
{/* <Link href={`/admin/broadcast/whatsapp/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Whatsapp Blast
</DropdownMenuItem>
</Link> */}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,403 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
listDataMedia,
listDataMediaBroadCast,
} from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing";
const BroadcastEmailTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = React.useState("");
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
fetchData();
}
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
setPagination({
pageIndex: 0,
pageSize: Number(showData),
});
}, [page, showData]);
async function fetchData() {
try {
loading();
const res = await listDataMediaBroadCast(
page - 1,
showData,
search,
categoryFilter?.sort().join(","),
statusFilter?.sort().join(",")
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", data);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
close();
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
const handleChange = (type: string, id: number, checked: boolean) => {
if (type === "category") {
if (checked) {
const temp: number[] = [...categoryFilter];
temp.push(id);
setCategoryFilter(temp);
} else {
const temp = categoryFilter.filter((a) => a !== id);
setCategoryFilter(temp);
}
} else {
if (checked) {
const temp: number[] = [...statusFilter];
temp.push(id);
setStatusFilter(temp);
} else {
const temp = statusFilter.filter((a) => a !== id);
setStatusFilter(temp);
}
}
};
return (
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
<div className="flex justify-between ">
<Link href="/admin/broadcast/campaign-list" className="mr-3">
<Button color="primary" size="md" className="text-sm">
<UserIcon />
Daftar Campaign
</Button>
</Link>
<Input
type="text"
placeholder="Search"
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onChange={(e) => setSearch(e.target.value)}
className="max-w-[300px]"
/>
<div className="flex flex-row gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Popover>
<PopoverTrigger asChild>
<Button size="md" variant="outline">
Filter
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 ">
<div className="flex flex-col gap-2 px-2">
<div className="flex justify-between text-sm">
<p>Filter</p>
<a
onClick={() => fetchData()}
className="cursor-pointer text-primary"
>
Simpan
</a>
</div>
<div className="flex flex-col gap-1 overflow-auto max-h-[300px] text-xs custom-scrollbar-table">
<p>Kategory</p>
{categories?.map((category: any) => (
<div className="flex items-center space-x-2">
<Checkbox
id={category.id}
checked={categoryFilter.includes(category.id)}
onCheckedChange={(e) =>
handleChange("category", category.id, Boolean(e))
}
/>
<label
htmlFor={category.id}
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{category.name}
</label>
</div>
))}
<p className="mt-3">Status</p>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(1)}
onCheckedChange={(e) =>
handleChange("status", 1, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Menunggu Review
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(2)}
onCheckedChange={(e) =>
handleChange("status", 2, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Diterima
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(3)}
onCheckedChange={(e) =>
handleChange("status", 3, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Minta Update{" "}
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(4)}
onCheckedChange={(e) =>
handleChange("status", 4, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Ditolak
</label>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default BroadcastEmailTable;

View File

@ -0,0 +1,25 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import BroadcastTable from "./create/component/table";
import { PlusIcon } from "lucide-react";
import EscalationTable from "../../shared/communication/escalation/components/escalation-table";
import InternalTable from "../../shared/communication/internal/components/internal-table";
import { useState } from "react";
import { Link } from "@/i18n/routing";
import { Button } from "@/components/ui/button";
import BroadcastEmailTable from "./create/component/table";
import BroadcastWhatsAppTable from "./whatsapp/component/table";
export default function AdminBroadcast() {
const [tab, setTab] = useState("Email Blast");
return (
<div>
<SiteBreadcrumb />
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
{tab === "Email Blast" && <BroadcastEmailTable />}
{tab === "WhatsApp Blast" && <BroadcastWhatsAppTable />}
</div>
</div>
);
}

View File

@ -0,0 +1,11 @@
import ContentBlast from "@/components/form/broadcast/content-blast-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
export default function CreateWABlast() {
return (
<div>
<SiteBreadcrumb />
{/* <ContentBlast /> */}
</div>
);
}

View File

@ -0,0 +1,105 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Judul",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "categoryName",
header: "Kategori",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
{
accessorKey: "createdAt",
header: "Tanggal Unggah",
cell: ({ row }) => (
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
),
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => <span>{row.getValue("statusName")}</span>,
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/contributor/content/image/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Detail
</DropdownMenuItem>
</Link>
<Link href={`/admin/broadcast/email/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Email Blast
</DropdownMenuItem>
</Link>
<Link href={`/admin/broadcast/whatsapp/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none cursor-pointer">
Whatsapp Blast
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,403 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
listDataMedia,
listDataMediaBroadCast,
} from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing";
const BroadcastWhatsAppTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = React.useState("");
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
fetchData();
}
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
setPagination({
pageIndex: 0,
pageSize: Number(showData),
});
}, [page, showData]);
async function fetchData() {
try {
loading();
const res = await listDataMediaBroadCast(
page - 1,
showData,
search,
categoryFilter?.sort().join(","),
statusFilter?.sort().join(",")
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", data);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
close();
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
const handleChange = (type: string, id: number, checked: boolean) => {
if (type === "category") {
if (checked) {
const temp: number[] = [...categoryFilter];
temp.push(id);
setCategoryFilter(temp);
} else {
const temp = categoryFilter.filter((a) => a !== id);
setCategoryFilter(temp);
}
} else {
if (checked) {
const temp: number[] = [...statusFilter];
temp.push(id);
setStatusFilter(temp);
} else {
const temp = statusFilter.filter((a) => a !== id);
setStatusFilter(temp);
}
}
};
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
<div className="flex justify-between ">
<Link href="/admin/broadcast/campaign-list" className="mr-3">
<Button color="primary" size="md" className="text-sm">
<UserIcon />
Daftar Campaign
</Button>
</Link>
<Input
type="text"
placeholder="Search"
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onChange={(e) => setSearch(e.target.value)}
className="max-w-[300px]"
/>
<div className="flex flex-row gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Popover>
<PopoverTrigger asChild>
<Button size="md" variant="outline">
Filter
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 ">
<div className="flex flex-col gap-2 px-2">
<div className="flex justify-between text-sm">
<p>Filter</p>
<a
onClick={() => fetchData()}
className="cursor-pointer text-primary"
>
Simpan
</a>
</div>
<div className="flex flex-col gap-1 overflow-auto max-h-[300px] text-xs custom-scrollbar-table">
<p>Kategory</p>
{categories?.map((category: any) => (
<div className="flex items-center space-x-2">
<Checkbox
id={category.id}
checked={categoryFilter.includes(category.id)}
onCheckedChange={(e) =>
handleChange("category", category.id, Boolean(e))
}
/>
<label
htmlFor={category.id}
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{category.name}
</label>
</div>
))}
<p className="mt-3">Status</p>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(1)}
onCheckedChange={(e) =>
handleChange("status", 1, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Menunggu Review
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(2)}
onCheckedChange={(e) =>
handleChange("status", 2, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Diterima
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(3)}
onCheckedChange={(e) =>
handleChange("status", 3, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Minta Update{" "}
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(4)}
onCheckedChange={(e) =>
handleChange("status", 4, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Ditolak
</label>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default BroadcastWhatsAppTable;

View File

@ -0,0 +1,288 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
getUserById,
saveUserInternal,
} from "@/service/management-user/management-user";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Link, useRouter } from "@/i18n/routing";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
import { useParams } from "next/navigation";
import { identity } from "@fullcalendar/core/internal";
const FormSchema = z.object({
fullname: z.string({
required_error: "Required",
}),
username: z.string({
required_error: "Required",
}),
identity: z.string({
required_error: "Required",
}),
address: z.string({
required_error: "Required",
}),
email: z.string({
required_error: "Required",
}),
phoneNumber: z.string({
required_error: "Required",
}),
password: z.string({
required_error: "Required",
}),
confirmPassword: z.string({
required_error: "Required",
}),
isValidPassword: z.boolean().refine((val) => val === true, {
message: "Check Password",
}),
});
export default function EditUserForm() {
const router = useRouter();
const params = useParams();
const id = params?.id;
const MySwal = withReactContent(Swal);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
password: "",
confirmPassword: "",
},
});
useEffect(() => {
initData();
}, []);
const initData = async () => {
loading();
const response = await getUserById(String(id));
const res = response?.data?.data;
close();
console.log("res", res);
form.setValue("fullname", res?.fullname);
form.setValue("username", res?.username);
form.setValue("phoneNumber", res?.phoneNumber);
form.setValue("address", res?.address);
form.setValue("email", res?.email);
};
async function save(data: z.infer<typeof FormSchema>) {
let req: any = {
id: Number(id),
firstName: data.fullname,
username: data.username,
address: data.address,
email: data.email,
identityNumber: data.identity,
password: data.password,
passwordConf: data.confirmPassword,
isDefault: false,
};
loading();
const response = await saveUserInternal(req);
if (response?.error) {
error(response.message);
return false;
}
close();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "Oke",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/management-user");
}
});
return false;
}
async function onSubmit(data: z.infer<typeof FormSchema>) {
MySwal.fire({
title: "Simpan Data?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-white p-10 w-full"
>
<p className="text-xl">Data Pengelola Media Hub</p>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap</FormLabel>
<FormControl>
<Input
placeholder="Masukkan nama lengkap"
{...field}
className="w-1/2"
readOnly
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
placeholder="Masukkan username"
{...field}
className="w-1/2"
readOnly
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="identity"
render={({ field }) => (
<FormItem>
<FormLabel>Nomor Identitas</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan nomor identitas"
{...field}
className="w-1/2"
readOnly
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Alamat</FormLabel>
<FormControl>
<Textarea
placeholder="Masukkan alamat"
className="resize-none w-1/2"
{...field}
readOnly
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Masukkan email"
{...field}
className="w-1/2"
readOnly
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>No. Handphone</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan nomor handphone"
{...field}
className="w-1/2 mb-5"
readOnly
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Link href="/admin/management-user">
<Button type="button" color="primary" variant="outline">
Back
</Button>
</Link>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,382 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { zodResolver } from "@hookform/resolvers/zod";
import { Check, ChevronsUpDown } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
AdministrationLevelList,
getListCompetencies,
getListEducation,
getListSchools,
getUserById,
saveUserInternal,
} from "@/service/management-user/management-user";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Link, useRouter } from "@/i18n/routing";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import dynamic from "next/dynamic";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
import { useParams } from "next/navigation";
import { identity } from "@fullcalendar/core/internal";
const PasswordChecklist = dynamic(() => import("react-password-checklist"), {
ssr: false,
});
interface RoleData {
id: number;
label: string;
name: string;
value: string;
levelNumber: number;
}
const FormSchema = z.object({
fullname: z.string({
required_error: "Required",
}),
username: z.string({
required_error: "Required",
}),
identity: z.string({
required_error: "Required",
}),
address: z.string({
required_error: "Required",
}),
email: z.string({
required_error: "Required",
}),
phoneNumber: z.string({
required_error: "Required",
}),
password: z.string({
required_error: "Required",
}),
confirmPassword: z.string({
required_error: "Required",
}),
isValidPassword: z.boolean().refine((val) => val === true, {
message: "Check Password",
}),
});
export default function EditUserForm() {
const router = useRouter();
const params = useParams();
const id = params?.id;
const MySwal = withReactContent(Swal);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
password: "",
confirmPassword: "",
},
});
const passwordVal = form.watch("password");
const confPasswordVal = form.watch("confirmPassword");
useEffect(() => {
initData();
}, []);
const initData = async () => {
loading();
const response = await getUserById(String(id));
const res = response?.data?.data;
close();
console.log("res", res);
form.setValue("fullname", res?.fullname);
form.setValue("username", res?.username);
form.setValue("phoneNumber", res?.phoneNumber);
form.setValue("address", res?.address);
form.setValue("email", res?.email);
};
async function save(data: z.infer<typeof FormSchema>) {
let req: any = {
id: Number(id),
firstName: data.fullname,
username: data.username,
address: data.address,
email: data.email,
identityNumber: data.identity,
password: data.password,
passwordConf: data.confirmPassword,
isDefault: false,
isAdmin: true,
};
loading();
const response = await saveUserInternal(req);
if (response?.error) {
error(response.message);
return false;
}
close();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "Oke",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/management-user");
}
});
return false;
}
async function onSubmit(data: z.infer<typeof FormSchema>) {
MySwal.fire({
title: "Simpan Data?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-white p-10 w-full"
>
<p className="text-xl">Data Pengelola Media Hub</p>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap</FormLabel>
<FormControl>
<Input
placeholder="Masukkan nama lengkap"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
placeholder="Masukkan username"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="identity"
render={({ field }) => (
<FormItem>
<FormLabel>Nomor Identitas</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan nomor identitas"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Alamat</FormLabel>
<FormControl>
<Textarea
placeholder="Masukkan alamat"
className="resize-none w-1/2"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Masukkan email"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>No. Handphone</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan nomor handphone"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Masukkan kata sandi"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Konfirmasi Kata Sandi</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Masukkan kata sandi"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<PasswordChecklist
rules={["minLength", "specialChar", "number", "capital", "match"]}
minLength={8}
value={passwordVal || ""}
valueAgain={confPasswordVal || ""}
onChange={(isValid) => {
form.setValue("isValidPassword", isValid);
}}
className="text-sm"
messages={{
minLength: "Password harus lebih dari 8 karakter",
specialChar: "Password harus memiliki spesial karakter",
number: "Password harus memiliki angka",
capital: "Password harus memiliki huruf kapital",
match: "Password sama",
}}
/>
<Link href="/admin/management-user">
<Button type="button" color="primary" variant="outline">
Back
</Button>
</Link>
<Button type="submit" color="primary" className="mx-3">
Submit
</Button>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,789 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { zodResolver } from "@hookform/resolvers/zod";
import { Check, ChevronsUpDown, Eye, EyeOff } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
AdministrationLevelList,
getListCompetencies,
getListEducation,
getListSchools,
saveUserInternal,
} from "@/service/management-user/management-user";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Link, useRouter } from "@/i18n/routing";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import dynamic from "next/dynamic";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
const PasswordChecklist = dynamic(() => import("react-password-checklist"), {
ssr: false,
});
const sns = [
{
key: 1,
id: "comment",
typeId: 1,
name: "Komentar Konten",
},
{
key: 2,
id: "fb",
typeId: 2,
name: "Facebook",
},
{
key: 3,
id: "ig",
typeId: 3,
name: "Instagram",
},
{
key: 4,
id: "twt",
typeId: 4,
name: "Twitter",
},
{
key: 5,
id: "yt",
typeId: 5,
name: "Youtube",
},
{
key: 6,
id: "emergency",
typeId: 6,
name: "Emergency Issue",
},
{
key: 7,
id: "email",
typeId: 7,
name: "Email",
},
{
key: 8,
id: "inbox",
typeId: 8,
name: "Pesan Masuk",
},
{
key: 9,
id: "whatsapp",
typeId: 9,
name: "Whatssapp",
},
{
key: 10,
id: "tiktok",
typeId: 10,
name: "Tiktok",
},
];
interface RoleData {
id: number;
label: string;
name: string;
value: string;
levelNumber: number;
}
const FormSchema = z.object({
level: z.string({
required_error: "Required",
}),
fullname: z.string({
required_error: "Required",
}),
username: z.string({
required_error: "Required",
}),
role: z.string({
required_error: "Required",
}),
nrp: z.string({
required_error: "Required",
}),
address: z.string({
required_error: "Required",
}),
email: z.string({
required_error: "Required",
}),
phoneNumber: z.string({
required_error: "Required",
}),
password: z.string({
required_error: "Required",
}),
confirmPassword: z.string({
required_error: "Required",
}),
isValidPassword: z.boolean().refine((val) => val === true, {
message: "Check Password",
}),
sns: z.array(z.string()).optional(),
education: z.string().optional(),
school: z.string().optional(),
competency: z.string().optional(),
});
export default function CreateUserForm() {
const router = useRouter();
const MySwal = withReactContent(Swal);
const levelName = getCookiesDecrypt("ulnae");
const [roleList, setRoleList] = useState<RoleData[]>([]);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [userEducations, setUserEducations] = useState<any>();
const [userSchools, setUserSchools] = useState<any>();
const [userCompetencies, setUserCompetencies] = useState<any>();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
password: "",
confirmPassword: "",
sns: [],
education: "1",
school: "4",
competency: "2",
},
});
const passwordVal = form.watch("password");
const confPasswordVal = form.watch("confirmPassword");
const selectedRole = form.watch("role");
useEffect(() => {
initFetch();
getDataAdditional();
}, []);
const initFetch = async () => {
const response = await AdministrationLevelList();
const res = response?.data?.data;
var levelsArr: RoleData[] = [];
res.forEach((levels: RoleData) => {
levelsArr.push({
id: levels.id,
label: levels.name,
name: levels.name,
value: String(levels.id),
levelNumber: levels.levelNumber,
});
});
setRoleList(levelsArr);
};
async function getDataAdditional() {
const resEducations = await getListEducation();
setUserEducations(resEducations?.data?.data);
const resSchools = await getListSchools();
setUserSchools(resSchools?.data?.data);
const resCompetencies = await getListCompetencies();
setUserCompetencies(resCompetencies?.data?.data);
}
const roles =
levelName == "MABES POLRI"
? [
{
id: "ADM-ID",
name: "Admin",
},
{
id: "EXE-ID",
name: "Eksekutif",
},
{
id: "APP-ID",
name: "Approver",
},
{
id: "CON-ID",
name: "Kontributor",
},
{
id: "SPV-ID",
name: "Supervisor Feedback Center",
},
{
id: "OPT-ID",
name: "Operator Feedback Center",
},
{
id: "KKUR-ID",
name: "Koor Kurator",
},
{
id: "KUR-ID",
name: "Kurator",
},
]
: [
{
id: "APP-ID",
name: "Approver",
},
{
id: "CON-ID",
name: "Kontributor",
},
];
async function save(data: z.infer<typeof FormSchema>) {
let req: any = {
firstName: data.fullname,
username: data.username,
roleId: data.role,
userLevelId: Number(data.level),
memberIdentity: data.nrp,
address: data.address,
email: data.email,
phoneNumber: data.phoneNumber,
password: data.password,
passwordConf: data.confirmPassword,
isDefault: false,
isAdmin: true,
};
if (data.role == "OPT-ID") {
// req.handledSocialMedia = data?.sns ? data.sns.join(",") : "";
if (data.role == "OPT-ID") {
let snsValue = data?.sns ? data.sns.join(",") : "";
// ✅ Jika hanya 1 value → tambahkan koma agar backend tidak error
if (data?.sns && data.sns.length === 1) {
snsValue = snsValue + ",";
}
req.handledSocialMedia = snsValue;
}
}
if (data.role == "KUR-ID") {
req.userEducationId = Number(data.education);
req.userSchoolsId = Number(data.school);
req.userCompetencyId = Number(data.competency);
}
loading();
const response = await saveUserInternal(req);
if (response?.error) {
error(response.message);
return false;
}
close();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "Oke",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/management-user");
}
});
return false;
}
async function onSubmit(data: z.infer<typeof FormSchema>) {
MySwal.fire({
title: "Simpan Data?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-white dark:bg-black p-10 w-full"
>
<p className="text-xl">Data Pengelola Media Hub</p>
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Pilih Level</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[400px] justify-between",
!field.value && "text-muted-foreground"
)}
>
{field.value
? roleList.find((role) => role.value === field.value)
?.label
: "Pilih level"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandInput />
<CommandList>
<CommandEmpty>No role found.</CommandEmpty>
<CommandGroup>
{roleList.map((role) => (
<CommandItem
value={role.label}
key={role.value}
onSelect={() => {
form.setValue("level", role.value);
}}
>
{role.label}
<Check
className={cn(
"ml-auto",
role.value === field.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap</FormLabel>
<FormControl>
<Input
placeholder="Masukkan nama lengkap"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
placeholder="Masukkan username"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Role</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-wrap gap-3 w-1/2"
>
{roles.map((role) => (
<FormItem
key={role.id}
className="flex items-center space-x-3 space-y-0"
>
<FormControl>
<RadioGroupItem value={role.id} />
</FormControl>
<FormLabel className="font-normal">
{role.name}
</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{selectedRole === "OPT-ID" && (
<FormField
control={form.control}
name="sns"
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel>Social Media Yang Ditangani</FormLabel>
</div>
<div className="grid grid-cols-5 gap-2 w-1/2">
{sns.map((item) => (
<FormField
key={item.id}
control={form.control}
name="sns"
render={({ field }) => {
return (
<FormItem
key={item.typeId}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(
String(item.typeId)
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...(field.value || []),
String(item.typeId),
])
: field.onChange(
(field.value || []).filter(
(value) =>
value !== String(item.typeId)
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}
</FormLabel>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
)}
{selectedRole === "KUR-ID" && (
<>
<FormField
control={form.control}
name="education"
render={({ field }) => (
<FormItem>
<FormLabel>Pendidikan Terakhir</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userEducations?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="school"
render={({ field }) => (
<FormItem>
<FormLabel>Universitas / Perguruan Tinggi</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userSchools?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="competency"
render={({ field }) => (
<FormItem>
<FormLabel>Kompetensi</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userCompetencies?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={form.control}
name="nrp"
render={({ field }) => (
<FormItem>
<FormLabel>Nomor Regitrasi Polri {`(NRP)`}</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan NRP"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Alamat</FormLabel>
<FormControl>
<Textarea
placeholder="Masukkan alamat"
className="resize-none w-1/2"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Masukkan email"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>No. Handphone</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan nomor handphone"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<div className="relative w-1/2">
<Input
type={showPassword ? "text" : "password"}
placeholder="Masukkan kata sandi"
{...field}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Konfirmasi Kata Sandi</FormLabel>
<FormControl>
<div className="relative w-1/2">
<Input
type={showConfirmPassword ? "text" : "password"}
placeholder="Masukkan kata sandi"
{...field}
/>
<button
type="button"
onClick={() =>
setShowConfirmPassword(!showConfirmPassword)
}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500"
>
{showConfirmPassword ? (
<EyeOff size={18} />
) : (
<Eye size={18} />
)}
</button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<PasswordChecklist
rules={["minLength", "specialChar", "number", "capital", "match"]}
minLength={8}
value={passwordVal || ""}
valueAgain={confPasswordVal || ""}
onChange={(isValid) => {
form.setValue("isValidPassword", isValid);
}}
className="text-sm"
messages={{
minLength: "Password harus lebih dari 8 karakter",
specialChar: "Password harus memiliki spesial karakter",
number: "Password harus memiliki angka",
capital: "Password harus memiliki huruf kapital",
match: "Password sama",
}}
/>
<Link href="/admin/management-user">
<Button type="button" color="primary" variant="outline">
Back
</Button>
</Link>
<Button type="submit" color="primary" className="mx-3">
Submit
</Button>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,741 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { zodResolver } from "@hookform/resolvers/zod";
import { Check, ChevronsUpDown } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
AdministrationLevelList,
getListCompetencies,
getListEducation,
getListSchools,
getUserById,
saveUserInternal,
} from "@/service/management-user/management-user";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Link, useRouter } from "@/i18n/routing";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import dynamic from "next/dynamic";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
import { useParams } from "next/navigation";
const sns = [
{
key: 1,
id: "comment",
typeId: 1,
name: "Komentar Konten",
},
{
key: 2,
id: "fb",
typeId: 2,
name: "Facebook",
},
{
key: 3,
id: "ig",
typeId: 3,
name: "Instagram",
},
{
key: 4,
id: "twt",
typeId: 4,
name: "Twitter",
},
{
key: 5,
id: "yt",
typeId: 5,
name: "Youtube",
},
{
key: 6,
id: "emergency",
typeId: 6,
name: "Emergency Issue",
},
{
key: 7,
id: "email",
typeId: 7,
name: "Email",
},
{
key: 8,
id: "inbox",
typeId: 8,
name: "Pesan Masuk",
},
{
key: 9,
id: "whatsapp",
typeId: 9,
name: "Whatssapp",
},
{
key: 10,
id: "tiktok",
typeId: 10,
name: "Tiktok",
},
];
interface RoleData {
id: number;
label: string;
name: string;
value: string;
levelNumber: number;
}
const FormSchema = z.object({
level: z.string({
required_error: "Required",
}),
fullname: z.string({
required_error: "Required",
}),
username: z.string({
required_error: "Required",
}),
role: z.string({
required_error: "Required",
}),
nrp: z.string({
required_error: "Required",
}),
address: z.string({
required_error: "Required",
}),
email: z.string({
required_error: "Required",
}),
phoneNumber: z.string({
required_error: "Required",
}),
password: z.string({
required_error: "Required",
}),
confirmPassword: z.string({
required_error: "Required",
}),
isValidPassword: z.boolean().refine((val) => val === true, {
message: "Check Password",
}),
sns: z.array(z.string()).optional(),
education: z.string().optional(),
school: z.string().optional(),
competency: z.string().optional(),
});
export default function DetailUserForm() {
const router = useRouter();
const params = useParams();
const id = params?.id;
const MySwal = withReactContent(Swal);
const levelName = getCookiesDecrypt("ulnae");
const [roleList, setRoleList] = useState<RoleData[]>([]);
const [userEducations, setUserEducations] = useState<any>();
const [userSchools, setUserSchools] = useState<any>();
const [userCompetencies, setUserCompetencies] = useState<any>();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
password: "",
confirmPassword: "",
sns: [],
education: "1",
school: "4",
competency: "2",
},
});
const selectedRole = form.watch("role");
useEffect(() => {
getDataAdditional();
initData();
}, []);
const initData = async () => {
loading();
const response = await getUserById(String(id));
const res = response?.data?.data;
close();
console.log("res", res);
if (Number(res.roleId) > 4) {
form.setValue("fullname", res?.fullname);
form.setValue("username", res?.username);
form.setValue("phoneNumber", res?.phoneNumber);
form.setValue("nrp", res?.memberIdentity);
form.setValue("address", res?.address);
form.setValue("email", res?.email);
form.setValue("role", res?.role?.code);
form.setValue("level", String(res?.userLevelId));
} else {
initFetch();
console.log("sadad", res?.role?.code);
form.setValue("fullname", res?.fullname);
form.setValue("username", res?.username);
form.setValue("phoneNumber", res?.phoneNumber);
form.setValue("nrp", res?.memberIdentity);
form.setValue("address", res?.address);
form.setValue("email", res?.email);
form.setValue("role", res?.role?.code);
form.setValue("level", String(res?.userLevelId));
}
};
const initFetch = async () => {
const response = await AdministrationLevelList();
const res = response?.data?.data;
var levelsArr: RoleData[] = [];
res.forEach((levels: RoleData) => {
levelsArr.push({
id: levels.id,
label: levels.name,
name: levels.name,
value: String(levels.id),
levelNumber: levels.levelNumber,
});
});
setRoleList(levelsArr);
};
async function getDataAdditional() {
const resEducations = await getListEducation();
setUserEducations(resEducations?.data?.data);
const resSchools = await getListSchools();
setUserSchools(resSchools?.data?.data);
const resCompetencies = await getListCompetencies();
setUserCompetencies(resCompetencies?.data?.data);
}
const roles =
levelName == "MABES POLRI"
? [
{
id: "ADM-ID",
name: "Admin",
},
{
id: "APP-ID",
name: "Approver",
},
{
id: "CON-ID",
name: "Kontributor",
},
{
id: "SPV-ID",
name: "Supervisor Feedback Center",
},
{
id: "OPT-ID",
name: "Operator Feedback Center",
},
{
id: "KKUR-ID",
name: "Koor Kurator",
},
{
id: "KUR-ID",
name: "Kurator",
},
]
: [
{
id: "APP-ID",
name: "Approver",
},
{
id: "CON-ID",
name: "Kontributor",
},
];
async function save(data: z.infer<typeof FormSchema>) {
let req: any = {
id: id,
firstName: data.fullname,
username: data.username,
roleId: data.role,
userLevelId: Number(data.level),
memberIdentity: data.nrp,
address: data.address,
email: data.email,
password: data.password,
passwordConf: data.confirmPassword,
isDefault: false,
};
if (data.role == "OPT-ID") {
req.handledSocialMedia = data?.sns ? data.sns.join(",") : "";
}
if (data.role == "KUR-ID") {
req.userEducationId = Number(data.education);
req.userSchoolsId = Number(data.school);
req.userCompetencyId = Number(data.competency);
}
loading();
const response = await saveUserInternal(req);
if (response?.error) {
error(response.message);
return false;
}
close();
MySwal.fire({
title: "Sukses",
icon: "success",
showCancelButton: true,
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/management-user");
}
});
return false;
}
async function onSubmit(data: z.infer<typeof FormSchema>) {
MySwal.fire({
title: "Simpan Data?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-white p-10 w-full"
>
<p className="text-xl">Data Pengelola Media Hub</p>
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Pilih Level</FormLabel>
<Popover>
<PopoverTrigger asChild disabled>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[400px] justify-between",
!field.value && "text-muted-foreground"
)}
>
{field.value
? roleList.find((role) => role.value === field.value)
?.label
: "Pilih level"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandInput />
<CommandList>
<CommandEmpty>No role found.</CommandEmpty>
<CommandGroup>
{roleList.map((role) => (
<CommandItem
value={role.label}
key={role.value}
onSelect={() => {
form.setValue("level", role.value);
}}
>
{role.label}
<Check
className={cn(
"ml-auto",
role.value === field.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap</FormLabel>
<FormControl>
<Input
placeholder="Masukkan nama lengkap"
{...field}
readOnly
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
placeholder="Masukkan username"
{...field}
readOnly
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Role</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-wrap gap-3 w-1/2"
disabled
>
{roles.map((role) => (
<FormItem
key={role.id}
className="flex items-center space-x-3 space-y-0"
>
<FormControl>
<RadioGroupItem value={role.id} />
</FormControl>
<FormLabel className="font-normal">
{role.name}
</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{selectedRole === "OPT-ID" && (
<FormField
control={form.control}
name="sns"
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel>Social Media Yang Ditangani</FormLabel>
</div>
<div className="grid grid-cols-5 gap-2 w-1/2">
{sns.map((item) => (
<FormField
key={item.id}
control={form.control}
name="sns"
render={({ field }) => {
return (
<FormItem
key={item.typeId}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
disabled
checked={field.value?.includes(
String(item.typeId)
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...(field.value || []),
String(item.typeId),
])
: field.onChange(
(field.value || []).filter(
(value) =>
value !== String(item.typeId)
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}
</FormLabel>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
)}
{selectedRole === "KUR-ID" && (
<>
<FormField
control={form.control}
name="education"
render={({ field }) => (
<FormItem>
<FormLabel>Pendidikan Terakhir</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
disabled
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userEducations?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="school"
render={({ field }) => (
<FormItem>
<FormLabel>Universitas / Perguruan Tinggi</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
disabled
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userSchools?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="competency"
render={({ field }) => (
<FormItem>
<FormLabel>Kompetensi</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
disabled
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userCompetencies?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={form.control}
name="nrp"
render={({ field }) => (
<FormItem>
<FormLabel>Nomor Regitrasi Polri {`(NRP)`}</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan NRP"
readOnly
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Alamat</FormLabel>
<FormControl>
<Textarea
placeholder="Masukkan alamat"
readOnly
className="resize-none w-1/2"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
readOnly
placeholder="Masukkan email"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>No. Handphone</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan nomor handphone"
{...field}
readOnly
className="w-1/2 mb-2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Link href="/admin/management-user">
<Button type="button" color="primary" variant="outline">
Back
</Button>
</Link>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,784 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { zodResolver } from "@hookform/resolvers/zod";
import { Check, ChevronsUpDown } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
AdministrationLevelList,
getListCompetencies,
getListEducation,
getListSchools,
getUserById,
saveUserInternal,
} from "@/service/management-user/management-user";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Link, useRouter } from "@/i18n/routing";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import dynamic from "next/dynamic";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
import { useParams } from "next/navigation";
const PasswordChecklist = dynamic(() => import("react-password-checklist"), {
ssr: false,
});
const sns = [
{
key: 1,
id: "comment",
typeId: 1,
name: "Komentar Konten",
},
{
key: 2,
id: "fb",
typeId: 2,
name: "Facebook",
},
{
key: 3,
id: "ig",
typeId: 3,
name: "Instagram",
},
{
key: 4,
id: "twt",
typeId: 4,
name: "Twitter",
},
{
key: 5,
id: "yt",
typeId: 5,
name: "Youtube",
},
{
key: 6,
id: "emergency",
typeId: 6,
name: "Emergency Issue",
},
{
key: 7,
id: "email",
typeId: 7,
name: "Email",
},
{
key: 8,
id: "inbox",
typeId: 8,
name: "Pesan Masuk",
},
{
key: 9,
id: "whatsapp",
typeId: 9,
name: "Whatssapp",
},
{
key: 10,
id: "tiktok",
typeId: 10,
name: "Tiktok",
},
];
interface RoleData {
id: number;
label: string;
name: string;
value: string;
levelNumber: number;
}
const FormSchema = z.object({
level: z.string({
required_error: "Required",
}),
fullname: z.string({
required_error: "Required",
}),
username: z.string({
required_error: "Required",
}),
role: z.string({
required_error: "Required",
}),
nrp: z.string({
required_error: "Required",
}),
address: z.string({
required_error: "Required",
}),
email: z.string({
required_error: "Required",
}),
phoneNumber: z.string({
required_error: "Required",
}),
password: z.string({
required_error: "Required",
}),
confirmPassword: z.string({
required_error: "Required",
}),
isValidPassword: z.boolean().refine((val) => val === true, {
message: "Check Password",
}),
sns: z.array(z.string()).optional(),
education: z.string().optional(),
school: z.string().optional(),
competency: z.string().optional(),
});
export default function EditUserForm() {
const router = useRouter();
const params = useParams();
const id = params?.id;
const MySwal = withReactContent(Swal);
const levelName = getCookiesDecrypt("ulnae");
const [roleList, setRoleList] = useState<RoleData[]>([]);
const [userEducations, setUserEducations] = useState<any>();
const [userSchools, setUserSchools] = useState<any>();
const [userCompetencies, setUserCompetencies] = useState<any>();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
password: "",
confirmPassword: "",
sns: [],
education: "1",
school: "4",
competency: "2",
},
});
const passwordVal = form.watch("password");
const confPasswordVal = form.watch("confirmPassword");
const selectedRole = form.watch("role");
useEffect(() => {
getDataAdditional();
initData();
}, []);
const initData = async () => {
loading();
const response = await getUserById(String(id));
const res = response?.data?.data;
close();
console.log("res", res);
if (Number(res.roleId) > 4) {
form.setValue("fullname", res?.fullname);
form.setValue("username", res?.username);
form.setValue("phoneNumber", res?.phoneNumber);
form.setValue("nrp", res?.memberIdentity);
form.setValue("address", res?.address);
form.setValue("email", res?.email);
form.setValue("role", res?.role?.code);
form.setValue("level", String(res?.userLevelId));
} else {
initFetch();
form.setValue("fullname", res?.fullname);
form.setValue("username", res?.username);
form.setValue("phoneNumber", res?.phoneNumber);
form.setValue("nrp", res?.memberIdentity);
form.setValue("address", res?.address);
form.setValue("email", res?.email);
form.setValue("role", res?.role?.code);
form.setValue("level", String(res?.userLevelId));
}
};
const initFetch = async () => {
const response = await AdministrationLevelList();
const res = response?.data?.data;
var levelsArr: RoleData[] = [];
res.forEach((levels: RoleData) => {
levelsArr.push({
id: levels.id,
label: levels.name,
name: levels.name,
value: String(levels.id),
levelNumber: levels.levelNumber,
});
});
setRoleList(levelsArr);
};
async function getDataAdditional() {
const resEducations = await getListEducation();
setUserEducations(resEducations?.data?.data);
const resSchools = await getListSchools();
setUserSchools(resSchools?.data?.data);
const resCompetencies = await getListCompetencies();
setUserCompetencies(resCompetencies?.data?.data);
}
const roles =
levelName == "MABES POLRI"
? [
{
id: "ADM-ID",
name: "Admin",
},
{
id: "APP-ID",
name: "Approver",
},
{
id: "CON-ID",
name: "Kontributor",
},
{
id: "SPV-ID",
name: "Supervisor Feedback Center",
},
{
id: "OPT-ID",
name: "Operator Feedback Center",
},
{
id: "KKUR-ID",
name: "Koor Kurator",
},
{
id: "KUR-ID",
name: "Kurator",
},
]
: [
{
id: "APP-ID",
name: "Approver",
},
{
id: "CON-ID",
name: "Kontributor",
},
];
async function save(data: z.infer<typeof FormSchema>) {
let req: any = {
id: Number(id),
firstName: data.fullname,
username: data.username,
roleId: data.role,
userLevelId: Number(data.level),
memberIdentity: data.nrp,
address: data.address,
email: data.email,
password: data.password,
passwordConf: data.confirmPassword,
isDefault: false,
isAdmin: true,
};
if (data.role == "OPT-ID") {
req.handledSocialMedia = data?.sns ? data.sns.join(",") : "";
}
if (data.role == "KUR-ID") {
req.userEducationId = Number(data.education);
req.userSchoolsId = Number(data.school);
req.userCompetencyId = Number(data.competency);
}
loading();
const response = await saveUserInternal(req);
if (response?.error) {
error(response.message);
return false;
}
close();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "Oke",
}).then((result) => {
if (result.isConfirmed) {
router.push("/admin/management-user");
}
});
return false;
}
async function onSubmit(data: z.infer<typeof FormSchema>) {
MySwal.fire({
title: "Simpan Data?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
}
return (
<div>
<SiteBreadcrumb />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 bg-white p-10 w-full"
>
<p className="text-xl">Data Pengelola Media Hub</p>
<FormField
control={form.control}
name="level"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Pilih Level</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-[400px] justify-between",
!field.value && "text-muted-foreground"
)}
>
{field.value
? roleList.find((role) => role.value === field.value)
?.label
: "Pilih level"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandInput />
<CommandList>
<CommandEmpty>No role found.</CommandEmpty>
<CommandGroup>
{roleList.map((role) => (
<CommandItem
value={role.label}
key={role.value}
onSelect={() => {
form.setValue("level", role.value);
}}
>
{role.label}
<Check
className={cn(
"ml-auto",
role.value === field.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fullname"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap</FormLabel>
<FormControl>
<Input
placeholder="Masukkan nama lengkap"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input
placeholder="Masukkan username"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Role</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-wrap gap-3 w-1/2"
>
{roles.map((role) => (
<FormItem
key={role.id}
className="flex items-center space-x-3 space-y-0"
>
<FormControl>
<RadioGroupItem value={role.id} />
</FormControl>
<FormLabel className="font-normal">
{role.name}
</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{selectedRole === "OPT-ID" && (
<FormField
control={form.control}
name="sns"
render={() => (
<FormItem>
<div className="mb-4">
<FormLabel>Social Media Yang Ditangani</FormLabel>
</div>
<div className="grid grid-cols-5 gap-2 w-1/2">
{sns.map((item) => (
<FormField
key={item.id}
control={form.control}
name="sns"
render={({ field }) => {
return (
<FormItem
key={item.typeId}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(
String(item.typeId)
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...(field.value || []),
String(item.typeId),
])
: field.onChange(
(field.value || []).filter(
(value) =>
value !== String(item.typeId)
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}
</FormLabel>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
)}
{selectedRole === "KUR-ID" && (
<>
<FormField
control={form.control}
name="education"
render={({ field }) => (
<FormItem>
<FormLabel>Pendidikan Terakhir</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userEducations?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="school"
render={({ field }) => (
<FormItem>
<FormLabel>Universitas / Perguruan Tinggi</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userSchools?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="competency"
render={({ field }) => (
<FormItem>
<FormLabel>Kompetensi</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{userCompetencies?.map((edu: any) => (
<SelectItem key={edu.id} value={String(edu.id)}>
{edu.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={form.control}
name="nrp"
render={({ field }) => (
<FormItem>
<FormLabel>Nomor Regitrasi Polri {`(NRP)`}</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan NRP"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem>
<FormLabel>Alamat</FormLabel>
<FormControl>
<Textarea
placeholder="Masukkan alamat"
className="resize-none w-1/2"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="Masukkan email"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel>No. Handphone</FormLabel>
<FormControl>
<Input
type="number"
placeholder="Masukkan nomor handphone"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Masukkan kata sandi"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Konfirmasi Kata Sandi</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Masukkan kata sandi"
{...field}
className="w-1/2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<PasswordChecklist
rules={["minLength", "specialChar", "number", "capital", "match"]}
minLength={8}
value={passwordVal || ""}
valueAgain={confPasswordVal || ""}
onChange={(isValid) => {
form.setValue("isValidPassword", isValid);
}}
className="text-sm"
messages={{
minLength: "Password harus lebih dari 8 karakter",
specialChar: "Password harus memiliki spesial karakter",
number: "Password harus memiliki angka",
capital: "Password harus memiliki huruf kapital",
match: "Password sama",
}}
/>
<Link href="/admin/management-user">
<Button type="button" color="primary" variant="outline">
Back
</Button>
</Link>
<Button type="submit" color="primary" className="mx-3">
Submit
</Button>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,82 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import UserExternalTable from "@/components/table/management-user/management-user-external-table";
import UserInternalTable from "@/components/table/management-user/management-user-internal-table";
import { Button } from "@/components/ui/button";
import ManagementUserVisualization from "@/components/visualization/management-user-viz";
import { Link, useRouter } from "@/i18n/routing";
import { PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import Cookies from "js-cookie";
import { getCookiesDecrypt } from "@/lib/utils";
export default function ManagementUser() {
const [isInternal, setIsInternal] = useState(true);
const [levelNumber, setLevelNumber] = useState<number | null>(null);
const router = useRouter();
useEffect(() => {
const encryptedLevel = Cookies.get("ulne");
if (encryptedLevel) {
const decryptedLevel = getCookiesDecrypt("ulne");
setLevelNumber(Number(decryptedLevel));
}
}, []);
useEffect(() => {
router.push("?page=1");
}, [isInternal]);
const showExternalButton = levelNumber !== 2 && levelNumber !== 3;
return (
<div>
<SiteBreadcrumb />
<section id="viz">
<ManagementUserVisualization />
</section>
<section className="flex flex-col gap-2 bg-white dark:bg-black rounded-lg p-3 mt-5">
<div className="flex justify-between py-3">
<p className="text-lg">
Data User {isInternal ? "Internal" : "Eksternal"}
</p>
{isInternal && (
<Link href="/admin/management-user/internal/create">
<Button color="primary" size="md">
<PlusIcon />
Add User
</Button>
</Link>
)}
</div>
<div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-5">
<Button
rounded="md"
onClick={() => setIsInternal(true)}
className={`hover:text-white ${
!isInternal ? "bg-white text-black" : "bg-black text-white"
}`}
>
User Internal
</Button>
{showExternalButton && (
<Button
rounded="md"
onClick={() => setIsInternal(false)}
className={`hover:text-white ${
!isInternal ? "bg-black text-white" : "bg-white text-black"
}`}
>
User Eksternal
</Button>
)}
</div>
{isInternal ? <UserInternalTable /> : <UserExternalTable />}
</section>
</div>
);
}

View File

@ -0,0 +1,100 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "date",
header: "Tanggal",
cell: ({ row }) => (
<span>
{row.original.startDate.split(" ")[0]} -{" "}
{row.original.endDate.split(" ")[0]}
</span>
),
},
{
accessorKey: "title",
header: "Judul Berita",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "pageUrl",
header: "Link Berita",
cell: ({ row }) => (
<a
className="cursor-pointer normal-case"
href={row.original.mediaUpload.pageUrl}
target="_blank"
>
{row.original.mediaUpload.pageUrl}
</a>
),
},
{
id: "actions",
accessorKey: "action",
header: "Aksi",
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/media-tracking/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -1,37 +1,27 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { toast } from "sonner";
import { close, error, loading, successCallback } from "@/config/swal";
import {
listData5Data,
listDataAllNonPagination,
mediaTrackingSave,
} from "@/lib/services/media-tracking/media-tracking";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { close, error, loading, successCallback } from "@/config/swal";
import { getMediaTracking, listDataAllNonPagination, listDataTracking, mediaTrackingSave } from "@/service/media-tracking/media-tracking";
import { useEffect, useState } from "react";
export default function TrackingMediaModal(props: { triggerFetch: () => void }) {
const [content, setContent] = useState<any>([]);
const [inputValue, setInputValue] = useState("");
const [selectedId, setSelectedId] = useState(0);
const [search, setSearch] = useState<string>("");
const [page, setPage] = useState(1);
const [showData, setShowData] = useState("10");
useEffect(() => {
initFecth();
}, []);
}, [page, showData, search]);
const initFecth = async () => {
const response = await listData5Data();
const response = await getMediaTracking(page - 1, search, showData);
setContent(response?.data?.data.content);
};
@ -97,22 +87,13 @@ export default function TrackingMediaModal(props: { triggerFetch: () => void })
</DialogHeader>
<div className="flex flex-col gap-2">
{" "}
<Input
type="text"
placeholder="Content"
value={inputValue}
onChange={handleInputChange}
/>{" "}
<Input type="text" placeholder="Content" value={inputValue} onChange={handleInputChange} />{" "}
<div className="w-full border rounded-sm text-xs p-2 flex flex-col max-h-[300px] overflow-auto">
{content.length > 0 &&
content.map((item: any) => (
<a
key={item.id}
className={`cursor-pointer px-2 py-4 ${
selectedId === item.id
? "font-bold bg-gray-200 rounded-sm"
: "hover:bg-gray-100 hover:font-semibold"
}`}
className={`cursor-pointer px-2 py-4 ${selectedId === item.id ? "font-bold bg-gray-200 rounded-sm" : "hover:bg-gray-100 hover:font-semibold"}`}
onClick={() => {
setSelectedId(item.id);
setInputValue(item.title);

View File

@ -0,0 +1,371 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { listEnableCategory } from "@/service/content/content";
import { close, loading } from "@/config/swal";
import { Label } from "@/components/ui/label";
import TrackingMediaModal from "./modal";
import { getMediaTracking } from "@/service/media-tracking/media-tracking";
const NewsTable = () => {
const router = useRouter();
const asPath = usePathname();
const searchParams = useSearchParams();
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [showTable, setShowTable] = React.useState(false);
const [onSearch, setOnSearch] = React.useState("");
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<any>([]);
const [searchTitle, setSearchTitle] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [imageData, setImageData] = React.useState<any>();
const [totalData, setTotalData] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState(1);
const [contentAll, setContentAll] = React.useState([]);
const [formatFilter, setFormatFilter] = React.useState<any>([]);
const [totalContent, setTotalContent] = React.useState();
const [search, setSearch] = React.useState<string>("");
const group = searchParams?.get("group");
const title = searchParams?.get("title");
const categorie = searchParams?.get("category");
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
// async function doneTyping() {
// fetchData();
// }
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
getCategories();
}, []);
React.useEffect(() => {
getDataTable();
}, [page, showData, search]);
const getDataTable = async () => {
const res = await getMediaTracking(page - 1, search, showData);
const data = res?.data?.data;
const newData = data?.content;
newData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(newData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
setTotalContent(data.totalElements);
};
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
React.useEffect(() => {
if (categorie) {
setCategoryFilter(
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
console.log(
"Kategori",
categorie,
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
}
}, [categorie]);
const handleCategoryFilter = (e: boolean, id: string) => {
let filter = [...categoryFilter];
if (e) {
filter = [...categoryFilter, String(id)];
} else {
filter.splice(categoryFilter.indexOf(id), 1);
}
console.log("checkbox filter", filter);
setCategoryFilter(filter);
router.push(`?category=${filter.join("&")}`);
};
const cleanCheckbox = () => {
setCategoryFilter([]);
setFormatFilter([]);
router.push(`?category=&title=`);
};
// async function getData() {
// if (asPath?.includes("/polda/") == true) {
// if (asPath?.split("/")[2] !== "[polda_name]") {
// const filter =
// categoryFilter?.length > 0
// ? categoryFilter?.sort().join(",")
// : categorie || "";
// const name = title == undefined ? "" : title;
// const format = formatFilter == undefined ? "" : formatFilter?.join(",");
// const filterGroup = group == undefined ? asPath.split("/")[2] : group;
// loading();
// const response = await listDataAll("", name, filter, "");
// close();
// // setGetTotalPage(response?.data?.data?.totalPages);
// // setContentImage(response?.data?.data?.content);
// // setTotalContent(response?.data?.data?.totalElements);
// const data = response?.data?.data;
// const contentData = data?.content;
// setImageData(contentData);
// setTotalData(data?.totalElements);
// setContentAll(response?.data?.data?.content);
// setTotalPage(data?.totalPages);
// setTotalContent(response?.data?.data?.totalElements);
// }
// }
// }
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
table.getColumn("judul")?.setFilterValue(e.target.value);
};
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
async function doneTyping() {
if (searchTitle == "" || searchTitle == undefined) {
router.push("?title=");
} else {
router.push(`?title=${searchTitle}`);
}
}
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3 border ">
<div className="flex justify-between items-center">
<p className="text-xl font-medium text-default-900">
Tracking Berita hari ini!
</p>
</div>
<div className="flex flex-col sm:flex-row lg:flex-row justify-between sm:items-center md:items-center lg:items-center px-1">
{/* <TrackingMediaModal triggerFetch={() => getDataTable()} /> */}
<div className=" flex flex-row items-center gap-3">
<div className="flex items-center py-2">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
1 - 100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
1 - 250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-[75px]">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
{/* <div className="flex justify-end mt-4">
<Button
color="primary"
size="md"
className="text-sm mr-3"
onClick={() => setShowTable(false)}
>
Tracking Berita Baru
</Button>
</div> */}
{/* </>
)} */}
</div>
);
};
export default NewsTable;

View File

@ -0,0 +1,12 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import NewsTable from "../../component/table";
import NewsDetailTable from "../component/table";
export default function DetailNews() {
return (
<div>
<SiteBreadcrumb />
<NewsDetailTable />
</div>
);
}

View File

@ -0,0 +1,186 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import { validateMediaLink } from "@/service/media-tracking/media-tracking";
import toast from "react-hot-toast";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "source",
header: "Media Online",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("source")}</span>
),
},
{
accessorKey: "title",
header: "Judul Berita",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("title")}</span>
),
},
// {
// accessorKey: "link",
// header: "Link Berita",
// cell: ({ row }) => (
// <span className="normal-case">{row.getValue("link")}</span>
// ),
// },
{
accessorKey: "link",
header: "Link Berita",
cell: ({ row }) => {
const link = row.getValue<string>("link");
if (!link) {
return <span className="text-muted-foreground">-</span>;
}
return (
<Link
href={link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800 break-all"
>
{link}
</Link>
);
},
},
{
id: "validation",
header: "Validasi",
cell: ({ row, table }) => {
const original = row.original;
// const isValid = original.isValid;
const isRelevant = original.isRelevant;
const link = original.link;
const updateRow = (data: Partial<any>) => {
table.options.meta?.updateData(row.index, data);
};
const handleValid = async () => {
try {
await validateMediaLink(original.id, true);
updateRow({
isRelevant: true,
});
table.options.meta?.refetchData?.();
} catch (err: any) {
toast.error(err.message);
}
};
const handleInvalid = async () => {
try {
await validateMediaLink(original.id, false);
updateRow({
isRelevant: false,
});
table.options.meta?.refetchData?.();
} catch (err: any) {
toast.error(err.message);
}
};
if (!link) {
return <span className="text-muted-foreground">-</span>;
}
if (isRelevant === true) {
return (
<Button
size="sm"
className="bg-green-600 hover:bg-green-700"
disabled
>
Relevan
</Button>
);
}
return (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={handleValid}
className="flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M18.7 7.2c-.4-.4-1-.4-1.4 0l-7.5 7.5l-3.1-3.1c-.4-.4-1-.4-1.4 0s-.4 1 0 1.4l3.8 3.8c.2.2.4.3.7.3s.5-.1.7-.3l8.2-8.2c.4-.4.4-1 0-1.4"
/>
</svg>
Relevan
</Button>
<Button size="sm" variant="outline" onClick={handleInvalid} className="flex text-center items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M6.758 17.243L12.001 12m5.243-5.243L12 12m0 0L6.758 6.757M12.001 12l5.243 5.243"
/>
</svg>
Tidak Relevan
</Button>
</div>
);
},
},
];
export default columns;

View File

@ -0,0 +1,272 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { listDataMedia } from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { link } from "fs";
import { getMediaTrackingResult } from "@/service/media-tracking/media-tracking";
const NewsDetailTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const param = useParams();
const id = param?.id;
const [search, setSearch] = React.useState("");
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [showTable, setShowTable] = React.useState(false);
const [link, setLink] = React.useState("");
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
meta: {
updateData: (rowIndex: number, value: Partial<any>) => {
setDataTable((old) =>
old.map((row, index) =>
index === rowIndex ? { ...row, ...value } : row
)
);
},
refetchData: () => {
fetchData();
},
},
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
// const handleKeyUp = () => {
// clearTimeout(typingTimer);
// typingTimer = setTimeout(doneTyping, doneTypingInterval);
// };
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
// async function doneTyping() {
// fetchData();
// }
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
setPagination({
pageIndex: 0,
pageSize: Number(showData),
});
}, [page, showData, id]);
async function fetchData() {
try {
loading();
const res = await getMediaTrackingResult({ id: id, page: page - 1 });
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", data);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
close();
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
const handleChange = (type: string, id: number, checked: boolean) => {
if (type === "category") {
if (checked) {
const temp: number[] = [...categoryFilter];
temp.push(id);
setCategoryFilter(temp);
} else {
const temp = categoryFilter.filter((a) => a !== id);
setCategoryFilter(temp);
}
} else {
if (checked) {
const temp: number[] = [...statusFilter];
temp.push(id);
setStatusFilter(temp);
} else {
const temp = statusFilter.filter((a) => a !== id);
setStatusFilter(temp);
}
}
};
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3 border">
<Table className="overflow-hidden mt-4">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-[75px]">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default NewsDetailTable;

View File

@ -0,0 +1,54 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Nama Media Online",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "link",
header: "Link",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
];
export default columns;

View File

@ -0,0 +1,302 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { listDataMedia } from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing";
const MediaOnlineTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = React.useState("");
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
// const handleKeyUp = () => {
// clearTimeout(typingTimer);
// typingTimer = setTimeout(doneTyping, doneTypingInterval);
// };
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
// async function doneTyping() {
// fetchData();
// }
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
// React.useEffect(() => {
// fetchData();
// setPagination({
// pageIndex: 0,
// pageSize: Number(showData),
// });
// }, [page, showData]);
// async function fetchData() {
// try {
// loading();
// const res = await listDataMedia(
// page - 1,
// showData,
// search,
// categoryFilter?.sort().join(","),
// statusFilter?.sort().join(",")
// );
// const data = res?.data?.data;
// const contentData = data?.content;
// contentData.forEach((item: any, index: number) => {
// item.no = (page - 1) * Number(showData) + index + 1;
// });
// console.log("contentData : ", data);
// setDataTable(contentData);
// setTotalData(data?.totalElements);
// setTotalPage(data?.totalPages);
// close();
// } catch (error) {
// console.error("Error fetching tasks:", error);
// }
// }
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
const handleChange = (type: string, id: number, checked: boolean) => {
if (type === "category") {
if (checked) {
const temp: number[] = [...categoryFilter];
temp.push(id);
setCategoryFilter(temp);
} else {
const temp = categoryFilter.filter((a) => a !== id);
setCategoryFilter(temp);
}
} else {
if (checked) {
const temp: number[] = [...statusFilter];
temp.push(id);
setStatusFilter(temp);
} else {
const temp = statusFilter.filter((a) => a !== id);
setStatusFilter(temp);
}
}
};
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
<div className="flex justify-between mb-10 items-center">
<p className="text-xl font-medium text-default-900">Media Online</p>
</div>
<div className="flex justify-between ">
<Link href="/admin/media-tracking/media-online/create">
<Button color="primary" size="md" className="text-sm mr-3">
Tambah Media Online
</Button>
</Link>
<Input
type="text"
placeholder="Search"
// onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onChange={(e) => setSearch(e.target.value)}
className="max-w-[300px]"
/>
<div className="flex flex-row gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default MediaOnlineTable;

View File

@ -0,0 +1,19 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTaskTa from "@/components/form/task-ta/task-ta-form";
import FormAskExpert from "@/components/form/shared/ask-expert-form";
import FormDoItYourself from "@/components/form/shared/do-it-yourself-form";
import FormMediaOnline from "@/components/form/media-tracking/media-tracking-form";
const MediaOnlineCreatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormMediaOnline />
</div>
</div>
);
};
export default MediaOnlineCreatePage;

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import MediaOnlineTable from "./component/table";
export default function AdminMediaOnline() {
return (
<div>
<SiteBreadcrumb />
<MediaOnlineTable />
</div>
);
}

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import NewsTable from "./component/table";
export default function AdminNews() {
return (
<div>
<SiteBreadcrumb />
<NewsTable />
</div>
);
}

View File

@ -0,0 +1,203 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { exportMediaTrackingToExcel } from "@/utils/export-media-tracking";
import { loading, close } from "@/config/swal";
import { error } from "@/lib/swal";
import {
DownloadIcon,
Eye,
MoreVertical,
SquarePen,
Trash2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Judul",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "resultTotal",
header: () => <div className="text-center w-full">Total Artikel</div>,
cell: ({ row }) => {
const value = row.getValue("resultTotal") as number | string | null;
const finalValue =
value === null || value === undefined || value === ""
? 0
: Number(value);
return <div className="text-center w-full">{finalValue}</div>;
},
},
{
accessorKey: "amplification",
header: () => <div className="text-center w-full">Jumlah Amplifikasi</div>,
cell: ({ row }) => {
const raw = row.getValue("amplification") as string | null;
let total = 0;
let invalidTotal = 0;
if (raw && typeof raw === "string") {
const parts = raw.split("/").map((v) => v.trim());
total = Number(parts[0]) || 0;
invalidTotal = Number(parts[1]) || 0;
}
return (
<div className="text-center w-full font-medium">
{total}
<span className="text-muted-foreground">/{invalidTotal}</span>
</div>
);
},
},
// {
// accessorKey: "status",
// header: "Status",
// cell: ({ row }) => <span>{row.getValue("status")}</span>,
// },
// {
// accessorKey: "isProcessing",
// header: () => <div className="text-center">Status</div>,
// cell: ({ row }) => {
// const raw = row.getValue("isProcessing");
// var status = "Sedang Diproses"
// if (Boolean(raw) == true) {
// status = "Selesai Diproses";
// }
// return <div className="text-center">{status}</div>;
// },
// },
{
accessorKey: "isProcessing",
header: () => <div className="text-center">Status</div>,
cell: ({ row }) => {
const raw = Boolean(row.getValue("isProcessing"));
const statusText = raw ? "Sedang Diproses" : "Sudah Selesai";
const colorClass = raw
? "bg-yellow-100 text-yellow-700 border border-yellow-300"
: "bg-green-100 text-green-700 border border-green-300";
return (
<div className="text-center">
<span
className={`px-2 py-1 rounded text-xs font-medium inline-block ${colorClass}`}
>
{statusText}
</span>
</div>
);
},
},
{
accessorKey: "createdAt",
header: () => <div className="text-center">Tanggal Penarikan</div>,
cell: ({ row }) => {
const raw = row.getValue("createdAt");
if (!raw || typeof raw !== "string")
return <div className="text-center">-</div>;
const date = new Date(raw);
if (isNaN(date.getTime())) return <div className="text-center">-</div>;
const formatted = date.toLocaleDateString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
});
return <div className="text-center">{formatted}</div>;
},
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/media-tracking/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b cursor-pointer text-default-700 group focus:bg-default focus:text-primary-foreground items-center rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View&nbsp;
{row.original.mediaUpload.fileType.secondaryName &&
row.original.mediaUpload.fileType.secondaryName.toLowerCase()}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
className="p-2 border-b cursor-pointer text-default-700 group rounded-none focus:bg-default focus:text-primary-foreground "
onClick={async () => {
try {
loading();
await exportMediaTrackingToExcel({
mediaTrackingId: row.original.id,
});
close();
} catch (e: any) {
close();
error(e.message || "Gagal export data");
}
}}
>
<DownloadIcon className="w-4 h-4 me-1.5" />
<p>Download</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,382 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { listEnableCategory } from "@/service/content/content";
import { close, loading } from "@/config/swal";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { link } from "fs";
import { listDataAll } from "@/service/landing/landing";
import SearchImageComponent from "@/components/form/media-tracking/search-image-card";
import SearchVideoComponent from "@/components/form/media-tracking/search-video-card";
import SearchDocumentComponent from "@/components/form/media-tracking/search-document-card";
import SearchAudioComponent from "@/components/form/media-tracking/search-audio-card";
import { getMediaTracking } from "@/service/media-tracking/media-tracking";
import { group } from "console";
import router from "next/router";
import { title } from "process";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
const ResultTable = () => {
const router = useRouter();
const asPath = usePathname();
const searchParams = useSearchParams();
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [showTable, setShowTable] = React.useState(false);
const [onSearch, setOnSearch] = React.useState("");
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<any>([]);
const [searchTitle, setSearchTitle] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [imageData, setImageData] = React.useState<any>();
const [totalData, setTotalData] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState(1);
const [contentAll, setContentAll] = React.useState([]);
const [formatFilter, setFormatFilter] = React.useState<any>([]);
const [totalContent, setTotalContent] = React.useState();
const [search, setSearch] = React.useState<string>("");
const group = searchParams?.get("group");
const title = searchParams?.get("title");
const categorie = searchParams?.get("category");
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
// async function doneTyping() {
// fetchData();
// }
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
getCategories();
}, []);
React.useEffect(() => {
getDataTable();
}, [page, showData, search]);
const getDataTable = async () => {
const res = await getMediaTracking(page - 1, search, showData);
const data = res?.data?.data;
const newData = data?.content;
newData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(newData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
setTotalContent(data.totalElements);
};
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
React.useEffect(() => {
if (categorie) {
setCategoryFilter(
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
console.log(
"Kategori",
categorie,
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
}
}, [categorie]);
const handleCategoryFilter = (e: boolean, id: string) => {
let filter = [...categoryFilter];
if (e) {
filter = [...categoryFilter, String(id)];
} else {
filter.splice(categoryFilter.indexOf(id), 1);
}
console.log("checkbox filter", filter);
setCategoryFilter(filter);
router.push(`?category=${filter.join("&")}`);
};
const cleanCheckbox = () => {
setCategoryFilter([]);
setFormatFilter([]);
router.push(`?category=&title=`);
};
// async function getData() {
// if (asPath?.includes("/polda/") == true) {
// if (asPath?.split("/")[2] !== "[polda_name]") {
// const filter =
// categoryFilter?.length > 0
// ? categoryFilter?.sort().join(",")
// : categorie || "";
// const name = title == undefined ? "" : title;
// const format = formatFilter == undefined ? "" : formatFilter?.join(",");
// const filterGroup = group == undefined ? asPath.split("/")[2] : group;
// loading();
// const response = await listDataAll("", name, filter, "");
// close();
// // setGetTotalPage(response?.data?.data?.totalPages);
// // setContentImage(response?.data?.data?.content);
// // setTotalContent(response?.data?.data?.totalElements);
// const data = response?.data?.data;
// const contentData = data?.content;
// setImageData(contentData);
// setTotalData(data?.totalElements);
// setContentAll(response?.data?.data?.content);
// setTotalPage(data?.totalPages);
// setTotalContent(response?.data?.data?.totalElements);
// }
// }
// }
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
table.getColumn("judul")?.setFilterValue(e.target.value);
};
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
async function doneTyping() {
if (searchTitle == "" || searchTitle == undefined) {
router.push("?title=");
} else {
router.push(`?title=${searchTitle}`);
}
}
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
return (
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3 border ">
<div className="flex flex-col sm:flex-row lg:flex-row justify-end sm:items-center md:items-center lg:items-center">
<div className=" flex flex-row justify-end items-center gap-3">
<div className="flex items-center">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
1 - 100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
1 - 250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex items-center py-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-[75px]">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
{/* <div className="flex justify-end mt-4">
<Button
color="primary"
size="md"
className="text-sm mr-3"
onClick={() => setShowTable(false)}
>
Tracking Berita Baru
</Button>
</div> */}
{/* </>
)} */}
</div>
);
};
export default ResultTable;

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import ResultTable from "./component/table";
export default function AdminResult() {
return (
<div>
<SiteBreadcrumb />
<ResultTable />
</div>
);
}

View File

@ -0,0 +1,84 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "date",
header: "Tanggal",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
{
accessorKey: "link",
header: "Link Berita",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
{
id: "actions",
accessorKey: "action",
header: "Aksi",
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link
href={`/admin/media-tracking/tb-news/detail/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,333 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { listEnableCategory } from "@/service/content/content";
import { close, loading } from "@/config/swal";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { link } from "fs";
import { listDataAll } from "@/service/landing/landing";
import SearchImageComponent from "@/components/form/media-tracking/search-image-card";
import SearchVideoComponent from "@/components/form/media-tracking/search-video-card";
import SearchDocumentComponent from "@/components/form/media-tracking/search-document-card";
import SearchAudioComponent from "@/components/form/media-tracking/search-audio-card";
const NewsTable = () => {
const router = useRouter();
const asPath = usePathname();
const searchParams = useSearchParams();
const [search, setSearch] = React.useState("");
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [showTable, setShowTable] = React.useState(false);
const [onSearch, setOnSearch] = React.useState("");
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<any>([]);
const [searchTitle, setSearchTitle] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [imageData, setImageData] = React.useState<any>();
const [totalData, setTotalData] = React.useState<number>(1);
const [totalPage, setTotalPage] = React.useState<number>(1);
const [contentAll, setContentAll] = React.useState([]);
const [formatFilter, setFormatFilter] = React.useState<any>([]);
const [totalContent, setTotalContent] = React.useState();
const group = searchParams?.get("group");
const title = searchParams?.get("title");
const categorie = searchParams?.get("category");
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
// async function doneTyping() {
// fetchData();
// }
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
React.useEffect(() => {
if (categorie) {
setCategoryFilter(
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
console.log(
"Kategori",
categorie,
categorie?.split("&")?.length > 1 ? categorie?.split("&") : [categorie]
);
}
}, [categorie]);
const handleCategoryFilter = (e: boolean, id: string) => {
let filter = [...categoryFilter];
if (e) {
filter = [...categoryFilter, String(id)];
} else {
filter.splice(categoryFilter.indexOf(id), 1);
}
console.log("checkbox filter", filter);
setCategoryFilter(filter);
router.push(`?category=${filter.join("&")}`);
};
const cleanCheckbox = () => {
setCategoryFilter([]);
setFormatFilter([]);
router.push(`?category=&title=`);
};
async function getData() {
if (asPath?.includes("/polda/") == true) {
if (asPath?.split("/")[2] !== "[polda_name]") {
const filter =
categoryFilter?.length > 0
? categoryFilter?.sort().join(",")
: categorie || "";
const name = title == undefined ? "" : title;
const format = formatFilter == undefined ? "" : formatFilter?.join(",");
const filterGroup = group == undefined ? asPath.split("/")[2] : group;
loading();
const response = await listDataAll("", name, filter, "");
close();
// setGetTotalPage(response?.data?.data?.totalPages);
// setContentImage(response?.data?.data?.content);
// setTotalContent(response?.data?.data?.totalElements);
const data = response?.data?.data;
const contentData = data?.content;
setImageData(contentData);
setTotalData(data?.totalElements);
setContentAll(response?.data?.data?.content);
setTotalPage(data?.totalPages);
setTotalContent(response?.data?.data?.totalElements);
}
}
}
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
async function doneTyping() {
if (searchTitle == "" || searchTitle == undefined) {
router.push("?title=");
} else {
router.push(`?title=${searchTitle}`);
}
}
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3 border ">
<div className="flex justify-between items-center">
<p className="text-xl font-medium text-default-900">
Tracking Berita hari ini!
</p>
</div>
<Dialog>
<DialogTrigger asChild>
<Button className="bg-blue-600" size="md">
Tracking Berita
</Button>
</DialogTrigger>
<DialogContent className="overflow-y-auto h-[500px] min-w-max mx-5">
<DialogHeader>
<DialogTitle>Form Tracking Berita</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4 px-5">
<div className="space-y-2 flex flex-col">
<Label htmlFor="link" className="text-sm font-medium">
Masukkan Link <span className="text-red-500">*</span>
</Label>
<input
value={searchTitle}
onChange={(e) => setSearchTitle(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
type="text"
placeholder="Search..."
className="pl-4 pr-4 py-1 w-full h-10 text-[15px] border focus:outline-none dark:text-white"
/>
</div>
<div className="flex flex-col gap-2 w-full">
<SearchImageComponent categoryFilter={categoryFilter} />
<SearchVideoComponent categoryFilter={categoryFilter} />
<SearchDocumentComponent categoryFilter={categoryFilter} />
<SearchAudioComponent categoryFilter={categoryFilter} />
</div>
</div>
<DialogFooter className="flex justify-end gap-2">
<Button onClick={cleanCheckbox} variant="outline">
Riset Filter
</Button>
<Button className="bg-blue-600 text-white">Tracking Berita</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Table className="overflow-hidden mt-4">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-[75px]">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
<div className="flex justify-end mt-4">
<Button
color="primary"
size="md"
className="text-sm mr-3"
onClick={() => setShowTable(false)}
>
Tracking Berita Baru
</Button>
</div>
{/* </>
)} */}
</div>
);
};
export default NewsTable;

View File

@ -0,0 +1,12 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import NewsTable from "../../component/table";
import NewsDetailTable from "../component/table";
export default function DetailNews() {
return (
<div>
<SiteBreadcrumb />
<NewsDetailTable />
</div>
);
}

View File

@ -0,0 +1,53 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "mediaOnline",
header: "Media Online",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
{
accessorKey: "link",
header: "Link Berita",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
];
export default columns;

View File

@ -0,0 +1,263 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { listDataMedia } from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { link } from "fs";
const NewsDetailTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = React.useState("");
const [showData, setShowData] = React.useState("10");
const [categories, setCategories] = React.useState<any>();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [showTable, setShowTable] = React.useState(false);
const [link, setLink] = React.useState("");
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
let typingTimer: any;
const doneTypingInterval = 1500;
// const handleKeyUp = () => {
// clearTimeout(typingTimer);
// typingTimer = setTimeout(doneTyping, doneTypingInterval);
// };
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
// async function doneTyping() {
// fetchData();
// }
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
// React.useEffect(() => {
// fetchData();
// setPagination({
// pageIndex: 0,
// pageSize: Number(showData),
// });
// }, [page, showData]);
// async function fetchData() {
// try {
// loading();
// const res = await listDataMedia(
// page - 1,
// showData,
// search,
// categoryFilter?.sort().join(","),
// statusFilter?.sort().join(",")
// );
// const data = res?.data?.data;
// const contentData = data?.content;
// contentData.forEach((item: any, index: number) => {
// item.no = (page - 1) * Number(showData) + index + 1;
// });
// console.log("contentData : ", data);
// setDataTable(contentData);
// setTotalData(data?.totalElements);
// setTotalPage(data?.totalPages);
// close();
// } catch (error) {
// console.error("Error fetching tasks:", error);
// }
// }
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
const handleChange = (type: string, id: number, checked: boolean) => {
if (type === "category") {
if (checked) {
const temp: number[] = [...categoryFilter];
temp.push(id);
setCategoryFilter(temp);
} else {
const temp = categoryFilter.filter((a) => a !== id);
setCategoryFilter(temp);
}
} else {
if (checked) {
const temp: number[] = [...statusFilter];
temp.push(id);
setStatusFilter(temp);
} else {
const temp = statusFilter.filter((a) => a !== id);
setStatusFilter(temp);
}
}
};
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3 border">
<Table className="overflow-hidden mt-4">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-[75px]">
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default NewsDetailTable;

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import NewsTable from "./component/table";
export default function AdminNews() {
return (
<div>
<SiteBreadcrumb />
<NewsTable />
</div>
);
}

View File

@ -0,0 +1,54 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Nama Media Online",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "link",
header: "Link",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
];
export default columns;

View File

@ -0,0 +1,358 @@
"use client";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { ChevronLeft, ChevronRight } from "lucide-react";
import {
listDataAllNonPagination,
listDataTracking,
mediaTrackingSave,
} from "@/service/media-tracking/media-tracking";
import { error } from "@/lib/swal";
import { toast } from "sonner";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { PaginationState } from "@tanstack/react-table";
import page from "../page";
import CustomPagination from "@/components/table/custom-pagination";
import { close, loading } from "@/config/swal";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
export default function TrackingBeritaCard() {
const [search, setSearch] = useState("");
const [content, setContent] = useState<any[]>([]);
const [selectedItems, setSelectedItems] = useState<number[]>([]);
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [showData, setShowData] = useState("6");
const MySwal = withReactContent(Swal);
useEffect(() => {
initFecth();
}, [showData, page]);
const initFecth = async () => {
loading();
const response = await listDataTracking(Number(showData), page - 1, search);
const data = response?.data?.data;
const newData = data?.content;
setTotalPage(data?.totalPages || 1);
newData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setContent(response?.data?.data?.content || []);
close();
};
const fecthAll = async (keyword: string) => {
const response = await listDataAllNonPagination(keyword);
setContent(response?.data?.data?.content || []);
};
const handleInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setSearch(value);
const response = await listDataTracking(Number(showData), 0, value);
setContent(response?.data?.data?.content || []);
};
// const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// const value = e.target.value;
// setSearch(value);
// if (value.trim() === "") {
// initFecth();
// } else {
// fecthAll(value);
// }
// };
const handleSelect = (id: number) => {
setSelectedItems((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
};
const doSave = async () => {
if (selectedItems.length === 0) {
MySwal.fire(
"Peringatan",
"Pilih minimal 1 berita untuk disimpan.",
"warning"
);
return;
}
try {
loading();
const promises = selectedItems.map(async (id) => {
const res = await mediaTrackingSave({
mediaUploadId: id,
duration: 24,
scrapingPeriod: 3,
});
// cek pesan API
if (!res?.data?.success) {
throw new Error(
res?.data?.message ||
"Limit media tracking per hari sudah tercapai. Maksimal 5 tracking per hari."
);
}
return res;
});
await Promise.all(promises);
close();
await MySwal.fire({
icon: "success",
title: "Berhasil!",
text: "Tracking berita berhasil ditambahkan.",
confirmButtonColor: "#2563eb",
});
setSelectedItems([]);
initFecth();
} catch (err: any) {
close();
MySwal.fire({
icon: "error",
title: "Gagal!",
text: err?.message || "Terjadi kesalahan saat menyimpan data.",
confirmButtonColor: "#dc2626",
});
}
};
// const doSave = async () => {
// if (selectedItems.length === 0) {
// toast("Pilih minimal 1 berita untuk disimpan.");
// return;
// }
// try {
// const promises = selectedItems.map((id) =>
// mediaTrackingSave({
// mediaUploadId: id,
// duration: 24,
// scrapingPeriod: 3,
// })
// );
// await Promise.all(promises);
// toast("Berhasil Menambahkan", {
// description: "",
// });
// setSelectedItems([]);
// initFecth();
// } catch (err: any) {
// error(err?.message || "Gagal menyimpan data.");
// }
// };
// const doSave = async () => {
// if (selectedItems.length === 0) {
// MySwal.fire(
// "Peringatan",
// "Pilih minimal 1 berita untuk disimpan.",
// "warning"
// );
// return;
// }
// try {
// loading();
// const promises = selectedItems.map((id) =>
// mediaTrackingSave({
// mediaUploadId: id,
// duration: 24,
// scrapingPeriod: 3,
// })
// );
// await Promise.all(promises);
// close();
// await MySwal.fire({
// icon: "success",
// title: "Berhasil!",
// text: "Tracking berita berhasil ditambahkan.",
// confirmButtonColor: "#2563eb",
// });
// setSelectedItems([]);
// initFecth();
// } catch (err: any) {
// close();
// MySwal.fire({
// icon: "error",
// title: "Gagal!",
// text: err?.message || "Terjadi kesalahan saat menyimpan data.",
// confirmButtonColor: "#dc2626",
// });
// }
// };
const slugify = (text: string) => {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)+/g, "");
};
const goToDetail = (item: any) => {
const type = item.type || "image";
const slug = slugify(item.title || "");
const url = `/in/${type}/detail/${item.id}-${slug}`;
window.location.href = url;
};
return (
<Card>
<div className="p-4 space-y-4">
<div className="flex justify-between items-center">
<Input
placeholder="Pencarian"
value={search}
onChange={handleInputChange}
className="max-w-sm"
/>
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="12">
1 - 6 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="12">
1 - 12 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="60">
1 - 60 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="120">
1 - 120 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{selectedItems.length > 0 && (
<div className="flex justify-between items-center">
<div className="text-sm text-blue-600 font-medium">
{selectedItems.length} Item Terpilih{" "}
<span className="text-black">
/ Tracking Berita tersisa {5 - selectedItems.length}
</span>
</div>
<Button className="bg-blue-600 text-white" onClick={doSave}>
Submit
</Button>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{content?.length > 0 &&
content.map((item: any) => (
<Card
key={item.id}
className="relative overflow-hidden shadow-sm border rounded-lg"
>
{/* KLIK GAMBAR = CHECKLIST */}
<div
className="cursor-pointer"
onClick={() => handleSelect(item.id)}
>
<img
src={item.thumbnailLink}
alt={item.title}
className="w-full h-[300px] object-cover"
/>
{/* CHECKBOX */}
<div className="absolute top-2 left-2">
<div className="w-5 h-5 border-2 border-white bg-white rounded-sm flex items-center justify-center">
{selectedItems.includes(item.id) && (
<div className="w-3 h-3 bg-blue-600 rounded-sm" />
)}
</div>
</div>
</div>
{/* KLIK JUDUL = DETAIL */}
<p
className="p-2 text-sm font-medium hover:underline cursor-pointer"
onClick={(e) => {
e.stopPropagation();
goToDetail(item);
}}
>
{item.title}
</p>
</Card>
))}
</div>
{/* <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{content?.length > 1 &&
content.map((item: any) => (
<Card
key={item.id}
className="relative overflow-hidden shadow-sm border rounded-lg cursor-pointer"
onClick={() => handleSelect(item.id)}
>
<img
src={item.thumbnailLink}
alt={item.title}
className="w-full h-[300px] object-cover"
/>
<div className="absolute top-2 left-2">
<div className="w-5 h-5 border-2 border-white bg-white rounded-sm flex items-center justify-center">
{selectedItems.includes(item.id) && (
<div className="w-3 h-3 bg-blue-600 rounded-sm" />
)}
</div>
</div>
<p className="p-2 text-sm font-medium text-gray-800 truncate">
{item.title}
</p>
</Card>
))}
</div> */}
<div className="mt-3">
{content && content?.length > 0 ? (
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
) : (
<p>No Data</p>
)}
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import TrackingBeritaCard from "./component/table";
export default function AdminTrackingBerita() {
return (
<div>
<SiteBreadcrumb />
<TrackingBeritaCard />
</div>
);
}

View File

@ -0,0 +1,12 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import PerformancePoldaViz from "@/components/visualization/performance-polda";
export default function PerformancePolda() {
return (
<div>
<SiteBreadcrumb />
<p className="font-semibold">PERFORMANCE KUMULATIF PER POLDA</p>
<PerformancePoldaViz />
</div>
);
}

View File

@ -0,0 +1,12 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import PerformancePolresViz from "@/components/visualization/performance-polres";
export default function PerformancePolda() {
return (
<div>
<SiteBreadcrumb />
<p className="font-semibold">PERFORMANCE KUMULATIF PER POLRES</p>
<PerformancePolresViz />
</div>
);
}

View File

@ -0,0 +1,13 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import PerformancePolresViz from "@/components/visualization/performance-polres";
import PerformanceSatkerViz from "@/components/visualization/performance-satker";
export default function PerformanceSatker() {
return (
<div>
<SiteBreadcrumb />
<p className="font-semibold">PERFORMANCE KUMULATIF PER SATKER</p>
<PerformanceSatkerViz />
</div>
);
}

View File

@ -1,19 +1,28 @@
import * as React from "react";
import { Link, useRouter } from "@/i18n/navigation";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { formatDateToIndonesian, getOnlyDate, htmlToString } from "@/lib/utils/globals";
import { cn } from "@/lib/utils/utils";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import {
Dialog,
DialogContent,
@ -21,15 +30,9 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import StaticToogle from "./static-toogle";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import StatusToogle from "./status-toogle";
import StaticToogle from "./static-toogle";
const columns: ColumnDef<any>[] = [
// {
@ -51,21 +54,17 @@ const columns: ColumnDef<any>[] = [
{
accessorKey: "createdAt",
header: "Tanggal Unggah",
cell: ({ row }) => <span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>,
},
{
accessorKey: "isStaticBanner",
header: "Static Banner",
cell: ({ row }) => (
<StaticToogle id={row.original.id} initChecked={row.original.isStaticBanner} />
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
),
},
{
accessorKey: "statusName",
header: "Status Banner",
cell: ({ row }) => <StatusToogle id={row.original.id} initChecked={row.original.isBanner} />,
cell: ({ row }) => (
<StatusToogle id={row.original.id} initChecked={row.original.isBanner} />
),
},
{
id: "actions",
accessorKey: "action",
@ -84,7 +83,11 @@ const columns: ColumnDef<any>[] = [
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Link href={`/contributor/content/image/detail/${row.original.id}`}>Detail</Link>
<Link
href={`/contributor/content/image/detail/${row.original.id}`}
>
Detail
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,24 +1,19 @@
"use client";
import * as React from "react";
import { useSearchParams } from "next/navigation";
import { Link, useRouter } from "@/i18n/navigation";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table";
import { close, loading } from "@/config/swal";
import { listBanner, listStaticBanner } from "@/lib/services/settings/settings";
import {
Table,
TableBody,
@ -28,7 +23,12 @@ import {
TableRow,
} from "@/components/ui/table";
import { useSearchParams } from "next/navigation";
import { close, loading } from "@/config/swal";
import { Link, useRouter } from "@/i18n/routing";
import columns from "./banner-column";
import { listBanner, listStaticBanner } from "@/service/settings/settings";
const BannerListTable = () => {
const router = useRouter();
@ -36,8 +36,11 @@ const BannerListTable = () => {
const [showData, setShowData] = React.useState("10");
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
@ -110,7 +113,10 @@ const BannerListTable = () => {
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>

View File

@ -0,0 +1,113 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import { setBanner } from "@/service/settings/settings";
import { error } from "@/config/swal";
import { useToast } from "@/components/ui/use-toast";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Judul",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "categoryName",
header: "Kategori",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
{
accessorKey: "createdAt",
header: "Tanggal Unggah",
cell: ({ row }) => (
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
),
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => <span>{row.getValue("statusName")}</span>,
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
const { toast } = useToast();
const handleBanner = async (id: number) => {
const response = setBanner(id, true);
toast({
title: "Success",
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Link
href={`/contributor/content/image/detail/${row.original.id}`}
>
Detail
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<a onClick={() => handleBanner(row.original.id)}>
Jadikan Banner
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,40 @@
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { useRouter } from "@/i18n/routing";
import { setStaticBanner } from "@/service/settings/settings";
export default function StaticToogle(props: {
id: number;
initChecked: boolean;
}) {
const { id, initChecked } = props;
const { toast } = useToast();
const router = useRouter();
const changeStaticBanner = async (id: number) => {
const response = await setStaticBanner(id);
if (response?.error) {
toast({
variant: "destructive",
title: response?.message,
});
return false;
}
toast({
title: "Success ",
});
router.push("/admin/settings/banner?dataChange=true");
};
return (
<Switch
id="static-toogle"
checked={initChecked}
onCheckedChange={(e) => {
changeStaticBanner(id);
}}
/>
);
}

View File

@ -0,0 +1,37 @@
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { useRouter } from "@/i18n/routing";
import { setBanner } from "@/service/settings/settings";
export default function StatusToogle(props: {
id: number;
initChecked: boolean;
}) {
const { id, initChecked } = props;
const { toast } = useToast();
const router = useRouter();
const disableBanner = async () => {
const response = await setBanner(id, false);
if (response?.error) {
toast({
variant: "destructive",
title: response?.message,
});
return false;
}
toast({
title: "Success ",
});
router.push("/admin/settings/banner?dataChange=true");
};
return (
<Switch
id="status-toogle"
checked={initChecked}
onCheckedChange={() => disableBanner()}
/>
);
}

View File

@ -0,0 +1,477 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UserIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { paginationBlog } from "@/service/blog/blog";
import { ticketingPagination } from "@/service/ticketing/ticketing";
import { Badge } from "@/components/ui/badge";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { listDataMedia } from "@/service/broadcast/broadcast";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link } from "@/i18n/routing";
import { data } from "jquery";
import { useToast } from "@/components/ui/use-toast";
import { setBanner } from "@/service/settings/settings";
import { id } from "date-fns/locale";
import CustomPagination from "@/components/table/custom-pagination";
const ContentListBanner = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [showData, setShowData] = React.useState("9");
const [categories, setCategories] = React.useState<any>();
const [data, setData] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [categoryFilter, setCategoryFilter] = React.useState<number[]>([]);
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [selectedItems, setSelectedItems] = React.useState<number[]>([]);
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [searchQuery, setSearchQuery] = React.useState("");
const [bannerCount, setBannerCount] = React.useState<number>(0);
React.useEffect(() => {
fetchBannerCount();
}, []);
async function fetchBannerCount() {
try {
const res = await listDataMedia(0, "100", "", "", "");
const banners = res?.data?.data?.content?.filter(
(item: any) => item.isBanner
);
setBannerCount(banners?.length || 0);
setBannerCount(data?.length || 0);
} catch (error) {
console.error("Error fetching banner count:", error);
}
}
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
let typingTimer: NodeJS.Timeout;
const doneTypingInterval = 1500;
const handleTyping = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
setPage(1);
fetchData();
}, doneTypingInterval);
};
React.useEffect(() => {
fetchData();
}, [categoryFilter, statusFilter]);
async function doneTyping() {
fetchData();
}
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
// setPagination({
// pageIndex: 0,
// pageSize: Number(showData),
// });
}, [page, showData]);
async function fetchData() {
try {
loading();
const res = await listDataMedia(
page - 1,
showData,
searchQuery,
categoryFilter?.sort().join(","),
statusFilter?.sort().join(",")
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", data);
setData(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
close();
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
React.useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const category = await listEnableCategory("");
const resCategory = category?.data?.data?.content;
setCategories(resCategory);
}
const handleChange = (type: string, id: number, checked: boolean) => {
if (type === "category") {
if (checked) {
const temp: number[] = [...categoryFilter];
temp.push(id);
setCategoryFilter(temp);
} else {
const temp = categoryFilter.filter((a) => a !== id);
setCategoryFilter(temp);
}
} else {
if (checked) {
const temp: number[] = [...statusFilter];
temp.push(id);
setStatusFilter(temp);
} else {
const temp = statusFilter.filter((a) => a !== id);
setStatusFilter(temp);
}
}
};
const handleSelect = (id: number) => {
setSelectedItems((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
);
};
const handleSelectAll = () => {
if (selectedItems.length === data.length) {
setSelectedItems([]);
} else {
setSelectedItems(data.map((item: any) => Number(item.id)));
}
};
const { toast } = useToast();
const handleBanner = async (ids: number[]) => {
try {
// const res = await Promise.all(ids.map((id) => setBanner(id, true)));
for (const element of ids) {
loading();
const res = await setBanner(element, true);
close();
if (res?.error) {
toast({
title: "Gagal",
description:
"Banner sudah melebihi batas maksimum (4 konten). Silahkan di disable banner Lainnya.",
variant: "destructive",
});
} else {
toast({
title: "Sukses",
description: `item berhasil dijadikan banner.`,
});
}
}
} catch (err) {
toast({
title: "Gagal",
description: "Terjadi kesalahan saat menjadikan banner.",
variant: "destructive",
});
}
};
return (
<>
<div className="flex justify-between ">
<Input
type="text"
placeholder="Search"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
handleTyping();
}}
className="max-w-[300px]"
onKeyDown={(e) => {
if (e.key === "Enter") {
fetchData();
}
}}
/>
{/* <div className="flex flex-row gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Popover>
<PopoverTrigger asChild>
<Button size="md" variant="outline">
Filter
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 ">
<div className="flex flex-col gap-2 px-2">
<div className="flex justify-between text-sm">
<p>Filter</p>
<a
onClick={() => fetchData()}
className="cursor-pointer text-primary"
>
Simpan
</a>
</div>
<div className="flex flex-col gap-1 overflow-auto max-h-[300px] text-xs custom-scrollbar-table">
<p>Kategory</p>
{categories?.map((category: any) => (
<div className="flex items-center space-x-2">
<Checkbox
id={category.id}
checked={categoryFilter.includes(category.id)}
onCheckedChange={(e) =>
handleChange("category", category.id, Boolean(e))
}
/>
<label
htmlFor={category.id}
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{category.name}
</label>
</div>
))}
<p className="mt-3">Status</p>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(1)}
onCheckedChange={(e) =>
handleChange("status", 1, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Menunggu Review
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(2)}
onCheckedChange={(e) =>
handleChange("status", 2, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Diterima
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(3)}
onCheckedChange={(e) =>
handleChange("status", 3, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Minta Update{" "}
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
checked={statusFilter.includes(4)}
onCheckedChange={(e) =>
handleChange("status", 4, Boolean(e))
}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Ditolak
</label>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div> */}
</div>
<div>
{/* Header select all + action */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Checkbox
checked={selectedItems.length === data.length}
onCheckedChange={handleSelectAll}
/>
<span className="text-black dark:text-white">Pilih Semua</span>
</div>
{selectedItems.length > 0 && (
<Button color="primary" onClick={() => handleBanner(selectedItems)}>
Jadikan Banner
</Button>
)}
</div>
{/* Grid Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{data?.map((item: any) => (
<div
key={item.id}
className="relative rounded-lg shadow-md overflow-hidden border border-gray-200"
>
<div className="absolute top-2 left-2 z-10">
<Checkbox
checked={selectedItems.includes(item.id)}
onCheckedChange={() => handleSelect(item.id)}
/>
</div>
<img
src={item.smallThumbnailLink || "/placeholder.jpg"}
alt={item.title}
className="w-full h-48 object-cover"
/>
<Link
href={`/contributor/content/image/detail/${item?.id}`}
className="p-3"
>
<h4 className="font-semibold text-sm">{item.title}</h4>
</Link>
</div>
))}
</div>
<div className="mt-3">
{data && data?.length > 0 ? (
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
) : (
<p>No Data</p>
)}
</div>
</div>
</>
);
};
export default ContentListBanner;

View File

@ -0,0 +1,56 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import ContentListTable from "./component/table";
import { useState } from "react";
import BannerListTable from "./component/banner-table";
import { Button } from "@/components/ui/button";
import ContentListBanner from "./component/table";
export default function AdminBanner() {
const [selectedTab, setSelectedTab] = useState("content");
return (
<div>
<SiteBreadcrumb />
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
<div className="flex justify-between">
{selectedTab === "content" ? "List Media" : " List Banner"}
<div className="flex flex-row gap-1 border-2 rounded-md w-fit mb-5">
<Button
rounded="md"
onClick={() => setSelectedTab("content")}
className={` hover:text-white
${
selectedTab === "content"
? "bg-black text-white "
: "bg-white text-black "
}`}
>
Konten
</Button>
<Button
rounded="md"
onClick={() => setSelectedTab("banner")}
className={` hover:text-white
${
selectedTab === "banner"
? "bg-black text-white "
: "bg-white text-black "
}`}
>
Banner
</Button>
</div>
</div>
{selectedTab === "content" ? (
<ContentListBanner />
) : (
<BannerListTable />
)}
</div>
</div>
);
}

View File

@ -0,0 +1,137 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Link, useRouter } from "@/i18n/routing";
import StatusToogle from "./status";
import { error } from "@/config/swal";
import { deleteCategory } from "@/service/settings/settings";
import { useToast } from "@/components/ui/use-toast";
import EditCategoryModal from "./edit";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Menubar,
MenubarContent,
MenubarMenu,
MenubarTrigger,
} from "@/components/ui/menubar";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "name",
header: "Nama Kategori",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("name")}</span>
),
},
{
accessorKey: "mediaTypesString",
header: "Tipe Konten",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("mediaTypesString")}</span>
),
},
{
accessorKey: "publishedLocation",
header: "Wilayah",
cell: ({ row }) => (
<span className="normal-case">{row.getValue("publishedLocation")}</span>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => (
<span className="normal-case">
<StatusToogle id={row.original.id} initValue={row.original.isPublish} />
</span>
),
},
{
id: "created",
accessorKey: "created",
header: "Dibuat Oleh",
cell: ({ row }) => (
<div className="flex flex-col items-center">
<span className="text-sm text-gray-500">{row.original.createdByName}</span><span className="text-xs font-bold">({row.original.createdByUserLevelName})</span>
</div>
),
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
const router = useRouter();
const { toast } = useToast();
const categoryDelete = async (id: number) => {
const response = await deleteCategory(id);
console.log(response);
if (response?.error) {
error(response.message);
return false;
}
toast({
title: "Sukses",
description: "Berhasil Delete",
});
router.push("/admin/settings/category?dataChange=true");
};
return (
<Menubar className="border-none dark:bg-black">
<MenubarMenu>
<MenubarTrigger>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</MenubarTrigger>
<MenubarContent className="flex flex-col gap-2 justify-center items-start p-4">
<EditCategoryModal
id={row.original.id}
isDetail={true}
thumbnailLink={row.original.thumbnailLink}
/>
<EditCategoryModal
id={row.original.id}
thumbnailLink={row.original.thumbnailLink}
/>
<a
onClick={() => categoryDelete(row.original.id)}
className="hover:underline cursor-pointer hover:text-destructive"
>
Delete
</a>
</MenubarContent>
</MenubarMenu>
</Menubar>
);
},
},
];
export default columns;

View File

@ -0,0 +1,600 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { getUserRoles, postCategory } from "@/service/settings/settings";
import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal";
import { UnitMapping } from "./unit-mapping";
import { useToast } from "@/components/ui/use-toast";
import { stringify } from "querystring";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import Image from "next/image";
import { Upload } from "tus-js-client";
import { getCookiesDecrypt } from "@/lib/utils";
import Cookies from "js-cookie";
import { useTranslations } from "next-intl";
const FormSchema = z.object({
title: z.string({
required_error: "Required",
}),
description: z.string({
required_error: "Required",
}),
contentType: z
.array(z.string())
.refine((value) => value.some((item) => item), {
message: "Required",
}),
selectedUser: z
.array(z.string())
.refine((value) => value.some((item) => item), {
message: "Required",
}),
publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "Required",
}),
file: z.string().optional(),
});
const listContent = [
{
id: "1",
name: "Foto",
},
{
id: "2",
name: "Audio Visual",
},
{
id: "3",
name: "Teks",
},
{
id: "4",
name: "Audio",
},
];
const publishToList = [
{
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function CreateCategoryModal() {
const router = useRouter();
const { toast } = useToast();
const roleId = getCookiesDecrypt("urie");
const levelNumber = getCookiesDecrypt("ulne");
const userLevelId = getCookiesDecrypt("ulie");
const poldaState = Cookies.get("state");
const t = useTranslations("Menu");
const [files, setFiles] = useState<File[]>([]);
const [isOpen, setIsOpen] = useState(false);
const [satkerData, setSatkerData] = useState<string[]>([]);
const [unitData, setUnitData] = useState<string[]>([]);
const [userList, setUserList] = useState<
{ id: string; name: string; isInternal: boolean }[]
>([]);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: { contentType: [], selectedUser: [], publishTo: [] },
});
const contentType = form.watch("contentType");
const isAllContentChecked = contentType && listContent.every((item) =>
contentType.includes(item.id)
);
const users = form.watch("selectedUser");
const isAllUserChecked = users && userList.every((item) => users.includes(item.id));
const target = form.watch("publishTo");
const isAllTargetChecked = target && publishToList.every((item) =>
target.includes(item.id)
);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
});
useEffect(() => {
getRoles();
if (Number(levelNumber) === 2) {
form.setValue("publishTo", ["polda"]);
setUnitData([String(userLevelId)]);
}
if (Number(levelNumber) === 3) {
form.setValue("publishTo", ["satker"]);
setSatkerData([String(userLevelId)]);
}
}, []);
async function getRoles() {
const response = await getUserRoles();
let dataRoles = response?.data?.data;
for (let i = 0; i < dataRoles.length; i++) {
dataRoles[i].id = String(dataRoles[i].id);
}
setUserList(dataRoles);
}
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
if (data.publishTo.includes("polda") || data.publishTo.includes("satker")) {
if (
(data.publishTo.includes("polda") && unitData?.length < 1) ||
(data.publishTo.includes("satker") && satkerData?.length < 1)
) {
const poldaValidation = data.publishTo.includes("polda");
const satkerValidation = data.publishTo.includes("satker");
toast({
title:
poldaValidation && satkerValidation
? "Pilih Polda dan Satker tujuan"
: `Pilih ${poldaValidation ? "Polda" : "Satker"} tujuan`,
variant: "destructive",
});
} else {
save(data);
}
} else {
save(data);
}
};
const save = async (data: z.infer<typeof FormSchema>) => {
const formMedia = new FormData();
loading();
const unit = unitData?.join(",");
const satker = satkerData?.join(",");
const join =
unitData?.length > 0 && satkerData?.length > 0
? unit + "," + satker
: unitData?.length > 0
? unit
: satkerData?.length > 0
? satker
: "";
formMedia.append("name", data.title);
formMedia.append("description", data.description);
formMedia.append("mediaTypes", data.contentType.join(","));
formMedia.append("publishedFor", data.selectedUser.join(","));
formMedia.append("file", files[0]);
formMedia.append("publishedLocation", data.publishTo.sort().join(","));
formMedia.append("publishedLocationLevel", join);
formMedia.append(
"isInt",
data.publishTo.includes("internasional") ? "true" : "false"
);
const response = await postCategory(formMedia);
close();
if (response?.error) {
toast({ title: stringify(response.message), variant: "destructive" });
return false;
}
toast({
title: "Succes",
description: "Kategori berhasil dibuat",
});
router.push("/admin/settings/category?dataChange=true");
setIsOpen(false);
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button color="primary" size="md">
{t("add-category", { defaultValue: "Add Category" })}
</Button>
</DialogTrigger>
<DialogContent
size="md"
className="sm:h-[300px] md:h-[300px] lg:h-[500px] overflow-y-auto"
>
<DialogHeader>
<DialogTitle> {t("add-category", { defaultValue: "Add Category" })}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white dark:bg-[#1f2937] rounded-sm"
>
<FormField
control={form.control}
name="contentType"
render={() => (
<FormItem>
<FormLabel>Tipe Konten</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex items-center gap-3">
<Checkbox
id="all"
checked={isAllContentChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"contentType",
listContent.map((item) => item.id)
);
} else {
form.setValue("contentType", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{listContent.map((item) => (
<FormField
key={item.id}
control={form.control}
name="contentType"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start"
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}
</FormLabel>
</div>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="selectedUser"
render={() => (
<FormItem>
<FormLabel>Target Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
checked={isAllUserChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"selectedUser",
userList.map((item) => item.id)
);
} else {
form.setValue("selectedUser", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{userList.map((item) => (
<FormField
key={item.id}
control={form.control}
name="selectedUser"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}
</FormLabel>
</div>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
{Number(levelNumber) === 1 ? (
<FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
checked={isAllTargetChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"publishTo",
publishToList.map((item) => item.id)
);
} else {
form.setValue("publishTo", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
isDetail={false}
sendDataToParent={(data) => setUnitData(data)}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
isDetail={false}
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
/>
)}
</>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
) : (
<FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox disabled checked />
<label htmlFor="all" className="text-sm">
{poldaState}
</label>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Kategori</FormLabel>
<Input
value={field.value}
placeholder="Masukkan Nama Kategori"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="file"
render={({ field }) => (
<FormItem>
<FormLabel>Thumbnail Category</FormLabel>
{files.length < 1 && (
<Fragment>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
Tarik file disini atau klik untuk upload.
</h4>
<div className=" text-xs text-muted-foreground">
( Upload file dengan format .jpg, .jpeg, atau .png.
Ukuran maksimal 100mb.)
</div>
</div>
</div>
</Fragment>
)}
{files.length > 0 && (
<div className="flex flex-row gap-2">
<img
src={URL.createObjectURL(files[0])}
className="w-[30%]"
alt="thumbnail"
/>
<a
onClick={() => handleRemoveFile(files[0])}
className="cursor-pointer"
>
<Icon icon="fa-solid:times" color="red" />
</a>
</div>
)}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Deskripsi</FormLabel>
<FormControl>
<Textarea placeholder="Deskripsi" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="submit"
color="primary"
size="md"
className="text-xs"
>
Tambah Kategori
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,661 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
getCategoryDetail,
getUserRoles,
postCategory,
} from "@/service/settings/settings";
import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal";
import { UnitMapping } from "./unit-mapping";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import Cookies from "js-cookie";
import { getCookiesDecrypt } from "@/lib/utils";
const FormSchema = z.object({
title: z.string({
required_error: "Required",
}),
description: z.string({
required_error: "Required",
}),
contentType: z
.array(z.string())
.refine((value) => value.some((item) => item), {
message: "Required",
}),
selectedUser: z
.array(z.string())
.refine((value) => value.some((item) => item), {
message: "Required",
}),
publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "Required",
}),
id: z.string({
required_error: "Required",
}),
file: z.string().optional(),
});
const listContent = [
{
id: "1",
name: "Foto",
},
{
id: "2",
name: "Audio Visual",
},
{
id: "3",
name: "Teks",
},
{
id: "4",
name: "Audio",
},
];
const publishToList = [
{
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function EditCategoryModal(props: {
id: string;
thumbnailLink: string;
isDetail?: boolean;
}) {
const { id, isDetail, thumbnailLink } = props;
const [files, setFiles] = useState<File[]>([]);
const [isOpen, setIsOpen] = useState(false);
const { toast } = useToast();
const roleId = getCookiesDecrypt("urie");
const levelNumber = getCookiesDecrypt("ulne");
const userLevelId = getCookiesDecrypt("ulie");
const poldaState = Cookies.get("state");
const router = useRouter();
const [satkerData, setSatkerData] = useState<string[]>([]);
const [unitData, setUnitData] = useState<string[]>([]);
const [userList, setUserList] = useState<
{ id: string; name: string; isInternal: boolean }[]
>([]);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: { contentType: [], selectedUser: [], publishTo: [] },
});
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
});
// useEffect(() => {
// initFetch();
// }, [id]);
const initFetch = async () => {
const req = await getCategoryDetail(id);
const data = req?.data?.data;
form.setValue("id", String(data?.id));
form.setValue("title", String(data?.name));
form.setValue("description", String(data?.description));
form.setValue("contentType", data?.mediaTypes?.split(",") || []);
form.setValue(
"selectedUser",
removeAndReturn(data?.publishedFor, [2, 3, 4])
);
form.setValue("publishTo", data?.publishedLocation?.split(",") || []);
form.setValue("file", thumbnailLink);
setUnitData(filterString(data?.publishedLocationLevel, "under"));
setSatkerData(filterString(data?.publishedLocationLevel, "above"));
};
function removeAndReturn(inputString: string, toRemove: number[]) {
const numbers = inputString?.split(",").map(Number);
const filteredNumbers = numbers?.filter((num) => !toRemove?.includes(num));
return filteredNumbers?.map(String);
}
function filterString(inputString: string, type: string) {
const numbers = inputString?.split(",").map(Number);
if (type === "above") {
const above700 = numbers?.filter((num) => num > 700);
return above700?.map(String);
} else {
const under700 = numbers?.filter((num) => num < 700);
return under700?.map(String);
}
}
const contentType = form.watch("contentType");
const isAllContentChecked =
contentType && listContent.every((item) => contentType.includes(item.id));
const users = form.watch("selectedUser");
const isAllUserChecked =
users && userList.every((item) => users.includes(item.id));
const target = form.watch("publishTo");
const isAllTargetChecked =
target && publishToList.every((item) => target.includes(item.id));
useEffect(() => {
getRoles();
}, []);
async function getRoles() {
const response = await getUserRoles();
let dataRoles = response?.data?.data;
for (let i = 0; i < dataRoles.length; i++) {
dataRoles[i].id = String(dataRoles[i].id);
}
setUserList(dataRoles);
}
function removeDuplicates(inputString: string): string {
const numbers = inputString.split(",");
const uniqueNumbers = Array.from(new Set(numbers));
return uniqueNumbers.join(",");
}
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
const formMedia = new FormData();
loading();
const unit = unitData?.join(",");
const satker = satkerData?.join(",");
const join =
unitData?.length > 0 && satkerData?.length > 0
? unit + "," + satker
: unitData?.length > 0
? unit
: satkerData?.length > 0
? satker
: "";
formMedia.append("id", data.id);
formMedia.append("name", data.title);
formMedia.append("description", data.description);
formMedia.append("mediaTypes", data.contentType.join(","));
formMedia.append("publishedFor", data.selectedUser.join(","));
formMedia.append("publishedLocation", data.publishTo.sort().join(","));
formMedia.append("publishedLocationLevel", removeDuplicates(join));
if (files?.length > 0) {
formMedia.append("file", files[0]);
}
const response = await postCategory(formMedia);
close();
if (response?.error == true) {
error(response.message);
return false;
}
toast({
title: "Succes",
description: "Kategori berhasil diubah",
});
router.push("/admin/settings/category?dataChange=true");
setIsOpen(false);
};
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger>
<a
onClick={() =>{
initFetch()
setIsOpen(true); } }
className="hover:underline cursor-pointer"
>
{isDetail ? "Detail" : "Edit"}
</a>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>Edit Kategori</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm"
>
<FormField
control={form.control}
name="contentType"
render={() => (
<FormItem>
<FormLabel>Tipe Konten</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex items-center gap-3">
<Checkbox
id="all"
checked={isAllContentChecked}
disabled={isDetail}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"contentType",
listContent.map((item) => item.id)
);
} else {
form.setValue("contentType", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{listContent.map((item) => (
<FormField
key={item.id}
control={form.control}
name="contentType"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start"
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}
</FormLabel>
</div>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="selectedUser"
render={() => (
<FormItem>
<FormLabel>Target Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
disabled={isDetail}
checked={isAllUserChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"selectedUser",
userList.map((item) => item.id)
);
} else {
form.setValue("selectedUser", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{userList.map((item) => (
<FormField
key={item.id}
control={form.control}
name="selectedUser"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}
</FormLabel>
</div>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
{Number(levelNumber) === 1 ? (
<FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
checked={isAllTargetChecked}
disabled={isDetail}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"publishTo",
publishToList.map((item) => item.id)
);
} else {
form.setValue("publishTo", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
sendDataToParent={(data) => setUnitData(data)}
isDetail={isDetail ? true : false}
initData={unitData}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
isDetail={isDetail ? true : false}
initData={satkerData}
/>
)}
</>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
) : (
<FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox disabled checked />
<label htmlFor="all" className="text-sm">
{poldaState}
</label>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Nama Kategori</FormLabel>
<Input
value={field.value}
readOnly={isDetail}
placeholder="Masukkan Nama Kategori"
onChange={field.onChange}
/>
<FormMessage />
</FormItem>
)}
/>
{isDetail ? (
<div className="flex flex-row gap-2">
<img src={thumbnailLink} className="w-[30%]" alt="thumbnail" />
</div>
) : (
<FormField
control={form.control}
name="file"
render={({ field }) => (
<FormItem>
<FormLabel>Thumbnail Category</FormLabel>
{files.length < 1 && field.value === "" && (
<Fragment>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
Tarik file disini atau klik untuk upload.
</h4>
<div className=" text-xs text-muted-foreground">
( Upload file dengan format .jpg, .jpeg, atau
.png. Ukuran maksimal 100mb.)
</div>
</div>
</div>
</Fragment>
)}
{field.value !== "" && (
<div className="flex flex-row gap-2">
<img
src={field.value}
className="w-[30%]"
alt="thumbnail"
/>
<a
onClick={() => form.setValue("file", "")}
className="cursor-pointer"
>
<Icon icon="fa-solid:times" color="red" />
</a>
</div>
)}
{files.length > 0 && (
<div className="flex flex-row gap-2">
<img
src={URL.createObjectURL(files[0])}
className="w-[30%]"
alt="thumbnail"
/>
<a
onClick={() => handleRemoveFile(files[0])}
className="cursor-pointer"
>
<Icon icon="fa-solid:times" color="red" />
</a>
</div>
)}
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Deskripsi</FormLabel>
<FormControl>
<Textarea
readOnly={isDetail}
placeholder="Deskripsi"
// className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!isDetail && (
<DialogFooter>
<Button
type="submit"
color="primary"
size="md"
className="text-xs"
>
Edit Kategori
</Button>
</DialogFooter>
)}
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,48 @@
"use client";
import { Switch } from "@/components/ui/switch";
import { error } from "@/config/swal";
import { useRouter } from "@/i18n/routing";
import { publishUnpublishCategory } from "@/service/settings/settings";
export default function StatusToogle(props: {
id: number;
initValue: boolean;
}) {
const { id, initValue } = props;
const router = useRouter();
// const publishCategory = async (id: number, status: string) => {
// const response = await publishUnpublishCategory(id, status);
// console.log(response);
// if (response?.error) {
// error(response.message);
// return false;
// }
// router.push("/admin/settings/category?dataChange=true");
// };
const publishCategory = async (id: number, status: string) => {
const response = await publishUnpublishCategory(id, status);
console.log("API Response:", response);
// cek error interceptor
if (response?.error) {
error(response.message || "Terjadi kesalahan");
return false;
}
// cek flag success asli dari backend
if (response?.data?.success === false) {
error(response?.data?.message || "Terjadi kesalahan");
return false;
}
router.push("/admin/settings/category?dataChange=true");
};
return (
<Switch
id={String(id)}
checked={initValue}
onCheckedChange={(e) => publishCategory(id, String(e))}
/>
);
}

View File

@ -0,0 +1,312 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link, useRouter } from "@/i18n/routing";
import { getCategories } from "@/service/settings/settings";
import CreateCategoryModal from "./create";
import { useTranslations } from "next-intl";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
const AdminCategoryTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations("Menu");
const dataChange = searchParams?.get("dataChange");
const [openModal, setOpenModal] = React.useState(false);
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [region, setRegion] = React.useState<any>();
const [regionFilter, setRegionFilter] = React.useState<string[]>([]);
const [statusFilter, setStatusFilter] = React.useState<string[]>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
const manualRegions = [
{ id: "semua", label: "Semua" },
{ id: "mabes", label: "Mabes" },
{ id: "polda", label: "Polda" },
{ id: "satker", label: "Satker" },
{ id: "internasional", label: "Internasional" },
];
React.useEffect(() => {
if (dataChange) {
router.push("/admin/settings/category");
}
fetchData();
}, [dataChange]);
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page, regionFilter]);
async function fetchData() {
try {
loading();
const regionQuery = regionFilter.length
? `&publishedLocation=${regionFilter.join(",")}`
: "";
const statusQuery = statusFilter.length
? `&isPublish=${statusFilter.join(",")}`
: "";
const response = await getCategories(
page - 1,
`${regionQuery}${statusQuery}`
);
const data = response?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * 10 + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
close();
} catch (error) {
console.error("Error fetching categories:", error);
}
}
const handleChange = (type: string, id: string, checked: boolean) => {
if (type === "region") {
if (id === "semua") {
if (checked) {
// Pilih semua (kecuali 'semua' itu sendiri)
setRegionFilter(["mabes", "polda", "satker", "internasional"]);
} else {
setRegionFilter([]);
}
} else {
let updated = checked
? [...regionFilter, id]
: regionFilter.filter((val) => val !== id);
// Jika semua sudah tercentang, maka otomatis centang "semua"
const allIds = ["mabes", "polda", "satker", "internasional"];
const allSelected = allIds.every((val) => updated.includes(val));
setRegionFilter(allSelected ? allIds : updated);
}
} else {
if (checked) {
setStatusFilter([...statusFilter, id]);
} else {
setStatusFilter(statusFilter.filter((val: any) => val !== id));
}
}
};
return (
<div className="w-full overflow-x-auto bg-white dark:bg-black p-4 rounded-sm space-y-3">
<div className="flex justify-between mb-10 items-center">
<p className="text-xl font-medium text-default-900">{t("category", { defaultValue: "Category" })}</p>
<CreateCategoryModal />
</div>
<div className="flex items-end justify-end">
<Popover>
<PopoverTrigger asChild>
<Button size="md" variant="outline">
Filter
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 ">
<div className="flex flex-col gap-2 px-2">
<div className="flex justify-between text-sm">
<p>Filter</p>
<a
onClick={() => fetchData()}
className="cursor-pointer text-primary"
>
Simpan
</a>
</div>
<div className="flex flex-col gap-1 overflow-auto max-h-[300px] text-xs custom-scrollbar-table">
<p>Wilayah</p>
{manualRegions.map((region) => (
<div key={region.id} className="flex items-center space-x-2">
<Checkbox
id={region.id}
checked={
region.id === "semua"
? ["mabes", "polda", "satker", "internasional"].every(
(val) => regionFilter.includes(val)
)
: regionFilter.includes(region.id)
}
onCheckedChange={(e) =>
handleChange("region", region.id, Boolean(e))
}
/>
<label
htmlFor={region.id}
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{region.label}
</label>
</div>
))}
<p className="mt-3">Status</p>
<div className="flex items-center space-x-2">
<Checkbox
id="aktif"
checked={statusFilter.includes("true")}
onCheckedChange={(e) =>
handleChange("status", "true", Boolean(e))
}
/>
<label
htmlFor="aktif"
className="text-xs font-medium leading-none"
>
Aktif
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="nonaktif"
checked={statusFilter.includes("false")}
onCheckedChange={(e) =>
handleChange("status", "false", Boolean(e))
}
/>
<label
htmlFor="nonaktif"
className="text-xs font-medium leading-none"
>
Non Aktif
</label>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default AdminCategoryTable;

View File

@ -0,0 +1,253 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useEffect, useState } from "react";
import { getUserLevelForAssignments } from "@/service/task";
const FormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "Required",
}),
});
interface UnitType {
id: number;
name: string;
subDestination: { id: number; name: string }[];
}
export function UnitMapping(props: {
unit: string;
sendDataToParent: (data: string[]) => void;
isDetail: boolean;
initData?: string[];
}) {
const { unit, sendDataToParent, isDetail } = props;
const [unitList, setUnitList] = useState<UnitType[]>([]);
const [satkerList, setSatkerList] = useState<{ id: number; name: string }[]>(
[]
);
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
items: props.initData ? props.initData : [],
},
});
useEffect(() => {
async function initState() {
const response = await getUserLevelForAssignments();
setupUnit(response?.data?.data.list);
}
initState();
}, []);
const unitType = form.watch("items");
const isAllUnitChecked = unitType && unitList.every((item) =>
unitType.includes(String(item.id))
);
const isAllSatkerChecked = unitType && satkerList.every((item) =>
unitType.includes(String(item.id))
);
const setupUnit = (data: UnitType[]) => {
const temp = data.filter((a) => a.name.includes("POLDA"));
const temp2 = data.filter((a) => a.name.includes("SATKER"));
setUnitList(temp);
setSatkerList(temp2[0].subDestination);
};
useEffect(() => {
sendDataToParent(form.getValues("items"));
}, [unitType]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<a
onClick={() => setIsOpen(true)}
className="text-primary cursor-pointer text-xs mr-3"
>
Pilih {unit}
</a>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{unit}</DialogTitle>
</DialogHeader>
{unit === "Polda" ? (
<Form {...form}>
<form className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<Checkbox
id={`all-${unit}`}
checked={isAllUnitChecked}
disabled={isDetail}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"items",
unitList.map((item) => String(item.id))
);
} else {
form.setValue("items", []);
}
}}
/>
<label htmlFor="all" className="text-sm text-black uppercase">
SEMUA {unit}
</label>
</div>
<FormField
control={form.control}
name="items"
render={() => (
<FormItem
className={`grid grid-cols-${unit === "Polda" ? "2" : "3"}`}
>
{unitList?.map((item: any) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={String(item.id)}
className="flex flex-row items-center space-x-3 space-y-0"
>
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(
String(item.id)
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
String(item.id),
])
: field.onChange(
field.value?.filter(
(value) => value !== String(item.id)
)
);
}}
/>
</FormControl>
<p className="text-sm text-black">{item.name}</p>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
) : (
<Form {...form}>
<form className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<Checkbox
id={`all-${unit}`}
checked={isAllSatkerChecked}
disabled={isDetail}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"items",
satkerList.map((item) => String(item.id))
);
} else {
form.setValue("items", []);
}
}}
/>
<label htmlFor="all" className="text-sm text-black uppercase">
SEMUA {unit}
</label>
</div>
<FormField
control={form.control}
name="items"
render={() => (
<FormItem
className={`grid grid-cols-${unit === "Polda" ? "2" : "3"}`}
>
{satkerList?.map((item: any) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => {
return (
<FormItem
key={String(item.id)}
className="flex flex-row items-center space-x-3 space-y-0"
>
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(
String(item.id)
)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
String(item.id),
])
: field.onChange(
field.value?.filter(
(value) => value !== String(item.id)
)
);
}}
/>
</FormControl>
<p className="text-sm text-black">{item.name}</p>
</FormItem>
);
}}
/>
))}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
)}
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,12 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import AdminCategoryTable from "./component/table";
export default function AdminCategory() {
return (
<>
<SiteBreadcrumb />
<AdminCategoryTable />
</>
);
}

View File

@ -0,0 +1,112 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Link, useRouter } from "@/i18n/routing";
import { error } from "@/config/swal";
import { deleteCategory, deleteDataFAQ } from "@/service/settings/settings";
import { useToast } from "@/components/ui/use-toast";
// import EditCategoryModal from "./edit";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Menubar,
MenubarContent,
MenubarMenu,
MenubarTrigger,
} from "@/components/ui/menubar";
import { htmlToString } from "@/utils/globals";
import EditFAQModal from "./edit";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "question",
header: "Pertanyaan",
cell: ({ row }) => (
<span className="normal-case">
{htmlToString(row.getValue("question"))}
</span>
),
},
{
accessorKey: "answer",
header: "Answer",
cell: ({ row }) => (
<span className="normal-case">
{htmlToString(row.getValue("answer"))}
</span>
),
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
const router = useRouter();
const { toast } = useToast();
const faqDelete = async (id: string) => {
const response = await deleteDataFAQ(id);
console.log(response);
if (response?.error) {
error(response.message);
return false;
}
toast({
title: "Sukses",
description: "Berhasil Delete",
});
router.push("/admin/settings/faq?dataChange=true");
};
return (
<Menubar className="border-none">
<MenubarMenu>
<MenubarTrigger>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</MenubarTrigger>
<MenubarContent className="flex flex-col gap-2 justify-center items-start p-4">
<EditFAQModal id={row.original.id} isDetail={true} />
<EditFAQModal id={row.original.id} isDetail={false} />
<a
onClick={() => faqDelete(row.original.id)}
className="hover:underline cursor-pointer hover:text-destructive"
>
Delete
</a>
</MenubarContent>
</MenubarMenu>
</Menubar>
);
},
},
];
export default columns;

View File

@ -0,0 +1,300 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
getUserRoles,
postCategory,
postDataFAQ,
} from "@/service/settings/settings";
import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal";
import { useToast } from "@/components/ui/use-toast";
import { stringify } from "querystring";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import Image from "next/image";
import { Upload } from "tus-js-client";
import { useTranslations } from "next-intl";
const FormSchema = z.object({
answer: z.string({
required_error: "Required",
}),
question: z.string({
required_error: "Required",
}),
publishTo: z.string({
required_error: "Required",
}),
// publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
// message: "Required",
// }),
});
const publishToList = [
{
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function CreateFAQModal() {
const router = useRouter();
const { toast } = useToast();
const t = useTranslations("Menu");
const [isOpen, setIsOpen] = useState(false);
const [satkerData, setSatkerData] = useState<string[]>([]);
const [unitData, setUnitData] = useState<string[]>([]);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: { publishTo: "wilayah" },
});
const target = form.watch("publishTo");
const isAllTargetChecked = target && publishToList.every((item) =>
target.includes(item.id)
);
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
const request = {
question: data.question,
answer: data.answer,
isInternational: data.publishTo === "wilayah" ? false : true,
isActive: true,
};
const response = await postDataFAQ(request);
close();
if (response?.error) {
toast({ title: stringify(response.message), variant: "destructive" });
return false;
}
toast({
title: "Succes",
description: "FAQ berhasil dibuat",
});
router.push("/admin/settings/faq?dataChange=true");
setIsOpen(false);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button color="primary" size="md">
{t("add", { defaultValue: "Add" })} FAQ
</Button>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{t("add", { defaultValue: "Add" })} FAQ</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm"
>
<FormField
control={form.control}
name="publishTo"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Wilayah Publish</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-row space-x-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="wilayah" />
</FormControl>
<FormLabel className="font-normal">Wilayah</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="international" />
</FormControl>
<FormLabel className="font-normal">
Internasional
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
checked={isAllTargetChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"publishTo",
publishToList.map((item) => item.id)
);
} else {
form.setValue("publishTo", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
isDetail={false}
sendDataToParent={(data) => setUnitData(data)}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
isDetail={false}
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
/>
)}
</>
))}
</div>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="question"
render={({ field }) => (
<FormItem>
<FormLabel>Pertanyaan</FormLabel>
<FormControl>
<Textarea placeholder="Masukkan pertanyaan" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="answer"
render={({ field }) => (
<FormItem>
<FormLabel>Jawaban</FormLabel>
<FormControl>
<Textarea placeholder="Masukkan jawaban" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="submit"
color="primary"
size="md"
className="text-xs"
>
Tambah FAQ
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,325 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
detailDataFAQ,
getUserRoles,
postCategory,
postDataFAQ,
} from "@/service/settings/settings";
import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal";
import { useToast } from "@/components/ui/use-toast";
import { stringify } from "querystring";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import Image from "next/image";
import { Upload } from "tus-js-client";
import { id } from "date-fns/locale";
import { htmlToString } from "@/utils/globals";
const FormSchema = z.object({
answer: z.string({
required_error: "Required",
}),
question: z.string({
required_error: "Required",
}),
publishTo: z.string({
required_error: "Required",
}),
// publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
// message: "Required",
// }),
});
const publishToList = [
{
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function EditFAQModal(props: { id: string; isDetail: boolean }) {
const { id, isDetail } = props;
const router = useRouter();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});
useEffect(() => {
initState();
}, [id]);
const initState = async () => {
const res = await detailDataFAQ(id);
const data = res?.data?.data;
form.setValue("question", htmlToString(data.question));
form.setValue("answer", htmlToString(data.answer));
form.setValue(
"publishTo",
data.isInternational ? "international" : "wilayah"
);
};
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
const request = {
id: Number(id),
question: data.question,
answer: data.answer,
isInternational: data.publishTo === "wilayah" ? false : true,
isActive: true,
};
const response = await postDataFAQ(request);
close();
if (response?.error) {
toast({ title: stringify(response.message), variant: "destructive" });
return false;
}
toast({
title: "Succes",
description: "FAQ berhasil diubah",
});
router.push("/admin/settings/faq?dataChange=true");
setIsOpen(false);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<a className="hover:underline cursor-pointer">
{isDetail ? "Detail" : "Edit"}
</a>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{isDetail ? "Detail" : "Edit"} FAQ</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm"
>
<FormField
control={form.control}
name="publishTo"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Wilayah Publish</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-row space-x-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="wilayah" />
</FormControl>
<FormLabel className="font-normal">Wilayah</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="international" />
</FormControl>
<FormLabel className="font-normal">
Internasional
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
disabled={isDetail}
checked={isAllTargetChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"publishTo",
publishToList.map((item) => item.id)
);
} else {
form.setValue("publishTo", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
isDetail={isDetail}
sendDataToParent={(data) => setUnitData(data)}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
isDetail={isDetail}
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
/>
)}
</>
))}
</div>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="question"
render={({ field }) => (
<FormItem>
<FormLabel>Pertanyaan</FormLabel>
<FormControl>
<Textarea
readOnly={isDetail}
placeholder="Masukkan pertanyaan"
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="answer"
render={({ field }) => (
<FormItem>
<FormLabel>Jawaban</FormLabel>
<FormControl>
<Textarea
readOnly={isDetail}
placeholder="Masukkan jawaban"
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!isDetail && (
<DialogFooter>
<Button
type="submit"
color="primary"
size="md"
className="text-xs"
>
Edit FAQ
</Button>
</DialogFooter>
)}
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,184 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link, useRouter } from "@/i18n/routing";
import { NewCampaignIcon } from "@/components/icon";
import { getCategories, getListFAQ } from "@/service/settings/settings";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import CreateFAQModal from "./create";
const AdminFAQTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const dataChange = searchParams?.get("dataChange");
const [openModal, setOpenModal] = React.useState(false);
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
if (dataChange) {
router.push("/admin/settings/faq");
}
fetchData();
}, [dataChange]);
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page]);
async function fetchData() {
try {
loading();
const response = await getListFAQ();
const data = response?.data?.data;
console.log("respone", response);
data.forEach((item: any, index: number) => {
item.no = (page - 1) * 10 + index + 1;
});
setDataTable(data);
setTotalData(data?.length);
setTotalPage(1);
close();
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
<div className="flex justify-between mb-10 items-center">
<p className="text-xl font-medium text-default-900">FAQ</p>
<CreateFAQModal />
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* <TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/> */}
</div>
);
};
export default AdminFAQTable;

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import AdminFAQTable from "./component/table";
export default function FAQSetting() {
return (
<div>
<SiteBreadcrumb />
<AdminFAQTable />
</div>
);
}

View File

@ -0,0 +1,121 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Link, useRouter } from "@/i18n/routing";
import { error } from "@/config/swal";
import { deleteCategory, deleteDataFAQ } from "@/service/settings/settings";
import { useToast } from "@/components/ui/use-toast";
// import EditCategoryModal from "./edit";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Menubar,
MenubarContent,
MenubarMenu,
MenubarTrigger,
} from "@/components/ui/menubar";
import { htmlToString } from "@/utils/globals";
import EditFAQModal from "./edit";
import EditFeedbackModal from "./edit";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "question",
header: "Poin Penilaian",
cell: ({ row }) => (
<span className="normal-case">
{htmlToString(row.getValue("question"))}
</span>
),
},
{
accessorKey: "isInternational",
header: "Wilayah Publish",
cell: ({ row }) => (
<span className="normal-case">
{row.getValue("isInternational") ? "Internasional" : "Wilayah"}
</span>
),
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
const router = useRouter();
const { toast } = useToast();
const faqDelete = async (id: string) => {
const response = await deleteDataFAQ(id);
console.log(response);
if (response?.error) {
error(response.message);
return false;
}
toast({
title: "Sukses",
description: "Berhasil Delete",
});
router.push("/admin/settings/feedback?dataChange=true");
};
return (
<Menubar className="border-none">
<MenubarMenu>
<MenubarTrigger>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</MenubarTrigger>
<MenubarContent className="flex flex-col gap-2 justify-center items-start p-4">
<EditFeedbackModal
id={row.original.id}
isDetail={true}
data={row.original}
/>
<EditFeedbackModal
id={row.original.id}
isDetail={false}
data={row.original}
/>
<a
onClick={() => faqDelete(row.original.id)}
className="hover:underline cursor-pointer hover:text-destructive"
>
Delete
</a>
</MenubarContent>
</MenubarMenu>
</Menubar>
);
},
},
];
export default columns;

View File

@ -0,0 +1,276 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
getUserRoles,
postCategory,
postDataFAQ,
postDataFeedback,
} from "@/service/settings/settings";
import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal";
import { useToast } from "@/components/ui/use-toast";
import { stringify } from "querystring";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import Image from "next/image";
import { Upload } from "tus-js-client";
import { useTranslations } from "next-intl";
const FormSchema = z.object({
question: z.string({
required_error: "Required",
}),
publishTo: z.string({
required_error: "Required",
}),
// publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
// message: "Required",
// }),
});
const publishToList = [
{
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function CreateFAQModal() {
const router = useRouter();
const { toast } = useToast();
const t = useTranslations("Menu");
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: { publishTo: "wilayah" },
});
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
const request = {
question: data.question,
isInternational: data.publishTo === "wilayah" ? false : true,
isActive: true,
};
const response = await postDataFeedback(request);
close();
if (response?.error) {
toast({ title: stringify(response.message), variant: "destructive" });
return false;
}
toast({
title: "Succes",
description: "Feedback berhasil dibuat",
});
router.push("/admin/settings/feedback?dataChange=true");
setIsOpen(false);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button color="primary" size="md">
{t("add", { defaultValue: "Add" })} Feedback
</Button>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{t("add", { defaultValue: "Add" })} Feedback</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm"
>
<FormField
control={form.control}
name="publishTo"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Wilayah Publish</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-row space-x-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="wilayah" />
</FormControl>
<FormLabel className="font-normal">Wilayah</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="international" />
</FormControl>
<FormLabel className="font-normal">
Internasional
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
checked={isAllTargetChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"publishTo",
publishToList.map((item) => item.id)
);
} else {
form.setValue("publishTo", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
isDetail={false}
sendDataToParent={(data) => setUnitData(data)}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
isDetail={false}
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
/>
)}
</>
))}
</div>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="question"
render={({ field }) => (
<FormItem>
<FormLabel>Pertanyaan</FormLabel>
<FormControl>
<Textarea placeholder="Masukkan pertanyaan" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="submit"
color="primary"
size="md"
className="text-xs"
>
Tambah Feeback
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,311 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useRouter } from "@/i18n/routing";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
detailDataFAQ,
detailDataFeedback,
getUserRoles,
postCategory,
postDataFAQ,
postDataFeedback,
} from "@/service/settings/settings";
import { Fragment, useEffect, useState } from "react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/config/swal";
import { useToast } from "@/components/ui/use-toast";
import { stringify } from "querystring";
import { useDropzone } from "react-dropzone";
import { CloudUpload } from "lucide-react";
import Image from "next/image";
import { Upload } from "tus-js-client";
import { id } from "date-fns/locale";
import { htmlToString } from "@/utils/globals";
const FormSchema = z.object({
question: z.string({
required_error: "Required",
}),
publishTo: z.string({
required_error: "Required",
}),
// publishTo: z.array(z.string()).refine((value) => value.some((item) => item), {
// message: "Required",
// }),
});
const publishToList = [
{
id: "mabes",
name: "Nasional",
},
{
id: "polda",
name: "Polda",
},
{
id: "satker",
name: "Satker",
},
{
id: "internasional",
name: "Internasional",
},
];
export default function EditFeedbackModal(props: {
id: string;
isDetail: boolean;
data: {
id: number;
question: string;
isInternational: boolean;
isActive: boolean;
};
}) {
const { id, isDetail, data } = props;
const router = useRouter();
const { toast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});
useEffect(() => {
initState();
}, [id]);
const initState = async () => {
form.setValue("question", htmlToString(data.question));
form.setValue(
"publishTo",
data.isInternational ? "international" : "wilayah"
);
};
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
const request = {
id: Number(id),
question: data.question,
isInternational: data.publishTo === "wilayah" ? false : true,
isActive: true,
};
const response = await postDataFeedback(request);
close();
if (response?.error) {
toast({ title: stringify(response.message), variant: "destructive" });
return false;
}
toast({
title: "Succes",
description: "Feedback berhasil diubah",
});
router.push("/admin/settings/feedback?dataChange=true");
setIsOpen(false);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<a className="hover:underline cursor-pointer">
{isDetail ? "Detail" : "Edit"}
</a>
</DialogTrigger>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>{isDetail ? "Detail" : "Edit"} Feedback</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-3 bg-white rounded-sm"
>
<FormField
control={form.control}
name="publishTo"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Wilayah Publish</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="flex flex-row space-x-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="wilayah" />
</FormControl>
<FormLabel className="font-normal">Wilayah</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="international" />
</FormControl>
<FormLabel className="font-normal">
Internasional
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
control={form.control}
name="publishTo"
render={() => (
<FormItem>
<FormLabel>Wilayah Publish</FormLabel>
<div className="flex flex-row items-center gap-2">
<div className="flex gap-3 items-center">
<Checkbox
id="all"
disabled={isDetail}
checked={isAllTargetChecked}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"publishTo",
publishToList.map((item) => item.id)
);
} else {
form.setValue("publishTo", []);
}
}}
/>
<label htmlFor="all" className="text-sm">
Semua
</label>
</div>
{publishToList.map((item) => (
<>
<FormField
key={item.id}
control={form.control}
name="publishTo"
render={({ field }) => {
return (
<FormItem
key={item.id}
className="flex flex-row items-start "
>
<div className="flex items-center gap-3">
<FormControl>
<Checkbox
disabled={isDetail}
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([
...field.value,
item.id,
])
: field.onChange(
field.value?.filter(
(value) => value !== item.id
)
);
}}
/>
</FormControl>
<FormLabel className="font-normal">
{item.name}{" "}
</FormLabel>
</div>
</FormItem>
);
}}
/>
{item.id === "polda" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
unit="Polda"
isDetail={isDetail}
sendDataToParent={(data) => setUnitData(data)}
/>
)}
{item.id === "satker" &&
form.getValues("publishTo")?.includes(item.id) && (
<UnitMapping
isDetail={isDetail}
unit="Satker"
sendDataToParent={(data) => setSatkerData(data)}
/>
)}
</>
))}
</div>
<FormMessage />
</FormItem>
)}
/> */}
<FormField
control={form.control}
name="question"
render={({ field }) => (
<FormItem>
<FormLabel>Pertanyaan</FormLabel>
<FormControl>
<Textarea
readOnly={isDetail}
placeholder="Masukkan pertanyaan"
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!isDetail && (
<DialogFooter>
<Button
type="submit"
color="primary"
size="md"
className="text-xs"
>
Edit Feedback
</Button>
</DialogFooter>
)}
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,183 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./column";
import { listEnableCategory } from "@/service/content/content";
import { Checkbox } from "@/components/ui/checkbox";
import { close, loading } from "@/config/swal";
import { Link, useRouter } from "@/i18n/routing";
import { NewCampaignIcon } from "@/components/icon";
import {
getCategories,
getListFAQ,
getListFeedback,
} from "@/service/settings/settings";
import CreateFAQModal from "./create";
import { useTranslations } from "next-intl";
const AdminFeedbackTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const dataChange = searchParams?.get("dataChange");
const [openModal, setOpenModal] = React.useState(false);
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
if (dataChange) {
router.push("/admin/settings/feedback");
}
fetchData();
}, [dataChange]);
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page]);
async function fetchData() {
try {
loading();
const response = await getListFeedback();
const data = response?.data?.data;
console.log("respone", response);
data.forEach((item: any, index: number) => {
item.no = (page - 1) * 10 + index + 1;
});
setDataTable(data);
setTotalData(data?.length);
setTotalPage(1);
close();
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
return (
<div className="w-full overflow-x-auto bg-white p-4 rounded-sm space-y-3">
<div className="flex justify-between mb-10 items-center">
<p className="text-xl font-medium text-default-900">Feedback</p>
<CreateFAQModal />
</div>
<Table className="overflow-hidden">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* <TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/> */}
</div>
);
};
export default AdminFeedbackTable;

View File

@ -0,0 +1,11 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import AdminFeedbackTable from "./component/table";
export default function FAQSetting() {
return (
<div>
<SiteBreadcrumb />
<AdminFeedbackTable />
</div>
);
}

View File

@ -0,0 +1,189 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import { deleteAdvertisements, setBanner } from "@/service/settings/settings";
import { error } from "@/config/swal";
import { useToast } from "@/components/ui/use-toast";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { deleteMedia } from "@/service/content/content";
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: "No",
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: "Judul",
cell: ({ row }) => <div className="w-[150px]">{row.getValue("title")}</div>,
},
{
accessorKey: "contentFileName",
header: "Judul Gambar",
cell: ({ row }) => (
<div className="w-[450px]">{row.getValue("contentFileName")}</div>
),
},
{
accessorKey: "placements",
header: "Posisi",
cell: ({ row }) => (
<div className="w-[150px]">{row.getValue("placements")}</div>
),
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => <span>{row.getValue("statusName")}</span>,
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
const levelNumber = getCookiesDecrypt("ulne");
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const response = await deleteAdvertisements(id);
if (response?.error) {
error(response.message);
return false;
}
success();
}
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
}
const handleDeleteAdvertisements = (id: any) => {
MySwal.fire({
title: "Hapus Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
{/* <DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/settings/iklan/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
<Link href={`/admin/settings/iklan/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => handleDeleteAdvertisements(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
</DropdownMenuContent> */}
<DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/settings/iklan/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{levelNumber === "1" && (
<>
<Link href={`/admin/settings/iklan/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => handleDeleteAdvertisements(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,110 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
formatDateToIndonesian,
getOnlyDate,
htmlToString,
} from "@/utils/globals";
import { Link, useRouter } from "@/i18n/routing";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import StatusToogle from "./status-toogle";
import StaticToogle from "./static-toogle";
const columns: ColumnDef<any>[] = [
// {
// accessorKey: "no",
// header: "No",
// cell: ({ row }) => <span>{row.getValue("no")}</span>,
// },
{
accessorKey: "title",
header: "Judul",
cell: ({ row }) => <span>{row.getValue("title")}</span>,
},
{
accessorKey: "categoryName",
header: "Kategori",
cell: ({ row }) => <span>{row.getValue("categoryName")}</span>,
},
{
accessorKey: "createdAt",
header: "Tanggal Unggah",
cell: ({ row }) => (
<span>{formatDateToIndonesian(row.getValue("createdAt"))}</span>
),
},
{
accessorKey: "isStaticBanner",
header: "Static Banner",
cell: ({ row }) => (
<StaticToogle
id={row.original.id}
initChecked={row.original.isStaticBanner}
/>
),
},
{
accessorKey: "statusName",
header: "Status Banner",
cell: ({ row }) => (
<StatusToogle id={row.original.id} initChecked={row.original.isBanner} />
),
},
{
id: "actions",
accessorKey: "action",
header: "Actions",
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Link
href={`/contributor/content/image/detail/${row.original.id}`}
>
Detail
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default columns;

View File

@ -0,0 +1,153 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useSearchParams } from "next/navigation";
import { close, loading } from "@/config/swal";
import { Link, useRouter } from "@/i18n/routing";
import columns from "./popup-column";
import { listBanner, listStaticBanner } from "@/service/settings/settings";
const IklanListTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [showData, setShowData] = React.useState("10");
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [getData, setGetData] = React.useState<any>([]);
const dataChange = searchParams?.get("dataChange");
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const table = useReactTable({
data: getData,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
if (dataChange) {
router.push("/admin/settings/banner");
}
getListBanner();
}, [dataChange]);
React.useEffect(() => {
getListBanner();
// getListStaticBanner();
}, [page, showData]);
async function getListBanner() {
loading();
let temp: any;
const response = await listBanner();
const data = response?.data?.data?.content;
console.log("banner", data);
setGetData(data);
close();
}
return (
<>
<Table className="overflow-hidden mt-10">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</>
);
};
export default IklanListTable;

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