diff --git a/.gitignore b/.gitignore index aedc945..67193fb 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,5 @@ dump.rdb .~lock.* node_modules -data/Documents \ No newline at end of file +data/Documents +deploy/target.dev-home.yaml \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-33-12-565Z.yml b/.playwright-cli/page-2026-07-12T20-33-12-565Z.yml new file mode 100644 index 0000000..00ab721 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-33-12-565Z.yml @@ -0,0 +1,39 @@ +- generic [ref=e3]: + - generic [ref=e4]: + - img "HH Logo" [ref=e6] + - heading "Welcome to PX360" [level=1] [ref=e7] + - paragraph [ref=e8]: Patient Experience Management System + - generic [ref=e10]: + - generic [ref=e11]: + - generic [ref=e12]: + - img [ref=e13] + - text: Email Address + - generic [ref=e16]: + - img [ref=e17] + - textbox "Email Address" [active] [ref=e20]: + - /placeholder: Enter your email + - generic [ref=e21]: + - generic [ref=e22]: + - img [ref=e23] + - text: Password + - generic [ref=e26]: + - img [ref=e27] + - textbox "Password" [ref=e31]: + - /placeholder: Enter your password + - button [ref=e32] [cursor=pointer]: + - img [ref=e33] + - generic [ref=e36]: + - generic [ref=e37] [cursor=pointer]: + - checkbox "Remember me" [ref=e38] + - generic [ref=e39]: Remember me + - link "Forgot password?" [ref=e40] [cursor=pointer]: + - /url: /accounts/password/reset/ + - button "Sign In" [ref=e41] [cursor=pointer]: + - img [ref=e42] + - text: Sign In + - generic [ref=e45]: + - paragraph [ref=e46]: + - text: Secure login powered by + - link "tenhal.sa" [ref=e47] [cursor=pointer]: + - /url: https://tenhal.sa + - paragraph [ref=e48]: © 2026 Al Hammadi Hospital \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-33-40-832Z.yml b/.playwright-cli/page-2026-07-12T20-33-40-832Z.yml new file mode 100644 index 0000000..4fd56af --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-33-40-832Z.yml @@ -0,0 +1,41 @@ +- generic [ref=e3]: + - generic [ref=e4]: + - img "HH Logo" [ref=e6] + - heading "Welcome to PX360" [level=1] [ref=e7] + - paragraph [ref=e8]: Patient Experience Management System + - generic [ref=e10]: + - generic [ref=e11]: + - generic [ref=e12]: + - img [ref=e49] + - text: Email Address + - generic [ref=e16]: + - img [ref=e52] + - textbox "Email Address" [ref=e20]: + - /placeholder: Enter your email + - text: e2e-px-employee@px360.test + - generic [ref=e21]: + - generic [ref=e22]: + - img [ref=e55] + - text: Password + - generic [ref=e26]: + - img [ref=e58] + - textbox "Password" [ref=e31]: + - /placeholder: Enter your password + - text: Dev@123456 + - button [active] [ref=e32] [cursor=pointer]: + - img [ref=e62] + - generic [ref=e36]: + - generic [ref=e37] [cursor=pointer]: + - checkbox "Remember me" [ref=e38] + - generic [ref=e39]: Remember me + - link "Forgot password?" [ref=e40] [cursor=pointer]: + - /url: /accounts/password/reset/ + - button "Sign In" [ref=e41] [cursor=pointer]: + - img [ref=e67] + - text: Sign In + - generic [ref=e45]: + - paragraph [ref=e46]: + - text: Secure login powered by + - link "tenhal.sa" [ref=e47] [cursor=pointer]: + - /url: https://tenhal.sa + - paragraph [ref=e48]: © 2026 Al Hammadi Hospital \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-33-55-371Z.yml b/.playwright-cli/page-2026-07-12T20-33-55-371Z.yml new file mode 100644 index 0000000..892fd0b --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-33-55-371Z.yml @@ -0,0 +1,227 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Departments" [ref=e141] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e142] + - generic [ref=e144]: Contact Center Department + - generic [ref=e147]: + - generic [ref=e148]: + - img [ref=e150] + - generic [ref=e154]: + - heading "Contact Center Department" [level=1] [ref=e155] + - paragraph [ref=e156]: قسم مركز الاتصال + - generic [ref=e157]: + - generic [ref=e158]: + - img [ref=e159] + - text: hh_n_contact_center_department + - generic [ref=e162]: Administrative + - generic [ref=e163]: + - img [ref=e164] + - text: E2E Test Hospital + - generic [ref=e168]: + - generic [ref=e170]: + - img [ref=e172] + - generic [ref=e177]: + - paragraph [ref=e178]: Staff + - paragraph [ref=e179]: "4" + - link "Open Complaints 3 3 total" [ref=e180] [cursor=pointer]: + - /url: /organizations/departments/d810102c-d14a-4661-8672-6ef85a77fc8f/complaints/?status=open + - generic [ref=e181]: + - img [ref=e183] + - generic [ref=e186]: + - paragraph [ref=e187]: Open Complaints + - paragraph [ref=e188]: "3" + - paragraph [ref=e189]: 3 total + - link "Pending Inquiries 0 2 total" [ref=e190] [cursor=pointer]: + - /url: /organizations/departments/d810102c-d14a-4661-8672-6ef85a77fc8f/inquiries/?status=open + - generic [ref=e191]: + - img [ref=e193] + - generic [ref=e196]: + - paragraph [ref=e197]: Pending Inquiries + - paragraph [ref=e198]: "0" + - paragraph [ref=e199]: 2 total + - link "Open Observations 1 1 total" [ref=e200] [cursor=pointer]: + - /url: /organizations/departments/d810102c-d14a-4661-8672-6ef85a77fc8f/observations/?status=open + - generic [ref=e201]: + - img [ref=e203] + - generic [ref=e206]: + - paragraph [ref=e207]: Open Observations + - paragraph [ref=e208]: "1" + - paragraph [ref=e209]: 1 total + - generic [ref=e210]: + - generic [ref=e211]: + - img [ref=e213] + - generic [ref=e215]: + - heading "Pending Actions" [level=3] [ref=e216] + - paragraph [ref=e217]: 3 items awaiting response + - table [ref=e219]: + - rowgroup [ref=e220]: + - row "Type Reference Subject SLA Deadline Action" [ref=e221]: + - columnheader "Type" [ref=e222] + - columnheader "Reference" [ref=e223] + - columnheader "Subject" [ref=e224] + - columnheader "SLA Deadline" [ref=e225] + - columnheader "Action" [ref=e226] + - rowgroup [ref=e227]: + - row "Complaint Response CMP-202607-0006 Test Complaint - Analysis Required - Respond" [ref=e228]: + - cell "Complaint Response" [ref=e229]: + - generic [ref=e230]: Complaint Response + - cell "CMP-202607-0006" [ref=e231] + - cell "Test Complaint - Analysis Required" [ref=e232] + - cell "-" [ref=e233] + - cell "Respond" [ref=e234]: + - link "Respond" [ref=e236] [cursor=pointer]: + - /url: /complaints/f8aa6216-bb94-455e-ba18-d16a01024662/explain/dM0ilaUpzA1OuAceyjvwV9oxYA0geW_jsgipvrYUt_g/ + - img [ref=e237] + - text: Respond + - row "Complaint Explanation CMP-202607-0005 Test Complaint - No Details - Respond" [ref=e239]: + - cell "Complaint Explanation" [ref=e240]: + - generic [ref=e241]: Complaint Explanation + - cell "CMP-202607-0005" [ref=e242] + - cell "Test Complaint - No Details" [ref=e243] + - cell "-" [ref=e244] + - cell "Respond" [ref=e245]: + - link "Respond" [ref=e247] [cursor=pointer]: + - /url: /complaints/9ca01d52-712d-4bca-b30d-6f1f818ff839/explain/iBIf0PrMq1RZYSEIDxSn4WZik4PFk6dunoNwarFTnXI/ + - img [ref=e248] + - text: Respond + - row "Complaint Explanation CMP-202607-0005 Test Complaint - No Details - Respond" [ref=e252]: + - cell "Complaint Explanation" [ref=e253]: + - generic [ref=e254]: Complaint Explanation + - cell "CMP-202607-0005" [ref=e255] + - cell "Test Complaint - No Details" [ref=e256] + - cell "-" [ref=e257] + - cell "Respond" [ref=e258]: + - link "Respond" [ref=e260] [cursor=pointer]: + - /url: /complaints/9ca01d52-712d-4bca-b30d-6f1f818ff839/explain/X1Zd54Tb1NcExZeRr6ACN5RtPCwB_RTKeqNeFcVP24E/ + - img [ref=e261] + - text: Respond + - generic [ref=e266]: + - button "Dashboard" [ref=e267] [cursor=pointer]: + - img [ref=e268] + - text: Dashboard + - button "Staff (4)" [ref=e270] [cursor=pointer]: + - img [ref=e271] + - text: Staff (4) + - button "Roles" [ref=e276] [cursor=pointer]: + - img [ref=e277] + - text: Roles + - button "Sections (2)" [ref=e289] [cursor=pointer]: + - img [ref=e290] + - text: Sections (2) + - button "Complaints (3)" [ref=e294] [cursor=pointer]: + - img [ref=e295] + - text: Complaints (3) + - button "Inquiries (2)" [ref=e298] [cursor=pointer]: + - img [ref=e299] + - text: Inquiries (2) + - button "Observations (1)" [ref=e302] [cursor=pointer]: + - img [ref=e303] + - text: Observations (1) + - button "Suggestions (1)" [ref=e306] [cursor=pointer]: + - img [ref=e307] + - text: Suggestions (1) + - button "Appreciations (2)" [ref=e309] [cursor=pointer]: + - img [ref=e310] + - text: Appreciations (2) + - button "Standards (0)" [ref=e312] [cursor=pointer]: + - img [ref=e313] + - text: Standards (0) + - button "Projects (8)" [ref=e316] [cursor=pointer]: + - img [ref=e317] + - text: Projects (8) + - paragraph [ref=e324]: Loading analytics... + - contentinfo [ref=e325]: + - paragraph [ref=e326]: + - text: Powered by + - link "tenhal.sa" [ref=e327] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-34-06-540Z.yml b/.playwright-cli/page-2026-07-12T20-34-06-540Z.yml new file mode 100644 index 0000000..1f12389 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-34-06-540Z.yml @@ -0,0 +1,258 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Inquiries Console" [level=1] [ref=e142] + - paragraph [ref=e143]: Manage patient inquiries and requests + - link "New Inquiry" [ref=e144] [cursor=pointer]: + - /url: /inquiries/new/ + - img [ref=e145] + - text: New Inquiry + - generic [ref=e146]: + - generic [ref=e147]: + - img [ref=e150] + - generic [ref=e153]: + - heading "Total Inquiries" [level=6] [ref=e154] + - heading "5" [level=2] [ref=e155] + - generic [ref=e156]: + - img [ref=e159] + - generic [ref=e162]: + - heading "Resolved" [level=6] [ref=e163] + - heading "2 (40.0%)" [level=2] [ref=e164] + - generic [ref=e165]: + - img [ref=e168] + - generic [ref=e171]: + - heading "In Progress" [level=6] [ref=e172] + - heading "1" [level=2] [ref=e173] + - generic [ref=e174]: + - img [ref=e177] + - generic [ref=e179]: + - heading "Overdue" [level=6] [ref=e180] + - heading "0" [level=2] [ref=e181] + - generic [ref=e182]: + - generic [ref=e183]: + - img [ref=e185] + - heading "Filters" [level=5] [ref=e186] + - generic [ref=e188]: + - generic [ref=e189]: + - generic [ref=e190]: Search + - textbox "Subject, contact name..." [ref=e191] + - generic [ref=e192]: + - generic [ref=e193]: Status + - combobox [ref=e194]: + - option "All Status" [selected] + - option "Open" + - option "In Progress" + - option "Resolved" + - option "Closed" + - generic [ref=e195]: + - generic [ref=e196]: Category + - combobox [ref=e197]: + - option "All Categories" [selected] + - option "General" + - option "Appointments" + - option "Billing" + - option "Medical Records" + - option "Pharmacy" + - option "Insurance" + - option "Feedback" + - option "Other" + - button "Filter" [ref=e198] [cursor=pointer]: + - img [ref=e199] + - text: Filter + - generic [ref=e202]: + - generic [ref=e203]: + - generic [ref=e204]: + - img [ref=e206] + - heading "All Inquiries" [level=5] [ref=e209] + - generic [ref=e210]: "Total: 5" + - paragraph [ref=e211]: "Showing: 1-5 of 5" + - generic [ref=e212]: + - table [ref=e213]: + - rowgroup [ref=e214]: + - row "Reference Subject Contact Category Status Priority Created Assigned" [ref=e215]: + - columnheader "Reference" [ref=e216] + - columnheader "Subject" [ref=e217] + - columnheader "Contact" [ref=e218] + - columnheader "Category" [ref=e219] + - columnheader "Status" [ref=e220] + - columnheader "Priority" [ref=e221] + - columnheader "Created" [ref=e222] + - columnheader "Assigned" [ref=e223] + - rowgroup [ref=e224]: + - row "INQ-202607-0007 test12121212 ismail mosa 0569874563 General Information Open Medium Jul 12, 2026 Unassigned" [ref=e225] [cursor=pointer]: + - cell "INQ-202607-0007" [ref=e226]: + - code [ref=e227]: INQ-202607-0007 + - cell "test12121212" [ref=e228]: + - link "test12121212" [ref=e229]: + - /url: /inquiries/b4a65498-e8d8-403c-82bb-ae09adc7fc1c/ + - cell "ismail mosa 0569874563" [ref=e230]: + - generic [ref=e231]: ismail mosa + - generic [ref=e232]: "0569874563" + - cell "General Information" [ref=e233] + - cell "Open" [ref=e234] + - cell "Medium" [ref=e235] + - cell "Jul 12, 2026" [ref=e236] + - cell "Unassigned" [ref=e237] + - row "INQ-202607-0006 test12121212 test 0569874563 General Information Open Medium Jul 12, 2026 Unassigned" [ref=e238] [cursor=pointer]: + - cell "INQ-202607-0006" [ref=e239]: + - code [ref=e240]: INQ-202607-0006 + - cell "test12121212" [ref=e241]: + - link "test12121212" [ref=e242]: + - /url: /inquiries/1c3eda08-ff03-4521-bcb8-ec137c79b417/ + - cell "test 0569874563" [ref=e243]: + - generic [ref=e244]: test + - generic [ref=e245]: "0569874563" + - cell "General Information" [ref=e246] + - cell "Open" [ref=e247] + - cell "Medium" [ref=e248] + - cell "Jul 12, 2026" [ref=e249] + - cell "Unassigned" [ref=e250] + - row "INQ-202607-0005 test test 0569874589 General Information In Progress Medium Jul 09, 2026 Unassigned" [ref=e251] [cursor=pointer]: + - cell "INQ-202607-0005" [ref=e252]: + - code [ref=e253]: INQ-202607-0005 + - cell "test" [ref=e254]: + - link "test" [ref=e255]: + - /url: /inquiries/c05309c2-3518-46ae-9b1c-c144acdb5d58/ + - cell "test 0569874589" [ref=e256]: + - generic [ref=e257]: test + - generic [ref=e258]: "0569874589" + - cell "General Information" [ref=e259] + - cell "In Progress" [ref=e260] + - cell "Medium" [ref=e261] + - cell "Jul 09, 2026" [ref=e262] + - cell "Unassigned" [ref=e263] + - row "INQ-202607-0004 hi ismail 0569874589 General Information Resolved Medium Jul 09, 2026 ismail mosa" [ref=e264] [cursor=pointer]: + - cell "INQ-202607-0004" [ref=e265]: + - code [ref=e266]: INQ-202607-0004 + - cell "hi" [ref=e267]: + - link "hi" [ref=e268]: + - /url: /inquiries/d1df0b83-5c16-4f51-b735-e89f612deb50/ + - cell "ismail 0569874589" [ref=e269]: + - generic [ref=e270]: ismail + - generic [ref=e271]: "0569874589" + - cell "General Information" [ref=e272] + - cell "Resolved" [ref=e273] + - cell "Medium" [ref=e274] + - cell "Jul 09, 2026" [ref=e275] + - cell "ismail mosa" [ref=e276] + - row "INQ-202607-0003 test test 0569874589 General Information Resolved Medium Jul 08, 2026 ismail mosa" [ref=e277] [cursor=pointer]: + - cell "INQ-202607-0003" [ref=e278]: + - code [ref=e279]: INQ-202607-0003 + - cell "test" [ref=e280]: + - link "test" [ref=e281]: + - /url: /inquiries/031cbd06-e2dc-4cd9-9b81-b1218e090d6c/ + - cell "test 0569874589" [ref=e282]: + - generic [ref=e283]: test + - generic [ref=e284]: "0569874589" + - cell "General Information" [ref=e285] + - cell "Resolved" [ref=e286] + - cell "Medium" [ref=e287] + - cell "Jul 08, 2026" [ref=e288] + - cell "ismail mosa" [ref=e289] + - generic [ref=e290]: + - generic [ref=e291]: + - generic [ref=e292]: Showing 1-5 of 5 + - generic [ref=e293]: + - generic [ref=e294]: Show + - combobox [ref=e295]: + - option "10" + - option "25" [selected] + - option "50" + - option "100" + - generic [ref=e296]: + - img [ref=e298] + - generic [ref=e300]: "1" + - img [ref=e302] + - contentinfo [ref=e304]: + - paragraph [ref=e305]: + - text: Powered by + - link "tenhal.sa" [ref=e306] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-34-28-605Z.yml b/.playwright-cli/page-2026-07-12T20-34-28-605Z.yml new file mode 100644 index 0000000..178fa5d --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-34-28-605Z.yml @@ -0,0 +1,172 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Inquiries" [ref=e141] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e142] + - generic [ref=e144]: INQ-202607-0007 + - generic [ref=e145]: Open + - generic [ref=e146]: Medium + - generic [ref=e147]: + - img [ref=e148] + - text: Not Sent to Dept + - generic [ref=e150]: + - heading "test12121212" [level=1] [ref=e151] + - generic [ref=e153]: + - img [ref=e154] + - text: 2026-07-12 + - generic [ref=e157]: + - generic [ref=e158]: + - img [ref=e160] + - generic [ref=e162]: Created + - generic [ref=e164]: + - img [ref=e166] + - generic [ref=e169]: Assigned + - generic [ref=e171]: + - img [ref=e173] + - generic [ref=e176]: Sent to Dept + - generic [ref=e178]: + - img [ref=e180] + - generic [ref=e182]: Dept Response + - generic [ref=e184]: + - img [ref=e186] + - generic [ref=e189]: Resolved + - generic [ref=e190]: + - generic [ref=e191]: + - img [ref=e193] + - generic [ref=e195]: + - paragraph [ref=e196]: Activate this inquiry + - paragraph [ref=e197]: Activation moves it to In Progress so actions become available. + - generic [ref=e198]: + - button "Cancel" [ref=e200] [cursor=pointer] + - button "Activate" [ref=e202] [cursor=pointer] + - navigation [ref=e203]: + - button "Details" [ref=e204] [cursor=pointer] + - button "Timeline" [ref=e205] [cursor=pointer] + - button "Notes" [ref=e206] [cursor=pointer] + - main [ref=e207]: + - generic [ref=e209]: + - generic [ref=e210]: + - generic [ref=e211]: + - paragraph + - paragraph [ref=e212]: "231313131" + - paragraph + - generic [ref=e213]: + - generic [ref=e215]: + - img [ref=e216] + - generic [ref=e219]: AI Analysis + - generic [ref=e220]: + - paragraph [ref=e221]: "231313131" + - paragraph [ref=e222]: "231313131" + - generic [ref=e223]: + - heading "Patient" [level=3] [ref=e224]: + - img [ref=e225] + - text: Patient + - generic [ref=e228]: + - generic [ref=e229]: ismail mosa + - generic [ref=e230]: "|" + - generic [ref=e231]: + - img [ref=e232] + - text: "0569874563" + - generic [ref=e235]: + - heading "Quick Actions" [level=3] [ref=e236] + - generic [ref=e237]: + - button "Activate" [ref=e239] [cursor=pointer]: + - img [ref=e240] + - generic [ref=e243]: Activate + - button "Cancel Inquiry" [ref=e245] [cursor=pointer]: + - img [ref=e246] + - generic [ref=e250]: Cancel Inquiry + - contentinfo [ref=e251]: + - paragraph [ref=e252]: + - text: Powered by + - link "tenhal.sa" [ref=e253] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-34-49-571Z.yml b/.playwright-cli/page-2026-07-12T20-34-49-571Z.yml new file mode 100644 index 0000000..8f4ebf2 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-34-49-571Z.yml @@ -0,0 +1,344 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Observations" [level=1] [ref=e142]: + - img [ref=e143] + - text: Observations + - paragraph [ref=e146]: Manage and triage staff-reported observations + - generic [ref=e147]: + - link "New Observation" [ref=e148] [cursor=pointer]: + - /url: /observations/create/ + - img [ref=e149] + - text: New Observation + - link "Public Form" [ref=e151] [cursor=pointer]: + - /url: /core/public/submit/ + - img [ref=e152] + - text: Public Form + - generic [ref=e153]: + - generic [ref=e155]: + - generic [ref=e156]: + - paragraph [ref=e157]: Total + - paragraph [ref=e158]: "2" + - img [ref=e160] + - generic [ref=e162]: + - generic [ref=e163]: + - paragraph [ref=e164]: New + - paragraph + - img [ref=e166] + - generic [ref=e170]: + - generic [ref=e171]: + - paragraph [ref=e172]: In Progress + - paragraph [ref=e173]: "2" + - img [ref=e175] + - generic [ref=e178]: + - generic [ref=e179]: + - img [ref=e181] + - heading "Filters" [level=5] [ref=e183] + - button [ref=e184] [cursor=pointer]: + - img [ref=e185] + - generic [ref=e188]: + - generic [ref=e189]: + - generic [ref=e190]: + - generic [ref=e191]: Search + - textbox "Tracking code, description..." [ref=e192] + - generic [ref=e193]: + - generic [ref=e194]: Status + - combobox [ref=e195]: + - option "All Statuses" [selected] + - option "Open" + - option "In Progress" + - option "Resolved" + - option "Closed" + - generic [ref=e196]: + - generic [ref=e197]: Severity + - combobox [ref=e198]: + - option "All Severities" [selected] + - option "Low" + - option "Medium" + - option "High" + - option "Critical" + - generic [ref=e199]: + - generic [ref=e200]: Category + - combobox [ref=e201]: + - option "All Categories" [selected] + - option "Patient Safety" + - option "Clinical Quality" + - option "Infection Control" + - option "Medication Safety" + - option "Equipment & Devices" + - option "Facility & Environment" + - option "Staff Behavior" + - option "Communication" + - option "Documentation" + - option "Process & Workflow" + - option "Security" + - option "IT & Systems" + - option "Housekeeping" + - option "Food Services" + - option "Other" + - generic [ref=e202]: + - generic [ref=e203]: Department + - combobox [ref=e204]: + - option "All Departments" [selected] + - option "Contact Center Department" + - option "Emergency Administrative Department" + - option "Executive Administration" + - option "Financial Collection & Claims Department" + - option "Inpatient Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Security Department" + - option "Anesthesia Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "Emergency Medicine Department" + - option "IVF" + - option "Infection Control Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Surgery Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Nursing Department" + - option "Biomedical Department" + - option "Facility Management & Maintenance Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "Information Technology Department" + - option "Laundry Department" + - generic [ref=e206] [cursor=pointer]: + - generic [ref=e207]: All Departments + - combobox "All Departments" [ref=e208] + - generic [ref=e209]: + - generic [ref=e210]: Assigned To + - combobox [ref=e211]: + - option "All Users" [selected] + - option "E2E PX Employee" + - option "test test" + - option "test test" + - generic [ref=e212]: + - generic [ref=e213]: Date From + - textbox [ref=e214] + - generic [ref=e215]: + - generic [ref=e216]: Date To + - textbox [ref=e217] + - generic [ref=e218]: + - button "Apply Filters" [ref=e219] [cursor=pointer]: + - img [ref=e220] + - text: Apply Filters + - link "Clear" [ref=e223] [cursor=pointer]: + - /url: /observations/ + - img [ref=e224] + - text: Clear + - generic [ref=e229]: + - img [ref=e231] + - heading "Observations List" [level=5] [ref=e233] + - generic [ref=e234]: Showing 1 to 4 of 4 observations + - table [ref=e237]: + - rowgroup [ref=e238]: + - row "Reference Date Category Description Severity Status Reporter Department Assigned Actions" [ref=e239]: + - columnheader "Reference" [ref=e240] + - columnheader "Date" [ref=e241] + - columnheader "Category" [ref=e242] + - columnheader "Description" [ref=e243] + - columnheader "Severity" [ref=e244] + - columnheader "Status" [ref=e245] + - columnheader "Reporter" [ref=e246] + - columnheader "Department" [ref=e247] + - columnheader "Assigned" [ref=e248] + - columnheader "Actions" [ref=e249] + - rowgroup [ref=e250]: + - row "OBS-202607-0001 Jul 08, 2026 16:33 - test observation Medium In Progress ismail mosa Contact Center Department ismail mosa" [ref=e251]: + - cell "OBS-202607-0001" [ref=e252] + - cell "Jul 08, 2026 16:33" [ref=e253]: + - text: Jul 08, 2026 + - text: 16:33 + - cell "-" [ref=e254] + - cell "test observation" [ref=e255]: + - generic [ref=e256]: test observation + - cell "Medium" [ref=e257] + - cell "In Progress" [ref=e258] + - cell "ismail mosa" [ref=e259] + - cell "Contact Center Department" [ref=e260] + - cell "ismail mosa" [ref=e261] + - cell [ref=e262]: + - link [ref=e263] [cursor=pointer]: + - /url: /observations/1d925706-110e-4bad-808f-26aae2029144/ + - img [ref=e264] + - row "OBS-202606-E2EHOSP-0056 Jun 22, 2026 10:16 Other Observation i notice some trash in the hall Medium In Progress E2E PX Admin Contact Center Department E2E PX Admin" [ref=e267]: + - cell "OBS-202606-E2EHOSP-0056" [ref=e268] + - cell "Jun 22, 2026 10:16" [ref=e269]: + - text: Jun 22, 2026 + - text: 10:16 + - cell "Other" [ref=e270]: + - generic [ref=e271]: Other + - cell "Observation i notice some trash in the hall" [ref=e272]: + - generic [ref=e273]: + - strong [ref=e274]: Observation + - text: i notice some trash in the hall + - cell "Medium" [ref=e275] + - cell "In Progress" [ref=e276] + - cell "E2E PX Admin" [ref=e277] + - cell "Contact Center Department" [ref=e278] + - cell "E2E PX Admin" [ref=e279] + - cell [ref=e280]: + - link [ref=e281] [cursor=pointer]: + - /url: /observations/b9a80c99-31dc-4cda-a1e3-c20d94a28729/ + - img [ref=e282] + - row "OBS-O8TCIC Jun 09, 2026 13:36 - Wet floor near the main entrance, no warning sign posted. Medium In Progress Anonymous Reporter - ismail mosa" [ref=e285]: + - cell "OBS-O8TCIC" [ref=e286] + - cell "Jun 09, 2026 13:36" [ref=e287]: + - text: Jun 09, 2026 + - text: 13:36 + - cell "-" [ref=e288] + - cell "Wet floor near the main entrance, no warning sign posted." [ref=e289]: + - generic [ref=e290]: Wet floor near the main entrance, no warning sign posted. + - cell "Medium" [ref=e291] + - cell "In Progress" [ref=e292] + - cell "Anonymous Reporter" [ref=e293] + - cell "-" [ref=e294] + - cell "ismail mosa" [ref=e295] + - cell [ref=e296]: + - link [ref=e297] [cursor=pointer]: + - /url: /observations/5f8fc6c6-3b38-4b88-8144-3f926c24e290/ + - img [ref=e298] + - row "OBS-QPE02F Jun 07, 2026 21:08 Other Observation safsafsad Medium Open ismail Daif - Unassigned" [ref=e301]: + - cell "OBS-QPE02F" [ref=e302] + - cell "Jun 07, 2026 21:08" [ref=e303]: + - text: Jun 07, 2026 + - text: 21:08 + - cell "Other" [ref=e304]: + - generic [ref=e305]: Other + - cell "Observation safsafsad" [ref=e306]: + - generic [ref=e307]: + - strong [ref=e308]: Observation + - text: safsafsad + - cell "Medium" [ref=e309] + - cell "Open" [ref=e310] + - cell "ismail Daif" [ref=e311] + - cell "-" [ref=e312] + - cell "Unassigned" [ref=e313] + - cell [ref=e314]: + - link [ref=e315] [cursor=pointer]: + - /url: /observations/327d9b79-2126-4800-8cd7-647f006271ce/ + - img [ref=e316] + - generic [ref=e319]: + - generic [ref=e320]: + - generic [ref=e321]: Showing 1-4 of 4 + - generic [ref=e322]: + - generic [ref=e323]: Show + - combobox [ref=e324]: + - option "10" + - option "25" [selected] + - option "50" + - option "100" + - generic [ref=e325]: + - img [ref=e327] + - generic [ref=e329]: "1" + - img [ref=e331] + - contentinfo [ref=e333]: + - paragraph [ref=e334]: + - text: Powered by + - link "tenhal.sa" [ref=e335] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-35-19-622Z.yml b/.playwright-cli/page-2026-07-12T20-35-19-622Z.yml new file mode 100644 index 0000000..96ab83c --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-35-19-622Z.yml @@ -0,0 +1,259 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Observations" [ref=e141] [cursor=pointer]: + - /url: /observations/ + - img [ref=e142] + - generic [ref=e144]: OBS-202607-0001 + - generic [ref=e145]: In Progress + - generic [ref=e146]: Medium + - generic [ref=e147]: + - img [ref=e148] + - text: Not Sent to Dept + - heading "test observation" [level=1] [ref=e151] + - generic [ref=e153]: + - generic [ref=e154]: + - img [ref=e156] + - generic [ref=e158]: Created + - generic [ref=e160]: + - img [ref=e162] + - generic [ref=e164]: Assigned + - generic [ref=e166]: + - img [ref=e168] + - generic [ref=e171]: Sent to Dept + - generic [ref=e173]: + - img [ref=e175] + - generic [ref=e177]: Dept Response + - generic [ref=e179]: + - img [ref=e181] + - generic [ref=e184]: Resolved + - navigation [ref=e185]: + - button "Details" [ref=e186] [cursor=pointer] + - button "Timeline" [ref=e187] [cursor=pointer] + - button "RCA" [ref=e188] [cursor=pointer] + - button "Notes" [ref=e189] [cursor=pointer] + - main [ref=e190]: + - generic [ref=e192]: + - generic [ref=e193]: + - paragraph [ref=e195]: test observation + - generic [ref=e196]: + - generic [ref=e197]: + - paragraph [ref=e198]: Category + - paragraph [ref=e199]: "-" + - generic [ref=e200]: + - paragraph [ref=e201]: Location + - paragraph [ref=e202]: "-" + - generic [ref=e203]: + - paragraph [ref=e204]: Incident Date + - paragraph [ref=e205]: 08 Jul 2026 + - generic [ref=e209]: + - img [ref=e210] + - paragraph [ref=e214]: No department assigned to this observation + - generic [ref=e215]: + - generic [ref=e216]: + - heading "Quick Actions" [level=3] [ref=e217] + - generic [ref=e218]: + - button "Respond to Patient" [ref=e219] [cursor=pointer]: + - img [ref=e220] + - generic [ref=e222]: Respond to Patient + - button "Reassign" [ref=e223] [cursor=pointer]: + - img [ref=e224] + - generic [ref=e227]: Reassign + - button "Send to Dept" [ref=e228] [cursor=pointer]: + - img [ref=e229] + - generic [ref=e232]: Send to Dept + - button "Resolve" [ref=e234] [cursor=pointer]: + - img [ref=e235] + - generic [ref=e238]: Resolve + - link "RCA" [ref=e239] [cursor=pointer]: + - /url: /rca/create/?related_model=observation&related_id=1d925706-110e-4bad-808f-26aae2029144 + - img [ref=e240] + - generic [ref=e243]: RCA + - link "QI Project" [ref=e244] [cursor=pointer]: + - /url: /projects/create/?related_model=observation&related_id=1d925706-110e-4bad-808f-26aae2029144 + - img [ref=e245] + - generic [ref=e247]: QI Project + - button "Delete" [ref=e249] [cursor=pointer]: + - img [ref=e250] + - generic [ref=e253]: Delete + - generic [ref=e254]: + - heading "Assignment" [level=3] [ref=e255]: + - img [ref=e256] + - text: Assignment + - list [ref=e261]: + - listitem [ref=e262]: + - generic [ref=e263]: Department + - generic [ref=e264]: Contact Center Department + - listitem [ref=e265]: + - generic [ref=e266]: Assigned To + - generic [ref=e267]: ismail mosa + - generic [ref=e268]: + - heading "Linked PX Action" [level=3] [ref=e269]: + - img [ref=e270] + - text: Linked PX Action + - paragraph [ref=e273]: Observation OBS-202607-0001 + - link "View Action" [ref=e274] [cursor=pointer]: + - /url: /actions/9b185c2a-e3fe-4e13-a727-7aab21df8754/ + - img [ref=e275] + - text: View Action + - generic [ref=e277]: + - heading "Triage" [level=3] [ref=e278]: + - img [ref=e279] + - text: Triage + - generic [ref=e280]: + - generic [ref=e281]: + - generic [ref=e282]: Department + - combobox [ref=e283]: + - option "Select department" + - option "Contact Center Department" [selected] + - option "Emergency Administrative Department" + - option "Executive Administration" + - option "Financial Collection & Claims Department" + - option "Inpatient Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Security Department" + - option "Anesthesia Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "Emergency Medicine Department" + - option "IVF" + - option "Infection Control Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Surgery Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Nursing Department" + - option "Biomedical Department" + - option "Facility Management & Maintenance Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "Information Technology Department" + - option "Laundry Department" + - generic [ref=e284]: + - generic [ref=e285]: Assign To + - combobox [ref=e286]: + - option "Select assignee" [selected] + - option "E2E PX Employee (e2e-px-employee@px360.test)" + - option "test test (9m5s6gzmft@ruutukf.com)" + - option "test test (esma3el555@gmail.com)" + - generic [ref=e287]: + - generic [ref=e288]: Status + - combobox [ref=e289]: + - option "Open" + - option "In Progress" [selected] + - option "Resolved" + - option "Closed" + - generic [ref=e290]: + - generic [ref=e291]: Note + - textbox "Add a note about this triage action (optional)..." [ref=e292] + - button "Update" [ref=e293] [cursor=pointer]: + - img [ref=e294] + - text: Update + - contentinfo [ref=e297]: + - paragraph [ref=e298]: + - text: Powered by + - link "tenhal.sa" [ref=e299] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-35-37-343Z.yml b/.playwright-cli/page-2026-07-12T20-35-37-343Z.yml new file mode 100644 index 0000000..d6747b0 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-35-37-343Z.yml @@ -0,0 +1,201 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Suggestions" [level=1] [ref=e142] + - paragraph [ref=e143]: Manage and review submitted suggestions + - link "New Suggestion" [ref=e145] [cursor=pointer]: + - /url: /suggestions/create/ + - img [ref=e146] + - text: New Suggestion + - generic [ref=e147]: + - generic [ref=e148]: + - img [ref=e151] + - generic [ref=e153]: + - heading "Total Suggestions" [level=6] [ref=e154] + - heading "3" [level=2] [ref=e155] + - generic [ref=e156]: + - img [ref=e159] + - generic [ref=e161]: + - heading "Compliments" [level=6] [ref=e162] + - heading "0" [level=2] [ref=e163] + - generic [ref=e164]: + - img [ref=e167] + - generic [ref=e169]: + - heading "Avg Rating" [level=6] [ref=e170] + - heading "0.0 /5" [level=2] [ref=e171] + - generic [ref=e172]: + - img [ref=e175] + - generic [ref=e178]: + - heading "Pending Review" [level=6] [ref=e179] + - heading "2" [level=2] [ref=e180] + - generic [ref=e181]: + - generic [ref=e182]: + - generic [ref=e183]: + - img [ref=e185] + - heading "Suggestions" [level=5] [ref=e187] + - generic [ref=e188]: + - link "All" [ref=e189] [cursor=pointer]: + - /url: "?" + - link "Suggestions" [ref=e190] [cursor=pointer]: + - /url: "?feedback_type=suggestion" + - link "Compliments" [ref=e191] [cursor=pointer]: + - /url: "?feedback_type=compliment" + - button "Advanced" [ref=e193] [cursor=pointer]: + - img [ref=e194] + - text: Advanced + - paragraph [ref=e195]: "Showing: 1-3 of 3" + - table [ref=e197]: + - rowgroup [ref=e198]: + - row "ID Type Contact Title Category Rating Sentiment Status Created Actions" [ref=e199]: + - columnheader "ID" [ref=e200] + - columnheader "Type" [ref=e201] + - columnheader "Contact" [ref=e202] + - columnheader "Title" [ref=e203] + - columnheader "Category" [ref=e204] + - columnheader "Rating" [ref=e205] + - columnheader "Sentiment" [ref=e206] + - columnheader "Status" [ref=e207] + - columnheader "Created" [ref=e208] + - columnheader "Actions" [ref=e209] + - rowgroup [ref=e210]: + - row "SGT-202607-0003 Suggestion ismail Daif t Other - Neutral Submitted Jul 09, 2026" [ref=e211] [cursor=pointer]: + - cell "SGT-202607-0003" [ref=e212] + - cell "Suggestion" [ref=e213] + - cell "ismail Daif" [ref=e214]: + - generic [ref=e215]: ismail Daif + - cell "t" [ref=e216]: + - generic [ref=e217]: t + - cell "Other" [ref=e218] + - cell "-" [ref=e219] + - cell "Neutral" [ref=e220] + - cell "Submitted" [ref=e221] + - cell "Jul 09, 2026" [ref=e222] + - cell [ref=e223]: + - link "View" [ref=e225]: + - /url: /suggestions/78f015c5-fbed-4e10-987f-aa8ced75545a/ + - img [ref=e226] + - row "SGT-202607-0002 Suggestion ismail test Other - Neutral Submitted Jul 09, 2026" [ref=e229] [cursor=pointer]: + - cell "SGT-202607-0002" [ref=e230] + - cell "Suggestion" [ref=e231] + - cell "ismail" [ref=e232]: + - generic [ref=e233]: ismail + - cell "test" [ref=e234]: + - generic [ref=e235]: test + - cell "Other" [ref=e236] + - cell "-" [ref=e237] + - cell "Neutral" [ref=e238] + - cell "Submitted" [ref=e239] + - cell "Jul 09, 2026" [ref=e240] + - cell [ref=e241]: + - link "View" [ref=e243]: + - /url: /suggestions/04b009f3-53aa-4eac-9baf-39b3633e56b0/ + - img [ref=e244] + - row "SGT-202607-0001 Suggestion ismail The suggestion titled 'test' with content 'test' appears … Other - Neutral Closed Jul 09, 2026" [ref=e247] [cursor=pointer]: + - cell "SGT-202607-0001" [ref=e248] + - cell "Suggestion" [ref=e249] + - cell "ismail" [ref=e250]: + - generic [ref=e251]: ismail + - cell "The suggestion titled 'test' with content 'test' appears …" [ref=e252]: + - generic [ref=e253]: The suggestion titled 'test' with content 'test' appears … + - cell "Other" [ref=e254] + - cell "-" [ref=e255] + - cell "Neutral" [ref=e256] + - cell "Closed" [ref=e257] + - cell "Jul 09, 2026" [ref=e258] + - cell [ref=e259]: + - link "View" [ref=e261]: + - /url: /suggestions/ef1e633b-7fae-46e2-baab-0d5bdd71e7b7/ + - img [ref=e262] + - contentinfo [ref=e265]: + - paragraph [ref=e266]: + - text: Powered by + - link "tenhal.sa" [ref=e267] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-35-50-710Z.yml b/.playwright-cli/page-2026-07-12T20-35-50-710Z.yml new file mode 100644 index 0000000..3cd5765 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-35-50-710Z.yml @@ -0,0 +1,188 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Suggestions" [ref=e141] [cursor=pointer]: + - /url: /suggestions/ + - img [ref=e142] + - generic [ref=e144]: test + - generic [ref=e145]: Submitted + - generic [ref=e146]: Medium + - generic [ref=e147]: + - heading "Suggestion Detail" [level=1] [ref=e148] + - link "Edit" [ref=e150] [cursor=pointer]: + - /url: /suggestions/04b009f3-53aa-4eac-9baf-39b3633e56b0/update/ + - img [ref=e151] + - text: Edit + - generic [ref=e155]: + - generic [ref=e156]: + - img [ref=e158] + - generic [ref=e160]: Submitted + - generic [ref=e162]: + - img [ref=e164] + - generic [ref=e167]: Assigned + - generic [ref=e169]: + - img [ref=e171] + - generic [ref=e174]: Reviewed + - generic [ref=e176]: + - img [ref=e178] + - generic [ref=e180]: Acknowledged + - generic [ref=e182]: + - img [ref=e184] + - generic [ref=e187]: Closed + - navigation [ref=e188]: + - button "Details" [ref=e189] [cursor=pointer] + - button "AI Analysis" [ref=e190] [cursor=pointer] + - button "Notes" [ref=e191] [cursor=pointer] + - main [ref=e192]: + - generic [ref=e195]: + - heading "Suggestion Details" [level=3] [ref=e196]: + - img [ref=e197] + - text: Suggestion Details + - paragraph [ref=e200]: test + - generic [ref=e201]: + - generic [ref=e202]: + - generic [ref=e203]: Hospital + - generic [ref=e204]: E2E Test Hospital + - generic [ref=e205]: + - generic [ref=e206]: Category + - generic [ref=e207]: Other + - generic [ref=e208]: + - generic [ref=e209]: Location + - generic [ref=e210]: Not specified + - generic [ref=e211]: + - generic [ref=e212]: Sentiment + - generic [ref=e214]: Neutral + - generic [ref=e215]: + - heading "Contact" [level=6] [ref=e216] + - generic [ref=e217]: + - generic [ref=e218]: ismail + - generic [ref=e219]: "0569874589" + - generic [ref=e221]: + - img [ref=e222] + - text: July 09, 2026 13:46 + - generic [ref=e225]: + - heading "Quick Actions" [level=3] [ref=e226] + - generic [ref=e227]: + - generic [ref=e228]: + - text: Status + - combobox [ref=e229]: + - option "Submitted" [selected] + - option "Reviewed" + - option "Acknowledged" + - option "Closed" + - option "Reopened" + - textbox "Add a note..." [ref=e230] + - button "Update" [ref=e231] [cursor=pointer]: + - img [ref=e232] + - text: Update + - separator [ref=e237] + - generic [ref=e238]: + - text: Assign To + - combobox [ref=e239]: + - option "Select user..." [selected] + - option "E2E PX Employee" + - option "test test" + - option "test test" + - button "Assign" [ref=e240] [cursor=pointer]: + - img [ref=e241] + - text: Assign + - separator [ref=e245] + - generic [ref=e246]: + - text: Department + - button "Send to Department" [ref=e247] [cursor=pointer]: + - img [ref=e248] + - text: Send to Department + - separator [ref=e251] + - contentinfo [ref=e252]: + - paragraph [ref=e253]: + - text: Powered by + - link "tenhal.sa" [ref=e254] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-36-00-595Z.yml b/.playwright-cli/page-2026-07-12T20-36-00-595Z.yml new file mode 100644 index 0000000..ddeb3a0 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-36-00-595Z.yml @@ -0,0 +1,221 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Appreciations" [level=1] [ref=e142]: + - img [ref=e143] + - text: Appreciations + - paragraph [ref=e145]: Manage and review patient appreciation submissions + - generic [ref=e146]: + - link "Create Internal" [ref=e147] [cursor=pointer]: + - /url: /appreciation/new/ + - img [ref=e148] + - text: Create Internal + - link "Public Form" [ref=e149] [cursor=pointer]: + - /url: /core/public/submit/ + - img [ref=e150] + - text: Public Form + - generic [ref=e154]: + - paragraph [ref=e155]: "0" + - paragraph [ref=e156]: Pending + - generic [ref=e157]: + - generic [ref=e159]: + - img [ref=e161] + - generic [ref=e164]: + - paragraph [ref=e165]: Total + - paragraph [ref=e166]: "3" + - generic [ref=e168]: + - img [ref=e170] + - generic [ref=e173]: + - paragraph [ref=e174]: Draft + - paragraph [ref=e175]: "0" + - generic [ref=e177]: + - img [ref=e179] + - generic [ref=e181]: + - paragraph [ref=e182]: Activated + - paragraph [ref=e183]: "0" + - generic [ref=e185]: + - img [ref=e187] + - generic [ref=e190]: + - paragraph [ref=e191]: Sent + - paragraph [ref=e192]: "3" + - generic [ref=e193]: + - link "Leaderboard See top performers" [ref=e194] [cursor=pointer]: + - /url: /appreciation/leaderboard/ + - generic [ref=e196]: + - img [ref=e198] + - generic [ref=e204]: + - heading "Leaderboard" [level=3] [ref=e205] + - paragraph [ref=e206]: See top performers + - link "My Badges View earned badges" [ref=e207] [cursor=pointer]: + - /url: /appreciation/badges/ + - generic [ref=e209]: + - img [ref=e211] + - generic [ref=e214]: + - heading "My Badges" [level=3] [ref=e215] + - paragraph [ref=e216]: View earned badges + - generic [ref=e217]: + - generic [ref=e218]: + - img [ref=e220] + - heading "Filters" [level=3] [ref=e222] + - generic [ref=e224]: + - generic [ref=e225]: + - generic [ref=e226]: Search + - textbox "Search messages..." [ref=e227] + - generic [ref=e228]: + - generic [ref=e229]: Status + - combobox [ref=e230]: + - option "All Status" [selected] + - option "Draft" + - option "Activated" + - option "AI Analyzed" + - option "Sent" + - option "Acknowledged" + - button "Search" [ref=e231] [cursor=pointer]: + - img [ref=e232] + - text: Search + - link "Clear" [ref=e235] [cursor=pointer]: + - /url: /appreciation/ + - table [ref=e237]: + - rowgroup [ref=e238]: + - row "From Message Staff Department Category Status Date Actions" [ref=e239]: + - columnheader "From" [ref=e240] + - columnheader "Message" [ref=e241] + - columnheader "Staff" [ref=e242] + - columnheader "Department" [ref=e243] + - columnheader "Category" [ref=e244] + - columnheader "Status" [ref=e245] + - columnheader "Date" [ref=e246] + - columnheader "Actions" [ref=e247] + - rowgroup [ref=e248]: + - row "ismail thank - Contact Center Department - Sent Jul 09, 2026" [ref=e249] [cursor=pointer]: + - cell "ismail" [ref=e250] + - cell "thank" [ref=e251]: + - generic [ref=e252]: thank + - cell "-" [ref=e253] + - cell "Contact Center Department" [ref=e254] + - cell "-" [ref=e255] + - cell "Sent" [ref=e256] + - cell "Jul 09, 2026" [ref=e257] + - cell [ref=e258]: + - link "View" [ref=e260]: + - /url: /appreciation/detail/5306821e-d596-498f-88c5-fa3addcdd491/ + - img [ref=e261] + - row "ismail mosa test - Contact Center Department - Sent Jul 09, 2026" [ref=e264] [cursor=pointer]: + - cell "ismail mosa" [ref=e265] + - cell "test" [ref=e266]: + - generic [ref=e267]: test + - cell "-" [ref=e268] + - cell "Contact Center Department" [ref=e269] + - cell "-" [ref=e270] + - cell "Sent" [ref=e271] + - cell "Jul 09, 2026" [ref=e272] + - cell [ref=e273]: + - link "View" [ref=e275]: + - /url: /appreciation/detail/3697b78b-5ff3-4392-8982-af20f58daebf/ + - img [ref=e276] + - row "ismail mosa thank you - Contact Center Department - Sent Jul 09, 2026" [ref=e279] [cursor=pointer]: + - cell "ismail mosa" [ref=e280] + - cell "thank you" [ref=e281]: + - generic [ref=e282]: thank you + - cell "-" [ref=e283] + - cell "Contact Center Department" [ref=e284] + - cell "-" [ref=e285] + - cell "Sent" [ref=e286] + - cell "Jul 09, 2026" [ref=e287] + - cell [ref=e288]: + - link "View" [ref=e290]: + - /url: /appreciation/detail/240ea5bd-a12d-4f76-8a79-a8dd9de5714a/ + - img [ref=e291] + - contentinfo [ref=e294]: + - paragraph [ref=e295]: + - text: Powered by + - link "tenhal.sa" [ref=e296] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-36-13-189Z.yml b/.playwright-cli/page-2026-07-12T20-36-13-189Z.yml new file mode 100644 index 0000000..9bee09b --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-36-13-189Z.yml @@ -0,0 +1,137 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Appreciations" [ref=e141] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e142] + - generic [ref=e144]: Detail + - generic [ref=e145]: Sent + - generic [ref=e146]: + - heading "Appreciation Detail" [level=1] [ref=e147]: + - img [ref=e148] + - text: Appreciation Detail + - generic [ref=e151]: 2026-07-09 13:55 — E2E Test Hospital + - navigation [ref=e152]: + - button "Details" [ref=e153] [cursor=pointer] + - button "AI Analysis" [ref=e154] [cursor=pointer] + - button "Notes" [ref=e155] [cursor=pointer] + - main [ref=e156]: + - generic [ref=e158]: + - generic [ref=e159]: + - heading "Appreciation Message" [level=3] [ref=e160]: + - img [ref=e161] + - text: Appreciation Message + - paragraph + - paragraph [ref=e163]: thank + - paragraph + - generic [ref=e164]: + - heading "Submitted By" [level=3] [ref=e165]: + - img [ref=e166] + - text: Submitted By + - generic [ref=e169]: + - generic [ref=e170]: + - paragraph [ref=e171]: Name + - paragraph [ref=e172]: ismail + - generic [ref=e173]: + - paragraph [ref=e174]: Phone + - paragraph [ref=e175]: "0569874589" + - generic [ref=e176]: + - heading "Sent" [level=3] [ref=e177]: + - img [ref=e178] + - text: Sent + - generic [ref=e182]: + - paragraph [ref=e183]: Sent At + - paragraph [ref=e184]: 2026-07-09 13:57 + - contentinfo [ref=e186]: + - paragraph [ref=e187]: + - text: Powered by + - link "tenhal.sa" [ref=e188] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-36-24-641Z.yml b/.playwright-cli/page-2026-07-12T20-36-24-641Z.yml new file mode 100644 index 0000000..1ec6ae9 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-36-24-641Z.yml @@ -0,0 +1,256 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e141]: + - generic [ref=e142]: + - link "Inquiries" [ref=e143] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e144] + - generic [ref=e146]: New Inquiry + - heading "Create New Inquiry" [level=1] [ref=e147] + - generic [ref=e149]: + - generic [ref=e150]: + - img [ref=e152] + - heading "Inquiry Information" [level=5] [ref=e155] + - generic [ref=e156]: + - generic [ref=e157]: + - heading "Organization" [level=6] [ref=e158]: + - img [ref=e159] + - text: Organization + - generic [ref=e162]: + - generic [ref=e163]: + - generic [ref=e164]: Location Type * + - combobox "Location Type *" [ref=e165]: + - option "Select Location Type" [selected] + - option "Outpatient" + - option "Inpatient" + - option "Emergency" + - option "General" + - generic [ref=e166]: + - generic [ref=e167]: Area + - combobox "Area" [ref=e168]: + - option "Select Area (optional)" [selected] + - option "Anesthesia (E2E-HOSP)" + - option "Dialysis (E2E-HOSP)" + - option "Emergency (E2E-HOSP)" + - option "Endoscopy (E2E-HOSP)" + - option "ICCU (E2E-HOSP)" + - option "ICU (E2E-HOSP)" + - option "ICU Stepdown (E2E-HOSP)" + - option "LTACU (E2E-HOSP)" + - option "Labor & Delivery (E2E-HOSP)" + - option "Main OR (E2E-HOSP)" + - option "Medical Ward (E2E-HOSP)" + - option "NICU - Neonatal Intensive Care Unit (E2E-HOSP)" + - option "Nursery (E2E-HOSP)" + - option "OB Wards (E2E-HOSP)" + - option "OPD1 (E2E-HOSP)" + - option "OPD2 (E2E-HOSP)" + - option "OPD3 (E2E-HOSP)" + - option "OPD4 (E2E-HOSP)" + - option "OPD5 (E2E-HOSP)" + - option "OPD6 (E2E-HOSP)" + - option "OPD7 (E2E-HOSP)" + - option "PICU (E2E-HOSP)" + - option "PICU Stepdown (E2E-HOSP)" + - option "Pediatric Ward (E2E-HOSP)" + - option "Recovery (E2E-HOSP)" + - option "Stepdown 3-4 (E2E-HOSP)" + - option "Surgical Wards (E2E-HOSP)" + - generic [ref=e169]: + - generic [ref=e170]: Department Category * + - combobox [ref=e171]: + - option "Select Category" [selected] + - option "Medical" + - option "Nursing" + - option "Administrative" + - option "Support Services" + - generic [ref=e172]: + - generic [ref=e173]: Department (Optional) + - combobox "Department (Optional)" [ref=e174]: + - option "Select Department" [selected] + - option "Anesthesia Department" + - option "Biomedical Department" + - option "Contact Center Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Emergency Administrative Department" + - option "Emergency Medicine Department" + - option "Executive Administration" + - option "Facility Management & Maintenance Department" + - option "Financial Collection & Claims Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "IVF" + - option "Infection Control Department" + - option "Information Technology Department" + - option "Inpatient Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Laundry Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Nursing Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Security Department" + - option "Surgery Department" + - generic [ref=e175]: + - generic [ref=e176]: Section + - combobox "Section" [ref=e177]: + - option "Select Section" [selected] + - generic [ref=e178]: + - heading "Contact Information" [level=6] [ref=e179]: + - img [ref=e180] + - text: Contact Information + - generic [ref=e183]: + - generic [ref=e184]: + - generic [ref=e185]: Contact Name * + - textbox "Contact Name *" [ref=e186] + - generic [ref=e187]: + - generic [ref=e188]: Contact Phone * + - textbox "Contact Phone *" [ref=e189] + - generic [ref=e190]: + - generic [ref=e191]: Contact Email + - textbox "Contact Email" [ref=e192] + - generic [ref=e193]: + - heading "Inquiry Details" [level=6] [ref=e194]: + - img [ref=e195] + - text: Inquiry Details + - generic [ref=e198]: + - generic [ref=e199]: + - generic [ref=e200]: Inquiry Type * + - combobox "Inquiry Type *" [ref=e201]: + - option "General Inquiry" [selected] + - option "Appointment Related" + - option "Billing & Insurance" + - option "Medical Records" + - option "Pharmacy" + - option "Insurance" + - option "Feedback" + - option "Other" + - generic [ref=e202]: + - generic [ref=e203]: Priority + - combobox "Priority" [ref=e204]: + - option "Low" + - option "Medium" [selected] + - option "High" + - option "Critical" + - generic [ref=e205]: + - generic [ref=e206]: Subject * + - textbox "Subject *" [ref=e207]: + - /placeholder: Brief subject + - generic [ref=e208]: + - generic [ref=e209]: Message * + - textbox "Message *" [ref=e210]: + - /placeholder: Describe your inquiry + - generic [ref=e211]: + - button "Create Inquiry" [ref=e212] [cursor=pointer]: + - img [ref=e213] + - text: Create Inquiry + - link "Cancel" [ref=e216] [cursor=pointer]: + - /url: /inquiries/ + - contentinfo [ref=e217]: + - paragraph [ref=e218]: + - text: Powered by + - link "tenhal.sa" [ref=e219] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-37-03-048Z.yml b/.playwright-cli/page-2026-07-12T20-37-03-048Z.yml new file mode 100644 index 0000000..9745960 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-37-03-048Z.yml @@ -0,0 +1,258 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e141]: + - generic [ref=e142]: + - link "Inquiries" [ref=e143] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e144] + - generic [ref=e146]: New Inquiry + - heading "Create New Inquiry" [level=1] [ref=e147] + - generic [ref=e149]: + - generic [ref=e150]: + - img [ref=e152] + - heading "Inquiry Information" [level=5] [ref=e155] + - generic [ref=e156]: + - generic [ref=e157]: + - heading "Organization" [level=6] [ref=e158]: + - img [ref=e159] + - text: Organization + - generic [ref=e162]: + - generic [ref=e163]: + - generic [ref=e164]: Location Type * + - combobox "Location Type *" [active] [ref=e165]: + - option "Select Location Type" [selected] + - option "Outpatient" + - option "Inpatient" + - option "Emergency" + - option "General" + - generic [ref=e166]: + - generic [ref=e167]: Area + - combobox "Area" [ref=e168]: + - option "Select Area (optional)" [selected] + - option "Anesthesia (E2E-HOSP)" + - option "Dialysis (E2E-HOSP)" + - option "Emergency (E2E-HOSP)" + - option "Endoscopy (E2E-HOSP)" + - option "ICCU (E2E-HOSP)" + - option "ICU (E2E-HOSP)" + - option "ICU Stepdown (E2E-HOSP)" + - option "LTACU (E2E-HOSP)" + - option "Labor & Delivery (E2E-HOSP)" + - option "Main OR (E2E-HOSP)" + - option "Medical Ward (E2E-HOSP)" + - option "NICU - Neonatal Intensive Care Unit (E2E-HOSP)" + - option "Nursery (E2E-HOSP)" + - option "OB Wards (E2E-HOSP)" + - option "OPD1 (E2E-HOSP)" + - option "OPD2 (E2E-HOSP)" + - option "OPD3 (E2E-HOSP)" + - option "OPD4 (E2E-HOSP)" + - option "OPD5 (E2E-HOSP)" + - option "OPD6 (E2E-HOSP)" + - option "OPD7 (E2E-HOSP)" + - option "PICU (E2E-HOSP)" + - option "PICU Stepdown (E2E-HOSP)" + - option "Pediatric Ward (E2E-HOSP)" + - option "Recovery (E2E-HOSP)" + - option "Stepdown 3-4 (E2E-HOSP)" + - option "Surgical Wards (E2E-HOSP)" + - generic [ref=e169]: + - generic [ref=e170]: Department Category * + - combobox [ref=e171]: + - option "Select Category" [selected] + - option "Medical" + - option "Nursing" + - option "Administrative" + - option "Support Services" + - generic [ref=e172]: + - generic [ref=e173]: Department (Optional) + - combobox "Department (Optional)" [ref=e174]: + - option "Select Department" [selected] + - option "Anesthesia Department" + - option "Biomedical Department" + - option "Contact Center Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Emergency Administrative Department" + - option "Emergency Medicine Department" + - option "Executive Administration" + - option "Facility Management & Maintenance Department" + - option "Financial Collection & Claims Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "IVF" + - option "Infection Control Department" + - option "Information Technology Department" + - option "Inpatient Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Laundry Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Nursing Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Security Department" + - option "Surgery Department" + - generic [ref=e175]: + - generic [ref=e176]: Section + - combobox "Section" [ref=e177]: + - option "Select Section" [selected] + - generic [ref=e178]: + - heading "Contact Information" [level=6] [ref=e179]: + - img [ref=e180] + - text: Contact Information + - generic [ref=e183]: + - generic [ref=e184]: + - generic [ref=e185]: Contact Name * + - textbox "Contact Name *" [ref=e186]: Test User + - generic [ref=e187]: + - generic [ref=e188]: Contact Phone * + - textbox "Contact Phone *" [ref=e189]: "+966501234567" + - generic [ref=e190]: + - generic [ref=e191]: Contact Email + - textbox "Contact Email" [ref=e192] + - generic [ref=e193]: + - heading "Inquiry Details" [level=6] [ref=e194]: + - img [ref=e195] + - text: Inquiry Details + - generic [ref=e198]: + - generic [ref=e199]: + - generic [ref=e200]: Inquiry Type * + - combobox "Inquiry Type *" [ref=e201]: + - option "General Inquiry" [selected] + - option "Appointment Related" + - option "Billing & Insurance" + - option "Medical Records" + - option "Pharmacy" + - option "Insurance" + - option "Feedback" + - option "Other" + - generic [ref=e202]: + - generic [ref=e203]: Priority + - combobox "Priority" [ref=e204]: + - option "Low" + - option "Medium" [selected] + - option "High" + - option "Critical" + - generic [ref=e205]: + - generic [ref=e206]: Subject * + - textbox "Subject *" [ref=e207]: + - /placeholder: Brief subject + - text: Playwright Test Inquiry + - generic [ref=e208]: + - generic [ref=e209]: Message * + - textbox "Message *" [ref=e210]: + - /placeholder: Describe your inquiry + - text: This is a test inquiry created by Playwright to verify px-employee access. + - generic [ref=e211]: + - button "Create Inquiry" [ref=e212] [cursor=pointer]: + - img [ref=e213] + - text: Create Inquiry + - link "Cancel" [ref=e216] [cursor=pointer]: + - /url: /inquiries/ + - contentinfo [ref=e217]: + - paragraph [ref=e218]: + - text: Powered by + - link "tenhal.sa" [ref=e219] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-39-22-820Z.yml b/.playwright-cli/page-2026-07-12T20-39-22-820Z.yml new file mode 100644 index 0000000..d20e4e7 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-39-22-820Z.yml @@ -0,0 +1,258 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e141]: + - generic [ref=e142]: + - link "Inquiries" [ref=e143] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e144] + - generic [ref=e146]: New Inquiry + - heading "Create New Inquiry" [level=1] [ref=e147] + - generic [ref=e149]: + - generic [ref=e150]: + - img [ref=e152] + - heading "Inquiry Information" [level=5] [ref=e155] + - generic [ref=e156]: + - generic [ref=e157]: + - heading "Organization" [level=6] [ref=e158]: + - img [ref=e159] + - text: Organization + - generic [ref=e162]: + - generic [ref=e163]: + - generic [ref=e164]: Location Type * + - combobox "Location Type *" [active] [ref=e165]: + - option "Select Location Type" + - option "Outpatient" + - option "Inpatient" + - option "Emergency" + - option "General" + - generic [ref=e166]: + - generic [ref=e167]: Area + - combobox "Area" [ref=e168]: + - option "Select Area (optional)" [selected] + - option "Anesthesia" + - option "Dialysis" + - option "Emergency" + - option "Endoscopy" + - option "ICCU" + - option "ICU" + - option "ICU Stepdown" + - option "LTACU" + - option "Labor & Delivery" + - option "Main OR" + - option "Medical Ward" + - option "NICU - Neonatal Intensive Care Unit" + - option "Nursery" + - option "OB Wards" + - option "OPD1" + - option "OPD2" + - option "OPD3" + - option "OPD4" + - option "OPD5" + - option "OPD6" + - option "OPD7" + - option "PICU" + - option "PICU Stepdown" + - option "Pediatric Ward" + - option "Recovery" + - option "Stepdown 3-4" + - option "Surgical Wards" + - generic [ref=e169]: + - generic [ref=e170]: Department Category * + - combobox [ref=e171]: + - option "Select Category" [selected] + - option "Medical" + - option "Nursing" + - option "Administrative" + - option "Support Services" + - generic [ref=e172]: + - generic [ref=e173]: Department (Optional) + - combobox "Department (Optional)" [ref=e174]: + - option "Select Department" [selected] + - option "Anesthesia Department" + - option "Biomedical Department" + - option "Contact Center Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Emergency Administrative Department" + - option "Emergency Medicine Department" + - option "Executive Administration" + - option "Facility Management & Maintenance Department" + - option "Financial Collection & Claims Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "IVF" + - option "Infection Control Department" + - option "Information Technology Department" + - option "Inpatient Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Laundry Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Nursing Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Security Department" + - option "Surgery Department" + - generic [ref=e175]: + - generic [ref=e176]: Section + - combobox "Section" [ref=e177]: + - option "Select Section" [selected] + - generic [ref=e178]: + - heading "Contact Information" [level=6] [ref=e179]: + - img [ref=e180] + - text: Contact Information + - generic [ref=e183]: + - generic [ref=e184]: + - generic [ref=e185]: Contact Name * + - textbox "Contact Name *" [ref=e186]: Test User + - generic [ref=e187]: + - generic [ref=e188]: Contact Phone * + - textbox "Contact Phone *" [ref=e189]: "+966501234567" + - generic [ref=e190]: + - generic [ref=e191]: Contact Email + - textbox "Contact Email" [ref=e192] + - generic [ref=e193]: + - heading "Inquiry Details" [level=6] [ref=e194]: + - img [ref=e195] + - text: Inquiry Details + - generic [ref=e198]: + - generic [ref=e199]: + - generic [ref=e200]: Inquiry Type * + - combobox "Inquiry Type *" [ref=e201]: + - option "General Inquiry" + - option "Appointment Related" [selected] + - option "Billing & Insurance" + - option "Medical Records" + - option "Pharmacy" + - option "Insurance" + - option "Feedback" + - option "Other" + - generic [ref=e202]: + - generic [ref=e203]: Priority + - combobox "Priority" [ref=e204]: + - option "Low" + - option "Medium" [selected] + - option "High" + - option "Critical" + - generic [ref=e205]: + - generic [ref=e206]: Subject * + - textbox "Subject *" [ref=e207]: + - /placeholder: Brief subject + - text: Playwright Test Inquiry + - generic [ref=e208]: + - generic [ref=e209]: Message * + - textbox "Message *" [ref=e210]: + - /placeholder: Describe your inquiry + - text: This is a test inquiry created by Playwright to verify px-employee access. + - generic [ref=e211]: + - button "Create Inquiry" [ref=e212] [cursor=pointer]: + - img [ref=e213] + - text: Create Inquiry + - link "Cancel" [ref=e216] [cursor=pointer]: + - /url: /inquiries/ + - contentinfo [ref=e217]: + - paragraph [ref=e218]: + - text: Powered by + - link "tenhal.sa" [ref=e219] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-03-758Z.yml b/.playwright-cli/page-2026-07-12T20-40-03-758Z.yml new file mode 100644 index 0000000..00ab721 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-03-758Z.yml @@ -0,0 +1,39 @@ +- generic [ref=e3]: + - generic [ref=e4]: + - img "HH Logo" [ref=e6] + - heading "Welcome to PX360" [level=1] [ref=e7] + - paragraph [ref=e8]: Patient Experience Management System + - generic [ref=e10]: + - generic [ref=e11]: + - generic [ref=e12]: + - img [ref=e13] + - text: Email Address + - generic [ref=e16]: + - img [ref=e17] + - textbox "Email Address" [active] [ref=e20]: + - /placeholder: Enter your email + - generic [ref=e21]: + - generic [ref=e22]: + - img [ref=e23] + - text: Password + - generic [ref=e26]: + - img [ref=e27] + - textbox "Password" [ref=e31]: + - /placeholder: Enter your password + - button [ref=e32] [cursor=pointer]: + - img [ref=e33] + - generic [ref=e36]: + - generic [ref=e37] [cursor=pointer]: + - checkbox "Remember me" [ref=e38] + - generic [ref=e39]: Remember me + - link "Forgot password?" [ref=e40] [cursor=pointer]: + - /url: /accounts/password/reset/ + - button "Sign In" [ref=e41] [cursor=pointer]: + - img [ref=e42] + - text: Sign In + - generic [ref=e45]: + - paragraph [ref=e46]: + - text: Secure login powered by + - link "tenhal.sa" [ref=e47] [cursor=pointer]: + - /url: https://tenhal.sa + - paragraph [ref=e48]: © 2026 Al Hammadi Hospital \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-08-887Z.yml b/.playwright-cli/page-2026-07-12T20-40-08-887Z.yml new file mode 100644 index 0000000..2dc8a26 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-08-887Z.yml @@ -0,0 +1,350 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Departments" [ref=e141] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e142] + - generic [ref=e144]: Contact Center Department + - generic [ref=e147]: + - generic [ref=e148]: + - img [ref=e150] + - generic [ref=e154]: + - heading "Contact Center Department" [level=1] [ref=e155] + - paragraph [ref=e156]: قسم مركز الاتصال + - generic [ref=e157]: + - generic [ref=e158]: + - img [ref=e159] + - text: hh_n_contact_center_department + - generic [ref=e162]: Administrative + - generic [ref=e163]: + - img [ref=e164] + - text: E2E Test Hospital + - generic [ref=e168]: + - generic [ref=e170]: + - img [ref=e172] + - generic [ref=e177]: + - paragraph [ref=e178]: Staff + - paragraph [ref=e179]: "4" + - link "Open Complaints 3 3 total" [ref=e180] [cursor=pointer]: + - /url: /organizations/departments/d810102c-d14a-4661-8672-6ef85a77fc8f/complaints/?status=open + - generic [ref=e181]: + - img [ref=e183] + - generic [ref=e186]: + - paragraph [ref=e187]: Open Complaints + - paragraph [ref=e188]: "3" + - paragraph [ref=e189]: 3 total + - link "Pending Inquiries 0 2 total" [ref=e190] [cursor=pointer]: + - /url: /organizations/departments/d810102c-d14a-4661-8672-6ef85a77fc8f/inquiries/?status=open + - generic [ref=e191]: + - img [ref=e193] + - generic [ref=e196]: + - paragraph [ref=e197]: Pending Inquiries + - paragraph [ref=e198]: "0" + - paragraph [ref=e199]: 2 total + - link "Open Observations 1 1 total" [ref=e200] [cursor=pointer]: + - /url: /organizations/departments/d810102c-d14a-4661-8672-6ef85a77fc8f/observations/?status=open + - generic [ref=e201]: + - img [ref=e203] + - generic [ref=e206]: + - paragraph [ref=e207]: Open Observations + - paragraph [ref=e208]: "1" + - paragraph [ref=e209]: 1 total + - generic [ref=e210]: + - generic [ref=e211]: + - img [ref=e213] + - generic [ref=e215]: + - heading "Pending Actions" [level=3] [ref=e216] + - paragraph [ref=e217]: 3 items awaiting response + - table [ref=e219]: + - rowgroup [ref=e220]: + - row "Type Reference Subject SLA Deadline Action" [ref=e221]: + - columnheader "Type" [ref=e222] + - columnheader "Reference" [ref=e223] + - columnheader "Subject" [ref=e224] + - columnheader "SLA Deadline" [ref=e225] + - columnheader "Action" [ref=e226] + - rowgroup [ref=e227]: + - row "Complaint Response CMP-202607-0006 Test Complaint - Analysis Required - Respond" [ref=e228]: + - cell "Complaint Response" [ref=e229]: + - generic [ref=e230]: Complaint Response + - cell "CMP-202607-0006" [ref=e231] + - cell "Test Complaint - Analysis Required" [ref=e232] + - cell "-" [ref=e233] + - cell "Respond" [ref=e234]: + - link "Respond" [ref=e236] [cursor=pointer]: + - /url: /complaints/f8aa6216-bb94-455e-ba18-d16a01024662/explain/dM0ilaUpzA1OuAceyjvwV9oxYA0geW_jsgipvrYUt_g/ + - img [ref=e237] + - text: Respond + - row "Complaint Explanation CMP-202607-0005 Test Complaint - No Details - Respond" [ref=e239]: + - cell "Complaint Explanation" [ref=e240]: + - generic [ref=e241]: Complaint Explanation + - cell "CMP-202607-0005" [ref=e242] + - cell "Test Complaint - No Details" [ref=e243] + - cell "-" [ref=e244] + - cell "Respond" [ref=e245]: + - link "Respond" [ref=e247] [cursor=pointer]: + - /url: /complaints/9ca01d52-712d-4bca-b30d-6f1f818ff839/explain/iBIf0PrMq1RZYSEIDxSn4WZik4PFk6dunoNwarFTnXI/ + - img [ref=e248] + - text: Respond + - row "Complaint Explanation CMP-202607-0005 Test Complaint - No Details - Respond" [ref=e252]: + - cell "Complaint Explanation" [ref=e253]: + - generic [ref=e254]: Complaint Explanation + - cell "CMP-202607-0005" [ref=e255] + - cell "Test Complaint - No Details" [ref=e256] + - cell "-" [ref=e257] + - cell "Respond" [ref=e258]: + - link "Respond" [ref=e260] [cursor=pointer]: + - /url: /complaints/9ca01d52-712d-4bca-b30d-6f1f818ff839/explain/X1Zd54Tb1NcExZeRr6ACN5RtPCwB_RTKeqNeFcVP24E/ + - img [ref=e261] + - text: Respond + - generic [ref=e266]: + - button "Dashboard" [ref=e267] [cursor=pointer]: + - img [ref=e268] + - text: Dashboard + - button "Staff (4)" [ref=e270] [cursor=pointer]: + - img [ref=e271] + - text: Staff (4) + - button "Roles" [ref=e276] [cursor=pointer]: + - img [ref=e277] + - text: Roles + - button "Sections (2)" [ref=e289] [cursor=pointer]: + - img [ref=e290] + - text: Sections (2) + - button "Complaints (3)" [ref=e294] [cursor=pointer]: + - img [ref=e295] + - text: Complaints (3) + - button "Inquiries (2)" [ref=e298] [cursor=pointer]: + - img [ref=e299] + - text: Inquiries (2) + - button "Observations (1)" [ref=e302] [cursor=pointer]: + - img [ref=e303] + - text: Observations (1) + - button "Suggestions (1)" [ref=e306] [cursor=pointer]: + - img [ref=e307] + - text: Suggestions (1) + - button "Appreciations (2)" [ref=e309] [cursor=pointer]: + - img [ref=e310] + - text: Appreciations (2) + - button "Standards (0)" [ref=e312] [cursor=pointer]: + - img [ref=e313] + - text: Standards (0) + - button "Projects (8)" [ref=e316] [cursor=pointer]: + - img [ref=e317] + - text: Projects (8) + - generic [ref=e321]: + - generic [ref=e322]: + - generic [ref=e323]: + - paragraph [ref=e324]: Avg Physician Rating + - paragraph [ref=e325]: "0.0" + - paragraph [ref=e326]: out of 5.0 + - generic [ref=e327]: + - paragraph [ref=e328]: Resolution Rate + - paragraph [ref=e329]: 0% + - paragraph [ref=e330]: 3 total + - generic [ref=e331]: + - paragraph [ref=e332]: Open Actions + - paragraph [ref=e333]: "3" + - paragraph [ref=e334]: PX actions pending + - generic [ref=e335]: + - paragraph [ref=e336]: Satisfaction Rate + - paragraph [ref=e337]: 0% + - paragraph [ref=e338]: resolution satisfaction + - generic [ref=e339]: + - paragraph [ref=e340]: Reopened + - paragraph [ref=e341]: "0" + - paragraph [ref=e342]: reopened complaints + - generic [ref=e343]: + - paragraph [ref=e344]: Reassigned + - paragraph [ref=e345]: "5" + - paragraph [ref=e346]: reassigned complaints + - generic [ref=e347]: + - generic [ref=e348]: + - heading "Complaint Trend" [level=4] [ref=e349] + - generic [ref=e351]: + - application "area chart with 1 data series" [ref=e352]: + - generic [ref=e355]: + - generic "4" [ref=e356] + - generic "3.5" [ref=e357] + - generic "3" [ref=e358] + - generic "2.5" [ref=e359] + - generic "2" [ref=e360] + - generic [ref=e361]: + - generic [ref=e365]: + - generic: + - generic: + - generic: "3" + - generic "2026-07" [ref=e371] + - tooltip: ● + - generic [ref=e372]: + - heading "Physician Rating Trend" [level=4] [ref=e373] + - paragraph [ref=e375]: No physician rating data + - generic [ref=e376]: + - heading "Complaint Status" [level=4] [ref=e377] + - generic [ref=e379]: + - application "donut chart with 1 data series" [ref=e380]: + - generic [ref=e386]: + - generic: + - generic: 100.0% + - button "In Progress, visible. Press Enter or Space to toggle." [ref=e390] [cursor=pointer]: + - generic [ref=e391]: + - img + - generic [ref=e392]: In Progress + - text: ● + - generic [ref=e393]: + - heading "Actions Overview" [level=4] [ref=e394] + - generic [ref=e396]: + - application "donut chart with 2 data series" [ref=e397]: + - generic [ref=e403]: + - generic: + - generic: 66.7% + - generic: + - generic: 33.3% + - generic [ref=e406]: + - button "In Progress, visible. Press Enter or Space to toggle." [ref=e407] [cursor=pointer]: + - generic [ref=e408]: + - img + - generic [ref=e409]: In Progress + - button "Open, visible. Press Enter or Space to toggle." [ref=e410] [cursor=pointer]: + - generic [ref=e411]: + - img + - generic [ref=e412]: Open + - text: ● ● + - generic [ref=e413]: + - heading "Complaint Severity" [level=4] [ref=e414] + - generic [ref=e416]: + - application "bar chart with 1 data series" [ref=e417]: + - generic [ref=e420]: + - generic "2" [ref=e421] + - generic "1.5" [ref=e422] + - generic "1" [ref=e423] + - generic "0.5" [ref=e424] + - generic "0" [ref=e425] + - generic [ref=e426]: + - generic [ref=e430]: + - generic: + - generic: + - generic: "2" + - generic: + - generic: "1" + - generic [ref=e435]: + - generic "Low" [ref=e436] + - generic "Medium" [ref=e437] + - tooltip: ● + - generic [ref=e438]: + - heading "Satisfaction Distribution" [level=4] [ref=e439] + - paragraph [ref=e441]: No satisfaction data + - generic [ref=e442]: + - heading "Top Physicians" [level=4] [ref=e443] + - paragraph [ref=e445]: No physician data + - generic [ref=e446]: + - heading "Top Categories" [level=4] [ref=e447] + - generic [ref=e449]: + - application "bar chart with 1 data series" [ref=e450]: + - generic [ref=e452]: + - generic [ref=e455]: + - generic: + - generic: + - generic: "3" + - generic "Institutional Issues" [ref=e460] + - generic [ref=e462]: + - generic "3.0" [ref=e463] + - generic "2.5" [ref=e464] + - generic "2.0" [ref=e465] + - generic "1.5" [ref=e466] + - generic "1.0" [ref=e467] + - generic "0.5" [ref=e468] + - generic "0.0" [ref=e469] + - tooltip: ● + - contentinfo [ref=e470]: + - paragraph [ref=e471]: + - text: Powered by + - link "tenhal.sa" [ref=e472] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-21-782Z.yml b/.playwright-cli/page-2026-07-12T20-40-21-782Z.yml new file mode 100644 index 0000000..1f12389 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-21-782Z.yml @@ -0,0 +1,258 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Inquiries Console" [level=1] [ref=e142] + - paragraph [ref=e143]: Manage patient inquiries and requests + - link "New Inquiry" [ref=e144] [cursor=pointer]: + - /url: /inquiries/new/ + - img [ref=e145] + - text: New Inquiry + - generic [ref=e146]: + - generic [ref=e147]: + - img [ref=e150] + - generic [ref=e153]: + - heading "Total Inquiries" [level=6] [ref=e154] + - heading "5" [level=2] [ref=e155] + - generic [ref=e156]: + - img [ref=e159] + - generic [ref=e162]: + - heading "Resolved" [level=6] [ref=e163] + - heading "2 (40.0%)" [level=2] [ref=e164] + - generic [ref=e165]: + - img [ref=e168] + - generic [ref=e171]: + - heading "In Progress" [level=6] [ref=e172] + - heading "1" [level=2] [ref=e173] + - generic [ref=e174]: + - img [ref=e177] + - generic [ref=e179]: + - heading "Overdue" [level=6] [ref=e180] + - heading "0" [level=2] [ref=e181] + - generic [ref=e182]: + - generic [ref=e183]: + - img [ref=e185] + - heading "Filters" [level=5] [ref=e186] + - generic [ref=e188]: + - generic [ref=e189]: + - generic [ref=e190]: Search + - textbox "Subject, contact name..." [ref=e191] + - generic [ref=e192]: + - generic [ref=e193]: Status + - combobox [ref=e194]: + - option "All Status" [selected] + - option "Open" + - option "In Progress" + - option "Resolved" + - option "Closed" + - generic [ref=e195]: + - generic [ref=e196]: Category + - combobox [ref=e197]: + - option "All Categories" [selected] + - option "General" + - option "Appointments" + - option "Billing" + - option "Medical Records" + - option "Pharmacy" + - option "Insurance" + - option "Feedback" + - option "Other" + - button "Filter" [ref=e198] [cursor=pointer]: + - img [ref=e199] + - text: Filter + - generic [ref=e202]: + - generic [ref=e203]: + - generic [ref=e204]: + - img [ref=e206] + - heading "All Inquiries" [level=5] [ref=e209] + - generic [ref=e210]: "Total: 5" + - paragraph [ref=e211]: "Showing: 1-5 of 5" + - generic [ref=e212]: + - table [ref=e213]: + - rowgroup [ref=e214]: + - row "Reference Subject Contact Category Status Priority Created Assigned" [ref=e215]: + - columnheader "Reference" [ref=e216] + - columnheader "Subject" [ref=e217] + - columnheader "Contact" [ref=e218] + - columnheader "Category" [ref=e219] + - columnheader "Status" [ref=e220] + - columnheader "Priority" [ref=e221] + - columnheader "Created" [ref=e222] + - columnheader "Assigned" [ref=e223] + - rowgroup [ref=e224]: + - row "INQ-202607-0007 test12121212 ismail mosa 0569874563 General Information Open Medium Jul 12, 2026 Unassigned" [ref=e225] [cursor=pointer]: + - cell "INQ-202607-0007" [ref=e226]: + - code [ref=e227]: INQ-202607-0007 + - cell "test12121212" [ref=e228]: + - link "test12121212" [ref=e229]: + - /url: /inquiries/b4a65498-e8d8-403c-82bb-ae09adc7fc1c/ + - cell "ismail mosa 0569874563" [ref=e230]: + - generic [ref=e231]: ismail mosa + - generic [ref=e232]: "0569874563" + - cell "General Information" [ref=e233] + - cell "Open" [ref=e234] + - cell "Medium" [ref=e235] + - cell "Jul 12, 2026" [ref=e236] + - cell "Unassigned" [ref=e237] + - row "INQ-202607-0006 test12121212 test 0569874563 General Information Open Medium Jul 12, 2026 Unassigned" [ref=e238] [cursor=pointer]: + - cell "INQ-202607-0006" [ref=e239]: + - code [ref=e240]: INQ-202607-0006 + - cell "test12121212" [ref=e241]: + - link "test12121212" [ref=e242]: + - /url: /inquiries/1c3eda08-ff03-4521-bcb8-ec137c79b417/ + - cell "test 0569874563" [ref=e243]: + - generic [ref=e244]: test + - generic [ref=e245]: "0569874563" + - cell "General Information" [ref=e246] + - cell "Open" [ref=e247] + - cell "Medium" [ref=e248] + - cell "Jul 12, 2026" [ref=e249] + - cell "Unassigned" [ref=e250] + - row "INQ-202607-0005 test test 0569874589 General Information In Progress Medium Jul 09, 2026 Unassigned" [ref=e251] [cursor=pointer]: + - cell "INQ-202607-0005" [ref=e252]: + - code [ref=e253]: INQ-202607-0005 + - cell "test" [ref=e254]: + - link "test" [ref=e255]: + - /url: /inquiries/c05309c2-3518-46ae-9b1c-c144acdb5d58/ + - cell "test 0569874589" [ref=e256]: + - generic [ref=e257]: test + - generic [ref=e258]: "0569874589" + - cell "General Information" [ref=e259] + - cell "In Progress" [ref=e260] + - cell "Medium" [ref=e261] + - cell "Jul 09, 2026" [ref=e262] + - cell "Unassigned" [ref=e263] + - row "INQ-202607-0004 hi ismail 0569874589 General Information Resolved Medium Jul 09, 2026 ismail mosa" [ref=e264] [cursor=pointer]: + - cell "INQ-202607-0004" [ref=e265]: + - code [ref=e266]: INQ-202607-0004 + - cell "hi" [ref=e267]: + - link "hi" [ref=e268]: + - /url: /inquiries/d1df0b83-5c16-4f51-b735-e89f612deb50/ + - cell "ismail 0569874589" [ref=e269]: + - generic [ref=e270]: ismail + - generic [ref=e271]: "0569874589" + - cell "General Information" [ref=e272] + - cell "Resolved" [ref=e273] + - cell "Medium" [ref=e274] + - cell "Jul 09, 2026" [ref=e275] + - cell "ismail mosa" [ref=e276] + - row "INQ-202607-0003 test test 0569874589 General Information Resolved Medium Jul 08, 2026 ismail mosa" [ref=e277] [cursor=pointer]: + - cell "INQ-202607-0003" [ref=e278]: + - code [ref=e279]: INQ-202607-0003 + - cell "test" [ref=e280]: + - link "test" [ref=e281]: + - /url: /inquiries/031cbd06-e2dc-4cd9-9b81-b1218e090d6c/ + - cell "test 0569874589" [ref=e282]: + - generic [ref=e283]: test + - generic [ref=e284]: "0569874589" + - cell "General Information" [ref=e285] + - cell "Resolved" [ref=e286] + - cell "Medium" [ref=e287] + - cell "Jul 08, 2026" [ref=e288] + - cell "ismail mosa" [ref=e289] + - generic [ref=e290]: + - generic [ref=e291]: + - generic [ref=e292]: Showing 1-5 of 5 + - generic [ref=e293]: + - generic [ref=e294]: Show + - combobox [ref=e295]: + - option "10" + - option "25" [selected] + - option "50" + - option "100" + - generic [ref=e296]: + - img [ref=e298] + - generic [ref=e300]: "1" + - img [ref=e302] + - contentinfo [ref=e304]: + - paragraph [ref=e305]: + - text: Powered by + - link "tenhal.sa" [ref=e306] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-22-923Z.yml b/.playwright-cli/page-2026-07-12T20-40-22-923Z.yml new file mode 100644 index 0000000..f34d18f --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-22-923Z.yml @@ -0,0 +1,243 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Complaints Registry" [level=1] [ref=e142] + - paragraph [ref=e143]: Manage and monitor patient feedback in real-time + - generic [ref=e144]: + - generic [ref=e145]: + - img [ref=e146] + - textbox "Search ID, Name or Dept..." [ref=e149] + - link "Public Form" [ref=e150] [cursor=pointer]: + - /url: /core/public/submit/ + - img [ref=e151] + - text: Public Form + - button "Export" [ref=e155] [cursor=pointer]: + - img [ref=e156] + - text: Export + - link "New Case" [ref=e159] [cursor=pointer]: + - /url: /complaints/new/ + - img [ref=e160] + - text: New Case + - generic [ref=e161]: + - generic [ref=e162]: + - img [ref=e165] + - generic [ref=e169]: + - heading "Total Received" [level=6] [ref=e170] + - heading "3" [level=2] [ref=e171] + - generic [ref=e172]: + - img [ref=e175] + - generic [ref=e178]: + - heading "Resolved" [level=6] [ref=e179] + - heading "0 (0.0%)" [level=2] [ref=e180] + - generic [ref=e181]: + - img [ref=e184] + - generic [ref=e187]: + - heading "Pending" [level=6] [ref=e188] + - heading "3 (100.0%)" [level=2] [ref=e189] + - generic [ref=e190]: + - img [ref=e193] + - generic [ref=e195]: + - heading "TAT Alert" [level=6] [ref=e196] + - heading "3 (100.0%)" [level=2] [ref=e197] + - generic [ref=e198]: + - generic [ref=e199]: + - generic [ref=e200]: + - img [ref=e202] + - heading "Complaints" [level=5] [ref=e204] + - generic [ref=e205]: + - link "All Cases" [ref=e206] [cursor=pointer]: + - /url: "?" + - link "Pending" [ref=e207] [cursor=pointer]: + - /url: "?status=open" + - link "In Progress" [ref=e208] [cursor=pointer]: + - /url: "?status=in_progress" + - link "Resolved" [ref=e209] [cursor=pointer]: + - /url: "?status=resolved" + - button "Advanced" [ref=e211] [cursor=pointer]: + - img [ref=e212] + - text: Advanced + - paragraph [ref=e213]: "Showing: 1-3 of 3" + - generic [ref=e214]: + - table [ref=e215]: + - rowgroup [ref=e216]: + - row "Complaint ID Patient Name Description Source Department Status SLA Severity Priority Assigned Actions" [ref=e217]: + - columnheader "Complaint ID" [ref=e218] + - columnheader "Patient Name" [ref=e219] + - columnheader "Description" [ref=e220] + - columnheader "Source" [ref=e221] + - columnheader "Department" [ref=e222] + - columnheader "Status" [ref=e223] + - columnheader "SLA" [ref=e224] + - columnheader "Severity" [ref=e225] + - columnheader "Priority" [ref=e226] + - columnheader "Assigned" [ref=e227] + - columnheader "Actions" [ref=e228] + - rowgroup [ref=e229]: + - row "CMP-202607-0006 test شكوى عامة Patient Patient Experience Department In Progress Overdue 21h 46m Medium Medium test test" [ref=e230] [cursor=pointer]: + - cell "CMP-202607-0006" [ref=e231] + - cell "test" [ref=e232]: + - generic [ref=e233]: test + - cell "شكوى عامة" [ref=e234]: + - generic [ref=e235]: شكوى عامة + - button "View description" [ref=e236]: + - img [ref=e237] + - cell "Patient" [ref=e246] + - cell "Patient Experience Department" [ref=e247] + - cell "In Progress" [ref=e248] + - cell "Overdue 21h 46m" [ref=e249] + - cell "Medium" [ref=e250] + - cell "Medium" [ref=e251] + - cell "test test" [ref=e252] + - cell [ref=e253]: + - generic [ref=e254]: + - link "View" [ref=e255]: + - /url: /complaints/f8aa6216-bb94-455e-ba18-d16a01024662/ + - img [ref=e256] + - link "Assign" [ref=e259]: + - /url: /complaints/f8aa6216-bb94-455e-ba18-d16a01024662/assign/ + - img [ref=e260] + - row "CMP-202607-0005 ismail شكوى اختبار Patient Contact Center Department In Progress Overdue 23h 22m Low Low test test" [ref=e263] [cursor=pointer]: + - cell "CMP-202607-0005" [ref=e264] + - cell "ismail" [ref=e265]: + - generic [ref=e266]: ismail + - cell "شكوى اختبار" [ref=e267]: + - generic [ref=e268]: شكوى اختبار + - button "View description" [ref=e269]: + - img [ref=e270] + - cell "Patient" [ref=e279] + - cell "Contact Center Department" [ref=e280] + - cell "In Progress" [ref=e281] + - cell "Overdue 23h 22m" [ref=e282] + - cell "Low" [ref=e283] + - cell "Low" [ref=e284] + - cell "test test" [ref=e285] + - cell [ref=e286]: + - link "View" [ref=e288]: + - /url: /complaints/9ca01d52-712d-4bca-b30d-6f1f818ff839/ + - img [ref=e289] + - row "CMP-202607-0004 OVR ismail اختبار عام Internal Contact Center Department In Progress Overdue 4d 7h 22m Low Low ismail mosa" [ref=e292] [cursor=pointer]: + - cell "CMP-202607-0004 OVR" [ref=e293] + - cell "ismail" [ref=e294]: + - generic [ref=e295]: ismail + - cell "اختبار عام" [ref=e296]: + - generic [ref=e297]: اختبار عام + - button "View description" [ref=e298]: + - img [ref=e299] + - cell "Internal" [ref=e308] + - cell "Contact Center Department" [ref=e309] + - cell "In Progress" [ref=e310] + - cell "Overdue 4d 7h 22m" [ref=e311] + - cell "Low" [ref=e312] + - cell "Low" [ref=e313] + - cell "ismail mosa" [ref=e314] + - cell [ref=e315]: + - link "View" [ref=e317]: + - /url: /complaints/1c4e8dde-48d8-42ab-8c8d-4ada8314fc40/ + - img [ref=e318] + - generic [ref=e321]: + - generic [ref=e322]: + - generic [ref=e323]: Showing 1-3 of 3 + - generic [ref=e324]: + - generic [ref=e325]: Show + - combobox [ref=e326]: + - option "10" + - option "25" [selected] + - option "50" + - option "100" + - generic [ref=e327]: + - img [ref=e329] + - generic [ref=e331]: "1" + - img [ref=e333] + - contentinfo [ref=e335]: + - paragraph [ref=e336]: + - text: Powered by + - link "tenhal.sa" [ref=e337] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-24-046Z.yml b/.playwright-cli/page-2026-07-12T20-40-24-046Z.yml new file mode 100644 index 0000000..8f4ebf2 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-24-046Z.yml @@ -0,0 +1,344 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Observations" [level=1] [ref=e142]: + - img [ref=e143] + - text: Observations + - paragraph [ref=e146]: Manage and triage staff-reported observations + - generic [ref=e147]: + - link "New Observation" [ref=e148] [cursor=pointer]: + - /url: /observations/create/ + - img [ref=e149] + - text: New Observation + - link "Public Form" [ref=e151] [cursor=pointer]: + - /url: /core/public/submit/ + - img [ref=e152] + - text: Public Form + - generic [ref=e153]: + - generic [ref=e155]: + - generic [ref=e156]: + - paragraph [ref=e157]: Total + - paragraph [ref=e158]: "2" + - img [ref=e160] + - generic [ref=e162]: + - generic [ref=e163]: + - paragraph [ref=e164]: New + - paragraph + - img [ref=e166] + - generic [ref=e170]: + - generic [ref=e171]: + - paragraph [ref=e172]: In Progress + - paragraph [ref=e173]: "2" + - img [ref=e175] + - generic [ref=e178]: + - generic [ref=e179]: + - img [ref=e181] + - heading "Filters" [level=5] [ref=e183] + - button [ref=e184] [cursor=pointer]: + - img [ref=e185] + - generic [ref=e188]: + - generic [ref=e189]: + - generic [ref=e190]: + - generic [ref=e191]: Search + - textbox "Tracking code, description..." [ref=e192] + - generic [ref=e193]: + - generic [ref=e194]: Status + - combobox [ref=e195]: + - option "All Statuses" [selected] + - option "Open" + - option "In Progress" + - option "Resolved" + - option "Closed" + - generic [ref=e196]: + - generic [ref=e197]: Severity + - combobox [ref=e198]: + - option "All Severities" [selected] + - option "Low" + - option "Medium" + - option "High" + - option "Critical" + - generic [ref=e199]: + - generic [ref=e200]: Category + - combobox [ref=e201]: + - option "All Categories" [selected] + - option "Patient Safety" + - option "Clinical Quality" + - option "Infection Control" + - option "Medication Safety" + - option "Equipment & Devices" + - option "Facility & Environment" + - option "Staff Behavior" + - option "Communication" + - option "Documentation" + - option "Process & Workflow" + - option "Security" + - option "IT & Systems" + - option "Housekeeping" + - option "Food Services" + - option "Other" + - generic [ref=e202]: + - generic [ref=e203]: Department + - combobox [ref=e204]: + - option "All Departments" [selected] + - option "Contact Center Department" + - option "Emergency Administrative Department" + - option "Executive Administration" + - option "Financial Collection & Claims Department" + - option "Inpatient Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Security Department" + - option "Anesthesia Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "Emergency Medicine Department" + - option "IVF" + - option "Infection Control Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Surgery Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Nursing Department" + - option "Biomedical Department" + - option "Facility Management & Maintenance Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "Information Technology Department" + - option "Laundry Department" + - generic [ref=e206] [cursor=pointer]: + - generic [ref=e207]: All Departments + - combobox "All Departments" [ref=e208] + - generic [ref=e209]: + - generic [ref=e210]: Assigned To + - combobox [ref=e211]: + - option "All Users" [selected] + - option "E2E PX Employee" + - option "test test" + - option "test test" + - generic [ref=e212]: + - generic [ref=e213]: Date From + - textbox [ref=e214] + - generic [ref=e215]: + - generic [ref=e216]: Date To + - textbox [ref=e217] + - generic [ref=e218]: + - button "Apply Filters" [ref=e219] [cursor=pointer]: + - img [ref=e220] + - text: Apply Filters + - link "Clear" [ref=e223] [cursor=pointer]: + - /url: /observations/ + - img [ref=e224] + - text: Clear + - generic [ref=e229]: + - img [ref=e231] + - heading "Observations List" [level=5] [ref=e233] + - generic [ref=e234]: Showing 1 to 4 of 4 observations + - table [ref=e237]: + - rowgroup [ref=e238]: + - row "Reference Date Category Description Severity Status Reporter Department Assigned Actions" [ref=e239]: + - columnheader "Reference" [ref=e240] + - columnheader "Date" [ref=e241] + - columnheader "Category" [ref=e242] + - columnheader "Description" [ref=e243] + - columnheader "Severity" [ref=e244] + - columnheader "Status" [ref=e245] + - columnheader "Reporter" [ref=e246] + - columnheader "Department" [ref=e247] + - columnheader "Assigned" [ref=e248] + - columnheader "Actions" [ref=e249] + - rowgroup [ref=e250]: + - row "OBS-202607-0001 Jul 08, 2026 16:33 - test observation Medium In Progress ismail mosa Contact Center Department ismail mosa" [ref=e251]: + - cell "OBS-202607-0001" [ref=e252] + - cell "Jul 08, 2026 16:33" [ref=e253]: + - text: Jul 08, 2026 + - text: 16:33 + - cell "-" [ref=e254] + - cell "test observation" [ref=e255]: + - generic [ref=e256]: test observation + - cell "Medium" [ref=e257] + - cell "In Progress" [ref=e258] + - cell "ismail mosa" [ref=e259] + - cell "Contact Center Department" [ref=e260] + - cell "ismail mosa" [ref=e261] + - cell [ref=e262]: + - link [ref=e263] [cursor=pointer]: + - /url: /observations/1d925706-110e-4bad-808f-26aae2029144/ + - img [ref=e264] + - row "OBS-202606-E2EHOSP-0056 Jun 22, 2026 10:16 Other Observation i notice some trash in the hall Medium In Progress E2E PX Admin Contact Center Department E2E PX Admin" [ref=e267]: + - cell "OBS-202606-E2EHOSP-0056" [ref=e268] + - cell "Jun 22, 2026 10:16" [ref=e269]: + - text: Jun 22, 2026 + - text: 10:16 + - cell "Other" [ref=e270]: + - generic [ref=e271]: Other + - cell "Observation i notice some trash in the hall" [ref=e272]: + - generic [ref=e273]: + - strong [ref=e274]: Observation + - text: i notice some trash in the hall + - cell "Medium" [ref=e275] + - cell "In Progress" [ref=e276] + - cell "E2E PX Admin" [ref=e277] + - cell "Contact Center Department" [ref=e278] + - cell "E2E PX Admin" [ref=e279] + - cell [ref=e280]: + - link [ref=e281] [cursor=pointer]: + - /url: /observations/b9a80c99-31dc-4cda-a1e3-c20d94a28729/ + - img [ref=e282] + - row "OBS-O8TCIC Jun 09, 2026 13:36 - Wet floor near the main entrance, no warning sign posted. Medium In Progress Anonymous Reporter - ismail mosa" [ref=e285]: + - cell "OBS-O8TCIC" [ref=e286] + - cell "Jun 09, 2026 13:36" [ref=e287]: + - text: Jun 09, 2026 + - text: 13:36 + - cell "-" [ref=e288] + - cell "Wet floor near the main entrance, no warning sign posted." [ref=e289]: + - generic [ref=e290]: Wet floor near the main entrance, no warning sign posted. + - cell "Medium" [ref=e291] + - cell "In Progress" [ref=e292] + - cell "Anonymous Reporter" [ref=e293] + - cell "-" [ref=e294] + - cell "ismail mosa" [ref=e295] + - cell [ref=e296]: + - link [ref=e297] [cursor=pointer]: + - /url: /observations/5f8fc6c6-3b38-4b88-8144-3f926c24e290/ + - img [ref=e298] + - row "OBS-QPE02F Jun 07, 2026 21:08 Other Observation safsafsad Medium Open ismail Daif - Unassigned" [ref=e301]: + - cell "OBS-QPE02F" [ref=e302] + - cell "Jun 07, 2026 21:08" [ref=e303]: + - text: Jun 07, 2026 + - text: 21:08 + - cell "Other" [ref=e304]: + - generic [ref=e305]: Other + - cell "Observation safsafsad" [ref=e306]: + - generic [ref=e307]: + - strong [ref=e308]: Observation + - text: safsafsad + - cell "Medium" [ref=e309] + - cell "Open" [ref=e310] + - cell "ismail Daif" [ref=e311] + - cell "-" [ref=e312] + - cell "Unassigned" [ref=e313] + - cell [ref=e314]: + - link [ref=e315] [cursor=pointer]: + - /url: /observations/327d9b79-2126-4800-8cd7-647f006271ce/ + - img [ref=e316] + - generic [ref=e319]: + - generic [ref=e320]: + - generic [ref=e321]: Showing 1-4 of 4 + - generic [ref=e322]: + - generic [ref=e323]: Show + - combobox [ref=e324]: + - option "10" + - option "25" [selected] + - option "50" + - option "100" + - generic [ref=e325]: + - img [ref=e327] + - generic [ref=e329]: "1" + - img [ref=e331] + - contentinfo [ref=e333]: + - paragraph [ref=e334]: + - text: Powered by + - link "tenhal.sa" [ref=e335] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-25-079Z.yml b/.playwright-cli/page-2026-07-12T20-40-25-079Z.yml new file mode 100644 index 0000000..d6747b0 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-25-079Z.yml @@ -0,0 +1,201 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Suggestions" [level=1] [ref=e142] + - paragraph [ref=e143]: Manage and review submitted suggestions + - link "New Suggestion" [ref=e145] [cursor=pointer]: + - /url: /suggestions/create/ + - img [ref=e146] + - text: New Suggestion + - generic [ref=e147]: + - generic [ref=e148]: + - img [ref=e151] + - generic [ref=e153]: + - heading "Total Suggestions" [level=6] [ref=e154] + - heading "3" [level=2] [ref=e155] + - generic [ref=e156]: + - img [ref=e159] + - generic [ref=e161]: + - heading "Compliments" [level=6] [ref=e162] + - heading "0" [level=2] [ref=e163] + - generic [ref=e164]: + - img [ref=e167] + - generic [ref=e169]: + - heading "Avg Rating" [level=6] [ref=e170] + - heading "0.0 /5" [level=2] [ref=e171] + - generic [ref=e172]: + - img [ref=e175] + - generic [ref=e178]: + - heading "Pending Review" [level=6] [ref=e179] + - heading "2" [level=2] [ref=e180] + - generic [ref=e181]: + - generic [ref=e182]: + - generic [ref=e183]: + - img [ref=e185] + - heading "Suggestions" [level=5] [ref=e187] + - generic [ref=e188]: + - link "All" [ref=e189] [cursor=pointer]: + - /url: "?" + - link "Suggestions" [ref=e190] [cursor=pointer]: + - /url: "?feedback_type=suggestion" + - link "Compliments" [ref=e191] [cursor=pointer]: + - /url: "?feedback_type=compliment" + - button "Advanced" [ref=e193] [cursor=pointer]: + - img [ref=e194] + - text: Advanced + - paragraph [ref=e195]: "Showing: 1-3 of 3" + - table [ref=e197]: + - rowgroup [ref=e198]: + - row "ID Type Contact Title Category Rating Sentiment Status Created Actions" [ref=e199]: + - columnheader "ID" [ref=e200] + - columnheader "Type" [ref=e201] + - columnheader "Contact" [ref=e202] + - columnheader "Title" [ref=e203] + - columnheader "Category" [ref=e204] + - columnheader "Rating" [ref=e205] + - columnheader "Sentiment" [ref=e206] + - columnheader "Status" [ref=e207] + - columnheader "Created" [ref=e208] + - columnheader "Actions" [ref=e209] + - rowgroup [ref=e210]: + - row "SGT-202607-0003 Suggestion ismail Daif t Other - Neutral Submitted Jul 09, 2026" [ref=e211] [cursor=pointer]: + - cell "SGT-202607-0003" [ref=e212] + - cell "Suggestion" [ref=e213] + - cell "ismail Daif" [ref=e214]: + - generic [ref=e215]: ismail Daif + - cell "t" [ref=e216]: + - generic [ref=e217]: t + - cell "Other" [ref=e218] + - cell "-" [ref=e219] + - cell "Neutral" [ref=e220] + - cell "Submitted" [ref=e221] + - cell "Jul 09, 2026" [ref=e222] + - cell [ref=e223]: + - link "View" [ref=e225]: + - /url: /suggestions/78f015c5-fbed-4e10-987f-aa8ced75545a/ + - img [ref=e226] + - row "SGT-202607-0002 Suggestion ismail test Other - Neutral Submitted Jul 09, 2026" [ref=e229] [cursor=pointer]: + - cell "SGT-202607-0002" [ref=e230] + - cell "Suggestion" [ref=e231] + - cell "ismail" [ref=e232]: + - generic [ref=e233]: ismail + - cell "test" [ref=e234]: + - generic [ref=e235]: test + - cell "Other" [ref=e236] + - cell "-" [ref=e237] + - cell "Neutral" [ref=e238] + - cell "Submitted" [ref=e239] + - cell "Jul 09, 2026" [ref=e240] + - cell [ref=e241]: + - link "View" [ref=e243]: + - /url: /suggestions/04b009f3-53aa-4eac-9baf-39b3633e56b0/ + - img [ref=e244] + - row "SGT-202607-0001 Suggestion ismail The suggestion titled 'test' with content 'test' appears … Other - Neutral Closed Jul 09, 2026" [ref=e247] [cursor=pointer]: + - cell "SGT-202607-0001" [ref=e248] + - cell "Suggestion" [ref=e249] + - cell "ismail" [ref=e250]: + - generic [ref=e251]: ismail + - cell "The suggestion titled 'test' with content 'test' appears …" [ref=e252]: + - generic [ref=e253]: The suggestion titled 'test' with content 'test' appears … + - cell "Other" [ref=e254] + - cell "-" [ref=e255] + - cell "Neutral" [ref=e256] + - cell "Closed" [ref=e257] + - cell "Jul 09, 2026" [ref=e258] + - cell [ref=e259]: + - link "View" [ref=e261]: + - /url: /suggestions/ef1e633b-7fae-46e2-baab-0d5bdd71e7b7/ + - img [ref=e262] + - contentinfo [ref=e265]: + - paragraph [ref=e266]: + - text: Powered by + - link "tenhal.sa" [ref=e267] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-26-032Z.yml b/.playwright-cli/page-2026-07-12T20-40-26-032Z.yml new file mode 100644 index 0000000..ddeb3a0 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-26-032Z.yml @@ -0,0 +1,221 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Appreciations" [level=1] [ref=e142]: + - img [ref=e143] + - text: Appreciations + - paragraph [ref=e145]: Manage and review patient appreciation submissions + - generic [ref=e146]: + - link "Create Internal" [ref=e147] [cursor=pointer]: + - /url: /appreciation/new/ + - img [ref=e148] + - text: Create Internal + - link "Public Form" [ref=e149] [cursor=pointer]: + - /url: /core/public/submit/ + - img [ref=e150] + - text: Public Form + - generic [ref=e154]: + - paragraph [ref=e155]: "0" + - paragraph [ref=e156]: Pending + - generic [ref=e157]: + - generic [ref=e159]: + - img [ref=e161] + - generic [ref=e164]: + - paragraph [ref=e165]: Total + - paragraph [ref=e166]: "3" + - generic [ref=e168]: + - img [ref=e170] + - generic [ref=e173]: + - paragraph [ref=e174]: Draft + - paragraph [ref=e175]: "0" + - generic [ref=e177]: + - img [ref=e179] + - generic [ref=e181]: + - paragraph [ref=e182]: Activated + - paragraph [ref=e183]: "0" + - generic [ref=e185]: + - img [ref=e187] + - generic [ref=e190]: + - paragraph [ref=e191]: Sent + - paragraph [ref=e192]: "3" + - generic [ref=e193]: + - link "Leaderboard See top performers" [ref=e194] [cursor=pointer]: + - /url: /appreciation/leaderboard/ + - generic [ref=e196]: + - img [ref=e198] + - generic [ref=e204]: + - heading "Leaderboard" [level=3] [ref=e205] + - paragraph [ref=e206]: See top performers + - link "My Badges View earned badges" [ref=e207] [cursor=pointer]: + - /url: /appreciation/badges/ + - generic [ref=e209]: + - img [ref=e211] + - generic [ref=e214]: + - heading "My Badges" [level=3] [ref=e215] + - paragraph [ref=e216]: View earned badges + - generic [ref=e217]: + - generic [ref=e218]: + - img [ref=e220] + - heading "Filters" [level=3] [ref=e222] + - generic [ref=e224]: + - generic [ref=e225]: + - generic [ref=e226]: Search + - textbox "Search messages..." [ref=e227] + - generic [ref=e228]: + - generic [ref=e229]: Status + - combobox [ref=e230]: + - option "All Status" [selected] + - option "Draft" + - option "Activated" + - option "AI Analyzed" + - option "Sent" + - option "Acknowledged" + - button "Search" [ref=e231] [cursor=pointer]: + - img [ref=e232] + - text: Search + - link "Clear" [ref=e235] [cursor=pointer]: + - /url: /appreciation/ + - table [ref=e237]: + - rowgroup [ref=e238]: + - row "From Message Staff Department Category Status Date Actions" [ref=e239]: + - columnheader "From" [ref=e240] + - columnheader "Message" [ref=e241] + - columnheader "Staff" [ref=e242] + - columnheader "Department" [ref=e243] + - columnheader "Category" [ref=e244] + - columnheader "Status" [ref=e245] + - columnheader "Date" [ref=e246] + - columnheader "Actions" [ref=e247] + - rowgroup [ref=e248]: + - row "ismail thank - Contact Center Department - Sent Jul 09, 2026" [ref=e249] [cursor=pointer]: + - cell "ismail" [ref=e250] + - cell "thank" [ref=e251]: + - generic [ref=e252]: thank + - cell "-" [ref=e253] + - cell "Contact Center Department" [ref=e254] + - cell "-" [ref=e255] + - cell "Sent" [ref=e256] + - cell "Jul 09, 2026" [ref=e257] + - cell [ref=e258]: + - link "View" [ref=e260]: + - /url: /appreciation/detail/5306821e-d596-498f-88c5-fa3addcdd491/ + - img [ref=e261] + - row "ismail mosa test - Contact Center Department - Sent Jul 09, 2026" [ref=e264] [cursor=pointer]: + - cell "ismail mosa" [ref=e265] + - cell "test" [ref=e266]: + - generic [ref=e267]: test + - cell "-" [ref=e268] + - cell "Contact Center Department" [ref=e269] + - cell "-" [ref=e270] + - cell "Sent" [ref=e271] + - cell "Jul 09, 2026" [ref=e272] + - cell [ref=e273]: + - link "View" [ref=e275]: + - /url: /appreciation/detail/3697b78b-5ff3-4392-8982-af20f58daebf/ + - img [ref=e276] + - row "ismail mosa thank you - Contact Center Department - Sent Jul 09, 2026" [ref=e279] [cursor=pointer]: + - cell "ismail mosa" [ref=e280] + - cell "thank you" [ref=e281]: + - generic [ref=e282]: thank you + - cell "-" [ref=e283] + - cell "Contact Center Department" [ref=e284] + - cell "-" [ref=e285] + - cell "Sent" [ref=e286] + - cell "Jul 09, 2026" [ref=e287] + - cell [ref=e288]: + - link "View" [ref=e290]: + - /url: /appreciation/detail/240ea5bd-a12d-4f76-8a79-a8dd9de5714a/ + - img [ref=e291] + - contentinfo [ref=e294]: + - paragraph [ref=e295]: + - text: Powered by + - link "tenhal.sa" [ref=e296] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-27-116Z.yml b/.playwright-cli/page-2026-07-12T20-40-27-116Z.yml new file mode 100644 index 0000000..1ec6ae9 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-27-116Z.yml @@ -0,0 +1,256 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e141]: + - generic [ref=e142]: + - link "Inquiries" [ref=e143] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e144] + - generic [ref=e146]: New Inquiry + - heading "Create New Inquiry" [level=1] [ref=e147] + - generic [ref=e149]: + - generic [ref=e150]: + - img [ref=e152] + - heading "Inquiry Information" [level=5] [ref=e155] + - generic [ref=e156]: + - generic [ref=e157]: + - heading "Organization" [level=6] [ref=e158]: + - img [ref=e159] + - text: Organization + - generic [ref=e162]: + - generic [ref=e163]: + - generic [ref=e164]: Location Type * + - combobox "Location Type *" [ref=e165]: + - option "Select Location Type" [selected] + - option "Outpatient" + - option "Inpatient" + - option "Emergency" + - option "General" + - generic [ref=e166]: + - generic [ref=e167]: Area + - combobox "Area" [ref=e168]: + - option "Select Area (optional)" [selected] + - option "Anesthesia (E2E-HOSP)" + - option "Dialysis (E2E-HOSP)" + - option "Emergency (E2E-HOSP)" + - option "Endoscopy (E2E-HOSP)" + - option "ICCU (E2E-HOSP)" + - option "ICU (E2E-HOSP)" + - option "ICU Stepdown (E2E-HOSP)" + - option "LTACU (E2E-HOSP)" + - option "Labor & Delivery (E2E-HOSP)" + - option "Main OR (E2E-HOSP)" + - option "Medical Ward (E2E-HOSP)" + - option "NICU - Neonatal Intensive Care Unit (E2E-HOSP)" + - option "Nursery (E2E-HOSP)" + - option "OB Wards (E2E-HOSP)" + - option "OPD1 (E2E-HOSP)" + - option "OPD2 (E2E-HOSP)" + - option "OPD3 (E2E-HOSP)" + - option "OPD4 (E2E-HOSP)" + - option "OPD5 (E2E-HOSP)" + - option "OPD6 (E2E-HOSP)" + - option "OPD7 (E2E-HOSP)" + - option "PICU (E2E-HOSP)" + - option "PICU Stepdown (E2E-HOSP)" + - option "Pediatric Ward (E2E-HOSP)" + - option "Recovery (E2E-HOSP)" + - option "Stepdown 3-4 (E2E-HOSP)" + - option "Surgical Wards (E2E-HOSP)" + - generic [ref=e169]: + - generic [ref=e170]: Department Category * + - combobox [ref=e171]: + - option "Select Category" [selected] + - option "Medical" + - option "Nursing" + - option "Administrative" + - option "Support Services" + - generic [ref=e172]: + - generic [ref=e173]: Department (Optional) + - combobox "Department (Optional)" [ref=e174]: + - option "Select Department" [selected] + - option "Anesthesia Department" + - option "Biomedical Department" + - option "Contact Center Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Emergency Administrative Department" + - option "Emergency Medicine Department" + - option "Executive Administration" + - option "Facility Management & Maintenance Department" + - option "Financial Collection & Claims Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "IVF" + - option "Infection Control Department" + - option "Information Technology Department" + - option "Inpatient Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Laundry Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Nursing Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Security Department" + - option "Surgery Department" + - generic [ref=e175]: + - generic [ref=e176]: Section + - combobox "Section" [ref=e177]: + - option "Select Section" [selected] + - generic [ref=e178]: + - heading "Contact Information" [level=6] [ref=e179]: + - img [ref=e180] + - text: Contact Information + - generic [ref=e183]: + - generic [ref=e184]: + - generic [ref=e185]: Contact Name * + - textbox "Contact Name *" [ref=e186] + - generic [ref=e187]: + - generic [ref=e188]: Contact Phone * + - textbox "Contact Phone *" [ref=e189] + - generic [ref=e190]: + - generic [ref=e191]: Contact Email + - textbox "Contact Email" [ref=e192] + - generic [ref=e193]: + - heading "Inquiry Details" [level=6] [ref=e194]: + - img [ref=e195] + - text: Inquiry Details + - generic [ref=e198]: + - generic [ref=e199]: + - generic [ref=e200]: Inquiry Type * + - combobox "Inquiry Type *" [ref=e201]: + - option "General Inquiry" [selected] + - option "Appointment Related" + - option "Billing & Insurance" + - option "Medical Records" + - option "Pharmacy" + - option "Insurance" + - option "Feedback" + - option "Other" + - generic [ref=e202]: + - generic [ref=e203]: Priority + - combobox "Priority" [ref=e204]: + - option "Low" + - option "Medium" [selected] + - option "High" + - option "Critical" + - generic [ref=e205]: + - generic [ref=e206]: Subject * + - textbox "Subject *" [ref=e207]: + - /placeholder: Brief subject + - generic [ref=e208]: + - generic [ref=e209]: Message * + - textbox "Message *" [ref=e210]: + - /placeholder: Describe your inquiry + - generic [ref=e211]: + - button "Create Inquiry" [ref=e212] [cursor=pointer]: + - img [ref=e213] + - text: Create Inquiry + - link "Cancel" [ref=e216] [cursor=pointer]: + - /url: /inquiries/ + - contentinfo [ref=e217]: + - paragraph [ref=e218]: + - text: Powered by + - link "tenhal.sa" [ref=e219] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-27-876Z.yml b/.playwright-cli/page-2026-07-12T20-40-27-876Z.yml new file mode 100644 index 0000000..c6b59e6 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-27-876Z.yml @@ -0,0 +1,124 @@ +- generic [active] [ref=e1]: + - banner [ref=e2]: + - generic [ref=e4]: + - generic [ref=e5]: + - img "HH Logo" [ref=e7] + - generic [ref=e8]: + - heading "PX360" [level=1] [ref=e9] + - paragraph [ref=e10]: Patient Experience Management + - generic [ref=e11]: + - link "🇬🇧 English" [ref=e12] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e13]: 🇬🇧 + - generic [ref=e14]: English + - link "🇸🇦 Arabic" [ref=e15] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e16]: 🇸🇦 + - generic [ref=e17]: Arabic + - main [ref=e18]: + - generic [ref=e21]: + - generic [ref=e22]: + - img [ref=e24] + - heading "Report an Observation" [level=1] [ref=e27] + - paragraph [ref=e28]: Help us improve by reporting issues you notice + - generic [ref=e30]: + - img [ref=e31] + - generic [ref=e34]: + - strong [ref=e35]: Anonymous Reporting + - paragraph [ref=e36]: You can submit this report anonymously. Providing your information is optional but may help us follow up if needed. + - generic [ref=e37]: + - generic [ref=e38]: + - generic [ref=e39]: + - img [ref=e40] + - text: Hospital + - generic [ref=e44]: (optional) + - combobox [ref=e45]: + - option "Select hospital" [selected] + - option "Al Nuzha" + - option "Al Olya" + - option "Al Suwaidi" + - option "Al-Hammadi Hospital (KPI Test)" + - option "E2E Test Hospital" + - generic [ref=e46]: + - generic [ref=e47]: + - img [ref=e48] + - text: Location Type + - generic [ref=e51]: "*" + - combobox [ref=e52]: + - option "Select Location Type" [selected] + - option "Outpatient (OP)" + - option "Inpatient (IP)" + - option "Emergency (ER)" + - option "General" + - generic [ref=e53]: + - generic [ref=e54]: + - img [ref=e55] + - text: Category + - generic [ref=e58]: (optional) + - combobox [ref=e59]: + - option "Select Category" [selected] + - option "Medical" + - option "Nursing" + - option "Administrative" + - option "Support Services" + - generic [ref=e60]: + - generic [ref=e61]: + - img [ref=e62] + - text: Description + - generic [ref=e65]: "*" + - textbox "Please describe what you observed in detail..." [ref=e66] + - paragraph [ref=e67]: Please describe what you observed in detail. + - generic [ref=e68]: + - generic [ref=e69]: + - img [ref=e70] + - text: Location Details + - generic [ref=e73]: (optional) + - textbox "e.g., Building A, Floor 2, Room 205" [ref=e74] + - generic [ref=e75]: + - generic [ref=e76]: + - img [ref=e77] + - text: When did this occur? + - textbox [ref=e79]: 2026-07-12T23:40:27 + - generic [ref=e80]: + - generic [ref=e81]: + - img [ref=e82] + - text: Attachments + - generic [ref=e84]: (optional) + - generic [ref=e85] [cursor=pointer]: + - img [ref=e86] + - paragraph [ref=e89]: Click to upload files + - paragraph [ref=e90]: Images, PDF, Word, Excel (max 10MB each) + - generic [ref=e91]: + - generic [ref=e92]: + - img [ref=e93] + - text: Your Information + - generic [ref=e96]: (optional) + - paragraph [ref=e97]: Providing your information helps us follow up if needed. Leave blank to submit anonymously. + - generic [ref=e98]: + - generic [ref=e99]: + - generic [ref=e100]: Staff ID + - textbox "Your staff ID (optional)" [ref=e101] + - generic [ref=e102]: + - generic [ref=e103]: Name + - textbox "Your name (optional)" [ref=e104] + - generic [ref=e105]: + - generic [ref=e106]: Phone + - textbox "Your phone number (optional)" [ref=e107] + - generic [ref=e108]: + - generic [ref=e109]: Email + - textbox "Your email (optional)" [ref=e110] + - button "Submit Observation" [ref=e111] [cursor=pointer]: + - img [ref=e112] + - text: Submit Observation + - link "Track an existing observation" [ref=e116] [cursor=pointer]: + - /url: /observations/track/ + - img [ref=e117] + - text: Track an existing observation + - contentinfo [ref=e120]: + - generic [ref=e121]: + - paragraph [ref=e122]: © 2026 Al Hammadi Hospital - PX360. All rights reserved. + - paragraph [ref=e123]: Your feedback helps us improve our services + - paragraph [ref=e124]: + - text: Powered by + - link "tenhal.sa" [ref=e125] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-29-030Z.yml b/.playwright-cli/page-2026-07-12T20-40-29-030Z.yml new file mode 100644 index 0000000..027f8ee --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-29-030Z.yml @@ -0,0 +1,133 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - generic [ref=e142]: + - link "Suggestions" [ref=e143] [cursor=pointer]: + - /url: /suggestions/ + - img [ref=e144] + - generic [ref=e146]: New + - heading "New Suggestion" [level=1] [ref=e147] + - link "Back" [ref=e148] [cursor=pointer]: + - /url: /suggestions/ + - img [ref=e149] + - text: Back + - generic [ref=e151]: + - generic [ref=e152]: + - img [ref=e154] + - heading "Suggestion Details" [level=5] [ref=e156] + - generic [ref=e157]: + - generic [ref=e158]: + - generic [ref=e159]: + - generic [ref=e160]: Your Name * + - textbox "Your full name" [ref=e161] + - generic [ref=e162]: + - generic [ref=e163]: Phone Number * + - textbox "Your phone number" [ref=e164] + - generic [ref=e165]: + - generic [ref=e166]: Hospital * + - combobox [ref=e167]: + - option "Select Hospital" [selected] + - option "Al Nuzha" + - option "Al Olya" + - option "Al Suwaidi" + - option "Al-Hammadi Hospital (KPI Test)" + - option "E2E Test Hospital" + - generic [ref=e168]: + - generic [ref=e169]: Message * + - textbox "Describe your suggestion in detail..." [ref=e170] + - button "Submit Suggestion" [ref=e172] [cursor=pointer]: + - img [ref=e173] + - text: Submit Suggestion + - contentinfo [ref=e176]: + - paragraph [ref=e177]: + - text: Powered by + - link "tenhal.sa" [ref=e178] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-30-328Z.yml b/.playwright-cli/page-2026-07-12T20-40-30-328Z.yml new file mode 100644 index 0000000..344e645 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-30-328Z.yml @@ -0,0 +1,260 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e141]: + - generic [ref=e142]: + - heading "New Appreciation" [level=1] [ref=e143]: + - img [ref=e144] + - text: New Appreciation + - paragraph [ref=e146]: Recognize a department for excellent service or care. + - link "Back" [ref=e147] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e148] + - text: Back + - generic [ref=e150]: + - generic [ref=e151]: + - generic [ref=e152]: Hospital * + - combobox "Hospital *" [ref=e153]: + - option "---------" + - option "Al Nuzha" + - option "Al Olya" + - option "Al Suwaidi" + - option "Al-Hammadi Hospital (KPI Test)" + - option "E2E Test Hospital" [selected] + - generic [ref=e154]: + - generic [ref=e155]: Department * + - combobox "Department *" [ref=e156]: + - option "---------" [selected] + - option "Anesthesia Department — Al Nuzha" + - option "Biomedical Department — Al Nuzha" + - option "Contact Center Department — Al Nuzha" + - option "Critical Care Department — Al Nuzha" + - option "Dental Department — Al Nuzha" + - option "Dermatology Department — Al Nuzha" + - option "Emergency Administrative Department — Al Nuzha" + - option "Emergency Medicine Department — Al Nuzha" + - option "Executive Administration — Al Nuzha" + - option "Facility Management & Maintenance Department — Al Nuzha" + - option "Financial Collection & Claims Department — Al Nuzha" + - option "Food Services Department — Al Nuzha" + - option "Housekeeping & Hospitality Department — Al Nuzha" + - option "IVF — Al Nuzha" + - option "Infection Control Department — Al Nuzha" + - option "Information Technology Department — Al Nuzha" + - option "Inpatient Department — Al Nuzha" + - option "Internal Medicine Department — Al Nuzha" + - option "Laboratory Department — Al Nuzha" + - option "Laundry Department — Al Nuzha" + - option "Medical Administration — Al Nuzha" + - option "Medical Ancillary Services Department — Al Nuzha" + - option "Medical Approvals Department — Al Nuzha" + - option "Medical Records Department — Al Nuzha" + - option "Nursing Department — Al Nuzha" + - option "ONCOLOGY Department — Al Nuzha" + - option "Obstetrics & Gynecology Department — Al Nuzha" + - option "Operating Rooms (OR) Department — Al Nuzha" + - option "Ophthalmology Department — Al Nuzha" + - option "Outpatient Department — Al Nuzha" + - option "Patient Affairs Department — Al Nuzha" + - option "Patient Experience Department — Al Nuzha" + - option "Pediatric Department — Al Nuzha" + - option "Pharmacy Department — Al Nuzha" + - option "Radiology Department — Al Nuzha" + - option "Security Department — Al Nuzha" + - option "Surgery Department — Al Nuzha" + - option "Facility Management & Maintenance Department — Al Olya" + - option "HR Department — Al Olya" + - option "Support Services Department — Al Olya" + - option "Anesthesia Department — Al Suwaidi" + - option "Biomedical Department — Al Suwaidi" + - option "Contact Center Department — Al Suwaidi" + - option "Critical Care Department — Al Suwaidi" + - option "Dental Department — Al Suwaidi" + - option "Dermatology Department — Al Suwaidi" + - option "Emergency Administrative Department — Al Suwaidi" + - option "Emergency Medicine Department — Al Suwaidi" + - option "Executive Administration — Al Suwaidi" + - option "Facility Management & Maintenance Department — Al Suwaidi" + - option "Financial Collection & Claims Department — Al Suwaidi" + - option "Food Services Department — Al Suwaidi" + - option "Housekeeping & Hospitality Department — Al Suwaidi" + - option "IVF — Al Suwaidi" + - option "Infection Control Department — Al Suwaidi" + - option "Information Technology Department — Al Suwaidi" + - option "Inpatient Department — Al Suwaidi" + - option "Internal Medicine Department — Al Suwaidi" + - option "Laboratory Department — Al Suwaidi" + - option "Laundry Department — Al Suwaidi" + - option "Medical Administration — Al Suwaidi" + - option "Medical Ancillary Services Department — Al Suwaidi" + - option "Medical Approvals Department — Al Suwaidi" + - option "Medical Records Department — Al Suwaidi" + - option "Nursing Department — Al Suwaidi" + - option "ONCOLOGY Department — Al Suwaidi" + - option "Obstetrics & Gynecology Department — Al Suwaidi" + - option "Operating Rooms (OR) Department — Al Suwaidi" + - option "Ophthalmology Department — Al Suwaidi" + - option "Outpatient Department — Al Suwaidi" + - option "Patient Affairs Department — Al Suwaidi" + - option "Patient Experience Department — Al Suwaidi" + - option "Pediatric Department — Al Suwaidi" + - option "Pharmacy Department — Al Suwaidi" + - option "Radiology Department — Al Suwaidi" + - option "Security Department — Al Suwaidi" + - option "Surgery Department — Al Suwaidi" + - option "Administration — Al-Hammadi Hospital (KPI Test)" + - option "Cardiology — Al-Hammadi Hospital (KPI Test)" + - option "Emergency — Al-Hammadi Hospital (KPI Test)" + - option "Housekeeping — Al-Hammadi Hospital (KPI Test)" + - option "ICU — Al-Hammadi Hospital (KPI Test)" + - option "Laboratory — Al-Hammadi Hospital (KPI Test)" + - option "Maintenance — Al-Hammadi Hospital (KPI Test)" + - option "Medical Reports — Al-Hammadi Hospital (KPI Test)" + - option "Nursing — Al-Hammadi Hospital (KPI Test)" + - option "Orthopedics — Al-Hammadi Hospital (KPI Test)" + - option "Pediatrics — Al-Hammadi Hospital (KPI Test)" + - option "Pharmacy — Al-Hammadi Hospital (KPI Test)" + - option "Reception — Al-Hammadi Hospital (KPI Test)" + - option "Security — Al-Hammadi Hospital (KPI Test)" + - option "Surgery — Al-Hammadi Hospital (KPI Test)" + - option "Anesthesia Department — E2E Test Hospital" + - option "Biomedical Department — E2E Test Hospital" + - option "Contact Center Department — E2E Test Hospital" + - option "Critical Care Department — E2E Test Hospital" + - option "Dental Department — E2E Test Hospital" + - option "Dermatology Department — E2E Test Hospital" + - option "E2E Dept 1781720032405 — E2E Test Hospital" + - option "E2E Dept 1781720188555 — E2E Test Hospital" + - option "Emergency Administrative Department — E2E Test Hospital" + - option "Emergency Medicine Department — E2E Test Hospital" + - option "Executive Administration — E2E Test Hospital" + - option "Facility Management & Maintenance Department — E2E Test Hospital" + - option "Financial Collection & Claims Department — E2E Test Hospital" + - option "Food Services Department — E2E Test Hospital" + - option "Housekeeping & Hospitality Department — E2E Test Hospital" + - option "IVF — E2E Test Hospital" + - option "Infection Control Department — E2E Test Hospital" + - option "Information Technology Department — E2E Test Hospital" + - option "Inpatient Department — E2E Test Hospital" + - option "Internal Medicine Department — E2E Test Hospital" + - option "Laboratory Department — E2E Test Hospital" + - option "Laundry Department — E2E Test Hospital" + - option "Medical Administration — E2E Test Hospital" + - option "Medical Ancillary Services Department — E2E Test Hospital" + - option "Medical Approvals Department — E2E Test Hospital" + - option "Medical Records Department — E2E Test Hospital" + - option "Nursing Department — E2E Test Hospital" + - option "ONCOLOGY Department — E2E Test Hospital" + - option "Obstetrics & Gynecology Department — E2E Test Hospital" + - option "Operating Rooms (OR) Department — E2E Test Hospital" + - option "Ophthalmology Department — E2E Test Hospital" + - option "Outpatient Department — E2E Test Hospital" + - option "Patient Affairs Department — E2E Test Hospital" + - option "Patient Experience Department — E2E Test Hospital" + - option "Pediatric Department — E2E Test Hospital" + - option "Pharmacy Department — E2E Test Hospital" + - option "Radiology Department — E2E Test Hospital" + - option "Security Department — E2E Test Hospital" + - option "Surgery Department — E2E Test Hospital" + - generic [ref=e157]: + - generic [ref=e158]: Message * + - textbox "Message *" [ref=e159]: + - /placeholder: Write your message of appreciation... + - generic [ref=e160]: + - button "Create Appreciation" [ref=e161] [cursor=pointer]: + - img [ref=e162] + - text: Create Appreciation + - link "Cancel" [ref=e164] [cursor=pointer]: + - /url: /appreciation/ + - contentinfo [ref=e165]: + - paragraph [ref=e166]: + - text: Powered by + - link "tenhal.sa" [ref=e167] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-51-825Z.yml b/.playwright-cli/page-2026-07-12T20-40-51-825Z.yml new file mode 100644 index 0000000..178fa5d --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-51-825Z.yml @@ -0,0 +1,172 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Inquiries" [ref=e141] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e142] + - generic [ref=e144]: INQ-202607-0007 + - generic [ref=e145]: Open + - generic [ref=e146]: Medium + - generic [ref=e147]: + - img [ref=e148] + - text: Not Sent to Dept + - generic [ref=e150]: + - heading "test12121212" [level=1] [ref=e151] + - generic [ref=e153]: + - img [ref=e154] + - text: 2026-07-12 + - generic [ref=e157]: + - generic [ref=e158]: + - img [ref=e160] + - generic [ref=e162]: Created + - generic [ref=e164]: + - img [ref=e166] + - generic [ref=e169]: Assigned + - generic [ref=e171]: + - img [ref=e173] + - generic [ref=e176]: Sent to Dept + - generic [ref=e178]: + - img [ref=e180] + - generic [ref=e182]: Dept Response + - generic [ref=e184]: + - img [ref=e186] + - generic [ref=e189]: Resolved + - generic [ref=e190]: + - generic [ref=e191]: + - img [ref=e193] + - generic [ref=e195]: + - paragraph [ref=e196]: Activate this inquiry + - paragraph [ref=e197]: Activation moves it to In Progress so actions become available. + - generic [ref=e198]: + - button "Cancel" [ref=e200] [cursor=pointer] + - button "Activate" [ref=e202] [cursor=pointer] + - navigation [ref=e203]: + - button "Details" [ref=e204] [cursor=pointer] + - button "Timeline" [ref=e205] [cursor=pointer] + - button "Notes" [ref=e206] [cursor=pointer] + - main [ref=e207]: + - generic [ref=e209]: + - generic [ref=e210]: + - generic [ref=e211]: + - paragraph + - paragraph [ref=e212]: "231313131" + - paragraph + - generic [ref=e213]: + - generic [ref=e215]: + - img [ref=e216] + - generic [ref=e219]: AI Analysis + - generic [ref=e220]: + - paragraph [ref=e221]: "231313131" + - paragraph [ref=e222]: "231313131" + - generic [ref=e223]: + - heading "Patient" [level=3] [ref=e224]: + - img [ref=e225] + - text: Patient + - generic [ref=e228]: + - generic [ref=e229]: ismail mosa + - generic [ref=e230]: "|" + - generic [ref=e231]: + - img [ref=e232] + - text: "0569874563" + - generic [ref=e235]: + - heading "Quick Actions" [level=3] [ref=e236] + - generic [ref=e237]: + - button "Activate" [ref=e239] [cursor=pointer]: + - img [ref=e240] + - generic [ref=e243]: Activate + - button "Cancel Inquiry" [ref=e245] [cursor=pointer]: + - img [ref=e246] + - generic [ref=e250]: Cancel Inquiry + - contentinfo [ref=e251]: + - paragraph [ref=e252]: + - text: Powered by + - link "tenhal.sa" [ref=e253] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-53-136Z.yml b/.playwright-cli/page-2026-07-12T20-40-53-136Z.yml new file mode 100644 index 0000000..0d8032a --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-53-136Z.yml @@ -0,0 +1,243 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e140]: + - generic [ref=e141]: + - heading "Complaints Registry" [level=1] [ref=e142] + - paragraph [ref=e143]: Manage and monitor patient feedback in real-time + - generic [ref=e144]: + - generic [ref=e145]: + - img [ref=e146] + - textbox "Search ID, Name or Dept..." [ref=e149] + - link "Public Form" [ref=e150] [cursor=pointer]: + - /url: /core/public/submit/ + - img [ref=e151] + - text: Public Form + - button "Export" [ref=e155] [cursor=pointer]: + - img [ref=e156] + - text: Export + - link "New Case" [ref=e159] [cursor=pointer]: + - /url: /complaints/new/ + - img [ref=e160] + - text: New Case + - generic [ref=e161]: + - generic [ref=e162]: + - img [ref=e165] + - generic [ref=e169]: + - heading "Total Received" [level=6] [ref=e170] + - heading "3" [level=2] [ref=e171] + - generic [ref=e172]: + - img [ref=e175] + - generic [ref=e178]: + - heading "Resolved" [level=6] [ref=e179] + - heading "0 (0.0%)" [level=2] [ref=e180] + - generic [ref=e181]: + - img [ref=e184] + - generic [ref=e187]: + - heading "Pending" [level=6] [ref=e188] + - heading "3 (100.0%)" [level=2] [ref=e189] + - generic [ref=e190]: + - img [ref=e193] + - generic [ref=e195]: + - heading "TAT Alert" [level=6] [ref=e196] + - heading "3 (100.0%)" [level=2] [ref=e197] + - generic [ref=e198]: + - generic [ref=e199]: + - generic [ref=e200]: + - img [ref=e202] + - heading "Complaints" [level=5] [ref=e204] + - generic [ref=e205]: + - link "All Cases" [ref=e206] [cursor=pointer]: + - /url: "?" + - link "Pending" [ref=e207] [cursor=pointer]: + - /url: "?status=open" + - link "In Progress" [ref=e208] [cursor=pointer]: + - /url: "?status=in_progress" + - link "Resolved" [ref=e209] [cursor=pointer]: + - /url: "?status=resolved" + - button "Advanced" [ref=e211] [cursor=pointer]: + - img [ref=e212] + - text: Advanced + - paragraph [ref=e213]: "Showing: 1-3 of 3" + - generic [ref=e214]: + - table [ref=e215]: + - rowgroup [ref=e216]: + - row "Complaint ID Patient Name Description Source Department Status SLA Severity Priority Assigned Actions" [ref=e217]: + - columnheader "Complaint ID" [ref=e218] + - columnheader "Patient Name" [ref=e219] + - columnheader "Description" [ref=e220] + - columnheader "Source" [ref=e221] + - columnheader "Department" [ref=e222] + - columnheader "Status" [ref=e223] + - columnheader "SLA" [ref=e224] + - columnheader "Severity" [ref=e225] + - columnheader "Priority" [ref=e226] + - columnheader "Assigned" [ref=e227] + - columnheader "Actions" [ref=e228] + - rowgroup [ref=e229]: + - row "CMP-202607-0006 test شكوى عامة Patient Patient Experience Department In Progress Overdue 21h 47m Medium Medium test test" [ref=e230] [cursor=pointer]: + - cell "CMP-202607-0006" [ref=e231] + - cell "test" [ref=e232]: + - generic [ref=e233]: test + - cell "شكوى عامة" [ref=e234]: + - generic [ref=e235]: شكوى عامة + - button "View description" [ref=e236]: + - img [ref=e237] + - cell "Patient" [ref=e246] + - cell "Patient Experience Department" [ref=e247] + - cell "In Progress" [ref=e248] + - cell "Overdue 21h 47m" [ref=e249] + - cell "Medium" [ref=e250] + - cell "Medium" [ref=e251] + - cell "test test" [ref=e252] + - cell [ref=e253]: + - generic [ref=e254]: + - link "View" [ref=e255]: + - /url: /complaints/f8aa6216-bb94-455e-ba18-d16a01024662/ + - img [ref=e256] + - link "Assign" [ref=e259]: + - /url: /complaints/f8aa6216-bb94-455e-ba18-d16a01024662/assign/ + - img [ref=e260] + - row "CMP-202607-0005 ismail شكوى اختبار Patient Contact Center Department In Progress Overdue 23h 22m Low Low test test" [ref=e263] [cursor=pointer]: + - cell "CMP-202607-0005" [ref=e264] + - cell "ismail" [ref=e265]: + - generic [ref=e266]: ismail + - cell "شكوى اختبار" [ref=e267]: + - generic [ref=e268]: شكوى اختبار + - button "View description" [ref=e269]: + - img [ref=e270] + - cell "Patient" [ref=e279] + - cell "Contact Center Department" [ref=e280] + - cell "In Progress" [ref=e281] + - cell "Overdue 23h 22m" [ref=e282] + - cell "Low" [ref=e283] + - cell "Low" [ref=e284] + - cell "test test" [ref=e285] + - cell [ref=e286]: + - link "View" [ref=e288]: + - /url: /complaints/9ca01d52-712d-4bca-b30d-6f1f818ff839/ + - img [ref=e289] + - row "CMP-202607-0004 OVR ismail اختبار عام Internal Contact Center Department In Progress Overdue 4d 7h 22m Low Low ismail mosa" [ref=e292] [cursor=pointer]: + - cell "CMP-202607-0004 OVR" [ref=e293] + - cell "ismail" [ref=e294]: + - generic [ref=e295]: ismail + - cell "اختبار عام" [ref=e296]: + - generic [ref=e297]: اختبار عام + - button "View description" [ref=e298]: + - img [ref=e299] + - cell "Internal" [ref=e308] + - cell "Contact Center Department" [ref=e309] + - cell "In Progress" [ref=e310] + - cell "Overdue 4d 7h 22m" [ref=e311] + - cell "Low" [ref=e312] + - cell "Low" [ref=e313] + - cell "ismail mosa" [ref=e314] + - cell [ref=e315]: + - link "View" [ref=e317]: + - /url: /complaints/1c4e8dde-48d8-42ab-8c8d-4ada8314fc40/ + - img [ref=e318] + - generic [ref=e321]: + - generic [ref=e322]: + - generic [ref=e323]: Showing 1-3 of 3 + - generic [ref=e324]: + - generic [ref=e325]: Show + - combobox [ref=e326]: + - option "10" + - option "25" [selected] + - option "50" + - option "100" + - generic [ref=e327]: + - img [ref=e329] + - generic [ref=e331]: "1" + - img [ref=e333] + - contentinfo [ref=e335]: + - paragraph [ref=e336]: + - text: Powered by + - link "tenhal.sa" [ref=e337] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-56-706Z.yml b/.playwright-cli/page-2026-07-12T20-40-56-706Z.yml new file mode 100644 index 0000000..96ab83c --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-56-706Z.yml @@ -0,0 +1,259 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Observations" [ref=e141] [cursor=pointer]: + - /url: /observations/ + - img [ref=e142] + - generic [ref=e144]: OBS-202607-0001 + - generic [ref=e145]: In Progress + - generic [ref=e146]: Medium + - generic [ref=e147]: + - img [ref=e148] + - text: Not Sent to Dept + - heading "test observation" [level=1] [ref=e151] + - generic [ref=e153]: + - generic [ref=e154]: + - img [ref=e156] + - generic [ref=e158]: Created + - generic [ref=e160]: + - img [ref=e162] + - generic [ref=e164]: Assigned + - generic [ref=e166]: + - img [ref=e168] + - generic [ref=e171]: Sent to Dept + - generic [ref=e173]: + - img [ref=e175] + - generic [ref=e177]: Dept Response + - generic [ref=e179]: + - img [ref=e181] + - generic [ref=e184]: Resolved + - navigation [ref=e185]: + - button "Details" [ref=e186] [cursor=pointer] + - button "Timeline" [ref=e187] [cursor=pointer] + - button "RCA" [ref=e188] [cursor=pointer] + - button "Notes" [ref=e189] [cursor=pointer] + - main [ref=e190]: + - generic [ref=e192]: + - generic [ref=e193]: + - paragraph [ref=e195]: test observation + - generic [ref=e196]: + - generic [ref=e197]: + - paragraph [ref=e198]: Category + - paragraph [ref=e199]: "-" + - generic [ref=e200]: + - paragraph [ref=e201]: Location + - paragraph [ref=e202]: "-" + - generic [ref=e203]: + - paragraph [ref=e204]: Incident Date + - paragraph [ref=e205]: 08 Jul 2026 + - generic [ref=e209]: + - img [ref=e210] + - paragraph [ref=e214]: No department assigned to this observation + - generic [ref=e215]: + - generic [ref=e216]: + - heading "Quick Actions" [level=3] [ref=e217] + - generic [ref=e218]: + - button "Respond to Patient" [ref=e219] [cursor=pointer]: + - img [ref=e220] + - generic [ref=e222]: Respond to Patient + - button "Reassign" [ref=e223] [cursor=pointer]: + - img [ref=e224] + - generic [ref=e227]: Reassign + - button "Send to Dept" [ref=e228] [cursor=pointer]: + - img [ref=e229] + - generic [ref=e232]: Send to Dept + - button "Resolve" [ref=e234] [cursor=pointer]: + - img [ref=e235] + - generic [ref=e238]: Resolve + - link "RCA" [ref=e239] [cursor=pointer]: + - /url: /rca/create/?related_model=observation&related_id=1d925706-110e-4bad-808f-26aae2029144 + - img [ref=e240] + - generic [ref=e243]: RCA + - link "QI Project" [ref=e244] [cursor=pointer]: + - /url: /projects/create/?related_model=observation&related_id=1d925706-110e-4bad-808f-26aae2029144 + - img [ref=e245] + - generic [ref=e247]: QI Project + - button "Delete" [ref=e249] [cursor=pointer]: + - img [ref=e250] + - generic [ref=e253]: Delete + - generic [ref=e254]: + - heading "Assignment" [level=3] [ref=e255]: + - img [ref=e256] + - text: Assignment + - list [ref=e261]: + - listitem [ref=e262]: + - generic [ref=e263]: Department + - generic [ref=e264]: Contact Center Department + - listitem [ref=e265]: + - generic [ref=e266]: Assigned To + - generic [ref=e267]: ismail mosa + - generic [ref=e268]: + - heading "Linked PX Action" [level=3] [ref=e269]: + - img [ref=e270] + - text: Linked PX Action + - paragraph [ref=e273]: Observation OBS-202607-0001 + - link "View Action" [ref=e274] [cursor=pointer]: + - /url: /actions/9b185c2a-e3fe-4e13-a727-7aab21df8754/ + - img [ref=e275] + - text: View Action + - generic [ref=e277]: + - heading "Triage" [level=3] [ref=e278]: + - img [ref=e279] + - text: Triage + - generic [ref=e280]: + - generic [ref=e281]: + - generic [ref=e282]: Department + - combobox [ref=e283]: + - option "Select department" + - option "Contact Center Department" [selected] + - option "Emergency Administrative Department" + - option "Executive Administration" + - option "Financial Collection & Claims Department" + - option "Inpatient Department" + - option "Medical Approvals Department" + - option "Medical Records Department" + - option "Outpatient Department" + - option "Patient Affairs Department" + - option "Patient Experience Department" + - option "Security Department" + - option "Anesthesia Department" + - option "Critical Care Department" + - option "Dental Department" + - option "Dermatology Department" + - option "Emergency Medicine Department" + - option "IVF" + - option "Infection Control Department" + - option "Internal Medicine Department" + - option "Laboratory Department" + - option "Medical Administration" + - option "Medical Ancillary Services Department" + - option "ONCOLOGY Department" + - option "Obstetrics & Gynecology Department" + - option "Operating Rooms (OR) Department" + - option "Ophthalmology Department" + - option "Pediatric Department" + - option "Pharmacy Department" + - option "Radiology Department" + - option "Surgery Department" + - option "E2E Dept 1781720032405" + - option "E2E Dept 1781720188555" + - option "Nursing Department" + - option "Biomedical Department" + - option "Facility Management & Maintenance Department" + - option "Food Services Department" + - option "Housekeeping & Hospitality Department" + - option "Information Technology Department" + - option "Laundry Department" + - generic [ref=e284]: + - generic [ref=e285]: Assign To + - combobox [ref=e286]: + - option "Select assignee" [selected] + - option "E2E PX Employee (e2e-px-employee@px360.test)" + - option "test test (9m5s6gzmft@ruutukf.com)" + - option "test test (esma3el555@gmail.com)" + - generic [ref=e287]: + - generic [ref=e288]: Status + - combobox [ref=e289]: + - option "Open" + - option "In Progress" [selected] + - option "Resolved" + - option "Closed" + - generic [ref=e290]: + - generic [ref=e291]: Note + - textbox "Add a note about this triage action (optional)..." [ref=e292] + - button "Update" [ref=e293] [cursor=pointer]: + - img [ref=e294] + - text: Update + - contentinfo [ref=e297]: + - paragraph [ref=e298]: + - text: Powered by + - link "tenhal.sa" [ref=e299] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-40-58-505Z.yml b/.playwright-cli/page-2026-07-12T20-40-58-505Z.yml new file mode 100644 index 0000000..3cd5765 --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-40-58-505Z.yml @@ -0,0 +1,188 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Suggestions" [ref=e141] [cursor=pointer]: + - /url: /suggestions/ + - img [ref=e142] + - generic [ref=e144]: test + - generic [ref=e145]: Submitted + - generic [ref=e146]: Medium + - generic [ref=e147]: + - heading "Suggestion Detail" [level=1] [ref=e148] + - link "Edit" [ref=e150] [cursor=pointer]: + - /url: /suggestions/04b009f3-53aa-4eac-9baf-39b3633e56b0/update/ + - img [ref=e151] + - text: Edit + - generic [ref=e155]: + - generic [ref=e156]: + - img [ref=e158] + - generic [ref=e160]: Submitted + - generic [ref=e162]: + - img [ref=e164] + - generic [ref=e167]: Assigned + - generic [ref=e169]: + - img [ref=e171] + - generic [ref=e174]: Reviewed + - generic [ref=e176]: + - img [ref=e178] + - generic [ref=e180]: Acknowledged + - generic [ref=e182]: + - img [ref=e184] + - generic [ref=e187]: Closed + - navigation [ref=e188]: + - button "Details" [ref=e189] [cursor=pointer] + - button "AI Analysis" [ref=e190] [cursor=pointer] + - button "Notes" [ref=e191] [cursor=pointer] + - main [ref=e192]: + - generic [ref=e195]: + - heading "Suggestion Details" [level=3] [ref=e196]: + - img [ref=e197] + - text: Suggestion Details + - paragraph [ref=e200]: test + - generic [ref=e201]: + - generic [ref=e202]: + - generic [ref=e203]: Hospital + - generic [ref=e204]: E2E Test Hospital + - generic [ref=e205]: + - generic [ref=e206]: Category + - generic [ref=e207]: Other + - generic [ref=e208]: + - generic [ref=e209]: Location + - generic [ref=e210]: Not specified + - generic [ref=e211]: + - generic [ref=e212]: Sentiment + - generic [ref=e214]: Neutral + - generic [ref=e215]: + - heading "Contact" [level=6] [ref=e216] + - generic [ref=e217]: + - generic [ref=e218]: ismail + - generic [ref=e219]: "0569874589" + - generic [ref=e221]: + - img [ref=e222] + - text: July 09, 2026 13:46 + - generic [ref=e225]: + - heading "Quick Actions" [level=3] [ref=e226] + - generic [ref=e227]: + - generic [ref=e228]: + - text: Status + - combobox [ref=e229]: + - option "Submitted" [selected] + - option "Reviewed" + - option "Acknowledged" + - option "Closed" + - option "Reopened" + - textbox "Add a note..." [ref=e230] + - button "Update" [ref=e231] [cursor=pointer]: + - img [ref=e232] + - text: Update + - separator [ref=e237] + - generic [ref=e238]: + - text: Assign To + - combobox [ref=e239]: + - option "Select user..." [selected] + - option "E2E PX Employee" + - option "test test" + - option "test test" + - button "Assign" [ref=e240] [cursor=pointer]: + - img [ref=e241] + - text: Assign + - separator [ref=e245] + - generic [ref=e246]: + - text: Department + - button "Send to Department" [ref=e247] [cursor=pointer]: + - img [ref=e248] + - text: Send to Department + - separator [ref=e251] + - contentinfo [ref=e252]: + - paragraph [ref=e253]: + - text: Powered by + - link "tenhal.sa" [ref=e254] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.playwright-cli/page-2026-07-12T20-41-00-163Z.yml b/.playwright-cli/page-2026-07-12T20-41-00-163Z.yml new file mode 100644 index 0000000..9bee09b --- /dev/null +++ b/.playwright-cli/page-2026-07-12T20-41-00-163Z.yml @@ -0,0 +1,137 @@ +- generic [ref=e2]: + - complementary [ref=e3]: + - generic [ref=e4]: + - img "Al Hammadi" [ref=e6] + - img "Al Hammadi" [ref=e8] + - navigation [ref=e9]: + - link [ref=e10] [cursor=pointer]: + - /url: /analytics/dashboard/ + - img [ref=e11] + - link [ref=e16] [cursor=pointer]: + - /url: /projects/my-tasks/ + - img [ref=e17] + - link [ref=e20] [cursor=pointer]: + - /url: / + - img [ref=e21] + - link [ref=e23] [cursor=pointer]: + - /url: /complaints/ + - img [ref=e24] + - link [ref=e27] [cursor=pointer]: + - /url: /inquiries/ + - img [ref=e28] + - link [ref=e31] [cursor=pointer]: + - /url: /observations/ + - img [ref=e32] + - link [ref=e35] [cursor=pointer]: + - /url: /suggestions/?feedback_type=suggestion + - img [ref=e36] + - link [ref=e38] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e39] + - link [ref=e41] [cursor=pointer]: + - /url: /organizations/patients/ + - img [ref=e42] + - button [ref=e46] [cursor=pointer]: + - img [ref=e47] + - button [ref=e52] [cursor=pointer]: + - img [ref=e53] + - link [ref=e58] [cursor=pointer]: + - /url: /organizations/departments/ + - img [ref=e59] + - link [ref=e63] [cursor=pointer]: + - /url: /projects/ + - img [ref=e64] + - button [ref=e69] [cursor=pointer]: + - img [ref=e70] + - button [ref=e74] [cursor=pointer]: + - img [ref=e75] + - button [ref=e80] [cursor=pointer]: + - img [ref=e81] + - button [ref=e88] [cursor=pointer]: + - img [ref=e89] + - button [ref=e93] [cursor=pointer]: + - img [ref=e94] + - link [ref=e96] [cursor=pointer]: + - /url: /accounts/acknowledgements/my-acknowledgements/ + - img [ref=e97] + - button [ref=e103] [cursor=pointer]: + - img [ref=e104] + - generic [ref=e107]: + - link "🇬🇧" [ref=e108] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e109]: 🇬🇧 + - link "🇸🇦" [ref=e110] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e111]: 🇸🇦 + - generic [ref=e112]: + - banner [ref=e113]: + - generic [ref=e114]: + - heading "Welcome, E2E" [level=2] [ref=e115] + - paragraph [ref=e116]: Welcome to PX360 Patient Experience Management + - generic [ref=e117]: + - button [ref=e119] [cursor=pointer]: + - img [ref=e120] + - generic [ref=e123]: + - link "🇬🇧 English" [ref=e124] [cursor=pointer]: + - /url: /core/set-language/?language=en + - generic [ref=e125]: 🇬🇧 + - generic [ref=e126]: English + - link "🇸🇦 Arabic" [ref=e127] [cursor=pointer]: + - /url: /core/set-language/?language=ar + - generic [ref=e128]: 🇸🇦 + - generic [ref=e129]: Arabic + - button "E2E PX Employee PX Employee E" [ref=e131] [cursor=pointer]: + - generic [ref=e132]: + - generic [ref=e133]: E2E PX Employee + - generic [ref=e134]: PX Employee + - generic [ref=e135]: E + - img [ref=e136] + - main [ref=e138]: + - generic [ref=e139]: + - generic [ref=e140]: + - link "Appreciations" [ref=e141] [cursor=pointer]: + - /url: /appreciation/ + - img [ref=e142] + - generic [ref=e144]: Detail + - generic [ref=e145]: Sent + - generic [ref=e146]: + - heading "Appreciation Detail" [level=1] [ref=e147]: + - img [ref=e148] + - text: Appreciation Detail + - generic [ref=e151]: 2026-07-09 13:55 — E2E Test Hospital + - navigation [ref=e152]: + - button "Details" [ref=e153] [cursor=pointer] + - button "AI Analysis" [ref=e154] [cursor=pointer] + - button "Notes" [ref=e155] [cursor=pointer] + - main [ref=e156]: + - generic [ref=e158]: + - generic [ref=e159]: + - heading "Appreciation Message" [level=3] [ref=e160]: + - img [ref=e161] + - text: Appreciation Message + - paragraph + - paragraph [ref=e163]: thank + - paragraph + - generic [ref=e164]: + - heading "Submitted By" [level=3] [ref=e165]: + - img [ref=e166] + - text: Submitted By + - generic [ref=e169]: + - generic [ref=e170]: + - paragraph [ref=e171]: Name + - paragraph [ref=e172]: ismail + - generic [ref=e173]: + - paragraph [ref=e174]: Phone + - paragraph [ref=e175]: "0569874589" + - generic [ref=e176]: + - heading "Sent" [level=3] [ref=e177]: + - img [ref=e178] + - text: Sent + - generic [ref=e182]: + - paragraph [ref=e183]: Sent At + - paragraph [ref=e184]: 2026-07-09 13:57 + - contentinfo [ref=e186]: + - paragraph [ref=e187]: + - text: Powered by + - link "tenhal.sa" [ref=e188] [cursor=pointer]: + - /url: https://tenhal.sa \ No newline at end of file diff --git a/.target-last-build b/.target-last-build index 0ffb86d..8f8b46d 100644 --- a/.target-last-build +++ b/.target-last-build @@ -1 +1 @@ -hh-dev:hh-dev-build-1783592335|1783592388 +hh-dev:hh-dev-build-1784025225|1784026493 diff --git a/PX360/settings.py b/PX360/settings.py index 163c203..8f892a9 100644 --- a/PX360/settings.py +++ b/PX360/settings.py @@ -207,4 +207,4 @@ META_APP_SECRET = "b80750bd12ab7f1c21d7d0ca891ba5ab" META_REDIRECT_URI = "https://micha-nonparabolic-lovie.ngrok-free.dev/social/callback/META/" META_WEBHOOK_VERIFY_TOKEN = "random_secret_string_khanfaheed123456" -EMAIL_LOGO_URL = "http://127.0.0.1:8000/static/img/HH_R_H_Logo.png" +EMAIL_LOGO_URL = "http://127.0.0.1:8000/static/images/logos/HH_P_H_Logo_CMYK---LANDSCIP1.png" diff --git a/apps/appreciation/forms.py b/apps/appreciation/forms.py index 6ae830e..3beedd4 100644 --- a/apps/appreciation/forms.py +++ b/apps/appreciation/forms.py @@ -1,5 +1,6 @@ """Forms for the appreciation module.""" from django import forms +from django.utils.translation import gettext_lazy as _ from apps.appreciation.models import Appreciation from apps.organizations.models import Department @@ -8,13 +9,25 @@ from apps.organizations.models import Department class AppreciationForm(forms.ModelForm): """Internal form for creating an appreciation (department-targeted).""" + national_id = forms.CharField( + label=_("National ID/Iqama No."), + max_length=10, + required=True, + widget=forms.TextInput(attrs={ + "class": "w-full px-4 py-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 outline-none", + "placeholder": _("National ID or Iqama number"), + "inputmode": "numeric", + "pattern": "[0-9]{10}", + "maxlength": "10", + "minlength": "10", + }), + help_text=_("Exactly 10 digits, numbers only."), + ) + class Meta: model = Appreciation - fields = ["hospital", "department", "message_en"] + fields = ["department", "message_en", "national_id"] widgets = { - "hospital": forms.Select( - attrs={"class": "w-full px-4 py-3 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 outline-none"} - ), "message_en": forms.Textarea( attrs={ "rows": 5, @@ -27,14 +40,21 @@ class AppreciationForm(forms.ModelForm): ), } + def clean_national_id(self): + national_id = self.cleaned_data.get("national_id", "").replace(" ", "") + if not national_id.isdigit(): + raise forms.ValidationError(_("National ID/Iqama must contain digits only.")) + if len(national_id) != 10: + raise forms.ValidationError(_("National ID/Iqama must be exactly 10 digits (you entered %(n)d).") % {"n": len(national_id)}) + return national_id + def __init__(self, *args, hospital=None, **kwargs): super().__init__(*args, **kwargs) if hospital: - self.fields["hospital"].initial = hospital - dept_qs = Department.objects.filter(status="active").select_related("hospital").order_by( - "hospital__name", "name" - ) + dept_qs = Department.objects.filter(hospital=hospital, status="active").order_by("name") + else: + dept_qs = Department.objects.filter(status="active").select_related("hospital").order_by("name") + self.fields["department"].label_from_instance = lambda obj: ( + f"{obj.name} — {obj.hospital.name}" if obj.hospital else obj.name + ) self.fields["department"].queryset = dept_qs - self.fields["department"].label_from_instance = lambda obj: ( - f"{obj.name} — {obj.hospital.name}" if obj.hospital else obj.name - ) diff --git a/apps/appreciation/migrations/0007_add_reminder_fields.py b/apps/appreciation/migrations/0007_add_reminder_fields.py new file mode 100644 index 0000000..c2660dc --- /dev/null +++ b/apps/appreciation/migrations/0007_add_reminder_fields.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.1 on 2026-07-13 16:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0006_appreciation_reference_number_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='appreciation', + name='dept_response_reminder_sent_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='appreciation', + name='dept_response_second_reminder_sent_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/apps/appreciation/migrations/0008_remove_ai_analyzed_status.py b/apps/appreciation/migrations/0008_remove_ai_analyzed_status.py new file mode 100644 index 0000000..84bffea --- /dev/null +++ b/apps/appreciation/migrations/0008_remove_ai_analyzed_status.py @@ -0,0 +1,21 @@ +from django.db import migrations + + +def convert_ai_analyzed_to_activated(apps, schema_editor): + Appreciation = apps.get_model("appreciation", "Appreciation") + Appreciation.objects.filter(status="ai_analyzed").update(status="activated") + + +def reverse_conversion(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("appreciation", "0007_add_reminder_fields"), + ] + + operations = [ + migrations.RunPython(convert_ai_analyzed_to_activated, reverse_conversion), + ] diff --git a/apps/appreciation/migrations/0009_add_national_id.py b/apps/appreciation/migrations/0009_add_national_id.py new file mode 100644 index 0000000..669b865 --- /dev/null +++ b/apps/appreciation/migrations/0009_add_national_id.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.1 on 2026-07-18 15:10 + +import apps.core.encryption +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('appreciation', '0008_remove_ai_analyzed_status'), + ] + + operations = [ + migrations.AddField( + model_name='appreciation', + name='national_id', + field=apps.core.encryption.EncryptedCharField(blank=True, help_text='National ID/Iqama of the submitter', max_length=512), + ), + migrations.AddField( + model_name='appreciation', + name='national_id_hash', + field=models.CharField(blank=True, db_index=True, max_length=64), + ), + ] diff --git a/apps/appreciation/models.py b/apps/appreciation/models.py index 1586847..25aeed3 100644 --- a/apps/appreciation/models.py +++ b/apps/appreciation/models.py @@ -12,6 +12,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio from django.contrib.contenttypes.models import ContentType from django.db import models +from apps.core.encryption import EncryptedCharField, compute_national_id_hash from apps.core.models import SoftDeleteModel, TimeStampedModel, UUIDModel @@ -220,6 +221,14 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): notification_sent = models.BooleanField(default=False) notification_sent_at = models.DateTimeField(null=True, blank=True) + # Reminder tracking + dept_response_reminder_sent_at = models.DateTimeField(null=True, blank=True) + dept_response_second_reminder_sent_at = models.DateTimeField(null=True, blank=True) + + # Submitter identity + national_id = EncryptedCharField(max_length=512, blank=True, help_text="National ID/Iqama of the submitter") + national_id_hash = models.CharField(max_length=64, blank=True, db_index=True) + # Metadata metadata = models.JSONField(default=dict, blank=True) @@ -245,6 +254,10 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): from apps.core.reference import generate_reference self.reference_number = generate_reference("APR", self.hospital) + if self.national_id: + self.national_id_hash = compute_national_id_hash(self.national_id) + else: + self.national_id_hash = "" super().save(*args, **kwargs) def get_owner(self): @@ -320,20 +333,16 @@ class Appreciation(UUIDModel, TimeStampedModel, SoftDeleteModel): def mark_ai_analyzed(self, analysis_data): from django.utils import timezone - if self.status != AppreciationStatus.ACTIVATED: - raise ValueError(f"Cannot mark as AI analyzed from '{self.status}' status. Must be in 'activated'.") - - self.status = AppreciationStatus.AI_ANALYZED self.ai_analyzed_at = timezone.now() self.ai_analysis = analysis_data - self.save(update_fields=["status", "ai_analyzed_at", "ai_analysis"]) + self.save(update_fields=["ai_analyzed_at", "ai_analysis"]) def send(self): """Send appreciation and trigger notification""" from django.utils import timezone - if self.status not in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED): - raise ValueError(f"Cannot send appreciation in '{self.status}' status. Must be 'activated' or 'ai_analyzed'.") + if self.status not in (AppreciationStatus.ACTIVATED,): + raise ValueError(f"Cannot send appreciation in '{self.status}' status. Must be 'activated'.") self.status = AppreciationStatus.SENT self.sent_at = timezone.now() diff --git a/apps/appreciation/tasks.py b/apps/appreciation/tasks.py index e40e6a7..929344a 100644 --- a/apps/appreciation/tasks.py +++ b/apps/appreciation/tasks.py @@ -15,10 +15,17 @@ def send_appreciation_notifications( email_body="", host="", ): - """Send email + SMS to champion, manager, and appreciated staff in the background. + """Send the appreciation email to the appreciated staff member with the + department champion and manager CC'd. - Replaces the synchronous notification loop that used to block the - appreciation_send_to response. + Behaviour: + - If ``staff_id`` is provided AND the staff member has an email: ONE email + is sent To: the staff member with champion + manager as CC: (deduplicated). + - If no staff member is selected (or the staff has no email): separate + emails are sent To: each of the champion and manager (current behaviour). + - In-app notifications are created for every recipient (To and CC) whose + email maps to a User account. + - SMS notifications are NOT sent (email-only flow). """ from apps.appreciation.models import Appreciation from apps.notifications.services import NotificationService, get_email_header_html @@ -40,20 +47,95 @@ def send_appreciation_notifications( link = f"https://{host}/appreciation/detail/{appreciation.pk}/" notified = [] - # Notify champion + manager - for target in targets: - display_name = ( - target["staff"].get_full_name() if target.get("staff") - else (target["user"].get_full_name() if target.get("user") else target["label"]) + # Generate the appreciation letter PDF once and reuse for all email notifications. + # Wrapped in try/except so email delivery still succeeds if PDF rendering fails. + pdf_attachment = None + try: + from apps.appreciation.ui_views import _render_appreciation_pdf_bytes, _appreciation_pdf_context + + ctx = _appreciation_pdf_context(appreciation) + pdf_bytes = _render_appreciation_pdf_bytes( + "appreciation/appreciation_letter.html", ctx, obj=appreciation, ) - if target.get("email"): + pdf_attachment = ( + f"appreciation_letter_{appreciation.reference_number}.pdf", + pdf_bytes, + "application/pdf", + ) + except Exception: + logger.exception( + f"Failed to generate appreciation letter PDF for {appreciation_id}; " + f"sending notifications without attachment" + ) + + # Build the champion + manager email list (in order: champion first, then manager) + target_emails_raw = [t["email"] for t in targets if t.get("email")] + + # Resolve the appreciated staff member (if any) and their email + staff_email = None + appreciated_staff = None + if staff_id: + try: + appreciated_staff = Staff.objects.get(id=staff_id) + s_email, _ = _staff_contact(appreciated_staff) + staff_email = s_email or None + except Staff.DoesNotExist: + appreciated_staff = None + + # Build the deduplicated CC list (exclude the staff member's own email) + seen_lower = {staff_email.lower()} if staff_email else set() + cc_emails = [] + for email in target_emails_raw: + key = email.lower() + if key not in seen_lower: + seen_lower.add(key) + cc_emails.append(email) + + attachments_arg = [pdf_attachment] if pdf_attachment else None + + if staff_email: + # CASE 1 (primary): ONE email to the staff member, CC champion + manager. + send_subject = email_subject or f"An Appreciation Has Been Submitted About You - {appreciation.reference_number}" + send_body = email_body or ( + f"An appreciation (#{appreciation.reference_number}) has been submitted about you." + ) + try: + NotificationService.send_email( + email=staff_email, + subject=send_subject, + message=send_body + f"\n\n{link}", + html_message=f""" +
+ {get_email_header_html()} +
+

You've Been Appreciated!

+

An appreciation #{appreciation.reference_number} has been submitted about you.

+

Message: {appreciation.message_en or 'N/A'}

+ {f'

Note: {note}

' if note else ''} +

View Appreciation

+
+
+ """, + related_object=appreciation, + attachments=attachments_arg, + cc=cc_emails, + ) + notified.append(staff_email) + for cc_addr in cc_emails: + notified.append(f"CC: {cc_addr}") + except Exception: + logger.exception(f"Failed to send staff email to {staff_email} with CC {cc_emails}") + + elif cc_emails: + # CASE 2 (fallback): no specific staff — separate emails to champion + manager. + send_subject = email_subject or f"Appreciation Sent to Department - {appreciation.reference_number}" + send_body = email_body or ( + f"Appreciation #{appreciation.reference_number} has been sent to your department ({department.name})." + ) + for email in cc_emails: try: - send_subject = email_subject or f"Appreciation Sent to Department - {appreciation.reference_number}" - send_body = email_body or ( - f"Appreciation #{appreciation.reference_number} has been sent to your department ({department.name})." - ) NotificationService.send_email( - email=target["email"], + email=email, subject=send_subject, message=send_body + f"\n\n{link}", html_message=f""" @@ -64,67 +146,57 @@ def send_appreciation_notifications(

Appreciation #{appreciation.reference_number} has been sent to your department ({department.name}).

Message: {appreciation.message_en or 'N/A'}

{f'

Note: {note}

' if note else ''} -

Role: {target['label']}

View Appreciation

""", related_object=appreciation, + attachments=attachments_arg, ) + notified.append(email) except Exception: - logger.exception(f"Failed to send email to {target.get('email')}") - if target.get("phone"): - try: - NotificationService.send_sms( - target["phone"], - f"PX360: Appreciation #{appreciation.reference_number} sent to {department.name}. {link}", - related_object=appreciation, - ) - except Exception: - pass - notified.append(f"{display_name} ({target['label']})") + logger.exception(f"Failed to send department email to {email}") - # Notify the appreciated staff member - if staff_id: - try: - appreciated_staff = Staff.objects.get(id=staff_id) - except Staff.DoesNotExist: - appreciated_staff = None - - if appreciated_staff: - s_email, s_phone = _staff_contact(appreciated_staff) - s_name = appreciated_staff.get_full_name() - if s_email: - try: - NotificationService.send_email( - email=s_email, - subject=f"An Appreciation Has Been Submitted About You - {appreciation.reference_number}", - message=f"An appreciation (#{appreciation.reference_number}) has been submitted about you.\n\nMessage: {appreciation.message_en or 'N/A'}\n\nView: {link}", - html_message=f""" -
- {get_email_header_html()} -
-

You've Been Appreciated!

-

An appreciation #{appreciation.reference_number} has been submitted about you.

-

Message: {appreciation.message_en or 'N/A'}

- {f'

Note: {note}

' if note else ''} -

View Appreciation

-
-
- """, - related_object=appreciation, - ) - notified.append(f"{s_name} (Staff)") - except Exception: - logger.exception(f"Failed to send email to staff {s_email}") - if s_phone: - try: - NotificationService.send_sms( - s_phone, - f"PX360: An appreciation has been submitted about you. {link}", - related_object=appreciation, - ) - except Exception: - pass + else: + logger.warning( + f"Appreciation {appreciation_id}: no recipients to notify " + f"(no staff email and no champion/manager emails)" + ) logger.info(f"Appreciation {appreciation_id} notifications sent to: {', '.join(notified)}") + + +@shared_task +def send_appreciation_reminder_email(appreciation_id, reminder_type, recipients): + """Send a reminder email to department champion and manager for an appreciation.""" + import logging + + logger = logging.getLogger(__name__) + + try: + from apps.appreciation.models import Appreciation + from apps.notifications.services import NotificationService, get_email_header_html + + appreciation = Appreciation.objects.get(pk=appreciation_id) + + for recipient in recipients: + NotificationService.send_email( + email=recipient["email"], + subject=f"Reminder: Appreciation {appreciation.reference_number} - Acknowledgment Required", + message=f"This is a reminder that appreciation {appreciation.reference_number} is awaiting acknowledgment. Please acknowledge it as soon as possible.", + html_message=f""" +
+ {get_email_header_html()} +
+

Reminder: Acknowledgment Required

+

This is a reminder that appreciation {appreciation.reference_number} is awaiting acknowledgment from your department.

+

Please acknowledge it as soon as possible.

+
+
+""", + related_object=appreciation, + ) + + logger.info(f"Appreciation {appreciation_id} {reminder_type} reminder sent to {len(recipients)} recipients") + except Exception as e: + logger.error(f"Failed to send appreciation reminder email for {appreciation_id}: {e}") diff --git a/apps/appreciation/ui_views.py b/apps/appreciation/ui_views.py index 6f33f04..863ba29 100644 --- a/apps/appreciation/ui_views.py +++ b/apps/appreciation/ui_views.py @@ -41,8 +41,22 @@ def appreciation_list(request): ).order_by("-created_at") user = request.user + selected_hospital = getattr(request, "tenant_hospital", None) + if user.is_px_admin(): - pass + if selected_hospital: + queryset = queryset.filter(hospital=selected_hospital) + elif user.is_hospital_admin() and user.hospital: + queryset = queryset.filter(hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) + elif user.is_department_manager() and user.department: + queryset = queryset.filter(department=user.department) + elif user.is_champion() and user.department: + queryset = queryset.filter(department=user.department) + elif user.is_source_user(): + queryset = queryset.filter(created_by=user) elif user.hospital: queryset = queryset.filter(hospital=user.hospital) else: @@ -61,16 +75,30 @@ def appreciation_list(request): ) base_qs = Appreciation.objects.all() - if not user.is_px_admin() and user.hospital: + if user.is_px_admin(): + if selected_hospital: + base_qs = base_qs.filter(hospital=selected_hospital) + elif user.is_hospital_admin() and user.hospital: base_qs = base_qs.filter(hospital=user.hospital) - elif not user.is_px_admin(): + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + base_qs = base_qs.filter(hospital=user.hospital) + elif user.is_department_manager() and user.department: + base_qs = base_qs.filter(department=user.department) + elif user.is_champion() and user.department: + base_qs = base_qs.filter(department=user.department) + elif user.is_source_user(): + base_qs = base_qs.filter(created_by=user) + elif user.hospital: + base_qs = base_qs.filter(hospital=user.hospital) + else: base_qs = base_qs.none() stats = { "total": base_qs.count(), "draft": base_qs.filter(status=AppreciationStatus.DRAFT).count(), "activated": base_qs.filter( - status__in=[AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED] + status=AppreciationStatus.ACTIVATED ).count(), "sent": base_qs.filter(status__in=[AppreciationStatus.SENT, AppreciationStatus.ACKNOWLEDGED]).count(), } @@ -132,6 +160,9 @@ def appreciation_detail(request, pk): appreciation_ct = ContentType.objects.get_for_model(appreciation) generic_notes = appreciation.notes.select_related("created_by").all() + has_recipient = appreciation.recipient is not None + is_activated = appreciation.status == AppreciationStatus.ACTIVATED + context = { "appreciation": appreciation, "metadata": metadata, @@ -140,13 +171,23 @@ def appreciation_detail(request, pk): "categories": categories, "hospital_departments": hospital_departments, "can_activate": appreciation.status == AppreciationStatus.DRAFT, - "can_send": appreciation.status in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED), + "can_send": is_activated and appreciation.department_id is not None, + "has_recipient": has_recipient, "is_recipient": False, "content_type_id": appreciation_ct.pk, "object_id": appreciation.pk, "notes": generic_notes, "notes_count": generic_notes.count(), } + _apr_steps = [ + {"label": _("Activate"), "icon": "zap", "done": appreciation.activated_at is not None}, + {"label": _("Select Staff"), "icon": "user-check", "done": has_recipient or appreciation.sent_at is not None}, + {"label": _("Send"), "icon": "send", "done": appreciation.sent_at is not None}, + ] + _apr_next = next((i for i, s in enumerate(_apr_steps) if not s["done"]), len(_apr_steps)) + if _apr_next < len(_apr_steps): + _apr_steps[_apr_next]["current"] = True + context["workflow_steps"] = _apr_steps return render(request, "appreciation/appreciation_detail.html", context) @@ -165,17 +206,30 @@ def appreciation_create(request): from .forms import AppreciationForm - hospital = user.hospital + hospital = getattr(request, "tenant_hospital", None) or user.hospital if request.method == "POST": form = AppreciationForm(request.POST, hospital=hospital) if form.is_valid(): appreciation = form.save(commit=False) appreciation.sender = user - appreciation.hospital = form.cleaned_data["hospital"] + appreciation.hospital = hospital appreciation.status = AppreciationStatus.DRAFT appreciation.save() form.save_m2m() + + staff_id = request.POST.get("staff_id") + if staff_id: + try: + from django.contrib.contenttypes.models import ContentType + staff_ct = ContentType.objects.get_for_model(Staff) + staff = Staff.objects.get(pk=staff_id, status="active") + appreciation.recipient_content_type = staff_ct + appreciation.recipient_object_id = staff.id + appreciation.save(update_fields=["recipient_content_type", "recipient_object_id"]) + except Staff.DoesNotExist: + pass + messages.success(request, _("Appreciation created successfully.")) return redirect("appreciation:appreciation_detail", pk=appreciation.pk) else: @@ -261,12 +315,71 @@ def appreciation_activate(request, pk): return redirect("appreciation:appreciation_detail", pk=appreciation.pk) +@login_required +@require_http_methods(["POST"]) +def appreciation_select_recipient(request, pk): + """Select the department and optional staff recipient for an appreciation (without sending).""" + appreciation = get_object_or_404(Appreciation, pk=pk) + + if appreciation.status != AppreciationStatus.ACTIVATED: + messages.error(request, _("Appreciation must be activated before selecting a recipient.")) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + + user = request.user + if not ( + user.is_px_admin() or user.is_hospital_admin() + or user.is_px_management() or user.is_px_employee() + ): + messages.error(request, _("Permission denied.")) + return redirect("appreciation:appreciation_list") + + department_id = request.POST.get("department_id") + staff_id = request.POST.get("staff_id") + + if request.POST.get("clear_recipient"): + appreciation.department = None + appreciation.recipient_content_type = None + appreciation.recipient_object_id = None + appreciation.save(update_fields=["department", "recipient_content_type", "recipient_object_id"]) + messages.info(request, _("Recipient cleared. Please select a new recipient.")) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + + if not department_id: + messages.error(request, _("Please select a department.")) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + + try: + department = Department.objects.get(id=department_id, status="active") + except Department.DoesNotExist: + messages.error(request, _("Invalid department selected.")) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + + appreciation.department = department + + if staff_id: + try: + from django.contrib.contenttypes.models import ContentType + staff_ct = ContentType.objects.get_for_model(Staff) + staff = Staff.objects.get(pk=staff_id, status="active") + appreciation.recipient_content_type = staff_ct + appreciation.recipient_object_id = staff.id + except Staff.DoesNotExist: + pass + else: + appreciation.recipient_content_type = None + appreciation.recipient_object_id = None + + appreciation.save() + messages.success(request, _("Recipient selected: {}").format(department.name)) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + + @login_required @require_http_methods(["POST"]) def appreciation_send(request, pk): appreciation = get_object_or_404(Appreciation, pk=pk) - if appreciation.status not in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED): + if appreciation.status != AppreciationStatus.ACTIVATED: messages.error(request, _("This appreciation must be activated before sending.")) return redirect("appreciation:appreciation_detail", pk=pk) @@ -341,7 +454,7 @@ def appreciation_send_to(request, pk): ) # Must be activated before it can be sent - if appreciation.status not in (AppreciationStatus.ACTIVATED, AppreciationStatus.AI_ANALYZED): + if appreciation.status != AppreciationStatus.ACTIVATED: return JsonResponse( {"success": False, "error": str(_("Activate this appreciation before sending it to a department."))}, status=400, @@ -435,7 +548,7 @@ def appreciation_send_to(request, pk): except Staff.DoesNotExist: pass - # Advance to SENT (validates status is ACTIVATED/AI_ANALYZED) + # Advance to SENT (validates status is ACTIVATED) appreciation.send() # Send notifications (email + SMS to champion, manager, staff) in the background @@ -508,6 +621,79 @@ def appreciation_acknowledge(request, pk): return redirect('appreciation:appreciation_detail', pk=pk) +@login_required +@require_http_methods(["POST"]) +def appreciation_send_dept_reminder(request, pk): + """Send a reminder to the department that hasn't acknowledged an appreciation.""" + appreciation = get_object_or_404(Appreciation, pk=pk) + + user = request.user + if not ( + user.is_px_admin() or user.is_hospital_admin() + or user.is_px_management() or user.is_px_employee() + ): + messages.error(request, _("You don't have permission to send reminders.")) + return redirect("appreciation:appreciation_detail", pk=pk) + + if appreciation.status not in (AppreciationStatus.SENT, AppreciationStatus.ACKNOWLEDGED): + messages.warning(request, _("Appreciation must be sent before sending reminders.")) + return redirect("appreciation:appreciation_detail", pk=pk) + + dept = appreciation.department + if not dept: + messages.error(request, _("No department assigned.")) + return redirect("appreciation:appreciation_detail", pk=pk) + + reminder_type = request.POST.get("reminder_type", "first") + + if reminder_type == "first" and appreciation.dept_response_reminder_sent_at: + messages.warning(request, _("First reminder already sent.")) + return redirect("appreciation:appreciation_detail", pk=pk) + if reminder_type == "second" and appreciation.dept_response_second_reminder_sent_at: + messages.warning(request, _("Second reminder already sent.")) + return redirect("appreciation:appreciation_detail", pk=pk) + if reminder_type == "second" and not appreciation.dept_response_reminder_sent_at: + messages.warning(request, _("Please send the first reminder before the second.")) + return redirect("appreciation:appreciation_detail", pk=pk) + + try: + from apps.notifications.services import NotificationService, get_email_header_html + from apps.organizations.department_contacts import get_champion_and_manager + + targets = get_champion_and_manager(dept) + if not targets: + messages.error(request, _(f"No champion or manager assigned to {dept.name}.")) + return redirect("appreciation:appreciation_detail", pk=pk) + + recipients = [] + for target in targets: + email = target.get("email") or "" + if email: + recipients.append({"email": email, "name": target.get("display_name", "")}) + + if reminder_type == "first": + appreciation.dept_response_reminder_sent_at = timezone.now() + else: + appreciation.dept_response_second_reminder_sent_at = timezone.now() + appreciation.save(update_fields=["dept_response_reminder_sent_at", "dept_response_second_reminder_sent_at"]) + + from apps.appreciation.tasks import send_appreciation_reminder_email + send_appreciation_reminder_email.delay(str(appreciation.id), reminder_type, recipients) + + AuditService.log_event( + event_type="appreciation_reminder_sent", + description=f"{reminder_type.capitalize()} reminder sent for appreciation {appreciation.reference_number} by {user.get_full_name()}", + content_object=appreciation, + ) + + messages.success(request, _(f"{reminder_type.capitalize()} reminder sent to {len(recipients)} recipient(s).")) + except Exception as e: + logger.error(f"Failed to send appreciation reminder: {e}") + messages.error(request, _("Failed to send reminder.")) + + return redirect("appreciation:appreciation_detail", pk=pk) + + # ============================================================================ # LEADERBOARD VIEWS # ============================================================================ @@ -533,8 +719,23 @@ def leaderboard_view(request): queryset = AppreciationStats.objects.filter(year=year, month=month) # Apply RBAC - if not user.is_px_admin() and user.hospital: + selected_hospital = getattr(request, "tenant_hospital", None) + if user.is_px_admin(): + if selected_hospital: + queryset = queryset.filter(hospital=selected_hospital) + elif user.is_hospital_admin() and user.hospital: queryset = queryset.filter(hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) + elif user.is_department_manager() and user.department: + queryset = queryset.filter(department=user.department) + elif user.is_champion() and user.department: + queryset = queryset.filter(department=user.department) + elif user.hospital: + queryset = queryset.filter(hospital=user.hospital) + else: + queryset = queryset.none() # Apply filters hospital_filter = request.GET.get('hospital') @@ -669,7 +870,7 @@ def category_list(request): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to manage categories.") return redirect('appreciation:appreciation_list') @@ -715,7 +916,7 @@ def category_create(request): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to create categories.") return redirect('appreciation:appreciation_list') @@ -773,7 +974,7 @@ def category_edit(request, pk): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to edit categories.") return redirect('appreciation:appreciation_list') @@ -818,7 +1019,7 @@ def category_delete(request, pk): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to delete categories.") return redirect('appreciation:appreciation_list') @@ -853,7 +1054,7 @@ def badge_list(request): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to manage badges.") return redirect('appreciation:appreciation_list') @@ -899,7 +1100,7 @@ def badge_create(request): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to create badges.") return redirect('appreciation:appreciation_list') @@ -961,7 +1162,7 @@ def badge_edit(request, pk): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to edit badges.") return redirect('appreciation:appreciation_list') @@ -1008,7 +1209,7 @@ def badge_delete(request, pk): user = request.user # Check permission - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to delete badges.") return redirect('appreciation:appreciation_list') @@ -1039,7 +1240,7 @@ def appreciation_restore(request, pk): """Restore deleted appreciation""" user = request.user - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to restore appreciation.") return redirect('config:deleted_items') @@ -1224,6 +1425,16 @@ def public_appreciation_submit(request): if not contact_name or not contact_phone or not message: return JsonResponse({"success": False, "message": "Name, phone, and message are required."}, status=400) + from apps.core.validators import validate_saudi_phone + from django.core.exceptions import ValidationError + try: + validate_saudi_phone(contact_phone) + except ValidationError: + return JsonResponse( + {"success": False, "message": "Please enter a valid Saudi mobile number (e.g. 05XXXXXXXX or +9665XXXXXXXX)."}, + status=400, + ) + if not hospital_id: return JsonResponse({"success": False, "message": "Please select a hospital."}, status=400) @@ -1273,13 +1484,143 @@ def public_appreciation_submit(request): logger.exception("ERROR in public_appreciation_submit") return JsonResponse({"success": False, "message": str(e)}, status=500) +def _appreciation_pdf_context(appreciation): + """Build the common context for the appreciation letter & certificate PDFs.""" + hospital = appreciation.hospital + hospital_name = hospital.name if hospital else "" + hospital_name_ar = getattr(hospital, "name_ar", "") if hospital else "" + + recipient = appreciation.recipient + recipient_name = appreciation.get_recipient_name() or "—" + department_name = appreciation.department.get_localized_name() if appreciation.department else "" + recipient_title = "" + if recipient is not None: + getter = getattr(recipient, "get_localized_job_title", None) + if callable(getter): + recipient_title = getter() or "" + recipient_title = recipient_title or getattr(recipient, "job_title", "") or "" + + meta = appreciation.metadata or {} + is_patient = meta.get("source") == "public_form" or bool(meta.get("submitted_by_name")) + patient_name = (meta.get("submitted_by_name") or "").strip() + + # Signer: sender (unless anonymous), else the activating officer, else generic PX team. + signer = appreciation.sender if (appreciation.sender and not appreciation.is_anonymous) else appreciation.activated_by + signer_name = _("Al Hammadi Patient Experience") + signer_role = _("Patient Experience Team") + if signer: + signer_name = signer.get_full_name() or signer_name + role_names = signer.get_role_names() if hasattr(signer, "get_role_names") else [] + if role_names: + signer_role = ", ".join(role_names) + + date_dt = appreciation.sent_at or appreciation.activated_at or appreciation.created_at + + return { + "hospital_name": hospital_name, + "hospital_name_ar": hospital_name_ar, + "recipient_name": recipient_name, + "recipient_title": recipient_title, + "department_name": department_name, + "is_patient": is_patient, + "patient_name": patient_name, + "message_en": appreciation.message_en or "", + "message_ar": appreciation.message_ar or "", + "has_ar": bool((appreciation.message_ar or "").strip()), + "signer_name": signer_name, + "signer_role": signer_role, + "date_dt": date_dt, + "reference": appreciation.reference_number or "", + } + + +def _render_appreciation_pdf_bytes(template_name, context, obj=None): + """Render a PDF template via WeasyPrint and return the raw PDF bytes. + + When ``obj`` is provided, its hospital letterhead is injected into the context + as ``letterhead_path`` (data URI) via the shared ``pdf_utils`` helper. + Used by both the PDF download views (wraps bytes in HttpResponse) and the + notification task (attaches bytes to email). + """ + from django.conf import settings + from django.template.loader import render_to_string + from weasyprint import HTML + + full_context = dict(context) + if obj is not None: + from apps.core.pdf_utils import _get_letterhead_data_uri + full_context.setdefault("letterhead_path", _get_letterhead_data_uri(obj)) + + html_string = render_to_string(template_name, full_context) + return HTML(string=html_string, base_url=str(settings.BASE_DIR / "static")).write_pdf() + + +def _appreciation_pdf_response(template_name, context, filename, obj=None): + """Render a PDF template via WeasyPrint and return it as an attachment response.""" + from django.http import HttpResponse + + pdf_file = _render_appreciation_pdf_bytes(template_name, context, obj=obj) + response = HttpResponse(pdf_file, content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{filename}"' + return response + + +def _check_appreciation_pdf_access(request, appreciation): + """Shared permission + activation gate for the appreciation PDF views.""" + user = request.user + allowed = ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + or user.is_px_employee() + or (user.hospital_id and appreciation.hospital_id == user.hospital_id) + ) + if not allowed: + messages.error(request, _("You don't have permission to view this appreciation.")) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + if not appreciation.activated_at: + messages.error(request, _("This appreciation must be activated before generating a document.")) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + if not appreciation.recipient: + messages.error(request, _("Select a recipient before generating a document.")) + return redirect("appreciation:appreciation_detail", pk=appreciation.pk) + return None + + @login_required -def appreciation_pdf(request, pk): - from apps.core.pdf_utils import generate_letterhead_pdf +def appreciation_letter(request, pk): + """Everyday Letter of Appreciation (portrait, message-forward, sender-signed).""" obj = get_object_or_404(Appreciation, id=pk) - return generate_letterhead_pdf( - 'appreciation/appreciation_pdf.html', - {'object': obj}, - f'appreciation_{obj.reference_number}.pdf', + denied = _check_appreciation_pdf_access(request, obj) + if denied: + return denied + ctx = _appreciation_pdf_context(obj) + return _appreciation_pdf_response( + "appreciation/appreciation_letter.html", + ctx, + f"appreciation_letter_{obj.reference_number}.pdf", obj=obj, ) + + +@login_required +def appreciation_certificate(request, pk): + """Formal Certificate of Appreciation (landscape, leadership-signed).""" + obj = get_object_or_404(Appreciation, id=pk) + denied = _check_appreciation_pdf_access(request, obj) + if denied: + return denied + ctx = _appreciation_pdf_context(obj) + hospital = obj.hospital + ctx["ceo_name"] = hospital.ceo.get_full_name() if hospital and hospital.ceo else "" + ctx["medical_director_name"] = ( + hospital.medical_director.get_full_name() if hospital and hospital.medical_director else "" + ) + ctx["hospital_initials"] = ( + "".join(w[0] for w in (hospital.name or "").split()[:2]).upper() if hospital and hospital.name else "HH" + ) + return _appreciation_pdf_response( + "appreciation/appreciation_certificate.html", + ctx, + f"appreciation_certificate_{obj.reference_number}.pdf", + ) diff --git a/apps/appreciation/urls.py b/apps/appreciation/urls.py index 55fe3de..6ba6ea4 100644 --- a/apps/appreciation/urls.py +++ b/apps/appreciation/urls.py @@ -34,8 +34,10 @@ urlpatterns = [ path('new/', ui_views.appreciation_create, name='appreciation_create'), path('detail//', ui_views.appreciation_detail, name='appreciation_detail'), path('detail//activate/', ui_views.appreciation_activate, name='appreciation_activate'), + path('detail//select-recipient/', ui_views.appreciation_select_recipient, name='appreciation_select_recipient'), path('detail//send/', ui_views.appreciation_send, name='appreciation_send'), path('detail//send-to/', ui_views.appreciation_send_to, name='appreciation_send_to'), + path('detail//send-reminder/', ui_views.appreciation_send_dept_reminder, name='appreciation_send_dept_reminder'), path('acknowledge//', ui_views.appreciation_acknowledge, name='appreciation_acknowledge'), path('leaderboard/', ui_views.leaderboard_view, name='leaderboard_view'), path('badges/', ui_views.my_badges_view, name='my_badges_view'), @@ -64,5 +66,7 @@ urlpatterns = [ # Public submission (no auth required) path('public/submit/', ui_views.public_appreciation_submit, name='public_appreciation_submit'), - path('detail//pdf/', ui_views.appreciation_pdf, name='appreciation_pdf'), + # PDF downloads (require activation) + path('detail//letter/', ui_views.appreciation_letter, name='appreciation_letter'), + path('detail//certificate/', ui_views.appreciation_certificate, name='appreciation_certificate'), ] diff --git a/apps/appreciation/views.py b/apps/appreciation/views.py index 439404b..a2624c2 100644 --- a/apps/appreciation/views.py +++ b/apps/appreciation/views.py @@ -398,7 +398,7 @@ class AppreciationBadgeViewSet(viewsets.ModelViewSet): queryset = AppreciationBadge.objects.all() serializer_class = AppreciationBadgeSerializer - permission_classes = [IsAuthenticated, IsPXAdminOrHospitalAdmin] + permission_classes = [IsAuthenticated] def get_queryset(self): """Filter badges by hospital""" diff --git a/apps/complaints/forms.py b/apps/complaints/forms.py index cc6729c..0bbb76f 100644 --- a/apps/complaints/forms.py +++ b/apps/complaints/forms.py @@ -26,6 +26,7 @@ from apps.complaints.models import ( ) from apps.core.models import PriorityChoices, SeverityChoices from apps.core.form_mixins import HospitalFieldMixin, DepartmentFieldMixin +from apps.core.validators import SAUDI_PHONE_HTML_PATTERN, validate_saudi_phone from apps.organizations.models import Area, Department, Hospital, LocationType, Patient, Staff, Section @@ -93,7 +94,12 @@ class PublicComplaintForm(forms.ModelForm): label=_("Mobile Number"), max_length=20, required=True, - widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your mobile number")}), + widget=forms.TextInput(attrs={ + "class": "form-control", + "placeholder": _("05XXXXXXXX or +9665XXXXXXXX"), + "pattern": SAUDI_PHONE_HTML_PATTERN, + "inputmode": "tel", + }), ) # Patient Information @@ -280,16 +286,10 @@ class PublicComplaintForm(forms.ModelForm): ).order_by("name_en") def clean_mobile_number(self): - """Validate mobile number format""" + """Validate Saudi mobile number format (05xxxxxxxx / 5xxxxxxxx / +9665xxxxxxxx / 9665xxxxxxxx).""" mobile_number = self.cleaned_data.get("mobile_number") - - # Remove spaces and dashes - mobile_number = mobile_number.replace(" ", "").replace("-", "") - - # Validate Saudi mobile format (05xxxxxxxx) - if not mobile_number.startswith("05") or len(mobile_number) != 10: - raise ValidationError(_("Please enter a valid Saudi mobile number (10 digits starting with 05)")) - + if mobile_number: + validate_saudi_phone(mobile_number) return mobile_number def clean_national_id(self): @@ -711,6 +711,7 @@ class InquiryForm(HospitalFieldMixin, forms.ModelForm): "contact_name", "contact_phone", "contact_email", + "national_id", "section", "priority", "source", @@ -763,6 +764,24 @@ class InquiryForm(HospitalFieldMixin, forms.ModelForm): department_id=department_id ).order_by("name_en") + def clean_area(self): + area = self.cleaned_data.get("area") + if not area: + return None + return area + + def clean_department(self): + dept = self.cleaned_data.get("department") + if not dept: + return None + return dept + + def clean_section(self): + section = self.cleaned_data.get("section") + if not section: + return None + return section + class SLAConfigForm(HospitalFieldMixin, forms.ModelForm): """Form for creating and editing SLA configurations""" @@ -869,7 +888,13 @@ class PublicInquiryForm(forms.Form): label=_("Phone Number"), max_length=20, required=True, - widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your phone number")}), + validators=[validate_saudi_phone], + widget=forms.TextInput(attrs={ + "class": "form-control", + "placeholder": _("05XXXXXXXX or +9665XXXXXXXX"), + "pattern": SAUDI_PHONE_HTML_PATTERN, + "inputmode": "tel", + }), ) email = forms.EmailField( @@ -878,6 +903,13 @@ class PublicInquiryForm(forms.Form): widget=forms.EmailInput(attrs={"class": "form-control", "placeholder": _("your@email.com")}), ) + national_id = forms.CharField( + label=_("National ID/Iqama"), + max_length=20, + required=False, + widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("National ID or Iqama (optional)")}), + ) + # Inquiry Details hospital = forms.ModelChoiceField( label=_("Hospital"), diff --git a/apps/complaints/migrations/0043_alter_inquiry_status.py b/apps/complaints/migrations/0043_alter_inquiry_status.py new file mode 100644 index 0000000..0abe28a --- /dev/null +++ b/apps/complaints/migrations/0043_alter_inquiry_status.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-07-15 15:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0042_inquiry_closed_by_inquiry_resolved_by'), + ] + + operations = [ + migrations.AlterField( + model_name='inquiry', + name='status', + field=models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('contact_no_response', 'Contacted - No Response')], db_index=True, default='open', max_length=25), + ), + ] diff --git a/apps/complaints/migrations/0044_alter_complaint_contact_phone_and_more.py b/apps/complaints/migrations/0044_alter_complaint_contact_phone_and_more.py new file mode 100644 index 0000000..c878320 --- /dev/null +++ b/apps/complaints/migrations/0044_alter_complaint_contact_phone_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.1 on 2026-07-18 14:46 + +import apps.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0043_alter_inquiry_status'), + ] + + operations = [ + migrations.AlterField( + model_name='complaint', + name='contact_phone', + field=models.CharField(blank=True, max_length=20, validators=[apps.core.validators.validate_saudi_phone]), + ), + migrations.AlterField( + model_name='inquiry', + name='contact_phone', + field=models.CharField(blank=True, max_length=20, validators=[apps.core.validators.validate_saudi_phone]), + ), + ] diff --git a/apps/complaints/migrations/0045_inquiry_add_national_id.py b/apps/complaints/migrations/0045_inquiry_add_national_id.py new file mode 100644 index 0000000..be3641e --- /dev/null +++ b/apps/complaints/migrations/0045_inquiry_add_national_id.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.1 on 2026-07-19 06:23 + +import apps.core.encryption +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('complaints', '0044_alter_complaint_contact_phone_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='inquiry', + name='national_id', + field=apps.core.encryption.EncryptedCharField(blank=True, help_text='National ID/Iqama of the contact', max_length=512), + ), + migrations.AddField( + model_name='inquiry', + name='national_id_hash', + field=models.CharField(blank=True, db_index=True, max_length=64), + ), + ] diff --git a/apps/complaints/models.py b/apps/complaints/models.py index f846acf..f568d60 100644 --- a/apps/complaints/models.py +++ b/apps/complaints/models.py @@ -16,9 +16,10 @@ from django.contrib.contenttypes.fields import GenericRelation from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ - from apps.core.encryption import EncryptedCharField, compute_national_id_hash, mask_national_id + from apps.core.models import PriorityChoices, SeverityChoices, SoftDeleteModel, TenantModel, TimeStampedModel, UUIDModel +from apps.core.validators import validate_saudi_phone from apps.organizations.models import LocationType @@ -213,7 +214,7 @@ class Complaint(UUIDModel, TimeStampedModel, SoftDeleteModel): # Contact information for anonymous/unregistered submissions contact_name = models.CharField(max_length=200, blank=True) - contact_phone = models.CharField(max_length=20, blank=True) + contact_phone = models.CharField(max_length=20, blank=True, validators=[validate_saudi_phone]) contact_email = models.EmailField(blank=True) # Public complaint form fields @@ -1539,8 +1540,10 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): # Contact information (if patient not in system) contact_name = models.CharField(max_length=200, blank=True) - contact_phone = models.CharField(max_length=20, blank=True) + contact_phone = models.CharField(max_length=20, blank=True, validators=[validate_saudi_phone]) contact_email = models.EmailField(blank=True) + national_id = EncryptedCharField(max_length=512, blank=True, help_text="National ID/Iqama of the contact") + national_id_hash = models.CharField(max_length=64, blank=True, db_index=True) # Organization hospital = models.ForeignKey("organizations.Hospital", on_delete=models.CASCADE, related_name="inquiries") @@ -1699,6 +1702,7 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): ("in_progress", _("In Progress")), ("resolved", _("Resolved")), ("closed", _("Closed")), + ("contact_no_response", _("Contacted - No Response")), ], default="open", db_index=True, @@ -2024,11 +2028,27 @@ class Inquiry(UUIDModel, TimeStampedModel, SoftDeleteModel): else: self.timeline_sla = "more_than_72_hours" + if self.national_id: + self.national_id_hash = compute_national_id_hash(self.national_id) + else: + self.national_id_hash = "" + super().save(*args, **kwargs) def __str__(self): return f"{self.subject} ({self.status})" + @property + def handling_department_id(self): + """The department currently responsible for handling this inquiry. + + When the inquiry has been forwarded, this is the receiving department + (``transferred_to_department``); otherwise it's the primary ``department``. + Used to gate department actions so a department that has forwarded an + inquiry away can no longer act on it — only the current handler can. + """ + return self.transferred_to_department_id or self.department_id + def get_absolute_url(self): from django.urls import reverse diff --git a/apps/complaints/tasks.py b/apps/complaints/tasks.py index 8a21fd5..eb36e59 100644 --- a/apps/complaints/tasks.py +++ b/apps/complaints/tasks.py @@ -3069,26 +3069,36 @@ def _apply_inquiry_ai_analysis(inquiry, analysis, emotion_analysis): inquiry.priority = analysis.get("priority", inquiry.priority) - department_name = analysis.get("department", "") - if department_name: - if department := Department.objects.filter( - hospital_id=inquiry.hospital.id, name__iexact=department_name, status="active" - ).first(): - inquiry.department = department - logger.info(f"Matched department exactly: {department.name}") - elif department := Department.objects.filter( - hospital_id=inquiry.hospital.id, name__icontains=department_name, status="active" - ).first(): - inquiry.department = department - logger.info(f"Matched department partially: {department.name} from '{department_name}'") + # Department auto-detection — skip when the user already provided one + # (quick-create modal sets metadata.ai_skip_department). For those, AI only + # fills subject + priority, never overriding the user's department. + if not (inquiry.metadata or {}).get("ai_skip_department"): + department_name = analysis.get("department", "") + if department_name: + if department := Department.objects.filter( + hospital_id=inquiry.hospital.id, name__iexact=department_name, status="active" + ).first(): + inquiry.department = department + logger.info(f"Matched department exactly: {department.name}") + elif department := Department.objects.filter( + hospital_id=inquiry.hospital.id, name__icontains=department_name, status="active" + ).first(): + inquiry.department = department + logger.info(f"Matched department partially: {department.name} from '{department_name}'") if analysis.get("brief_summary_en"): inquiry.ai_brief_en = analysis["brief_summary_en"] if analysis.get("brief_summary_ar"): inquiry.ai_brief_ar = analysis["brief_summary_ar"] + # For quick-created inquiries (no user-authored subject), let the AI title + # become the subject. Guarded by a metadata flag so user subjects are never + # overwritten for inquiries created via the full form. if not inquiry.metadata: inquiry.metadata = {} + if inquiry.metadata.get("subject_pending_ai") and analysis.get("title_en"): + inquiry.subject = analysis["title_en"][:500] + inquiry.metadata["subject_pending_ai"] = False inquiry.metadata["ai_analysis"] = { "title_en": analysis.get("title_en", ""), @@ -3123,6 +3133,7 @@ def _apply_inquiry_ai_analysis(inquiry, analysis, emotion_analysis): inquiry.save( update_fields=[ + "subject", "priority", "department", "ai_brief_en", @@ -3696,3 +3707,39 @@ def send_department_notification_task(payload): f"{emailed} emailed, {smsed} smsed" ) return {"emailed": emailed, "smsed": smsed} + + +@shared_task +def send_inquiry_reminder_email(inquiry_id, reminder_type, recipients): + """Send a reminder email to department champion and manager for an inquiry.""" + import logging + + logger = logging.getLogger(__name__) + + try: + from apps.complaints.models import Inquiry + from apps.notifications.services import NotificationService, get_email_header_html + + inquiry = Inquiry.objects.get(pk=inquiry_id) + + for recipient in recipients: + NotificationService.send_email( + email=recipient["email"], + subject=f"Reminder: Inquiry #{inquiry.reference_number} - Response Required", + message=f"This is a reminder that inquiry #{inquiry.reference_number} is awaiting your department's response. Please submit your response as soon as possible.", + html_message=f""" +
+ {get_email_header_html()} +
+

Reminder: Response Required

+

This is a reminder that inquiry #{inquiry.reference_number} is awaiting your department's response.

+

Please submit your response as soon as possible.

+
+
+""", + related_object=inquiry, + ) + + logger.info(f"Inquiry {inquiry_id} {reminder_type} reminder sent to {len(recipients)} recipients") + except Exception as e: + logger.error(f"Failed to send inquiry reminder email for {inquiry_id}: {e}") diff --git a/apps/complaints/ui_views.py b/apps/complaints/ui_views.py index adccf64..d741dd4 100644 --- a/apps/complaints/ui_views.py +++ b/apps/complaints/ui_views.py @@ -25,6 +25,25 @@ from apps.organizations.models import Department, Hospital, Staff from apps.observations.models import Observation from apps.px_sources.models import SourceUser, PXSource + +def _safe_next_redirect(request, fallback): + """Redirect to request.POST['next'] if it's a safe relative URL. + + Blocks open-redirect attacks. A URL is considered safe only if it starts + with a single leading slash and is not a protocol-relative form (``//`` + or ``/\\``) which browsers may resolve against a different host. + Falls back to ``fallback`` (a redirect()/reverse() result) otherwise. + """ + next_url = request.POST.get("next") if hasattr(request, "POST") else None + if ( + next_url + and next_url.startswith("/") + and not next_url.startswith("//") + and not next_url.startswith("/\\") + ): + return redirect(next_url) + return fallback + from .models import ( Complaint, ComplaintAttachment, @@ -330,6 +349,9 @@ def complaint_list(request): queryset = queryset.filter(hospital=selected_hospital) elif user.is_hospital_admin() and user.hospital: queryset = queryset.filter(hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) elif user.is_department_manager() and user.department: queryset = queryset.filter(department=user.department) elif user.is_champion() and user.department: @@ -877,9 +899,7 @@ def complaint_detail(request, pk): context["notes"] = complaint.notes.select_related("created_by").all() context["notes_count"] = context["notes"].count() - # Datastar variant for testing: /complaints//?ds=1 renders complaint_detail_ds.html - template = "complaints/complaint_detail_ds.html" if request.GET.get("ds") else "complaints/complaint_detail.html" - return render(request, template, context) + return render(request, "complaints/complaint_detail.html", context) @login_required @@ -1462,6 +1482,18 @@ def complaint_send_dept_reminder(request, pk): ) department = involved_dept.department + reminder_type = request.POST.get("reminder_type", "first") + + if reminder_type == "first" and involved_dept.first_reminder_sent_at: + messages.warning(request, _("First reminder already sent to this department.")) + return redirect("complaints:complaint_detail", pk=pk) + if reminder_type == "second" and involved_dept.second_reminder_sent_at: + messages.warning(request, _("Second reminder already sent to this department.")) + return redirect("complaints:complaint_detail", pk=pk) + if reminder_type == "second" and not involved_dept.first_reminder_sent_at: + messages.warning(request, _("Please send the first reminder before the second.")) + return redirect("complaints:complaint_detail", pk=pk) + targets = get_champion_and_manager(department) if not targets: messages.error( @@ -1522,10 +1554,16 @@ def complaint_send_dept_reminder(request, pk): notified.append(f"{display_name} ({label})") if sent_any: + if reminder_type == "first": + involved_dept.first_reminder_sent_at = timezone.now() + else: + involved_dept.second_reminder_sent_at = timezone.now() + involved_dept.save(update_fields=["first_reminder_sent_at", "second_reminder_sent_at"]) + ComplaintUpdate.objects.create( complaint=complaint, update_type="communication", - message=f"Manual reminder sent to {department.get_localized_name()} — {', '.join(notified)}", + message=f"Manual {reminder_type} reminder sent to {department.get_localized_name()} — {', '.join(notified)}", created_by=user, metadata={ "event_type": "manual_reminder", @@ -2762,7 +2800,7 @@ def inquiry_list(request): from .models import Inquiry # Base queryset with optimizations - queryset = Inquiry.objects.select_related("patient", "hospital", "department", "assigned_to", "responded_by") + queryset = Inquiry.objects.select_related("patient", "hospital", "department", "outgoing_department", "assigned_to", "responded_by") # Apply RBAC filters user = request.user @@ -2774,6 +2812,9 @@ def inquiry_list(request): queryset = queryset.filter(hospital=selected_hospital) elif user.is_hospital_admin() and user.hospital: queryset = queryset.filter(hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) elif user.is_department_manager() and user.department: queryset = queryset.filter( Q(department=user.department) | Q(outgoing_department=user.department) @@ -2789,33 +2830,55 @@ def inquiry_list(request): else: queryset = queryset.none() - # Apply filters + # Snapshot the RBAC base before direction/filters so we can compute + # incoming/outgoing counts for the tabs. + base_qs = queryset + + # Apply non-direction filters (status, category, hospital, department, search) status_filter = request.GET.get("status") if status_filter: - queryset = queryset.filter(status=status_filter) + base_qs = base_qs.filter(status=status_filter) category_filter = request.GET.get("category") if category_filter: - queryset = queryset.filter(category=category_filter) + base_qs = base_qs.filter(category=category_filter) hospital_filter = request.GET.get("hospital") if hospital_filter: - queryset = queryset.filter(hospital_id=hospital_filter) + base_qs = base_qs.filter(hospital_id=hospital_filter) department_filter = request.GET.get("department") if department_filter: - queryset = queryset.filter(department_id=department_filter) + base_qs = base_qs.filter(department_id=department_filter) - # Search search_query = request.GET.get("search") if search_query: - queryset = queryset.filter( + base_qs = base_qs.filter( Q(subject__icontains=search_query) | Q(message__icontains=search_query) | Q(contact_name__icontains=search_query) | Q(contact_email__icontains=search_query) ) + # Direction split: incoming vs outgoing + direction = request.GET.get("direction", "incoming") + dept = getattr(user, "department", None) + is_dept_scoped = user.is_department_manager() or user.is_champion() + if is_dept_scoped and dept: + sent_away = Q(department=dept) & Q(transferred_to_department__isnull=False) & ~Q(transferred_to_department=dept) + incoming_qs = base_qs.filter( + Q(department=dept) | Q(transferred_to_department=dept) | Q(outgoing_department=dept) + ).exclude(sent_away) + outgoing_qs = base_qs.filter(sent_away) + else: + # PX roles: incoming = not forwarded; outgoing = forwarded to a department + incoming_qs = base_qs.exclude(transferred_to_department__isnull=False) + outgoing_qs = base_qs.filter(transferred_to_department__isnull=False) + + incoming_count = incoming_qs.count() + outgoing_count = outgoing_qs.count() + queryset = outgoing_qs if direction == "outgoing" else incoming_qs + # Ordering order_by = request.GET.get("order_by", "-created_at") queryset = queryset.order_by(order_by) @@ -2850,6 +2913,9 @@ def inquiry_list(request): "stats": stats, "departments": departments, "filters": request.GET, + "direction": direction, + "incoming_count": incoming_count, + "outgoing_count": outgoing_count, } return render(request, "complaints/inquiry_list.html", context) @@ -2884,7 +2950,11 @@ def inquiry_detail(request, pk): # Check access user = request.user if not user.is_px_admin(): - if user.is_hospital_admin() and inquiry.hospital != user.hospital: + if user.is_px_management() or user.is_px_employee(): + if user.hospital and inquiry.hospital != user.hospital: + messages.error(request, "You don't have permission to view this inquiry.") + return redirect("inquiries:inquiry_list") + elif user.is_hospital_admin() and inquiry.hospital != user.hospital: messages.error(request, "You don't have permission to view this inquiry.") return redirect("inquiries:inquiry_list") elif ( @@ -3146,6 +3216,151 @@ def inquiry_send_to_staff(request, pk): return redirect("inquiries:inquiry_detail", pk=pk) +@login_required +@require_http_methods(["POST"]) +def inquiry_quick_create(request): + """Quick-create an inquiry from the simple modal (PX staff only). + + Fields: message, contact name, phone (required), email (optional), + location type, category, department, section. The inquiry is auto-activated + and assigned to the current user. AI fills subject (from its title) and + priority in the background; the user's department/section/category are kept. + """ + from .models import Inquiry, InquiryUpdate + + user = request.user + if not ( + user.is_px_admin() or user.is_hospital_admin() + or user.is_px_management() or user.is_px_employee() + ): + return JsonResponse( + {"success": False, "error": str(_("You don't have permission to create inquiries."))}, + status=403, + ) + + message = (request.POST.get("message") or "").strip() + contact_name = (request.POST.get("contact_name") or "").strip() + contact_phone = (request.POST.get("contact_phone") or "").strip() + contact_email = (request.POST.get("contact_email") or "").strip() + location_type = (request.POST.get("location_type") or "").strip() + department_id = (request.POST.get("department") or "").strip() + section_id = (request.POST.get("section") or "").strip() + + # Required-field validation (the inquiry-type `category` is intentionally NOT + # collected here — the model field is required at DB level so it is defaulted + # to "general" below. The department-category in the modal is a UI filter only.) + missing = { + "message": message, + "contact_name": contact_name, + "contact_phone": contact_phone, + "location_type": location_type, + "department": department_id, + } + for field, value in missing.items(): + if not value: + return JsonResponse( + {"success": False, "error": str(_("This field is required: {}").format(field))}, + status=400, + ) + + hospital = getattr(request, "tenant_hospital", None) or user.hospital + if not hospital: + return JsonResponse({"success": False, "error": str(_("No hospital assigned to your account."))}, status=400) + + # Validate location_type + from apps.complaints.models import Inquiry as _Inquiry + valid_location_types = {c for c, _ in _Inquiry._meta.get_field("location_type").choices or []} + if valid_location_types and location_type not in valid_location_types: + return JsonResponse({"success": False, "error": str(_("Invalid location type."))}, status=400) + + # Validate department belongs to the hospital + from apps.organizations.models import Department, Section + try: + department = Department.objects.get(id=department_id, hospital=hospital, status="active") + except Department.DoesNotExist: + return JsonResponse({"success": False, "error": str(_("Invalid department for this hospital."))}, status=400) + + # Section is optional — only validate if provided + section = None + if section_id: + try: + section = Section.objects.get(id=section_id, department=department) + except Section.DoesNotExist: + return JsonResponse( + {"success": False, "error": str(_("Selected section does not belong to this department."))}, + status=400, + ) + + try: + inquiry = Inquiry.objects.create( + hospital=hospital, + created_by=user, + message=message, + contact_name=contact_name, + contact_phone=contact_phone, + contact_email=contact_email, + location_type=location_type, + category="general", + department=department, + section=section, + # The inquiry is routed to this department for handling — mark it as + # sent so it appears in the department's incoming queue (not outgoing). + sent_to_department=True, + sent_to_department_at=timezone.now(), + # Defaults — AI refines subject/priority in the background (NOT department) + subject=message[:80], + priority="medium", + # Auto-activate + auto-assign to the creator + status="in_progress", + assigned_to=user, + assigned_at=timezone.now(), + activated_at=timezone.now(), + metadata={ + "subject_pending_ai": True, # AI fills the subject from its generated title + "ai_skip_department": True, # user provided department — AI must not override it + }, + ) + + # Background AI analysis (fills subject/priority/department) + notifications + from apps.complaints.tasks import analyze_inquiry_with_ai, notify_staff_new_item + analyze_inquiry_with_ai.delay(str(inquiry.id)) + notify_staff_new_item.delay("inquiry", str(inquiry.id)) + + InquiryUpdate.objects.create( + inquiry=inquiry, + update_type="note", + message="Inquiry created.", + created_by=user, + ) + roles_display = ", ".join(user.get_role_names()) if hasattr(user, "get_role_names") else "" + InquiryUpdate.objects.create( + inquiry=inquiry, + update_type="assignment", + message=f"Inquiry auto-activated and assigned to {user.get_full_name()} ({roles_display})", + created_by=user, + ) + + AuditService.log_event( + event_type="inquiry_created", + description=f"Quick inquiry created: {inquiry.subject}", + user=user, + content_object=inquiry, + metadata={"category": inquiry.category, "quick_create": True}, + ) + + return JsonResponse({ + "success": True, + "inquiry_id": str(inquiry.id), + "reference_number": inquiry.reference_number, + }) + except Exception as e: + logger.error(f"Quick inquiry create failed: {e}") + return JsonResponse( + {"success": False, "error": str(_("An error occurred while creating the inquiry."))}, + status=500, + ) + + @login_required @require_http_methods(["GET", "POST"]) def inquiry_create(request): @@ -3431,6 +3646,8 @@ def inquiry_assign(request, pk): user.is_px_admin() or user.is_hospital_admin() or (user.is_department_manager() and user.department and user.department == inquiry.department) + or user.is_px_management() + or user.is_px_employee() ) if not can_assign: messages.error(request, "You don't have permission to assign inquiries.") @@ -3509,24 +3726,37 @@ def inquiry_change_status(request, pk): if not ( user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() + or (user.is_department_manager() and user.department_id == inquiry.handling_department_id) + or (user.is_champion() and user.department_id == inquiry.handling_department_id) ): messages.error(request, "You don't have permission to change inquiry status.") - return redirect("inquiries:inquiry_detail", pk=pk) + return _safe_next_redirect(request, redirect("inquiries:inquiry_detail", pk=pk)) new_status = request.POST.get("status") note = request.POST.get("note", "") if not new_status: messages.error(request, "Please select a status.") - return redirect("inquiries:inquiry_detail", pk=pk) + return _safe_next_redirect(request, redirect("inquiries:inquiry_detail", pk=pk)) old_status = inquiry.status inquiry.status = new_status # Handle status-specific logic - if new_status == "resolved" and not inquiry.response: - messages.error(request, "Please add a response before resolving.") - return redirect("inquiries:inquiry_detail", pk=pk) + # Department users (champion / dept manager) must record a patient contact + # outcome before closing/resolving an inquiry. PX roles follow their own flow. + if new_status in ("resolved", "closed") and (user.is_champion() or user.is_department_manager()): + if inquiry.contact_status not in ("contacted", "contacted_no_response"): + messages.error(request, _("Please record a patient contact outcome before closing.")) + return _safe_next_redirect(request, redirect("inquiries:inquiry_detail", pk=pk)) + + # "Contacted - No Response" is a terminal status set when the department could + # not reach the patient. Auto-sync the contact_status tracking so outreach data + # stays consistent without double data entry. + if new_status == "contact_no_response": + inquiry.contact_status = "contacted_no_response" + inquiry.contact_status_at = timezone.now() + inquiry.contact_status_by = user inquiry._acting_user = request.user inquiry.save() @@ -3551,7 +3781,67 @@ def inquiry_change_status(request, pk): ) messages.success(request, f"Inquiry status changed to {new_status}.") - return redirect("inquiries:inquiry_detail", pk=pk) + return _safe_next_redirect(request, redirect("inquiries:inquiry_detail", pk=pk)) + + +@login_required +@require_http_methods(["POST"]) +def inquiry_set_contact_status(request, pk): + """Record the outcome of the department's patient-contact attempt. + + Sets the existing ``contact_status`` field (not_contacted / contacted / + contacted_no_response). Used by the department (champion/manager) that owns + the inquiry to log communication outcomes with the patient. + """ + from .models import Inquiry, InquiryUpdate + + inquiry = get_object_or_404(Inquiry, pk=pk) + + user = request.user + if not ( + user.is_px_admin() or user.is_hospital_admin() + or user.is_px_management() or user.is_px_employee() + or (user.is_department_manager() and user.department_id == inquiry.handling_department_id) + or (user.is_champion() and user.department_id == inquiry.handling_department_id) + ): + messages.error(request, _("You don't have permission to set the contact status.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + new_contact_status = request.POST.get("contact_status") + note = request.POST.get("note", "").strip() + + valid = {c for c, _ in Inquiry._meta.get_field("contact_status").choices} + if new_contact_status not in valid: + messages.error(request, _("Please select a valid contact status.")) + return redirect("inquiries:inquiry_detail", pk=pk) + + old_contact_status = inquiry.contact_status + inquiry.contact_status = new_contact_status + inquiry.contact_status_at = timezone.now() + inquiry.contact_status_by = user + inquiry.save(update_fields=["contact_status", "contact_status_at", "contact_status_by"]) + + display = dict(Inquiry._meta.get_field("contact_status").choices).get(new_contact_status, new_contact_status) + InquiryUpdate.objects.create( + inquiry=inquiry, + update_type="communication", + message=note or _("Contact status set to: {status}").format(status=display), + created_by=user, + ) + + AuditService.log_event( + event_type="inquiry_contact_status", + description=_("Contact status changed from {old} to {new}").format( + old=old_contact_status or "—", new=new_contact_status + ), + user=user, + content_object=inquiry, + metadata={"old_contact_status": old_contact_status, "new_contact_status": new_contact_status}, + ) + + messages.success(request, _("Contact status updated to: {status}").format(status=display)) + + return _safe_next_redirect(request, redirect("inquiries:inquiry_detail", pk=pk)) @login_required @@ -3566,6 +3856,8 @@ def inquiry_reopen(request, pk): user.is_px_admin() or user.is_hospital_admin() or (user.is_department_manager() and user.department == inquiry.department) + or user.is_px_management() + or user.is_px_employee() ): messages.error(request, "You don't have permission to reopen inquiries.") return redirect("inquiries:inquiry_detail", pk=pk) @@ -3643,6 +3935,8 @@ def inquiry_respond(request, pk): and user.department and user.department in [inquiry.department, inquiry.outgoing_department] ) + or user.is_px_management() + or user.is_px_employee() ) if not can_respond: messages.error(request, "You don't have permission to respond to inquiries.") @@ -3755,8 +4049,9 @@ def inquiry_transfer_to_department(request, pk): if not ( user.is_px_admin() or user.is_hospital_admin() - or user.is_department_manager() + or (user.is_department_manager() and user.department_id == inquiry.handling_department_id) or user.is_px_management() or user.is_px_employee() + or (user.is_champion() and user.department_id == inquiry.handling_department_id) ): messages.error(request, _("You don't have permission to transfer inquiries to departments.")) return redirect("inquiries:inquiry_detail", pk=pk) @@ -4000,8 +4295,9 @@ def inquiry_send_to(request, pk): if not ( user.is_px_admin() or user.is_hospital_admin() - or user.is_department_manager() + or (user.is_department_manager() and user.department_id == inquiry.handling_department_id) or user.is_px_management() or user.is_px_employee() + or (user.is_champion() and user.department_id == inquiry.handling_department_id) ): return JsonResponse({ "success": False, @@ -4097,9 +4393,8 @@ def inquiry_send_to(request, pk): inquiry.transferred_to_department = department inquiry.transfer_count = (inquiry.transfer_count or 0) + 1 - if inquiry.department_id == department.pk: - inquiry.sent_to_department = True - inquiry.sent_to_department_at = timezone.now() + inquiry.sent_to_department = True + inquiry.sent_to_department_at = timezone.now() sla_config = inquiry.get_sla_config() if sla_config and sla_config.dept_response_hours: @@ -4184,6 +4479,8 @@ def inquiry_department_response(request, pk): inquiry.department == user.department or inquiry.outgoing_department == user.department )) + or user.is_px_management() + or user.is_px_employee() ): messages.error(request, "You don't have permission to respond to this inquiry.") return redirect("inquiries:inquiry_detail", pk=pk) @@ -4243,28 +4540,6 @@ Generate a JSON response with: content_object=inquiry, ) - # Notify inquirer that department has responded - try: - from apps.notifications.services import NotificationService - from apps.core.utils import build_public_track_url - - track_url = build_public_track_url("inquiry", inquiry.reference_number) - if inquiry.contact_phone: - NotificationService.send_sms( - phone=inquiry.contact_phone, - message=f"PX360: Your inquiry {inquiry.reference_number} has been responded to. View: {track_url}", - related_object=inquiry, - ) - if inquiry.contact_email: - NotificationService.send_email( - email=inquiry.contact_email, - subject=f"PX360: Response to Your Inquiry {inquiry.reference_number}", - message=f"Your inquiry has been responded to.\n\nView: {track_url}", - related_object=inquiry, - ) - except Exception as e: - logger.warning(f"Failed to send inquirer notification: {e}") - messages.success(request, "Department response submitted successfully.") if request.headers.get('X-Requested-With') == 'XMLHttpRequest': from django.http import JsonResponse @@ -4307,30 +4582,29 @@ def inquiry_send_dept_response_reminder(request, pk): reminder_type = request.POST.get("reminder_type", "first") + if reminder_type == "first" and inquiry.dept_response_reminder_sent_at: + messages.warning(request, "First reminder already sent.") + return redirect("inquiries:inquiry_detail", pk=pk) + if reminder_type == "second" and inquiry.dept_response_second_reminder_sent_at: + messages.warning(request, "Second reminder already sent.") + return redirect("inquiries:inquiry_detail", pk=pk) + if reminder_type == "second" and not inquiry.dept_response_reminder_sent_at: + messages.warning(request, "Please send the first reminder before the second.") + return redirect("inquiries:inquiry_detail", pk=pk) + try: - from apps.notifications.services import NotificationService, get_email_header_html + from apps.organizations.department_contacts import get_champion_and_manager + + targets = get_champion_and_manager(dept) + if not targets: + messages.error(request, f"No champion or manager assigned to {dept.name}.") + return redirect("inquiries:inquiry_detail", pk=pk) recipients = [] - if dept.champion and dept.champion.user and dept.champion.user.email: - recipients.append(dept.champion.user) - - for recipient in recipients: - NotificationService.send_email( - email=recipient.email, - subject=f"Reminder: Inquiry #{inquiry.reference_number} - Response Required", - message=f"This is a reminder that inquiry #{inquiry.reference_number} is awaiting your department's response. Please submit your response as soon as possible.", - html_message=f""" -
- {get_email_header_html()} -
-

Reminder: Response Required

-

This is a reminder that inquiry #{inquiry.reference_number} is awaiting your department's response.

-

Please submit your response as soon as possible.

-
-
-""", - related_object=inquiry, - ) + for target in targets: + email = target.get("email") or "" + if email: + recipients.append({"email": email, "name": target.get("display_name", "")}) if reminder_type == "first": inquiry.dept_response_reminder_sent_at = timezone.now() @@ -4338,6 +4612,9 @@ def inquiry_send_dept_response_reminder(request, pk): inquiry.dept_response_second_reminder_sent_at = timezone.now() inquiry.save() + from .tasks import send_inquiry_reminder_email + send_inquiry_reminder_email.delay(str(inquiry.id), reminder_type, recipients) + InquiryUpdate.objects.create( inquiry=inquiry, update_type="note", @@ -4400,7 +4677,7 @@ def inquiry_update_contact_stage(request, pk): inquiry = get_object_or_404(Inquiry, pk=pk) user = request.user - if not (user.is_px_admin() or user.is_hospital_admin() or inquiry.assigned_to == user): + if not (user.is_px_admin() or user.is_hospital_admin() or inquiry.assigned_to == user or user.is_px_management() or user.is_px_employee()): messages.error(request, _("You don't have permission to update contact tracking.")) return redirect("inquiries:inquiry_detail", pk=pk) @@ -4885,6 +5162,7 @@ def public_inquiry_submit(request): name = request.POST.get("name") email = request.POST.get("email") phone = request.POST.get("phone") + national_id = request.POST.get("national_id", "") hospital_id = request.POST.get("hospital") location_type = request.POST.get("location_type", "") department_id = request.POST.get("department", "") @@ -4949,6 +5227,7 @@ def public_inquiry_submit(request): contact_name=name, contact_phone=phone, contact_email=email, + national_id=national_id, status="open", is_outgoing=False, ) @@ -6508,7 +6787,7 @@ def adverse_action_add(request, complaint_pk): "complaint": complaint, "action_type_choices": ComplaintAdverseAction.ActionType.choices, "severity_choices": ComplaintAdverseAction.SeverityLevel.choices, - "staff_list": Staff.objects.filter(hospital=complaint.hospital, is_active=True), + "staff_list": Staff.objects.filter(hospital=complaint.hospital, status="active"), }, ) @@ -6561,11 +6840,10 @@ def adverse_action_add(request, complaint_pk): "complaint": complaint, "action_type_choices": ComplaintAdverseAction.ActionType.choices, "severity_choices": ComplaintAdverseAction.SeverityLevel.choices, - "staff_list": Staff.objects.filter(hospital=complaint.hospital, is_active=True) + "staff_list": Staff.objects.filter(hospital=complaint.hospital, status="active") if complaint.hospital - else Staff.objects.filter(is_active=True), + else Staff.objects.filter(status="active"), } - return render(request, "complaints/adverse_action_form.html", context) @@ -6623,9 +6901,9 @@ def adverse_action_edit(request, pk): "complaint": complaint, "action_type_choices": ComplaintAdverseAction.ActionType.choices, "severity_choices": ComplaintAdverseAction.SeverityLevel.choices, - "staff_list": Staff.objects.filter(hospital=complaint.hospital, is_active=True) + "staff_list": Staff.objects.filter(hospital=complaint.hospital, status="active") if complaint.hospital - else Staff.objects.filter(is_active=True), + else Staff.objects.filter(status="active"), "selected_staff": list(adverse_action.involved_staff.values_list("id", flat=True)), } diff --git a/apps/complaints/urls_inquiries.py b/apps/complaints/urls_inquiries.py index 14c1ae8..385692b 100644 --- a/apps/complaints/urls_inquiries.py +++ b/apps/complaints/urls_inquiries.py @@ -9,11 +9,13 @@ urlpatterns = [ # Inquiries UI Views path("", ui_views.inquiry_list, name="inquiry_list"), path("new/", ui_views.inquiry_create, name="inquiry_create"), + path("new/quick/", ui_views.inquiry_quick_create, name="inquiry_quick_create"), path("/", ui_views.inquiry_detail, name="inquiry_detail"), path("/edit/", ui_views.inquiry_edit, name="inquiry_edit"), path("/activate/", ui_views.inquiry_activate, name="inquiry_activate"), path("/assign/", ui_views.inquiry_assign, name="inquiry_assign"), path("/change-status/", ui_views.inquiry_change_status, name="inquiry_change_status"), + path("/contact-status/", ui_views.inquiry_set_contact_status, name="inquiry_set_contact_status"), path("/reopen/", ui_views.inquiry_reopen, name="inquiry_reopen"), path("/add-note/", ui_views.inquiry_add_note, name="inquiry_add_note"), path("/respond/", ui_views.inquiry_respond, name="inquiry_respond"), diff --git a/apps/complaints/views.py b/apps/complaints/views.py index 3419328..0a86902 100644 --- a/apps/complaints/views.py +++ b/apps/complaints/views.py @@ -1721,7 +1721,7 @@ Provide 3-5 suggestions. Keep them concise and focused on how to respond to the user.is_px_admin() or (user.is_hospital_admin() and user.hospital == complaint.hospital) or (user.is_department_manager() and user.department == complaint.department) - or (user.is_px_employee and complaint.hospital == user.hospital) + or (user.is_px_employee() and complaint.hospital == user.hospital) ) if not can_generate: return Response( @@ -1804,7 +1804,7 @@ Provide 3-5 suggestions. Keep them concise and focused on how to respond to the user.is_px_admin() or (user.is_hospital_admin() and user.hospital == complaint.hospital) or (user.is_department_manager() and user.department == complaint.department) - or (user.is_px_employee and complaint.hospital == user.hospital) + or (user.is_px_employee() and complaint.hospital == user.hospital) ) if not can_generate: return Response( @@ -2992,6 +2992,8 @@ class InquiryViewSet(viewsets.ModelViewSet): user = request.user can_generate = ( user.is_px_admin() + or user.is_px_management() + or user.is_px_employee() or (user.is_hospital_admin() and user.hospital == inquiry.hospital) or inquiry.assigned_to == user ) @@ -5210,28 +5212,6 @@ Generate JSON with "summary_en" and "summary_ar".""" except Exception: pass - # Notify inquirer - track_url = build_public_track_url("inquiry", inquiry.reference_number) - if inquiry.contact_phone: - try: - NotificationService.send_sms( - phone=inquiry.contact_phone, - message=f"PX360: Your inquiry {inquiry.reference_number} has been responded to. View: {track_url}", - related_object=inquiry, - ) - except Exception: - pass - if inquiry.contact_email: - try: - NotificationService.send_email( - email=inquiry.contact_email, - subject=f"PX360: Response to Your Inquiry {inquiry.reference_number}", - message=f"Your inquiry has been responded to.\n\nView: {track_url}", - related_object=inquiry, - ) - except Exception: - pass - return render(request, "complaints/inquiry_response_success_token.html", {"inquiry": inquiry}) return render(request, "complaints/inquiry_response_form_token.html", {"inquiry": inquiry}) diff --git a/apps/core/ai_service.py b/apps/core/ai_service.py index 1fb10e9..9e62d6b 100644 --- a/apps/core/ai_service.py +++ b/apps/core/ai_service.py @@ -1010,8 +1010,8 @@ class AIService: "title": title or "Complaint", "title_en": title or "Complaint", "title_ar": title or "شكوى", - "short_description_en": description[:200] if description else "", - "short_description_ar": description[:200] if description else "", + "short_description_en": "", + "short_description_ar": "", "severity": "medium", "priority": "medium", "category": "other", @@ -1032,8 +1032,8 @@ class AIService: "title": title or "Complaint", "title_en": title or "Complaint", "title_ar": title or "شكوى", - "short_description_en": description[:200] if description else "", - "short_description_ar": description[:200] if description else "", + "short_description_en": "", + "short_description_ar": "", "severity": "medium", "priority": "medium", "category": "other", @@ -1182,8 +1182,8 @@ Always use EXACT names and codes from the provided taxonomy - do not invent new "title_ar": subject, "brief_summary_en": "Inquiry", "brief_summary_ar": "استفسار", - "short_description_en": message[:200] if message else "", - "short_description_ar": message[:200] if message else "", + "short_description_en": "", + "short_description_ar": "", "priority": "medium", "department": "", "reasoning_en": "AI analysis failed, using default values", @@ -1198,8 +1198,8 @@ Always use EXACT names and codes from the provided taxonomy - do not invent new "title_ar": subject, "brief_summary_en": "Inquiry", "brief_summary_ar": "استفسار", - "short_description_en": message[:200] if message else "", - "short_description_ar": message[:200] if message else "", + "short_description_en": "", + "short_description_ar": "", "priority": "medium", "department": "", "reasoning_en": f"AI service unavailable: {str(e)}", @@ -1418,8 +1418,8 @@ Always use EXACT names and codes from the provided taxonomy - do not invent new "title_ar": title or "ملاحظة", "brief_summary_en": "Observation", "brief_summary_ar": "ملاحظة", - "short_description_en": description[:200] if description else "", - "short_description_ar": description[:200] if description else "", + "short_description_en": "", + "short_description_ar": "", "severity": "medium", "category": "Other", "department": "", @@ -1439,8 +1439,8 @@ Always use EXACT names and codes from the provided taxonomy - do not invent new "title_ar": title or "ملاحظة", "brief_summary_en": "Observation", "brief_summary_ar": "ملاحظة", - "short_description_en": description[:200] if description else "", - "short_description_ar": description[:200] if description else "", + "short_description_en": "", + "short_description_ar": "", "severity": "medium", "category": "Other", "department": "", @@ -2218,10 +2218,10 @@ Respond in JSON format: except json.JSONDecodeError as e: logger.error(f"Failed to parse suggestion AI response: {e}") return { - "short_description_en": message[:200] if message else "", - "short_description_ar": message[:200] if message else "", - "description_en": message[:500] if message else "", - "description_ar": message[:500] if message else "", + "short_description_en": "", + "short_description_ar": "", + "description_en": "", + "description_ar": "", "category": "other", "priority": "medium", "suggested_actions": [], @@ -2233,10 +2233,10 @@ Respond in JSON format: except AIServiceError as e: logger.error(f"AI service error for suggestion: {e}") return { - "short_description_en": message[:200] if message else "", - "short_description_ar": message[:200] if message else "", - "description_en": message[:500] if message else "", - "description_ar": message[:500] if message else "", + "short_description_en": "", + "short_description_ar": "", + "description_en": "", + "description_ar": "", "category": "other", "priority": "medium", "suggested_actions": [], diff --git a/apps/core/middleware.py b/apps/core/middleware.py index de8714e..c3cb377 100644 --- a/apps/core/middleware.py +++ b/apps/core/middleware.py @@ -118,7 +118,9 @@ class DepartmentRespondentMiddleware(MiddlewareMixin): if not request.user.is_champion(): return None - if request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_department_manager() or request.user.is_director(): + if (request.user.is_px_admin() or request.user.is_hospital_admin() + or request.user.is_department_manager() or request.user.is_director() + or request.user.is_px_employee() or request.user.is_px_management()): return None if request.headers.get("x-requested-with") == "XMLHttpRequest": diff --git a/apps/core/validators.py b/apps/core/validators.py new file mode 100644 index 0000000..e8db3a3 --- /dev/null +++ b/apps/core/validators.py @@ -0,0 +1,45 @@ +""" +Shared validators used across multiple apps. +""" +import re + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +# Regex for a Saudi mobile number after stripping spaces/dashes/parens/plus. +# Accepts any of: +# 05XXXXXXXX (10 digits, local with trunk 0) +# 5XXXXXXXX (9 digits, no trunk) +# 9665XXXXXXXX (12 digits, country code without +) +# +9665XXXXXXXX (13 digits, country code with +) +SAUDI_MOBILE_REGEX = re.compile(r"^(?:\+?966|0)?5\d{8}$") + +# HTML5 form pattern equivalent (HTML anchors ^...$ implicitly). +# Exposed for reuse in widget attrs and template . +SAUDI_PHONE_HTML_PATTERN = r"(\+?966|0)?5[0-9]{8}" + + +def validate_saudi_phone(value): + """ + Validate that ``value`` is a Saudi mobile number. + + Accepts the following formats (spaces, dashes and parentheses are ignored): + 05XXXXXXXX + 5XXXXXXXX + 9665XXXXXXXX + +9665XXXXXXXX + + Stored value is returned unchanged (no normalization). + Raises ``ValidationError`` if the number is not a valid Saudi mobile. + """ + if value is None or value == "": + return value + + cleaned = re.sub(r"[\s\-()]", "", str(value)) + if not SAUDI_MOBILE_REGEX.match(cleaned): + raise ValidationError( + _("Please enter a valid Saudi mobile number " + "(e.g. 05XXXXXXXX or +9665XXXXXXXX)."), + code="invalid_saudi_phone", + ) + return value diff --git a/apps/feedback/forms.py b/apps/feedback/forms.py index adb24a5..55f6f25 100644 --- a/apps/feedback/forms.py +++ b/apps/feedback/forms.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ from apps.organizations.models import Department, Hospital, Patient, Staff from apps.core.form_mixins import HospitalFieldMixin +from apps.core.validators import SAUDI_PHONE_HTML_PATTERN, validate_saudi_phone from .models import Feedback, FeedbackResponse, FeedbackStatus, FeedbackType, FeedbackCategory @@ -259,7 +260,13 @@ class PublicSuggestionForm(forms.Form): label=_("Mobile Number"), max_length=20, required=True, - widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your mobile number")}), + validators=[validate_saudi_phone], + widget=forms.TextInput(attrs={ + "class": "form-control", + "placeholder": _("05XXXXXXXX or +9665XXXXXXXX"), + "pattern": SAUDI_PHONE_HTML_PATTERN, + "inputmode": "tel", + }), ) contact_email = forms.EmailField( label=_("Email Address"), diff --git a/apps/feedback/migrations/0009_add_activation_fields.py b/apps/feedback/migrations/0009_add_activation_fields.py new file mode 100644 index 0000000..e21f842 --- /dev/null +++ b/apps/feedback/migrations/0009_add_activation_fields.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.1 on 2026-07-14 12:00 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0008_feedback_reference_number_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='feedback', + name='activated_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='feedback', + name='activated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activated_feedbacks', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/feedback/migrations/0010_alter_feedback_contact_phone.py b/apps/feedback/migrations/0010_alter_feedback_contact_phone.py new file mode 100644 index 0000000..2c40f8a --- /dev/null +++ b/apps/feedback/migrations/0010_alter_feedback_contact_phone.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-07-18 14:46 + +import apps.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('feedback', '0009_add_activation_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='feedback', + name='contact_phone', + field=models.CharField(blank=True, max_length=20, validators=[apps.core.validators.validate_saudi_phone]), + ), + ] diff --git a/apps/feedback/models.py b/apps/feedback/models.py index 5765851..671a911 100644 --- a/apps/feedback/models.py +++ b/apps/feedback/models.py @@ -15,6 +15,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.core.models import PriorityChoices, SoftDeleteModel, TimeStampedModel, UUIDModel +from apps.core.validators import validate_saudi_phone class FeedbackType(models.TextChoices): @@ -94,7 +95,7 @@ class Feedback(UUIDModel, TimeStampedModel, SoftDeleteModel): is_anonymous = models.BooleanField(default=False) contact_name = models.CharField(max_length=200, blank=True) contact_email = models.EmailField(blank=True) - contact_phone = models.CharField(max_length=20, blank=True) + contact_phone = models.CharField(max_length=20, blank=True, validators=[validate_saudi_phone]) encounter_id = models.CharField( max_length=100, blank=True, db_index=True, help_text="Related encounter ID if applicable" @@ -196,6 +197,10 @@ class Feedback(UUIDModel, TimeStampedModel, SoftDeleteModel): status = models.CharField( max_length=20, choices=FeedbackStatus.choices, default=FeedbackStatus.SUBMITTED, db_index=True ) + activated_at = models.DateTimeField(null=True, blank=True) + activated_by = models.ForeignKey( + "accounts.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="activated_feedbacks" + ) # Assignment assigned_to = models.ForeignKey( diff --git a/apps/feedback/urls.py b/apps/feedback/urls.py index 94b66df..ba5fc62 100644 --- a/apps/feedback/urls.py +++ b/apps/feedback/urls.py @@ -17,6 +17,7 @@ urlpatterns = [ path("/update/", views.feedback_update, name="feedback_update"), path("/delete/", views.feedback_delete, name="feedback_delete"), # Workflow actions + path("/activate/", views.feedback_activate, name="feedback_activate"), path("/assign/", views.feedback_assign, name="feedback_assign"), path("/change-status/", views.feedback_change_status, name="feedback_change_status"), path("/send-to-department/", views.feedback_send_to_department, name="feedback_send_to_department"), diff --git a/apps/feedback/views.py b/apps/feedback/views.py index e985c06..62a7ed7 100644 --- a/apps/feedback/views.py +++ b/apps/feedback/views.py @@ -259,11 +259,14 @@ def feedback_detail(request, pk): hospital=feedback.hospital, status="active" ).order_by("name"), "status_choices": FeedbackStatus.choices, + "can_activate": feedback.activated_at is None, + "is_active": feedback.activated_at is not None, "can_edit": ( - user.is_px_admin() + (user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() - or user.is_px_employee() + or user.is_px_employee()) + and feedback.activated_at is not None ), "can_admin": user.is_px_admin() or user.is_hospital_admin(), "can_send_to_department": ( @@ -282,8 +285,8 @@ def feedback_detail(request, pk): # Workflow stepper _fb_steps = [ {"label": "Submitted", "icon": "plus-circle", "done": True}, - {"label": "Assigned", "icon": "user-plus", "done": feedback.assigned_to is not None}, - {"label": "Reviewed", "icon": "eye", "done": feedback.status in ("reviewed", "acknowledged", "closed")}, + {"label": "Activated", "icon": "zap", "done": feedback.activated_at is not None}, + {"label": "Sent to Dept", "icon": "send", "done": feedback.department_id is not None or feedback.status in ("acknowledged", "closed")}, {"label": "Acknowledged", "icon": "thumbs-up", "done": feedback.status in ("acknowledged", "closed")}, {"label": "Closed", "icon": "check-circle-2", "done": feedback.status == "closed"}, ] @@ -298,7 +301,6 @@ def feedback_detail(request, pk): @login_required @require_http_methods(["GET", "POST"]) def feedback_create(request): - from apps.organizations.models import LegacyLocation, LegacyMainSection, LegacySubSection, Hospital from apps.feedback.models import FeedbackType, FeedbackCategory, FeedbackStatus communication_request = None @@ -313,11 +315,12 @@ def feedback_create(request): except Exception: pass + hospital = getattr(request, "tenant_hospital", None) or request.user.hospital + if request.method == "POST": contact_name = request.POST.get("contact_name", "").strip() contact_phone = request.POST.get("contact_phone", "").strip() message = request.POST.get("message", "").strip() - hospital_id = request.POST.get("hospital", "") title = request.POST.get("title", message[:100]).strip() errors = [] @@ -327,23 +330,15 @@ def feedback_create(request): errors.append("Phone is required") if not message: errors.append("Message is required") - if not hospital_id: - errors.append("Hospital is required") + if hospital is None: + errors.append("No hospital context. Contact your administrator.") if errors: for e in errors: messages.error(request, e) else: try: - hospital = Hospital.objects.get(id=hospital_id) - - # Auto-link patient by phone (local first, then HIS). Never blocks creation. - from apps.organizations.patient_lookup import find_or_link_patient - - linked_patient = find_or_link_patient(phone=contact_phone, hospital=hospital) - feedback = Feedback( - patient=linked_patient, hospital=hospital, feedback_type=FeedbackType.SUGGESTION, title=title, @@ -365,10 +360,11 @@ def feedback_create(request): except Exception: pass - from apps.feedback.tasks import analyze_suggestion_with_ai from apps.complaints.tasks import notify_staff_new_item + from apps.feedback.tasks import analyze_suggestion_with_ai, link_feedback_patient analyze_suggestion_with_ai.delay(str(feedback.id)) notify_staff_new_item.delay("suggestion", str(feedback.id)) + link_feedback_patient.delay(str(feedback.id), phone=contact_phone) FeedbackResponse.objects.create( feedback=feedback, @@ -400,9 +396,8 @@ def feedback_create(request): except Exception as e: messages.error(request, f"Error creating suggestion: {str(e)}") - hospitals = Hospital.objects.filter(status="active").order_by("name") context = { - "hospitals": hospitals, + "selected_hospital_id": str(hospital.id) if hospital else "", "is_create": True, "communication_request": communication_request, "prefill": prefill, @@ -418,7 +413,7 @@ def feedback_update(request, pk): # Check permission user = request.user - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee()): messages.error(request, "You don't have permission to edit this suggestion.") return redirect("feedback:feedback_detail", pk=pk) @@ -473,6 +468,14 @@ def feedback_update(request, pk): "feedback": feedback, "patients": patients, "is_create": False, + "prefill": { + "contact_name": feedback.contact_name or "", + "contact_phone": feedback.contact_phone or "", + "contact_email": feedback.contact_email or "", + "message": feedback.message or "", + "title": feedback.title or "", + }, + "selected_hospital_id": feedback.hospital_id or "", } return render(request, "feedback/feedback_form.html", context) @@ -486,7 +489,7 @@ def feedback_delete(request, pk): # Check permission user = request.user - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee()): messages.error(request, "You don't have permission to delete this suggestion.") return redirect("feedback:feedback_detail", pk=pk) @@ -525,7 +528,7 @@ def comment_import_list(request): """ from django.core.exceptions import PermissionDenied - if not (request.user.is_px_admin() or request.user.is_hospital_admin()): + if not (request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_px_employee()): raise PermissionDenied from .models import CommentImport @@ -547,7 +550,7 @@ def comment_list(request): """ from django.core.exceptions import PermissionDenied - if not (request.user.is_px_admin() or request.user.is_hospital_admin()): + if not (request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_px_employee()): raise PermissionDenied from .models import PatientComment @@ -616,7 +619,7 @@ def action_plan_list(request): """ from django.core.exceptions import PermissionDenied - if not (request.user.is_px_admin() or request.user.is_hospital_admin()): + if not (request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_px_employee()): raise PermissionDenied from .models import CommentActionPlan @@ -653,7 +656,7 @@ def export_comments_step1(request): """ from django.core.exceptions import PermissionDenied - if not (request.user.is_px_admin() or request.user.is_hospital_admin()): + if not (request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_px_employee()): raise PermissionDenied from .models import PatientComment @@ -682,7 +685,7 @@ def export_comments_step2(request): """ from django.core.exceptions import PermissionDenied - if not (request.user.is_px_admin() or request.user.is_hospital_admin()): + if not (request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_px_employee()): raise PermissionDenied from .models import PatientComment @@ -708,7 +711,7 @@ def export_action_plans(request): """ from django.core.exceptions import PermissionDenied - if not (request.user.is_px_admin() or request.user.is_hospital_admin()): + if not (request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_px_employee()): raise PermissionDenied from .models import CommentActionPlan @@ -723,6 +726,53 @@ def export_action_plans(request): return export_action_plans(qs) +@login_required +@require_http_methods(["POST"]) +def feedback_activate(request, pk): + """Activate a suggestion and auto-assign to the activating user.""" + feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False) + + user = request.user + if not ( + user.is_px_admin() + or user.is_hospital_admin() + or user.is_px_management() + or user.is_px_employee() + ): + messages.error(request, _("You don't have permission to activate this suggestion.")) + return redirect("feedback:feedback_detail", pk=pk) + + if feedback.activated_at: + messages.warning(request, _("This suggestion has already been activated.")) + return redirect("feedback:feedback_detail", pk=pk) + + feedback.activated_at = timezone.now() + feedback.activated_by = user + feedback.assigned_to = user + feedback.assigned_at = timezone.now() + if feedback.status == FeedbackStatus.SUBMITTED: + feedback.status = FeedbackStatus.REVIEWED + feedback.save() + + FeedbackResponse.objects.create( + feedback=feedback, + response_type="note", + message=f"Suggestion activated and assigned to {user.get_full_name()}", + created_by=user, + is_internal=True, + ) + + AuditService.log_event( + event_type="suggestion_activated", + description=f"Suggestion '{feedback.title}' activated by {user.get_full_name()}", + user=user, + content_object=feedback, + ) + + messages.success(request, _("Suggestion activated successfully.")) + return redirect("feedback:feedback_detail", pk=pk) + + @login_required @require_http_methods(["POST"]) def feedback_assign(request, pk): @@ -887,7 +937,7 @@ def feedback_add_response(request, pk): user = request.user if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() - or user.is_department_manager()): + or user.is_department_manager() or user.is_px_employee()): messages.error(request, "You don't have permission to add responses to this suggestion.") return redirect("feedback:feedback_detail", pk=pk) @@ -920,7 +970,7 @@ def feedback_toggle_featured(request, pk): # Check permission user = request.user - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to feature this suggestion.") return redirect("feedback:feedback_detail", pk=pk) @@ -941,7 +991,7 @@ def feedback_toggle_follow_up(request, pk): # Check permission user = request.user - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to modify this suggestion.") return redirect("feedback:feedback_detail", pk=pk) @@ -1070,7 +1120,7 @@ def feedback_create_action(request, pk): feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False) user = request.user - if not (user.is_px_admin() or user.is_hospital_admin()): + if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to create actions from suggestions.") return redirect("feedback:feedback_detail", pk=pk) diff --git a/apps/integrations/api_serializers.py b/apps/integrations/api_serializers.py index 183301f..e3f0c5d 100644 --- a/apps/integrations/api_serializers.py +++ b/apps/integrations/api_serializers.py @@ -8,6 +8,7 @@ from apps.complaints.models import Complaint, Inquiry from apps.observations.models import Observation from apps.appreciation.models import Appreciation, AppreciationStatus, AppreciationVisibility from apps.feedback.models import Feedback, FeedbackType, FeedbackStatus, FeedbackCategory +from apps.physicians.models import PhysicianIndividualRating # --------------------------------------------------------------------------- @@ -36,7 +37,7 @@ class ExternalComplaintCreateSerializer(serializers.Serializer): # Hospital context hospital = serializers.CharField(max_length=200, required=True) - # Location hierarchy (all optional) + # Location hierarchy location_type = serializers.ChoiceField( choices=[("OP", "Outpatient"), ("IP", "Inpatient"), ("ER", "Emergency"), ("GENERAL", "General")], required=False, @@ -44,7 +45,7 @@ class ExternalComplaintCreateSerializer(serializers.Serializer): default="", ) area = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") - department = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + department_code = serializers.CharField(max_length=200, required=True) section = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") # Complaint content @@ -72,17 +73,19 @@ class ExternalComplaintCreateSerializer(serializers.Serializer): except Area.DoesNotExist: raise serializers.ValidationError({"area": f"Area '{area_name}' not found for this hospital."}) - dept_name = data.get("department") - if dept_name: + department_code = data.get("department_code") + if department_code: from apps.organizations.models import Department try: data["department"] = Department.objects.get( - name__iexact=dept_name.strip(), hospital=hospital, status="active" + code__iexact=department_code.strip(), hospital=hospital, status="active" ) except Department.DoesNotExist: raise serializers.ValidationError( - {"department": f"Department '{dept_name}' not found for this hospital."} + {"department_code": f"Department with code '{department_code}' not found for this hospital."} ) + else: + data["department"] = None section_name = data.get("section") if section_name: @@ -156,7 +159,7 @@ class ExternalInquiryCreateSerializer(serializers.Serializer): default="", ) area = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") - department = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + department_code = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") section = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") # Inquiry content @@ -194,17 +197,19 @@ class ExternalInquiryCreateSerializer(serializers.Serializer): except Area.DoesNotExist: raise serializers.ValidationError({"area": f"Area '{area_name}' not found for this hospital."}) - dept_name = data.get("department") - if dept_name: + department_code = data.get("department_code") + if department_code: from apps.organizations.models import Department try: data["department"] = Department.objects.get( - name__iexact=dept_name.strip(), hospital=hospital, status="active" + code__iexact=department_code.strip(), hospital=hospital, status="active" ) except Department.DoesNotExist: raise serializers.ValidationError( - {"department": f"Department '{dept_name}' not found for this hospital."} + {"department_code": f"Department with code '{department_code}' not found for this hospital."} ) + else: + data["department"] = None section_name = data.get("section") if section_name: @@ -467,7 +472,7 @@ class ExternalComplaintSatisfactionSerializer(serializers.Serializer): class ExternalDoctorRatingCreateSerializer(serializers.Serializer): - hospital_id = serializers.IntegerField(required=True) + hospital = serializers.CharField(max_length=200, required=True) doctor_id = serializers.CharField(max_length=50, required=True) doctor_name = serializers.CharField(max_length=300, required=False, allow_blank=True, default="") rating = serializers.IntegerField(min_value=1, max_value=5, required=True) @@ -481,15 +486,15 @@ class ExternalDoctorRatingCreateSerializer(serializers.Serializer): allow_blank=True, default="", ) - department_name = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") + department_code = serializers.CharField(max_length=200, required=False, allow_blank=True, default="") admit_date = serializers.DateField(required=False, allow_null=True, default=None) discharge_date = serializers.DateField(required=False, allow_null=True, default=None) - def validate_hospital_id(self, value): + def validate_hospital(self, value): from apps.organizations.models import Hospital try: - return Hospital.objects.get(id=value) + return Hospital.objects.get(name__iexact=value.strip()) except Hospital.DoesNotExist: raise serializers.ValidationError("Hospital not found.") @@ -497,21 +502,105 @@ class ExternalDoctorRatingCreateSerializer(serializers.Serializer): return value.strip() def validate(self, data): - from apps.organizations.models import Staff + from apps.organizations.models import Department, Staff + from apps.physicians.adapter import DoctorRatingAdapter - hospital = data.get("hospital_id") + hospital = data.get("hospital") doctor_id = data.get("doctor_id", "").strip() - if hospital and doctor_id: - staff = Staff.objects.filter( - hospital=hospital, employee_id=doctor_id + # Resolve department code → human-readable name (the rating model stores + # a text department_name; HIS sends a stable department_code instead). + department_code = data.get("department_code", "").strip() + data["department_name"] = "" + dept = None + if department_code and hospital: + dept = Department.objects.filter( + code__iexact=department_code, hospital=hospital, status="active" ).first() - if not staff: + if not dept: raise serializers.ValidationError( - { - "doctor_id": f"Doctor with employee ID '{doctor_id}' not found at this hospital." - } + {"department_code": f"Department with code '{department_code}' not found for this hospital."} ) + data["department_name"] = dept.name_en or dept.name + + if hospital and doctor_id: + doctor_name = (data.get("doctor_name") or "").strip() + + # 1. Canonical multi-strategy lookup (employee_id → license_number → name). + staff = DoctorRatingAdapter.find_staff_by_doctor_id( + doctor_id=doctor_id, hospital=hospital, doctor_name=doctor_name + ) + + # 2. Auto-create the Staff if not found and we have a name to populate + # the required first_name/last_name fields. + if not staff: + if not doctor_name: + raise serializers.ValidationError( + { + "doctor_name": ( + "Staff not found for this doctor_id; provide doctor_name " + "to auto-create the staff record." + ) + } + ) + name_parts = doctor_name.split() + first_name = name_parts[0] + last_name = name_parts[-1] if len(name_parts) > 1 else name_parts[0] + staff, created = Staff.objects.get_or_create( + employee_id=doctor_id, + defaults={ + "hospital": hospital, + "first_name": first_name, + "last_name": last_name, + "name": doctor_name, + "staff_type": Staff.StaffType.PHYSICIAN, + "job_title": "Physician", + "physician": True, + "status": "active", + "department": dept, + }, + ) + data["_staff_created"] = created + data["_staff"] = staff return data + + +class ExternalDoctorRatingRetrieveSerializer(serializers.ModelSerializer): + """Read-only serializer for retrieving a doctor rating.""" + + hospital_name = serializers.CharField(source="hospital.name", read_only=True) + staff_id = serializers.CharField(read_only=True, default=None) + doctor_name_display = serializers.SerializerMethodField() + + class Meta: + model = PhysicianIndividualRating + fields = [ + "id", + "staff_id", + "hospital_name", + "source", + "source_reference", + "doctor_id", + "doctor_name", + "doctor_name_raw", + "doctor_name_display", + "department_name", + "patient_uhid", + "patient_name", + "patient_type", + "admit_date", + "discharge_date", + "rating", + "feedback", + "rating_date", + "is_aggregated", + "aggregated_at", + "created_at", + "updated_at", + ] + read_only_fields = fields + + def get_doctor_name_display(self, obj): + return obj.doctor_name or obj.doctor_name_raw or "" diff --git a/apps/integrations/api_views.py b/apps/integrations/api_views.py index 472db84..69c4a58 100644 --- a/apps/integrations/api_views.py +++ b/apps/integrations/api_views.py @@ -1,5 +1,5 @@ """ -External API views for complaints, inquiries, observations, appreciations, and suggestions. +External API views for complaints, inquiries, observations, appreciations, suggestions, and doctor ratings. All endpoints require X-API-Key header authentication. """ @@ -24,6 +24,7 @@ from .api_serializers import ( ExternalComplaintRetrieveSerializer, ExternalComplaintSatisfactionSerializer, ExternalDoctorRatingCreateSerializer, + ExternalDoctorRatingRetrieveSerializer, ExternalInquiryCreateSerializer, ExternalInquiryRetrieveSerializer, ExternalObservationCreateSerializer, @@ -161,8 +162,8 @@ class ExternalComplaintCreateView(ExternalAPIBase, APIView): patient=None, hospital=data["hospital"], department=data.get("department"), - section=data.get("section"), - area=data.get("area"), + section=data.get("section") or None, + area=data.get("area") or None, location_type=data.get("location_type", ""), title=data["title"], description=data["description"], @@ -355,9 +356,9 @@ class ExternalInquiryCreateView(ExternalAPIBase, APIView): inquiry = Inquiry.objects.create( patient=None, hospital=data["hospital"], - department=data.get("department"), - section=data.get("section"), - area=data.get("area"), + department=data.get("department") or None, + section=data.get("section") or None, + area=data.get("area") or None, location_type=data.get("location_type", ""), subject=data["subject"], message=data["message"], @@ -955,9 +956,9 @@ class ExternalDepartmentsListView(ExternalAPIBase, APIView): class ExternalSectionsListView(ExternalAPIBase, APIView): """ - GET /api/v1/external/sections/?hospital=&department= + GET /api/v1/external/sections/?hospital=&department_code= - List sections for a department. + List sections for a department (identified by its stable HIS department code). """ entity_name = "lookup" @@ -966,11 +967,11 @@ class ExternalSectionsListView(ExternalAPIBase, APIView): from apps.organizations.models import Department, Hospital, Section hospital_name = request.query_params.get("hospital", "").strip() - department_name = request.query_params.get("department", "").strip() + department_code = request.query_params.get("department_code", "").strip() - if not hospital_name or not department_name: + if not hospital_name or not department_code: return Response( - {"detail": "hospital and department query parameters are required."}, + {"detail": "hospital and department_code query parameters are required."}, status=status.HTTP_400_BAD_REQUEST, ) @@ -981,10 +982,13 @@ class ExternalSectionsListView(ExternalAPIBase, APIView): try: department = Department.objects.get( - name__iexact=department_name, hospital=hospital, status="active" + code__iexact=department_code, hospital=hospital, status="active" ) except Department.DoesNotExist: - return Response({"detail": "Department not found."}, status=status.HTTP_404_NOT_FOUND) + return Response( + {"detail": f"Department with code '{department_code}' not found for this hospital."}, + status=status.HTTP_404_NOT_FOUND, + ) qs = Section.objects.filter(department=department, status="active").order_by("name_en") @@ -1023,8 +1027,9 @@ class ExternalDoctorRatingCreateView(ExternalAPIBase, APIView): ser.is_valid(raise_exception=True) data = ser.validated_data - hospital = data["hospital_id"] + hospital = data["hospital"] staff = data.pop("_staff") + staff_created = data.pop("_staff_created", False) api_key = getattr(request, "api_key", None) if api_key and api_key.hospital and hospital != api_key.hospital: @@ -1068,6 +1073,23 @@ class ExternalDoctorRatingCreateView(ExternalAPIBase, APIView): staff.physician = True staff.save(update_fields=["physician"]) + if staff_created: + AuditService.log_event( + event_type="external_staff_auto_created", + description=( + f"Staff auto-created via external API: {staff.get_full_name()} " + f"(employee_id={staff.employee_id})" + ), + content_object=staff, + metadata={ + "staff_id": str(staff.id), + "employee_id": staff.employee_id, + "doctor_id": data["doctor_id"], + "hospital_id": str(hospital.id), + "source": "external_api", + }, + ) + AuditService.log_event( event_type="external_doctor_rating_created", description=f"Doctor rating created via external API: {staff.get_full_name()} - {data['rating']}/5", @@ -1085,9 +1107,98 @@ class ExternalDoctorRatingCreateView(ExternalAPIBase, APIView): { "success": True, "rating_id": str(rating.id), - "staff_id": str(staff.id), - "doctor_name": staff.get_full_name(), "rating": data["rating"], }, status=status.HTTP_201_CREATED, ) + + +class ExternalDoctorRatingRetrieveView(ExternalAPIBase, APIView): + """ + GET /api/v1/external/doctor-ratings/list/ + GET /api/v1/external/doctor-ratings// + + List doctor ratings (scoped to the API key's hospital) or retrieve a + single rating by its UUID. + + List filters (all optional, in addition to hospital scoping): + - doctor_id HIS employee ID (exact match) + - rating Integer 1-5 (exact match) + - created_from/created_to Created-at date range + - updated_from/updated_to Updated-at date range + - rating_from/rating_to Rating-date range (`rating_date` column) + - page, page_size Pagination (default 20, max 100) + """ + + entity_name = "doctor_ratings" + + def get(self, request, rating_id=None): + access_err = self.check_entity_access() + if access_err: + return access_err + + if rating_id: + return self._retrieve(request, rating_id) + return self._list(request) + + def _retrieve(self, request, rating_id): + from apps.physicians.models import PhysicianIndividualRating + + qs = PhysicianIndividualRating.objects.filter(id=rating_id) + qs = self.filter_queryset_by_hospital(qs) + rating = qs.first() + if not rating: + return Response({"detail": "Doctor rating not found."}, status=status.HTTP_404_NOT_FOUND) + + ser = ExternalDoctorRatingRetrieveSerializer(rating) + return Response(ser.data) + + def _list(self, request): + from apps.physicians.models import PhysicianIndividualRating + + qs = PhysicianIndividualRating.objects.all() + qs = self.filter_queryset_by_hospital(qs) + qs = self.apply_list_filters(qs, request) + + # Doctor-rating-specific filters + doctor_id = request.query_params.get("doctor_id", "").strip() + if doctor_id: + qs = qs.filter(doctor_id=doctor_id) + + rating_value = request.query_params.get("rating", "").strip() + if rating_value: + try: + qs = qs.filter(rating=int(rating_value)) + except ValueError: + pass + + rating_from = request.query_params.get("rating_from", "").strip() + if rating_from: + try: + qs = qs.filter(rating_date__gte=self._parse_date(rating_from, start=True)) + except (ValueError, TypeError): + pass + + rating_to = request.query_params.get("rating_to", "").strip() + if rating_to: + try: + qs = qs.filter(rating_date__lte=self._parse_date(rating_to, start=False)) + except (ValueError, TypeError): + pass + + page_size = min(int(request.query_params.get("page_size", 20)), 100) + page = int(request.query_params.get("page", 1)) + offset = (page - 1) * page_size + total = qs.count() + + items = qs[offset : offset + page_size] + ser = ExternalDoctorRatingRetrieveSerializer(items, many=True) + + return Response( + { + "count": total, + "page": page, + "page_size": page_size, + "results": ser.data, + } + ) diff --git a/apps/integrations/migrations/0004_alter_externalapikey_allowed_entities_help_text.py b/apps/integrations/migrations/0004_alter_externalapikey_allowed_entities_help_text.py new file mode 100644 index 0000000..1e52f2a --- /dev/null +++ b/apps/integrations/migrations/0004_alter_externalapikey_allowed_entities_help_text.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-07-19 08:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0003_external_api_key'), + ] + + operations = [ + migrations.AlterField( + model_name='externalapikey', + name='allowed_entities', + field=models.JSONField(blank=True, default=list, help_text='List of entities this key can access: ["complaints","inquiries","observations","appreciations","suggestions","doctor_ratings"]'), + ), + ] diff --git a/apps/integrations/models.py b/apps/integrations/models.py index 87386ac..eea1b0b 100644 --- a/apps/integrations/models.py +++ b/apps/integrations/models.py @@ -59,7 +59,7 @@ class ExternalAPIKey(UUIDModel, TimeStampedModel): allowed_entities = models.JSONField( default=list, blank=True, - help_text='List of entities this key can access: ["complaints","inquiries","observations","appreciations","suggestions"]', + help_text='List of entities this key can access: ["complaints","inquiries","observations","appreciations","suggestions","doctor_ratings"]', ) # Rate limiting diff --git a/apps/integrations/urls_external.py b/apps/integrations/urls_external.py index f192f01..d30a9c3 100644 --- a/apps/integrations/urls_external.py +++ b/apps/integrations/urls_external.py @@ -14,6 +14,7 @@ from .api_views import ( ExternalComplaintSatisfactionView, ExternalDepartmentsListView, ExternalDoctorRatingCreateView, + ExternalDoctorRatingRetrieveView, ExternalHospitalsListView, ExternalInquiryCreateView, ExternalInquiryRetrieveView, @@ -145,4 +146,14 @@ urlpatterns = [ ExternalDoctorRatingCreateView.as_view(), name="external-doctor-rating-create", ), + path( + "doctor-ratings/list/", + ExternalDoctorRatingRetrieveView.as_view(), + name="external-doctor-rating-list", + ), + path( + "doctor-ratings//", + ExternalDoctorRatingRetrieveView.as_view(), + name="external-doctor-rating-detail", + ), ] diff --git a/apps/journeys/ui_views.py b/apps/journeys/ui_views.py index 3be33cf..dcfd582 100644 --- a/apps/journeys/ui_views.py +++ b/apps/journeys/ui_views.py @@ -48,12 +48,22 @@ def journey_instance_list(request): # Apply RBAC filters user = request.user + selected_hospital = getattr(request, "tenant_hospital", None) + if user.is_px_admin(): - pass # See all + if selected_hospital: + queryset = queryset.filter(hospital=selected_hospital) elif user.is_hospital_admin() and user.hospital: queryset = queryset.filter(hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) elif user.is_department_manager() and user.department: queryset = queryset.filter(department=user.department) + elif user.is_champion() and user.department: + queryset = queryset.filter(department=user.department) + elif user.is_source_user(): + queryset = queryset.filter(created_by=user) elif user.hospital: queryset = queryset.filter(hospital=user.hospital) else: @@ -185,8 +195,22 @@ def journey_template_list(request): # Apply RBAC filters user = request.user + selected_hospital = getattr(request, "tenant_hospital", None) + if user.is_px_admin(): - pass # See all + if selected_hospital: + queryset = queryset.filter(hospital=selected_hospital) + elif user.is_hospital_admin() and user.hospital: + queryset = queryset.filter(hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(hospital=user.hospital) + elif user.is_department_manager() and user.department: + queryset = queryset.filter(department=user.department) + elif user.is_champion() and user.department: + queryset = queryset.filter(department=user.department) + elif user.is_source_user(): + queryset = queryset.filter(created_by=user) elif user.hospital: queryset = queryset.filter(hospital=user.hospital) else: diff --git a/apps/notifications/services.py b/apps/notifications/services.py index b09c593..6df44e3 100644 --- a/apps/notifications/services.py +++ b/apps/notifications/services.py @@ -23,17 +23,8 @@ def get_email_header_html(): logo_url = getattr(settings, "EMAIL_LOGO_URL", f"{settings.STATIC_URL}img/HH_P_V_Logo(hospital)_.png") if not logo_url.startswith("http"): logo_url = f"https://{settings.ALLOWED_HOSTS[0]}{logo_url}" if getattr(settings, "ALLOWED_HOSTS", []) else logo_url - return f"""
- - - - - -
- Al Hammadi - - مستشفى الحمادي -
+ return f"""
+ Al Hammadi
 
""" @@ -295,6 +286,8 @@ class NotificationService: metadata=None, notification_type="system", user=None, + attachments=None, + cc=None, ): """ Send Email notification and create corresponding in-app notification. @@ -308,6 +301,14 @@ class NotificationService: metadata: Additional metadata dict (optional) notification_type: Type of notification (default: 'system') user: User instance to create in-app notification for (optional, will try to lookup by email) + attachments: Optional list of (filename, content_bytes, mimetype) tuples. + Only applied on the standard Django email backend path; silently + skipped on the external Email API path (which would need its own + upload mechanism). + cc: Optional list of email addresses to CC. Only applied on the + standard Django email backend path; silently skipped on the + external Email API path. In-app notifications are created for + each CC'd user whose email maps to a User account. Returns: NotificationLog instance @@ -326,16 +327,34 @@ class NotificationService: from apps.accounts.models import User + # Normalise CC list to a clean list of strings (drop None/empty) + cc_list = [addr for addr in (cc or []) if addr] + + # Enrich metadata with CC trace + log_metadata = dict(metadata or {}) + if cc_list: + log_metadata = {**log_metadata, "cc": cc_list} + # Check if Email API is enabled and use it (simulator or external API) email_api_config = settings.EXTERNAL_NOTIFICATION_API.get("email", {}) if email_api_config.get("enabled", False): + skipped = [] + if attachments: + skipped.append(f"{len(attachments)} attachment(s)") + if cc_list: + skipped.append(f"{len(cc_list)} CC recipient(s)") + if skipped: + logger.warning( + f"Email API path does not support {' or '.join(skipped)}; " + f"sending email to {email} without them." + ) log = NotificationService.send_email_via_api( message=message, email=email, subject=subject, html_message=html_message, related_object=related_object, - metadata=metadata, + metadata=log_metadata, ) # Create in-app notification after sending via API NotificationService._create_in_app_notification_from_email( @@ -351,31 +370,61 @@ class NotificationService: message=message, content_object=related_object, provider="console", # TODO: Replace with actual provider - metadata=metadata or {}, + metadata=log_metadata, ) - # Create in-app notification + # Create in-app notification for the primary (To:) recipient NotificationService._create_in_app_notification_from_email( log, email, subject, message, notification_type, related_object, user ) + # Create in-app notifications for each CC'd user (silently skips emails + # that don't map to a User account). + for cc_addr in cc_list: + if cc_addr.lower() == email.lower(): + continue # Already covered by the To: notification + NotificationService._create_in_app_notification_from_email( + log, cc_addr, subject, message, notification_type, related_object, None + ) + # Check if Email is enabled email_config = settings.NOTIFICATION_CHANNELS.get("email", {}) if not email_config.get("enabled", True): - logger.info(f"[Email Console] To: {email} | Subject: {subject} | Message: {message}") + cc_summary = f" | CC: {', '.join(cc_list)}" if cc_list else "" + logger.info(f"[Email Console] To: {email}{cc_summary} | Subject: {subject} | Message: {message}") log.mark_sent() return log # Send email using Django's email backend try: - send_mail( - subject=subject, - message=message, - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=[email], - html_message=html_message, - fail_silently=False, - ) + if attachments or cc_list: + from django.core.mail import EmailMultiAlternatives + + msg = EmailMultiAlternatives( + subject=subject, + body=message, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[email], + cc=cc_list if cc_list else None, + ) + if html_message: + msg.attach_alternative(html_message, "text/html") + for att in (attachments or []): + # att is (filename, content_bytes, mimetype) + try: + msg.attach(*att) + except Exception: + logger.exception(f"Failed to attach {att[0] if att else '?'} to email for {email}") + msg.send(fail_silently=False) + else: + send_mail( + subject=subject, + message=message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[email], + html_message=html_message, + fail_silently=False, + ) log.mark_sent() logger.info(f"Email sent to {email}: {subject}") except Exception as e: diff --git a/apps/observations/forms.py b/apps/observations/forms.py index 9183c63..ddbf90d 100644 --- a/apps/observations/forms.py +++ b/apps/observations/forms.py @@ -8,6 +8,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.accounts.models import User +from apps.core.validators import SAUDI_PHONE_HTML_PATTERN, validate_saudi_phone from apps.organizations.models import Area, Department, Hospital, LocationType from apps.organizations.models import Section as OrgSection @@ -77,6 +78,7 @@ class ObservationPublicForm(forms.ModelForm): "reporter_name", "reporter_phone", "reporter_email", + "national_id", ] widgets = { "location_type": forms.Select( @@ -139,6 +141,12 @@ class ObservationPublicForm(forms.ModelForm): "placeholder": "Your email (optional)", } ), + "national_id": forms.TextInput( + attrs={ + "class": "form-control", + "placeholder": "National ID/Iqama (optional)", + } + ), } def __init__(self, *args, **kwargs): @@ -198,11 +206,15 @@ class ObservationInternalForm(forms.ModelForm): "location_type", "area", "description", + "title", + "location_text", "section", "incident_datetime", - "patient_file_number", + "patient_name", + "patient_national_id", + "patient_phone", + "patient_email", "assigned_department", - "assigned_to", "px_source", ] widgets = { @@ -225,16 +237,46 @@ class ObservationInternalForm(forms.ModelForm): "placeholder": "Please describe what you observed in detail...", } ), + "title": forms.TextInput( + attrs={ + "class": "form-control", + "placeholder": "Brief title (optional)", + } + ), + "location_text": forms.TextInput( + attrs={ + "class": "form-control", + "placeholder": "Specific location details (e.g. room number, floor)", + } + ), "incident_datetime": forms.DateTimeInput( attrs={ "class": "form-control", "type": "datetime-local", } ), - "patient_file_number": forms.TextInput( + "patient_name": forms.TextInput( attrs={ "class": "form-control", - "placeholder": "Patient MRN / file number", + "placeholder": "Patient name (optional)", + } + ), + "patient_national_id": forms.TextInput( + attrs={ + "class": "form-control", + "placeholder": "National ID/Iqama (optional)", + } + ), + "patient_phone": forms.TextInput( + attrs={ + "class": "form-control", + "placeholder": "Patient phone (optional)", + } + ), + "patient_email": forms.EmailInput( + attrs={ + "class": "form-control", + "placeholder": "Patient email (optional)", } ), "assigned_department": forms.Select( @@ -242,11 +284,6 @@ class ObservationInternalForm(forms.ModelForm): "class": "form-select", } ), - "assigned_to": forms.Select( - attrs={ - "class": "form-select", - } - ), } section = forms.ModelChoiceField( @@ -311,15 +348,6 @@ class ObservationInternalForm(forms.ModelForm): self.fields["assigned_department"].queryset = Department.objects.filter(status="active").order_by("name_en") self.fields["assigned_department"].empty_label = "Select department (optional)" - if hospital: - from apps.core.utils import get_assignable_users - self.fields["assigned_to"].queryset = get_assignable_users(hospital) - else: - self.fields["assigned_to"].queryset = User.objects.filter(is_active=True).order_by( - "first_name", "last_name" - ) - self.fields["assigned_to"].empty_label = "Select assignee (optional)" - def clean_description(self): description = self.cleaned_data.get("description", "") if len(description.strip()) < 10: @@ -725,7 +753,13 @@ class PublicObservationForm(forms.ModelForm): label=_("Mobile Number"), max_length=20, required=True, - widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your mobile number")}), + validators=[validate_saudi_phone], + widget=forms.TextInput(attrs={ + "class": "form-control", + "placeholder": _("05XXXXXXXX or +9665XXXXXXXX"), + "pattern": SAUDI_PHONE_HTML_PATTERN, + "inputmode": "tel", + }), ) reporter_email = forms.EmailField( label=_("Email Address"), diff --git a/apps/observations/migrations/0021_alter_observation_reporter_phone.py b/apps/observations/migrations/0021_alter_observation_reporter_phone.py new file mode 100644 index 0000000..715af0b --- /dev/null +++ b/apps/observations/migrations/0021_alter_observation_reporter_phone.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-07-18 14:46 + +import apps.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0020_remove_observation_dept_response_acceptance_notes_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='observation', + name='reporter_phone', + field=models.CharField(blank=True, help_text='Optional phone number for follow-up', max_length=20, validators=[apps.core.validators.validate_saudi_phone]), + ), + ] diff --git a/apps/observations/migrations/0022_observation_add_national_id.py b/apps/observations/migrations/0022_observation_add_national_id.py new file mode 100644 index 0000000..ba0480d --- /dev/null +++ b/apps/observations/migrations/0022_observation_add_national_id.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-07-19 06:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0021_alter_observation_reporter_phone'), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='national_id', + field=models.CharField(blank=True, help_text='Optional National ID/Iqama of the reporter', max_length=20), + ), + ] diff --git a/apps/observations/migrations/0023_add_patient_info_fields.py b/apps/observations/migrations/0023_add_patient_info_fields.py new file mode 100644 index 0000000..bbefb03 --- /dev/null +++ b/apps/observations/migrations/0023_add_patient_info_fields.py @@ -0,0 +1,33 @@ +# Generated by Django 6.0.1 on 2026-07-19 08:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('observations', '0022_observation_add_national_id'), + ] + + operations = [ + migrations.AddField( + model_name='observation', + name='patient_email', + field=models.EmailField(blank=True, help_text='Email of the patient', max_length=254), + ), + migrations.AddField( + model_name='observation', + name='patient_name', + field=models.CharField(blank=True, help_text='Name of the patient involved', max_length=200), + ), + migrations.AddField( + model_name='observation', + name='patient_national_id', + field=models.CharField(blank=True, help_text='National ID/Iqama of the patient', max_length=20), + ), + migrations.AddField( + model_name='observation', + name='patient_phone', + field=models.CharField(blank=True, help_text='Phone number of the patient', max_length=20), + ), + ] diff --git a/apps/observations/models.py b/apps/observations/models.py index ff5e8cc..a67e2fd 100644 --- a/apps/observations/models.py +++ b/apps/observations/models.py @@ -20,6 +20,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from apps.core.models import SoftDeleteModel, TimeStampedModel, UUIDModel +from apps.core.validators import validate_saudi_phone from apps.organizations.models import LocationType @@ -363,13 +364,18 @@ class Observation(UUIDModel, TimeStampedModel, SoftDeleteModel): # Optional reporter information (anonymous supported) reporter_staff_id = models.CharField(max_length=50, blank=True, help_text="Optional staff ID of the reporter") reporter_name = models.CharField(max_length=200, blank=True, help_text="Optional name of the reporter") - reporter_phone = models.CharField(max_length=20, blank=True, help_text="Optional phone number for follow-up") + reporter_phone = models.CharField(max_length=20, blank=True, validators=[validate_saudi_phone], help_text="Optional phone number for follow-up") reporter_email = models.EmailField(blank=True, help_text="Optional email for follow-up") + national_id = models.CharField(max_length=20, blank=True, help_text="Optional National ID/Iqama of the reporter") # Patient / subject information patient_file_number = models.CharField( max_length=100, blank=True, help_text="Medical record number / file number of the patient" ) + patient_name = models.CharField(max_length=200, blank=True, help_text="Name of the patient involved") + patient_national_id = models.CharField(max_length=20, blank=True, help_text="National ID/Iqama of the patient") + patient_phone = models.CharField(max_length=20, blank=True, help_text="Phone number of the patient") + patient_email = models.EmailField(blank=True, help_text="Email of the patient") # Status and workflow status = models.CharField( diff --git a/apps/observations/services.py b/apps/observations/services.py index d869239..f866d93 100644 --- a/apps/observations/services.py +++ b/apps/observations/services.py @@ -55,6 +55,7 @@ class ObservationService: reporter_name: str = "", reporter_phone: str = "", reporter_email: str = "", + national_id: str = "", client_ip: str = None, user_agent: str = "", attachments: list = None, @@ -99,6 +100,7 @@ class ObservationService: reporter_name=reporter_name, reporter_phone=reporter_phone, reporter_email=reporter_email, + national_id=national_id, client_ip=client_ip, user_agent=user_agent, hospital=hospital, diff --git a/apps/observations/tasks.py b/apps/observations/tasks.py index 20cf7e9..b5337bb 100644 --- a/apps/observations/tasks.py +++ b/apps/observations/tasks.py @@ -627,3 +627,39 @@ def send_observation_dept_response_reminders(): "first_reminder_count": first_reminder_count, "second_reminder_count": second_reminder_count, } + + +@shared_task +def send_observation_reminder_email(observation_id, reminder_type, recipients): + """Send a reminder email to department champion and manager for an observation.""" + import logging + + logger = logging.getLogger(__name__) + + try: + from apps.observations.models import Observation + from apps.notifications.services import NotificationService, get_email_header_html + + observation = Observation.objects.get(pk=observation_id) + + for recipient in recipients: + NotificationService.send_email( + email=recipient["email"], + subject=f"Reminder: Observation {observation.tracking_code} - Response Required", + message=f"This is a reminder that observation {observation.tracking_code} is awaiting your department's response. Please submit your response as soon as possible.", + html_message=f""" +
+ {get_email_header_html()} +
+

Reminder: Response Required

+

This is a reminder that observation {observation.tracking_code} is awaiting your department's response.

+

Please submit your response as soon as possible.

+
+
+""", + related_object=observation, + ) + + logger.info(f"Observation {observation_id} {reminder_type} reminder sent to {len(recipients)} recipients") + except Exception as e: + logger.error(f"Failed to send observation reminder email for {observation_id}: {e}") diff --git a/apps/observations/views.py b/apps/observations/views.py index 09033f0..770d3e1 100644 --- a/apps/observations/views.py +++ b/apps/observations/views.py @@ -195,6 +195,7 @@ def observation_create_public(request): reporter_name=form.cleaned_data.get("reporter_name", ""), reporter_phone=form.cleaned_data.get("reporter_phone", ""), reporter_email=form.cleaned_data.get("reporter_email", ""), + national_id=form.cleaned_data.get("national_id", ""), client_ip=client_ip, user_agent=user_agent, attachments=attachments, @@ -316,8 +317,8 @@ def observation_create(request): description=form.cleaned_data["description"], severity=form.cleaned_data.get("severity") or request.POST.get("severity", "medium"), category=None, - title="", - location_text="", + title=form.cleaned_data.get("title", "") or "", + location_text=form.cleaned_data.get("location_text", "") or "", incident_datetime=form.cleaned_data.get("incident_datetime"), reporter_staff_id=user.employee_id or "", reporter_name=user.get_full_name(), @@ -332,23 +333,28 @@ def observation_create(request): ) assigned_dept = form.cleaned_data.get("assigned_department") - assigned_user = form.cleaned_data.get("assigned_to") section = form.cleaned_data.get("section") if assigned_dept: observation.assigned_department = assigned_dept - if assigned_user: - observation.assigned_to = assigned_user - observation.status = ObservationStatus.IN_PROGRESS - ObservationStatusLog.objects.create( - observation=observation, - from_status=ObservationStatus.OPEN, - to_status=ObservationStatus.IN_PROGRESS, - comment="Auto-assigned during creation", - ) if section: observation.section = section + + observation.patient_name = form.cleaned_data.get("patient_name", "") or "" + observation.patient_national_id = form.cleaned_data.get("patient_national_id", "") or "" + observation.patient_phone = form.cleaned_data.get("patient_phone", "") or "" + observation.patient_email = form.cleaned_data.get("patient_email", "") or "" + + observation.assigned_to = user + observation.status = ObservationStatus.IN_PROGRESS observation.save() + ObservationStatusLog.objects.create( + observation=observation, + from_status=ObservationStatus.OPEN, + to_status=ObservationStatus.IN_PROGRESS, + comment="Auto-assigned during creation", + ) + comm_req_id = request.POST.get("comm_req") if comm_req_id: try: @@ -644,13 +650,13 @@ def observation_detail(request, pk): "status_form": status_form, "note_form": note_form, "status_choices": ObservationStatus.choices, - "can_triage": user.has_perm("observations.triage_observation") or user.is_px_admin(), + "can_triage": user.has_perm("observations.triage_observation") or user.is_px_admin() or user.is_px_employee(), "can_convert": user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee(), "can_send_to_department": user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() or user.is_department_manager() or user.is_px_management(), "can_respond_to_department": user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() or (user.is_champion() and observation.assigned_department == user.department), "can_send_reminder": user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee(), "can_delete": user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee(), - "can_admin": user.is_px_admin() or user.is_hospital_admin(), + "can_admin": user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee(), "linked_rcas": linked_rcas, } @@ -734,7 +740,7 @@ def observation_triage(request, pk): # Check permission user = request.user - if not (user.has_perm("observations.triage_observation") or user.is_px_admin()): + if not (user.has_perm("observations.triage_observation") or user.is_px_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to triage observations.") return redirect("observations:observation_detail", pk=pk) @@ -770,7 +776,7 @@ def observation_change_status(request, pk): # Check permission user = request.user - if not (user.has_perm("observations.triage_observation") or user.is_px_admin() or user.is_hospital_admin()): + if not (user.has_perm("observations.triage_observation") or user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to change observation status.") return redirect("observations:observation_detail", pk=pk) @@ -1619,6 +1625,8 @@ def observation_department_response(request, pk): if not ( user.is_px_admin() or user.is_hospital_admin() + or user.is_px_management() + or user.is_px_employee() or (user.is_champion() and observation.assigned_department == user.department) ): messages.error(request, "You don't have permission to respond to this observation.") @@ -1681,61 +1689,31 @@ Generate a JSON response with: is_internal=True, ) - try: - from apps.notifications.services import NotificationService, get_email_header_html - from apps.core.utils import build_public_track_url - - track_url = build_public_track_url("observation", observation.tracking_code) - - if observation.reporter_phone: - NotificationService.send_sms( - phone=observation.reporter_phone, - message=f"PX360: Your observation {observation.tracking_code} has been responded to. View details: {track_url}", - related_object=observation, - metadata={"notification_type": "observation_department_response"}, - ) - - if observation.reporter_email: - email_subject = f"PX360: Response to Your Observation {observation.tracking_code}" - email_body = ( - f"Dear Valued Reporter,\n\n" - f"Your observation {observation.tracking_code} has been responded to.\n\n" - f"To view the full response, please visit:\n{track_url}\n\n" - f"Thank you for your contribution.\n\n" - f"Tracking Code: {observation.tracking_code}\n" - f"This is an automated message from PX 360." - ) - NotificationService.send_email( - email=observation.reporter_email, - subject=email_subject, - message=email_body, - html_message=f""" -
- {get_email_header_html()} -
-

Response to Your Observation

-

Dear Valued Reporter,

-

Your observation {observation.tracking_code} has been responded to.

- -

Tracking Code: {observation.tracking_code}

-

Thank you for your contribution.

-
-
-""", - related_object=observation, - metadata={"notification_type": "observation_department_response_email"}, - ) - except Exception as e: - logger.warning(f"Failed to send observation response notification: {e}") - messages.success(request, "Department response submitted successfully.") + # Prefer a user-supplied relative next URL (e.g. back to the department + # observation detail page) over the default department landing page. + # Block protocol-relative URLs (``//host/`` or ``/\\host/``) to prevent + # open-redirect abuse. + next_url = request.POST.get("next") + if not ( + next_url + and next_url.startswith("/") + and not next_url.startswith("//") + and not next_url.startswith("/\\") + ): + next_url = None if request.headers.get('X-Requested-With') == 'XMLHttpRequest': from django.http import JsonResponse from django.urls import reverse - redirect_url = reverse("organizations:department_detail", kwargs={"pk": user.department.pk}) if user.department else reverse("observations:observation_detail", kwargs={"pk": pk}) + if next_url: + redirect_url = next_url + elif user.department: + redirect_url = reverse("organizations:department_detail", kwargs={"pk": user.department.pk}) + else: + redirect_url = reverse("observations:observation_detail", kwargs={"pk": pk}) return JsonResponse({"success": True, "redirect_url": redirect_url}) + if next_url: + return redirect(next_url) if user.department: return redirect("organizations:department_detail", pk=user.department.pk) return redirect("observations:observation_detail", pk=pk) @@ -1771,30 +1749,30 @@ def observation_send_dept_response_reminder(request, pk): reminder_type = request.POST.get("reminder_type", "first") + if reminder_type == "first" and observation.dept_response_reminder_sent_at: + messages.warning(request, "First reminder already sent.") + return redirect("observations:observation_detail", pk=pk) + if reminder_type == "second" and observation.dept_response_second_reminder_sent_at: + messages.warning(request, "Second reminder already sent.") + return redirect("observations:observation_detail", pk=pk) + if reminder_type == "second" and not observation.dept_response_reminder_sent_at: + messages.warning(request, "Please send the first reminder before the second.") + return redirect("observations:observation_detail", pk=pk) + try: from apps.notifications.services import NotificationService, get_email_header_html + from apps.organizations.department_contacts import get_champion_and_manager + + targets = get_champion_and_manager(dept) + if not targets: + messages.error(request, f"No champion or manager assigned to {dept.name}.") + return redirect("observations:observation_detail", pk=pk) recipients = [] - if dept.champion and dept.champion.user and dept.champion.user.email: - recipients.append(dept.champion.user) - - for recipient in recipients: - NotificationService.send_email( - email=recipient.email, - subject=f"Reminder: Observation {observation.tracking_code} - Response Required", - message=f"This is a reminder that observation {observation.tracking_code} is awaiting your department's response. Please submit your response as soon as possible.", - html_message=f""" -
- {get_email_header_html()} -
-

Reminder: Response Required

-

This is a reminder that observation {observation.tracking_code} is awaiting your department's response.

-

Please submit your response as soon as possible.

-
-
-""", - related_object=observation, - ) + for target in targets: + email = target.get("email") or "" + if email: + recipients.append({"email": email, "name": target.get("display_name", "")}) if reminder_type == "first": observation.dept_response_reminder_sent_at = timezone.now() @@ -1802,6 +1780,9 @@ def observation_send_dept_response_reminder(request, pk): observation.dept_response_second_reminder_sent_at = timezone.now() observation.save() + from apps.observations.tasks import send_observation_reminder_email + send_observation_reminder_email.delay(str(observation.id), reminder_type, recipients) + ObservationNote.objects.create( observation=observation, note=f"Manual {reminder_type} reminder sent by {user.get_full_name()}", @@ -1823,11 +1804,13 @@ def observation_send_dept_response_reminder(request, pk): @login_required -@permission_required("observations.manage_categories", raise_exception=True) def category_list(request): """ List observation categories. """ + if not (request.user.has_perm("observations.manage_categories") or request.user.is_px_admin() or request.user.is_px_employee()): + from django.core.exceptions import PermissionDenied + raise PermissionDenied categories = ObservationCategory.objects.all().order_by("sort_order", "name_en") context = { @@ -1838,12 +1821,14 @@ def category_list(request): @login_required -@permission_required("observations.manage_categories", raise_exception=True) @require_http_methods(["GET", "POST"]) def category_create(request): """ Create new observation category. """ + if not (request.user.has_perm("observations.manage_categories") or request.user.is_px_admin() or request.user.is_px_employee()): + from django.core.exceptions import PermissionDenied + raise PermissionDenied if request.method == "POST": form = ObservationCategoryForm(request.POST) if form.is_valid(): @@ -1862,12 +1847,14 @@ def category_create(request): @login_required -@permission_required("observations.manage_categories", raise_exception=True) @require_http_methods(["GET", "POST"]) def category_edit(request, pk): """ Edit observation category. """ + if not (request.user.has_perm("observations.manage_categories") or request.user.is_px_admin() or request.user.is_px_employee()): + from django.core.exceptions import PermissionDenied + raise PermissionDenied category = get_object_or_404(ObservationCategory, pk=pk) if request.method == "POST": @@ -1889,12 +1876,14 @@ def category_edit(request, pk): @login_required -@permission_required("observations.manage_categories", raise_exception=True) @require_http_methods(["POST"]) def category_delete(request, pk): """ Delete observation category. """ + if not (request.user.has_perm("observations.manage_categories") or request.user.is_px_admin() or request.user.is_px_employee()): + from django.core.exceptions import PermissionDenied + raise PermissionDenied category = get_object_or_404(ObservationCategory, pk=pk) # Check if category is in use diff --git a/apps/organizations/management/commands/export_sections.py b/apps/organizations/management/commands/export_sections.py new file mode 100644 index 0000000..7e22adf --- /dev/null +++ b/apps/organizations/management/commands/export_sections.py @@ -0,0 +1,59 @@ +""" +Management command to export sections (English name, Arabic name, department) to a CSV file. + +Usage: + python manage.py export_sections --output-dir ./exports +""" +import csv +import os + +from django.core.management.base import BaseCommand + +from apps.organizations.models import Section + + +class Command(BaseCommand): + help = 'Export sections (English name, Arabic name, department) to a CSV file' + + def add_arguments(self, parser): + parser.add_argument( + '--output-dir', + type=str, + default='./exports', + help='Directory to export the sections CSV file to', + ) + + def handle(self, *args, **options): + output_dir = options['output_dir'] + os.makedirs(output_dir, exist_ok=True) + + filepath = os.path.join(output_dir, 'sections.csv') + qs = ( + Section.objects.all() + .select_related('department') + .order_by('department__name_en', 'name_en') + ) + + seen = set() + rows = [] + for section in qs: + dept = section.department + row = ( + section.name_en or '', + section.name_ar or '', + dept.name_en if dept else '', + dept.name_ar if dept else '', + ) + if row in seen: + continue + seen.add(row) + rows.append(row) + + with open(filepath, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.writer(f) + writer.writerow(['Name (EN)', 'Name (AR)', 'Department (EN)', 'Department (AR)']) + writer.writerows(rows) + + self.stdout.write(self.style.SUCCESS( + f'Exported {len(rows)} unique sections to {filepath}' + )) diff --git a/apps/organizations/ui_views.py b/apps/organizations/ui_views.py index 6d07923..10b102e 100644 --- a/apps/organizations/ui_views.py +++ b/apps/organizations/ui_views.py @@ -1939,9 +1939,28 @@ def department_detail(request, pk): "involved_staff__staff" ).distinct().order_by("-created_at") - inquiries = Inquiry.objects.filter( - Q(department=department, sent_to_department=True) | Q(outgoing_department=department) - ).select_related("assigned_to", "patient").order_by("-created_at") + inquiry_direction = request.GET.get("inquiry_direction", "incoming") + sent_away = ( + Q(department=department) + & Q(transferred_to_department__isnull=False) + & ~Q(transferred_to_department=department) + ) + incoming_inquiries_qs = Inquiry.objects.filter( + Q(department=department, sent_to_department=True) + | Q(outgoing_department=department) + | Q(transferred_to_department=department) + ).exclude(sent_away) + outgoing_inquiries_qs = Inquiry.objects.filter(sent_away) + + if inquiry_direction == "outgoing": + inquiries = outgoing_inquiries_qs + else: + inquiries = incoming_inquiries_qs + inquiries = inquiries.select_related("assigned_to", "patient", "department", "outgoing_department", "transferred_to_department").order_by("-created_at") + + incoming_count = incoming_inquiries_qs.count() + outgoing_count = outgoing_inquiries_qs.count() + all_inquiries_count = incoming_count + outgoing_count observations = Observation.objects.filter( assigned_department=department, sent_to_department=True @@ -2166,18 +2185,17 @@ def department_detail(request, pk): pending_inquiries = Inquiry.objects.filter( Q(outgoing_department=department) | Q(department=department, sent_to_department=True), transferred_at__isnull=False, - department_responded_at__isnull=True, status__in=["open", "in_progress"], ).order_by("dept_response_sla_due_at") for inq in pending_inquiries: pending_actions.append({ "type": "inquiry_response", - "type_label": _("Inquiry Response"), + "type_label": _("Inquiry to Handle"), "reference": inq.reference_number or str(inq.id), "subject": inq.subject or inq.message[:80] if inq.message else "", "sla_due_at": inq.dept_response_sla_due_at, "is_overdue": inq.dept_response_sla_due_at and inq.dept_response_sla_due_at < dj_tz.now(), - "url": "/inquiries/{}/department-response/".format(inq.pk), + "url": "/organizations/departments/{}/inquiries/{}/".format(department.pk, inq.pk), "badge_color": "cyan", "item_id": str(inq.pk), "existing_en": inq.department_response_en or "", @@ -2241,6 +2259,10 @@ def department_detail(request, pk): "staff_complaint_counts": staff_complaint_counts, "complaints": complaints[:5], "inquiries": inquiries[:5], + "inquiry_direction": inquiry_direction, + "incoming_count": incoming_count, + "outgoing_count": outgoing_count, + "all_inquiries_count": all_inquiries_count, "observations": observations[:5], "appreciations": appreciations[:50], "suggestions": suggestions[:50], @@ -2423,7 +2445,7 @@ def department_complaints_list(request, pk): "complaint_dept_map": complaint_dept_map, "status_choices": Complaint.Status.choices if hasattr(Complaint, 'Status') else [], "list_stats": status_counts, - "status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed")}, + "status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed"), "contact_no_response": _("Contacted - No Response")}, } return render(request, "organizations/department_complaints.html", context) @@ -2442,9 +2464,26 @@ def department_inquiries_list(request, pk): messages.error(request, _("You don't have permission to view this department.")) return redirect("organizations:department_list") - queryset = Inquiry.objects.filter( - Q(department=department, sent_to_department=True) | Q(outgoing_department=department) - ).select_related("assigned_to", "patient").order_by("-created_at") + direction = request.GET.get("direction", "incoming") + + # Direction split (department POV): + # outgoing = this department transferred the inquiry away to another department + # incoming = inquiries this department currently handles (owned & not sent away, or transferred TO it) + sent_away = ( + Q(department=department) + & Q(transferred_to_department__isnull=False) + & ~Q(transferred_to_department=department) + ) + if direction == "outgoing": + queryset = Inquiry.objects.filter(sent_away) + else: + queryset = Inquiry.objects.filter( + Q(department=department, sent_to_department=True) + | Q(outgoing_department=department) + | Q(transferred_to_department=department) + ).exclude(sent_away) + + queryset = queryset.select_related("assigned_to", "patient").order_by("-created_at") from django.db.models import Count list_stats = queryset.values("status").annotate(count=Count("pk")) @@ -2495,7 +2534,8 @@ def department_inquiries_list(request, pk): "date_to": date_to or "", "can_respond": can_respond, "list_stats": status_counts, - "status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed")}, + "direction": direction, + "status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed"), "contact_no_response": _("Contacted - No Response")}, } return render(request, "organizations/department_inquiries.html", context) @@ -2731,12 +2771,8 @@ def department_inquiry_detail(request, pk, ipk): can_respond = ( user.is_px_admin() or user.is_hospital_admin() - or (user.is_department_manager() and user.department == department) - or ( - user.is_champion() - and user.department - and user.department in [inquiry.department, inquiry.outgoing_department] - ) + or (user.is_department_manager() and user.department_id == inquiry.handling_department_id) + or (user.is_champion() and user.department_id == inquiry.handling_department_id) ) location_parts = [] @@ -2750,12 +2786,21 @@ def department_inquiry_detail(request, pk, ipk): inquiry_ct = ContentType.objects.get_for_model(Inquiry) notes = inquiry.notes.select_related("created_by").order_by("-created_at") + departments = ( + Department.objects.filter(hospital=department.hospital, status="active") + .exclude(pk=department.pk) + .order_by("name_en") + ) + users = User.objects.filter(hospital=department.hospital, is_active=True) + context = { "department": department, "inquiry": inquiry, "location_str": " > ".join(location_parts) if location_parts else "", "can_respond": can_respond, "notes": notes, + "departments": departments, + "users": users, "content_type_id": inquiry_ct.pk, "object_id": inquiry.pk, } @@ -3084,6 +3129,17 @@ def department_record_inquiry(request, pk): "created_at": inquiry.created_at.strftime("%Y-%m-%d %H:%M"), "due_at": inquiry.due_at.strftime("%Y-%m-%d %H:%M") if hasattr(inquiry, 'due_at') and inquiry.due_at else None, "ai_summary": inquiry.short_description_en if hasattr(inquiry, 'short_description_en') else "", + "department_response_en": inquiry.department_response_en or "", + "department_response_ar": inquiry.department_response_ar or "", + "department_responded_at": inquiry.department_responded_at.strftime("%Y-%m-%d %H:%M") if inquiry.department_responded_at else None, + "priority_display": inquiry.get_priority_display() if hasattr(inquiry, 'priority') and inquiry.priority else "", + "contact_status_display": inquiry.get_contact_status_display() if inquiry.contact_status else "", + "response": inquiry.response or inquiry.response_en or "", + "responded_at": inquiry.responded_at.strftime("%Y-%m-%d %H:%M") if inquiry.responded_at else None, + "responded_by": inquiry.responded_by.get_full_name() if inquiry.responded_by else None, + "resolved_at": inquiry.resolved_at.strftime("%Y-%m-%d %H:%M") if inquiry.resolved_at else None, + "resolved_by": inquiry.resolved_by.get_full_name() if inquiry.resolved_by else None, + "outgoing_department": inquiry.outgoing_department.get_localized_name() if inquiry.outgoing_department else None, } return JsonResponse(data) diff --git a/apps/organizations/views.py b/apps/organizations/views.py index d58e546..15efad6 100644 --- a/apps/organizations/views.py +++ b/apps/organizations/views.py @@ -928,7 +928,7 @@ def api_department_staff(request, department_id): staff_qs = ( Staff.objects.select_related("user") - .filter(department=dept, hospital_id=dept.hospital_id, user__is_active=True) + .filter(department=dept, hospital_id=dept.hospital_id, status="active") .order_by("first_name", "last_name") ) diff --git a/apps/presentations/views.py b/apps/presentations/views.py index 9ce85bc..cd80e87 100644 --- a/apps/presentations/views.py +++ b/apps/presentations/views.py @@ -357,7 +357,33 @@ def presentation_generate(request): @login_required def template_list(request): + user = request.user + selected_hospital = getattr(request, "tenant_hospital", None) + templates = ReportTemplate.objects.select_related('created_by', 'hospital').prefetch_related('template_slides') + + # Apply RBAC filters (previously missing entirely — every user saw all hospitals' templates) + if user.is_px_admin(): + if selected_hospital: + templates = templates.filter(Q(hospital=selected_hospital) | Q(hospital__isnull=True)) + elif user.is_hospital_admin() and user.hospital: + templates = templates.filter(Q(hospital=user.hospital) | Q(hospital__isnull=True)) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + templates = templates.filter(Q(hospital=user.hospital) | Q(hospital__isnull=True)) + else: + templates = templates.filter(hospital__isnull=True) + elif user.is_department_manager() and user.department: + templates = templates.filter(Q(hospital=user.hospital) | Q(hospital__isnull=True)) + elif user.is_champion() and user.department: + templates = templates.filter(Q(hospital=user.hospital) | Q(hospital__isnull=True)) + elif user.is_source_user(): + templates = templates.filter(created_by=user) + elif user.hospital: + templates = templates.filter(Q(hospital=user.hospital) | Q(hospital__isnull=True)) + else: + templates = templates.filter(hospital__isnull=True) + context = { 'templates': templates, 'data_sources': REPORT_DATA_SOURCES, diff --git a/apps/projects/ui_views.py b/apps/projects/ui_views.py index 1e0e1d5..2c1958a 100644 --- a/apps/projects/ui_views.py +++ b/apps/projects/ui_views.py @@ -52,7 +52,7 @@ def _notify_project_team(project): if not recipient_ids: return - recipients = Staff.objects.filter(id__in=recipient_ids, user__isnull=False).select_related("user") + recipients = Staff.objects.filter(id__in=recipient_ids).select_related("user") subject = f"New QI Project: {project.name}" plain = ( @@ -71,7 +71,7 @@ def _notify_project_team(project):
""" for sp in recipients: - email = getattr(sp.user, "email", None) + email = sp.email or getattr(sp.user, "email", None) if not email: continue try: @@ -1165,7 +1165,12 @@ def template_list(request): queryset = QIProject.objects.filter(is_template=True).select_related("hospital").prefetch_related("departments") # Apply RBAC filters - if not user.is_px_admin(): + selected_hospital = getattr(request, "tenant_hospital", None) + if user.is_px_admin(): + if selected_hospital: + queryset = queryset.filter(Q(hospital=selected_hospital) | Q(hospital__isnull=True)) + else: + # hospital_admin path (only px_admin/hospital_admin reach this point — gated above) queryset = queryset.filter(Q(hospital=user.hospital) | Q(hospital__isnull=True)) # Search diff --git a/apps/surveys/his_views.py b/apps/surveys/his_views.py index 59b20e3..41a618f 100644 --- a/apps/surveys/his_views.py +++ b/apps/surveys/his_views.py @@ -513,10 +513,21 @@ def bulk_job_list(request): List all bulk survey jobs for the user. """ user = request.user + selected_hospital = getattr(request, "tenant_hospital", None) - # Filter jobs + # Filter jobs by RBAC if user.is_px_admin(): - jobs = BulkSurveyJob.objects.all() + if selected_hospital: + jobs = BulkSurveyJob.objects.filter(hospital=selected_hospital) + else: + jobs = BulkSurveyJob.objects.none() + elif user.is_hospital_admin() and user.hospital: + jobs = BulkSurveyJob.objects.filter(hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + jobs = BulkSurveyJob.objects.filter(hospital=user.hospital) + else: + jobs = BulkSurveyJob.objects.none() elif user.hospital: jobs = BulkSurveyJob.objects.filter(hospital=user.hospital) else: diff --git a/apps/surveys/ui_views.py b/apps/surveys/ui_views.py index 31a09bf..7f54b2e 100644 --- a/apps/surveys/ui_views.py +++ b/apps/surveys/ui_views.py @@ -599,10 +599,22 @@ def survey_comments_list(request): # Apply RBAC filters user = request.user + selected_hospital = getattr(request, "tenant_hospital", None) + if user.is_px_admin(): - pass # See all + if selected_hospital: + queryset = queryset.filter(survey_template__hospital=selected_hospital) elif user.is_hospital_admin() and user.hospital: queryset = queryset.filter(survey_template__hospital=user.hospital) + elif user.is_px_management() or user.is_px_employee(): + if user.hospital: + queryset = queryset.filter(survey_template__hospital=user.hospital) + elif user.is_department_manager() and user.department: + queryset = queryset.filter(survey_template__department=user.department) + elif user.is_champion() and user.department: + queryset = queryset.filter(survey_template__department=user.department) + elif user.is_source_user(): + queryset = queryset.filter(created_by=user) elif user.hospital: queryset = queryset.filter(survey_template__hospital=user.hospital) else: @@ -1817,8 +1829,19 @@ def survey_analytics_dashboard(request): messages.error(request, "You don't have permission to view analytics.") return redirect("surveys:instance_list") - # Get available templates - templates = SurveyTemplate.objects.filter(is_active=True).order_by("name") + # Get available templates (scoped to user's hospital; system-wide templates included) + selected_hospital = getattr(request, "tenant_hospital", None) + if user.is_px_admin(): + scope_hospital = selected_hospital + else: + scope_hospital = user.hospital + + templates = SurveyTemplate.objects.filter(is_active=True) + if scope_hospital: + templates = templates.filter(Q(hospital=scope_hospital) | Q(hospital__isnull=True)) + else: + templates = templates.filter(hospital__isnull=True) + templates = templates.order_by("name") # Get selected template from query param selected_template_id = request.GET.get("template") diff --git a/config/settings/base.py b/config/settings/base.py index d709d02..2f209bf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -441,7 +441,7 @@ DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL", default="noreply@px360.sa") SITE_URL = env("SITE_URL", default="http://localhost:8000") BASE_URL = SITE_URL SURVEY_BASE_URL = SITE_URL -EMAIL_LOGO_URL = env("EMAIL_LOGO_URL", default=f"{SITE_URL}{STATIC_URL}img/HH_P_H_Logo.png") +EMAIL_LOGO_URL = env("EMAIL_LOGO_URL", default=f"{SITE_URL}{STATIC_URL}images/logos/HH_P_H_Logo_CMYK---LANDSCIP1.png") # HIS Integration Settings HIS_API_URL = env("HIS_API_URL", default="https://his.alhammadi.med.sa/SSRCE/API/FetchPatientVisitTimeStamps") diff --git a/config/settings/dev.py b/config/settings/dev.py index f8c5c8c..5532131 100644 --- a/config/settings/dev.py +++ b/config/settings/dev.py @@ -6,7 +6,7 @@ from .base import * # noqa DEBUG = True -ALLOWED_HOSTS = ["192.168.8.13","localhost", "127.0.0.1", "0.0.0.0", ".ngrok-free.app", "micha-nonparabolic-lovie.ngrok-free.dev"] +ALLOWED_HOSTS = ["192.168.8.13","localhost", "127.0.0.1", "0.0.0.0"] # Database - Use PostgreSQL even in dev for consistency # Override with SQLite if needed for quick local testing @@ -17,11 +17,7 @@ ALLOWED_HOSTS = ["192.168.8.13","localhost", "127.0.0.1", "0.0.0.0", ".ngrok-fre # ) # } -CSRF_TRUSTED_ORIGINS = [ - "https://*.ngrok-free.app", - "https://*.ngrok.app", - "https://micha-nonparabolic-lovie.ngrok-free.dev", -] +CSRF_TRUSTED_ORIGINS = [] # Database inherits from base.py (PostgreSQL) # To temporarily switch back to SQLite for testing, uncomment: diff --git a/deploy/compose.dev-home.yml b/deploy/compose.dev-home.yml new file mode 100644 index 0000000..e44c869 --- /dev/null +++ b/deploy/compose.dev-home.yml @@ -0,0 +1,78 @@ +# PX360 — DEV compose for `target deploy` (bundled postgres + redis) +# +# target injects into the `web_app` service on deploy: +# - container_name: -web_app (so the shared Caddy can route to it) +# - networks: [target-proxy, default] +# - removes any `ports:` (the shared Caddy handles ingress) +# +# The web service MUST be named `web_app` — target's Caddy injection looks for it. +# `{{ .ImageTag }}` is replaced by target with the freshly built image tag. +# +# env file (in the deploy dir ~/.target/hh-dev/): +# .env — ALL config + secrets, shipped by `target deploy` (deploy/env.dev) +services: + web_app: + image: {{ .ImageTag }} + restart: unless-stopped + env_file: + - .env + volumes: + - media_volume:/app/media + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + ports: + - "8888:8000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + celery: + image: {{ .ImageTag }} + restart: unless-stopped + command: celery -A config worker -l info --concurrency=2 + env_file: + - .env + + celery-beat: + image: {{ .ImageTag }} + restart: unless-stopped + command: celery -A config beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler + env_file: + - .env + + db: + image: postgres:15-alpine + restart: unless-stopped + ports: + - "5432:5432" + env_file: + - .env + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-px360}"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: unless-stopped + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + pg_data: + redis_data: + media_volume: diff --git a/deploy/compose.dev.yml b/deploy/compose.dev.yml index e44c869..83b463f 100644 --- a/deploy/compose.dev.yml +++ b/deploy/compose.dev.yml @@ -24,7 +24,7 @@ services: redis: condition: service_healthy ports: - - "8888:8000" + - "8000:8000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health/"] interval: 30s diff --git a/deploy/target.dev.yaml b/deploy/target.dev.yaml index 7dd6205..a03a930 100644 --- a/deploy/target.dev.yaml +++ b/deploy/target.dev.yaml @@ -2,12 +2,12 @@ # Usage: `make deploy-dev` (runs: target deploy --config deploy/target.dev.yaml) app: name: "hh-dev" # isolated project dir: ~/.target/hh-dev/ - domain: "px360.ismailmosaibrahim.com" # shared Caddy routes this domain → web_app - port: 8888 + domain: "px360.hh.med.sa" # shared Caddy routes this domain → web_app + port: 8000 server: - ip: "192.168.8.73" - user: "ismail" + ip: "10.132.16.5" + user: "ahh-ops" ssh_key: "~/.ssh/id_rsa" build: @@ -15,7 +15,7 @@ build: context: "." # repo root (where the Dockerfile lives) deploy: - use_caddy: false # shared target-caddy handles HTTPS + use_caddy: false # shared target-caddy handles HTTPS caddy_email: "ismail.mosa.ibrahim@gmail.com" compose_file: "deploy/compose.dev.yml" # user-provided compose (uses {{ .ImageTag }}) env_file: "deploy/env.dev" # shipped to ~/.target/hh-dev/.env each deploy diff --git a/docs/API_ENDPOINTS.md b/docs/API_ENDPOINTS.md index d6c718c..6b64bfb 100644 --- a/docs/API_ENDPOINTS.md +++ b/docs/API_ENDPOINTS.md @@ -705,3 +705,4 @@ For interactive API documentation with try-it-out functionality: --- **Last Updated:** December 14, 2025 + diff --git a/docs/PX360_External_API.postman_collection.json b/docs/PX360_External_API.postman_collection.json index 26194de..f250233 100644 --- a/docs/PX360_External_API.postman_collection.json +++ b/docs/PX360_External_API.postman_collection.json @@ -744,7 +744,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"hospital_id\": 1,\n \"doctor_id\": \"10738\",\n \"doctor_name\": \"Dr. Omaymah Yaqoub\",\n \"rating\": 4,\n \"feedback\": \"Great doctor, very caring and attentive.\",\n \"rating_date\": \"2026-06-10\",\n \"patient_uhid\": \"UHID12345\",\n \"patient_name\": \"Ahmed Ali\",\n \"patient_type\": \"OP\",\n \"department_name\": \"Internal Medicine\"\n}" + "raw": "{\n \"hospital\": \"Al Nuzha\",\n \"doctor_id\": \"10738\",\n \"doctor_name\": \"Dr. Omaymah Yaqoub\",\n \"rating\": 4,\n \"feedback\": \"Great doctor, very caring and attentive.\",\n \"rating_date\": \"2026-06-10\",\n \"patient_uhid\": \"UHID12345\",\n \"patient_name\": \"Ahmed Ali\",\n \"patient_type\": \"OP\",\n \"department_name\": \"Internal Medicine\"\n}" }, "url": { "raw": "{{base_url}}/api/v1/external/doctor-ratings/", @@ -765,7 +765,7 @@ }, "status": "Created", "code": 201, - "body": "{\n \"success\": true,\n \"rating_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"staff_id\": \"f1e2d3c4-b5a6-7890-abcd-ef1234567890\",\n \"doctor_name\": \"OMAYMAH YAQOUB ELAMEIAN\",\n \"rating\": 4\n}" + "body": "{\n \"success\": true,\n \"rating_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n \"rating\": 4\n}" }, { "name": "Doctor Not Found", diff --git a/docs/external-api.md b/docs/external-api.md index 067166f..f120986 100644 --- a/docs/external-api.md +++ b/docs/external-api.md @@ -545,7 +545,7 @@ GET /api/v1/external/suggestions// ### Submit a Doctor Rating -Submit a patient rating for a doctor. The `doctor_id` must be an existing employee ID in the Staff table for the given hospital. +Submit a patient rating for a doctor. The `doctor_id` should identify the doctor (existing staff matched by employee ID, license number, or name; otherwise a new Staff record is auto-created — see below). ``` POST /api/v1/external/doctor-ratings/ @@ -555,10 +555,10 @@ POST /api/v1/external/doctor-ratings/ | Field | Required | Type | Description | |---|---|---|---| -| `hospital_id` | Yes | integer | Hospital ID | -| `doctor_id` | Yes | string | Doctor's employee ID (must exist in the hospital's Staff records) | +| `hospital` | Yes | string | Hospital name (case-insensitive) | +| `doctor_id` | Yes | string | Doctor's employee ID | | `rating` | Yes | integer | Rating from 1 to 5 | -| `doctor_name` | No | string | Doctor name (stored for reference) | +| `doctor_name` | Conditional | string | Doctor name. Required when `doctor_id` does not match an existing staff record (see auto-creation below). Otherwise stored for reference. | | `feedback` | No | string | Patient feedback text | | `rating_date` | No | date | Date of rating (`YYYY-MM-DD`, defaults to current date) | | `patient_uhid` | No | string | Patient unique health ID | @@ -568,11 +568,13 @@ POST /api/v1/external/doctor-ratings/ | `admit_date` | No | date | Admission date (`YYYY-MM-DD`) | | `discharge_date` | No | date | Discharge date (`YYYY-MM-DD`) | +**Staff lookup & auto-creation:** When a rating is submitted, the server looks up the doctor by (in order) `employee_id`, `license_number`, then `doctor_name` (exact / contains / first+last name split). If no existing staff is matched, a new Staff record is **auto-created** with `staff_type=physician`, `job_title="Physician"`, `employee_id=doctor_id`, and the provided name. In that case `doctor_name` is required; omitting it returns `400`. The auto-creation is logged as an `external_staff_auto_created` audit event. + **Example:** ```json { - "hospital_id": 1, + "hospital": "Al Nuzha", "doctor_id": "10738", "doctor_name": "Dr. Omaymah Yaqoub", "rating": 4, @@ -591,8 +593,6 @@ POST /api/v1/external/doctor-ratings/ { "success": true, "rating_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "staff_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", - "doctor_name": "OMAYMAH YAQOUB ELAMEIAN", "rating": 4 } ``` @@ -601,21 +601,95 @@ POST /api/v1/external/doctor-ratings/ | HTTP Status | Condition | |---|---| -| `400` | `doctor_id` not found in the hospital's Staff records | +| `400` | `doctor_id` matches no existing staff AND `doctor_name` is missing (cannot auto-create) | | `400` | `rating` outside 1-5 range | -| `400` | `hospital_id` not found | +| `400` | `hospital` not found | | `403` | Hospital does not match API key scope | -**Error Example (doctor not found):** +**Error Example (cannot auto-create without a name):** ```json { - "doctor_id": [ - "Doctor with employee ID '99999' not found at this hospital." + "doctor_name": [ + "Staff not found for this doctor_id; provide doctor_name to auto-create the staff record." ] } ``` +### List Doctor Ratings + +Returns a paginated list of doctor ratings, scoped to the API key's hospital. + +``` +GET /api/v1/external/doctor-ratings/list/ +``` + +**Query Parameters (all optional):** + +| Parameter | Description | +|---|---| +| `doctor_id` | Filter by HIS employee ID (exact match) | +| `rating` | Filter by rating value 1-5 (exact match) | +| `rating_from` | Rating-date lower bound (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) | +| `rating_to` | Rating-date upper bound (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) | +| `created_from` / `created_to` | Created-at date range | +| `updated_from` / `updated_to` | Updated-at date range | +| `page` | Page number (default `1`) | +| `page_size` | Page size (default `20`, max `100`) | + +**Response (200):** + +```json +{ + "count": 42, + "page": 1, + "page_size": 20, + "results": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "staff_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", + "hospital_name": "Al Nuzha", + "source": "his_api", + "source_reference": "", + "doctor_id": "10738", + "doctor_name": "OMAYMAH YAQOUB ELAMEIAN", + "doctor_name_raw": "10738 - OMAYMAH YAQOUB ELAMEIAN", + "doctor_name_display": "OMAYMAH YAQOUB ELAMEIAN", + "department_name": "Internal Medicine", + "patient_uhid": "UHID12345", + "patient_name": "Ahmed Ali", + "patient_type": "OP", + "admit_date": null, + "discharge_date": null, + "rating": 4, + "feedback": "Great doctor, very caring and attentive.", + "rating_date": "2026-06-10T00:00:00+03:00", + "is_aggregated": false, + "aggregated_at": null, + "created_at": "2026-06-10T14:22:11+03:00", + "updated_at": "2026-06-10T14:22:11+03:00" + } + ] +} +``` + +### Retrieve a Doctor Rating + +Retrieve a single rating by its UUID (returned in the `rating_id` field on creation). + +``` +GET /api/v1/external/doctor-ratings// +``` + +**Response (200):** Same object shape as a single item from the list endpoint. + +**Error Responses:** + +| HTTP Status | Condition | +|---|---| +| `404` | Rating does not exist, or is outside the API key's hospital scope | +| `403` | API key does not have access to `doctor_ratings` | + --- ## Common Patterns diff --git a/exports/admin_notifications.html b/exports/admin_notifications.html new file mode 100644 index 0000000..2292610 --- /dev/null +++ b/exports/admin_notifications.html @@ -0,0 +1,506 @@ + + + + + + Admin Notification Templates + + + + + + + +
+ + + + + + + + + +
+ ▼ New Complaint (Admin Notification) +
+ + + + + + + + +
+ + + + + + +
+

إلى إدارة تجربة المريض

+

تحية طيبة

+ +

+ نود إبلاغكم باستلام شكوى جديدة بتاريخ: 20/يوليو/2026 10:45 ص برقم المرجع: CMP-202607-0067 الأولوية: عالية +

+ +

+ للاطلاع عليها وتفعيلها، يمكنكم النقر على +

+ +

+ (تفعيل الشكوى) +

+ +

+ نرجو مراجعتها وتفعيلها في أقرب وقت ممكن، وللإفادة إضغط على +

+ +

+ (نموذج الإفادة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Patient Experience Management

+

Greetings,

+ +

+ We would like to inform you that a new complaint has been received on: 20/July/2026 10:45 AM with reference: CMP-202607-0067 Priority: High +

+ +

+ To view and activate it, you can click on: +

+ +

+ (Activate Complaint) +

+ +

+ Please review and activate it at your earliest convenience, and to provide feedback, click on: +

+ +

+ (Feedback Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Communication Request +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم علاقات المرضى

+

تحية طيبة

+ +

+ نود إبلاغكم بطلب تواصل جديد بتاريخ: 20/يوليو/2026 01:15 م من قبل المستخدم: نورة القحطاني رقم الطلب: CR-2026-0158 +

+ +

+ للاطلاع على الطلب والرد عليه، يمكنكم النقر على +

+ +

+ (نموذج الطلب) +

+ +

+ نرجو الرد على الطلب في أقرب وقت، وللإفادة إضغط على +

+ +

+ (نموذج الرد) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Patient Relations Department

+

Greetings,

+ +

+ We would like to inform you that a new communication request has been submitted on: 20/July/2026 01:15 PM by user: Noura Al-Qahtani Request ID: CR-2026-0158 +

+ +

+ To view and respond to the request, you can click on: +

+ +

+ (Request Form) +

+ +

+ Please respond to the request at your earliest convenience, and to provide feedback, click on: +

+ +

+ (Reply Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Invitation Expired +
+ + + + + + + + +
+ + + + + + +
+

إلى السيد / محمد العمري

+

تحية طيبة

+ +

+ نود إبلاغكم بأن دعوتكم للانضمام إلى نظام PX360 قد انتهت صلاحيتها بتاريخ: 19/يوليو/2026 11:59 م +

+ +

+ لطلب دعوة جديدة، يرجى التواصل مع المسؤول، وللإطلاع إضغط على +

+ +

+ (طلب دعوة جديدة) +

+ +

+ سيتم إنشاء رابط دعوة جديد لكم لإكمال إعداد الحساب، وللإفادة إضغط على +

+ +

+ (نموذج الإفادة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Mr. Mohammed Al-Omari

+

Greetings,

+ +

+ We would like to inform you that your invitation to join PX360 has expired on: 19/July/2026 11:59 PM +

+ +

+ To request a new invitation, please contact your administrator, and to view, click on: +

+ +

+ (Request New Invitation) +

+ +

+ A new invitation link will be generated for you to complete your account setup, and to provide feedback, click on: +

+ +

+ (Feedback Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ PX360 Weekly Digest +
+ + + + + + + + +
+ + + + + + +
+

ملخص الأسبوع - مستشفى الحمادي

+

تحية طيبة

+ +

+ نود إبلاغكم بالملخص الأسبوعي لأداء تجربة المريض للأسبوع: 14-20 يوليو 2026 إجمالي الشكاوى: 24 | معدل الالتزام بـ SLA: 87% +

+ +

+ للاطلاع على التفاصيل الكاملة، يمكنكم النقر على +

+ +

+ (لوحة المعلومات) +

+ +

+ نرجو مراجعة المؤشرات والتنبيهات والرد على المخاطر العالية، وللإفادة إضغط على +

+ +

+ (نموذج الإفادة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

Weekly Digest - Al Hammadi Hospital

+

Greetings,

+ +

+ We would like to share the weekly patient experience performance digest for the week: 14-20 July 2026 Total Complaints: 24 | SLA Compliance: 87% +

+ +

+ To view the full dashboard, you can click on: +

+ +

+ (Dashboard) +

+ +

+ Please review the metrics, early warnings, and respond to high-risk items, and to provide feedback, click on: +

+ +

+ (Feedback Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/appointment_email.html b/exports/appointment_email.html new file mode 100644 index 0000000..cca6f50 --- /dev/null +++ b/exports/appointment_email.html @@ -0,0 +1,140 @@ + + + + + + Appointment Confirmation Template + + + + + + + +
+ + + + + + + + + +
+ ▼ Appointment Confirmation +
+ + + + + + + + +
+ + + + + + +
+

إلى المريض / ماجد الحربي

+

تحية طيبة

+ +

+ نود إبلاغكم بتأكيد موعدكم بتاريخ: 25/يوليو/2026 10:30 ص رقم الموعد: APT-202607-0214 القسم: العيادات الخارجية - الباطنة الطبيب: د. سامي إبراهيم +

+ +

+ للاطلاع على تفاصيل الموعد، يمكنكم النقر على +

+ +

+ (تفاصيل الموعد) +

+ +

+ نرجو الحضور قبل الموعد بـ 15 دقيقة لإتمام التسجيل. لإعادة الجدولة إضغط على +

+ +

+ (إعادة الجدولة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Patient / Majed Al-Harbi

+

Greetings,

+ +

+ We would like to confirm your appointment on: 25/July/2026 10:30 AM Appointment ID: APT-202607-0214 Department: Outpatient - Internal Medicine Doctor: Dr. Sami Ibrahim +

+ +

+ To view the appointment details, you can click on: +

+ +

+ (Appointment Details) +

+ +

+ Please arrive 15 minutes before the appointment for registration. To reschedule, click on: +

+ +

+ (Reschedule) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/complaint_escalation_artboard_variant.html b/exports/complaint_escalation_artboard_variant.html new file mode 100644 index 0000000..96a2754 --- /dev/null +++ b/exports/complaint_escalation_artboard_variant.html @@ -0,0 +1,311 @@ + + + + + + Complaint Escalation - Artboard Variant + + + + +
+ + + + +
▼ Stage 1: New Complaint Notification
+
+
+ +
+

إلى قسم المختبر

+

تحية طيبة

+

+ نود إبلاغكم باستلام شكوى بتاريخ: + 23/يونيو/2026 12:15 م + من قبل وزارة الصحة برقم الهوية: + 1113543183 +

+

للإطلاع عليها، يمكنكم النقر على

+

(نموذج الشكوى)

+

نرجو النظر في الأمر والتحقق خلال 24 ساعة، وللإفادة إضغط على

+

(نموذج الرد على الشكوى)

+
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ رابط +

نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض

+
+ +
+ +
+

To: Laboratory Department

+

Greetings,

+

+ We would like to inform you that a complaint has been received on: + 23/June/2026 12:15 PM + by the Ministry of Health with ID number: + 1113543183 +

+

To view it, you can click on:

+

(Complaint Form)

+

Please look into the matter and verify within 24 hours, and to provide feedback, click on:

+

(Complaint Response Form)

+
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ Link +

We appreciate your commitment.
Regards, Patient Relations and Experience Management

+
+ +
+
+ + + + + +
▼ Stage 2: First Reminder Notification
+
+
+ +
+

إلى قسم الأمن

+

+ نقدر لكم انشغالكم وحرصكم، نود + تذكيركم + بالشكوى المقدمة + بتاريخ: 21/يونيو/2026 02:23 م + من قبل المراجع برقم الهوية: + 2343965287 +

+

(نموذج الشكوى)

+

نرجو منكم التكرم بالرد على هذه الشكوى خلال الـ 24 ساعة القادمة، حرصاً على تحسين تجربة المريض وضمان معالجة الشكوى في الوقت المناسب، للرد نأمل النقر على

+

(نموذج الرد على الشكوى)

+
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل على:

+

التحويلة: 1379 — رقم التواصل: 0537695000

+
+ رابط +

نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض

+
+ +
+ +
+

To: Security Department

+

+ We appreciate your dedication and diligence. We would like to + remind you + regarding the submitted complaint + dated: 21/June/2026 02:23 PM + by the visitor with National ID: + 2343965287 +

+

(Complaint Form)

+

Please kindly respond to this complaint within the next 24 hours, in our commitment to enhancing patient experience and ensuring timely resolution. To reply, please click on:

+

(Complaint Response Form)

+
+

For inquiries or more information, please contact us via:

+

Extension: 1379 — Contact Number: 0537695000

+
+ Link +

We appreciate your commitment.
Regards, Patient Relations and Experience Management

+
+ +
+
+ + + + + +
▼ Stage 3: Second Reminder Notification
+
+
+ +
+

إلى قسم المالية

+

تحية طيبة

+

+ نشكر لكم تعاونكم ونقدّر انشغالكم، ولكن نرجو + ملاحظة الوقت المتبقي للرد هو 6 ساعات + على الشكوى المقدمة بتاريخ: + 22/يونيو/2026 04:20 م + من قبل مجلس الضمان الصحي برقم الهوية: + 1050208576 +

+

(نموذج الشكوى)

+

لذا نأمل منكم التكرم بالرد والإفادة في أقرب وقت ممكن لتجنب أي تأخير إضافي في معالجة الشكوى. وللرد، يرجى النقر على

+

(نموذج الرد على الشكوى)

+
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل معنا:

+

التحويلة: 1379 — رقم التواصل: 0537695000

+
+ رابط +

نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض

+
+ +
+ +
+

To: Financial Department

+

Greetings,

+

+ We appreciate your cooperation and value your time, but please + note that the remaining time to respond is 6 hours + regarding the complaint submitted on: + 22/June/2026 04:20 PM + by the Council of Health Insurance with ID number: + 1050208576 +

+

(Complaint Form)

+

Therefore, we kindly request your response and feedback as soon as possible to avoid any further delay in processing the complaint. To reply, please click on:

+

(Complaint Response Form)

+
+

For inquiries or more information, please contact us via:

+

Extension: 1379 — Contact Number: 0537695000

+
+ Link +

We appreciate your commitment.
Regards, Patient Relations and Experience Management

+
+ +
+
+ +
+ + + diff --git a/exports/complaint_escalation_templates.html b/exports/complaint_escalation_templates.html new file mode 100644 index 0000000..a1fbf34 --- /dev/null +++ b/exports/complaint_escalation_templates.html @@ -0,0 +1,383 @@ + + + + + + Complaint System Escalation Templates + + + + + + + +
+ + + + + + + + + + +
+ ▼ Stage 1: New Complaint Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم المختبر

+

تحية طيبة

+ +

+ نود إبلاغكم باستلام شكوى بتاريخ: 23/يونيو/2026 12:15 م من قبل وزارة الصحة برقم الهوية: 1113543183 +

+ +

+ للإطلاع عليها، يمكنكم النقر على +

+ +

+ (نموذج الشكوى) +

+ +

+ نرجو النظر في الأمر والتحقق خلال 24 ساعة، وللإفادة إضغط على +

+ +

+ (نموذج الرد على الشكوى) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Laboratory Department

+

Greetings,

+ +

+ We would like to inform you that a complaint has been received on: 23/June/2026 12:15 PM by the Ministry of Health with ID number: 1113543183 +

+ +

+ To view it, you can click on: +

+ +

+ (Complaint Form) +

+ +

+ Please look into the matter and verify within 24 hours, and to provide feedback, click on: +

+ +

+ (Complaint Response Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + + +
+ ▼ Stage 2: First Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الأمن

+ +

+ نقدر لكم انشغالكم وحرصكم، نود + تذكيركم + بالشكوى المقدمة +
+ بتاريخ: 21/يونيو/2026 02:23 م +
+ من قبل المراجع برقم الهوية: 2343965287 +

+ +

+ (نموذج الشكوى) +

+ +

+ نرجو منكم التكرم بالرد على هذه الشكوى خلال الـ 24 ساعة القادمة، حرصاً على تحسين تجربة المريض وضمان معالجة الشكوى في الوقت المناسب , للرد نأمل النقر على +

+ +

+ (نموذج الرد على الشكوى) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل على:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Security Department

+ +

+ We appreciate your dedication and diligence. We would like to remind you regarding the submitted complaint +
+ Dated: 21/June/2026 02:23 PM +
+ By the visitor with National ID: 2343965287 +

+ +

+ (Complaint Form) +

+ +

+ Please kindly respond to this complaint within the next 24 hours, in our commitment to enhancing patient experience and ensuring timely resolution. To reply, please click on: +

+ +

+ (Complaint Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + + +
+ ▼ Stage 3: Second Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم المالية

+

تحية طيبة

+ +

+ نشكر لكم تعاونكم ونقدّر انشغالكم، ولكن نرجو ملاحظة الوقت المتبقي للرد هو 6 ساعات على الشكوى المقدمة بتاريخ: 22/يونيو/2026 04:20 م من قبل مجلس الضمان الصحي برقم الهوية: 1050208576 +

+ +

+ (نموذج الشكوى) +

+ +

+ لذا نأمل منكم التكرم بالرد والإفادة في أقرب وقت ممكن لتجنب أي تأخير إضافي في معالجة الشكوى. وللرد، يرجى النقر على +

+ +

+ (نموذج الرد على الشكوى) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل معنا:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Financial Department

+

Greetings,

+ +

+ We appreciate your cooperation and value your time, but please note that the remaining time to respond is 6 hours regarding the complaint submitted on: 22/June/2026 04:20 PM by the Council of Health Insurance with ID number: 1050208576 +

+ +

+ (Complaint Form) +

+ +

+ Therefore, we kindly request your response and feedback as soon as possible to avoid any further delay in processing the complaint. To reply, please click on: +

+ +

+ (Complaint Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/explanation_requests.html b/exports/explanation_requests.html new file mode 100644 index 0000000..0e4761e --- /dev/null +++ b/exports/explanation_requests.html @@ -0,0 +1,502 @@ + + + + + + Explanation Request Templates + + + + + + + +
+ + + + + + + + + +
+ ▼ Explanation Request (Complaint) +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الطوارئ

+

تحية طيبة

+ +

+ نود إبلاغكم بإسناد شكوى لقسمكم للرد عليها، بتاريخ: 20/يوليو/2026 09:00 ص برقم المرجع: CMP-202607-0072 الموعد النهائي: 22/يوليو/2026 05:00 م +

+ +

+ للاطلاع على تفاصيل الشكوى، يمكنكم النقر على +

+ +

+ (نموذج الشكوى) +

+ +

+ نرجو تقديم رد القسم قبل الموعد النهائي، وللإفادة إضغط على +

+ +

+ (نموذج الرد) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Emergency Department

+

Greetings,

+ +

+ Your department has been assigned to respond to a complaint, dated: 20/July/2026 09:00 AM with reference: CMP-202607-0072 Deadline: 22/July/2026 05:00 PM +

+ +

+ To view the complaint details, you can click on: +

+ +

+ (Complaint Form) +

+ +

+ Please submit your department's response before the deadline, and to provide feedback, click on: +

+ +

+ (Response Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Explanation Reminder (First) +
+ + + + + + + + +
+ + + + + + +
+

إلى السيد / سعد الدوسري

+ +

+ نقدر لكم انشغالكم وحرصكم، نود + تذكيركم + بتقديم رد القسم على الشكوى +
+ بتاريخ: 19/يوليو/2026 11:00 ص +
+ برقم المرجع: CMP-202607-0069 +

+ +

+ (نموذج الشكوى) +

+ +

+ نرجو منكم التكرم بتقديم الشرح خلال الـ 24 ساعة القادمة لتجنب التصعيد إلى مديركم المباشر , للرد نأمل النقر على +

+ +

+ (نموذج الرد) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل على:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Mr. Saad Al-Dosari

+ +

+ We appreciate your dedication and diligence. We would like to remind you to submit your department's response regarding the complaint +
+ Dated: 19/July/2026 11:00 AM +
+ Reference: CMP-202607-0069 +

+ +

+ (Complaint Form) +

+ +

+ Please kindly submit your explanation within the next 24 hours to avoid escalation to your direct manager. To reply, please click on: +

+ +

+ (Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Explanation Second Reminder (Final) +
+ + + + + + + + +
+ + + + + + +
+

إلى السيد / فهد الشمري

+

تحية طيبة

+ +

+ نشكر لكم تعاونكم، ولكن نرجو الملاحظة بأن هذا تذكيرك النهائي والعاجل لتقديم الشرح بخصوص الشكوى بتاريخ: 18/يوليو/2026 02:30 م برقم المرجع: CMP-202607-0065 +

+ +

+ (نموذج الشكوى) +

+ +

+ إذا لم يتم تقديم الشرح قبل الموعد النهائي، سيتم تصعيد الأمر إلى مديركم المباشر. يرجى التقديم فوراً +

+ +

+ (نموذج الرد) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل معنا:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Mr. Fahad Al-Shammari

+

Greetings,

+ +

+ We appreciate your cooperation, but please note that this is your FINAL and URGENT reminder to submit your explanation regarding the complaint dated: 18/July/2026 02:30 PM with reference: CMP-202607-0065 +

+ +

+ (Complaint Form) +

+ +

+ If the explanation is not submitted before the deadline, this matter will be escalated to your direct manager. Please submit immediately: +

+ +

+ (Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Inquiry Explanation Request +
+ + + + + + + + +
+ + + + + + +
+

إلى السيد / عبدالله الغامدي

+

تحية طيبة

+ +

+ نود إبلاغكم بطلب رد على استفسار بتاريخ: 20/يوليو/2026 08:00 ص برقم المرجع: INQ-202607-0021 الموضوع: تأخر موعد موعد العيادة +

+ +

+ للاطلاع على تفاصيل الاستفسار، يمكنكم النقر على +

+ +

+ (نموذج الاستفسار) +

+ +

+ نرجو تقديم ردكم في أقرب وقت ممكن، وللإفادة إضغط على +

+ +

+ (نموذج الرد) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Mr. Abdullah Al-Ghamdi

+

Greetings,

+ +

+ You have been requested to provide a response regarding an inquiry, dated: 20/July/2026 08:00 AM with reference: INQ-202607-0021 Subject: Clinic appointment delay +

+ +

+ To view the inquiry details, you can click on: +

+ +

+ (Inquiry Form) +

+ +

+ Please submit your response as soon as possible, and to provide feedback, click on: +

+ +

+ (Response Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/inquiry_escalation_logo_header_variant.html b/exports/inquiry_escalation_logo_header_variant.html new file mode 100644 index 0000000..fe5d302 --- /dev/null +++ b/exports/inquiry_escalation_logo_header_variant.html @@ -0,0 +1,412 @@ + + + + + + Inquiry Escalation - Logo Header Variant + + + + + + + + +
+ + + + + + + + + +
+ ▼ Stage 1: New Inquiry Notification +
+ + + + + + + + +
+ + Al Hammadi Hospital +
 
+
 
+ + + + + + +
+

إلى قسم الطوارئ

+

تحية طيبة

+ +

+ نود إبلاغكم باستلام استفسار بتاريخ: 15/يوليو/2026 09:30 ص من قبل المراجع برقم المرجع: INQ-202607-0012 +

+ +

+ للإطلاع عليها، يمكنكم النقر على +

+ +

+ (نموذج الاستفسار) +

+ +

+ نرجو النظر في الأمر والرد خلال 24 ساعة، وللإفادة إضغط على +

+ +

+ (نموذج الرد على الاستفسار) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Emergency Department

+

Greetings,

+ +

+ We would like to inform you that an inquiry has been received on: 15/July/2026 09:30 AM by the visitor with reference number: INQ-202607-0012 +

+ +

+ To view it, you can click on: +

+ +

+ (Inquiry Form) +

+ +

+ Please look into the matter and reply within 24 hours, and to provide feedback, click on: +

+ +

+ (Inquiry Response Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 2: First Reminder Notification +
+ + + + + + + + +
+ + Al Hammadi Hospital +
 
+
 
+ + + + + + +
+

إلى قسم الأشعة

+ +

+ نقدر لكم انشغالكم وحرصكم، نود + تذكيركم + بالاستفسار المقدم +
+ بتاريخ: 10/يوليو/2026 02:15 م +
+ برقم المرجع: INQ-202607-0008 +

+ +

+ (نموذج الاستفسار) +

+ +

+ نرجو منكم التكرم بالرد على هذا الاستفسار خلال الـ 24 ساعة القادمة، حرصاً على تحسين تجربة المريض وضمان معالجة الاستفسار في الوقت المناسب , للرد نأمل النقر على +

+ +

+ (نموذج الرد على الاستفسار) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل على:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Radiology Department

+ +

+ We appreciate your dedication and diligence. We would like to remind you regarding the submitted inquiry +
+ Dated: 10/July/2026 02:15 PM +
+ Reference Number: INQ-202607-0008 +

+ +

+ (Inquiry Form) +

+ +

+ Please kindly respond to this inquiry within the next 24 hours, in our commitment to enhancing patient experience and ensuring timely resolution. To reply, please click on: +

+ +

+ (Inquiry Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 3: Second Reminder Notification +
+ + + + + + + + +
+ + Al Hammadi Hospital +
 
+
 
+ + + + + + +
+

إلى قسم الصيدلية

+

تحية طيبة

+ +

+ نشكر لكم تعاونكم ونقدّر انشغالكم، ولكن نرجو ملاحظة الوقت المتبقي للرد هو 6 ساعات على الاستفسار المقدم بتاريخ: 08/يوليو/2026 11:45 ص برقم المرجع: INQ-202607-0003 +

+ +

+ (نموذج الاستفسار) +

+ +

+ لذا نأمل منكم التكرم بالرد والإفادة في أقرب وقت ممكن لتجنب أي تأخير إضافي في معالجة الاستفسار. وللرد، يرجى النقر على +

+ +

+ (نموذج الرد على الاستفسار) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل معنا:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Pharmacy Department

+

Greetings,

+ +

+ We appreciate your cooperation and value your time, but please note that the remaining time to respond is 6 hours regarding the inquiry submitted on: 08/July/2026 11:45 AM with reference number: INQ-202607-0003 +

+ +

+ (Inquiry Form) +

+ +

+ Therefore, we kindly request your response and feedback as soon as possible to avoid any further delay in processing the inquiry. To reply, please click on: +

+ +

+ (Inquiry Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/inquiry_escalation_templates.html b/exports/inquiry_escalation_templates.html new file mode 100644 index 0000000..5bfe7d1 --- /dev/null +++ b/exports/inquiry_escalation_templates.html @@ -0,0 +1,380 @@ + + + + + + Inquiry System Escalation Templates + + + + + + + +
+ + + + + + + + + +
+ ▼ Stage 1: New Inquiry Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الطوارئ

+

تحية طيبة

+ +

+ نود إبلاغكم باستلام استفسار بتاريخ: 15/يوليو/2026 09:30 ص من قبل المراجع برقم المرجع: INQ-202607-0012 +

+ +

+ للإطلاع عليها، يمكنكم النقر على +

+ +

+ (نموذج الاستفسار) +

+ +

+ نرجو النظر في الأمر والرد خلال 24 ساعة، وللإفادة إضغط على +

+ +

+ (نموذج الرد على الاستفسار) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Emergency Department

+

Greetings,

+ +

+ We would like to inform you that an inquiry has been received on: 15/July/2026 09:30 AM by the visitor with reference number: INQ-202607-0012 +

+ +

+ To view it, you can click on: +

+ +

+ (Inquiry Form) +

+ +

+ Please look into the matter and reply within 24 hours, and to provide feedback, click on: +

+ +

+ (Inquiry Response Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 2: First Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الأشعة

+ +

+ نقدر لكم انشغالكم وحرصكم، نود + تذكيركم + بالاستفسار المقدم +
+ بتاريخ: 10/يوليو/2026 02:15 م +
+ برقم المرجع: INQ-202607-0008 +

+ +

+ (نموذج الاستفسار) +

+ +

+ نرجو منكم التكرم بالرد على هذا الاستفسار خلال الـ 24 ساعة القادمة، حرصاً على تحسين تجربة المريض وضمان معالجة الاستفسار في الوقت المناسب , للرد نأمل النقر على +

+ +

+ (نموذج الرد على الاستفسار) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل على:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Radiology Department

+ +

+ We appreciate your dedication and diligence. We would like to remind you regarding the submitted inquiry +
+ Dated: 10/July/2026 02:15 PM +
+ Reference Number: INQ-202607-0008 +

+ +

+ (Inquiry Form) +

+ +

+ Please kindly respond to this inquiry within the next 24 hours, in our commitment to enhancing patient experience and ensuring timely resolution. To reply, please click on: +

+ +

+ (Inquiry Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 3: Second Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الصيدلية

+

تحية طيبة

+ +

+ نشكر لكم تعاونكم ونقدّر انشغالكم، ولكن نرجو ملاحظة الوقت المتبقي للرد هو 6 ساعات على الاستفسار المقدم بتاريخ: 08/يوليو/2026 11:45 ص برقم المرجع: INQ-202607-0003 +

+ +

+ (نموذج الاستفسار) +

+ +

+ لذا نأمل منكم التكرم بالرد والإفادة في أقرب وقت ممكن لتجنب أي تأخير إضافي في معالجة الاستفسار. وللرد، يرجى النقر على +

+ +

+ (نموذج الرد على الاستفسار) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل معنا:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Pharmacy Department

+

Greetings,

+ +

+ We appreciate your cooperation and value your time, but please note that the remaining time to respond is 6 hours regarding the inquiry submitted on: 08/July/2026 11:45 AM with reference number: INQ-202607-0003 +

+ +

+ (Inquiry Form) +

+ +

+ Therefore, we kindly request your response and feedback as soon as possible to avoid any further delay in processing the inquiry. To reply, please click on: +

+ +

+ (Inquiry Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/observation_escalation_templates.html b/exports/observation_escalation_templates.html new file mode 100644 index 0000000..7c85f67 --- /dev/null +++ b/exports/observation_escalation_templates.html @@ -0,0 +1,380 @@ + + + + + + Observation System Escalation Templates + + + + + + + +
+ + + + + + + + + +
+ ▼ Stage 1: New Observation Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم التمريض

+

تحية طيبة

+ +

+ نود إبلاغكم بصدور ملاحظة بتاريخ: 18/يوليو/2026 08:45 ص من قبل الموظف: أحمد محمد برمز التتبع: OBS-202607-0034 +

+ +

+ للإطلاع عليها، يمكنكم النقر على +

+ +

+ (نموذج الملاحظة) +

+ +

+ نرجو النظر في الأمر والتحقق خلال 24 ساعة، وللإفادة إضغط على +

+ +

+ (نموذج الرد على الملاحظة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Nursing Department

+

Greetings,

+ +

+ We would like to inform you that an observation has been reported on: 18/July/2026 08:45 AM by staff member: Ahmed Mohammed with tracking code: OBS-202607-0034 +

+ +

+ To view it, you can click on: +

+ +

+ (Observation Form) +

+ +

+ Please look into the matter and verify within 24 hours, and to provide feedback, click on: +

+ +

+ (Observation Response Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 2: First Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الجراحة

+ +

+ نقدر لكم انشغالكم وحرصكم، نود + تذكيركم + بالملاحظة المقدمة +
+ بتاريخ: 14/يوليو/2026 11:20 م +
+ برمز التتبع: OBS-202607-0028 +

+ +

+ (نموذج الملاحظة) +

+ +

+ نرجو منكم التكرم بالرد على هذه الملاحظة خلال الـ 24 ساعة القادمة، حرصاً على تحسين تجربة المريض وضمان معالجة الملاحظة في الوقت المناسب , للرد نأمل النقر على +

+ +

+ (نموذج الرد على الملاحظة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل على:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Surgery Department

+ +

+ We appreciate your dedication and diligence. We would like to remind you regarding the reported observation +
+ Dated: 14/July/2026 11:20 PM +
+ Tracking Code: OBS-202607-0028 +

+ +

+ (Observation Form) +

+ +

+ Please kindly respond to this observation within the next 24 hours, in our commitment to enhancing patient experience and ensuring timely resolution. To reply, please click on: +

+ +

+ (Observation Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 3: Second Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم العناية المركزة

+

تحية طيبة

+ +

+ نشكر لكم تعاونكم ونقدّر انشغالكم، ولكن نرجو ملاحظة الوقت المتبقي للرد هو 4 ساعات على الملاحظة المقدمة بتاريخ: 11/يوليو/2026 03:10 م برمز التتبع: OBS-202607-0021 +

+ +

+ (نموذج الملاحظة) +

+ +

+ لذا نأمل منكم التكرم بالرد والإفادة في أقرب وقت ممكن لتجنب أي تأخير إضافي في معالجة الملاحظة. وللرد، يرجى النقر على +

+ +

+ (نموذج الرد على الملاحظة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل معنا:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Critical Care Department

+

Greetings,

+ +

+ We appreciate your cooperation and value your time, but please note that the remaining time to respond is 4 hours regarding the observation reported on: 11/July/2026 03:10 PM with tracking code: OBS-202607-0021 +

+ +

+ (Observation Form) +

+ +

+ Therefore, we kindly request your response and feedback as soon as possible to avoid any further delay in processing the observation. To reply, please click on: +

+ +

+ (Observation Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/sections.csv b/exports/sections.csv new file mode 100644 index 0000000..bd8ebff --- /dev/null +++ b/exports/sections.csv @@ -0,0 +1,172 @@ +Name (EN),Name (AR),Department (EN),Department (AR) +Appointment Office,,Contact Center Department,قسم مركز الاتصال +Operator Office,,Contact Center Department,قسم مركز الاتصال +CCU,,Critical Care Department,قسم العناية المركزة +ICU,,Critical Care Department,قسم العناية المركزة +ICU Stepdown,,Critical Care Department,قسم العناية المركزة +Dermatology,,Dermatology Department,قسم الجلدية +Admission Office,,Emergency Administrative Department,إدارة الطوارئ +Emergency Coordination,,Emergency Administrative Department,إدارة الطوارئ +Emergency Management,,Emergency Administrative Department,إدارة الطوارئ +Emergency Reception,,Emergency Administrative Department,إدارة الطوارئ +Administrative Assistant,,Executive Administration,الإدارة التنفيذية +Chief Administrative Officer,,Executive Administration,الإدارة التنفيذية +Secretary,,Executive Administration,الإدارة التنفيذية +Software Programmer,,Executive Administration,الإدارة التنفيذية +Electrical Maintenance,,Facility Management & Maintenance Department,قسم إدارة المرافق والصيانة +General Maintenance,,Facility Management & Maintenance Department,قسم إدارة المرافق والصيانة +Mechanical Maintenance,,Facility Management & Maintenance Department,قسم إدارة المرافق والصيانة +Safety,,Facility Management & Maintenance Department,قسم إدارة المرافق والصيانة +Water Treatment,,Facility Management & Maintenance Department,قسم إدارة المرافق والصيانة +Financial Collection & Claims,,Financial Collection & Claims Department,قسم المالية والتحصيل والمطالبات +General,,Information Technology Department,قسم تقنية المعلومات +Service Desk,,Information Technology Department,قسم تقنية المعلومات +Software and Applications,,Information Technology Department,قسم تقنية المعلومات +Social Worker,,Inpatient Department,قسم التنويم +AKU,,Internal Medicine Department,قسم الباطنية +Cardiology,,Internal Medicine Department,قسم الباطنية +Endocrinology,,Internal Medicine Department,قسم الباطنية +Endoscopy,,Internal Medicine Department,قسم الباطنية +Gastroenterology,,Internal Medicine Department,قسم الباطنية +Infectious Disease,,Internal Medicine Department,قسم الباطنية +Internal Medicine,,Internal Medicine Department,قسم الباطنية +Long-Term Acute Care Unit (LTACU),,Internal Medicine Department,قسم الباطنية +Medical Ward,,Internal Medicine Department,قسم الباطنية +Nephrology,,Internal Medicine Department,قسم الباطنية +Neurology,,Internal Medicine Department,قسم الباطنية +Psychiatry,,Internal Medicine Department,قسم الباطنية +Pulmonology,,Internal Medicine Department,قسم الباطنية +Rheumatology,,Internal Medicine Department,قسم الباطنية +Blood Donation & Blood Bank,,Laboratory Department,قسم المختبر +Clinical Chemistry,,Laboratory Department,قسم المختبر +General,,Laboratory Department,قسم المختبر +Hematology,,Laboratory Department,قسم المختبر +Histopathology,,Laboratory Department,قسم المختبر +Microbiology,,Laboratory Department,قسم المختبر +Parasitology,,Laboratory Department,قسم المختبر +Phlepotomy,,Laboratory Department,قسم المختبر +Receiving ِArea,,Laboratory Department,قسم المختبر +Send Out,,Laboratory Department,قسم المختبر +Serology & Molecular,,Laboratory Department,قسم المختبر +Special Chemistry,,Laboratory Department,قسم المختبر +General,,Medical Administration,الإدارة الطبية +Medical Director,,Medical Administration,الإدارة الطبية +Secretary,,Medical Administration,الإدارة الطبية +Anesthesia Technology,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +CSSD,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Cardiac Physiology Services,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Clinical Nutrition,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Dental Services,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Director of Ancillary Medical Services,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Emergency Medical Services,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +General,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Health Education,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Neurophysiology Services,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Optometrist Services,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Physical Therapy,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Respiratory Therapy,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Secretary,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Swallowing Speech and Hearing Unit,,Medical Ancillary Services Department,قسم الخدمات الطبية المساندة +Emergency,,Medical Approvals Department,قسم الموافقات الطبية +General,,Medical Approvals Department,قسم الموافقات الطبية +Inpatient,,Medical Approvals Department,قسم الموافقات الطبية +Outpatient,,Medical Approvals Department,قسم الموافقات الطبية +AKU,,Nursing Department,قسم التمريض +Anesthesia,,Nursing Department,قسم التمريض +Cathlab,,Nursing Department,قسم التمريض +Emergency,,Nursing Department,قسم التمريض +Endoscopy,,Nursing Department,قسم التمريض +ICU/CCU,,Nursing Department,قسم التمريض +ICU/CCU Stepdown,,Nursing Department,قسم التمريض +L&D,,Nursing Department,قسم التمريض +LTACU,,Nursing Department,قسم التمريض +Main OR,,Nursing Department,قسم التمريض +Management,,Nursing Department,قسم التمريض +Medical Ward,,Nursing Department,قسم التمريض +NICU,,Nursing Department,قسم التمريض +NICU ISOLATOIN,,Nursing Department,قسم التمريض +Nursery,,Nursing Department,قسم التمريض +OB/OR,,Nursing Department,قسم التمريض +OPD 5,,Nursing Department,قسم التمريض +OPD 6,,Nursing Department,قسم التمريض +OPD 7,,Nursing Department,قسم التمريض +OPD1,,Nursing Department,قسم التمريض +OPD2,,Nursing Department,قسم التمريض +OPD3,,Nursing Department,قسم التمريض +OPD4 Dental,,Nursing Department,قسم التمريض +OPD4 Ophtha,,Nursing Department,قسم التمريض +Obgyne Ward,,Nursing Department,قسم التمريض +Oncology,,Nursing Department,قسم التمريض +Orthopedic Cast Technician,,Nursing Department,قسم التمريض +Outpatient,,Nursing Department,قسم التمريض +PICU,,Nursing Department,قسم التمريض +PICU Stepdown,,Nursing Department,قسم التمريض +Pediatric Ward,,Nursing Department,قسم التمريض +Recovery,,Nursing Department,قسم التمريض +Surgical Ward,,Nursing Department,قسم التمريض +Hematology,,ONCOLOGY Department,قسم الأورام +Clinics,,Obstetrics & Gynecology Department,قسم النساء والولادة +Labor & Delivery (L&D) / Obstetrics Operating Rooms,,Obstetrics & Gynecology Department,قسم النساء والولادة +Wards,,Obstetrics & Gynecology Department,قسم النساء والولادة +Coordination,,Outpatient Department,قسم العيادات الخارجية +Management,,Outpatient Department,قسم العيادات الخارجية +Medical Reports,,Outpatient Department,قسم العيادات الخارجية +Medical Secretarial,,Outpatient Department,قسم العيادات الخارجية +Outpatient Reception,,Outpatient Department,قسم العيادات الخارجية +Patient Affairs Director,,Patient Affairs Department,قسم علاقات المرضى +Secretary,,Patient Affairs Department,قسم علاقات المرضى +Patient Experience Director,,Patient Experience Department,قسم تجربة المريض +Patient Experience Staff,,Patient Experience Department,قسم تجربة المريض +Circumcision,,Pediatric Department,قسم الأطفال +General Pediatrics,,Pediatric Department,قسم الأطفال +NICU,,Pediatric Department,قسم الأطفال +NICU ISOLATOIN,,Pediatric Department,قسم الأطفال +NURSERY,,Pediatric Department,قسم الأطفال +PICU,,Pediatric Department,قسم الأطفال +PICU STEPDOWN,,Pediatric Department,قسم الأطفال +Pediatric Cardiology,,Pediatric Department,قسم الأطفال +Pediatric Endocrinology & Diabetes,,Pediatric Department,قسم الأطفال +Pediatric Gastroenterology,,Pediatric Department,قسم الأطفال +Pediatric General Surgery,,Pediatric Department,قسم الأطفال +Pediatric Hematology,,Pediatric Department,قسم الأطفال +Pediatric Nephrology,,Pediatric Department,قسم الأطفال +Pediatric Neurology,,Pediatric Department,قسم الأطفال +Pediatric Neurosurgery,,Pediatric Department,قسم الأطفال +Pediatric Orthopedic Surgery,,Pediatric Department,قسم الأطفال +Pediatric Otolaryngology (ENT),,Pediatric Department,قسم الأطفال +Pediatric Pulmonology,,Pediatric Department,قسم الأطفال +Pediatric Urology,,Pediatric Department,قسم الأطفال +Pediatric Ward,,Pediatric Department,قسم الأطفال +6th Floor Pharmacy,,Pharmacy Department,قسم الصيدلية +Central Outpatient Pharmacy,,Pharmacy Department,قسم الصيدلية +ER Pharmacy,,Pharmacy Department,قسم الصيدلية +General,,Pharmacy Department,قسم الصيدلية +IV Admixture Unit,,Pharmacy Department,قسم الصيدلية +Inpatient Oncology Pharmacy,,Pharmacy Department,قسم الصيدلية +Inpatient Pharmacy,,Pharmacy Department,قسم الصيدلية +Narcotic Pharmacy,,Pharmacy Department,قسم الصيدلية +Outpatient Oncology Pharmacy,,Pharmacy Department,قسم الصيدلية +Pediatric Pharmacy,,Pharmacy Department,قسم الصيدلية +Pharmacy Warehouse,,Pharmacy Department,قسم الصيدلية +Bone Mineral Density (BMD),,Radiology Department,قسم الأشعة +CT,,Radiology Department,قسم الأشعة +Fluoroscopy,,Radiology Department,قسم الأشعة +General,,Radiology Department,قسم الأشعة +Interventional Radiology (IR),,Radiology Department,قسم الأشعة +MRI,,Radiology Department,قسم الأشعة +Mamography,,Radiology Department,قسم الأشعة +Ultrasound,,Radiology Department,قسم الأشعة +X-Ray,,Radiology Department,قسم الأشعة +Camera Room,,Security Department,قسم الأمن +Security Guards,,Security Department,قسم الأمن +Bariatric,,Surgery Department,قسم الجراحة +ENT,,Surgery Department,قسم الجراحة +General Surgery,,Surgery Department,قسم الجراحة +Neurosurgery,,Surgery Department,قسم الجراحة +Orthopedic Surgery,,Surgery Department,قسم الجراحة +Plastic Surgery,,Surgery Department,قسم الجراحة +Spine Surgery,,Surgery Department,قسم الجراحة +Surgical Ward,,Surgery Department,قسم الجراحة +Thoracic Surgery,,Surgery Department,قسم الجراحة +Urology,,Surgery Department,قسم الجراحة +Vscular Surgery,,Surgery Department,قسم الجراحة diff --git a/exports/status_changes.html b/exports/status_changes.html new file mode 100644 index 0000000..f452a19 --- /dev/null +++ b/exports/status_changes.html @@ -0,0 +1,384 @@ + + + + + + Status Change Notification Templates + + + + + + + +
+ + + + + + + + + +
+ ▼ Observation Assigned +
+ + + + + + + + +
+ + + + + + +
+

إلى السيد / أحمد محمد

+

تحية طيبة

+ +

+ نود إبلاغكم بتعيين ملاحظة لكم للمراجعة واتخاذ الإجراء اللاحق، بتاريخ: 18/يوليو/2026 09:00 ص برمز التتبع: OBS-202607-0034 +

+ +

+ للإطلاع على التفاصيل، يمكنكم النقر على +

+ +

+ (نموذج الملاحظة) +

+ +

+ نرجو مراجعة التفاصيل وتحديث الحالة خلال 24 ساعة، وللإفادة إضغط على +

+ +

+ (نموذج التحديث) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Mr. Ahmed Mohammed

+

Greetings,

+ +

+ We would like to inform you that an observation has been assigned to you for review and action, dated: 18/July/2026 09:00 AM with tracking code: OBS-202607-0034 +

+ +

+ To view the details, you can click on: +

+ +

+ (Observation Form) +

+ +

+ Please review the details and update the status within 24 hours, and to provide feedback, click on: +

+ +

+ (Update Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Observation Resolved +
+ + + + + + + + +
+ + + + + + +
+

إلى السيد / خالد العنزي

+

تحية طيبة

+ +

+ نود إبلاغكم بأنه قد تم حل الملاحظة المسندة إليكم بتاريخ: 19/يوليو/2026 11:30 ص برمز التتبع: OBS-202607-0041 +

+ +

+ للاطلاع على تفاصيل الحل، يمكنكم النقر على +

+ +

+ (نموذج الملاحظة) +

+ +

+ نرجو مراجعة تفاصيل الحل والإفادة في حال وجود أي ملاحظات، وللرد إضغط على +

+ +

+ (نموذج الرد) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Mr. Khalid Al-Anazi

+

Greetings,

+ +

+ We would like to inform you that the observation assigned to you has been resolved, dated: 19/July/2026 11:30 AM with tracking code: OBS-202607-0041 +

+ +

+ To view the resolution details, you can click on: +

+ +

+ (Observation Form) +

+ +

+ Please review the resolution details and provide feedback if any, and to reply, click on: +

+ +

+ (Reply Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Observation Monthly Follow-Up +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الجودة

+

تحية طيبة

+ +

+ نود تذكيركم بمتابعة المراجعة الشهرية للملاحظة التي تم حلها بتاريخ: 19/يونيو/2026 برمز التتبع: OBS-202606-0019 +

+ +

+ للاطلاع على التفاصيل والتحقق من استمرار فعالية الإجراءات، يمكنكم النقر على +

+ +

+ (نموذج الملاحظة) +

+ +

+ نرجو تحديث الملاحظة بملاحظات المتابعة، وللإفادة إضغط على +

+ +

+ (نموذج التحديث) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Quality Department

+

Greetings,

+ +

+ We would like to remind you about the monthly follow-up review for the observation resolved on: 19/June/2026 with tracking code: OBS-202606-0019 +

+ +

+ To view details and verify the corrective actions remain effective, you can click on: +

+ +

+ (Observation Form) +

+ +

+ Please update the observation with follow-up notes, and to provide feedback, click on: +

+ +

+ (Update Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/suggestion_escalation_templates.html b/exports/suggestion_escalation_templates.html new file mode 100644 index 0000000..f47d613 --- /dev/null +++ b/exports/suggestion_escalation_templates.html @@ -0,0 +1,380 @@ + + + + + + Suggestion System Escalation Templates + + + + + + + +
+ + + + + + + + + +
+ ▼ Stage 1: New Suggestion Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم العيادات الخارجية

+

تحية طيبة

+ +

+ نود إبلاغكم باستلام اقتراح بتاريخ: 20/يوليو/2026 03:00 م من قبل المراجع: فهد العتيبي +

+ +

+ للإطلاع عليه، يمكنكم النقر على +

+ +

+ (نموذج الاقتراح) +

+ +

+ نرجو دراسة الاقتراح وتقديم الملاحظات خلال 48 ساعة، وللإفادة إضغط على +

+ +

+ (نموذج الرد على الاقتراح) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Outpatient Department

+

Greetings,

+ +

+ We would like to inform you that a suggestion has been received on: 20/July/2026 03:00 PM from the visitor: Fahad Al-Otaibi +

+ +

+ To view it, you can click on: +

+ +

+ (Suggestion Form) +

+ +

+ Please review the suggestion and provide feedback within 48 hours, and to respond, click on: +

+ +

+ (Suggestion Response Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 2: First Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم تقنية المعلومات

+ +

+ نقدر لكم انشغالكم وحرصكم، نود + تذكيركم + بالاقتراح المقدم +
+ بتاريخ: 12/يوليو/2026 10:00 ص +
+ من قبل الموظف: منى السالم +

+ +

+ (نموذج الاقتراح) +

+ +

+ نرجو منكم التكرم بالرد على هذا الاقتراح خلال الـ 48 ساعة القادمة، حرصاً على تحسين تجربة المريض وضمان دراسة الاقتراح في الوقت المناسب , للرد نأمل النقر على +

+ +

+ (نموذج الرد على الاقتراح) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل على:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Information Technology Department

+ +

+ We appreciate your dedication and diligence. We would like to remind you regarding the submitted suggestion +
+ Dated: 12/July/2026 10:00 AM +
+ From staff member: Mona Al-Salem +

+ +

+ (Suggestion Form) +

+ +

+ Please kindly respond to this suggestion within the next 48 hours, in our commitment to enhancing patient experience and ensuring timely review. To reply, please click on: +

+ +

+ (Suggestion Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Stage 3: Second Reminder Notification +
+ + + + + + + + +
+ + + + + + +
+

إلى قسم الموارد البشرية

+

تحية طيبة

+ +

+ نشكر لكم تعاونكم ونقدّر انشغالكم، ولكن نرجو ملاحظة اقتراب الموعد النهائي للرد (24 ساعة) على الاقتراح المقدم بتاريخ: 05/يوليو/2026 09:15 ص من قبل المراجع: سارة الحربي +

+ +

+ (نموذج الاقتراح) +

+ +

+ لذا نأمل منكم التكرم بالرد والإفادة في أقرب وقت ممكن لتجنب أي تأخير إضافي في دراسة الاقتراح. وللرد، يرجى النقر على +

+ +

+ (نموذج الرد على الاقتراح) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، يُرجى التواصل معنا:

+

التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Human Resources Department

+

Greetings,

+ +

+ We appreciate your cooperation and value your time, but please note that the deadline for response is approaching (24 hours) regarding the suggestion submitted on: 05/July/2026 09:15 AM from the visitor: Sarah Al-Harbi +

+ +

+ (Suggestion Form) +

+ +

+ Therefore, we kindly request your response and feedback as soon as possible to avoid any further delay in reviewing the suggestion. To reply, please click on: +

+ +

+ (Suggestion Response Form) +

+ +
+

For inquiries or more information, please contact us via:

+

Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/exports/survey_emails.html b/exports/survey_emails.html new file mode 100644 index 0000000..fb55437 --- /dev/null +++ b/exports/survey_emails.html @@ -0,0 +1,262 @@ + + + + + + Survey Email Templates + + + + + + + +
+ + + + + + + + + +
+ ▼ Survey Invitation (Patient) +
+ + + + + + + + +
+ + + + + + +
+

إلى المريض / عبدالرحمن السبيعي

+

تحية طيبة

+ +

+ نشكركم على اختياركم مستشفى الحمادي لاحتياجاتكم الصحية، ونأمل أن تكون زيارتكم بتاريخ: 18/يوليو/2026 قد حققت تطلعاتكم. +

+ +

+ نقدّر جداً تخصيص بضع دقائق لتعبئة استبيان الرضا. ملاحظاتكم تساعدنا على تحسين خدماتنا +

+ +

+ (ابدأ الاستبيان) +

+ +

+ يستغرق الاستبيان 3-5 دقائق فقط، وللاطلاع على التفاصيل إضغط على +

+ +

+ (التفاصيل) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Patient / Abdulrahman Al-Subaie

+

Greetings,

+ +

+ Thank you for choosing Al Hammadi Hospital for your healthcare needs. We hope your visit on: 18/July/2026 met your expectations. +

+ +

+ We would greatly appreciate it if you could take a few minutes to complete our satisfaction survey. Your feedback helps us improve our services. +

+ +

+ (Start Survey) +

+ +

+ The survey takes only 3-5 minutes, and to view details, click on: +

+ +

+ (Details) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ + + + + + + + + + +
+ ▼ Survey Results (Department Head) +
+ + + + + + + + +
+ + + + + + +
+

إلى السيد / مدير قسم الجراحة

+

تحية طيبة

+ +

+ نود إبلاغكم بتوفّر نتائج استبيان تجربة المريض لقسمكم للفترة: 1-15 يوليو 2026 المتوسط العام: 4.6/5 عدد الردود: 87 +

+ +

+ للاطلاع على التقرير التفصيلي، يمكنكم النقر على +

+ +

+ (التقرير الكامل) +

+ +

+ نرجو إعداد خطة عمل لمعالجة مجالات التحسين قبل نهاية الشهر، وللإفادة إضغط على +

+ +

+ (نموذج الإفادة) +

+ +
+

للإستفسارات أو لمزيد من المعلومات، على التحويلة: 1379

+

رقم التواصل: 0537695000

+
+ + + + + +
+ رابط +
+ +

+ نحن نقدر التزامكم، مع تحيات إدارة قسم علاقات المرضى وتجربة المريض +

+
+ +
+ + + + + + +
+

To: Surgery Department Head

+

Greetings,

+ +

+ The patient experience survey results for your department are now available for the period: 1-15 July 2026 Overall Score: 4.6/5 Responses: 87 +

+ +

+ To view the detailed report, you can click on: +

+ +

+ (Full Report) +

+ +

+ Please prepare an action plan to address the areas for improvement by the end of the month, and to provide feedback, click on: +

+ +

+ (Feedback Form) +

+ +
+

For inquiries or more information, via Extension: 1379

+

Contact Number: 0537695000

+
+ + + + + +
+ Link +
+ +

+ We appreciate your commitment.
Regards, Patient Relations and Experience Management +

+
+ +
+ جميع الحقوق محفوظة © Al Hammadi Hospitals Group +
+ +
+ + + diff --git a/requirements.txt b/requirements.txt index e339aa5..602c322 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,6 @@ cron-descriptor==1.4.5 cryptography==41.0.7 cssselect2==0.8.0 cupshelpers==1.0 -datastar-py==1.0.0 dbus-python==1.3.2 defer==1.0.6 deprecation==2.0.7 diff --git a/static/vendor/chartjs/chart.umd.min.js b/static/vendor/chartjs/chart.umd.min.js new file mode 100644 index 0000000..008464f --- /dev/null +++ b/static/vendor/chartjs/chart.umd.min.js @@ -0,0 +1,14 @@ +/*! + * Chart.js v4.5.1 + * https://www.chartjs.org + * (c) 2025 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Jo},get Decimation(){return ta},get Filler(){return ba},get Legend(){return Ma},get SubTitle(){return Pa},get Title(){return ka},get Tooltip(){return Na}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!function(t){return"symbol"==typeof t||"object"==typeof t&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const n=e.length;let o=0,a=n;if(t._sorted){const{iScale:r,vScale:l,_parsed:h}=t,c=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null,d=r.axis,{min:u,max:f,minDefined:g,maxDefined:p}=r.getUserBounds();if(g){if(o=Math.min(it(h,d,u).lo,i?n:it(e,d,r.getPixelForValue(u)).lo),c){const t=h.slice(0,o+1).reverse().findIndex((t=>!s(t[l.axis])));o-=Math.max(0,t)}o=Z(o,0,n-1)}if(p){let t=Math.max(it(h,r.axis,f,!0).hi+1,i?0:it(e,d,r.getPixelForValue(f),!0).hi+1);if(c){const e=h.slice(t-1).findIndex((t=>!s(t[l.axis])));t+=Math.max(0,e)}a=Z(t,o,n)-o}else a=n-o}return{start:o,count:a}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class xt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var bt=new xt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Jt{constructor(t){if(t instanceof Jt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Jt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Zt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Zt(t)?t:new Jt(t)}function te(t){return Zt(t)?t:new Jt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function xe(t,e){return me(t).getPropertyValue(e)}const be=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=be[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Me(t.height*s),o=Me(t.width*s);t.height=Me(t.height),t.width=Me(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=xe(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Ze(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Ze(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Ze(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Je(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Ze(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Je(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const xi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,bi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(xi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(bi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:J,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hb||l(n,x,p)&&0!==r(n,x),v=()=>!b||0===r(o,p)||l(o,x,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==x&&(b=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r!s(t[e.axis])));n.lo-=Math.max(0,a);const r=i.slice(n.hi).findIndex((t=>!s(t[e.axis])));n.hi+=Math.max(0,r)}return n}if(o._sharedOptions){const t=a[0],s="function"==typeof t.getRange&&t.getRange(e);if(s){const t=r(a,e,i-s),n=r(a,e,i+s);return{lo:t.lo,hi:n.hi}}}}return{lo:0,hi:a.length-1}}function $i(t,e,i,s,n){const o=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=o.length;t{t[a]&&t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Ki={evaluateInteractionItems:$i,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?Yi(t,n,o,s,a):Xi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tYi(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Xi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>qi(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>qi(t,ve(e,t),"y",i.intersect,s)}};const Gi=["left","top","right","bottom"];function Ji(t,e){return t.filter((t=>t.pos===e))}function Zi(t,e){return t.filter((t=>-1===Gi.indexOf(t.pos)&&t.box.axis===e))}function Qi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function ts(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!Gi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function os(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Qi(Ji(e,"left"),!0),n=Qi(Ji(e,"right")),o=Qi(Ji(e,"top"),!0),a=Qi(Ji(e,"bottom")),r=Zi(e,"x"),l=Zi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ji(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);is(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=ts(l.concat(h),d);os(r.fullSize,g,d,p),os(l,g,d,p),os(h,g,d,p)&&os(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),rs(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,rs(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class hs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class cs extends hs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const ds="$chartjs",us={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},fs=t=>null===t||""===t;const gs=!!Se&&{passive:!0};function ps(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,gs)}function ms(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function xs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.addedNodes,s),e=e&&!ms(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function bs(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||ms(i.removedNodes,s),e=e&&!ms(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const _s=new Map;let ys=0;function vs(){const t=window.devicePixelRatio;t!==ys&&(ys=t,_s.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function Ms(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){_s.size||window.addEventListener("resize",vs),_s.set(t,e)}(t,o),a}function ws(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){_s.delete(t),_s.size||window.removeEventListener("resize",vs)}(t)}function ks(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=us[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,gs)}(s,e,n),n}class Ss extends hs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[ds]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",fs(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(fs(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[ds])return!1;const i=e[ds].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[ds],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:xs,detach:bs,resize:Ms}[e]||ks;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:ws,detach:ws,resize:ws}[e]||ps)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function Ps(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?cs:Ss}var Ds=Object.freeze({__proto__:null,BasePlatform:hs,BasicPlatform:cs,DomPlatform:Ss,_detectPlatform:Ps});const Cs="transparent",Os={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Cs),n=s.valid&&Qt(e||Cs);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class As{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Os[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new As(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(bt.add(this._chart,i),!0):void 0}}function Ls(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Es(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function Vs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Ws(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Ns=t=>"reset"===t||"none"===t,Hs=(t,e)=>e?t:Object.assign({},t);class js{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Is(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Ws(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Bs(t,"x")),o=e.yAxisID=l(i.yAxisID,Bs(t,"y")),a=e.rAxisID=l(i.rAxisID,Bs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Ws(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Es(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Hs(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Ts(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Ns(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Ns(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Ns(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function Ys(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for(Us(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,qs=(t,e)=>Math.min(e||t,t);function Ks(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Js(t){return t.drawTicks?t.tickLength:0}function Zs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Qs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class tn extends $s{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=Z(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Js(t.grid)-e.padding-Zs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(Z((h.highest.height+6)/o,-1,1)),Math.asin(Z(a/r,-1,1))-Math.asin(Z(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Zs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Js(n)+o):(t.height=this.maxHeight,t.width=Js(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Js(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,x=function(t){return Ae(i,t,p)};let b,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)b=x(this.bottom),w=this.bottom-u,S=b-m,D=x(t.top)+m,O=t.bottom;else if("bottom"===a)b=x(this.top),D=t.top,O=x(t.bottom)-m,w=b+m,S=this.top+u;else if("left"===a)b=x(this.right),M=this.right-u,k=b-m,P=x(t.left)+m,C=t.right;else if("right"===a)b=x(this.left),P=t.left,C=x(t.right)-m,M=b+m,k=this.left+u;else if("x"===e){if("center"===a)b=x((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=b+m,S=w+u}else if("y"===e){if("center"===a)b=x((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];b=x(this.chart.scales[t].getPixelForValue(e))}M=b-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}x.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return x}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class sn{constructor(){this.controllers=new en(js,"datasets",!0),this.elements=new en($s,"elements"),this.plugins=new en(Object,"plugins"),this.scales=new en(tn,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function an(t,e){return e||!1!==t?!0===t?{}:t:null}function rn(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function ln(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function hn(t){if("x"===t||"y"===t||"r"===t)return t}function cn(t,...e){if(hn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&hn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function dn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function un(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=ln(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=cn(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return dn(t,"x",i[0])||dn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=b(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||ln(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),b(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];b(e,[ue.scales[e.type],ue.scale])})),a}function fn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=un(t,e)}function gn(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const pn=new Map,mn=new Set;function xn(t,e){let i=pn.get(t);return i||(i=e(),pn.set(t,i),mn.add(i)),i}const bn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class _n{constructor(t){this._config=function(t){return(t=t||{}).data=gn(t.data),fn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=gn(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),fn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return xn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return xn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return xn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return xn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>bn(r,t,e)))),e.forEach((t=>bn(r,s,t))),e.forEach((t=>bn(r,re[n]||{},t))),e.forEach((t=>bn(r,ue,t))),e.forEach((t=>bn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),mn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=yn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||vn(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=yn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function yn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const vn=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const Mn=["top","bottom","left","right","chartArea"];function wn(t,e){return"top"===t||"bottom"===t||-1===Mn.indexOf(t)&&"x"===e}function kn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function Sn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function Pn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Dn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Cn={},On=t=>{const e=Dn(t);return Object.values(Cn).filter((t=>t.canvas===e)).pop()};function An(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}class Tn{static defaults=ue;static instances=Cn;static overrides=re;static registry=nn;static version="4.5.1";static getChart=On;static register(...t){nn.add(...t),Ln()}static unregister(...t){nn.remove(...t),Ln()}constructor(t,e){const s=this.config=new _n(e),n=Dn(t),o=On(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||Ps(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new on,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Cn[this.id]=this,r&&l?(bt.listen(this,"complete",Sn),bt.listen(this,"progress",Pn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return nn}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return bt.stop(this),this}resize(t,e){bt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=cn(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=cn(o,n),r=l(n.type,e.dtype);void 0!==n.position&&wn(n.position,a)===wn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(nn.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{ls.configure(this,t,t.options),ls.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(kn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{ls.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){An(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;ls.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i={meta:t,index:t.index,cancelable:!0},s=Ni(this,t);!1!==this.notifyPlugins("beforeDatasetDraw",i)&&(s&&Ie(e,s),t.controller.draw(),s&&ze(e),i.cancelable=!1,this.notifyPlugins("afterDatasetDraw",i))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Ki.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),bt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Ln(){return u(Tn.instances,(t=>t._plugins.invalidate()))}function En(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Rn{static override(t){Object.assign(Rn.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return En()}parse(){return En()}format(){return En()}add(){return En()}diff(){return En()}startOf(){return En()}endOf(){return En()}}var In={_date:Rn};function zn(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Vn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data,{labels:{pointStyle:i,textAlign:s,color:n,useBorderRadius:o,borderRadius:a}}=t.legend.options;return e.labels.length&&e.datasets.length?e.labels.map(((e,r)=>{const l=t.getDatasetMeta(0).controller.getStyle(r);return{text:e,fillStyle:l.backgroundColor,fontColor:n,hidden:!t.getDataVisibility(r),lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:l.borderWidth,strokeStyle:l.borderColor,textAlign:s,pointStyle:i,borderRadius:o&&(a||l.borderRadius),index:r}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nJ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>J(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),x=g(C,h,d),b=g(C+E,c,u);s=(p-x)/2,n=(m-b)/2,o=-(p+x)/2,a=-(m+b)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),x=(i.width-o)/f,b=(i.height-o)/g,_=Math.max(Math.min(x,b)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Un=Object.freeze({__proto__:null,BarController:class extends js{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Vn(t,e,i,s)}parseArrayData(t,e,i,s){return Vn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=this._cachedMeta.controller.getParsed(e),l=r&&r[i.axis],h=t=>{const e=t._parsed.find((t=>t[i.axis]===l)),n=e&&e[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!h(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){const t=this.chart.scales,e=this.chart.options.indexAxis;return Object.keys(t).filter((i=>t[i].axis===e)).shift()}_getAxis(){const t={},e=this.getFirstScaleIdForIndexAxis();for(const i of this.chart.data.datasets)t[l("x"===this.chart.options.indexAxis?i.xAxisID:i.yAxisID,e)]=!0;return Object.keys(t)}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(x-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);x=Math.max(Math.min(x,h),o),d=x+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(x))}if(x===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;x+=t,u-=t}return{size:u,base:x,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;const c=this._getAxisCount();if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,d="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=x?g:{};if(i=b){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),x||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends $n{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:Yn,RadarController:class extends js{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>x,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),b||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Xn(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return Z(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:Z(n.innerStart,0,a),innerEnd:Z(n.innerEnd,0,a)}}function qn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function Kn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,x=n-p-f,{outerStart:b,outerEnd:_,innerStart:y,innerEnd:v}=Xn(e,u,d,x-m),M=d-b,w=d-_,k=m+b/M,S=x-_/w,P=u+y,D=u+v,O=m+y/P,A=x-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=qn(w,S,a,r);t.arc(e.x,e.y,_,S,x+E)}const i=qn(D,x,a,r);if(t.lineTo(i.x,i.y),v>0){const e=qn(D,A,a,r);t.arc(e.x,e.y,v,x+E,A+Math.PI)}const s=(x-v/u+(m+y/u))/2;if(t.arc(a,r,u,x-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=qn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=qn(M,m,a,r);if(t.lineTo(n.x,n.y),b>0){const e=qn(M,k,a,r);t.arc(e.x,e.y,b,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Gn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u,borderRadius:f}=l,g="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,g?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let p=e.endAngle;if(o){Kn(t,e,i,s,p,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,p),l.selfJoin&&p-a>=C&&0===f&&"miter"!==c&&function(t,e,i){const{startAngle:s,x:n,y:o,outerRadius:a,innerRadius:r,options:l}=e,{borderWidth:h,borderJoinStyle:c}=l,d=Math.min(h/a,G(s-i));if(t.beginPath(),t.arc(n,o,a-h/2,s+d/2,i-d/2),r>0){const e=Math.min(h/r,G(s-i));t.arc(n,o,r+h/2,i-e/2,s+e/2,!0)}else{const e=Math.min(h/2,a*G(s-i));if("round"===c)t.arc(n,o,e,i-C/2,s+C/2,!0);else if("bevel"===c){const a=2*e*e,r=-a*Math.cos(i+C/2)+n,l=-a*Math.sin(i+C/2)+o,h=a*Math.cos(s+C/2)+n,c=a*Math.sin(s+C/2)+o;t.lineTo(r,l),t.lineTo(h,c)}}t.closePath(),t.moveTo(0,0),t.rect(0,0,t.canvas.width,t.canvas.height),t.clip("evenodd")}(t,e,p),o||(Kn(t,e,i,s,p,n),t.stroke())}function Jn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Qn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[b(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[b(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=s,x=0,f=g=i),p=i}_()}function io(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?eo:to}const so="function"==typeof Path2D;function no(t,e,i,s){so&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Jn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=io(e);for(const r of n)Jn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class oo extends $s{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a),g=J(n,a,r)&&a!==r,p=f>=O||g,m=tt(o,h+u,c+u);return p&&m}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){Kn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function mo(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,x=!s(a),b=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!x&&!b)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),x&&b&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=x?a:M,w=b?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(x&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return b&&u&&w!==r?i.length&&V(i[i.length-1].value,r,xo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):b&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class _o extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const yo=t=>Math.floor(z(t)),vo=(t,e)=>Math.pow(10,yo(t)+e);function Mo(t){return 1===t/Math.pow(10,yo(t))}function wo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function ko(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=yo(e);let o=function(t,e){let i=yo(e-t);for(;wo(t,e,i)>10;)i++;for(;wo(t,e,i)<10;)i--;return Math.min(i,yo(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:Mo(g),significand:u}),s}class So extends tn{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===vo(this.min,0)?vo(this.min,-1):vo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(vo(i,-1)),o(vo(s,1)))),i<=0&&n(vo(s,-1)),s<=0&&o(vo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=ko({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function Po(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Do(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Co(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Ao(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function To(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function Lo(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Eo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(Po(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/Po(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Co(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));Lo(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash||[]),o.lineDashOffset=n.dashOffset,o.beginPath(),Eo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Io={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},zo=Object.keys(Io);function Fo(t,e){return t-e}function Vo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Bo(t,e,i,s){const n=zo.length;for(let o=zo.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function No(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class Ho extends tn{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new In._date(t.adapters.date);s.init(e),b(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Vo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Bo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=zo.length-1;o>=zo.indexOf(i);o--){const i=zo[o];if(Io[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return zo[i?zo.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=zo.indexOf(t)+1,i=zo.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=Z(s,0,o),n=Z(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Bo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var $o=Object.freeze({__proto__:null,CategoryScale:class extends tn{static id="category";static defaults={ticks:{callback:mo}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Z(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:po(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return mo.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:_o,LogarithmicScale:So,RadialLinearScale:Ro,TimeScale:Ho,TimeSeriesScale:class extends Ho{static id="timeseries";static defaults=Ho.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=jo(e,this.min),this._tableRange=jo(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(jo(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return jo(this._table,i*this._tableRange+this._minPos,!0)}}});const Yo=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Uo=Yo.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Xo(t){return Yo[t%Yo.length]}function qo(t){return Uo[t%Uo.length]}function Ko(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n instanceof Yn?e=function(t,e){return t.backgroundColor=t.data.map((()=>qo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Xo(e),t.backgroundColor=qo(e),++e}(i,e))}}function Go(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Jo={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n,a=Go(s)||(r=n)&&(r.borderColor||r.backgroundColor)||o&&Go(o)||"rgba(0,0,0,0.1)"!==ue.borderColor||"rgba(0,0,0,0.1)"!==ue.backgroundColor;var r;if(!i.forceOverride&&a)return;const l=Ko(t);s.forEach(l)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Qo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var ta={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Qo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=Z(it(e,o.axis,a).lo,0,i-1)),s=h?Z(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const x=[],b=e+i-1,_=t[e].x,y=t[b].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&x.push({...t[e],x:p}),s!==u&&s!==i&&x.push({...t[s],x:p})}o>0&&i!==u&&x.push(t[i]),x.push(a),h=e,m=0,f=g=l,c=d=u=o}}return x}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Qo(t)}};function ea(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ia(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function sa(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function na(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ia(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new oo({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function oa(t){return t&&!1!==t.fill}function aa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function ra(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function la(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&ua(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;oa(i)&&ua(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;oa(s)&&"beforeDatasetDraw"===i.drawTime&&ua(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const _a=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class ya extends $s{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=_a(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=va(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=_a(o,d),x=this.isHorizontal(),b=this._computeTitleHeight();f=x?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+b,line:0}:{x:this.left+c,y:ft(n,this.top+b+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),x?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+b+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,x?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),x)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=va(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class wa extends $s{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var ka={id:"title",_element:wa,start(t,e,i){!function(t,e){const i=new wa({ctx:t.ctx,options:e,chart:t});ls.configure(t,i,e),ls.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;ls.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa=new WeakMap;var Pa={id:"subtitle",start(t,e,i){const s=new wa({ctx:t.ctx,options:i,chart:t});ls.configure(t,s,i),ls.addBox(t,s),Sa.set(t,s)},stop(t){ls.removeBox(t,Sa.get(t)),Sa.delete(t)},beforeUpdate(t,e,i){const s=Sa.get(t);ls.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Da={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Aa(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Ta(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,x=0,b=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(b+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),b){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){x=Math.max(x,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),x+=p.width,{width:x,height:m}}function La(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ea(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||La(t,e,i,s),yAlign:s}}function Ra(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:Z(g,0,s.width-e.width),y:Z(p,0,s.height-e.height)}}function Ia(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function za(t){return Ca([],Oa(t))}function Fa(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const Va={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Fa(i,t);Ca(e.before,Oa(Ba(n,"beforeLabel",this,t))),Ca(e.lines,Ba(n,"label",this,t)),Ca(e.after,Oa(Ba(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return za(Ba(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Ba(i,"beforeFooter",this,t),n=Ba(i,"footer",this,t),o=Ba(i,"afterFooter",this,t);let a=[];return a=Ca(a,Oa(s)),a=Ca(a,Oa(n)),a=Ca(a,Oa(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Fa(t.callbacks,e);s.push(Ba(i,"labelColor",this,e)),n.push(Ba(i,"labelPointStyle",this,e)),o.push(Ba(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Da[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Ta(this,i),a=Object.assign({},t,e),r=Ea(this.chart,i,a),l=Ra(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,x,b,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,b=_+o,y=_-o):(p=d+f,m=p+o,b=_-o,y=_+o),x=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(b=u,_=b-o,p=m-o,x=m+o):(b=u+g,_=b+o,p=m+o,x=m-o),y=b),{x1:p,x2:m,x3:x,y1:b,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ia(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let x,b,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ia(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Da[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Ta(this,t),a=Object.assign({},i,this._size),r=Ea(e,t,a),l=Ra(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Da[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Na={id:"tooltip",_element:Wa,positioners:Da,afterInit(t,e,i){i&&(t.tooltip=new Wa({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Va},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return Tn.register(Un,$o,go,t),Tn.helpers={...Hi},Tn._adapters=In,Tn.Animation=As,Tn.Animations=Ts,Tn.animator=bt,Tn.controllers=nn.controllers.items,Tn.DatasetController=js,Tn.Element=$s,Tn.elements=go,Tn.Interaction=Ki,Tn.layouts=ls,Tn.platforms=Ds,Tn.Scale=tn,Tn.Ticks=ae,Object.assign(Tn,Un,$o,go,t,Ds),Tn.Chart=Tn,"undefined"!=typeof window&&(window.Chart=Tn),Tn})); +//# sourceMappingURL=chart.umd.min.js.map diff --git a/static/vendor/chartjs/chartjs-plugin-datalabels.min.js b/static/vendor/chartjs/chartjs-plugin-datalabels.min.js new file mode 100644 index 0000000..e84e6ff --- /dev/null +++ b/static/vendor/chartjs/chartjs-plugin-datalabels.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-plugin-datalabels v2.2.0 + * https://chartjs-plugin-datalabels.netlify.app + * (c) 2017-2022 chartjs-plugin-datalabels contributors + * Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js/helpers"),require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js/helpers","chart.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).ChartDataLabels=e(t.Chart.helpers,t.Chart)}(this,(function(t,e){"use strict";var r=function(){if("undefined"!=typeof window){if(window.devicePixelRatio)return window.devicePixelRatio;var t=window.screen;if(t)return(t.deviceXDPI||1)/(t.logicalXDPI||1)}return 1}(),a=function(e){var r,a=[];for(e=[].concat(e);e.length;)"string"==typeof(r=e.pop())?a.unshift.apply(a,r.split("\n")):Array.isArray(r)?e.push.apply(e,r):t.isNullOrUndef(e)||a.unshift(""+r);return a},o=function(t,e,r){var a,o=[].concat(e),n=o.length,i=t.font,l=0;for(t.font=r.string,a=0;ar.right&&(a|=2),er.bottom&&(a|=4),a}function u(t,e){var r,a,o=e.anchor,n=t;return e.clamp&&(n=function(t,e){for(var r,a,o,n=t.x0,i=t.y0,l=t.x1,u=t.y1,d=s(n,i,e),c=s(l,u,e);d|c&&!(d&c);)8&(r=d||c)?(a=n+(l-n)*(e.top-i)/(u-i),o=e.top):4&r?(a=n+(l-n)*(e.bottom-i)/(u-i),o=e.bottom):2&r?(o=i+(u-i)*(e.right-n)/(l-n),a=e.right):1&r&&(o=i+(u-i)*(e.left-n)/(l-n),a=e.left),r===d?d=s(n=a,i=o,e):c=s(l=a,u=o,e);return{x0:n,x1:l,y0:i,y1:u}}(n,e.area)),"start"===o?(r=n.x0,a=n.y0):"end"===o?(r=n.x1,a=n.y1):(r=(n.x0+n.x1)/2,a=(n.y0+n.y1)/2),function(t,e,r,a,o){switch(o){case"center":r=a=0;break;case"bottom":r=0,a=1;break;case"right":r=1,a=0;break;case"left":r=-1,a=0;break;case"top":r=0,a=-1;break;case"start":r=-r,a=-a;break;case"end":break;default:o*=Math.PI/180,r=Math.cos(o),a=Math.sin(o)}return{x:t,y:e,vx:r,vy:a}}(r,a,t.vx,t.vy,e.align)}var d=function(t,e){var r=(t.startAngle+t.endAngle)/2,a=Math.cos(r),o=Math.sin(r),n=t.innerRadius,i=t.outerRadius;return u({x0:t.x+a*n,y0:t.y+o*n,x1:t.x+a*i,y1:t.y+o*i,vx:a,vy:o},e)},c=function(t,e){var r=l(t,e.origin),a=r.x*t.options.radius,o=r.y*t.options.radius;return u({x0:t.x-a,y0:t.y-o,x1:t.x+a,y1:t.y+o,vx:r.x,vy:r.y},e)},h=function(t,e){var r=l(t,e.origin),a=t.x,o=t.y,n=0,i=0;return t.horizontal?(a=Math.min(t.x,t.base),n=Math.abs(t.base-t.x)):(o=Math.min(t.y,t.base),i=Math.abs(t.base-t.y)),u({x0:a,y0:o+i,x1:a+n,y1:o,vx:r.x,vy:r.y},e)},f=function(t,e){var r=l(t,e.origin);return u({x0:t.x,y0:t.y,x1:t.x+(t.width||0),y1:t.y+(t.height||0),vx:r.x,vy:r.y},e)},x=function(t){return Math.round(t*r)/r};function y(t,e){var r=e.chart.getDatasetMeta(e.datasetIndex).vScale;if(!r)return null;if(void 0!==r.xCenter&&void 0!==r.yCenter)return{x:r.xCenter,y:r.yCenter};var a=r.getBasePixel();return t.horizontal?{x:a,y:null}:{x:null,y:a}}function v(t,e,r){var a=r.backgroundColor,o=r.borderColor,n=r.borderWidth;(a||o&&n)&&(t.beginPath(),function(t,e,r,a,o,n){var i=Math.PI/2;if(n){var l=Math.min(n,o/2,a/2),s=e+l,u=r+l,d=e+a-l,c=r+o-l;t.moveTo(e,u),sr.x+r.w+2||t.y>r.y+r.h+2)},intersects:function(t){var e,r,a,o=this._points(),n=t._points(),i=[M(o[0],o[1]),M(o[0],o[3])];for(this._rotation!==t._rotation&&i.push(M(n[0],n[1]),M(n[0],n[3])),e=0;et.getProps([e],!0)[e]}),n=a.geometry(),i=$(l,a.model(),n),o._box.update(i,n,a.rotation()));(function(t,e){var r,a,o,n;for(r=t.length-1;r>=0;--r)for(o=t[r].$layout,a=r-1;a>=0&&o._visible;--a)(n=t[a].$layout)._visible&&o._box.intersects(n._box)&&e(o,n)})(t,(function(t,e){var r=t._hidable,a=e._hidable;r&&a||a?e._visible=!1:r&&(t._visible=!1)}))}(t)},lookup:function(t,e){var r,a;for(r=t.length-1;r>=0;--r)if((a=t[r].$layout)&&a._visible&&a._box.contains(e))return t[r];return null},draw:function(t,e){var r,a,o,n,i,l;for(r=0,a=e.length;re!==null&&typeof e=="object"&&(Object.getPrototypeOf(e)===Object.prototype||Object.getPrototypeOf(e)===null),vt=e=>{for(let t in e)if(x(e,t))return!1;return!0},te=(e,t)=>{for(let n in e){let r=e[n];J(r)||Array.isArray(r)?te(r,t):e[n]=t(r)}},xe=e=>{let t={};for(let[n,r]of e){let s=n.split("."),i=s.pop(),o=s.reduce((a,l)=>a[l]??={},t);o[i]=r}return t};var Le=[],Ze=[],He=0,Ne=0,Qe=0,Ye,B,Pe=0,L=()=>{He++},N=()=>{--He||(St(),K())},H=e=>{Ye=B,B=e},_=()=>{B=Ye,Ye=void 0},he=e=>an.bind(0,{previousValue:e,t:e,e:1}),Xe=Symbol("computed"),_e=e=>{let t=cn.bind(0,{e:17,getter:e});return t[Xe]=1,t},w=e=>{let t={d:e,e:2};B&&tt(t,B),H(t),L();try{t.d()}finally{N(),_()}return wt.bind(0,t)},St=()=>{for(;Ne"getter"in e?Tt(e):At(e,e.t),Tt=e=>{H(e),Mt(e);try{let t=e.t;return t!==(e.t=e.getter(t))}finally{_(),xt(e)}},At=(e,t)=>(e.e=1,e.previousValue!==(e.previousValue=t)),et=e=>{let t=e.e;if(!(t&64)){e.e=t|64;let n=e.r;n?et(n.o):Ze[Qe++]=e}},Rt=(e,t)=>{if(t&16||t&32&&Lt(e.s,e)){H(e),Mt(e),L();try{e.d()}finally{N(),_(),xt(e)}return}t&32&&(e.e=t&-33);let n=e.s;for(;n;){let r=n.c,s=r.e;s&64&&Rt(r,r.e=s&-65),n=n.i}},an=(e,...t)=>{if(t.length){if(e.t!==(e.t=t[0])){e.e=17;let r=e.r;return r&&(ln(r),He||St()),!0}return!1}let n=e.t;if(e.e&16&&At(e,n)){let r=e.r;r&&Fe(r)}return B&&tt(e,B),n},cn=e=>{let t=e.e;if(t&16||t&32&&Lt(e.s,e)){if(Tt(e)){let n=e.r;n&&Fe(n)}}else t&32&&(e.e=t&-33);return B&&tt(e,B),e.t},wt=e=>{let t=e.s;for(;t;)t=Ce(t,e);let n=e.r;n&&Ce(n),e.e=0},tt=(e,t)=>{let n=t.a;if(n&&n.c===e)return;let r=n?n.i:t.s;if(r&&r.c===e){r.p=Pe,t.a=r;return}let s=e.m;if(s&&s.p===Pe&&s.o===t)return;let i=t.a=e.m={p:Pe,c:e,o:t,l:n,i:r,u:s};r&&(r.l=i),n?n.i=i:t.s=i,s?s.n=i:e.r=i},Ce=(e,t=e.o)=>{let n=e.c,r=e.l,s=e.i,i=e.n,o=e.u;if(s?s.l=r:t.a=r,r?r.i=s:t.s=s,i?i.u=o:n.m=o,o)o.n=i;else if(!(n.r=i))if("getter"in n){let a=n.s;if(a){n.e=17;do a=Ce(a,n);while(a)}}else"previousValue"in n||wt(n);return s},ln=e=>{let t=e.n,n;e:for(;;){let r=e.o,s=r.e;if(s&60?s&12?s&4?!(s&48)&&un(e,r)?(r.e=s|40,s&=1):s=0:r.e=s&-9|32:s=0:r.e=s|32,s&2&&et(r),s&1){let i=r.r;if(i){let o=(e=i).n;o&&(n={t,f:n},t=o);continue}}if(e=t){t=e.n;continue}for(;n;)if(e=n.t,n=n.f,e){t=e.n;continue e}break}},Mt=e=>{Pe++,e.a=void 0,e.e=e.e&-57|4},xt=e=>{let t=e.a,n=t?t.i:e.s;for(;n;)n=Ce(n,e);e.e&=-5},Lt=(e,t)=>{let n,r=0,s=!1;e:for(;;){let i=e.c,o=i.e;if(t.e&16)s=!0;else if((o&17)===17){if(bt(i)){let a=i.r;a.n&&Fe(a),s=!0}}else if((o&33)===33){(e.n||e.u)&&(n={t:e,f:n}),e=i.s,t=i,++r;continue}if(!s){let a=e.i;if(a){e=a;continue}}for(;r--;){let a=t.r,l=a.n;if(l?(e=n.t,n=n.f):e=a,s){if(bt(t)){l&&Fe(a),t=e.o;continue}s=!1}else t.e&=-33;if(t=e.o,e.i){e=e.i;continue e}}return s}},Fe=e=>{do{let t=e.o,n=t.e;(n&48)===32&&(t.e=n|16,n&2&&et(t))}while(e=e.n)},un=(e,t)=>{let n=t.a;for(;n;){if(n===e)return!0;n=n.l}return!1},oe=e=>{let t=ne,n=e.split(".");for(let r of n){if(t==null||!x(t,r))return;t=t[r]}return t},Oe=(e,t="")=>{let n=Array.isArray(e);if(n||J(e)){let r=n?[]:{};for(let i in e)r[i]=he(Oe(e[i],`${t+i}.`));let s=he(0);return new Proxy(r,{get(i,o){if(!(o==="toJSON"&&!x(r,o)))return n&&o in Array.prototype?(s(),r[o]):typeof o=="symbol"?r[o]:((!x(r,o)||r[o]()==null)&&(r[o]=he(""),K(t+o,""),s(s()+1)),r[o]())},set(i,o,a){let l=t+o;if(n&&o==="length"){let c=r[o]-a;if(r[o]=a,c>0){let u={};for(let f=a;f{if(e!==void 0&&t!==void 0&&Le.push([e,t]),!He&&Le.length){let n=xe(Le);Le.length=0,document.dispatchEvent(new CustomEvent(ee,{detail:n}))}},k=(e,{ifMissing:t}={})=>{L();for(let n in e)e[n]==null?t||delete ne[n]:Nt(e[n],n,ne,"",t);N()},R=(e,t)=>k(xe(e),t),Nt=(e,t,n,r,s)=>{if(J(e)){x(n,t)&&(J(n[t])||Array.isArray(n[t]))||(n[t]={});for(let i in e)e[i]==null?s||delete n[t][i]:Nt(e[i],i,n[t],`${r+t}.`,s)}else s&&x(n,t)||(n[t]=e)},Et=e=>typeof e=="string"?RegExp(e.replace(/^\/|\/$/g,"")):e,V=({include:e=/.*/,exclude:t=/(?!)/}={},n=ne)=>{let r=Et(e),s=Et(t),i=[],o=[[n,""]];for(;o.length;){let[a,l]=o.pop();for(let c in a){let u=l+c;J(a[c])?o.push([a[c],`${u}.`]):r.test(u)&&!s.test(u)&&i.push([u,oe(u)])}}return xe(i)},ne=Oe({});var z=e=>e instanceof HTMLElement||e instanceof SVGElement||e instanceof MathMLElement;var ae=e=>e.replace(/([A-Z]+)([A-Z][a-z])/g,"$1-$2").replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/([a-z])([0-9]+)/gi,"$1-$2").replace(/([0-9]+)([a-z])/gi,"$1-$2").replace(/[\s_]+/g,"-").toLowerCase(),Pt=e=>ae(e).replace(/-./g,t=>t[1].toUpperCase()),Ot=e=>ae(e).replace(/-/g,"_");var fn=/^(?:(?:async\s+)?function\b|(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>)/,ce=(e,t={})=>{let{reviveFunctionStrings:n=!1}=t;try{return n?JSON.parse(e,(r,s)=>{if(typeof s!="string")return s;let i=s.trim();if(!fn.test(i))return s;try{let o=Function(`return (${i})`)();return typeof o=="function"?o:s}catch{return s}}):JSON.parse(e)}catch{return Function(`return (${e})`)()}},Ct={camel:e=>e.replace(/-[a-z]/g,t=>t[1].toUpperCase()),snake:e=>e.replace(/-/g,"_"),pascal:e=>e[0].toUpperCase()+Ct.camel(e.slice(1))},P=(e,t,n="camel")=>{for(let r of t.get("case")||[n])e=Ct[r]?.(e)||e;return e},W=e=>`data-${e}`,nt=e=>e;var dn="https://data-star.dev/errors",ye=(e,t,n={})=>{Object.assign(n,e);let r=new Error,s=Ot(t),i=new URLSearchParams({metadata:JSON.stringify(n)}).toString(),o=JSON.stringify(n,null,2);return r.message=`${t} -More info: ${dn}/${s}?${i} -Context: ${o}`,r},ve=new Map,rt=new Map,_t=new Map,kt=new Proxy({},{get:(e,t)=>ve.get(t)?.apply,has:(e,t)=>ve.has(t),ownKeys:()=>Reflect.ownKeys(ve),set:()=>!1,deleteProperty:()=>!1}),Ee=new Map,ke=[],st=new Set,be=new Set,Ft=!1,p=e=>{ke.push(e),ke.length===1&&setTimeout(()=>{for(let n of ke)st.add(n.name),rt.set(n.name,n);ke.length=0;let t=be.size?[...be]:[document.documentElement];for(let n of t)bn(n,!be.has(n));st.clear()})},D=e=>{ve.set(e.name,e)};document.addEventListener(j,e=>{let t=_t.get(e.detail.type);t&&t.apply({error:ye.bind(0,{plugin:{type:"watcher",name:t.name},element:{id:e.target.id,tag:e.target.tagName}})},e.detail.argsRaw)});var Se=e=>{_t.set(e.name,e)},Ht=e=>{for(let t of e){let n=Ee.get(t);if(n&&Ee.delete(t))for(let r of n.values())for(let s of r.values())s()}},Dt=W("ignore"),pn=`[${Dt}]`,It=e=>e.hasAttribute(`${Dt}__self`)||!!e.closest(pn),De=(e,t)=>{for(let n of e)if(!It(n)){let r=new Set;for(let s in n.dataset){let i=s.replace(/[A-Z]/g,"-$&").toLowerCase();r.add(i),it(n,i,n.dataset[s],t)}for(let s of Array.from(n.attributes)){if(!s.name.startsWith("data-"))continue;let i=s.name.slice(5);r.has(i)||it(n,i,s.value,t)}}},mn=e=>{for(let{target:t,type:n,attributeName:r,addedNodes:s,removedNodes:i}of e)if(n==="childList"){for(let o of i)z(o)&&(Ht([o]),Ht(o.querySelectorAll("*")));for(let o of s)z(o)&&(De([o]),De(o.querySelectorAll("*")))}else if(n==="attributes"&&r.startsWith("data-")&&z(t)&&!It(t)){let o=r.slice(5),a=nt(o);if(!a)continue;let l=t.getAttribute(r);if(l===null){let c=Ee.get(t);if(c){let u=c.get(a);if(u){for(let f of u.values())f();c.delete(a)}}}else it(t,o,l)}},gn=new MutationObserver(mn),hn=e=>{let[t,...n]=e.split("__"),[r,s]=t.split(/:(.+)/),i=new Map;for(let o of n){let[a,...l]=o.split(".");i.set(a,new Set(l))}return{pluginName:r,key:s,mods:i}},yn=()=>be.has(document.documentElement),vn=()=>{Ft||!yn()||(Ft=!0,document.dispatchEvent(new Event(yt)))},bn=(e=document.documentElement,t=!0)=>{z(e)&&De([e],!0),De(e.querySelectorAll("*"),!0),t&&(gn.observe(e,{subtree:!0,childList:!0,attributes:!0}),be.add(e),vn())};var it=(e,t,n,r)=>{let s=nt(t);if(!s)return;let{pluginName:i,key:o,mods:a}=hn(s),l=rt.get(i);if((!r||st.has(i))&&!!l){let u={el:e,rawKey:s,mods:a,error:ye.bind(0,{plugin:{type:"attribute",name:l.name},element:{id:e.id,tag:e.tagName},expression:{rawKey:s,key:o,value:n}}),key:o,value:n,loadedPluginNames:{actions:new Set(ve.keys()),attributes:new Set(rt.keys())},rx:void 0},f=l.requirement&&(typeof l.requirement=="string"?l.requirement:l.requirement.key)||"allowed",m=l.requirement&&(typeof l.requirement=="string"?l.requirement:l.requirement.value)||"allowed",h=o!=null&&o!=="",v=n!=null&&n!=="";if(h){if(f==="denied")throw u.error("KeyNotAllowed")}else if(f==="must")throw u.error("KeyRequired");if(v){if(m==="denied")throw u.error("ValueNotAllowed")}else if(m==="must")throw u.error("ValueRequired");if(f==="exclusive"||m==="exclusive"){if(h&&v)throw u.error("KeyAndValueProvided");if(!h&&!v)throw u.error("KeyOrValueRequired")}let d=new Map;if(v){let y;u.rx=(...$)=>(y||(y=En(n,{returnsValue:l.returnsValue,argNames:l.argNames,cleanups:d})),y(e,...$))}let g=l.apply(u);g&&d.set("attribute",g);let b=Ee.get(e);if(b){let y=b.get(s);if(y)for(let $ of y.values())$()}else b=new Map,Ee.set(e,b);b.set(s,d)}},En=(e,{returnsValue:t=!1,argNames:n=[],cleanups:r=new Map}={})=>{let s="";if(t){let l=/(\/(\\\/|[^/])*\/|"(\\"|[^"])*"|'(\\'|[^'])*'|`(\\`|[^`])*`|\(\s*((function)\s*\(\s*\)|(\(\s*\))\s*=>)\s*(?:\{[\s\S]*?\}|[^;){]*)\s*\)\s*\(\s*\)|[^;])+/gm,c=e.trim().match(l);if(c){let u=c.length-1,f=c[u].trim();f.startsWith("return")||(c[u]=`return (${f});`),s=c.join(`; -`)}}else s=e.trim();let i=new Map,o=RegExp(`(?:${Je})(.*?)(?:${Ke})`,"gm"),a=0;for(let l of s.matchAll(o)){let c=l[1],u=`__escaped${a++}`;i.set(u,c),s=s.replace(Je+c+Ke,u)}s=s.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\$]|\$(?!\{))*`)|\$\{([^{}]*)\}|\$([a-zA-Z_\d]\w*(?:[.-]\w+)*)/g,(l,c,u,f)=>c?l:u!==void 0?`\${${u.replace(/\$([a-zA-Z_\d]\w*(?:[.-]\w+)*)/g,(m,h)=>h.split(".").reduce((v,d)=>`${v}['${d}']`,"$"))}}`:f.split(".").reduce((m,h)=>`${m}['${h}']`,"$")),s=s.replaceAll(/@([A-Za-z_$][\w$]*)\(/g,'__action("$1",evt,');for(let[l,c]of i)s=s.replace(l,c);try{let l=Function("el","$","__action","evt",...n,s);return(c,...u)=>{let f=(m,h,...v)=>{let d=ye.bind(0,{plugin:{type:"action",name:m},element:{id:c.id,tag:c.tagName},expression:{fnContent:s,value:e}}),g=kt[m];if(g)return g({el:c,evt:h,error:d,cleanups:r},...v);throw d("UndefinedAction")};try{return l(c,ne,f,void 0,...u)}catch(m){throw console.error(m),ye({element:{id:c.id,tag:c.tagName},expression:{fnContent:s,value:e},error:m.message},"ExecuteExpression")}}}catch(l){throw console.error(l),ye({expression:{fnContent:s,value:e},error:l.message},"GenerateExpression")}};D({name:"peek",apply(e,t){H();try{return t()}finally{_()}}});D({name:"setAll",apply(e,t,n){H();let r=V(n);te(r,()=>t),k(r),_()}});D({name:"toggleAll",apply(e,t){H();let n=V(t);te(n,r=>!r),k(n),_()}});var Ie=new Map,ot=e=>!["GET","DELETE"].includes(e),Te=(e,t,n=!0)=>D({name:e,apply:async({el:r,evt:s,error:i,cleanups:o},a,{selector:l,headers:c,contentType:u="json",filterSignals:{include:f=/.*/,exclude:m=/(^|\.)_/}={},openWhenHidden:h=n,payload:v,requestCancellation:d="auto",retry:g="auto",retryInterval:b=1e3,retryScaler:y=2,retryMaxWait:$=3e4,retryMaxCount:O=10}={})=>{let C=d instanceof AbortController?d:new AbortController;(d==="auto"||d==="cleanup")&&(Ie.get(t)?.get(a)?.abort(),Ie.has(t)||Ie.set(t,new Map),Ie.get(t).set(a,C)),d==="cleanup"&&(o.get(`@${e}`)?.(),o.set(`@${e}`,async()=>{C.abort(),await Promise.resolve()}));let de=()=>{};try{if(!a?.length)throw i("FetchNoUrlProvided",{action:D});let ie={Accept:"text/event-stream, text/html, application/json","Datastar-Request":!0};u==="json"&&ot(t)&&(ie["Content-Type"]="application/json");let q=Object.assign({},ie,c),Y={input:"",method:t,headers:q,openWhenHidden:h,retry:g,retryInterval:b,retryScaler:y,retryMaxWait:$,retryMaxCount:O,signal:C.signal,onopen:async E=>{E.status>=400&&re(Sn,r,{status:E.status.toString()})},onmessage:E=>{if(!E.event.startsWith("datastar"))return;let U=E.event,T={};for(let S of E.data.split(` -`)){let A=S.indexOf(" "),X=S.slice(0,A),M=S.slice(A+1);(T[X]||=[]).push(M)}let F=Object.fromEntries(Object.entries(T).map(([S,A])=>[S,A.join(` -`)]));re(U,r,F)},onerror:E=>{if(Vt(E))throw i("FetchExpectedTextEventStream",{url:a})}},Ge=()=>{let E=new URL(a,document.baseURI),U=new URLSearchParams(E.search);if(u==="json"){H();let T=v!==void 0?v:V({include:f,exclude:m});_();let F=JSON.stringify(T);ot(t)?Y.body=F:U.set("datastar",F)}else if(u==="form"){let T=l?document.querySelector(l):r.closest("form");if(!T)throw i("FetchFormNotFound",{action:D,selector:l});if(!T.noValidate&&!T.checkValidity()){T.reportValidity();return}let F=new FormData(T),S=r;if(r===T&&s instanceof SubmitEvent)S=s.submitter;else{let M=pe=>pe.preventDefault();T.addEventListener("submit",M),de=()=>{T.removeEventListener("submit",M)}}if(S instanceof HTMLButtonElement||S instanceof HTMLInputElement&&S.type==="submit"){let M=S.getAttribute("name");M&&F.append(M,S.value)}let A=T.getAttribute("enctype")==="multipart/form-data";A||(q["Content-Type"]="application/x-www-form-urlencoded");let X=new URLSearchParams(F);if(ot(t))A?Y.body=F:Y.body=X;else for(let[M,pe]of X)U.append(M,pe)}else throw i("FetchInvalidContentType",{action:D,contentType:u});return E.search=U.toString(),Y.input=E.toString(),Y};re(at,r,{});try{await Ln(r,Ge)}catch(E){if(!Vt(E))throw i("FetchFailed",{method:t,url:a,error:E.message})}}finally{re(ct,r,{}),de(),o.delete(`@${e}`)}}});Te("get","GET",!1);Te("patch","PATCH");Te("post","POST");Te("put","PUT");Te("delete","DELETE");var at="started",ct="finished",Sn="error",Tn="retrying",An="retries-failed",re=(e,t,n)=>document.dispatchEvent(new CustomEvent(j,{detail:{type:e,el:t,argsRaw:n}})),Vt=e=>`${e}`.includes("text/event-stream"),Rn=async(e,t)=>{let n=e.getReader(),r=await n.read();for(;!r.done;)t(r.value),r=await n.read()},wn=e=>{let t,n,r,s=!1;return i=>{t?t=xn(t,i):(t=i,n=0,r=-1);let o=t.length,a=0;for(;n{let r=$t(),s=new TextDecoder;return(i,o)=>{if(!i.length)n?.(r),r=$t();else if(o>0){let a=s.decode(i.subarray(0,o)),l=o+(i[o+1]===32?2:1),c=s.decode(i.subarray(l));switch(a){case"data":r.data=r.data?`${r.data} -${c}`:c;break;case"event":r.event=c;break;case"id":e(r.id=c);break;case"retry":{let u=+c;Number.isNaN(u)||t(r.retry=u);break}}}}},xn=(e,t)=>{let n=new Uint8Array(e.length+t.length);return n.set(e),n.set(t,e.length),n},$t=()=>({data:"",event:"",id:"",retry:void 0}),Ln=(e,t)=>new Promise((n,r)=>{let s=t();if(!s)return;let{input:i,signal:o,headers:a,onopen:l,onmessage:c,onclose:u,openWhenHidden:f,fetch:m,retry:h="auto",retryInterval:v=1e3,retryScaler:d=2,retryMaxWait:g=3e4,retryMaxCount:b=10,responseOverrides:y,...$}=s,O={...a},C,de=()=>{if(C.abort(),!document.hidden){let S=t();if(!S)return;i=S.input,$.body=S.body,F()}};f||document.addEventListener("visibilitychange",de);let ie,q=()=>{document.removeEventListener("visibilitychange",de),clearTimeout(ie),C.abort()};o?.addEventListener("abort",()=>{q(),n()});let Y=m||window.fetch,Ge=l||(()=>{}),E=0,U=v,T=()=>{E{C=new AbortController;let S=C.signal;try{let A=await Y(i,{...$,headers:O,signal:S});await Ge(A);let X=async(G,me,Be,we,...on)=>{let gt={[Be]:await me.text()};for(let We of on){let Ue=me.headers.get(`datastar-${ae(We)}`);if(we){let Me=we[We];Me&&(Ue=typeof Me=="string"?Me:JSON.stringify(Me))}Ue&&(gt[We]=Ue)}re(G,e,gt),q(),n()},M=A.status,pe=M===204,mt=M>=300&&M<400,sn=M>=400&&M<600;if(M!==200){if(u?.(),h!=="never"&&!pe&&!mt&&(h==="always"||h==="error"&&sn)){T();return}q(),n();return}E=0,v=U;let je=A.headers.get("Content-Type");if(je?.includes("text/html"))return await X("datastar-patch-elements",A,"elements",y,"selector","mode","namespace","useViewTransition");if(je?.includes("application/json"))return await X("datastar-patch-signals",A,"signals",y,"onlyIfMissing");if(je?.includes("text/javascript")){let G=document.createElement("script"),me=A.headers.get("datastar-script-attributes");if(me)for(let[Be,we]of Object.entries(JSON.parse(me)))G.setAttribute(Be,we);G.textContent=await A.text(),document.head.appendChild(G),q();return}if(await Rn(A.body,wn(Mn(G=>{G?O["last-event-id"]=G:delete O["last-event-id"]},G=>{U=v=G},c))),u?.(),h==="always"&&!mt){T();return}q(),n()}catch{if(!S.aborted)try{T()}catch(A){q(),r(A)}}};F()});p({name:"attr",requirement:{value:"must"},returnsValue:!0,apply({el:e,key:t,rx:n}){let r=(a,l)=>{l===""||l===!0?e.setAttribute(a,""):l===!1||l==null?e.removeAttribute(a):typeof l=="string"?e.setAttribute(a,l):typeof l=="function"?e.setAttribute(a,l.toString()):e.setAttribute(a,JSON.stringify(l,(c,u)=>typeof u=="function"?u.toString():u))},s=t?()=>{i.disconnect();let a=n();r(t,a),i.observe(e,{attributeFilter:[t]})}:()=>{i.disconnect();let a=n(),l=Object.keys(a);for(let c of l)r(c,a[c]);i.observe(e,{attributeFilter:l})},i=new MutationObserver(s),o=w(s);return()=>{i.disconnect(),o()}}});var Ve=(e,...t)=>({get:n=>n[e],set:(n,r)=>{n[e]=r},events:t}),qt=(e,...t)=>({get:n=>n.getAttribute(e),set:(n,r)=>{n.setAttribute(e,`${r}`)},events:t}),lt=(e=!1,...t)=>({get:(n,r)=>r==="string"||e&&r==="undefined"?n.value:+n.value,set:(n,r)=>{n.value=`${r}`},events:t}),Nn=/^data:(?[^;]+);base64,(?.*)$/,Gt=Symbol("empty"),Pn=(e,t,n,r,s,i)=>{let o=W(CSS.escape(n)),a=t?`[${o}]`:`[${o}="${CSS.escape(r)}"]`;if(i===void 0&&e instanceof HTMLInputElement&&e.type==="radio"){let f=[...document.querySelectorAll(a)].find(m=>m instanceof HTMLInputElement&&m.checked);f&&R([[r,f.value]],{ifMissing:!0})}if(!Array.isArray(i)||e instanceof HTMLSelectElement&&e.multiple)return R([[r,s.get(e,typeof i)]],{ifMissing:!0}),r;let l=document.querySelectorAll(a),c=[],u=0;for(let f of l){if(c.push([`${r}.${u}`,s.get(f,typeof(x(i,u)?i[u]:void 0))]),e===f)break;u++}return R(c,{ifMissing:!0}),`${r}.${u}`};p({name:"bind",requirement:"exclusive",apply({el:e,key:t,rawKey:n,mods:r,value:s,error:i}){let o=t!=null?P(t,r):s,a=r.get("prop"),l=r.get("event"),c=null;if(e instanceof HTMLInputElement)switch(e.type){case"range":case"number":c=lt(!1,"input");break;case"checkbox":c={get:(d,g)=>d.value!=="on"?g==="boolean"?d.checked:d.checked?d.value:"":g==="string"?d.checked?d.value:"":d.checked,set:(d,g)=>{d.checked=typeof g=="string"?g===d.value:g},events:["input"]};break;case"radio":e.getAttribute("name")?.length||e.setAttribute("name",o),c={get:(d,g)=>d.checked?g==="number"?+d.value:d.value:Gt,set:(d,g)=>{d.checked=g===(typeof g=="number"?+d.value:d.value)},events:["input"]};break;case"file":{let d=()=>{let g=[...e.files||[]],b=[];Promise.all(g.map(y=>new Promise($=>{let O=new FileReader;O.onload=()=>{if(typeof O.result!="string")throw i("InvalidFileResultType",{resultType:typeof O.result});let C=O.result.match(Nn);if(!C?.groups)throw i("InvalidDataUri",{result:O.result});b.push({name:y.name,contents:C.groups.contents,mime:C.groups.mime})},O.onloadend=()=>$(),O.readAsDataURL(y)}))).then(()=>{R([[o,b]])})};return e.addEventListener("change",d),()=>{e.removeEventListener("change",d)}}default:c=lt(!0,"input")}else if(e instanceof HTMLSelectElement&&e.multiple){let d=new Map;c={get:g=>[...g.selectedOptions].map(b=>{let y=d.get(b.value);return y==="string"||y==null?b.value:+b.value}),set:(g,b)=>{for(let y of g.options)b.includes(y.value)?(d.set(y.value,"string"),y.selected=!0):b.includes(+y.value)?(d.set(y.value,"number"),y.selected=!0):y.selected=!1},events:["change"]}}else e instanceof HTMLSelectElement?c=lt(!0,"change"):e instanceof HTMLTextAreaElement?c=Ve("value","input"):e instanceof HTMLElement&&e.tagName.includes("-")?c="value"in e?Ve("value","input","change"):qt("value","input","change"):e instanceof HTMLElement&&"value"in e?c=Ve("value","change"):c=qt("value","change");if(!c)throw i("InvalidBindAdapter");let u=a&&[...a][0];if(a&&!u)throw i("BindPropNameMissing");if(u){let d=Pt(u);c=Ve(d,...l?[...l]:c.events)}else l&&(c.events=[...l]);let f=oe(o),m=Pn(e,t,n,o,c,f),h=()=>{let d=oe(m);if(d!=null){let g=c.get(e,typeof d);g!==Gt&&R([[m,g]])}};for(let d of c.events)e.addEventListener(d,h);e.addEventListener(ge,h);let v=w(()=>{c.set(e,oe(m))});return()=>{v();for(let d of c.events)e.removeEventListener(d,h);e.removeEventListener(ge,h)}}});p({name:"class",requirement:{value:"must"},returnsValue:!0,apply({key:e,el:t,mods:n,rx:r}){e&&=P(e,n,"kebab");let s,i=()=>{o.disconnect(),s=e?{[e]:r()}:r();for(let l in s){let c=l.split(/\s+/).filter(u=>u.length>0);if(s[l])for(let u of c)t.classList.contains(u)||t.classList.add(u);else for(let u of c)t.classList.contains(u)&&t.classList.remove(u)}o.observe(t,{attributeFilter:["class"]})},o=new MutationObserver(i),a=w(i);return()=>{o.disconnect(),a();for(let l in s){let c=l.split(/\s+/).filter(u=>u.length>0);for(let u of c)t.classList.remove(u)}}}});p({name:"computed",requirement:{value:"must"},returnsValue:!0,apply({key:e,mods:t,rx:n,error:r}){if(e)R([[P(e,t),_e(n)]]);else{let s=Object.assign({},n());te(s,i=>{if(typeof i=="function")return _e(i);throw r("ComputedExpectedFunction")}),k(s)}}});p({name:"effect",requirement:{key:"denied",value:"must"},apply:({rx:e})=>w(e)});p({name:"indicator",requirement:"exclusive",apply({el:e,key:t,mods:n,value:r}){let s=t!=null?P(t,n):r,i=0;R([[s,!1]]);let o=a=>{let{type:l,el:c}=a.detail;if(c===e)switch(l){case at:i++,R([[s,!0]]);break;case ct:i=Math.max(0,i-1),R([[s,i>0]]);break}};return document.addEventListener(j,o),()=>{i=0,R([[s,!1]]),document.removeEventListener(j,o)}}});var Z=e=>{if(!e||e.size<=0)return 0;for(let t of e){if(t.endsWith("ms"))return+t.replace("ms","");if(t.endsWith("s"))return+t.replace("s","")*1e3;try{return Number.parseFloat(t)}catch{}}return 0},se=(e,t,n=!1)=>e?e.has(t.toLowerCase()):n,jt=(e,t="")=>{if(e&&e.size>0)for(let n of e)return n;return t};var ut=(e,t)=>(...n)=>{setTimeout(()=>{e(...n)},t)},Bt=(e,t,n=!0,r=!1,s=!1)=>{let i=null,o=0;return(...a)=>{n&&!o?(e(...a),i=null):i=a,(!o||s)&&(o&&clearTimeout(o),o=setTimeout(()=>{r&&i!==null&&e(...i),i=null,o=0},t))}},le=(e,t)=>{let n=t.get("delay");if(n){let i=Z(n);e=ut(e,i)}let r=t.get("debounce");if(r){let i=Z(r),o=se(r,"leading",!1),a=!se(r,"notrailing",!1);e=Bt(e,i,o,a,!0)}let s=t.get("throttle");if(s){let i=Z(s),o=!se(s,"noleading",!1),a=se(s,"trailing",!1);e=Bt(e,i,o,a)}return e};var $e=(e=document.documentElement)=>"startViewTransition"in e,Q=(e,t)=>{if(t.has("viewtransition")&&$e()){let n=e;e=(...r)=>document.startViewTransition(()=>n(...r))}return e};p({name:"init",requirement:{key:"denied",value:"must"},apply({rx:e,mods:t}){let n=()=>{L(),e(),N()};n=Q(n,t);let r=0,s=t.get("delay");s&&(r=Z(s),r>0&&(n=ut(n,r))),n()}});p({name:"json-signals",requirement:{key:"denied"},apply({el:e,value:t,mods:n}){let r=n.has("terse")?0:2,s={};t&&(s=ce(t));let i=()=>{o.disconnect(),e.textContent=JSON.stringify(V(s),null,r),o.observe(e,{childList:!0,characterData:!0,subtree:!0})},o=new MutationObserver(i),a=w(i);return()=>{o.disconnect(),a()}}});p({name:"on",requirement:"must",argNames:["evt"],apply({el:e,key:t,mods:n,rx:r}){let s=e;n.has("window")?s=window:n.has("document")&&(s=document);let i=c=>{L(),r(c),N()};i=Q(i,n),i=le(i,n);let o=P(t,n,"kebab"),a={capture:n.has("capture"),passive:n.has("passive"),once:n.has("once")};if(n.has("outside")){s=document;let c=i;i=u=>{e.contains(u?.target)||c(u)}}(o===j||o===ee)&&(s=document);let l=c=>{c&&(n.has("prevent")&&c.preventDefault(),n.has("stop")&&c.stopPropagation(),e instanceof HTMLFormElement&&o==="submit"&&c.preventDefault()),i(c)};return s.addEventListener(o,l,a),()=>{s.removeEventListener(o,l,a)}}});var Wt=(e,t,n)=>Math.max(t,Math.min(n,e));var ft=new WeakSet;p({name:"on-intersect",requirement:{key:"denied",value:"must"},apply({el:e,mods:t,rx:n}){let r=()=>{L(),n(),N()};r=Q(r,t),r=le(r,t);let s={threshold:0};if(t.has("full"))s.threshold=1;else if(t.has("half"))s.threshold=.5;else{let a=t.get("threshold");a&&(s.threshold=Wt(Number(jt(a)),0,100)/100)}let i=t.has("exit"),o=new IntersectionObserver(a=>{for(let l of a)l.isIntersecting!==i&&(r(),o&&ft.has(e)&&o.disconnect())},s);return o.observe(e),t.has("once")&&ft.add(e),()=>{t.has("once")||ft.delete(e),o&&(o.disconnect(),o=null)}}});p({name:"on-interval",requirement:{key:"denied",value:"must"},apply({mods:e,rx:t}){let n=()=>{L(),t(),N()};n=Q(n,e);let r=1e3,s=e.get("duration");s&&(r=Z(s),se(s,"leading",!1)&&n());let i=setInterval(n,r);return()=>{clearInterval(i)}}});p({name:"on-signal-patch",requirement:{value:"must"},argNames:["patch"],returnsValue:!0,apply({el:e,key:t,mods:n,rx:r,error:s}){if(t&&t!=="filter")throw s("KeyNotAllowed");let i=W(`${this.name}-filter`),o=e.getAttribute(i),a={};o&&(a=ce(o));let l=!1,c=le(u=>{if(l)return;let f=V(a,u.detail);if(!vt(f)){l=!0,L();try{r(f)}finally{N(),l=!1}}},n);return document.addEventListener(ee,c),()=>{document.removeEventListener(ee,c)}}});p({name:"ref",requirement:"exclusive",apply({el:e,key:t,mods:n,value:r}){let s=t!=null?P(t,n):r;R([[s,e]])}});var Ut="none",Jt="display";p({name:"show",requirement:{key:"denied",value:"must"},returnsValue:!0,apply({el:e,rx:t}){let n=()=>{r.disconnect(),t()?e.style.display===Ut&&e.style.removeProperty(Jt):e.style.setProperty(Jt,Ut),r.observe(e,{attributeFilter:["style"]})},r=new MutationObserver(n),s=w(n);return()=>{r.disconnect(),s()}}});p({name:"signals",returnsValue:!0,apply({key:e,mods:t,rx:n}){let r=t.has("ifmissing");if(e){e=P(e,t);let s=n?.();R([[e,s]],{ifMissing:r})}else{let s=Object.assign({},n?.());k(s,{ifMissing:r})}}});p({name:"style",requirement:{value:"must"},returnsValue:!0,apply({key:e,el:t,rx:n}){let{style:r}=t,s=new Map,i=(c,u)=>{let f=s.get(c);!u&&u!==0?f!==void 0&&(f?r.setProperty(c,f):r.removeProperty(c)):(f===void 0&&s.set(c,r.getPropertyValue(c)),r.setProperty(c,String(u)))},o=()=>{if(a.disconnect(),e)i(e,n());else{let c=n();for(let[u,f]of s)u in c||(f?r.setProperty(u,f):r.removeProperty(u));for(let u in c)i(ae(u),c[u])}a.observe(t,{attributeFilter:["style"]})},a=new MutationObserver(o),l=w(o);return()=>{a.disconnect(),l();for(let[c,u]of s)u?r.setProperty(c,u):r.removeProperty(c)}}});p({name:"text",requirement:{key:"denied",value:"must"},returnsValue:!0,apply({el:e,rx:t}){let n=()=>{r.disconnect(),e.textContent=`${t()}`,r.observe(e,{childList:!0,characterData:!0,subtree:!0})},r=new MutationObserver(n),s=w(n);return()=>{r.disconnect(),s()}}});var Kt=(e,t)=>e.includes(t),On=["remove","outer","inner","replace","prepend","append","before","after"],Cn=["html","svg","mathml"];Se({name:"datastar-patch-elements",apply(e,t){let n=typeof t.selector=="string"?t.selector:"",r=typeof t.mode=="string"?t.mode:"outer",s=typeof t.namespace=="string"?t.namespace:"html",i=typeof t.useViewTransition=="string"&&t.useViewTransition.trim()==="true",o=typeof t.viewTransitionSelector=="string"?t.viewTransitionSelector:"",a=t.elements;if(!Kt(On,r))throw e.error("PatchElementsInvalidMode",{mode:r});if(!n&&r!=="outer"&&r!=="replace")throw e.error("PatchElementsExpectedSelector");if(!Kt(Cn,s))throw e.error("PatchElementsInvalidNamespace",{namespace:s});let l={selector:n,mode:r,namespace:s,elements:a};if(i&&$e()){let c=document.documentElement;if(o){let u=document.querySelector(o);u&&$e(u)&&(c=u)}c.startViewTransition(()=>zt(e,l))}else zt(e,l)}});var zt=({error:e},{selector:t,mode:n,namespace:r,elements:s})=>{let i=document.createDocumentFragment(),o=typeof s!="string"&&!!s;if(typeof s=="string"){let a=s.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,""),l=/<\/html>/.test(a),c=/<\/head>/.test(a),u=/<\/body>/.test(a),f=r==="svg"?"svg":r==="mathml"?"math":"",m=f?`<${f}>${s}`:s,h=new DOMParser().parseFromString(l||c||u?s:``,"text/html");if(l)i.appendChild(h.documentElement);else if(c&&u)i.appendChild(h.head),i.appendChild(h.body);else if(c)i.appendChild(h.head);else if(u)i.appendChild(h.body);else if(f){let v=h.querySelector("template").content.querySelector(f);for(let d of v.childNodes)i.appendChild(d)}else i=h.querySelector("template").content}else s&&(s instanceof DocumentFragment?i=s:s instanceof Element&&i.appendChild(s));if(!t&&(n==="outer"||n==="replace")){let a=Array.from(i.children);for(let l of a){let c;if(l instanceof HTMLHtmlElement)c=document.documentElement;else if(l instanceof HTMLBodyElement)c=document.body;else if(l instanceof HTMLHeadElement)c=document.head;else if(c=document.getElementById(l.id),!c){console.warn(e("PatchElementsNoTargetsFound"),{element:{id:l.id}});continue}Qt(n,l,[c],!0)}}else{let a=document.querySelectorAll(t);if(!a.length){console.warn(e("PatchElementsNoTargetsFound"),{selector:t});return}let l=o&&n!=="remove"?[a[0]]:a;l.length===1&&(o=!0),Qt(n,i,l,o)}},pt=new WeakSet;for(let e of document.querySelectorAll("script"))pt.add(e);var tn=e=>{let t=e instanceof HTMLScriptElement?[e]:e.querySelectorAll("script");for(let n of t)if(!pt.has(n)){let r=document.createElement("script");for(let{name:s,value:i}of n.attributes)r.setAttribute(s,i);r.text=n.text,n.replaceWith(r),pt.add(r)}},Zt=(e,t,n,r)=>{let s=!1;for(let i of e){if(r&&s)break;let o=r?t:t.cloneNode(!0);tn(o),i[n](o),s=!0}},Qt=(e,t,n,r)=>{switch(e){case"remove":for(let s of n)s.remove();break;case"outer":case"inner":{let s=!1;for(let i of n){if(r&&s)break;let o=r?t:t.cloneNode(!0);Hn(i,o,e),tn(i);let a=i.closest("[data-scope-children]");a&&a.dispatchEvent(new CustomEvent(ze,{bubbles:!1})),s=!0}}break;case"replace":Zt(n,t,"replaceWith",r);break;case"prepend":case"append":case"before":case"after":Zt(n,t,e,r)}},I=new Map,fe=new Set,ue=new Map,Ae=new Set,qe=document.createElement("div");qe.hidden=!0;var Re=W("ignore-morph"),Fn=`[${Re}]`,Hn=(e,t,n="outer")=>{if(z(e)&&z(t)&&e.hasAttribute(Re)&&t.hasAttribute(Re)||e.parentElement?.closest(Fn))return;let r=document.createElement("div");r.append(t),document.body.insertAdjacentElement("afterend",qe);let s=e.querySelectorAll("[id]");for(let{id:a,tagName:l}of s)ue.has(a)?Ae.add(a):ue.set(a,l);e instanceof Element&&e.id&&(ue.has(e.id)?Ae.add(e.id):ue.set(e.id,e.tagName)),fe.clear();let i=r.querySelectorAll("[id]");for(let{id:a,tagName:l}of i)fe.has(a)?Ae.add(a):ue.get(a)===l&&fe.add(a);for(let a of Ae)fe.delete(a);ue.clear(),Ae.clear(),I.clear();let o=n==="outer"?e.parentElement:e;en(o,s),en(r,i),nn(o,r,n==="outer"?e:null,e.nextSibling),qe.remove()},nn=(e,t,n=null,r=null)=>{e instanceof HTMLTemplateElement&&t instanceof HTMLTemplateElement&&(e=e.content,t=t.content),n??=e.firstChild;for(let s of t.childNodes){if(n&&n!==r){let i=_n(s,n,r);if(i){if(i!==n){let o=n;for(;o&&o!==i;){let a=o;o=o.nextSibling,Xt(a)}}dt(i,s),n=i.nextSibling;continue}}if(s instanceof Element&&fe.has(s.id)){let i=document.getElementById(s.id),o=i;for(;o=o.parentNode;){let a=I.get(o);a&&(a.delete(s.id),a.size||I.delete(o))}rn(e,i,n),dt(i,s),n=i.nextSibling;continue}if(I.has(s)){let i=s.namespaceURI,o=s.tagName,a=i&&i!=="http://www.w3.org/1999/xhtml"?document.createElementNS(i,o):document.createElement(o);e.insertBefore(a,n),dt(a,s),n=a.nextSibling}else{let i=document.importNode(s,!0);e.insertBefore(i,n),n=i.nextSibling}}for(;n&&n!==r;){let s=n;n=n.nextSibling,Xt(s)}},_n=(e,t,n)=>{let r=null,s=e.nextSibling,i=0,o=0,a=I.get(e)?.size||0,l=t;for(;l&&l!==n;){if(Yt(l,e)){let c=!1,u=I.get(l),f=I.get(e);if(f&&u){for(let m of u)if(f.has(m)){c=!0;break}}if(c)return l;if(!r&&!I.has(l)){if(!a)return l;r=l}}if(o+=I.get(l)?.size||0,o>a)break;r===null&&s&&Yt(l,s)&&(i++,s=s.nextSibling,i>=2&&(r=void 0)),l=l.nextSibling}return r||null},Yt=(e,t)=>e.nodeType===t.nodeType&&e.tagName===t.tagName&&(!e.id||e.id===t.id),Xt=e=>{I.has(e)?rn(qe,e,null):e.parentNode?.removeChild(e)},rn=(e,t,n)=>{if("moveBefore"in e){e.moveBefore(t,n);return}e.insertBefore(t,n)},kn=W("preserve-attr"),dt=(e,t)=>{let n=t.nodeType;if(n===1){let r=e,s=t,i=r.hasAttribute("data-scope-children");if(r.hasAttribute(Re)&&s.hasAttribute(Re))return e;let o=(t.getAttribute(kn)??"").split(" "),a=(c,u,f)=>{let m=u.hasAttribute(f);return c.hasAttribute(f)!==m&&!o.includes(f)?(c[f]=m,!0):!1},l=!1;if(r instanceof HTMLInputElement&&s instanceof HTMLInputElement&&s.type!=="file"){let c=s.getAttribute("value");r.getAttribute("value")!==c&&!o.includes("value")&&(r.value=c??"",l=!0),l=a(r,s,"checked")||l,a(r,s,"disabled")}else if(r instanceof HTMLTextAreaElement&&s instanceof HTMLTextAreaElement){let c=s.value;r.defaultValue!==c&&(r.value=c,l=!0)}else r instanceof HTMLOptionElement&&s instanceof HTMLOptionElement&&(l=a(r,s,"selected")||l);for(let{name:c,value:u}of s.attributes)r.getAttribute(c)!==u&&!o.includes(c)&&r.setAttribute(c,u);for(let{name:c}of Array.from(r.attributes))!s.hasAttribute(c)&&!o.includes(c)&&r.removeAttribute(c);l&&(r instanceof HTMLOptionElement?r.closest("select"):r)?.dispatchEvent(new Event(ge,{bubbles:!0})),i&&!r.hasAttribute("data-scope-children")&&r.setAttribute("data-scope-children",""),r instanceof HTMLTemplateElement&&s instanceof HTMLTemplateElement?r.innerHTML=s.innerHTML:r.isEqualNode(s)||nn(r,s),i&&r.dispatchEvent(new CustomEvent(ze,{bubbles:!1}))}return(n===8||n===3)&&e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e},en=(e,t)=>{for(let n of t)if(fe.has(n.id)){let r=n;for(;r&&r!==e;){let s=I.get(r);s||(s=new Set,I.set(r,s)),s.add(n.id),r=r.parentElement}}};Se({name:"datastar-patch-signals",apply({error:e},{signals:t,onlyIfMissing:n}){if(typeof t!="string")throw e("PatchSignalsExpectedSignals");let r=typeof n=="string"&&n.trim()==="true";k(ce(t),{ifMissing:r})}});export{D as action,kt as actions,p as attribute,L as beginBatch,_e as computed,w as effect,N as endBatch,V as filtered,oe as getPath,k as mergePatch,R as mergePaths,ne as root,he as signal,H as startPeeking,_ as stopPeeking,Se as watcher}; -//# sourceMappingURL=datastar.js.map diff --git a/static/vendor/font-awesome/css/all.min.css b/static/vendor/font-awesome/css/all.min.css new file mode 100644 index 0000000..0a2cf52 --- /dev/null +++ b/static/vendor/font-awesome/css/all.min.css @@ -0,0 +1,6 @@ +/*! + * Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2022 Fonticons, Inc. + */ +.fa{font-family:var(--fa-style-family,"Font Awesome 6 Free");font-weight:var(--fa-style,900)}.fa,.fa-brands,.fa-duotone,.fa-light,.fa-regular,.fa-solid,.fa-thin,.fab,.fad,.fal,.far,.fas,.fat{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:var(--fa-display,inline-block);font-style:normal;font-variant:normal;line-height:1;text-rendering:auto}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0);animation-delay:var(--fa-animation-delay,0);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;transition-delay:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%;z-index:var(--fa-stack-z-index,auto)}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:var(--fa-inverse,#fff)}.fa-0:before{content:"\30"}.fa-1:before{content:"\31"}.fa-2:before{content:"\32"}.fa-3:before{content:"\33"}.fa-4:before{content:"\34"}.fa-5:before{content:"\35"}.fa-6:before{content:"\36"}.fa-7:before{content:"\37"}.fa-8:before{content:"\38"}.fa-9:before{content:"\39"}.fa-a:before{content:"\41"}.fa-address-book:before,.fa-contact-book:before{content:"\f2b9"}.fa-address-card:before,.fa-contact-card:before,.fa-vcard:before{content:"\f2bb"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-anchor:before{content:"\f13d"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-double-down:before,.fa-angles-down:before{content:"\f103"}.fa-angle-double-left:before,.fa-angles-left:before{content:"\f100"}.fa-angle-double-right:before,.fa-angles-right:before{content:"\f101"}.fa-angle-double-up:before,.fa-angles-up:before{content:"\f102"}.fa-ankh:before{content:"\f644"}.fa-apple-alt:before,.fa-apple-whole:before{content:"\f5d1"}.fa-archway:before{content:"\f557"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-down-1-9:before,.fa-sort-numeric-asc:before,.fa-sort-numeric-down:before{content:"\f162"}.fa-arrow-down-9-1:before,.fa-sort-numeric-desc:before,.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-arrow-down-a-z:before,.fa-sort-alpha-asc:before,.fa-sort-alpha-down:before{content:"\f15d"}.fa-arrow-down-long:before,.fa-long-arrow-down:before{content:"\f175"}.fa-arrow-down-short-wide:before,.fa-sort-amount-desc:before,.fa-sort-amount-down-alt:before{content:"\f884"}.fa-arrow-down-wide-short:before,.fa-sort-amount-asc:before,.fa-sort-amount-down:before{content:"\f160"}.fa-arrow-down-z-a:before,.fa-sort-alpha-desc:before,.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-left-long:before,.fa-long-arrow-left:before{content:"\f177"}.fa-arrow-pointer:before,.fa-mouse-pointer:before{content:"\f245"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-right-arrow-left:before,.fa-exchange:before{content:"\f0ec"}.fa-arrow-right-from-bracket:before,.fa-sign-out:before{content:"\f08b"}.fa-arrow-right-long:before,.fa-long-arrow-right:before{content:"\f178"}.fa-arrow-right-to-bracket:before,.fa-sign-in:before{content:"\f090"}.fa-arrow-left-rotate:before,.fa-arrow-rotate-back:before,.fa-arrow-rotate-backward:before,.fa-arrow-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-arrow-right-rotate:before,.fa-arrow-rotate-forward:before,.fa-arrow-rotate-right:before,.fa-redo:before{content:"\f01e"}.fa-arrow-trend-down:before{content:"\e097"}.fa-arrow-trend-up:before{content:"\e098"}.fa-arrow-turn-down:before,.fa-level-down:before{content:"\f149"}.fa-arrow-turn-up:before,.fa-level-up:before{content:"\f148"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-up-1-9:before,.fa-sort-numeric-up:before{content:"\f163"}.fa-arrow-up-9-1:before,.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-arrow-up-a-z:before,.fa-sort-alpha-up:before{content:"\f15e"}.fa-arrow-up-from-bracket:before{content:"\e09a"}.fa-arrow-up-long:before,.fa-long-arrow-up:before{content:"\f176"}.fa-arrow-up-right-from-square:before,.fa-external-link:before{content:"\f08e"}.fa-arrow-up-short-wide:before,.fa-sort-amount-up-alt:before{content:"\f885"}.fa-arrow-up-wide-short:before,.fa-sort-amount-up:before{content:"\f161"}.fa-arrow-up-z-a:before,.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-arrows-h:before,.fa-arrows-left-right:before{content:"\f07e"}.fa-arrows-rotate:before,.fa-refresh:before,.fa-sync:before{content:"\f021"}.fa-arrows-up-down:before,.fa-arrows-v:before{content:"\f07d"}.fa-arrows-up-down-left-right:before,.fa-arrows:before{content:"\f047"}.fa-asterisk:before{content:"\2a"}.fa-at:before{content:"\40"}.fa-atom:before{content:"\f5d2"}.fa-audio-description:before{content:"\f29e"}.fa-austral-sign:before{content:"\e0a9"}.fa-award:before{content:"\f559"}.fa-b:before{content:"\42"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before,.fa-carriage-baby:before{content:"\f77d"}.fa-backward:before{content:"\f04a"}.fa-backward-fast:before,.fa-fast-backward:before{content:"\f049"}.fa-backward-step:before,.fa-step-backward:before{content:"\f048"}.fa-bacon:before{content:"\f7e5"}.fa-bacteria:before{content:"\e059"}.fa-bacterium:before{content:"\e05a"}.fa-bag-shopping:before,.fa-shopping-bag:before{content:"\f290"}.fa-bahai:before{content:"\f666"}.fa-baht-sign:before{content:"\e0ac"}.fa-ban:before,.fa-cancel:before{content:"\f05e"}.fa-ban-smoking:before,.fa-smoking-ban:before{content:"\f54d"}.fa-band-aid:before,.fa-bandage:before{content:"\f462"}.fa-barcode:before{content:"\f02a"}.fa-bars:before,.fa-navicon:before{content:"\f0c9"}.fa-bars-progress:before,.fa-tasks-alt:before{content:"\f828"}.fa-bars-staggered:before,.fa-reorder:before,.fa-stream:before{content:"\f550"}.fa-baseball-ball:before,.fa-baseball:before{content:"\f433"}.fa-baseball-bat-ball:before{content:"\f432"}.fa-basket-shopping:before,.fa-shopping-basket:before{content:"\f291"}.fa-basketball-ball:before,.fa-basketball:before{content:"\f434"}.fa-bath:before,.fa-bathtub:before{content:"\f2cd"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-battery-5:before,.fa-battery-full:before,.fa-battery:before{content:"\f240"}.fa-battery-3:before,.fa-battery-half:before{content:"\f242"}.fa-battery-2:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-4:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-bed:before{content:"\f236"}.fa-bed-pulse:before,.fa-procedures:before{content:"\f487"}.fa-beer-mug-empty:before,.fa-beer:before{content:"\f0fc"}.fa-bell:before{content:"\f0f3"}.fa-bell-concierge:before,.fa-concierge-bell:before{content:"\f562"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bicycle:before{content:"\f206"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-bitcoin-sign:before{content:"\e0b4"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blog:before{content:"\f781"}.fa-bold:before{content:"\f032"}.fa-bolt:before,.fa-zap:before{content:"\f0e7"}.fa-bolt-lightning:before{content:"\e0b7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-atlas:before,.fa-book-atlas:before{content:"\f558"}.fa-bible:before,.fa-book-bible:before{content:"\f647"}.fa-book-journal-whills:before,.fa-journal-whills:before{content:"\f66a"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-open-reader:before,.fa-book-reader:before{content:"\f5da"}.fa-book-quran:before,.fa-quran:before{content:"\f687"}.fa-book-dead:before,.fa-book-skull:before{content:"\f6b7"}.fa-bookmark:before{content:"\f02e"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before,.fa-border-top-left:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-archive:before,.fa-box-archive:before{content:"\f187"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\e05b"}.fa-boxes-alt:before,.fa-boxes-stacked:before,.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-brazilian-real-sign:before{content:"\e46c"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broom:before{content:"\f51a"}.fa-broom-ball:before,.fa-quidditch-broom-ball:before,.fa-quidditch:before{content:"\f458"}.fa-brush:before{content:"\f55d"}.fa-bug:before{content:"\f188"}.fa-bug-slash:before{content:"\e490"}.fa-building:before{content:"\f1ad"}.fa-bank:before,.fa-building-columns:before,.fa-institution:before,.fa-museum:before,.fa-university:before{content:"\f19c"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burger:before,.fa-hamburger:before{content:"\f805"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before,.fa-bus-simple:before{content:"\f55e"}.fa-briefcase-clock:before,.fa-business-time:before{content:"\f64a"}.fa-c:before{content:"\43"}.fa-birthday-cake:before,.fa-cake-candles:before,.fa-cake:before{content:"\f1fd"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-alt:before,.fa-calendar-days:before{content:"\f073"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-week:before{content:"\f784"}.fa-calendar-times:before,.fa-calendar-xmark:before{content:"\f273"}.fa-camera-alt:before,.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-camera-rotate:before{content:"\e0d8"}.fa-campground:before{content:"\f6bb"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-battery-car:before,.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-alt:before,.fa-car-rear:before{content:"\f5de"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-flatbed:before,.fa-dolly-flatbed:before{content:"\f474"}.fa-cart-flatbed-suitcase:before,.fa-luggage-cart:before{content:"\f59d"}.fa-cart-plus:before{content:"\f217"}.fa-cart-shopping:before,.fa-shopping-cart:before{content:"\f07a"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cedi-sign:before{content:"\e0df"}.fa-cent-sign:before{content:"\e3f5"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-blackboard:before,.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before,.fa-chalkboard-user:before{content:"\f51c"}.fa-champagne-glasses:before,.fa-glass-cheers:before{content:"\f79f"}.fa-charging-station:before{content:"\f5e7"}.fa-area-chart:before,.fa-chart-area:before{content:"\f1fe"}.fa-bar-chart:before,.fa-chart-bar:before{content:"\f080"}.fa-chart-column:before{content:"\e0e3"}.fa-chart-gantt:before{content:"\e0e4"}.fa-chart-line:before,.fa-line-chart:before{content:"\f201"}.fa-chart-pie:before,.fa-pie-chart:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-double:before{content:"\f560"}.fa-check-to-slot:before,.fa-vote-yea:before{content:"\f772"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-arrow-circle-down:before,.fa-circle-arrow-down:before{content:"\f0ab"}.fa-arrow-circle-left:before,.fa-circle-arrow-left:before{content:"\f0a8"}.fa-arrow-circle-right:before,.fa-circle-arrow-right:before{content:"\f0a9"}.fa-arrow-circle-up:before,.fa-circle-arrow-up:before{content:"\f0aa"}.fa-check-circle:before,.fa-circle-check:before{content:"\f058"}.fa-chevron-circle-down:before,.fa-circle-chevron-down:before{content:"\f13a"}.fa-chevron-circle-left:before,.fa-circle-chevron-left:before{content:"\f137"}.fa-chevron-circle-right:before,.fa-circle-chevron-right:before{content:"\f138"}.fa-chevron-circle-up:before,.fa-circle-chevron-up:before{content:"\f139"}.fa-circle-dollar-to-slot:before,.fa-donate:before{content:"\f4b9"}.fa-circle-dot:before,.fa-dot-circle:before{content:"\f192"}.fa-arrow-alt-circle-down:before,.fa-circle-down:before{content:"\f358"}.fa-circle-exclamation:before,.fa-exclamation-circle:before{content:"\f06a"}.fa-circle-h:before,.fa-hospital-symbol:before{content:"\f47e"}.fa-adjust:before,.fa-circle-half-stroke:before{content:"\f042"}.fa-circle-info:before,.fa-info-circle:before{content:"\f05a"}.fa-arrow-alt-circle-left:before,.fa-circle-left:before{content:"\f359"}.fa-circle-minus:before,.fa-minus-circle:before{content:"\f056"}.fa-circle-notch:before{content:"\f1ce"}.fa-circle-pause:before,.fa-pause-circle:before{content:"\f28b"}.fa-circle-play:before,.fa-play-circle:before{content:"\f144"}.fa-circle-plus:before,.fa-plus-circle:before{content:"\f055"}.fa-circle-question:before,.fa-question-circle:before{content:"\f059"}.fa-circle-radiation:before,.fa-radiation-alt:before{content:"\f7ba"}.fa-arrow-alt-circle-right:before,.fa-circle-right:before{content:"\f35a"}.fa-circle-stop:before,.fa-stop-circle:before{content:"\f28d"}.fa-arrow-alt-circle-up:before,.fa-circle-up:before{content:"\f35b"}.fa-circle-user:before,.fa-user-circle:before{content:"\f2bd"}.fa-circle-xmark:before,.fa-times-circle:before,.fa-xmark-circle:before{content:"\f057"}.fa-city:before{content:"\f64f"}.fa-clapperboard:before{content:"\e131"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock-four:before,.fa-clock:before{content:"\f017"}.fa-clock-rotate-left:before,.fa-history:before{content:"\f1da"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-arrow-down:before,.fa-cloud-download-alt:before,.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-arrow-up:before,.fa-cloud-upload-alt:before,.fa-cloud-upload:before{content:"\f0ee"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-clover:before{content:"\e139"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-code-commit:before{content:"\f386"}.fa-code-compare:before{content:"\e13a"}.fa-code-fork:before{content:"\e13b"}.fa-code-merge:before{content:"\f387"}.fa-code-pull-request:before{content:"\e13c"}.fa-coins:before{content:"\f51e"}.fa-colon-sign:before{content:"\e140"}.fa-comment:before{content:"\f075"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before,.fa-commenting:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comment-sms:before,.fa-sms:before{content:"\f7cd"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compass-drafting:before,.fa-drafting-compass:before{content:"\f568"}.fa-compress:before{content:"\f066"}.fa-computer-mouse:before,.fa-mouse:before{content:"\f8cc"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-couch:before{content:"\f4b8"}.fa-credit-card-alt:before,.fa-credit-card:before{content:"\f09d"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before,.fa-crop-simple:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-cruzeiro-sign:before{content:"\e152"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-d:before{content:"\44"}.fa-database:before{content:"\f1c0"}.fa-backspace:before,.fa-delete-left:before{content:"\f55a"}.fa-democrat:before{content:"\f747"}.fa-desktop-alt:before,.fa-desktop:before{content:"\f390"}.fa-dharmachakra:before{content:"\f655"}.fa-diagram-next:before{content:"\e476"}.fa-diagram-predecessor:before{content:"\e477"}.fa-diagram-project:before,.fa-project-diagram:before{content:"\f542"}.fa-diagram-successor:before{content:"\e47a"}.fa-diamond:before{content:"\f219"}.fa-diamond-turn-right:before,.fa-directions:before{content:"\f5eb"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dna:before{content:"\f471"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before,.fa-dollar:before,.fa-usd:before{content:"\24"}.fa-dolly-box:before,.fa-dolly:before{content:"\f472"}.fa-dong-sign:before{content:"\e169"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dove:before{content:"\f4ba"}.fa-compress-alt:before,.fa-down-left-and-up-right-to-center:before{content:"\f422"}.fa-down-long:before,.fa-long-arrow-alt-down:before{content:"\f309"}.fa-download:before{content:"\f019"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-droplet:before,.fa-tint:before{content:"\f043"}.fa-droplet-slash:before,.fa-tint-slash:before{content:"\f5c7"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-e:before{content:"\45"}.fa-deaf:before,.fa-deafness:before,.fa-ear-deaf:before,.fa-hard-of-hearing:before{content:"\f2a4"}.fa-assistive-listening-systems:before,.fa-ear-listen:before{content:"\f2a2"}.fa-earth-africa:before,.fa-globe-africa:before{content:"\f57c"}.fa-earth-america:before,.fa-earth-americas:before,.fa-earth:before,.fa-globe-americas:before{content:"\f57d"}.fa-earth-asia:before,.fa-globe-asia:before{content:"\f57e"}.fa-earth-europe:before,.fa-globe-europe:before{content:"\f7a2"}.fa-earth-oceania:before,.fa-globe-oceania:before{content:"\e47b"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elevator:before{content:"\e16d"}.fa-ellipsis-h:before,.fa-ellipsis:before{content:"\f141"}.fa-ellipsis-v:before,.fa-ellipsis-vertical:before{content:"\f142"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelopes-bulk:before,.fa-mail-bulk:before{content:"\f674"}.fa-equals:before{content:"\3d"}.fa-eraser:before{content:"\f12d"}.fa-ethernet:before{content:"\f796"}.fa-eur:before,.fa-euro-sign:before,.fa-euro:before{content:"\f153"}.fa-exclamation:before{content:"\21"}.fa-expand:before{content:"\f065"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper-empty:before,.fa-eye-dropper:before,.fa-eyedropper:before{content:"\f1fb"}.fa-eye-low-vision:before,.fa-low-vision:before{content:"\f2a8"}.fa-eye-slash:before{content:"\f070"}.fa-f:before{content:"\46"}.fa-angry:before,.fa-face-angry:before{content:"\f556"}.fa-dizzy:before,.fa-face-dizzy:before{content:"\f567"}.fa-face-flushed:before,.fa-flushed:before{content:"\f579"}.fa-face-frown:before,.fa-frown:before{content:"\f119"}.fa-face-frown-open:before,.fa-frown-open:before{content:"\f57a"}.fa-face-grimace:before,.fa-grimace:before{content:"\f57f"}.fa-face-grin:before,.fa-grin:before{content:"\f580"}.fa-face-grin-beam:before,.fa-grin-beam:before{content:"\f582"}.fa-face-grin-beam-sweat:before,.fa-grin-beam-sweat:before{content:"\f583"}.fa-face-grin-hearts:before,.fa-grin-hearts:before{content:"\f584"}.fa-face-grin-squint:before,.fa-grin-squint:before{content:"\f585"}.fa-face-grin-squint-tears:before,.fa-grin-squint-tears:before{content:"\f586"}.fa-face-grin-stars:before,.fa-grin-stars:before{content:"\f587"}.fa-face-grin-tears:before,.fa-grin-tears:before{content:"\f588"}.fa-face-grin-tongue:before,.fa-grin-tongue:before{content:"\f589"}.fa-face-grin-tongue-squint:before,.fa-grin-tongue-squint:before{content:"\f58a"}.fa-face-grin-tongue-wink:before,.fa-grin-tongue-wink:before{content:"\f58b"}.fa-face-grin-wide:before,.fa-grin-alt:before{content:"\f581"}.fa-face-grin-wink:before,.fa-grin-wink:before{content:"\f58c"}.fa-face-kiss:before,.fa-kiss:before{content:"\f596"}.fa-face-kiss-beam:before,.fa-kiss-beam:before{content:"\f597"}.fa-face-kiss-wink-heart:before,.fa-kiss-wink-heart:before{content:"\f598"}.fa-face-laugh:before,.fa-laugh:before{content:"\f599"}.fa-face-laugh-beam:before,.fa-laugh-beam:before{content:"\f59a"}.fa-face-laugh-squint:before,.fa-laugh-squint:before{content:"\f59b"}.fa-face-laugh-wink:before,.fa-laugh-wink:before{content:"\f59c"}.fa-face-meh:before,.fa-meh:before{content:"\f11a"}.fa-face-meh-blank:before,.fa-meh-blank:before{content:"\f5a4"}.fa-face-rolling-eyes:before,.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-face-sad-cry:before,.fa-sad-cry:before{content:"\f5b3"}.fa-face-sad-tear:before,.fa-sad-tear:before{content:"\f5b4"}.fa-face-smile:before,.fa-smile:before{content:"\f118"}.fa-face-smile-beam:before,.fa-smile-beam:before{content:"\f5b8"}.fa-face-smile-wink:before,.fa-smile-wink:before{content:"\f4da"}.fa-face-surprise:before,.fa-surprise:before{content:"\f5c2"}.fa-face-tired:before,.fa-tired:before{content:"\f5c8"}.fa-fan:before{content:"\f863"}.fa-faucet:before{content:"\e005"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before,.fa-feather-pointed:before{content:"\f56b"}.fa-file:before{content:"\f15b"}.fa-file-arrow-down:before,.fa-file-download:before{content:"\f56d"}.fa-file-arrow-up:before,.fa-file-upload:before{content:"\f574"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-excel:before{content:"\f1c3"}.fa-arrow-right-from-file:before,.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-arrow-right-to-file:before,.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-alt:before,.fa-file-lines:before,.fa-file-text:before{content:"\f15c"}.fa-file-medical:before{content:"\f477"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-video:before{content:"\f1c8"}.fa-file-medical-alt:before,.fa-file-waveform:before{content:"\f478"}.fa-file-word:before{content:"\f1c2"}.fa-file-archive:before,.fa-file-zipper:before{content:"\f1c6"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-filter-circle-dollar:before,.fa-funnel-dollar:before{content:"\f662"}.fa-filter-circle-xmark:before{content:"\e17b"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-extinguisher:before{content:"\f134"}.fa-fire-alt:before,.fa-fire-flame-curved:before{content:"\f7e4"}.fa-burn:before,.fa-fire-flame-simple:before{content:"\f46a"}.fa-fish:before{content:"\f578"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-floppy-disk:before,.fa-save:before{content:"\f0c7"}.fa-florin-sign:before{content:"\e184"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-folder-tree:before{content:"\f802"}.fa-font:before{content:"\f031"}.fa-football-ball:before,.fa-football:before{content:"\f44e"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before,.fa-forward-fast:before{content:"\f050"}.fa-forward-step:before,.fa-step-forward:before{content:"\f051"}.fa-franc-sign:before{content:"\e18f"}.fa-frog:before{content:"\f52e"}.fa-futbol-ball:before,.fa-futbol:before,.fa-soccer-ball:before{content:"\f1e3"}.fa-g:before{content:"\47"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-dashboard:before,.fa-gauge-med:before,.fa-gauge:before,.fa-tachometer-alt-average:before{content:"\f624"}.fa-gauge-high:before,.fa-tachometer-alt-fast:before,.fa-tachometer-alt:before{content:"\f625"}.fa-gauge-simple-med:before,.fa-gauge-simple:before,.fa-tachometer-average:before{content:"\f629"}.fa-gauge-simple-high:before,.fa-tachometer-fast:before,.fa-tachometer:before{content:"\f62a"}.fa-gavel:before,.fa-legal:before{content:"\f0e3"}.fa-cog:before,.fa-gear:before{content:"\f013"}.fa-cogs:before,.fa-gears:before{content:"\f085"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-glasses:before{content:"\f530"}.fa-globe:before{content:"\f0ac"}.fa-golf-ball-tee:before,.fa-golf-ball:before{content:"\f450"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\f19d"}.fa-greater-than:before{content:"\3e"}.fa-greater-than-equal:before{content:"\f532"}.fa-grip-horizontal:before,.fa-grip:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-guarani-sign:before{content:"\e19a"}.fa-guitar:before{content:"\f7a6"}.fa-gun:before{content:"\e19b"}.fa-h:before{content:"\48"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-paper:before,.fa-hand:before{content:"\f256"}.fa-hand-back-fist:before,.fa-hand-rock:before{content:"\f255"}.fa-allergies:before,.fa-hand-dots:before{content:"\f461"}.fa-fist-raised:before,.fa-hand-fist:before{content:"\f6de"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-dollar:before,.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-droplet:before,.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\e05c"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\e05d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before,.fa-sign-language:before,.fa-signing:before{content:"\f2a7"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before,.fa-hands-american-sign-language-interpreting:before,.fa-hands-asl-interpreting:before{content:"\f2a3"}.fa-hands-bubbles:before,.fa-hands-wash:before{content:"\e05e"}.fa-hands-clapping:before{content:"\e1a8"}.fa-hands-holding:before{content:"\f4c2"}.fa-hands-praying:before,.fa-praying-hands:before{content:"\f684"}.fa-handshake:before{content:"\f2b5"}.fa-hands-helping:before,.fa-handshake-angle:before{content:"\f4c4"}.fa-handshake-alt-slash:before,.fa-handshake-simple-slash:before{content:"\e05f"}.fa-handshake-slash:before{content:"\e060"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-drive:before,.fa-hdd:before{content:"\f0a0"}.fa-hashtag:before{content:"\23"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-head-side-cough:before{content:"\e061"}.fa-head-side-cough-slash:before{content:"\e062"}.fa-head-side-mask:before{content:"\e063"}.fa-head-side-virus:before{content:"\e064"}.fa-header:before,.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before,.fa-headphones-simple:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before,.fa-heart-crack:before{content:"\f7a9"}.fa-heart-pulse:before,.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-hard-hat:before,.fa-hat-hard:before,.fa-helmet-safety:before{content:"\f807"}.fa-highlighter:before{content:"\f591"}.fa-hippo:before{content:"\f6ed"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital-alt:before,.fa-hospital-wide:before,.fa-hospital:before{content:"\f0f8"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub-person:before,.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hourglass-2:before,.fa-hourglass-half:before,.fa-hourglass:before{content:"\f254"}.fa-hourglass-empty:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-home-alt:before,.fa-home-lg-alt:before,.fa-home:before,.fa-house:before{content:"\f015"}.fa-home-lg:before,.fa-house-chimney:before{content:"\e3af"}.fa-house-chimney-crack:before,.fa-house-damage:before{content:"\f6f1"}.fa-clinic-medical:before,.fa-house-chimney-medical:before{content:"\f7f2"}.fa-house-chimney-user:before{content:"\e065"}.fa-house-chimney-window:before{content:"\e00d"}.fa-house-crack:before{content:"\e3b1"}.fa-house-laptop:before,.fa-laptop-house:before{content:"\e066"}.fa-house-medical:before{content:"\e3b2"}.fa-home-user:before,.fa-house-user:before{content:"\e1b0"}.fa-hryvnia-sign:before,.fa-hryvnia:before{content:"\f6f2"}.fa-i:before{content:"\49"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-heart-music-camera-bolt:before,.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before,.fa-id-card-clip:before{content:"\f47f"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-image-portrait:before,.fa-portrait:before{content:"\f3e0"}.fa-images:before{content:"\f302"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-indian-rupee-sign:before,.fa-indian-rupee:before,.fa-inr:before{content:"\e1bc"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-italic:before{content:"\f033"}.fa-j:before{content:"\4a"}.fa-jedi:before{content:"\f669"}.fa-fighter-jet:before,.fa-jet-fighter:before{content:"\f0fb"}.fa-joint:before{content:"\f595"}.fa-k:before{content:"\4b"}.fa-kaaba:before{content:"\f66b"}.fa-key:before{content:"\f084"}.fa-keyboard:before{content:"\f11c"}.fa-khanda:before{content:"\f66d"}.fa-kip-sign:before{content:"\e1c4"}.fa-first-aid:before,.fa-kit-medical:before{content:"\f479"}.fa-kiwi-bird:before{content:"\f535"}.fa-l:before{content:"\4c"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-medical:before{content:"\f812"}.fa-lari-sign:before{content:"\e1c8"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-left-long:before,.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-arrows-alt-h:before,.fa-left-right:before{content:"\f337"}.fa-lemon:before{content:"\f094"}.fa-less-than:before{content:"\3c"}.fa-less-than-equal:before{content:"\f537"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-chain-broken:before,.fa-chain-slash:before,.fa-link-slash:before,.fa-unlink:before{content:"\f127"}.fa-lira-sign:before{content:"\f195"}.fa-list-squares:before,.fa-list:before{content:"\f03a"}.fa-list-check:before,.fa-tasks:before{content:"\f0ae"}.fa-list-1-2:before,.fa-list-numeric:before,.fa-list-ol:before{content:"\f0cb"}.fa-list-dots:before,.fa-list-ul:before{content:"\f0ca"}.fa-litecoin-sign:before{content:"\e1d3"}.fa-location-arrow:before{content:"\f124"}.fa-location-crosshairs:before,.fa-location:before{content:"\f601"}.fa-location-dot:before,.fa-map-marker-alt:before{content:"\f3c5"}.fa-location-pin:before,.fa-map-marker:before{content:"\f041"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\e067"}.fa-m:before{content:"\4d"}.fa-magnet:before{content:"\f076"}.fa-magnifying-glass:before,.fa-search:before{content:"\f002"}.fa-magnifying-glass-dollar:before,.fa-search-dollar:before{content:"\f688"}.fa-magnifying-glass-location:before,.fa-search-location:before{content:"\f689"}.fa-magnifying-glass-minus:before,.fa-search-minus:before{content:"\f010"}.fa-magnifying-glass-plus:before,.fa-search-plus:before{content:"\f00e"}.fa-manat-sign:before{content:"\e1d5"}.fa-map:before{content:"\f279"}.fa-map-location:before,.fa-map-marked:before{content:"\f59f"}.fa-map-location-dot:before,.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-pin:before{content:"\f276"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-and-venus:before{content:"\f224"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before,.fa-mars-stroke-right:before{content:"\f22b"}.fa-mars-stroke-up:before,.fa-mars-stroke-v:before{content:"\f22a"}.fa-glass-martini-alt:before,.fa-martini-glass:before{content:"\f57b"}.fa-cocktail:before,.fa-martini-glass-citrus:before{content:"\f561"}.fa-glass-martini:before,.fa-martini-glass-empty:before{content:"\f000"}.fa-mask:before{content:"\f6fa"}.fa-mask-face:before{content:"\e1d7"}.fa-masks-theater:before,.fa-theater-masks:before{content:"\f630"}.fa-expand-arrows-alt:before,.fa-maximize:before{content:"\f31e"}.fa-medal:before{content:"\f5a2"}.fa-memory:before{content:"\f538"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-comment-alt:before,.fa-message:before{content:"\f27a"}.fa-meteor:before{content:"\f753"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before,.fa-microphone-lines:before{content:"\f3c9"}.fa-microphone-alt-slash:before,.fa-microphone-lines-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-mill-sign:before{content:"\e1ed"}.fa-compress-arrows-alt:before,.fa-minimize:before{content:"\f78c"}.fa-minus:before,.fa-subtract:before{content:"\f068"}.fa-mitten:before{content:"\f7b5"}.fa-mobile-android:before,.fa-mobile-phone:before,.fa-mobile:before{content:"\f3ce"}.fa-mobile-button:before{content:"\f10b"}.fa-mobile-alt:before,.fa-mobile-screen-button:before{content:"\f3cd"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-1:before,.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-1-wave:before,.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before,.fa-money-check-dollar:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mug-hot:before{content:"\f7b6"}.fa-coffee:before,.fa-mug-saucer:before{content:"\f0f4"}.fa-music:before{content:"\f001"}.fa-n:before{content:"\4e"}.fa-naira-sign:before{content:"\e1f6"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-not-equal:before{content:"\f53e"}.fa-note-sticky:before,.fa-sticky-note:before{content:"\f249"}.fa-notes-medical:before{content:"\f481"}.fa-o:before{content:"\4f"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-oil-can:before{content:"\f613"}.fa-om:before{content:"\f679"}.fa-otter:before{content:"\f700"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-p:before{content:"\50"}.fa-pager:before{content:"\f815"}.fa-paint-roller:before{content:"\f5aa"}.fa-paint-brush:before,.fa-paintbrush:before{content:"\f1fc"}.fa-palette:before{content:"\f53f"}.fa-pallet:before{content:"\f482"}.fa-panorama:before{content:"\e209"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-passport:before{content:"\f5ab"}.fa-file-clipboard:before,.fa-paste:before{content:"\f0ea"}.fa-pause:before{content:"\f04c"}.fa-paw:before{content:"\f1b0"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before,.fa-pen-clip:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-ruler:before,.fa-pencil-ruler:before{content:"\f5ae"}.fa-edit:before,.fa-pen-to-square:before{content:"\f044"}.fa-pencil-alt:before,.fa-pencil:before{content:"\f303"}.fa-people-arrows-left-right:before,.fa-people-arrows:before{content:"\e068"}.fa-people-carry-box:before,.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before,.fa-percentage:before{content:"\25"}.fa-male:before,.fa-person:before{content:"\f183"}.fa-biking:before,.fa-person-biking:before{content:"\f84a"}.fa-person-booth:before{content:"\f756"}.fa-diagnoses:before,.fa-person-dots-from-line:before{content:"\f470"}.fa-female:before,.fa-person-dress:before{content:"\f182"}.fa-hiking:before,.fa-person-hiking:before{content:"\f6ec"}.fa-person-praying:before,.fa-pray:before{content:"\f683"}.fa-person-running:before,.fa-running:before{content:"\f70c"}.fa-person-skating:before,.fa-skating:before{content:"\f7c5"}.fa-person-skiing:before,.fa-skiing:before{content:"\f7c9"}.fa-person-skiing-nordic:before,.fa-skiing-nordic:before{content:"\f7ca"}.fa-person-snowboarding:before,.fa-snowboarding:before{content:"\f7ce"}.fa-person-swimming:before,.fa-swimmer:before{content:"\f5c4"}.fa-person-walking:before,.fa-walking:before{content:"\f554"}.fa-blind:before,.fa-person-walking-with-cane:before{content:"\f29d"}.fa-peseta-sign:before{content:"\e221"}.fa-peso-sign:before{content:"\e222"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before,.fa-phone-flip:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-volume:before,.fa-volume-control-phone:before{content:"\f2a0"}.fa-photo-film:before,.fa-photo-video:before{content:"\f87c"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\e069"}.fa-play:before{content:"\f04b"}.fa-plug:before{content:"\f1e6"}.fa-add:before,.fa-plus:before{content:"\2b"}.fa-plus-minus:before{content:"\e43c"}.fa-podcast:before{content:"\f2ce"}.fa-poo:before{content:"\f2fe"}.fa-poo-bolt:before,.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-power-off:before{content:"\f011"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before,.fa-prescription-bottle-medical:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-pump-medical:before{content:"\e06a"}.fa-pump-soap:before{content:"\e06b"}.fa-puzzle-piece:before{content:"\f12e"}.fa-q:before{content:"\51"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\3f"}.fa-quote-left-alt:before,.fa-quote-left:before{content:"\f10d"}.fa-quote-right-alt:before,.fa-quote-right:before{content:"\f10e"}.fa-r:before{content:"\52"}.fa-radiation:before{content:"\f7b9"}.fa-rainbow:before{content:"\f75b"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-ad:before,.fa-rectangle-ad:before{content:"\f641"}.fa-list-alt:before,.fa-rectangle-list:before{content:"\f022"}.fa-rectangle-times:before,.fa-rectangle-xmark:before,.fa-times-rectangle:before,.fa-window-close:before{content:"\f410"}.fa-recycle:before{content:"\f1b8"}.fa-registered:before{content:"\f25d"}.fa-repeat:before{content:"\f363"}.fa-mail-reply:before,.fa-reply:before{content:"\f3e5"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-republican:before{content:"\f75e"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-ribbon:before{content:"\f4d6"}.fa-right-from-bracket:before,.fa-sign-out-alt:before{content:"\f2f5"}.fa-exchange-alt:before,.fa-right-left:before{content:"\f362"}.fa-long-arrow-alt-right:before,.fa-right-long:before{content:"\f30b"}.fa-right-to-bracket:before,.fa-sign-in-alt:before{content:"\f2f6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rotate:before,.fa-sync-alt:before{content:"\f2f1"}.fa-rotate-back:before,.fa-rotate-backward:before,.fa-rotate-left:before,.fa-undo-alt:before{content:"\f2ea"}.fa-redo-alt:before,.fa-rotate-forward:before,.fa-rotate-right:before{content:"\f2f9"}.fa-route:before{content:"\f4d7"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-rouble:before,.fa-rub:before,.fa-ruble-sign:before,.fa-ruble:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-rupee-sign:before,.fa-rupee:before{content:"\f156"}.fa-rupiah-sign:before{content:"\e23d"}.fa-s:before{content:"\53"}.fa-sailboat:before{content:"\e445"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-balance-scale:before,.fa-scale-balanced:before{content:"\f24e"}.fa-balance-scale-left:before,.fa-scale-unbalanced:before{content:"\f515"}.fa-balance-scale-right:before,.fa-scale-unbalanced-flip:before{content:"\f516"}.fa-school:before{content:"\f549"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-screwdriver:before{content:"\f54a"}.fa-screwdriver-wrench:before,.fa-tools:before{content:"\f7d9"}.fa-scroll:before{content:"\f70e"}.fa-scroll-torah:before,.fa-torah:before{content:"\f6a0"}.fa-sd-card:before{content:"\f7c2"}.fa-section:before{content:"\e447"}.fa-seedling:before,.fa-sprout:before{content:"\f4d8"}.fa-server:before{content:"\f233"}.fa-shapes:before,.fa-triangle-circle-square:before{content:"\f61f"}.fa-arrow-turn-right:before,.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-share-from-square:before,.fa-share-square:before{content:"\f14d"}.fa-share-alt:before,.fa-share-nodes:before{content:"\f1e0"}.fa-ils:before,.fa-shekel-sign:before,.fa-shekel:before,.fa-sheqel-sign:before,.fa-sheqel:before{content:"\f20b"}.fa-shield:before{content:"\f132"}.fa-shield-alt:before,.fa-shield-blank:before{content:"\f3ed"}.fa-shield-virus:before{content:"\e06c"}.fa-ship:before{content:"\f21a"}.fa-shirt:before,.fa-t-shirt:before,.fa-tshirt:before{content:"\f553"}.fa-shoe-prints:before{content:"\f54b"}.fa-shop:before,.fa-store-alt:before{content:"\f54f"}.fa-shop-slash:before,.fa-store-alt-slash:before{content:"\e070"}.fa-shower:before{content:"\f2cc"}.fa-shrimp:before{content:"\e448"}.fa-random:before,.fa-shuffle:before{content:"\f074"}.fa-shuttle-space:before,.fa-space-shuttle:before{content:"\f197"}.fa-sign-hanging:before,.fa-sign:before{content:"\f4d9"}.fa-signal-5:before,.fa-signal-perfect:before,.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-map-signs:before,.fa-signs-post:before{content:"\f277"}.fa-sim-card:before{content:"\f7c4"}.fa-sink:before{content:"\e06d"}.fa-sitemap:before{content:"\f0e8"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before,.fa-sliders:before{content:"\f1de"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\e06e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before,.fa-unsorted:before{content:"\f0dc"}.fa-sort-desc:before,.fa-sort-down:before{content:"\f0dd"}.fa-sort-asc:before,.fa-sort-up:before{content:"\f0de"}.fa-spa:before{content:"\f5bb"}.fa-pastafarianism:before,.fa-spaghetti-monster-flying:before{content:"\f67b"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spoon:before,.fa-utensil-spoon:before{content:"\f2e5"}.fa-spray-can:before{content:"\f5bd"}.fa-air-freshener:before,.fa-spray-can-sparkles:before{content:"\f5d0"}.fa-square:before{content:"\f0c8"}.fa-external-link-square:before,.fa-square-arrow-up-right:before{content:"\f14c"}.fa-caret-square-down:before,.fa-square-caret-down:before{content:"\f150"}.fa-caret-square-left:before,.fa-square-caret-left:before{content:"\f191"}.fa-caret-square-right:before,.fa-square-caret-right:before{content:"\f152"}.fa-caret-square-up:before,.fa-square-caret-up:before{content:"\f151"}.fa-check-square:before,.fa-square-check:before{content:"\f14a"}.fa-envelope-square:before,.fa-square-envelope:before{content:"\f199"}.fa-square-full:before{content:"\f45c"}.fa-h-square:before,.fa-square-h:before{content:"\f0fd"}.fa-minus-square:before,.fa-square-minus:before{content:"\f146"}.fa-parking:before,.fa-square-parking:before{content:"\f540"}.fa-pen-square:before,.fa-pencil-square:before,.fa-square-pen:before{content:"\f14b"}.fa-phone-square:before,.fa-square-phone:before{content:"\f098"}.fa-phone-square-alt:before,.fa-square-phone-flip:before{content:"\f87b"}.fa-plus-square:before,.fa-square-plus:before{content:"\f0fe"}.fa-poll-h:before,.fa-square-poll-horizontal:before{content:"\f682"}.fa-poll:before,.fa-square-poll-vertical:before{content:"\f681"}.fa-square-root-alt:before,.fa-square-root-variable:before{content:"\f698"}.fa-rss-square:before,.fa-square-rss:before{content:"\f143"}.fa-share-alt-square:before,.fa-square-share-nodes:before{content:"\f1e1"}.fa-external-link-square-alt:before,.fa-square-up-right:before{content:"\f360"}.fa-square-xmark:before,.fa-times-square:before,.fa-xmark-square:before{content:"\f2d3"}.fa-stairs:before{content:"\e289"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before,.fa-star-half-stroke:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-gbp:before,.fa-pound-sign:before,.fa-sterling-sign:before{content:"\f154"}.fa-stethoscope:before{content:"\f0f1"}.fa-stop:before{content:"\f04d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\e06f"}.fa-store:before{content:"\f54e"}.fa-store-slash:before{content:"\e071"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stroopwafel:before{content:"\f551"}.fa-subscript:before{content:"\f12c"}.fa-suitcase:before{content:"\f0f2"}.fa-medkit:before,.fa-suitcase-medical:before{content:"\f0fa"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superscript:before{content:"\f12b"}.fa-swatchbook:before{content:"\f5c3"}.fa-synagogue:before{content:"\f69b"}.fa-syringe:before{content:"\f48e"}.fa-t:before{content:"\54"}.fa-table:before{content:"\f0ce"}.fa-table-cells:before,.fa-th:before{content:"\f00a"}.fa-table-cells-large:before,.fa-th-large:before{content:"\f009"}.fa-columns:before,.fa-table-columns:before{content:"\f0db"}.fa-table-list:before,.fa-th-list:before{content:"\f00b"}.fa-ping-pong-paddle-ball:before,.fa-table-tennis-paddle-ball:before,.fa-table-tennis:before{content:"\f45d"}.fa-tablet-android:before,.fa-tablet:before{content:"\f3fb"}.fa-tablet-button:before{content:"\f10a"}.fa-tablet-alt:before,.fa-tablet-screen-button:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-digital-tachograph:before,.fa-tachograph-digital:before{content:"\f566"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-temperature-0:before,.fa-temperature-empty:before,.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-temperature-4:before,.fa-temperature-full:before,.fa-thermometer-4:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-temperature-2:before,.fa-temperature-half:before,.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-temperature-1:before,.fa-temperature-quarter:before,.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-temperature-3:before,.fa-temperature-three-quarters:before,.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-tenge-sign:before,.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-remove-format:before,.fa-text-slash:before{content:"\f87d"}.fa-text-width:before{content:"\f035"}.fa-thermometer:before{content:"\f491"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumb-tack:before,.fa-thumbtack:before{content:"\f08d"}.fa-ticket:before{content:"\f145"}.fa-ticket-alt:before,.fa-ticket-simple:before{content:"\f3ff"}.fa-timeline:before{content:"\e29c"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\e072"}.fa-toolbox:before{content:"\f552"}.fa-tooth:before{content:"\f5c9"}.fa-torii-gate:before{content:"\f6a1"}.fa-broadcast-tower:before,.fa-tower-broadcast:before{content:"\f519"}.fa-tractor:before{content:"\f722"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\e041"}.fa-train:before{content:"\f238"}.fa-subway:before,.fa-train-subway:before{content:"\f239"}.fa-train-tram:before,.fa-tram:before{content:"\f7da"}.fa-transgender-alt:before,.fa-transgender:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-arrow-up:before,.fa-trash-restore:before{content:"\f829"}.fa-trash-alt:before,.fa-trash-can:before{content:"\f2ed"}.fa-trash-can-arrow-up:before,.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-exclamation-triangle:before,.fa-triangle-exclamation:before,.fa-warning:before{content:"\f071"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-shipping-fast:before,.fa-truck-fast:before{content:"\f48b"}.fa-ambulance:before,.fa-truck-medical:before{content:"\f0f9"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-truck-loading:before,.fa-truck-ramp-box:before{content:"\f4de"}.fa-teletype:before,.fa-tty:before{content:"\f1e4"}.fa-try:before,.fa-turkish-lira-sign:before,.fa-turkish-lira:before{content:"\e2bb"}.fa-level-down-alt:before,.fa-turn-down:before{content:"\f3be"}.fa-level-up-alt:before,.fa-turn-up:before{content:"\f3bf"}.fa-television:before,.fa-tv-alt:before,.fa-tv:before{content:"\f26c"}.fa-u:before{content:"\55"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-universal-access:before{content:"\f29a"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before,.fa-unlock-keyhole:before{content:"\f13e"}.fa-arrows-alt-v:before,.fa-up-down:before{content:"\f338"}.fa-arrows-alt:before,.fa-up-down-left-right:before{content:"\f0b2"}.fa-long-arrow-alt-up:before,.fa-up-long:before{content:"\f30c"}.fa-expand-alt:before,.fa-up-right-and-down-left-from-center:before{content:"\f424"}.fa-external-link-alt:before,.fa-up-right-from-square:before{content:"\f35d"}.fa-upload:before{content:"\f093"}.fa-user:before{content:"\f007"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-clock:before{content:"\f4fd"}.fa-user-doctor:before,.fa-user-md:before{content:"\f0f0"}.fa-user-cog:before,.fa-user-gear:before{content:"\f4fe"}.fa-user-graduate:before{content:"\f501"}.fa-user-friends:before,.fa-user-group:before{content:"\f500"}.fa-user-injured:before{content:"\f728"}.fa-user-alt:before,.fa-user-large:before{content:"\f406"}.fa-user-alt-slash:before,.fa-user-large-slash:before{content:"\f4fa"}.fa-user-lock:before{content:"\f502"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-edit:before,.fa-user-pen:before{content:"\f4ff"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before,.fa-user-xmark:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before,.fa-users-gear:before{content:"\f509"}.fa-users-slash:before{content:"\e073"}.fa-cutlery:before,.fa-utensils:before{content:"\f2e7"}.fa-v:before{content:"\56"}.fa-shuttle-van:before,.fa-van-shuttle:before{content:"\f5b6"}.fa-vault:before{content:"\e2c5"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-vest:before{content:"\e085"}.fa-vest-patches:before{content:"\e086"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-video-camera:before,.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-virus:before{content:"\e074"}.fa-virus-covid:before{content:"\e4a8"}.fa-virus-covid-slash:before{content:"\e4a9"}.fa-virus-slash:before{content:"\e075"}.fa-viruses:before{content:"\e076"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before,.fa-volleyball:before{content:"\f45f"}.fa-volume-high:before,.fa-volume-up:before{content:"\f028"}.fa-volume-down:before,.fa-volume-low:before{content:"\f027"}.fa-volume-off:before{content:"\f026"}.fa-volume-mute:before,.fa-volume-times:before,.fa-volume-xmark:before{content:"\f6a9"}.fa-vr-cardboard:before{content:"\f729"}.fa-w:before{content:"\57"}.fa-wallet:before{content:"\f555"}.fa-magic:before,.fa-wand-magic:before{content:"\f0d0"}.fa-magic-wand-sparkles:before,.fa-wand-magic-sparkles:before{content:"\e2ca"}.fa-wand-sparkles:before{content:"\f72b"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-ladder-water:before,.fa-swimming-pool:before,.fa-water-ladder:before{content:"\f5c5"}.fa-wave-square:before{content:"\f83e"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weight-scale:before,.fa-weight:before{content:"\f496"}.fa-wheelchair:before{content:"\f193"}.fa-glass-whiskey:before,.fa-whiskey-glass:before{content:"\f7a0"}.fa-wifi-3:before,.fa-wifi-strong:before,.fa-wifi:before{content:"\f1eb"}.fa-wind:before{content:"\f72e"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before,.fa-wine-glass-empty:before{content:"\f5ce"}.fa-krw:before,.fa-won-sign:before,.fa-won:before{content:"\f159"}.fa-wrench:before{content:"\f0ad"}.fa-x:before{content:"\58"}.fa-x-ray:before{content:"\f497"}.fa-close:before,.fa-multiply:before,.fa-remove:before,.fa-times:before,.fa-xmark:before{content:"\f00d"}.fa-y:before{content:"\59"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen-sign:before,.fa-yen:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-z:before{content:"\5a"}.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host,:root{--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-family:"Font Awesome 6 Brands";font-weight:400}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-adn:before{content:"\f170"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-alipay:before{content:"\f642"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-amilia:before{content:"\f36d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-pay:before{content:"\f415"}.fa-artstation:before{content:"\f77a"}.fa-asymmetrik:before{content:"\f372"}.fa-atlassian:before{content:"\f77b"}.fa-audible:before{content:"\f373"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-aws:before{content:"\f375"}.fa-bandcamp:before{content:"\f2d5"}.fa-battle-net:before{content:"\f835"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bilibili:before{content:"\e3d9"}.fa-bimobject:before{content:"\f378"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bootstrap:before{content:"\f836"}.fa-bots:before{content:"\e340"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-buromobelexperte:before{content:"\f37f"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-cloudflare:before{content:"\e07d"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cmplid:before{content:"\e360"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cotton-bureau:before{content:"\f89e"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-critical-role:before{content:"\f6c9"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\e052"}.fa-dashcube:before{content:"\f210"}.fa-deezer:before{content:"\e077"}.fa-delicious:before{content:"\f1a5"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dhl:before{content:"\f790"}.fa-diaspora:before{content:"\f791"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-draft2digital:before{content:"\f396"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drupal:before{content:"\f1a9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edge-legacy:before{content:"\e078"}.fa-elementor:before{content:"\f430"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-etsy:before{content:"\f2d7"}.fa-evernote:before{content:"\f839"}.fa-expeditedssl:before{content:"\f23e"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-figma:before{content:"\f799"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\e007"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-fly:before{content:"\f417"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-fulcrum:before{content:"\f50b"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-gofore:before{content:"\f3a7"}.fa-golang:before{content:"\e40f"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-pay:before{content:"\e079"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guilded:before{content:"\e07e"}.fa-gulp:before{content:"\f3ae"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hashnode:before{content:"\e499"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-hive:before{content:"\e07f"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-hotjar:before{content:"\f3b1"}.fa-houzz:before{content:"\f27c"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-ideal:before{content:"\e013"}.fa-imdb:before{content:"\f2d8"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\e055"}.fa-instalod:before{content:"\e081"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joomla:before{content:"\f1aa"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaggle:before{content:"\f5fa"}.fa-keybase:before{content:"\f4f5"}.fa-keycdn:before{content:"\f3ba"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-korvue:before{content:"\f42f"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-leanpub:before{content:"\f212"}.fa-less:before{content:"\f41d"}.fa-line:before{content:"\f3c0"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-mailchimp:before{content:"\f59e"}.fa-mandalorian:before{content:"\f50f"}.fa-markdown:before{content:"\f60f"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medapps:before{content:"\f3c6"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-mendeley:before{content:"\f7b3"}.fa-microblog:before{content:"\e01a"}.fa-microsoft:before{content:"\f3ca"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\e056"}.fa-mizuni:before{content:"\f3cc"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-octopus-deploy:before{content:"\e082"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-old-republic:before{content:"\f510"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-padlet:before{content:"\e4a0"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-palfed:before{content:"\f3d8"}.fa-patreon:before{content:"\f3d9"}.fa-paypal:before{content:"\f1ed"}.fa-perbyte:before{content:"\e083"}.fa-periscope:before{content:"\f3da"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\e01e"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pix:before{content:"\e43a"}.fa-playstation:before{content:"\f3df"}.fa-product-hunt:before{content:"\f288"}.fa-pushed:before{content:"\f3e1"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-r-project:before{content:"\f4f7"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-renren:before{content:"\f18b"}.fa-replyd:before{content:"\f3e6"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-rev:before{content:"\f5b2"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-rust:before{content:"\e07a"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-schlix:before{content:"\f3ea"}.fa-scribd:before{content:"\f28a"}.fa-searchengin:before{content:"\f3eb"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-servicestack:before{content:"\f3ec"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shopify:before{content:"\e057"}.fa-shopware:before{content:"\f5b5"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sith:before{content:"\f512"}.fa-sitrox:before{content:"\e44a"}.fa-sketch:before{content:"\f7c6"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-slideshare:before{content:"\f1e7"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-square:before{content:"\f2ad"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spotify:before{content:"\f1bc"}.fa-square-font-awesome:before{content:"\f425"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-sticker-mule:before{content:"\f3f7"}.fa-strava:before{content:"\f428"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-superpowers:before{content:"\f2dd"}.fa-supple:before{content:"\f3f9"}.fa-suse:before{content:"\f7d6"}.fa-swift:before{content:"\f8e1"}.fa-symfony:before{content:"\f83d"}.fa-teamspeak:before{content:"\f4f9"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-the-red-yeti:before{content:"\f69d"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-think-peaks:before{content:"\f731"}.fa-tiktok:before{content:"\e07b"}.fa-trade-federation:before{content:"\f513"}.fa-trello:before{content:"\f181"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-uncharted:before{content:"\e084"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\e049"}.fa-unsplash:before{content:"\e07c"}.fa-untappd:before{content:"\f405"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-vaadin:before{content:"\f408"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-viber:before{content:"\f409"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-vuejs:before{content:"\f41f"}.fa-watchman-monitoring:before{content:"\e087"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-whmcs:before{content:"\f40d"}.fa-wikipedia-w:before{content:"\f266"}.fa-windows:before{content:"\f17a"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wodu:before{content:"\e088"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-family:"Font Awesome 6 Free";font-weight:400}:host,:root{--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-family:"Font Awesome 6 Free";font-weight:900}@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f250,u+f252,u+f27a} \ No newline at end of file diff --git a/static/vendor/font-awesome/webfonts/fa-brands-400.ttf b/static/vendor/font-awesome/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000..227f022 Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-brands-400.ttf differ diff --git a/static/vendor/font-awesome/webfonts/fa-brands-400.woff2 b/static/vendor/font-awesome/webfonts/fa-brands-400.woff2 new file mode 100644 index 0000000..73c5c12 Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-brands-400.woff2 differ diff --git a/static/vendor/font-awesome/webfonts/fa-regular-400.ttf b/static/vendor/font-awesome/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000..c8ed46d Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-regular-400.ttf differ diff --git a/static/vendor/font-awesome/webfonts/fa-regular-400.woff2 b/static/vendor/font-awesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000..c9291c7 Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-regular-400.woff2 differ diff --git a/static/vendor/font-awesome/webfonts/fa-solid-900.ttf b/static/vendor/font-awesome/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000..99b35ad Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-solid-900.ttf differ diff --git a/static/vendor/font-awesome/webfonts/fa-solid-900.woff2 b/static/vendor/font-awesome/webfonts/fa-solid-900.woff2 new file mode 100644 index 0000000..c7bd59c Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-solid-900.woff2 differ diff --git a/static/vendor/font-awesome/webfonts/fa-v4compatibility.ttf b/static/vendor/font-awesome/webfonts/fa-v4compatibility.ttf new file mode 100644 index 0000000..be0afc2 Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-v4compatibility.ttf differ diff --git a/static/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 b/static/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 new file mode 100644 index 0000000..37a9b8c Binary files /dev/null and b/static/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 differ diff --git a/static/vendor/fonts/appreciation/great-vibes-latin.woff2 b/static/vendor/fonts/appreciation/great-vibes-latin.woff2 new file mode 100644 index 0000000..3dc01f4 Binary files /dev/null and b/static/vendor/fonts/appreciation/great-vibes-latin.woff2 differ diff --git a/static/vendor/fonts/certificate/cinzel-400.woff2 b/static/vendor/fonts/certificate/cinzel-400.woff2 new file mode 100644 index 0000000..94cbeac Binary files /dev/null and b/static/vendor/fonts/certificate/cinzel-400.woff2 differ diff --git a/static/vendor/fonts/certificate/cinzel-600.woff2 b/static/vendor/fonts/certificate/cinzel-600.woff2 new file mode 100644 index 0000000..a4b5e1b Binary files /dev/null and b/static/vendor/fonts/certificate/cinzel-600.woff2 differ diff --git a/static/vendor/fonts/certificate/cinzel-700.woff2 b/static/vendor/fonts/certificate/cinzel-700.woff2 new file mode 100644 index 0000000..a13fb08 Binary files /dev/null and b/static/vendor/fonts/certificate/cinzel-700.woff2 differ diff --git a/static/vendor/fonts/certificate/cormorant-garamond-400-italic.woff2 b/static/vendor/fonts/certificate/cormorant-garamond-400-italic.woff2 new file mode 100644 index 0000000..d927264 Binary files /dev/null and b/static/vendor/fonts/certificate/cormorant-garamond-400-italic.woff2 differ diff --git a/static/vendor/fonts/certificate/cormorant-garamond-400.woff2 b/static/vendor/fonts/certificate/cormorant-garamond-400.woff2 new file mode 100644 index 0000000..096df0a Binary files /dev/null and b/static/vendor/fonts/certificate/cormorant-garamond-400.woff2 differ diff --git a/static/vendor/fonts/certificate/cormorant-garamond-500-italic.woff2 b/static/vendor/fonts/certificate/cormorant-garamond-500-italic.woff2 new file mode 100644 index 0000000..1a4e58f Binary files /dev/null and b/static/vendor/fonts/certificate/cormorant-garamond-500-italic.woff2 differ diff --git a/static/vendor/fonts/certificate/cormorant-garamond-500.woff2 b/static/vendor/fonts/certificate/cormorant-garamond-500.woff2 new file mode 100644 index 0000000..efade81 Binary files /dev/null and b/static/vendor/fonts/certificate/cormorant-garamond-500.woff2 differ diff --git a/static/vendor/fonts/certificate/playfair-display-400-italic.woff2 b/static/vendor/fonts/certificate/playfair-display-400-italic.woff2 new file mode 100644 index 0000000..30828de Binary files /dev/null and b/static/vendor/fonts/certificate/playfair-display-400-italic.woff2 differ diff --git a/static/vendor/fonts/certificate/playfair-display-400.woff2 b/static/vendor/fonts/certificate/playfair-display-400.woff2 new file mode 100644 index 0000000..141ea84 Binary files /dev/null and b/static/vendor/fonts/certificate/playfair-display-400.woff2 differ diff --git a/static/vendor/fonts/certificate/playfair-display-500-italic.woff2 b/static/vendor/fonts/certificate/playfair-display-500-italic.woff2 new file mode 100644 index 0000000..8e3e680 Binary files /dev/null and b/static/vendor/fonts/certificate/playfair-display-500-italic.woff2 differ diff --git a/static/vendor/fonts/certificate/playfair-display-500.woff2 b/static/vendor/fonts/certificate/playfair-display-500.woff2 new file mode 100644 index 0000000..64d6e11 Binary files /dev/null and b/static/vendor/fonts/certificate/playfair-display-500.woff2 differ diff --git a/static/vendor/fonts/certificate/playfair-display-600-italic.woff2 b/static/vendor/fonts/certificate/playfair-display-600-italic.woff2 new file mode 100644 index 0000000..78abff9 Binary files /dev/null and b/static/vendor/fonts/certificate/playfair-display-600-italic.woff2 differ diff --git a/static/vendor/fonts/certificate/playfair-display-600.woff2 b/static/vendor/fonts/certificate/playfair-display-600.woff2 new file mode 100644 index 0000000..c19fbc1 Binary files /dev/null and b/static/vendor/fonts/certificate/playfair-display-600.woff2 differ diff --git a/static/vendor/fonts/certificate/playfair-display-700.woff2 b/static/vendor/fonts/certificate/playfair-display-700.woff2 new file mode 100644 index 0000000..39eefe0 Binary files /dev/null and b/static/vendor/fonts/certificate/playfair-display-700.woff2 differ diff --git a/static/vendor/sortablejs/Sortable.min.js b/static/vendor/sortablejs/Sortable.min.js new file mode 100644 index 0000000..17bb16c --- /dev/null +++ b/static/vendor/sortablejs/Sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.15.0 - MIT | git://github.com/SortableJS/Sortable.git */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function M(o){for(var t=1;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function N(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&p(t,e)||o&&t===n)return t}while(t!==n&&(t=(i=t).host&&i!==document&&i.host.nodeType?i.host:i.parentNode))}var i;return null}var g,m=/\s+/g;function I(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(m," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(m," ")))}function P(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function v(t,e){var n="";if("string"==typeof t)n=t;else do{var o=P(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function b(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[j]._onDragOver(o)}}var i,r,a}function Yt(t){q&&q.parentNode[j]._isOutsideThisEl(t.target)}function Bt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[j]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return It(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Bt.supportPointer&&"PointerEvent"in window&&!u,emptyInsertThreshold:5};for(n in K.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Pt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Mt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),Et.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,x())}function Ft(t,e,n,o,i,r,a,l){var s,c,u=t[j],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||k(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function jt(t){t.draggable=!1}function Ht(){Ct=!1}function Lt(t){return setTimeout(t,0)}function Kt(t){return clearTimeout(t)}Bt.prototype={constructor:Bt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(gt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,q):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Tt.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Tt.push(o)}}(o),!q&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=N(l,t.draggable,o,!1))&&l.animated||J===l)){if(nt=B(l),it=B(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return U({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),z("filter",n,{evt:e}),void(i&&e.cancelable&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=N(s,t.trim(),o,!1))return U({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),z("filter",n,{evt:e}),!0}))return void(i&&e.cancelable&&e.preventDefault());t.handle&&!N(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!q&&n.parentNode===r&&(o=k(n),$=r,V=(q=n).parentNode,Q=q.nextSibling,J=n,at=a.group,st={target:Bt.dragged=q,clientX:(e||t).clientX,clientY:(e||t).clientY},ht=st.clientX-o.left,ft=st.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,q.style["will-change"]="all",o=function(){z("delayEnded",i,{evt:t}),Bt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(q.draggable=!0),i._triggerDragStart(t,e),U({sortable:i,name:"choose",originalEvent:t}),I(q,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){b(q,t.trim(),jt)}),h(l,"dragover",Xt),h(l,"mousemove",Xt),h(l,"touchmove",Xt),h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,q.draggable=!0),z("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Bt.eventCanceled?this._onDrop():(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){q&&jt(q),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;f(t,"mouseup",this._disableDelayedDrag),f(t,"touchend",this._disableDelayedDrag),f(t,"touchcancel",this._disableDelayedDrag),f(t,"mousemove",this._delayedDragTouchMoveHandler),f(t,"touchmove",this._delayedDragTouchMoveHandler),f(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(q,"dragend",this),h($,"dragstart",this._onDragStart));try{document.selection?Lt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;yt=!1,$&&q?(z("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Yt),n=this.options,t||I(q,n.dragClass,!1),I(q,n.ghostClass,!0),Bt.active=this,t&&this._appendGhost(),U({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ct){this._lastX=ct.clientX,this._lastY=ct.clientY,kt();for(var t=document.elementFromPoint(ct.clientX,ct.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ct.clientX,ct.clientY))!==e;)e=t;if(q.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j])if(e[j]._onDragOver({clientX:ct.clientX,clientY:ct.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=(t=e).parentNode);Rt()}},_onTouchMove:function(t){if(st){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Z&&v(Z,!0),a=Z&&r&&r.a,l=Z&&r&&r.d,e=Ot&&bt&&E(bt),a=(i.clientX-st.clientX+o.x)/(a||1)+(e?e[0]-_t[0]:0)/(a||1),l=(i.clientY-st.clientY+o.y)/(l||1)+(e?e[1]-_t[1]:0)/(l||1);if(!Bt.active&&!yt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))n.right+10||t.clientX<=n.right&&t.clientY>n.bottom&&t.clientX>=n.left:t.clientX>n.right&&t.clientY>n.top||t.clientX<=n.right&&t.clientY>n.bottom+10}(n,r,this)&&!g.animated){if(g===q)return O(!1);if((l=g&&a===n.target?g:l)&&(w=k(l)),!1!==Ft($,a,q,o,l,w,n,!!l))return x(),g&&g.nextSibling?a.insertBefore(q,g.nextSibling):a.appendChild(q),V=a,A(),O(!0)}else if(g&&function(t,e,n){n=k(X(n.el,0,n.options,!0));return e?t.clientX{t(e)}))}class t{constructor(){this._events=void 0,this._events={}}on(t,i){e(t,(e=>{const t=this._events[e]||[] +t.push(i),this._events[e]=t}))}off(t,i){var s=arguments.length +0!==s?e(t,(e=>{if(1===s)return void delete this._events[e] +const t=this._events[e] +void 0!==t&&(t.splice(t.indexOf(i),1),this._events[e]=t)})):this._events={}}trigger(t,...i){var s=this +e(t,(e=>{const t=s._events[e] +void 0!==t&&t.forEach((e=>{e.apply(s,i)}))}))}}const i=e=>(e=e.filter(Boolean)).length<2?e[0]||"":1==l(e)?"["+e.join("")+"]":"(?:"+e.join("|")+")",s=e=>{if(!o(e))return e.join("") +let t="",i=0 +const s=()=>{i>1&&(t+="{"+i+"}")} +return e.forEach(((n,o)=>{n!==e[o-1]?(s(),t+=n,i=1):i++})),s(),t},n=e=>{let t=c(e) +return i(t)},o=e=>new Set(e).size!==e.length,r=e=>(e+"").replace(/([\$\(\)\*\+\.\?\[\]\^\{\|\}\\])/gu,"\\$1"),l=e=>e.reduce(((e,t)=>Math.max(e,a(t))),0),a=e=>c(e).length,c=e=>Array.from(e),d=e=>{if(1===e.length)return[[e]] +let t=[] +const i=e.substring(1) +return d(i).forEach((function(i){let s=i.slice(0) +s[0]=e.charAt(0)+s[0],t.push(s),s=i.slice(0),s.unshift(e.charAt(0)),t.push(s)})),t},u=[[0,65535]] +let p,h +const g={},f={"/":"⁄∕",0:"߀",a:"ⱥɐɑ",aa:"ꜳ",ae:"æǽǣ",ao:"ꜵ",au:"ꜷ",av:"ꜹꜻ",ay:"ꜽ",b:"ƀɓƃ",c:"ꜿƈȼↄ",d:"đɗɖᴅƌꮷԁɦ",e:"ɛǝᴇɇ",f:"ꝼƒ",g:"ǥɠꞡᵹꝿɢ",h:"ħⱨⱶɥ",i:"ɨı",j:"ɉȷ",k:"ƙⱪꝁꝃꝅꞣ",l:"łƚɫⱡꝉꝇꞁɭ",m:"ɱɯϻ",n:"ꞥƞɲꞑᴎлԉ",o:"øǿɔɵꝋꝍᴑ",oe:"œ",oi:"ƣ",oo:"ꝏ",ou:"ȣ",p:"ƥᵽꝑꝓꝕρ",q:"ꝗꝙɋ",r:"ɍɽꝛꞧꞃ",s:"ßȿꞩꞅʂ",t:"ŧƭʈⱦꞇ",th:"þ",tz:"ꜩ",u:"ʉ",v:"ʋꝟʌ",vy:"ꝡ",w:"ⱳ",y:"ƴɏỿ",z:"ƶȥɀⱬꝣ",hv:"ƕ"} +for(let e in f){let t=f[e]||"" +for(let i=0;ie.normalize(t),y=e=>c(e).reduce(((e,t)=>e+O(t)),""),O=e=>(e=m(e).toLowerCase().replace(v,(e=>g[e]||"")),m(e,"NFC")) +const b=e=>{const t={},i=(e,i)=>{const s=t[e]||new Set,o=new RegExp("^"+n(s)+"$","iu") +i.match(o)||(s.add(r(i)),t[e]=s)} +for(let t of function*(e){for(const[t,i]of e)for(let e=t;e<=i;e++){let t=String.fromCharCode(e),i=y(t) +i!=t.toLowerCase()&&(i.length>3||0!=i.length&&(yield{folded:i,composed:t,code_point:e}))}}(e))i(t.folded,t.folded),i(t.folded,t.composed) +return t},w=e=>{const t=b(e),s={} +let o=[] +for(let e in t){let i=t[e] +i&&(s[e]=n(i)),e.length>1&&o.push(r(e))}o.sort(((e,t)=>t.length-e.length)) +const l=i(o) +return h=new RegExp("^"+l,"u"),s},_=(e,t=1)=>(t=Math.max(t,e.length-1),i(d(e).map((e=>((e,t=1)=>{let i=0 +return e=e.map((e=>(p[e]&&(i+=e.length),p[e]||e))),i>=t?s(e):""})(e,t))))),C=(e,t=!0)=>{let n=e.length>1?1:0 +return i(e.map((e=>{let i=[] +const o=t?e.length():e.length()-1 +for(let t=0;t{for(const i of t){if(i.start!=e.start||i.end!=e.end)continue +if(i.substrs.join("")!==e.substrs.join(""))continue +let t=e.parts +const s=e=>{for(const i of t){if(i.start===e.start&&i.substr===e.substr)return!1 +if(1!=e.length&&1!=i.length){if(e.starti.start)return!0 +if(i.starte.start)return!0}}return!1} +if(!(i.parts.filter(s).length>0))return!0}return!1} +class I{constructor(){this.parts=[],this.substrs=[],this.start=0,this.end=0}add(e){e&&(this.parts.push(e),this.substrs.push(e.substr),this.start=Math.min(e.start,this.start),this.end=Math.max(e.end,this.end))}last(){return this.parts[this.parts.length-1]}length(){return this.parts.length}clone(e,t){let i=new I,s=JSON.parse(JSON.stringify(this.parts)),n=s.pop() +for(const e of s)i.add(e) +let o=t.substr.substring(0,e-n.start),r=o.length +return i.add({start:n.start,end:n.start+r,length:r,substr:o}),i}}const A=e=>{var t +void 0===p&&(p=w(t||u)),e=y(e) +let i="",s=[new I] +for(let t=0;t0){l=l.sort(((e,t)=>e.length()-t.length())) +for(let e of l)S(e,s)||s.push(e)}else if(t>0&&1==a.size&&!a.has("3")){i+=C(s,!1) +let e=new I +const t=s[0] +t&&e.add(t.last()),s=[e]}}return i+=C(s,!0),i},x=(e,t)=>{if(e)return e[t]},k=(e,t)=>{if(e){for(var i,s=t.split(".");(i=s.shift())&&(e=e[i]););return e}},F=(e,t,i)=>{var s,n +return e?(e+="",null==t.regex||-1===(n=e.search(t.regex))?0:(s=t.string.length/e.length,0===n&&(s+=.5),s*i)):0},L=(e,t)=>{var i=e[t] +if("function"==typeof i)return i +i&&!Array.isArray(i)&&(e[t]=[i])},E=(e,t)=>{if(Array.isArray(e))e.forEach(t) +else for(var i in e)e.hasOwnProperty(i)&&t(e[i],i)},T=(e,t)=>"number"==typeof e&&"number"==typeof t?e>t?1:e(t=y(t+"").toLowerCase())?1:t>e?-1:0 +class P{constructor(e,t){this.items=void 0,this.settings=void 0,this.items=e,this.settings=t||{diacritics:!0}}tokenize(e,t,i){if(!e||!e.length)return[] +const s=[],n=e.split(/\s+/) +var o +return i&&(o=new RegExp("^("+Object.keys(i).map(r).join("|")+"):(.*)$")),n.forEach((e=>{let i,n=null,l=null +o&&(i=e.match(o))&&(n=i[1],e=i[2]),e.length>0&&(l=this.settings.diacritics?A(e)||null:r(e),l&&t&&(l="\\b"+l)),s.push({string:e,regex:l?new RegExp(l,"iu"):null,field:n})})),s}getScoreFunction(e,t){var i=this.prepareSearch(e,t) +return this._getScoreFunction(i)}_getScoreFunction(e){const t=e.tokens,i=t.length +if(!i)return function(){return 0} +const s=e.options.fields,n=e.weights,o=s.length,r=e.getAttrFn +if(!o)return function(){return 1} +const l=1===o?function(e,t){const i=s[0].field +return F(r(t,i),e,n[i]||1)}:function(e,t){var i=0 +if(e.field){const s=r(t,e.field) +!e.regex&&s?i+=1/o:i+=F(s,e,1)}else E(n,((s,n)=>{i+=F(r(t,n),e,s)})) +return i/o} +return 1===i?function(e){return l(t[0],e)}:"and"===e.options.conjunction?function(e){var s,n=0 +for(let i of t){if((s=l(i,e))<=0)return 0 +n+=s}return n/i}:function(e){var s=0 +return E(t,(t=>{s+=l(t,e)})),s/i}}getSortFunction(e,t){var i=this.prepareSearch(e,t) +return this._getSortFunction(i)}_getSortFunction(e){var t,i=[] +const s=this,n=e.options,o=!e.query&&n.sort_empty?n.sort_empty:n.sort +if("function"==typeof o)return o.bind(this) +const r=function(t,i){return"$score"===t?i.score:e.getAttrFn(s.items[i.id],t)} +if(o)for(let t of o)(e.query||"$score"!==t.field)&&i.push(t) +if(e.query){t=!0 +for(let e of i)if("$score"===e.field){t=!1 +break}t&&i.unshift({field:"$score",direction:"desc"})}else i=i.filter((e=>"$score"!==e.field)) +return i.length?function(e,t){var s,n +for(let o of i){if(n=o.field,s=("desc"===o.direction?-1:1)*T(r(n,e),r(n,t)))return s}return 0}:null}prepareSearch(e,t){const i={} +var s=Object.assign({},t) +if(L(s,"sort"),L(s,"sort_empty"),s.fields){L(s,"fields") +const e=[] +s.fields.forEach((t=>{"string"==typeof t&&(t={field:t,weight:1}),e.push(t),i[t.field]="weight"in t?t.weight:1})),s.fields=e}return{options:s,query:e.toLowerCase().trim(),tokens:this.tokenize(e,s.respect_word_boundaries,i),total:0,items:[],weights:i,getAttrFn:s.nesting?k:x}}search(e,t){var i,s,n=this +s=this.prepareSearch(e,t),t=s.options,e=s.query +const o=t.score||n._getScoreFunction(s) +e.length?E(n.items,((e,n)=>{i=o(e),(!1===t.filter||i>0)&&s.items.push({score:i,id:n})})):E(n.items,((e,t)=>{s.items.push({score:1,id:t})})) +const r=n._getSortFunction(s) +return r&&s.items.sort(r),s.total=s.items.length,"number"==typeof t.limit&&(s.items=s.items.slice(0,t.limit)),s}}const N=(e,t)=>{if(Array.isArray(e))e.forEach(t) +else for(var i in e)e.hasOwnProperty(i)&&t(e[i],i)},j=e=>{if(e.jquery)return e[0] +if(e instanceof HTMLElement)return e +if($(e)){var t=document.createElement("template") +return t.innerHTML=e.trim(),t.content.firstChild}return document.querySelector(e)},$=e=>"string"==typeof e&&e.indexOf("<")>-1,V=(e,t)=>{var i=document.createEvent("HTMLEvents") +i.initEvent(t,!0,!1),e.dispatchEvent(i)},q=(e,t)=>{Object.assign(e.style,t)},D=(e,...t)=>{var i=H(t);(e=M(e)).map((e=>{i.map((t=>{e.classList.add(t)}))}))},R=(e,...t)=>{var i=H(t);(e=M(e)).map((e=>{i.map((t=>{e.classList.remove(t)}))}))},H=e=>{var t=[] +return N(e,(e=>{"string"==typeof e&&(e=e.trim().split(/[\11\12\14\15\40]/)),Array.isArray(e)&&(t=t.concat(e))})),t.filter(Boolean)},M=e=>(Array.isArray(e)||(e=[e]),e),z=(e,t,i)=>{if(!i||i.contains(e))for(;e&&e.matches;){if(e.matches(t))return e +e=e.parentNode}},B=(e,t=0)=>t>0?e[e.length-1]:e[0],K=(e,t)=>{if(!e)return-1 +t=t||e.nodeName +for(var i=0;e=e.previousElementSibling;)e.matches(t)&&i++ +return i},Q=(e,t)=>{N(t,((t,i)=>{null==t?e.removeAttribute(i):e.setAttribute(i,""+t)}))},G=(e,t)=>{e.parentNode&&e.parentNode.replaceChild(t,e)},U=(e,t)=>{if(null===t)return +if("string"==typeof t){if(!t.length)return +t=new RegExp(t,"i")}const i=e=>3===e.nodeType?(e=>{var i=e.data.match(t) +if(i&&e.data.length>0){var s=document.createElement("span") +s.className="highlight" +var n=e.splitText(i.index) +n.splitText(i[0].length) +var o=n.cloneNode(!0) +return s.appendChild(o),G(n,s),1}return 0})(e):((e=>{1!==e.nodeType||!e.childNodes||/(script|style)/i.test(e.tagName)||"highlight"===e.className&&"SPAN"===e.tagName||Array.from(e.childNodes).forEach((e=>{i(e)}))})(e),0) +i(e)},J="undefined"!=typeof navigator&&/Mac/.test(navigator.userAgent)?"metaKey":"ctrlKey" +var W={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:null,createOnBlur:!1,createFilter:null,highlight:!0,openOnFocus:!0,shouldOpen:null,maxOptions:50,maxItems:null,hideSelected:null,duplicates:!1,addPrecedence:!1,selectOnTab:!1,preload:null,allowEmptyOption:!1,refreshThrottle:300,loadThrottle:300,loadingClass:"loading",dataAttr:null,optgroupField:"optgroup",valueField:"value",labelField:"text",disabledField:"disabled",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"ts-wrapper",controlClass:"ts-control",dropdownClass:"ts-dropdown",dropdownContentClass:"ts-dropdown-content",itemClass:"item",optionClass:"option",dropdownParent:null,controlInput:'',copyClassesToDropdown:!1,placeholder:null,hidePlaceholder:null,shouldLoad:function(e){return e.length>0},render:{}} +const X=e=>null==e?null:Y(e),Y=e=>"boolean"==typeof e?e?"1":"0":e+"",Z=e=>(e+"").replace(/&/g,"&").replace(//g,">").replace(/"/g,"""),ee=(e,t)=>{var i +return function(s,n){var o=this +i&&(o.loading=Math.max(o.loading-1,0),clearTimeout(i)),i=setTimeout((function(){i=null,o.loadedSearches[s]=!0,e.call(o,s,n)}),t)}},te=(e,t,i)=>{var s,n=e.trigger,o={} +for(s of(e.trigger=function(){var i=arguments[0] +if(-1===t.indexOf(i))return n.apply(e,arguments) +o[i]=arguments},i.apply(e,[]),e.trigger=n,t))s in o&&n.apply(e,o[s])},ie=(e,t=!1)=>{e&&(e.preventDefault(),t&&e.stopPropagation())},se=(e,t,i,s)=>{e.addEventListener(t,i,s)},ne=(e,t)=>!!t&&(!!t[e]&&1===(t.altKey?1:0)+(t.ctrlKey?1:0)+(t.shiftKey?1:0)+(t.metaKey?1:0)),oe=(e,t)=>{const i=e.getAttribute("id") +return i||(e.setAttribute("id",t),t)},re=e=>e.replace(/[\\"']/g,"\\$&"),le=(e,t)=>{t&&e.append(t)} +function ae(e,t){var i=Object.assign({},W,t),s=i.dataAttr,n=i.labelField,o=i.valueField,r=i.disabledField,l=i.optgroupField,a=i.optgroupLabelField,c=i.optgroupValueField,d=e.tagName.toLowerCase(),u=e.getAttribute("placeholder")||e.getAttribute("data-placeholder") +if(!u&&!i.allowEmptyOption){let t=e.querySelector('option[value=""]') +t&&(u=t.textContent)}var p={placeholder:u,options:[],optgroups:[],items:[],maxItems:null} +return"select"===d?(()=>{var t,d=p.options,u={},h=1 +let g=0 +var f=e=>{var t=Object.assign({},e.dataset),i=s&&t[s] +return"string"==typeof i&&i.length&&(t=Object.assign(t,JSON.parse(i))),t},v=(e,t)=>{var s=X(e.value) +if(null!=s&&(s||i.allowEmptyOption)){if(u.hasOwnProperty(s)){if(t){var a=u[s][l] +a?Array.isArray(a)?a.push(t):u[s][l]=[a,t]:u[s][l]=t}}else{var c=f(e) +c[n]=c[n]||e.textContent,c[o]=c[o]||s,c[r]=c[r]||e.disabled,c[l]=c[l]||t,c.$option=e,c.$order=c.$order||++g,u[s]=c,d.push(c)}e.selected&&p.items.push(s)}} +p.maxItems=e.hasAttribute("multiple")?null:1,N(e.children,(e=>{var i,s,n +"optgroup"===(t=e.tagName.toLowerCase())?((n=f(i=e))[a]=n[a]||i.getAttribute("label")||"",n[c]=n[c]||h++,n[r]=n[r]||i.disabled,n.$order=n.$order||++g,p.optgroups.push(n),s=n[c],N(i.children,(e=>{v(e,s)}))):"option"===t&&v(e)}))})():(()=>{const t=e.getAttribute(s) +if(t)p.options=JSON.parse(t),N(p.options,(e=>{p.items.push(e[o])})) +else{var r=e.value.trim()||"" +if(!i.allowEmptyOption&&!r.length)return +const t=r.split(i.delimiter) +N(t,(e=>{const t={} +t[n]=e,t[o]=e,p.options.push(t)})),p.items=t}})(),Object.assign({},W,p,t)}var ce=0 +class de extends(function(e){return e.plugins={},class extends e{constructor(...e){super(...e),this.plugins={names:[],settings:{},requested:{},loaded:{}}}static define(t,i){e.plugins[t]={name:t,fn:i}}initializePlugins(e){var t,i +const s=this,n=[] +if(Array.isArray(e))e.forEach((e=>{"string"==typeof e?n.push(e):(s.plugins.settings[e.name]=e.options,n.push(e.name))})) +else if(e)for(t in e)e.hasOwnProperty(t)&&(s.plugins.settings[t]=e[t],n.push(t)) +for(;i=n.shift();)s.require(i)}loadPlugin(t){var i=this,s=i.plugins,n=e.plugins[t] +if(!e.plugins.hasOwnProperty(t))throw new Error('Unable to find "'+t+'" plugin') +s.requested[t]=!0,s.loaded[t]=n.fn.apply(i,[i.plugins.settings[t]||{}]),s.names.push(t)}require(e){var t=this,i=t.plugins +if(!t.plugins.loaded.hasOwnProperty(e)){if(i.requested[e])throw new Error('Plugin has circular dependency ("'+e+'")') +t.loadPlugin(e)}return i.loaded[e]}}}(t)){constructor(e,t){var i +super(),this.control_input=void 0,this.wrapper=void 0,this.dropdown=void 0,this.control=void 0,this.dropdown_content=void 0,this.focus_node=void 0,this.order=0,this.settings=void 0,this.input=void 0,this.tabIndex=void 0,this.is_select_tag=void 0,this.rtl=void 0,this.inputId=void 0,this._destroy=void 0,this.sifter=void 0,this.isOpen=!1,this.isDisabled=!1,this.isReadOnly=!1,this.isRequired=void 0,this.isInvalid=!1,this.isValid=!0,this.isLocked=!1,this.isFocused=!1,this.isInputHidden=!1,this.isSetup=!1,this.ignoreFocus=!1,this.ignoreHover=!1,this.hasOptions=!1,this.currentResults=void 0,this.lastValue="",this.caretPos=0,this.loading=0,this.loadedSearches={},this.activeOption=null,this.activeItems=[],this.optgroups={},this.options={},this.userOptions={},this.items=[],this.refreshTimeout=null,ce++ +var s=j(e) +if(s.tomselect)throw new Error("Tom Select already initialized on this element") +s.tomselect=this,i=(window.getComputedStyle&&window.getComputedStyle(s,null)).getPropertyValue("direction") +const n=ae(s,t) +this.settings=n,this.input=s,this.tabIndex=s.tabIndex||0,this.is_select_tag="select"===s.tagName.toLowerCase(),this.rtl=/rtl/i.test(i),this.inputId=oe(s,"tomselect-"+ce),this.isRequired=s.required,this.sifter=new P(this.options,{diacritics:n.diacritics}),n.mode=n.mode||(1===n.maxItems?"single":"multi"),"boolean"!=typeof n.hideSelected&&(n.hideSelected="multi"===n.mode),"boolean"!=typeof n.hidePlaceholder&&(n.hidePlaceholder="multi"!==n.mode) +var o=n.createFilter +"function"!=typeof o&&("string"==typeof o&&(o=new RegExp(o)),o instanceof RegExp?n.createFilter=e=>o.test(e):n.createFilter=e=>this.settings.duplicates||!this.options[e]),this.initializePlugins(n.plugins),this.setupCallbacks(),this.setupTemplates() +const r=j("
"),l=j("
"),a=this._render("dropdown"),c=j('
'),d=this.input.getAttribute("class")||"",u=n.mode +var p +if(D(r,n.wrapperClass,d,u),D(l,n.controlClass),le(r,l),D(a,n.dropdownClass,u),n.copyClassesToDropdown&&D(a,d),D(c,n.dropdownContentClass),le(a,c),j(n.dropdownParent||r).appendChild(a),$(n.controlInput)){p=j(n.controlInput) +E(["autocorrect","autocapitalize","autocomplete","spellcheck"],(e=>{s.getAttribute(e)&&Q(p,{[e]:s.getAttribute(e)})})),p.tabIndex=-1,l.appendChild(p),this.focus_node=p}else n.controlInput?(p=j(n.controlInput),this.focus_node=p):(p=j(""),this.focus_node=l) +this.wrapper=r,this.dropdown=a,this.dropdown_content=c,this.control=l,this.control_input=p,this.setup()}setup(){const e=this,t=e.settings,i=e.control_input,s=e.dropdown,n=e.dropdown_content,o=e.wrapper,l=e.control,a=e.input,c=e.focus_node,d={passive:!0},u=e.inputId+"-ts-dropdown" +Q(n,{id:u}),Q(c,{role:"combobox","aria-haspopup":"listbox","aria-expanded":"false","aria-controls":u}) +const p=oe(c,e.inputId+"-ts-control"),h="label[for='"+(e=>e.replace(/['"\\]/g,"\\$&"))(e.inputId)+"']",g=document.querySelector(h),f=e.focus.bind(e) +if(g){se(g,"click",f),Q(g,{for:p}) +const t=oe(g,e.inputId+"-ts-label") +Q(c,{"aria-labelledby":t}),Q(n,{"aria-labelledby":t})}if(o.style.width=a.style.width,e.plugins.names.length){const t="plugin-"+e.plugins.names.join(" plugin-") +D([o,s],t)}(null===t.maxItems||t.maxItems>1)&&e.is_select_tag&&Q(a,{multiple:"multiple"}),t.placeholder&&Q(i,{placeholder:t.placeholder}),!t.splitOn&&t.delimiter&&(t.splitOn=new RegExp("\\s*"+r(t.delimiter)+"+\\s*")),t.load&&t.loadThrottle&&(t.load=ee(t.load,t.loadThrottle)),se(s,"mousemove",(()=>{e.ignoreHover=!1})),se(s,"mouseenter",(t=>{var i=z(t.target,"[data-selectable]",s) +i&&e.onOptionHover(t,i)}),{capture:!0}),se(s,"click",(t=>{const i=z(t.target,"[data-selectable]") +i&&(e.onOptionSelect(t,i),ie(t,!0))})),se(l,"click",(t=>{var s=z(t.target,"[data-ts-item]",l) +s&&e.onItemSelect(t,s)?ie(t,!0):""==i.value&&(e.onClick(),ie(t,!0))})),se(c,"keydown",(t=>e.onKeyDown(t))),se(i,"keypress",(t=>e.onKeyPress(t))),se(i,"input",(t=>e.onInput(t))),se(c,"blur",(t=>e.onBlur(t))),se(c,"focus",(t=>e.onFocus(t))),se(i,"paste",(t=>e.onPaste(t))) +const v=t=>{const n=t.composedPath()[0] +if(!o.contains(n)&&!s.contains(n))return e.isFocused&&e.blur(),void e.inputState() +n==i&&e.isOpen?t.stopPropagation():ie(t,!0)},m=()=>{e.isOpen&&e.positionDropdown()} +se(document,"mousedown",v),se(window,"scroll",m,d),se(window,"resize",m,d),this._destroy=()=>{document.removeEventListener("mousedown",v),window.removeEventListener("scroll",m),window.removeEventListener("resize",m),g&&g.removeEventListener("click",f)},this.revertSettings={innerHTML:a.innerHTML,tabIndex:a.tabIndex},a.tabIndex=-1,a.insertAdjacentElement("afterend",e.wrapper),e.sync(!1),t.items=[],delete t.optgroups,delete t.options,se(a,"invalid",(()=>{e.isValid&&(e.isValid=!1,e.isInvalid=!0,e.refreshState())})),e.updateOriginalInput(),e.refreshItems(),e.close(!1),e.inputState(),e.isSetup=!0,a.disabled?e.disable():a.readOnly?e.setReadOnly(!0):e.enable(),e.on("change",this.onChange),D(a,"tomselected","ts-hidden-accessible"),e.trigger("initialize"),!0===t.preload&&e.preload()}setupOptions(e=[],t=[]){this.addOptions(e),E(t,(e=>{this.registerOptionGroup(e)}))}setupTemplates(){var e=this,t=e.settings.labelField,i=e.settings.optgroupLabelField,s={optgroup:e=>{let t=document.createElement("div") +return t.className="optgroup",t.appendChild(e.options),t},optgroup_header:(e,t)=>'
'+t(e[i])+"
",option:(e,i)=>"
"+i(e[t])+"
",item:(e,i)=>"
"+i(e[t])+"
",option_create:(e,t)=>'
Add '+t(e.input)+"
",no_results:()=>'
No results found
',loading:()=>'
',not_loading:()=>{},dropdown:()=>"
"} +e.settings.render=Object.assign({},s,e.settings.render)}setupCallbacks(){var e,t,i={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",item_select:"onItemSelect",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"} +for(e in i)(t=this.settings[i[e]])&&this.on(e,t)}sync(e=!0){const t=this,i=e?ae(t.input,{delimiter:t.settings.delimiter}):t.settings +t.setupOptions(i.options,i.optgroups),t.setValue(i.items||[],!0),t.lastQuery=null}onClick(){var e=this +if(e.activeItems.length>0)return e.clearActiveItems(),void e.focus() +e.isFocused&&e.isOpen?e.blur():e.focus()}onMouseDown(){}onChange(){V(this.input,"input"),V(this.input,"change")}onPaste(e){var t=this +t.isInputHidden||t.isLocked?ie(e):t.settings.splitOn&&setTimeout((()=>{var e=t.inputValue() +if(e.match(t.settings.splitOn)){var i=e.trim().split(t.settings.splitOn) +E(i,(e=>{X(e)&&(this.options[e]?t.addItem(e):t.createItem(e))}))}}),0)}onKeyPress(e){var t=this +if(!t.isLocked){var i=String.fromCharCode(e.keyCode||e.which) +return t.settings.create&&"multi"===t.settings.mode&&i===t.settings.delimiter?(t.createItem(),void ie(e)):void 0}ie(e)}onKeyDown(e){var t=this +if(t.ignoreHover=!0,t.isLocked)9!==e.keyCode&&ie(e) +else{switch(e.keyCode){case 65:if(ne(J,e)&&""==t.control_input.value)return ie(e),void t.selectAll() +break +case 27:return t.isOpen&&(ie(e,!0),t.close()),void t.clearActiveItems() +case 40:if(!t.isOpen&&t.hasOptions)t.open() +else if(t.activeOption){let e=t.getAdjacent(t.activeOption,1) +e&&t.setActiveOption(e)}return void ie(e) +case 38:if(t.activeOption){let e=t.getAdjacent(t.activeOption,-1) +e&&t.setActiveOption(e)}return void ie(e) +case 13:return void(t.canSelect(t.activeOption)?(t.onOptionSelect(e,t.activeOption),ie(e)):(t.settings.create&&t.createItem()||document.activeElement==t.control_input&&t.isOpen)&&ie(e)) +case 37:return void t.advanceSelection(-1,e) +case 39:return void t.advanceSelection(1,e) +case 9:return void(t.settings.selectOnTab&&(t.canSelect(t.activeOption)&&(t.onOptionSelect(e,t.activeOption),ie(e)),t.settings.create&&t.createItem()&&ie(e))) +case 8:case 46:return void t.deleteSelection(e)}t.isInputHidden&&!ne(J,e)&&ie(e)}}onInput(e){if(this.isLocked)return +const t=this.inputValue() +this.lastValue!==t&&(this.lastValue=t,""!=t?(this.refreshTimeout&&clearTimeout(this.refreshTimeout),this.refreshTimeout=((e,t)=>t>0?setTimeout(e,t):(e.call(null),null))((()=>{this.refreshTimeout=null,this._onInput()}),this.settings.refreshThrottle)):this._onInput())}_onInput(){const e=this.lastValue +this.settings.shouldLoad.call(this,e)&&this.load(e),this.refreshOptions(),this.trigger("type",e)}onOptionHover(e,t){this.ignoreHover||this.setActiveOption(t,!1)}onFocus(e){var t=this,i=t.isFocused +if(t.isDisabled||t.isReadOnly)return t.blur(),void ie(e) +t.ignoreFocus||(t.isFocused=!0,"focus"===t.settings.preload&&t.preload(),i||t.trigger("focus"),t.activeItems.length||(t.inputState(),t.refreshOptions(!!t.settings.openOnFocus)),t.refreshState())}onBlur(e){if(!1!==document.hasFocus()){var t=this +if(t.isFocused){t.isFocused=!1,t.ignoreFocus=!1 +var i=()=>{t.close(),t.setActiveItem(),t.setCaret(t.items.length),t.trigger("blur")} +t.settings.create&&t.settings.createOnBlur?t.createItem(null,i):i()}}}onOptionSelect(e,t){var i,s=this +t.parentElement&&t.parentElement.matches("[data-disabled]")||(t.classList.contains("create")?s.createItem(null,(()=>{s.settings.closeAfterSelect&&s.close()})):void 0!==(i=t.dataset.value)&&(s.lastQuery=null,s.addItem(i),s.settings.closeAfterSelect&&s.close(),!s.settings.hideSelected&&e.type&&/click/.test(e.type)&&s.setActiveOption(t)))}canSelect(e){return!!(this.isOpen&&e&&this.dropdown_content.contains(e))}onItemSelect(e,t){var i=this +return!i.isLocked&&"multi"===i.settings.mode&&(ie(e),i.setActiveItem(t,e),!0)}canLoad(e){return!!this.settings.load&&!this.loadedSearches.hasOwnProperty(e)}load(e){const t=this +if(!t.canLoad(e))return +D(t.wrapper,t.settings.loadingClass),t.loading++ +const i=t.loadCallback.bind(t) +t.settings.load.call(t,e,i)}loadCallback(e,t){const i=this +i.loading=Math.max(i.loading-1,0),i.lastQuery=null,i.clearActiveOption(),i.setupOptions(e,t),i.refreshOptions(i.isFocused&&!i.isInputHidden),i.loading||R(i.wrapper,i.settings.loadingClass),i.trigger("load",e,t)}preload(){var e=this.wrapper.classList +e.contains("preloaded")||(e.add("preloaded"),this.load(""))}setTextboxValue(e=""){var t=this.control_input +t.value!==e&&(t.value=e,V(t,"update"),this.lastValue=e)}getValue(){return this.is_select_tag&&this.input.hasAttribute("multiple")?this.items:this.items.join(this.settings.delimiter)}setValue(e,t){te(this,t?[]:["change"],(()=>{this.clear(t),this.addItems(e,t)}))}setMaxItems(e){0===e&&(e=null),this.settings.maxItems=e,this.refreshState()}setActiveItem(e,t){var i,s,n,o,r,l,a=this +if("single"!==a.settings.mode){if(!e)return a.clearActiveItems(),void(a.isFocused&&a.inputState()) +if("click"===(i=t&&t.type.toLowerCase())&&ne("shiftKey",t)&&a.activeItems.length){for(l=a.getLastActive(),(n=Array.prototype.indexOf.call(a.control.children,l))>(o=Array.prototype.indexOf.call(a.control.children,e))&&(r=n,n=o,o=r),s=n;s<=o;s++)e=a.control.children[s],-1===a.activeItems.indexOf(e)&&a.setActiveItemClass(e) +ie(t)}else"click"===i&&ne(J,t)||"keydown"===i&&ne("shiftKey",t)?e.classList.contains("active")?a.removeActiveItem(e):a.setActiveItemClass(e):(a.clearActiveItems(),a.setActiveItemClass(e)) +a.inputState(),a.isFocused||a.focus()}}setActiveItemClass(e){const t=this,i=t.control.querySelector(".last-active") +i&&R(i,"last-active"),D(e,"active last-active"),t.trigger("item_select",e),-1==t.activeItems.indexOf(e)&&t.activeItems.push(e)}removeActiveItem(e){var t=this.activeItems.indexOf(e) +this.activeItems.splice(t,1),R(e,"active")}clearActiveItems(){R(this.activeItems,"active"),this.activeItems=[]}setActiveOption(e,t=!0){e!==this.activeOption&&(this.clearActiveOption(),e&&(this.activeOption=e,Q(this.focus_node,{"aria-activedescendant":e.getAttribute("id")}),Q(e,{"aria-selected":"true"}),D(e,"active"),t&&this.scrollToOption(e)))}scrollToOption(e,t){if(!e)return +const i=this.dropdown_content,s=i.clientHeight,n=i.scrollTop||0,o=e.offsetHeight,r=e.getBoundingClientRect().top-i.getBoundingClientRect().top+n +r+o>s+n?this.scroll(r-s+o,t):r{e.setActiveItemClass(t)})))}inputState(){var e=this +e.control.contains(e.control_input)&&(Q(e.control_input,{placeholder:e.settings.placeholder}),e.activeItems.length>0||!e.isFocused&&e.settings.hidePlaceholder&&e.items.length>0?(e.setTextboxValue(),e.isInputHidden=!0):(e.settings.hidePlaceholder&&e.items.length>0&&Q(e.control_input,{placeholder:""}),e.isInputHidden=!1),e.wrapper.classList.toggle("input-hidden",e.isInputHidden))}inputValue(){return this.control_input.value.trim()}focus(){var e=this +e.isDisabled||e.isReadOnly||(e.ignoreFocus=!0,e.control_input.offsetWidth?e.control_input.focus():e.focus_node.focus(),setTimeout((()=>{e.ignoreFocus=!1,e.onFocus()}),0))}blur(){this.focus_node.blur(),this.onBlur()}getScoreFunction(e){return this.sifter.getScoreFunction(e,this.getSearchOptions())}getSearchOptions(){var e=this.settings,t=e.sortField +return"string"==typeof e.sortField&&(t=[{field:e.sortField}]),{fields:e.searchField,conjunction:e.searchConjunction,sort:t,nesting:e.nesting}}search(e){var t,i,s=this,n=this.getSearchOptions() +if(s.settings.score&&"function"!=typeof(i=s.settings.score.call(s,e)))throw new Error('Tom Select "score" setting must be a function that returns a function') +return e!==s.lastQuery?(s.lastQuery=e,t=s.sifter.search(e,Object.assign(n,{score:i})),s.currentResults=t):t=Object.assign({},s.currentResults),s.settings.hideSelected&&(t.items=t.items.filter((e=>{let t=X(e.id) +return!(t&&-1!==s.items.indexOf(t))}))),t}refreshOptions(e=!0){var t,i,s,n,o,r,l,a,c,d +const u={},p=[] +var h=this,g=h.inputValue() +const f=g===h.lastQuery||""==g&&null==h.lastQuery +var v=h.search(g),m=null,y=h.settings.shouldOpen||!1,O=h.dropdown_content +f&&(m=h.activeOption)&&(c=m.closest("[data-group]")),n=v.items.length,"number"==typeof h.settings.maxOptions&&(n=Math.min(n,h.settings.maxOptions)),n>0&&(y=!0) +const b=(e,t)=>{let i=u[e] +if(void 0!==i){let e=p[i] +if(void 0!==e)return[i,e.fragment]}let s=document.createDocumentFragment() +return i=p.length,p.push({fragment:s,order:t,optgroup:e}),[i,s]} +for(t=0;t0&&(d=d.cloneNode(!0),Q(d,{id:l.$id+"-clone-"+i,"aria-selected":null}),d.classList.add("ts-cloned"),R(d,"active"),h.activeOption&&h.activeOption.dataset.value==n&&c&&c.dataset.group===o.toString()&&(m=d)),a.appendChild(d),""!=o&&(u[o]=s)}}var w +h.settings.lockOptgroupOrder&&p.sort(((e,t)=>e.order-t.order)),l=document.createDocumentFragment(),E(p,(e=>{let t=e.fragment,i=e.optgroup +if(!t||!t.children.length)return +let s=h.optgroups[i] +if(void 0!==s){let e=document.createDocumentFragment(),i=h.render("optgroup_header",s) +le(e,i),le(e,t) +let n=h.render("optgroup",{group:s,options:e}) +le(l,n)}else le(l,t)})),O.innerHTML="",le(O,l),h.settings.highlight&&(w=O.querySelectorAll("span.highlight"),Array.prototype.forEach.call(w,(function(e){var t=e.parentNode +t.replaceChild(e.firstChild,e),t.normalize()})),v.query.length&&v.tokens.length&&E(v.tokens,(e=>{U(O,e.regex)}))) +var _=e=>{let t=h.render(e,{input:g}) +return t&&(y=!0,O.insertBefore(t,O.firstChild)),t} +if(h.loading?_("loading"):h.settings.shouldLoad.call(h,g)?0===v.items.length&&_("no_results"):_("not_loading"),(a=h.canCreate(g))&&(d=_("option_create")),h.hasOptions=v.items.length>0||a,y){if(v.items.length>0){if(m||"single"!==h.settings.mode||null==h.items[0]||(m=h.getOption(h.items[0])),!O.contains(m)){let e=0 +d&&!h.settings.addPrecedence&&(e=1),m=h.selectable()[e]}}else d&&(m=d) +e&&!h.isOpen&&(h.open(),h.scrollToOption(m,"auto")),h.setActiveOption(m)}else h.clearActiveOption(),e&&h.isOpen&&h.close(!1)}selectable(){return this.dropdown_content.querySelectorAll("[data-selectable]")}addOption(e,t=!1){const i=this +if(Array.isArray(e))return i.addOptions(e,t),!1 +const s=X(e[i.settings.valueField]) +return null!==s&&!i.options.hasOwnProperty(s)&&(e.$order=e.$order||++i.order,e.$id=i.inputId+"-opt-"+e.$order,i.options[s]=e,i.lastQuery=null,t&&(i.userOptions[s]=t,i.trigger("option_add",s,e)),s)}addOptions(e,t=!1){E(e,(e=>{this.addOption(e,t)}))}registerOption(e){return this.addOption(e)}registerOptionGroup(e){var t=X(e[this.settings.optgroupValueField]) +return null!==t&&(e.$order=e.$order||++this.order,this.optgroups[t]=e,t)}addOptionGroup(e,t){var i +t[this.settings.optgroupValueField]=e,(i=this.registerOptionGroup(t))&&this.trigger("optgroup_add",i,t)}removeOptionGroup(e){this.optgroups.hasOwnProperty(e)&&(delete this.optgroups[e],this.clearCache(),this.trigger("optgroup_remove",e))}clearOptionGroups(){this.optgroups={},this.clearCache(),this.trigger("optgroup_clear")}updateOption(e,t){const i=this +var s,n +const o=X(e),r=X(t[i.settings.valueField]) +if(null===o)return +const l=i.options[o] +if(null==l)return +if("string"!=typeof r)throw new Error("Value must be set in option data") +const a=i.getOption(o),c=i.getItem(o) +if(t.$order=t.$order||l.$order,delete i.options[o],i.uncacheValue(r),i.options[r]=t,a){if(i.dropdown_content.contains(a)){const e=i._render("option",t) +G(a,e),i.activeOption===a&&i.setActiveOption(e)}a.remove()}c&&(-1!==(n=i.items.indexOf(o))&&i.items.splice(n,1,r),s=i._render("item",t),c.classList.contains("active")&&D(s,"active"),G(c,s)),i.lastQuery=null}removeOption(e,t){const i=this +e=Y(e),i.uncacheValue(e),delete i.userOptions[e],delete i.options[e],i.lastQuery=null,i.trigger("option_remove",e),i.removeItem(e,t)}clearOptions(e){const t=(e||this.clearFilter).bind(this) +this.loadedSearches={},this.userOptions={},this.clearCache() +const i={} +E(this.options,((e,s)=>{t(e,s)&&(i[s]=e)})),this.options=this.sifter.items=i,this.lastQuery=null,this.trigger("option_clear")}clearFilter(e,t){return this.items.indexOf(t)>=0}getOption(e,t=!1){const i=X(e) +if(null===i)return null +const s=this.options[i] +if(null!=s){if(s.$div)return s.$div +if(t)return this._render("option",s)}return null}getAdjacent(e,t,i="option"){var s +if(!e)return null +s="item"==i?this.controlChildren():this.dropdown_content.querySelectorAll("[data-selectable]") +for(let i=0;i0?s[i+1]:s[i-1] +return null}getItem(e){if("object"==typeof e)return e +var t=X(e) +return null!==t?this.control.querySelector(`[data-value="${re(t)}"]`):null}addItems(e,t){var i=this,s=Array.isArray(e)?e:[e] +const n=(s=s.filter((e=>-1===i.items.indexOf(e))))[s.length-1] +s.forEach((e=>{i.isPending=e!==n,i.addItem(e,t)}))}addItem(e,t){te(this,t?[]:["change","dropdown_close"],(()=>{var i,s +const n=this,o=n.settings.mode,r=X(e) +if((!r||-1===n.items.indexOf(r)||("single"===o&&n.close(),"single"!==o&&n.settings.duplicates))&&null!==r&&n.options.hasOwnProperty(r)&&("single"===o&&n.clear(t),"multi"!==o||!n.isFull())){if(i=n._render("item",n.options[r]),n.control.contains(i)&&(i=i.cloneNode(!0)),s=n.isFull(),n.items.splice(n.caretPos,0,r),n.insertAtCaret(i),n.isSetup){if(!n.isPending&&n.settings.hideSelected){let e=n.getOption(r),t=n.getAdjacent(e,1) +t&&n.setActiveOption(t)}n.isPending||n.settings.closeAfterSelect||n.refreshOptions(n.isFocused&&"single"!==o),0!=n.settings.closeAfterSelect&&n.isFull()?n.close():n.isPending||n.positionDropdown(),n.trigger("item_add",r,i),n.isPending||n.updateOriginalInput({silent:t})}(!n.isPending||!s&&n.isFull())&&(n.inputState(),n.refreshState())}}))}removeItem(e=null,t){const i=this +if(!(e=i.getItem(e)))return +var s,n +const o=e.dataset.value +s=K(e),e.remove(),e.classList.contains("active")&&(n=i.activeItems.indexOf(e),i.activeItems.splice(n,1),R(e,"active")),i.items.splice(s,1),i.lastQuery=null,!i.settings.persist&&i.userOptions.hasOwnProperty(o)&&i.removeOption(o,t),s{})){3===arguments.length&&(t=arguments[2]),"function"!=typeof t&&(t=()=>{}) +var i,s=this,n=s.caretPos +if(e=e||s.inputValue(),!s.canCreate(e))return t(),!1 +s.lock() +var o=!1,r=e=>{if(s.unlock(),!e||"object"!=typeof e)return t() +var i=X(e[s.settings.valueField]) +if("string"!=typeof i)return t() +s.setTextboxValue(),s.addOption(e,!0),s.setCaret(n),s.addItem(i),t(e),o=!0} +return i="function"==typeof s.settings.create?s.settings.create.call(this,e,r):{[s.settings.labelField]:e,[s.settings.valueField]:e},o||r(i),!0}refreshItems(){var e=this +e.lastQuery=null,e.isSetup&&e.addItems(e.items),e.updateOriginalInput(),e.refreshState()}refreshState(){const e=this +e.refreshValidityState() +const t=e.isFull(),i=e.isLocked +e.wrapper.classList.toggle("rtl",e.rtl) +const s=e.wrapper.classList +var n +s.toggle("focus",e.isFocused),s.toggle("disabled",e.isDisabled),s.toggle("readonly",e.isReadOnly),s.toggle("required",e.isRequired),s.toggle("invalid",!e.isValid),s.toggle("locked",i),s.toggle("full",t),s.toggle("input-active",e.isFocused&&!e.isInputHidden),s.toggle("dropdown-active",e.isOpen),s.toggle("has-options",(n=e.options,0===Object.keys(n).length)),s.toggle("has-items",e.items.length>0)}refreshValidityState(){var e=this +e.input.validity&&(e.isValid=e.input.validity.valid,e.isInvalid=!e.isValid)}isFull(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems}updateOriginalInput(e={}){const t=this +var i,s +const n=t.input.querySelector('option[value=""]') +if(t.is_select_tag){const o=[],r=t.input.querySelectorAll("option:checked").length +function l(e,i,s){return e||(e=j('")),e!=n&&t.input.append(e),o.push(e),(e!=n||r>0)&&(e.selected=!0),e}t.input.querySelectorAll("option:checked").forEach((e=>{e.selected=!1})),0==t.items.length&&"single"==t.settings.mode?l(n,"",""):t.items.forEach((e=>{if(i=t.options[e],s=i[t.settings.labelField]||"",o.includes(i.$option)){l(t.input.querySelector(`option[value="${re(e)}"]:not(:checked)`),e,s)}else i.$option=l(i.$option,e,s)}))}else t.input.value=t.getValue() +t.isSetup&&(e.silent||t.trigger("change",t.getValue()))}open(){var e=this +e.isLocked||e.isOpen||"multi"===e.settings.mode&&e.isFull()||(e.isOpen=!0,Q(e.focus_node,{"aria-expanded":"true"}),e.refreshState(),q(e.dropdown,{visibility:"hidden",display:"block"}),e.positionDropdown(),q(e.dropdown,{visibility:"visible",display:"block"}),e.focus(),e.trigger("dropdown_open",e.dropdown))}close(e=!0){var t=this,i=t.isOpen +e&&(t.setTextboxValue(),"single"===t.settings.mode&&t.items.length&&t.inputState()),t.isOpen=!1,Q(t.focus_node,{"aria-expanded":"false"}),q(t.dropdown,{display:"none"}),t.settings.hideSelected&&t.clearActiveOption(),t.refreshState(),i&&t.trigger("dropdown_close",t.dropdown)}positionDropdown(){if("body"===this.settings.dropdownParent){var e=this.control,t=e.getBoundingClientRect(),i=e.offsetHeight+t.top+window.scrollY,s=t.left+window.scrollX +q(this.dropdown,{width:t.width+"px",top:i+"px",left:s+"px"})}}clear(e){var t=this +if(t.items.length){var i=t.controlChildren() +E(i,(e=>{t.removeItem(e,!0)})),t.inputState(),e||t.updateOriginalInput(),t.trigger("clear")}}insertAtCaret(e){const t=this,i=t.caretPos,s=t.control +s.insertBefore(e,s.children[i]||null),t.setCaret(i+1)}deleteSelection(e){var t,i,s,n,o,r=this +t=e&&8===e.keyCode?-1:1,i={start:(o=r.control_input).selectionStart||0,length:(o.selectionEnd||0)-(o.selectionStart||0)} +const l=[] +if(r.activeItems.length)n=B(r.activeItems,t),s=K(n),t>0&&s++,E(r.activeItems,(e=>l.push(e))) +else if((r.isFocused||"single"===r.settings.mode)&&r.items.length){const e=r.controlChildren() +let s +t<0&&0===i.start&&0===i.length?s=e[r.caretPos-1]:t>0&&i.start===r.inputValue().length&&(s=e[r.caretPos]),void 0!==s&&l.push(s)}if(!r.shouldDelete(l,e))return!1 +for(ie(e,!0),void 0!==s&&r.setCaret(s);l.length;)r.removeItem(l.pop()) +return r.inputState(),r.positionDropdown(),r.refreshOptions(!1),!0}shouldDelete(e,t){const i=e.map((e=>e.dataset.value)) +return!(!i.length||"function"==typeof this.settings.onDelete&&!1===this.settings.onDelete(i,t))}advanceSelection(e,t){var i,s,n=this +n.rtl&&(e*=-1),n.inputValue().length||(ne(J,t)||ne("shiftKey",t)?(s=(i=n.getLastActive(e))?i.classList.contains("active")?n.getAdjacent(i,e,"item"):i:e>0?n.control_input.nextElementSibling:n.control_input.previousElementSibling)&&(s.classList.contains("active")&&n.removeActiveItem(i),n.setActiveItemClass(s)):n.moveCaret(e))}moveCaret(e){}getLastActive(e){let t=this.control.querySelector(".last-active") +if(t)return t +var i=this.control.querySelectorAll(".active") +return i?B(i,e):void 0}setCaret(e){this.caretPos=this.items.length}controlChildren(){return Array.from(this.control.querySelectorAll("[data-ts-item]"))}lock(){this.setLocked(!0)}unlock(){this.setLocked(!1)}setLocked(e=this.isReadOnly||this.isDisabled){this.isLocked=e,this.refreshState()}disable(){this.setDisabled(!0),this.close()}enable(){this.setDisabled(!1)}setDisabled(e){this.focus_node.tabIndex=e?-1:this.tabIndex,this.isDisabled=e,this.input.disabled=e,this.control_input.disabled=e,this.setLocked()}setReadOnly(e){this.isReadOnly=e,this.input.readOnly=e,this.control_input.readOnly=e,this.setLocked()}destroy(){var e=this,t=e.revertSettings +e.trigger("destroy"),e.off(),e.wrapper.remove(),e.dropdown.remove(),e.input.innerHTML=t.innerHTML,e.input.tabIndex=t.tabIndex,R(e.input,"tomselected","ts-hidden-accessible"),e._destroy(),delete e.input.tomselect}render(e,t){var i,s +const n=this +if("function"!=typeof this.settings.render[e])return null +if(!(s=n.settings.render[e].call(this,t,Z)))return null +if(s=j(s),"option"===e||"option_create"===e?t[n.settings.disabledField]?Q(s,{"aria-disabled":"true"}):Q(s,{"data-selectable":""}):"optgroup"===e&&(i=t.group[n.settings.optgroupValueField],Q(s,{"data-group":i}),t.group[n.settings.disabledField]&&Q(s,{"data-disabled":""})),"option"===e||"item"===e){const i=Y(t[n.settings.valueField]) +Q(s,{"data-value":i}),"item"===e?(D(s,n.settings.itemClass),Q(s,{"data-ts-item":""})):(D(s,n.settings.optionClass),Q(s,{role:"option",id:t.$id}),t.$div=s,n.options[i]=t)}return s}_render(e,t){const i=this.render(e,t) +if(null==i)throw"HTMLElement expected" +return i}clearCache(){E(this.options,(e=>{e.$div&&(e.$div.remove(),delete e.$div)}))}uncacheValue(e){const t=this.getOption(e) +t&&t.remove()}canCreate(e){return this.settings.create&&e.length>0&&this.settings.createFilter.call(this,e)}hook(e,t,i){var s=this,n=s[t] +s[t]=function(){var t,o +return"after"===e&&(t=n.apply(s,arguments)),o=i.apply(s,arguments),"instead"===e?o:("before"===e&&(t=n.apply(s,arguments)),t)}}}return de.define("change_listener",(function(){se(this.input,"change",(()=>{this.sync()}))})),de.define("checkbox_options",(function(e){var t=this,i=t.onOptionSelect +t.settings.hideSelected=!1 +const s=Object.assign({className:"tomselect-checkbox",checkedClassNames:void 0,uncheckedClassNames:void 0},e) +var n=function(e,t){t?(e.checked=!0,s.uncheckedClassNames&&e.classList.remove(...s.uncheckedClassNames),s.checkedClassNames&&e.classList.add(...s.checkedClassNames)):(e.checked=!1,s.checkedClassNames&&e.classList.remove(...s.checkedClassNames),s.uncheckedClassNames&&e.classList.add(...s.uncheckedClassNames))},o=function(e){setTimeout((()=>{var t=e.querySelector("input."+s.className) +t instanceof HTMLInputElement&&n(t,e.classList.contains("selected"))}),1)} +t.hook("after","setupTemplates",(()=>{var e=t.settings.render.option +t.settings.render.option=(i,o)=>{var r=j(e.call(t,i,o)),l=document.createElement("input") +s.className&&l.classList.add(s.className),l.addEventListener("click",(function(e){ie(e)})),l.type="checkbox" +const a=X(i[t.settings.valueField]) +return n(l,!!(a&&t.items.indexOf(a)>-1)),r.prepend(l),r}})),t.on("item_remove",(e=>{var i=t.getOption(e) +i&&(i.classList.remove("selected"),o(i))})),t.on("item_add",(e=>{var i=t.getOption(e) +i&&o(i)})),t.hook("instead","onOptionSelect",((e,s)=>{if(s.classList.contains("selected"))return s.classList.remove("selected"),t.removeItem(s.dataset.value),t.refreshOptions(),void ie(e,!0) +i.call(t,e,s),o(s)}))})),de.define("clear_button",(function(e){const t=this,i=Object.assign({className:"clear-button",title:"Clear All",html:e=>`
`},e) +t.on("initialize",(()=>{var e=j(i.html(i)) +e.addEventListener("click",(e=>{t.isLocked||(t.clear(),"single"===t.settings.mode&&t.settings.allowEmptyOption&&t.addItem(""),e.preventDefault(),e.stopPropagation())})),t.control.appendChild(e)}))})),de.define("drag_drop",(function(){var e=this +if("multi"!==e.settings.mode)return +var t=e.lock,i=e.unlock +let s,n=!0 +e.hook("after","setupTemplates",(()=>{var t=e.settings.render.item +e.settings.render.item=(i,o)=>{const r=j(t.call(e,i,o)) +Q(r,{draggable:"true"}) +const l=e=>{e.preventDefault(),r.classList.add("ts-drag-over"),a(r,s)},a=(e,t)=>{var i,s,n +void 0!==t&&(((e,t)=>{do{var i +if(e==(t=null==(i=t)?void 0:i.previousElementSibling))return!0}while(t&&t.previousElementSibling) +return!1})(t,r)?(s=t,null==(n=(i=e).parentNode)||n.insertBefore(s,i.nextSibling)):((e,t)=>{var i +null==(i=e.parentNode)||i.insertBefore(t,e)})(e,t))} +return se(r,"mousedown",(e=>{n||ie(e),e.stopPropagation()})),se(r,"dragstart",(e=>{s=r,setTimeout((()=>{r.classList.add("ts-dragging")}),0)})),se(r,"dragenter",l),se(r,"dragover",l),se(r,"dragleave",(()=>{r.classList.remove("ts-drag-over")})),se(r,"dragend",(()=>{var t +document.querySelectorAll(".ts-drag-over").forEach((e=>e.classList.remove("ts-drag-over"))),null==(t=s)||t.classList.remove("ts-dragging"),s=void 0 +var i=[] +e.control.querySelectorAll("[data-value]").forEach((e=>{if(e.dataset.value){let t=e.dataset.value +t&&i.push(t)}})),e.setValue(i)})),r}})),e.hook("instead","lock",(()=>(n=!1,t.call(e)))),e.hook("instead","unlock",(()=>(n=!0,i.call(e))))})),de.define("dropdown_header",(function(e){const t=this,i=Object.assign({title:"Untitled",headerClass:"dropdown-header",titleRowClass:"dropdown-header-title",labelClass:"dropdown-header-label",closeClass:"dropdown-header-close",html:e=>'
'+e.title+'×
'},e) +t.on("initialize",(()=>{var e=j(i.html(i)),s=e.querySelector("."+i.closeClass) +s&&s.addEventListener("click",(e=>{ie(e,!0),t.close()})),t.dropdown.insertBefore(e,t.dropdown.firstChild)}))})),de.define("caret_position",(function(){var e=this +e.hook("instead","setCaret",(t=>{"single"!==e.settings.mode&&e.control.contains(e.control_input)?(t=Math.max(0,Math.min(e.items.length,t)))==e.caretPos||e.isPending||e.controlChildren().forEach(((i,s)=>{s{if(!e.isFocused)return +const i=e.getLastActive(t) +if(i){const s=K(i) +e.setCaret(t>0?s+1:s),e.setActiveItem(),R(i,"last-active")}else e.setCaret(e.caretPos+t)}))})),de.define("dropdown_input",(function(){const e=this +e.settings.shouldOpen=!0,e.hook("before","setup",(()=>{e.focus_node=e.control,D(e.control_input,"dropdown-input") +const t=j(' {% if appreciation.custom_message %}
@@ -199,16 +238,6 @@

{{ appreciation.custom_message }}

{% endif %} - {% if appreciation.cc_list %} -
-

{% trans "CC" %}

-
- {% for email in appreciation.cc_list %} - {{ email }} - {% endfor %} -
-
- {% endif %} {% if appreciation.acknowledged_at %}
@@ -219,6 +248,20 @@ {% endif %} {% endif %} + +
+

+ + {% trans "Appreciation Letter" %} +

+

{% trans "Generate a formal letter of appreciation as a PDF." %}

+ + + {% trans "Download Letter" %} + +
{% if appreciation.ai_analysis %} @@ -273,74 +316,134 @@
+
- {% if can_activate %} -
-

- {% trans "Activate Appreciation" %} -

-
- {% csrf_token %} -

{% trans "Activate this appreciation to make it ready for routing to a department." %}

- -
-
- {% endif %} - - {% if can_send %} -
+

- {% trans "Send to Department" %} + {% trans "Details" %}

-

{% trans "Route this appreciation to a department or a specific person for acknowledgment." %}

- +
+
+

{% trans "Status" %}

+

{{ appreciation.get_status_display }}

+
+ {% if appreciation.category %} +
+

{% trans "Category" %}

+

{{ appreciation.category.get_localized_name }}

+
+ {% endif %} + {% if appreciation.activated_at %} +
+

{% trans "Activated" %}

+

{{ appreciation.activated_at|date:"Y-m-d H:i" }}

+
+ {% endif %} + {% if appreciation.department %} +
+

{% trans "Department" %}

+

{{ appreciation.department.get_localized_name }}

+
+ {% endif %} + {% if appreciation.recipient %} +
+

{% trans "Recipient" %}

+

{{ appreciation.get_recipient_name }}

+
+ {% endif %} + {% if appreciation.sent_at %} +
+

{% trans "Sent At" %}

+

{{ appreciation.sent_at|date:"Y-m-d H:i" }}

+
+ {% endif %} + {% if appreciation.acknowledged_at %} +
+
+ +

{% trans "Acknowledged" %} {{ appreciation.acknowledged_at|date:"Y-m-d H:i" }}

+
+
+ {% endif %} +
- {% endif %}
- - diff --git a/templates/organizations/department_detail.html b/templates/organizations/department_detail.html index 8d39441..cf8bb5a 100644 --- a/templates/organizations/department_detail.html +++ b/templates/organizations/department_detail.html @@ -212,12 +212,11 @@ {% trans "Respond" %} {% elif action.type == 'inquiry_response' %} - + + + {% trans "Handle" %} + {% else %} @@ -470,7 +469,7 @@ {% trans "Complaints" %} ({{ stats.total_complaints }}) + +
+ + + @@ -833,15 +864,23 @@ + + {% empty %} - + {% endfor %}
{% trans "Reference" %} {% trans "Subject" %} {% trans "Contact" %}{% trans "Department" %}{% trans "Outgoing Dept" %} {% trans "Assigned To" %} {% trans "Status" %} {% trans "Created" %}{{ i.reference_number }} {{ i.subject }} {{ i.contact_name|default:"-" }}{{ i.department.name|default:"-" }}{{ i.outgoing_department.name|default:"-" }} {{ i.assigned_to.get_full_name|default:"-" }} {{ i.get_status_display }} + {% if i.contact_status == 'contacted_no_response' %} + {% trans "No Response" %} + {% elif i.contact_status == 'contacted' %} + {% trans "Contacted" %} + {% endif %} {{ i.created_at|date:"Y-m-d" }} @@ -849,16 +888,16 @@ {% trans "View" %} - {% if can_respond and i.status in 'open,in_progress' %} - - {% trans "Respond" %} + {% if can_respond and i.handling_department_id == department.pk and i.status in 'open,in_progress' %} + + {% trans "Handle" %} {% endif %}
{% trans "No inquiries for this department" %}
{% trans "No inquiries for this department" %}
@@ -1434,6 +1473,7 @@ {% trans "Start Date" as t_start_date %} {% trans "Target Completion" as t_target_date %} {% trans "Open Project" as t_open_project %} +{% trans "Department Response" as t_dept_response %} var recordModalLabels = { complaint: '{{ t_complaint|escapejs }}', @@ -1478,6 +1518,7 @@ var recordModalLabels = { start_date: '{{ t_start_date|escapejs }}', target_date: '{{ t_target_date|escapejs }}', open_project: '{{ t_open_project|escapejs }}', + dept_response: '{{ t_dept_response|escapejs }}', }; var recordApiUrls = { @@ -1562,6 +1603,9 @@ function renderRecordModal(type, d) { if (type === 'inquiry') { html += infoCard(recordModalLabels.contact, [d.contact_name, d.contact_phone, d.contact_email].filter(function(v) { return v && v !== '-'; }).join(' | ')); if (d.category && d.category !== '-') html += infoCard(recordModalLabels.category, d.category); + if (d.priority_display) html += infoCard(recordModalLabels.priority, d.priority_display); + if (d.contact_status_display) html += infoCard(recordModalLabels.contact, d.contact_status_display); + if (d.outgoing_department) html += infoCard(recordModalLabels.department + ' (Outgoing)', d.outgoing_department); } if (type === 'observation') { html += infoCard(recordModalLabels.reporter, d.reporter || '-'); @@ -1624,6 +1668,35 @@ function renderRecordModal(type, d) { html += '
'; } + if (type === 'inquiry' && (d.department_response_en || d.department_response_ar)) { + html += '
'; + html += '

' + recordModalLabels.dept_response; + if (d.department_responded_at) html += '' + escHtml(d.department_responded_at) + ''; + html += '

'; + if (d.department_response_en) html += '

' + escHtml(d.department_response_en) + '

'; + if (d.department_response_ar) html += '

' + escHtml(d.department_response_ar) + '

'; + html += '
'; + } + + if (type === 'inquiry' && d.response) { + html += '
'; + html += '

{% trans "Response to Patient" | escapejs %}'; + if (d.responded_at) html += '' + escHtml(d.responded_at) + ''; + html += '

'; + if (d.responded_by) html += '

{% trans "By" | escapejs %} ' + escHtml(d.responded_by) + '

'; + html += '

' + escHtml(d.response) + '

'; + html += '
'; + } + + if (type === 'inquiry' && d.resolved_at) { + html += '
'; + html += ''; + html += '{% trans "Resolved" | escapejs %} ' + escHtml(d.resolved_at); + if (d.resolved_by) html += ' · ' + escHtml(d.resolved_by); + html += ''; + html += '
'; + } + if (type === 'appreciation') { if (d.description_ar) { html += '
'; diff --git a/templates/organizations/department_inquiries.html b/templates/organizations/department_inquiries.html index d2e07a3..d8426b8 100644 --- a/templates/organizations/department_inquiries.html +++ b/templates/organizations/department_inquiries.html @@ -20,6 +20,30 @@
+ +
+ +
@@ -34,6 +58,7 @@ + + + + + {% if inquiry.status == 'in_progress' %} + +
+ {% csrf_token %} + + + +
+ + +
+ {% csrf_token %} + + + +
+ + +
+ {% csrf_token %} + + + +
+ + + + {% endif %} +
+ {% endif %} + {% if inquiry.subject %}

{{ inquiry.subject }}

{% endif %} @@ -111,6 +168,16 @@

{% trans "Created" %}

{{ inquiry.created_at|date:"Y-m-d H:i" }}

+
+

{% trans "Contact Status" %}

+

+ {{ inquiry.get_contact_status_display|default:"—" }} + {% if inquiry.contact_status_at %}{{ inquiry.contact_status_at|date:"Y-m-d H:i" }}{% endif %} +

+
@@ -135,5 +202,5 @@ -{% include "components/department_response_modal.html" %} +{% include "components/send_to_modal.html" %} {% endblock %} diff --git a/templates/organizations/department_observation_detail.html b/templates/organizations/department_observation_detail.html index 109e962..fe1c1da 100644 --- a/templates/organizations/department_observation_detail.html +++ b/templates/organizations/department_observation_detail.html @@ -41,7 +41,7 @@
{% if can_respond and not observation.department_responded_at %} - diff --git a/templates/presentations/template_detail.html b/templates/presentations/template_detail.html index d297c0a..8da389d 100644 --- a/templates/presentations/template_detail.html +++ b/templates/presentations/template_detail.html @@ -4,7 +4,6 @@ {% block title %}{{ template.name }} — PX360{% endblock %} {% block extra_css %} -