Browse Source

initial commit

= 3 years ago
commit
86887c57df
100 changed files with 14812 additions and 0 deletions
  1. 15 0
      .editorconfig
  2. 57 0
      .env-example
  3. 5 0
      .gitattributes
  4. 18 0
      .gitignore
  5. 13 0
      .styleci.yml
  6. 79 0
      README.md
  7. 257 0
      app/Console/Commands/Gem.php
  8. 79 0
      app/Console/Commands/InstantiateStatTree.php
  9. 143 0
      app/Console/Commands/TestCriteriaQueries.php
  10. 105 0
      app/Console/Commands/canvasToClientRmReasons.php
  11. 41 0
      app/Console/Kernel.php
  12. 55 0
      app/Exceptions/Handler.php
  13. 252 0
      app/Helpers/TimeLine.php
  14. 17 0
      app/Helpers/TimeSlot.php
  15. 386 0
      app/Helpers/class.Diff.php
  16. 642 0
      app/Helpers/fdb.php
  17. 759 0
      app/Helpers/helpers.php
  18. 219 0
      app/Http/Controllers/AdminController.php
  19. 306 0
      app/Http/Controllers/AppointmentController.php
  20. 39 0
      app/Http/Controllers/CareMonthController.php
  21. 44 0
      app/Http/Controllers/ClauseArgController.php
  22. 99 0
      app/Http/Controllers/ClauseController.php
  23. 135 0
      app/Http/Controllers/Controller.php
  24. 282 0
      app/Http/Controllers/DnaController.php
  25. 758 0
      app/Http/Controllers/FDBPGController.php
  26. 106 0
      app/Http/Controllers/GuestController.php
  27. 2075 0
      app/Http/Controllers/HomeController.php
  28. 208 0
      app/Http/Controllers/LoginController.php
  29. 421 0
      app/Http/Controllers/McpController.php
  30. 415 0
      app/Http/Controllers/NoteController.php
  31. 628 0
      app/Http/Controllers/PatientController.php
  32. 28 0
      app/Http/Controllers/PayerController.php
  33. 2640 0
      app/Http/Controllers/PracticeManagementController.php
  34. 697 0
      app/Http/Controllers/StatTreeController.php
  35. 320 0
      app/Http/Controllers/StatTreeLineController.php
  36. 48 0
      app/Http/Controllers/StatTreeLineReportColumnController.php
  37. 30 0
      app/Http/Controllers/TicketController.php
  38. 74 0
      app/Http/Kernel.php
  39. 21 0
      app/Http/Middleware/Authenticate.php
  40. 17 0
      app/Http/Middleware/CheckForMaintenanceMode.php
  41. 18 0
      app/Http/Middleware/EncryptCookies.php
  42. 30 0
      app/Http/Middleware/EnsureAdminPro.php
  43. 27 0
      app/Http/Middleware/EnsureClientIsNotShadowOfPro.php
  44. 29 0
      app/Http/Middleware/EnsureMcpPro.php
  45. 29 0
      app/Http/Middleware/EnsureNaPro.php
  46. 37 0
      app/Http/Middleware/EnsureProCanAccessPatient.php
  47. 51 0
      app/Http/Middleware/ProAuthenticated.php
  48. 28 0
      app/Http/Middleware/RedirectAuthenticatedPro.php
  49. 27 0
      app/Http/Middleware/RedirectIfAuthenticated.php
  50. 18 0
      app/Http/Middleware/TrimStrings.php
  51. 20 0
      app/Http/Middleware/TrustHosts.php
  52. 23 0
      app/Http/Middleware/TrustProxies.php
  53. 23 0
      app/Http/Middleware/VerifyCsrfToken.php
  54. 62 0
      app/Lib/Backend.php
  55. 11 0
      app/Models/Account.php
  56. 19 0
      app/Models/AccountClient.php
  57. 21 0
      app/Models/AccountInvite.php
  58. 32 0
      app/Models/ActionItem.php
  59. 13 0
      app/Models/ActionItemContentUpdate.php
  60. 15 0
      app/Models/ActionItemFax.php
  61. 10 0
      app/Models/ActionItemStatusUpdate.php
  62. 10 0
      app/Models/Amendment.php
  63. 10 0
      app/Models/AmendmentDecision.php
  64. 21 0
      app/Models/AppSession.php
  65. 35 0
      app/Models/Appointment.php
  66. 17 0
      app/Models/AppointmentConfirmationDecision.php
  67. 13 0
      app/Models/AppointmentConfirmationRequest.php
  68. 9 0
      app/Models/BDTDevice.php
  69. 18 0
      app/Models/BDTMeasurement.php
  70. 43 0
      app/Models/Bill.php
  71. 10 0
      app/Models/BillCareMonthEntry.php
  72. 10 0
      app/Models/BillStatusUpdate.php
  73. 34 0
      app/Models/BillingReport.php
  74. 117 0
      app/Models/CareMonth.php
  75. 10 0
      app/Models/CareMonthCmRmReason.php
  76. 18 0
      app/Models/CareMonthEntry.php
  77. 52 0
      app/Models/Claim.php
  78. 16 0
      app/Models/ClaimEDI.php
  79. 25 0
      app/Models/ClaimLine.php
  80. 12 0
      app/Models/ClaimLineIcd.php
  81. 12 0
      app/Models/ClaimVersion.php
  82. 16 0
      app/Models/Clause.php
  83. 13 0
      app/Models/ClauseArg.php
  84. 891 0
      app/Models/Client.php
  85. 10 0
      app/Models/ClientAllyUpdate.php
  86. 34 0
      app/Models/ClientBDTDevice.php
  87. 17 0
      app/Models/ClientBDTMeasurement.php
  88. 11 0
      app/Models/ClientCanvasDataCustomItem.php
  89. 10 0
      app/Models/ClientCmRmReason.php
  90. 10 0
      app/Models/ClientDocument.php
  91. 19 0
      app/Models/ClientEligibleRefresh.php
  92. 28 0
      app/Models/ClientInfoLine.php
  93. 10 0
      app/Models/ClientInfoLineUpdate.php
  94. 19 0
      app/Models/ClientMBPayerValidationResult.php
  95. 10 0
      app/Models/ClientMcpUpdate.php
  96. 15 0
      app/Models/ClientMeasurementDaysPerMonth.php
  97. 31 0
      app/Models/ClientMemo.php
  98. 17 0
      app/Models/ClientMemoUpdate.php
  99. 10 0
      app/Models/ClientPointUpdate.php
  100. 73 0
      app/Models/ClientPrimaryCoverage.php

+ 15 - 0
.editorconfig

@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2

+ 57 - 0
.env-example

@@ -0,0 +1,57 @@
+APP_NAME=Stag
+APP_ENV=local
+APP_KEY=base64:1yKRgeLWTeEXTfh51vXtJaf6GJaRUn4NHzdlOdyDpRY=
+APP_DEBUG=true
+APP_URL=http://localhost
+
+LOG_CHANNEL=stack
+
+DB_CONNECTION=pgsql
+DB_HOST=127.0.0.1
+DB_PORT=5432
+DB_DATABASE=stag2
+DB_USERNAME=postgres
+DB_PASSWORD=pass
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=file
+QUEUE_CONNECTION=sync
+SESSION_DRIVER=file
+SESSION_LIFETIME=120
+
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=smtp.mailtrap.io
+MAIL_PORT=2525
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+MAIL_ENCRYPTION=null
+MAIL_FROM_ADDRESS=null
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_APP_CLUSTER=mt1
+
+MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
+
+BACKEND_URL="http://localhost:8080/api"
+ADMIN_PORTAL_URL="http://localhost:3000"
+
+AUTH_URL="http://localhost:3002"
+
+BACKEND_WS_URL=http://localhost:8080/ws
+AGORA_APPID=9d04292b03524927a8fe9d937a448d85
+AGORA_MODE=screen
+
+POINT_IMPL_DATE=2021-10-29

+ 5 - 0
.gitattributes

@@ -0,0 +1,5 @@
+* text=auto
+*.css linguist-vendored
+*.scss linguist-vendored
+*.js linguist-vendored
+CHANGELOG.md export-ignore

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+/node_modules
+/public/hot
+/public/storage
+/storage/*.key
+/vendor
+.env
+.env.backup
+.phpunit.result.cache
+Homestead.json
+Homestead.yaml
+npm-debug.log
+yarn-error.log
+/.idea
+/public/js/app.js.LICENSE.txt
+/public/fullcalendar-5.3.2/examples/
+/public/fullcalendar-5.3.2/LICENSE.txt
+/public/fullcalendar-5.3.2/README.md
+/page-sections

+ 13 - 0
.styleci.yml

@@ -0,0 +1,13 @@
+php:
+  preset: laravel
+  disabled:
+    - unused_use
+  finder:
+    not-name:
+      - index.php
+      - server.php
+js:
+  finder:
+    not-name:
+      - webpack.mix.js
+css: true

+ 79 - 0
README.md

@@ -0,0 +1,79 @@
+<p align="center"><img src="https://res.cloudinary.com/dtfbvvkyp/image/upload/v1566331377/laravel-logolockup-cmyk-red.svg" width="400"></p>
+
+<p align="center">
+<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.svg" alt="Build Status"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/d/total.svg" alt="Total Downloads"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/v/stable.svg" alt="Latest Stable Version"></a>
+<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/license.svg" alt="License"></a>
+</p>
+
+## About Laravel
+
+Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
+
+- [Simple, fast routing engine](https://laravel.com/docs/routing).
+- [Powerful dependency injection container](https://laravel.com/docs/container).
+- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
+- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
+- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
+- [Robust background job processing](https://laravel.com/docs/queues).
+- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
+
+Laravel is accessible, powerful, and provides tools required for large, robust applications.
+
+## Learning Laravel
+
+Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
+
+If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
+
+## Laravel Sponsors
+
+We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
+
+- **[Vehikl](https://vehikl.com/)**
+- **[Tighten Co.](https://tighten.co)**
+- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
+- **[64 Robots](https://64robots.com)**
+- **[Cubet Techno Labs](https://cubettech.com)**
+- **[Cyber-Duck](https://cyber-duck.co.uk)**
+- **[Many](https://www.many.co.uk)**
+- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
+- **[DevSquad](https://devsquad.com)**
+- [UserInsights](https://userinsights.com)
+- [Fragrantica](https://www.fragrantica.com)
+- [SOFTonSOFA](https://softonsofa.com/)
+- [User10](https://user10.com)
+- [Soumettre.fr](https://soumettre.fr/)
+- [CodeBrisk](https://codebrisk.com)
+- [1Forge](https://1forge.com)
+- [TECPRESSO](https://tecpresso.co.jp/)
+- [Runtime Converter](http://runtimeconverter.com/)
+- [WebL'Agence](https://weblagence.com/)
+- [Invoice Ninja](https://www.invoiceninja.com)
+- [iMi digital](https://www.imi-digital.de/)
+- [Earthlink](https://www.earthlink.ro/)
+- [Steadfast Collective](https://steadfastcollective.com/)
+- [We Are The Robots Inc.](https://watr.mx/)
+- [Understand.io](https://www.understand.io/)
+- [Abdel Elrafa](https://abdelelrafa.com)
+- [Hyper Host](https://hyper.host)
+- [Appoly](https://www.appoly.co.uk)
+- [OP.GG](https://op.gg)
+- [云软科技](http://www.yunruan.ltd/)
+
+## Contributing
+
+Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
+
+## Code of Conduct
+
+In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
+
+## Security Vulnerabilities
+
+If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
+
+## License
+
+The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

+ 257 - 0
app/Console/Commands/Gem.php

@@ -0,0 +1,257 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class Gem extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'gem
+                            {path:path-in-gem}
+                            [{--write-to-storage}]';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    public $output = '';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        global $argv;
+        $in = json_decode(file_get_contents(base_path("gem/forms/{$argv[2]}/spec.json")));
+        $outPath = base_path("gem/forms/{$argv[2]}/build");
+        $this->output = $this->ln('<div class="gem-nodes">', 1);
+
+        foreach ($in as $node) {
+            $this->output .= $this->gem($node);
+        }
+
+        $this->output .= $this->ln('</div>', 1);
+
+        // write form
+        $template = file_get_contents(base_path("gem/templates/form.blade.php"));
+        $this->output = str_replace('<!-- __GENERATED_MARKUP__ -->', $this->output, $template);
+        file_put_contents(base_path("gem/forms/{$argv[2]}/build/form.blade.php"), $this->output);
+
+        // write processor
+        copy(base_path("gem/templates/processor.php"), base_path("gem/forms/{$argv[2]}/build/processor.php"));
+
+        // write summary
+        copy(base_path("gem/templates/summary.php"), base_path("gem/forms/{$argv[2]}/build/summary.php"));
+
+        // copy output to store/section
+        if(in_array("--write-to-storage", $argv) !== FALSE) {
+            copy(base_path("gem/forms/{$argv[2]}/build/form.blade.php"), base_path("storage/sections/{$argv[2]}/form.blade.php"));
+            copy(base_path("gem/forms/{$argv[2]}/build/processor.php"), base_path("storage/sections/{$argv[2]}/processor.php"));
+            copy(base_path("gem/forms/{$argv[2]}/build/summary.php"), base_path("storage/sections/{$argv[2]}/summary.php"));
+        }
+
+        echo "OK";
+    }
+
+    public function gem($_node, $_parentKey = false, $_level = 0)
+    {
+        $output = '';
+        $key = ($_parentKey ? $_parentKey . '__' : '') . $_node->K;
+
+        // start container
+        $output .= $this->ln('<div ' .
+            'class="my-3 node node-level-' . $_level . '" ' .
+            'data-key="' . $key . '">', $_level + 1);
+
+        // label
+        $output .= $this->ln('<label>' . $this->formatText($_node->Q) . '</label>', $_level + 2);
+
+        // input
+        $fieldLevel = $_level + 1;
+        if (!!@$_node->T) {
+            switch ($_node->T) {
+                case 'YNUM':
+                    // start
+                    $output .= $this->ln('<div class="d-flex align-items-center">', $fieldLevel + 1);
+
+                    // YES
+                    $output .= $this->ln('<label class="d-inline-flex align-items-center my-0 mr-3">', $fieldLevel + 2);
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        "{{ @\$contentData['{$key}'] === 'YES' ? 'checked' : '' }} " .
+                        'name="' . $key . '" type="radio" value="YES" class="mr-1">', $fieldLevel + 3);
+                    $output .= $this->ln('<span>Yes</span>', $fieldLevel + 3);
+                    $output .= $this->ln('</label>', $fieldLevel + 2);
+
+                    // NO
+                    $output .= $this->ln('<label class="d-inline-flex align-items-center my-0 mr-3">', $fieldLevel + 2);
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        "{{ @\$contentData['{$key}'] === 'NO' ? 'checked' : '' }} " .
+                        'name="' . $key . '" type="radio" value="NO" class="mr-1">', $fieldLevel + 3);
+                    $output .= $this->ln('<span>No</span>', $fieldLevel + 3);
+                    $output .= $this->ln('</label>', $fieldLevel + 2);
+
+                    // NO
+                    $output .= $this->ln('<label class="d-inline-flex align-items-center my-0 mr-3">', $fieldLevel + 2);
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        "{{ @\$contentData['{$key}'] === 'UNKNOWN' ? 'checked' : '' }} " .
+                        'name="' . $key . '" type="radio" value="UNKNOWN" class="mr-1">', $fieldLevel + 3);
+                    $output .= $this->ln('<span>Unknown</span>', $fieldLevel + 3);
+                    $output .= $this->ln('</label>', $fieldLevel + 2);
+
+                    // MEMO
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        'value="' . "{{ @\$contentData['{$key}_memo'] }}" . '" ' .
+                        'name="' . $key . '_memo" type="text" class="form-control form-control-sm" placeholder="Memo">', $fieldLevel + 2);
+
+                    // end
+                    $output .= $this->ln('</div>', $fieldLevel + 1);
+                    break;
+
+                case 'SRV':
+                    // start
+                    $output .= $this->ln('<div class="d-flex align-items-center">', $fieldLevel + 1);
+
+                    // VALUE
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        'value="' . "{{ @\$contentData['{$key}'] }}" . '" ' .
+                        'name="' . $key . '" type="text" class="form-control form-control-sm" placeholder="Answer">', $fieldLevel + 2);
+
+                    // UNKNOWN
+                    $output .= $this->ln('<label class="d-inline-flex align-items-center my-0 ml-3 mr-2">', $fieldLevel + 2);
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        "{{ @\$contentData['{$key}_unknown'] ? 'checked' : '' }} " .
+                        'name="' . $key . '_unknown" type="checkbox" class="mr-1">', $fieldLevel + 3);
+                    $output .= $this->ln('<span>Unknown</span>', $fieldLevel + 3);
+                    $output .= $this->ln('</label>', $fieldLevel + 2);
+
+                    // MEMO
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        'value="' . "{{ @\$contentData['{$key}_memo'] }}" . '" ' .
+                        'name="' . $key . '_memo" type="text" class="form-control form-control-sm" placeholder="Memo">', $fieldLevel + 2);
+
+                    // end
+                    $output .= $this->ln('</div>', $fieldLevel + 1);
+                    break;
+
+                case 'Text with Memo':
+                    // start
+                    $output .= $this->ln('<div class="d-flex align-items-center">', $fieldLevel + 1);
+
+                    // VALUE
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        'value="' . "{{ @\$contentData['{$key}'] }}" . '" ' .
+                        'name="' . $key . '" type="text" class="form-control form-control-sm mr-2" placeholder="Answer">', $fieldLevel + 2);
+
+                    // MEMO
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        'value="' . "{{ @\$contentData['{$key}_memo'] }}" . '" ' .
+                        'name="' . $key . '_memo" type="text" class="form-control form-control-sm" placeholder="Memo">', $fieldLevel + 2);
+
+                    // end
+                    $output .= $this->ln('</div>', $fieldLevel + 1);
+                    break;
+
+                case 'Multi Checkbox with Other':
+                    // start
+                    $output .= $this->ln('<div class="mt-3">', $fieldLevel + 1);
+
+                    foreach ($_node->Options as $option) {
+                        $output .= $this->ln('<label class="d-flex align-items-center mb-1 mr-2">', $fieldLevel + 2);
+                        $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                            "{{ isset(\$contentData['{$key}']) && in_array('" . str_replace("'", "\\'", $option) . "', \$contentData['{$key}']) !== FALSE ? 'checked' : '' }} " .
+                            'name="' . $key . '[]" value="' . $option . '" type="checkbox" class="mr-1">', $fieldLevel + 3);
+                        $output .= $this->ln('<span>' . $option . '</span>', $fieldLevel + 3);
+                        $output .= $this->ln('</label>', $fieldLevel + 2);
+                    }
+
+                    // OTHER
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        'value="' . "{{ @\$contentData['{$key}_other'] }}" . '" ' .
+                        'name="' . $key . '_other" type="text" class="form-control form-control-sm my-3" placeholder="Other">', $fieldLevel + 2);
+
+                    // end
+                    $output .= $this->ln('</div>', $fieldLevel + 1);
+                    break;
+
+                case 'Accept':
+                    // start
+                    $output .= $this->ln('<div class="d-flex align-items-center">', $fieldLevel + 1);
+
+                    // YES
+                    $output .= $this->ln('<label class="d-inline-flex align-items-center my-0 mr-3">', $fieldLevel + 2);
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        "{{ @\$contentData['{$key}'] === 'ACCEPT' ? 'checked' : '' }} " .
+                        'name="' . $key . '" type="radio" value="ACCEPT" class="mr-1">', $fieldLevel + 3);
+                    $output .= $this->ln('<span>Accept</span>', $fieldLevel + 3);
+                    $output .= $this->ln('</label>', $fieldLevel + 2);
+
+                    // NO
+                    $output .= $this->ln('<label class="d-inline-flex align-items-center my-0 mr-3">', $fieldLevel + 2);
+                    $output .= $this->ln('<input onchange="onGemVChange_{{ $formID }}(this)" '.
+                        "{{ @\$contentData['{$key}'] === 'REJECT' ? 'checked' : '' }} " .
+                        'name="' . $key . '" type="radio" value="REJECT" class="mr-1">', $fieldLevel + 3);
+                    $output .= $this->ln('<span>Reject</span>', $fieldLevel + 3);
+                    $output .= $this->ln('</label>', $fieldLevel + 2);
+
+                    // end
+                    $output .= $this->ln('</div>', $fieldLevel + 1);
+                    break;
+
+                default:
+                    dump("Unknown type: {$_node->T}");
+                    break;
+            }
+        }
+
+        // subs
+        if (!!@$_node->S) {
+            $output .= $this->ln('<div class="subs pl-4">', $_level + 2);
+            foreach ($_node->S as $sub) {
+                $output .= $this->gem($sub, $key, $_level + 2);
+            }
+            $output .= $this->ln('</div>', $_level + 2);
+        }
+
+        // close container
+        $output .= $this->ln('</div>', $_level + 1);
+        $output .= $this->nl();
+
+        return $output;
+    }
+
+    public function formatText($_text) {
+        $_text = preg_replace("/\{\{(.+?)\}\}/", '<span field="$1"></span>', $_text);
+        return nl2br($_text);
+    }
+
+    public function ln($_line, $_indent = 1, $_nlAfter = true)
+    {
+        return str_repeat(" ", $_indent * 4) . $_line . ($_nlAfter ? "\n" : '');
+    }
+
+    public function nl()
+    {
+        return "\n";
+    }
+
+}

+ 79 - 0
app/Console/Commands/InstantiateStatTree.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\Client;
+use App\Models\Pro;
+use App\Models\StatTree;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
+
+class InstantiateStatTree extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'stat-tree:instantiate {slug} {pro-class}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Instantiate Stat Tree For Multiple Pros';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        global $argv;
+
+        // load the tree from slug and ensure it is a template
+        /** @var StatTree $statTree */
+        $statTree = StatTree::where('slug', $argv[2])->first();
+        if(!$statTree) {
+            $this->error("Tree with specified slug does not exist!");
+            return 0;
+        }
+        if(!$statTree->is_template) {
+            $this->error("Tree with specified slug is not a template!");
+            return 0;
+        }
+
+        // check pro-class and get pro uids
+        $this->info($argv[3]);
+
+        $pros = Pro::where('is_active', TRUE)->whereRaw($argv[3])->get();
+        $this->info(count($pros) . " pros found.");
+
+        $this->info("Instantiating...");
+        $doneCount = 0;
+        if(count($pros) > 0) {
+            foreach ($pros as $pro) {
+                $statTree->instantiateForPro($pro);
+                $doneCount++;
+                if($doneCount % 50 == 0) {
+                    $this->info("$doneCount / " . count($pros) . " completed.");
+                }
+            }
+            $this->info("$doneCount / " . count($pros) . " completed.");
+        }
+
+    }
+
+}

+ 143 - 0
app/Console/Commands/TestCriteriaQueries.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\Client;
+use Illuminate\Console\Command;
+
+class TestCriteriaQueries extends Command
+{
+
+
+
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'criteria:test';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Test criteria queries';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+       
+        $testSteps = [
+            'MCB Primary? YES',        
+            [
+                'criteriaName'=>'MCP',
+                'arguments'=>[
+                    'pro_id'=>'d88b20df-4c2e-430a-a7a1-7d08fbbc1c82'
+                ]
+            ]
+        ];
+
+        $query = Client::whereRaw('id > :id', ['id' => 0]);
+
+        $query = $this->queryBuilder($query, $testSteps);
+
+        $this->info("The count is: ".$query->count());
+
+        $records = $query->get();
+        foreach($records as $record){
+            $this->info($record->uid);
+        }
+    }
+
+   
+
+    function queryBuilder($query, $testCriteria){
+
+        foreach($testCriteria as $criterion){
+
+            $clause = '';
+            $params = [];
+
+            if(is_string($criterion)){
+                // $clause = $this->criteria[$criterion]['clause'];
+                // $params = $this->criteria[$criterion]['arguments'];
+                $query = $query->whereRaw($this->criteria[$criterion]);
+            }
+            if(is_array($criterion)){
+                $criteriaName = $criterion['criteriaName'];
+                $clause = $this->criteria[$criteriaName]['clause'];
+                $params = $criterion['arguments'];  
+                $query = $query->whereRaw($clause, $params);           
+            }
+
+          
+        }
+    
+        return $query;
+    }
+    
+    protected $criteria = [
+
+        //with preset arguments
+        'Client active? YES' => "()",
+        'Client active? NO' => "()",
+        'MCP assigned? YES' => "()",
+        'MCP assigned? NO' => "()",
+        'Future MCP appointment? YES' => "()",
+        'Future MCP appointment? NO' => "()",
+
+        'MCB Primary? YES' => "(is_part_b_primary = 'YES')",
+
+        'MCB Primary? NO' => "()",
+        'Has active RM device? YES' => "()",
+        'Has active RM device? NO' => "()",
+        'Active device type? Weight' => "()",
+        'Active device type? BP' => "()",
+        'Active device type? Weight ONLY' => "()",
+        'Active device type? BP ONLY' => "()",
+        'Active device type? Weight + BP' => "()",
+        'Type 2 Diabetic' => "(client.id IN (SELECT client_id FROM dx))",
+
+        //with user defined arguments
+        'MCP' => [
+            'argument_ui_map' => [
+                'pro_uid' => [
+                    'field_type' => 'record',
+                    'table' => 'pro',
+                    'display' => '{name_display} - {npi}',
+                    'value' => 'uid' 
+                ]
+            ],
+            'arguments' => ['pro_uid'],
+            'clause' => "(client.mcp_pro_id = (SELECT pro.id FROM pro WHERE pro.uid = :pro_uid))"
+        ],
+        'Chart Creation Date Between' => [
+            'argument_ui_map' => [
+                'starting_date' => [
+                    'field_type' => 'date'
+                ],
+                'ending_date' => [
+                    'field_type' => 'date'
+                ]
+            ],
+            'arguments' => ['starting_date', 'ending_date'],
+            'clause' => "(client.created_at::DATE >= :starting_date AND client.created_at::DATE =< :ending_date)"
+        ]
+    ];
+}

+ 105 - 0
app/Console/Commands/canvasToClientRmReasons.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Models\Client;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Http;
+
+class canvasToClientRmReasons extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'canvas:toClientRmReasons';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Canvas -> client RM reasons';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+    
+        $clients = Client::where('shadow_pro_id', null)->get();
+        $this->info("Processing ".count($clients)." clients");
+
+        $successCount = 0;
+        foreach($clients as $client){
+            $this->info("Processing client:: ".$client->cell_number);
+            $canvasJson = $client->canvas_data;
+            if(!$canvasJson){
+                $this->info("No canvas");
+                continue;
+            }
+           
+            $canvas = json_decode($canvasJson, true);
+            $dx = isset($canvas['dx'])?$canvas['dx']:null; 
+
+            if(!$dx){
+                $this->info("No DX");
+                continue;
+            }
+                
+            $dxItems = $dx['items'];
+            $positionIndex = 0;
+            foreach($dxItems as $dxItem){
+                if(!isset($dxItem['icd'])){
+                    $this->info("DX item has no icd");
+                    continue;
+                }
+                if(!isset($dxItem['title'])){
+                    $this->info("DX item has no title");
+                    continue;
+                }
+                $icd = $dxItem['icd'];
+                $description = $dxItem['title'];
+                $this->info('Client UID: '.$client->uid.' icd:'.$icd.' description:'.$description);
+                $javaResponse = $this->submitToJava([
+                    'clientUid'=>$client->uid, 
+                    'icd'=>$icd, 
+                    'description'=>$description, 
+                    'secret'=>'superman2021',
+                    'cellNumber'=>2025507072,
+                    'positionIndex' => $positionIndex
+                ]);
+                $this->info("Java response: ".json_encode($javaResponse));
+                $positionIndex++;
+            }
+            $successCount++;
+        }
+
+        $this->info("success count: ".$successCount);
+    }
+
+    private function submitToJava($data)
+    {
+        $url =  config('stag.backendUrl') . '/dev/createClientRmReason';
+        $response = Http::asForm()
+            ->withHeaders([
+               
+            ])
+            ->post($url, $data)
+            ->json();
+        return $response;
+    }
+}

+ 41 - 0
app/Console/Kernel.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console;
+
+use Illuminate\Console\Scheduling\Schedule;
+use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+
+class Kernel extends ConsoleKernel
+{
+    /**
+     * The Artisan commands provided by your application.
+     *
+     * @var array
+     */
+    protected $commands = [
+        Commands\InstantiateStatTree::class
+    ];
+
+    /**
+     * Define the application's command schedule.
+     *
+     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
+     * @return void
+     */
+    protected function schedule(Schedule $schedule)
+    {
+        // $schedule->command('inspire')->hourly();
+    }
+
+    /**
+     * Register the commands for the application.
+     *
+     * @return void
+     */
+    protected function commands()
+    {
+        $this->load(__DIR__.'/Commands');
+
+        require base_path('routes/console.php');
+    }
+}

+ 55 - 0
app/Exceptions/Handler.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Exceptions;
+
+use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use Throwable;
+
+class Handler extends ExceptionHandler
+{
+    /**
+     * A list of the exception types that are not reported.
+     *
+     * @var array
+     */
+    protected $dontReport = [
+        //
+    ];
+
+    /**
+     * A list of the inputs that are never flashed for validation exceptions.
+     *
+     * @var array
+     */
+    protected $dontFlash = [
+        'password',
+        'password_confirmation',
+    ];
+
+    /**
+     * Report or log an exception.
+     *
+     * @param  \Throwable  $exception
+     * @return void
+     *
+     * @throws \Exception
+     */
+    public function report(Throwable $exception)
+    {
+        parent::report($exception);
+    }
+
+    /**
+     * Render an exception into an HTTP response.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Throwable  $exception
+     * @return \Symfony\Component\HttpFoundation\Response
+     *
+     * @throws \Throwable
+     */
+    public function render($request, Throwable $exception)
+    {
+        return parent::render($request, $exception);
+    }
+}

+ 252 - 0
app/Helpers/TimeLine.php

@@ -0,0 +1,252 @@
+<?php
+
+namespace App\Helpers;
+
+use DateTime;
+
+class TimeLine {
+
+    public $start = null;
+    public $end = null;
+    public $available = [];
+    public $unavailable = [];
+
+    public $removeStart = null;
+    public $removeEnd = null;
+
+    public function __construct(DateTime $_start, DateTime $_end)
+    {
+        $this->start = $_start->getTimestamp();
+        $this->end = $_end->getTimestamp();
+    }
+
+    public function addAvailability(DateTime $_start, DateTime $_end) {
+
+        $this->available[] = new TimeSlot($_start, $_end);
+
+        // sort by start
+        usort($this->available, function ($item1, $item2) {
+            return $item1->start <=> $item2->start;
+        });
+
+        // compact (join adjacent overlapping slots)
+        // $this->vdump($this->available);
+        $this->normalize();
+        $this->vdump($this->available);
+    }
+
+    public function removeAvailability(DateTime $_start, DateTime $_end) {
+
+        $removeStart = $_start->getTimestamp();
+        $removeEnd = $_end->getTimestamp();
+
+        $cleaned = $this->removeEmbeddedInAvailSlot($removeStart, $removeEnd);
+        if(!$cleaned) {
+            for ($i=0; $i<count($this->available); $i++) $this->available[$i]->processed = false;
+            $this->removeStart($removeStart, $removeEnd);
+            for ($i=0; $i<count($this->available); $i++) $this->available[$i]->processed = false;
+            $this->removeEnd($removeStart, $removeEnd);
+            $this->removeEmbeddedInRemoveSlot($removeStart, $removeEnd);
+        }
+
+        // sort by start
+        usort($this->available, function ($item1, $item2) {
+            return $item1->start <=> $item2->start;
+        });
+        $this->normalize();
+        $this->vdump($this->available);
+
+        // add to unavailable
+        $this->unavailable[] = new TimeSlot($_start, $_end);
+        // $this->normalizeUnavailable();
+    }
+
+    private function removeEmbeddedInAvailSlot($removeStart, $removeEnd) {
+        for ($i=0; $i<count($this->available); $i++) {
+
+            // removeStart at or after slot start
+            // removeEnd at or before slot end
+            // split slot into 2
+            if ($removeStart >= $this->available[$i]->start &&
+                $removeEnd <= $this->available[$i]->end) {
+                $newSlot = new TimeSlot(new DateTime(), new DateTime());
+                $newSlot->start = $removeEnd;
+                $newSlot->end = $this->available[$i]->end;
+
+                $this->available[$i]->end = $removeStart;
+                array_splice($this->available, $i + 1, 0, [$newSlot]);
+                return true; // nothing more to do
+            }
+        }
+
+        return false;
+    }
+
+    private function removeEmbeddedInRemoveSlot($removeStart, $removeEnd) {
+
+        $allDone = true;
+
+        for ($i=0; $i<count($this->available); $i++) {
+
+            // removeStart at or before slot start
+            // removeEnd at or after slot end
+            // remove slot
+            if ($removeStart <= $this->available[$i]->start &&
+                $removeEnd >= $this->available[$i]->end) {
+                array_splice($this->available, $i, 1);
+                $allDone = false;
+            }
+        }
+
+        if(!$allDone) {
+            $this->removeEmbeddedInRemoveSlot($removeStart, $removeEnd); // recurse till clean
+        }
+
+        return false;
+    }
+
+    private function removeStart($removeStart, $removeEnd) {
+
+        $allDone = true;
+
+        for ($i=0; $i<count($this->available); $i++) {
+
+            if($this->available[$i]->processed) continue;
+
+            // removeStart at or after slot start
+            // removeStart at or before slot end
+            // update slot to end at removeStart
+            if($removeStart >= $this->available[$i]->start &&
+                $removeStart <= $this->available[$i]->end) {
+                $this->available[$i]->end = $removeStart;
+                $this->available[$i]->processed = true;
+                $allDone = false;
+                break;
+            }
+
+        }
+
+        if(!$allDone) {
+            $this->removeStart($removeStart, $removeEnd); // recurse till clean
+        }
+
+    }
+
+    private function removeEnd($removeStart, $removeEnd) {
+
+        $allDone = true;
+
+        for ($i=0; $i<count($this->available); $i++) {
+
+            if($this->available[$i]->processed) continue;
+
+            // removeEnd at or after slot start
+            // removeEnd at or before slot end
+            // update slot to start at removeEnd
+            if($removeEnd >= $this->available[$i]->start &&
+                $removeEnd <= $this->available[$i]->end) {
+                $this->available[$i]->start = $removeEnd;
+                $this->available[$i]->processed = true;
+                $allDone = false;
+                break;
+            }
+
+        }
+
+        if(!$allDone) {
+            $this->removeEnd($removeStart, $removeEnd); // recurse till clean
+        }
+
+    }
+
+    private function normalize() {
+
+        $allDone = true;
+
+        for ($i=0; $i<count($this->available)-1; $i++) {
+            if($this->available[$i]->end >= $this->available[$i+1]->start && // ends in the middle of next slot
+                $this->available[$i]->end <= $this->available[$i+1]->end)
+            {
+                // extend self & delete next
+                $this->available[$i]->end = $this->available[$i+1]->end;
+                array_splice($this->available, $i+1, 1);
+                $allDone = false;
+                break;
+            }
+
+            if($this->available[$i]->end >= $this->available[$i+1]->end) // ends after next slot end
+            {
+                // delete next
+                array_splice($this->available, $i+1, 1);
+                $allDone = false;
+                break;
+            }
+
+            if($this->available[$i]->start >= $this->available[$i]->end) // starts and ends at the same time!
+            {
+                // delete self
+                array_splice($this->available, $i, 1);
+                $allDone = false;
+                break;
+            }
+        }
+
+        if(!$allDone) {
+            $this->normalize(); // recurse till clean
+        }
+
+    }
+
+    private function normalizeUnavailable() {
+
+        $allDone = true;
+
+        for ($i=0; $i<count($this->unavailable)-1; $i++) {
+            if($this->unavailable[$i]->end >= $this->unavailable[$i+1]->start && // ends in the middle of next slot
+                $this->unavailable[$i]->end <= $this->unavailable[$i+1]->end)
+            {
+                // extend self & delete next
+                $this->unavailable[$i]->end = $this->unavailable[$i+1]->end;
+                array_splice($this->unavailable, $i+1, 1);
+                $allDone = false;
+                break;
+            }
+
+            if($this->unavailable[$i]->end >= $this->unavailable[$i+1]->end) // ends after next slot end
+            {
+                // delete next
+                array_splice($this->unavailable, $i+1, 1);
+                $allDone = false;
+                break;
+            }
+
+            if($this->unavailable[$i]->start >= $this->unavailable[$i]->end) // starts and ends at the same time!
+            {
+                // delete self
+                array_splice($this->unavailable, $i, 1);
+                $allDone = false;
+                break;
+            }
+        }
+
+        if(!$allDone) {
+            $this->normalizeUnavailable(); // recurse till clean
+        }
+
+    }
+
+    public function getAvailable() {
+        return $this->available;
+    }
+
+    public function getUnavailable() {
+        return $this->unavailable;
+    }
+
+    public function vdump($_x) {
+//        echo "<pre>";
+//        print_r($_x);
+//        echo "</pre><hr>";
+    }
+
+}

+ 17 - 0
app/Helpers/TimeSlot.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Helpers;
+
+class TimeSlot {
+
+    public $start = null;
+    public $end = null;
+    public $processed = false;
+
+    public function __construct(\DateTime $_start, \DateTime $_end)
+    {
+        $this->start = $_start->getTimestamp();
+        $this->end = $_end->getTimestamp();
+    }
+
+}

+ 386 - 0
app/Helpers/class.Diff.php

@@ -0,0 +1,386 @@
+<?php
+
+/*
+
+class.Diff.php
+
+A class containing a diff implementation
+
+Created by Kate Morley - http://iamkate.com/ - and released under the terms of
+the CC0 1.0 Universal legal code:
+
+http://creativecommons.org/publicdomain/zero/1.0/legalcode
+
+*/
+
+// A class containing functions for computing diffs and formatting the output.
+class Diff{
+
+  // define the constants
+  const UNMODIFIED = 0;
+  const DELETED    = 1;
+  const INSERTED   = 2;
+
+  /* Returns the diff for two strings. The return value is an array, each of
+   * whose values is an array containing two values: a line (or character, if
+   * $compareCharacters is true), and one of the constants DIFF::UNMODIFIED (the
+   * line or character is in both strings), DIFF::DELETED (the line or character
+   * is only in the first string), and DIFF::INSERTED (the line or character is
+   * only in the second string). The parameters are:
+   *
+   * $string1           - the first string
+   * $string2           - the second string
+   * $compareCharacters - true to compare characters, and false to compare
+   *                      lines; this optional parameter defaults to false
+   */
+  public static function compare(
+      $string1, $string2, $compareCharacters = false){
+
+    // initialise the sequences and comparison start and end positions
+    $start = 0;
+    if ($compareCharacters){
+      $sequence1 = $string1;
+      $sequence2 = $string2;
+      $end1 = strlen($string1) - 1;
+      $end2 = strlen($string2) - 1;
+    }else{
+      $sequence1 = preg_split('/\R/', $string1);
+      $sequence2 = preg_split('/\R/', $string2);
+      $end1 = count($sequence1) - 1;
+      $end2 = count($sequence2) - 1;
+    }
+
+    // skip any common prefix
+    while ($start <= $end1 && $start <= $end2
+        && $sequence1[$start] == $sequence2[$start]){
+      $start ++;
+    }
+
+    // skip any common suffix
+    while ($end1 >= $start && $end2 >= $start
+        && $sequence1[$end1] == $sequence2[$end2]){
+      $end1 --;
+      $end2 --;
+    }
+
+    // compute the table of longest common subsequence lengths
+    $table = self::computeTable($sequence1, $sequence2, $start, $end1, $end2);
+
+    // generate the partial diff
+    $partialDiff =
+        self::generatePartialDiff($table, $sequence1, $sequence2, $start);
+
+    // generate the full diff
+    $diff = array();
+    for ($index = 0; $index < $start; $index ++){
+      $diff[] = array($sequence1[$index], self::UNMODIFIED);
+    }
+    while (count($partialDiff) > 0) $diff[] = array_pop($partialDiff);
+    for ($index = $end1 + 1;
+        $index < ($compareCharacters ? strlen($sequence1) : count($sequence1));
+        $index ++){
+      $diff[] = array($sequence1[$index], self::UNMODIFIED);
+    }
+
+    // return the diff
+    return $diff;
+
+  }
+
+  /* Returns the diff for two files. The parameters are:
+   *
+   * $file1             - the path to the first file
+   * $file2             - the path to the second file
+   * $compareCharacters - true to compare characters, and false to compare
+   *                      lines; this optional parameter defaults to false
+   */
+  public static function compareFiles(
+      $file1, $file2, $compareCharacters = false){
+
+    // return the diff of the files
+    return self::compare(
+        file_get_contents($file1),
+        file_get_contents($file2),
+        $compareCharacters);
+
+  }
+
+  /* Returns the table of longest common subsequence lengths for the specified
+   * sequences. The parameters are:
+   *
+   * $sequence1 - the first sequence
+   * $sequence2 - the second sequence
+   * $start     - the starting index
+   * $end1      - the ending index for the first sequence
+   * $end2      - the ending index for the second sequence
+   */
+  private static function computeTable(
+      $sequence1, $sequence2, $start, $end1, $end2){
+
+    // determine the lengths to be compared
+    $length1 = $end1 - $start + 1;
+    $length2 = $end2 - $start + 1;
+
+    // initialise the table
+    $table = array(array_fill(0, $length2 + 1, 0));
+
+    // loop over the rows
+    for ($index1 = 1; $index1 <= $length1; $index1 ++){
+
+      // create the new row
+      $table[$index1] = array(0);
+
+      // loop over the columns
+      for ($index2 = 1; $index2 <= $length2; $index2 ++){
+
+        // store the longest common subsequence length
+        if ($sequence1[$index1 + $start - 1]
+            == $sequence2[$index2 + $start - 1]){
+          $table[$index1][$index2] = $table[$index1 - 1][$index2 - 1] + 1;
+        }else{
+          $table[$index1][$index2] =
+              max($table[$index1 - 1][$index2], $table[$index1][$index2 - 1]);
+        }
+
+      }
+    }
+
+    // return the table
+    return $table;
+
+  }
+
+  /* Returns the partial diff for the specificed sequences, in reverse order.
+   * The parameters are:
+   *
+   * $table     - the table returned by the computeTable function
+   * $sequence1 - the first sequence
+   * $sequence2 - the second sequence
+   * $start     - the starting index
+   */
+  private static function generatePartialDiff(
+      $table, $sequence1, $sequence2, $start){
+
+    //  initialise the diff
+    $diff = array();
+
+    // initialise the indices
+    $index1 = count($table) - 1;
+    $index2 = count($table[0]) - 1;
+
+    // loop until there are no items remaining in either sequence
+    while ($index1 > 0 || $index2 > 0){
+
+      // check what has happened to the items at these indices
+      if ($index1 > 0 && $index2 > 0
+          && $sequence1[$index1 + $start - 1]
+              == $sequence2[$index2 + $start - 1]){
+
+        // update the diff and the indices
+        $diff[] = array($sequence1[$index1 + $start - 1], self::UNMODIFIED);
+        $index1 --;
+        $index2 --;
+
+      }elseif ($index2 > 0
+          && $table[$index1][$index2] == $table[$index1][$index2 - 1]){
+
+        // update the diff and the indices
+        $diff[] = array($sequence2[$index2 + $start - 1], self::INSERTED);
+        $index2 --;
+
+      }else{
+
+        // update the diff and the indices
+        $diff[] = array($sequence1[$index1 + $start - 1], self::DELETED);
+        $index1 --;
+
+      }
+
+    }
+
+    // return the diff
+    return $diff;
+
+  }
+
+  /* Returns a diff as a string, where unmodified lines are prefixed by '  ',
+   * deletions are prefixed by '- ', and insertions are prefixed by '+ '. The
+   * parameters are:
+   *
+   * $diff      - the diff array
+   * $separator - the separator between lines; this optional parameter defaults
+   *              to "\n"
+   */
+  public static function toString($diff, $separator = "\n"){
+
+    // initialise the string
+    $string = '';
+
+    // loop over the lines in the diff
+    foreach ($diff as $line){
+
+      // extend the string with the line
+      switch ($line[1]){
+        case self::UNMODIFIED : $string .= '  ' . $line[0];break;
+        case self::DELETED    : $string .= '- ' . $line[0];break;
+        case self::INSERTED   : $string .= '+ ' . $line[0];break;
+      }
+
+      // extend the string with the separator
+      $string .= $separator;
+
+    }
+
+    // return the string
+    return $string;
+
+  }
+
+  /* Returns a diff as an HTML string, where unmodified lines are contained
+   * within 'span' elements, deletions are contained within 'del' elements, and
+   * insertions are contained within 'ins' elements. The parameters are:
+   *
+   * $diff      - the diff array
+   * $separator - the separator between lines; this optional parameter defaults
+   *              to '<br>'
+   */
+  public static function toHTML($diff, $separator = '<br>'){
+
+    // initialise the HTML
+    $html = '';
+
+    // loop over the lines in the diff
+    foreach ($diff as $line){
+
+      // extend the HTML with the line
+      switch ($line[1]){
+        case self::UNMODIFIED : $element = 'span'; break;
+        case self::DELETED    : $element = 'del';  break;
+        case self::INSERTED   : $element = 'ins';  break;
+      }
+      $html .=
+          '<' . $element . '>'
+          . htmlspecialchars($line[0])
+          . '</' . $element . '>';
+
+      // extend the HTML with the separator
+      $html .= $separator;
+
+    }
+
+    // return the HTML
+    return $html;
+
+  }
+
+  /* Returns a diff as an HTML table. The parameters are:
+   *
+   * $diff        - the diff array
+   * $indentation - indentation to add to every line of the generated HTML; this
+   *                optional parameter defaults to ''
+   * $separator   - the separator between lines; this optional parameter
+   *                defaults to '<br>'
+   */
+  public static function toTable($diff, $indentation = '', $separator = '<br>'){
+
+    // initialise the HTML
+    $html = $indentation . "<table class=\"diff\">\n";
+
+    // loop over the lines in the diff
+    $index = 0;
+    while ($index < count($diff)){
+
+      // determine the line type
+      switch ($diff[$index][1]){
+
+        // display the content on the left and right
+        case self::UNMODIFIED:
+          $leftCell =
+              self::getCellContent(
+                  $diff, $indentation, $separator, $index, self::UNMODIFIED);
+          $rightCell = $leftCell;
+          break;
+
+        // display the deleted on the left and inserted content on the right
+        case self::DELETED:
+          $leftCell =
+              self::getCellContent(
+                  $diff, $indentation, $separator, $index, self::DELETED);
+          $rightCell =
+              self::getCellContent(
+                  $diff, $indentation, $separator, $index, self::INSERTED);
+          break;
+
+        // display the inserted content on the right
+        case self::INSERTED:
+          $leftCell = '';
+          $rightCell =
+              self::getCellContent(
+                  $diff, $indentation, $separator, $index, self::INSERTED);
+          break;
+
+      }
+
+      // extend the HTML with the new row
+      $html .=
+          $indentation
+          . "  <tr>\n"
+          . $indentation
+          . '    <td class="diff'
+          . ($leftCell == $rightCell
+              ? 'Unmodified'
+              : ($leftCell == '' ? 'Blank' : 'Deleted'))
+          . '">'
+          . $leftCell
+          . "</td>\n"
+          . $indentation
+          . '    <td class="diff'
+          . ($leftCell == $rightCell
+              ? 'Unmodified'
+              : ($rightCell == '' ? 'Blank' : 'Inserted'))
+          . '">'
+          . $rightCell
+          . "</td>\n"
+          . $indentation
+          . "  </tr>\n";
+
+    }
+
+    // return the HTML
+    return $html . $indentation . "</table>\n";
+
+  }
+
+  /* Returns the content of the cell, for use in the toTable function. The
+   * parameters are:
+   *
+   * $diff        - the diff array
+   * $indentation - indentation to add to every line of the generated HTML
+   * $separator   - the separator between lines
+   * $index       - the current index, passes by reference
+   * $type        - the type of line
+   */
+  private static function getCellContent(
+      $diff, $indentation, $separator, &$index, $type){
+
+    // initialise the HTML
+    $html = '';
+
+    // loop over the matching lines, adding them to the HTML
+    while ($index < count($diff) && $diff[$index][1] == $type){
+      $html .=
+          '<span>'
+          . htmlspecialchars($diff[$index][0])
+          . '</span>'
+          . $separator;
+      $index ++;
+    }
+
+    // return the HTML
+    return $html;
+
+  }
+
+}
+
+?>

+ 642 - 0
app/Helpers/fdb.php

@@ -0,0 +1,642 @@
+<?php
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+
+if(!function_exists('side_effects_info')) {
+    function side_effects_info($_drugs) {
+
+        $result = [];
+
+        // collect gcn-seq-nos
+        $gcnSeqNos = [];
+        $rxMap = [];
+        foreach ($_drugs as $drug) {
+            if(@$drug->data && $drug->data->gcnSeqno) {
+                $gcnSeqNos[] = $drug->data->gcnSeqno;
+                $rxMap[$drug->data->gcnSeqno] = $drug->data->name;
+            }
+        }
+        if(!count($gcnSeqNos)) return $result;
+
+        $gcnSeqNos = implode(',', array_map(function($_x) {
+            return "'" . $_x . "'";
+        }, $gcnSeqNos));
+
+        $sides = DB::connection('pgsql_fdb')->select("
+SELECT r1.gcn_seqno, r1.side, sm.side_freq, sm.side_sev, sm.dxid, dx.dxid_desc56
+FROM rsidegc0_gcnseqno_link r1
+    JOIN rsidema3_mstr sm ON r1.side = sm.side
+    JOIN rfmldx0_dxid dx ON sm.dxid = dx.dxid
+WHERE r1.gcn_seqno IN (" . $gcnSeqNos . ")
+ORDER BY r1.gcn_seqno, sm.side_sev DESC, sm.side_freq ASC
+            "
+        );
+
+        $seMap = [];
+
+        foreach ($sides as $se) {
+
+            if(!isset($seMap[$rxMap[$se->gcn_seqno]])) {
+                $seMap[$rxMap[$se->gcn_seqno]] = [];
+            }
+
+            $seMap[$rxMap[$se->gcn_seqno]][] = [
+                "gcn_seqno" => $se->gcn_seqno,
+                "side_freq" => $se->side_freq,
+                "side_sev" => $se->side_sev,
+                "dxid_desc56" => $se->dxid_desc56,
+            ];
+        }
+
+        $result = $seMap;
+
+        return $result;
+    }
+}
+
+if(!function_exists('precautions_info')) {
+    function precautions_info($_drugs) {
+
+    }
+}
+
+if(!function_exists('contraindications_info')) {
+    function contraindications_info($_drugs, $_problems) {
+
+        $result = [];
+
+        // collect routed med ids
+        $routedMedIds = [];
+        $rxMap = [];
+        foreach ($_drugs as $drug) {
+            if(@$drug->data && $drug->data->routedMedId) {
+                $routedMedIds[] = $drug->data->routedMedId;
+                $rxMap[$drug->data->routedMedId] = $drug->data->name;
+            }
+        }
+        if(!count($routedMedIds)) return $result;
+
+        $routedMedIds = implode(',', array_map(function($_x) {
+            return "'" . $_x . "'";
+        }, $routedMedIds));
+
+        $contraindications = DB::connection('pgsql_fdb')->select("
+SELECT r1.routed_med_id, r1.ddxcn, r2.dxid, r2.ddxcn_sl, r3.dxid_desc56
+FROM rddcmrm0_routed_med_link r1
+    JOIN rddcmma1_contra_mstr r2 ON r1.ddxcn = r2.ddxcn
+    JOIN rfmldx0_dxid r3 ON r2.dxid = r3.dxid
+WHERE r1.routed_med_id IN (" . $routedMedIds . ")
+ORDER BY r1.routed_med_id, r3.dxid_desc56, r2.ddxcn_sl
+            "
+        );
+
+        $ciMap = [];
+
+        foreach ($contraindications as $ci) {
+
+            if(!isset($ciMap[$rxMap[$ci->routed_med_id]])) {
+                $ciMap[$rxMap[$ci->routed_med_id]] = [];
+            }
+
+            $flag = false;
+            foreach ($_problems as $_problem) {
+                if($_problem->data->dxid === $ci->dxid) {
+                    $flag = true;
+                    break;
+                }
+            }
+
+            $ciMap[$rxMap[$ci->routed_med_id]][] = [
+                "dxid" => $ci->dxid,
+                "dxid_desc56" => $ci->dxid_desc56,
+                "flag" => $flag
+            ];
+        }
+
+        return $ciMap;
+
+    }
+}
+
+if(!function_exists('drug_allergy_info')) {
+    function drug_allergy_info($_drugs, $_allergies, $_rxPivot = true) {
+        $output = [];
+
+        /*
+        for each allergy
+            if damConceptIdType = 1 // allergen-group-id
+                // https://docs.fdbhealth.com/display/MKDOCUS/Screening+an+NDC+for+a+DAM_ALRGN_GRP+Allergen+-+Illustration+of+Scenario+C
+                ...
+            elseif damConceptIdType = 2 // medication-name-id
+                // https://docs.fdbhealth.com/display/MKDOCUS/Screening+an+NDC+for+a+MED_NAME_ID+Allergen+-+Illustration+of+Scenario+B
+                ...
+            elseif damConceptIdType = 6 // base-ingredient-id
+                // https://docs.fdbhealth.com/display/MKDOCUS/Screening+an+NDC+for+an+Ingredient+Allergen+-+Illustration+of+Scenario+A
+                ...
+            endif
+        endfor
+        */
+
+        foreach ($_allergies as $allergy) {
+            foreach ($_drugs as $rxItem) {
+                if($allergy->data->damConceptIdType == 6) { // ingredient
+                    if (drugAllergyIngredientAllergenVsSingleRx($allergy, $rxItem)) {
+                        $output[] = "<b>{$rxItem->data->name}</b> can cause allergic reactions " . ($_rxPivot ? 'since' : 'if') . " the patient is allergic to <b>{$allergy->data->name}</b>.";
+                    }
+                }
+                else if($allergy->data->damConceptIdType == 2) { // medication
+                    if (drugAllergyMedicationAllergenVsSingleRx($allergy, $rxItem)) {
+                        $output[] = "<b>{$rxItem->data->name}</b> (medication) can cause allergic reactions " . ($_rxPivot ? 'since' : 'if') . " the patient is allergic to <b>{$allergy->data->name}</b>.";
+                    }
+                }
+                else if($allergy->data->damConceptIdType == 1) { // allergen group
+                    if (drugAllergyGroupAllergenVsSingleRx($allergy, $rxItem)) {
+                        $output[] = "<b>{$rxItem->data->name}</b> can cause allergic reactions " . ($_rxPivot ? 'since' : 'if') . " the patient is allergic to <b>{$allergy->data->name}</b>.";
+                    }
+                }
+            }
+        }
+
+        if(!count($output)) return '';
+
+        return "<ol class='pl-0 ml-3 mb-0'>" . implode("", array_map(function ($_x) {
+                return "<li class='mb-1'>" . $_x . "</li>";
+            }, $output)) . "</ol>";
+    }
+}
+if(!function_exists('drugAllergyIngredientAllergenVsSingleRx')) {
+    function drugAllergyIngredientAllergenVsSingleRx($_allergen, $_rx)
+    {
+
+        $matches = DB::connection('pgsql_fdb')->select("
+    (
+        -- ingredients from medication
+        SELECT R1.related_hic_seqn as hic_seqn, R2.hic_desc
+        FROM RHICHCR0_HIC_HIC_LINK R1
+                 JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+        WHERE R1.hic_seqn IN (
+            SELECT S2.dam_alrgn_hic_seqn
+            FROM RMEDMHL0_MED_HICLSEQNO_LINK S1
+                     JOIN RDAMHHA0_HIC_HICL_ALG_LINK S2 ON S1.hicl_seqno = S2.hicl_seqno
+            WHERE S1.med_concept_id = :medid
+              AND S1.med_concept_id_typ = 3
+        )
+    )
+    INTERSECT
+    (
+        -- all ingredients directly and related from allergens
+        (
+            SELECT R1.related_hic_seqn as hic_seqn, R2.hic_desc
+            FROM RHICHCR0_HIC_HIC_LINK R1
+                     JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+            WHERE R1.hic_seqn = :allergenConceptID
+        )
+        UNION
+        -- all ingredients via related dam allergen groups
+        (
+            SELECT r3.hic_seqn, r4.hic_desc
+            FROM RDAMGHC0_HIC_ALRGN_GRP_LINK R1
+                     JOIN rdamagd1_alrgn_grp_desc R2 on R1.dam_alrgn_grp = R2.dam_alrgn_grp
+                     JOIN RDAMGHC0_HIC_ALRGN_GRP_LINK R3 on R1.dam_alrgn_grp = R3.dam_alrgn_grp
+                     JOIN RHICD5_HIC_DESC R4 on r3.hic_seqn = r4.hic_seqn
+            WHERE R1.hic_seqn = :allergenConceptID
+              AND R2.dam_alrgn_grp_status_cd = 0
+            ORDER BY r3.hic_seqn
+        )
+    )
+    ",
+            ['medid' => $_rx->data->medId, 'allergenConceptID' => $_allergen->data->damConceptId]
+        );
+
+        return !!count($matches);
+    }
+}
+if(!function_exists('drugAllergyMedicationAllergenVsSingleRx')) {
+    function drugAllergyMedicationAllergenVsSingleRx($_allergen, $_rx)
+    {
+
+        $matches = DB::connection('pgsql_fdb')->select("
+    (
+        -- ingredients from medication
+        SELECT R1.related_hic_seqn as hic_seqn
+        FROM RHICHCR0_HIC_HIC_LINK R1
+                 JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+        WHERE R1.hic_seqn IN (
+            SELECT S2.dam_alrgn_hic_seqn
+            FROM RMEDMHL0_MED_HICLSEQNO_LINK S1
+                     JOIN RDAMHHA0_HIC_HICL_ALG_LINK S2 ON S1.hicl_seqno = S2.hicl_seqno
+            WHERE S1.med_concept_id = :medid
+              AND S1.med_concept_id_typ = 3
+        )
+    )
+    INTERSECT
+    (
+        -- all ingredients directly and related from allergens
+        SELECT R1.dam_alrgn_hic_seqn as hic_seqn
+        FROM RDAMHHA0_HIC_HICL_ALG_LINK R1
+        WHERE R1.hicl_seqno IN (
+            SELECT R1.hicl_seqno
+            FROM RMEDMHL0_MED_HICLSEQNO_LINK R1
+            WHERE R1.med_concept_id_typ = 1
+              AND R1.med_concept_id = :allergenConceptID
+              AND MED_CONCEPT_HICL_SRC_CD = 0
+        )
+    )
+    ",
+            ['medid' => $_rx->data->medId, 'allergenConceptID' => $_allergen->data->damConceptId]
+        );
+
+        return !!count($matches);
+    }
+}
+if(!function_exists('drugAllergyGroupAllergenVsSingleRx')) {
+    function drugAllergyGroupAllergenVsSingleRx($_allergen, $_rx)
+    {
+
+        $matches = DB::connection('pgsql_fdb')->select("
+(
+    -- ingredients from medication
+    SELECT R1.related_hic_seqn as hic_seqn
+    FROM RHICHCR0_HIC_HIC_LINK R1
+             JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+    WHERE R1.hic_seqn IN (
+        SELECT S2.dam_alrgn_hic_seqn
+        FROM RMEDMHL0_MED_HICLSEQNO_LINK S1
+                 JOIN RDAMHHA0_HIC_HICL_ALG_LINK S2 ON S1.hicl_seqno = S2.hicl_seqno
+        WHERE S1.med_concept_id = :medid
+          AND S1.med_concept_id_typ = 3
+    )
+)
+INTERSECT
+(
+    -- ingredients from medication allergen
+    (
+        SELECT R1.hic_seqn as hic_seqn
+        FROM RDAMGHC0_HIC_ALRGN_GRP_LINK R1
+        WHERE R1.DAM_ALRGN_GRP = :allergenConceptID
+    )
+    UNION
+    (
+        SELECT distinct s1.hic_seqn
+        FROM RDAMXHC0_HIC_ALRGN_XSENSE_LINK S1
+        WHERE S1.dam_alrgn_xsense IN (
+            SELECT R1.dam_alrgn_xsense
+            FROM RDAMGX0_ALRGN_GRP_XSENSE_LINK R1
+                     JOIN RDAMCSD1_XSENSIT_ALLERGY_DESC R2 on R1.dam_alrgn_xsense = R2.dam_alrgn_xsense
+            WHERE R1.dam_alrgn_grp = :allergenConceptID
+              AND R2.dam_alrgn_xsense_status_cd = 0
+        )
+    )
+)
+",
+            ['medid' => $_rx->data->medId, 'allergenConceptID' => $_allergen->data->damConceptId]
+        );
+
+        return !!count($matches);
+    }
+}
+
+if(!function_exists('drug_drug_interaction_info')) {
+    function drug_drug_interaction_info($_drugs) {
+
+        if(count($_drugs) < 2) return "";
+
+        $leftIndex = 0;
+
+        $output = [];
+
+        // convert to simple array
+        $drugsArray = [];
+        foreach ($_drugs as $drug) {
+            $drugsArray[] = $drug;
+        }
+
+        for ($i=$leftIndex; $i<count($drugsArray) - 1; $i++) {
+            for ($j=$i + 1; $j<count($drugsArray); $j++) {
+                $output[] = drugDrugInteractionSinglePair($drugsArray[$i], $drugsArray[$j]);
+            }
+        }
+
+        $output = array_filter($output, function($_x) {
+            return !!$_x;
+        });
+
+        return implode("<br>", $output);
+    }
+}
+if(!function_exists('drug_drug_interaction_info_with_pivot')) {
+    function drug_drug_interaction_info_with_pivot($_pivot, $_drugs) {
+
+        if(!count($_drugs)) return "";
+
+        $leftIndex = 0;
+
+        $output = [];
+
+        foreach ($_drugs as $drug) {
+            $output[] = drugDrugInteractionSinglePair($_pivot, $drug);
+        }
+
+        $output = array_filter($output, function($_x) {
+            return !!$_x;
+        });
+
+        if(!count($output)) return '';
+
+        return "<ol class='pl-0 ml-3 mb-0'>" . implode("", array_map(function ($_x) {
+                return "<li class='mb-1'>" . $_x . "</li>";
+            }, $output)) . "</ol>";
+
+    }
+}
+if(!function_exists('drugDrugInteractionSinglePair')) {
+    function drugDrugInteractionSinglePair($_rx1, $_rx2)
+    {
+
+        $output = [];
+
+        // get active ingredient DDI_CODEX values for drug 1 and 2
+        $rx1ActiveDdiCodex = getActiveDdiCodexFromGcnSeqNo($_rx1->data->gcnSeqno);
+        $rx2ActiveDdiCodex = getActiveDdiCodexFromGcnSeqNo($_rx2->data->gcnSeqno);
+        if (!$rx1ActiveDdiCodex || !$rx2ActiveDdiCodex || !count($rx1ActiveDdiCodex) || !count($rx2ActiveDdiCodex)) return "";
+
+        //        dump($rx1_DDI_CODEX);
+        //        dump($rx2_DDI_CODEX);
+
+        // get inactive ingredient DDI_CODEX values for drug 1 and 2
+        // to get this we need to first get the NDCs of the drugs
+        $rx1Ndc = getNdcFromMedId($_rx1->data->medId);
+        $rx2Ndc = getNdcFromMedId($_rx2->data->medId);
+
+        //        dump($rx1Ndc);
+        //        dump($rx2Ndc);
+
+        $rx1InactiveDdiCodex = getInactiveDdiCodexFromNdc($rx1Ndc);
+        $rx2InactiveDdiCodex = getInactiveDdiCodexFromNdc($rx2Ndc);
+        // if(!$rx1InactiveDdiCodex || !$rx2InactiveDdiCodex || !count($rx1InactiveDdiCodex) || !count($rx2InactiveDdiCodex)) return "";
+
+        //        dump($rx1InactiveDdiCodex);
+        //        dump($rx2InactiveDdiCodex);
+
+        // get ddi codex - monox pairs for drug 1 & 2
+        $rx1ActiveDdiCodexMonoxPairs = getDdiCodexMonoxPairs($rx1ActiveDdiCodex);
+        $rx1InactiveDdiCodexMonoxPairs = getDdiCodexMonoxPairs($rx1InactiveDdiCodex);
+        $rx2ActiveDdiCodexMonoxPairs = getDdiCodexMonoxPairs($rx2ActiveDdiCodex);
+        $rx2InactiveDdiCodexMonoxPairs = getDdiCodexMonoxPairs($rx2InactiveDdiCodex);
+
+        //        dump($rx1ActiveDdiCodexMonoxPairs);
+        //        dump($rx1InactiveDdiCodexMonoxPairs);
+        //        dump($rx2ActiveDdiCodexMonoxPairs);
+        //        dump($rx2InactiveDdiCodexMonoxPairs);
+
+        // compare 1-active to 2-active and 2-inactive
+        $activeCatches = [];
+        foreach ($rx1ActiveDdiCodexMonoxPairs as $compareLeft) {
+            foreach ($rx2ActiveDdiCodexMonoxPairs as $compareRight) {
+                if ($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                    $activeCatches[] = $compareLeft->ddi_codex;
+                }
+            }
+            foreach ($rx2InactiveDdiCodexMonoxPairs as $compareRight) {
+                if ($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                    $activeCatches[] = $compareLeft->ddi_codex;
+                }
+            }
+        }
+
+        // compare 1-inactive to 2-active and 2-inactive
+        $inactiveCatches = [];
+        foreach ($rx1InactiveDdiCodexMonoxPairs as $compareLeft) {
+            foreach ($rx2ActiveDdiCodexMonoxPairs as $compareRight) {
+                if ($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                    $inactiveCatches[] = $compareLeft->ddi_codex;
+                }
+            }
+            foreach ($rx2InactiveDdiCodexMonoxPairs as $compareRight) {
+                if ($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                    $inactiveCatches[] = $compareLeft->ddi_codex;
+                }
+            }
+        }
+
+        if (count($activeCatches)) {
+            $output[] = "<b>{$_rx2->data->name}</b> interacts with one or more active ingredients in <b>{$_rx1->data->name}</b>.";
+        }
+        if (count($inactiveCatches)) {
+            $output[] = "<b>{$_rx2->data->name}</b> interacts with one or more inactive ingredients in <b>{$_rx1->data->name}</b>.";
+        }
+
+        // TODO: find out and show the names of the actual ingredients causing interaction
+
+        return implode("<br>", $output);
+    }
+}
+if(!function_exists('getActiveDdiCodexFromGcnSeqNo')) {
+    function getActiveDdiCodexFromGcnSeqNo($_gcnSeqNo)
+    {
+        $ddiCodexArray = [];
+        $query = DB::connection('pgsql_fdb')->select("
+    SELECT r1.ddi_codex
+    FROM RADIMGC4_GCNSEQNO_LINK r1
+    WHERE r1.gcn_seqno = :gcnSeqNo
+                ",
+            ['gcnSeqNo' => $_gcnSeqNo]
+        );
+        if (count($query)) {
+            $ddiCodexArray = array_map(function ($_x) {
+                return $_x->ddi_codex;
+            }, $query);
+        }
+        return $ddiCodexArray;
+    }
+}
+if(!function_exists('getNdcFromMedId')) {
+    function getNdcFromMedId($_medId)
+    {
+        $ndcArray = [];
+        $query = DB::connection('pgsql_fdb')->select("
+    select ndc from rmindc1_ndc_medid where medid = :medId
+                ",
+            ['medId' => $_medId]
+        );
+        if (count($query)) {
+            $ndcArray = array_map(function ($_x) {
+                return $_x->ndc;
+            }, $query);
+        }
+        return $ndcArray;
+    }
+}
+if(!function_exists('getInactiveDdiCodexFromNdc')) {
+    function getInactiveDdiCodexFromNdc($_ndc)
+    {
+        $ddiCodexArray = [];
+        if(!count($_ndc)) return $ddiCodexArray;
+        $query = DB::connection('pgsql_fdb')->select("
+    SELECT distinct r1.ddi_codex
+    FROM RDDIMIN0_NDC_INACTV_DDIM_LINK r1
+    WHERE r1.ddi_ndc IN (" . implode(',', array_map(function ($_x) {
+                return "'" . $_x . "'";
+            }, $_ndc)) . ")
+                "
+        );
+        if (count($query)) {
+            $ddiCodexArray = array_map(function ($_x) {
+                return $_x->ddi_codex;
+            }, $query);
+        }
+        return $ddiCodexArray;
+    }
+}
+if(!function_exists('getDdiCodexMonoxPairs')) {
+    function getDdiCodexMonoxPairs($_ddiCodexArray)
+    {
+        $ddiCodexMonoxPairsArray = [];
+        if(!count($_ddiCodexArray)) return $ddiCodexMonoxPairsArray;
+        if (count($_ddiCodexArray)) {
+            $ddiCodexMonoxPairsArray = DB::connection('pgsql_fdb')->select("
+SELECT r1.ddi_codex, r1.ddi_monox
+FROM RADIMMA5_MSTR r1
+WHERE r1.ddi_codex IN (" . implode(',', array_map(function ($_x) {
+                    return "'" . $_x . "'";
+                }, $_ddiCodexArray)) . ")
+            "
+            );
+        }
+        return $ddiCodexMonoxPairsArray;
+    }
+}
+
+if(!function_exists('duplicate_therapy_info')) {
+    function duplicate_therapy_info($_drugs) {
+
+        if(!$_drugs || count($_drugs) < 2) return '';
+
+        $dptClasses = [];
+        foreach ($_drugs as $drug) {
+            $drug->dpt = getDptClassFromGcnSeqNo($drug->data->gcnSeqno);
+        }
+
+        $leftIndex = 0;
+        $matches = [];
+
+        // convert to simple array
+        $drugsArray = [];
+        foreach ($_drugs as $drug) {
+            $drugsArray[] = $drug;
+        }
+
+        for ($i = $leftIndex; $i < count($drugsArray) - 1; $i++) {
+            for ($j = $i + 1; $j < count($drugsArray); $j++) {
+                $compareResult = compareDPTs($drugsArray[$i]->dpt, $drugsArray[$j]->dpt);
+                foreach ($compareResult as $c) {
+                    $matches[] = "<b>{$drugsArray[$i]->data->name}</b> and <b>{$drugsArray[$j]->data->name}</b> both participate in the duplicate therapy class <b>{$c->dpt_class_desc}</b> (duplicates allowed: {$c->dpt_allowance})";
+                }
+            }
+        }
+
+        if(!count($matches)) return '';
+
+        return "<ol class='pl-0 ml-3 mb-0'>" . implode("", array_map(function ($_x) {
+                return "<li class='mb-1'>" . $_x . "</li>";
+            }, $matches)) . "</ol>";
+    }
+}
+
+if(!function_exists('duplicate_therapy_info_with_pivot')) {
+    function duplicate_therapy_info_with_pivot($_pivot, $_drugs) {
+
+        if(!$_drugs || count($_drugs) < 1) return '';
+
+        $_pivot->dpt = getDptClassFromGcnSeqNo($_pivot->data->gcnSeqno);
+
+        $dptClasses = [];
+        foreach ($_drugs as $drug) {
+            $drug->dpt = getDptClassFromGcnSeqNo($drug->data->gcnSeqno);
+        }
+
+        $leftIndex = 0;
+        $matches = [];
+        foreach ($_drugs as $drug) {
+            $compareResult = compareDPTs($_pivot->dpt, $drug->dpt);
+            foreach ($compareResult as $c) {
+                $matches[] = "<b>{$_pivot->data->name}</b> and <b>{$drug->data->name}</b> both participate in the duplicate therapy class <b>{$c->dpt_class_desc}</b> (duplicates allowed: {$c->dpt_allowance})";
+            }
+        }
+
+        if(!count($matches)) return '';
+
+        return "<ol class='pl-0 ml-3 mb-0'>" . implode("", array_map(function ($_x) {
+                return "<li class='mb-1'>" . $_x . "</li>";
+            }, $matches)) . "</ol>";
+    }
+}
+
+if (!function_exists('getDptClassFromGcnSeqNo')) {
+    function getDptClassFromGcnSeqNo($_gcnSeqNo)
+    {
+        return DB::connection('pgsql_fdb')->select("
+    SELECT distinct r1.dpt_class_id, r2.dpt_allowance, r2.dpt_class_desc
+    FROM RDPTGC0_GCNSEQNO_LINK r1
+    JOIN RDPTCL0_CLASS_ID r2 on r1.dpt_class_id = r2.dpt_class_id
+    WHERE r1.gcn_seqno = :gcnSeqNo
+                ",
+            ['gcnSeqNo' => $_gcnSeqNo]
+        );
+    }
+}
+if (!function_exists('compareDPTs')) {
+    function compareDPTs($_dptArray1, $_dptArray2)
+    {
+        $output = [];
+        for ($i = 0; $i < count($_dptArray1); $i++) {
+            for ($j = 0; $j < count($_dptArray2); $j++) {
+                if ($_dptArray1[$i]->dpt_class_id == $_dptArray2[$j]->dpt_class_id) {
+                    $output[] = json_decode(json_encode([
+                        "dpt_allowance" => $_dptArray1[$i]->dpt_allowance,
+                        "dpt_class_desc" => $_dptArray1[$i]->dpt_class_desc
+                    ]));
+                }
+            }
+        }
+        return $output;
+    }
+}
+
+if(!function_exists('coadministration_info')) {
+    function coadministration_info($_drugs) {
+
+        if(count($_drugs) < 2) return "";
+
+        // collect gcn-seq-nos
+        $gcnSeqNos = [];
+        $rxMap = [];
+        foreach ($_drugs as $drug) {
+            if(@$drug->data && $drug->data->gcnSeqno) {
+                $gcnSeqNos[] = $drug->data->gcnSeqno;
+                $rxMap[$drug->data->gcnSeqno] = $drug->data->name;
+            }
+        }
+        if(!count($gcnSeqNos)) return $result;
+
+        $gcnSeqNos = implode(',', array_map(function($_x) {
+            return "'" . $_x . "'";
+        }, $gcnSeqNos));
+
+        $coadministration = DB::connection('pgsql_fdb')->select("
+SELECT distinct r1.coadmin_dosing_text
+FROM radige0_ddi_gcnseqno_except r1
+WHERE r1.side_a_gcn_seqno in ($gcnSeqNos) AND r1.side_b_gcn_seqno in ($gcnSeqNos)
+            "
+        );
+
+        if(!$coadministration || !count($coadministration)) return '';
+
+        return "<ol class='pl-0 ml-3 mb-0'>" . implode("", array_map(function ($_x) {
+                return "<li class='mb-1'>" . $_x->coadmin_dosing_text . "</li>";
+            }, $coadministration)) . "</ol>";
+
+        return $coadministration;
+    }
+}

+ 759 - 0
app/Helpers/helpers.php

@@ -0,0 +1,759 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: tatu
+ * Date: 6/23/20
+ * Time: 12:10 AM
+ */
+
+use App\Models\AppSession;
+use App\Models\Client;
+use App\Models\Pro;
+use App\Models\Bill;
+//require_once './class.Diff.php';
+use Illuminate\Support\Facades\Http;
+use Soundasleep\Html2Text as Html2Text;
+
+if(!function_exists('toFeetAndInches')) {
+    function toFeetAndInches($value, $ftLabel = ' ft.', $inLabel = ' in.') {
+        if(!$value) return '-';
+        $value = round($value);
+        $ft = round(floor($value / 12));
+        $in = $value % 12;
+        return "$ft$ftLabel $in$inLabel";
+    }
+}
+
+if(!function_exists('callJava')) {
+    function callJava($endPoint, $data, $sessionKey) {
+        $url = config('stag.backendUrl') . $endPoint;
+        $response = Http::asForm()
+            ->withHeaders([
+                'sessionKey' => $sessionKey
+            ])
+            ->post($url, $data)
+            ->body();
+        return $response;
+    }
+}
+
+if(!function_exists('feetFromInches')) {
+    function feetFromInches($value) {
+        if(!$value) return 0;
+        $value = round($value);
+        return round(floor($value / 12));
+    }
+}
+
+if(!function_exists('inchesAfterFeetFromInches')) {
+    function inchesAfterFeetFromInches($value) {
+        if(!$value) return 0;
+        $value = round($value);
+        return round($value % 12);
+    }
+}
+
+if(!function_exists('genericBills')) {
+    function genericBills(Pro $performerPro, $patient, $entityType, $entityUid) {
+        $genericBills = Bill::where('bill_service_type', 'GENERIC');
+        if($performerPro->pro_type !== 'ADMIN') {
+            $genericBills = $genericBills->where('generic_pro_id', $performerPro->id);
+        }
+        if($patient) {
+            $genericBills = $genericBills->where('client_id', $patient->id);
+        }
+        if($entityType && $entityUid) {
+            $genericBills = $genericBills
+                ->where('generic_target_entity_type', $entityType)
+                ->where('generic_target_entity_uid', $entityUid);
+        }
+        return $genericBills->orderBy('created_at', 'DESC')->get();
+    }
+}
+
+if(!function_exists('hasActiveGenericBill')) {
+    function hasActiveGenericBill(Pro $performerPro, $patient, $entityType, $entityUid) {
+        $genericBills = Bill::where('bill_service_type', 'GENERIC')->where('is_cancelled', false);
+        if($performerPro->pro_type !== 'ADMIN') {
+            $genericBills = $genericBills->where('generic_pro_id', $performerPro->id);
+        }
+        if($patient) {
+            $genericBills = $genericBills->where('client_id', $patient->id);
+        }
+        if($entityType && $entityUid) {
+            $genericBills = $genericBills
+                ->where('generic_target_entity_type', $entityType)
+                ->where('generic_target_entity_uid', $entityUid);
+        }
+        return $genericBills->count() > 0;
+    }
+}
+
+if(!function_exists('queryLineExcept')) {
+    function queryLineExcept($except = []) {
+        $params = request()->all();
+        $final = [];
+        foreach ($params as $k => $v) {
+            if(in_array($k, $except) === FALSE) {
+                if(is_array($v)) $v = implode(',', $v);
+                $final[] = "$k=" . urlencode($v);
+            }
+        }
+        return implode('&', $final);
+    }
+}
+
+if(!function_exists('sortColumnHead')) {
+    function sortColumnHead($url, $label, $sortKey, $defaultDirection = 'ASC') {
+        $currentSortKey = request()->input('sort');
+        $currentDir = request()->input('dir');
+        $targetDir = $currentDir ? ($currentDir === 'ASC' ? 'DESC' : 'ASC') : $defaultDirection;
+        echo '<a href="' . $url . '?sort=' . $sortKey . '&dir=' . $targetDir . '&' . queryLineExcept(['sort', 'dir']) . '">' . $label . '</a>';
+        if($currentSortKey === $sortKey) {
+            if($currentDir === 'ASC') {
+                echo "&nbsp;&nbsp;↑";
+            }
+            elseif($currentDir === 'DESC') {
+                echo "&nbsp;&nbsp;↓";
+            }
+        }
+    }
+}
+
+if(!function_exists('html2Text')) {
+    function html2Text($old, $new){
+
+    }
+}
+
+if(!function_exists('diff')) {
+    function diff($old, $new){
+  //      return Diff::toHTML(Diff::compare($old, $new));
+    }
+}
+
+if(!function_exists('get_current_session')) {
+    function get_current_session(){
+        return AppSession::where('session_key', request()->cookie('sessionKey'))->first();
+    }
+}
+
+if(!function_exists('get_current_date')) {
+    function get_current_date($timezone = null){
+        if(!$timezone){
+            $timezone = 'US\Eastern';
+        }
+        $date = new DateTime("now", new DateTimeZone('US/Eastern') );
+        return $date->format('Y-m-d');
+    }
+}
+
+
+
+if(!function_exists('friendly_date_time')) {
+    function friendly_date_time($value, $includeTime = true, $default = '-', $long_year=false) {
+        if(!$value || empty($value)) return $default;
+        try {
+            if($includeTime) {
+                $realTimezone = 'US/Eastern';
+                $date = new DateTime($value);
+                $date->setTimezone(new DateTimeZone($realTimezone));
+                return $date->format("m/d/y" . ($includeTime ? ", h:ia" : "")) . ($includeTime ? ' EST' : '');
+            }
+            else {
+                $result = strtotime($value);
+                if($long_year){
+                    $result = date("m/d/Y" . ($includeTime ? ", h:ia" : ""), $result);
+                }else{
+                    $result = date("m/d/y" . ($includeTime ? ", h:ia" : ""), $result);
+                }
+                return $result;
+            }
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('friendlier_date_time')) {
+    function friendlier_date_time($value, $includeTime = true, $default = '-') {
+        return friendly_date_time($value, $includeTime, $default);
+    }
+}
+
+if(!function_exists('friendly_date_time_short')) {
+    function friendly_date_time_short($value, $includeTime = true, $default = '-') {
+        if(!$value || empty($value)) return $default;
+        try {
+            $result = strtotime($value);
+            $result = date("m/d/y" . ($includeTime ? ", h:ia" : ""), $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('friendly_date_time_short_with_tz')) {
+    function friendly_date_time_short_with_tz($value, $includeTime = true, $tz='UTC', $default = '-') {
+
+        if(!$value || empty($value)) return $default;
+        try {
+
+            $realTimezone = resolve_timezone($tz);
+            $date = new DateTime($value);
+            $date->setTimezone(new DateTimeZone($realTimezone));
+
+            return $date->format("m/d/y" . ($includeTime ? ", h:iA" : ""));
+
+        }
+        catch (Exception $e) {
+            return $e->getMessage();
+        }
+    }
+}
+
+if(!function_exists('friendly_date_time_short_with_tz_from_timestamp')) {
+    function friendly_date_time_short_with_tz_from_timestamp($value, $tz='UTC', $default = '-') {
+
+        if(!$value || empty($value)) return $default;
+        try {
+
+            $realTimezone = resolve_timezone($tz);
+            $date = new DateTime("@$value");
+            $date->setTimezone(new DateTimeZone($realTimezone));
+
+            return $date->format("m/d/y, h:iA");
+
+        }
+        catch (Exception $e) {
+            return $e->getMessage();
+        }
+    }
+}
+
+if(!function_exists('postgres_date_time_short_with_tz')) {
+    function postgres_date_time_short_with_tz($value, $includeTime = true, $tz='UTC', $default = '-') {
+
+        if(!$value || empty($value)) return $default;
+        try {
+
+            $realTimezone = resolve_timezone($tz);
+            $date = new DateTime($value);
+            $date->setTimezone(new DateTimeZone($realTimezone));
+
+            return $date->format("Y-m-d" . ($includeTime ? " h:i:s" : ""));
+
+        }
+        catch (Exception $e) {
+            return $e->getMessage();
+        }
+    }
+}
+
+if(!function_exists('postgres_date_time_short_with_tz_from_timestamp')) {
+    function postgres_date_time_short_with_tz_from_timestamp($value, $tz='UTC', $default = '-') {
+
+        if(!$value || empty($value)) return $default;
+        try {
+
+            $realTimezone = resolve_timezone($tz);
+            $date = new DateTime("@$value");
+            $date->setTimezone(new DateTimeZone($realTimezone));
+
+            return $date->format("Y-m-d h:i:s");
+
+        }
+        catch (Exception $e) {
+            return $e->getMessage();
+        }
+    }
+}
+
+if(!function_exists('friendly_date_time_short_with_tz_from_timestamp_divide1000')) {
+    function friendly_date_time_short_with_tz_from_timestamp_divide1000($value, $tz='UTC', $default = '-') {
+
+        if(!$value || empty($value)) return $default;
+        try {
+            $value = (floor($value / 1000));
+            $realTimezone = resolve_timezone($tz);
+            $date = new DateTime("@$value");
+            $date->setTimezone(new DateTimeZone($realTimezone));
+
+            return $date->format("m/d/y, h:iA");
+
+        }
+        catch (Exception $e) {
+            return $e->getMessage();
+        }
+    }
+}
+
+if(!function_exists('friendly_date_short_with_tz_from_timestamp_divide1000')) {
+    function friendly_date_short_with_tz_from_timestamp_divide1000($value, $tz='EASTERN', $default = '-') {
+
+        if(!$value || empty($value)) return $default;
+        try {
+            $value = (floor($value / 1000));
+            $realTimezone = resolve_timezone($tz);
+            $date = new DateTime("@$value");
+            $date->setTimezone(new DateTimeZone($realTimezone));
+
+            return $date->format("m/d/y");
+
+        }
+        catch (Exception $e) {
+            return $e->getMessage();
+        }
+    }
+}
+
+if(!function_exists('friendly_date')) {
+    function friendly_date($value) {
+        if(!$value || empty($value)) return '';
+        try {
+            $result = strtotime($value);
+            $result = date("m/d/Y", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('friendly_date_month_year')) {
+    function friendly_date_month_year($value) {
+        if(!$value || empty($value)) return '';
+        try {
+            $result = strtotime($value);
+            $result = date("M Y", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('friendlier_date')) {
+    function friendlier_date($value) {
+        if(!$value || empty($value)) return '';
+        try {
+            $result = strtotime($value);
+            $result = date("m/d/Y", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('relative_friendly_date')) {
+    function relative_friendly_date($value) {
+        if(!$value || empty($value)) return '';
+        try {
+            $result = date_diff(date_create($value), date_create('now'))->days;
+            if ($result > 30) $result = date('F Y', strtotime($value));
+            else if ($result > 1) $result .= ' days ago';
+            else if ($result === 1) $result = 'Yesterday';
+            else if ($result === 0) $result = 'Today';
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('unfriendly_date')) {
+    function unfriendly_date($value) {
+        if(!$value || empty($value)) return '';
+        try {
+            $result = strtotime($value);
+            $result = date("Y-m-d", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('friendly_time')) {
+    function friendly_time($value, $default = '-') {
+        if(!$value || empty($value)) return $default;
+        try {
+            $result = strtotime($value);
+            $result = date("h:i a", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('military_time')) {
+    function military_time($value,  $tz='UTC', $default = '-') {
+
+        if(!$value || empty($value)) return $default;
+        try {
+            $realTimezone = resolve_timezone($tz);
+            $date = new DateTime($value);
+            $date->setTimezone(new DateTimeZone($realTimezone));
+            return $date->format("H:i");
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('resolve_timezone')) {
+    function resolve_timezone($value) {
+        try {
+            switch ($value) {
+                case 'ALASKA': {
+                    return 'US/Alaska';
+                }
+                case 'CENTRAL': {
+                    return 'US/Central';
+                }
+                case 'EASTERN': {
+                    return 'US/Eastern';
+                }
+                case 'HAWAII': {
+                    return 'US/Hawaii';
+                }
+                case 'MOUNTAIN': {
+                    return 'US/Mountain';
+                }
+                case 'PACIFIC': {
+                    return 'US/Pacific';
+                }
+                case 'PUERTO_RICO': {
+                    return 'America/Puerto_Rico';
+                }
+                case 'UTC': {
+                    return 'UTC';
+                }
+                }
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+// $date = new DateTime('2000-01-01', new DateTimeZone('Pacific/Nauru'));
+// echo $date->format('Y-m-d H:i:sP') . "\n";
+
+if(!function_exists('friendly_month')) {
+    function friendly_month($value) {
+        if(!$value || empty($value)) return "-";
+        try {
+            $result = strtotime($value);
+            $result = date("F o", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('day_part_from_date')) {
+    function day_part_from_date($value) {
+        if(!$value || empty($value)) return "-";
+        try {
+            $result = strtotime($value);
+            $result = date("d", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('month_part_from_date')) {
+    function month_part_from_date($value) {
+        if(!$value || empty($value)) return "-";
+        try {
+            $result = strtotime($value);
+            $result = date("m", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('year_part_from_date')) {
+    function year_part_from_date($value) {
+        if(!$value || empty($value)) return "-";
+        try {
+            $result = strtotime($value);
+            $result = date("Y", $result);
+            return $result;
+        }
+        catch (Exception $e) {
+            return $value;
+        }
+    }
+}
+
+if(!function_exists('friendly_money')){
+    function friendly_money($value){
+        return number_format((float)$value, 2, '.', '');
+    }
+}
+
+if(!function_exists('time_in_hrminsec')) {
+    function time_in_hrminsec($value, $default = '-') {
+        if(!$value || empty($value)) return $default;
+        $value = intval($value);
+        $minutes = intval($value / 60);
+        $seconds = $value % 60;
+        $hours = 0;
+        if($minutes >= 60) {
+            $hours = intval($minutes / 60);
+            $minutes = $minutes % 60;
+        }
+        $output = [];
+        if($hours > 0) {
+            $output[] = "{$hours}h";
+        }
+        if($minutes > 0) {
+            $output[] = "{$minutes}m";
+        }
+        if($seconds > 0) {
+            $output[] = "{$seconds}s";
+        }
+        return implode(" ", $output);
+    }
+}
+
+if(!function_exists('sanitize_field_name')) {
+    function sanitize_field_name($name) {
+        $result = strtolower($name);
+        return preg_replace("/[^0-9a-z]/i", "_", $result);
+    }
+}
+
+if(!function_exists('sanitize_state_name')) {
+    function sanitize_state_name($name) {
+        $result = strtolower($name);
+        return ucwords(preg_replace("/_/i", " ", $result));
+    }
+}
+
+if(!function_exists('renderNoteTemplate')) {
+    function renderNoteTemplate($template, $topLevel)
+    {
+        echo
+            '<div class="note-template-item" ' .
+            'template="' . (isset($template->template) ? $template->template : $template->text) . '" ' .
+            'type="' . (isset($template->type) ? $template->type : "value") . '" ' .
+            '>' .
+            '<div class="note-template-text d-flex align-items-center">' .
+            '<span class="label">' .
+            '<input type="checkbox" />' .
+            '<span>' . $template->text . '</span>' .
+            '</span>';
+        if (isset($template->type) && $template->type === 'plus-minus') {
+            echo '<div class="ml-auto mr-2 text-nowrap">';
+            echo '<a href="#" class="plus-trigger"><i class="fa fa-plus-circle"></i></a>';
+            echo '<a href="#" class="minus-trigger ml-1"><i class="fa fa-minus-circle"></i></a>';
+            echo '</div>';
+        }
+        echo '</div>';
+        if (isset($template->children) && count($template->children)) {
+            echo '<i class="fa fa-chevron-right has-children"></i>';
+            echo '<div class="note-template-children">';
+            foreach ($template->children as $t) {
+                renderNoteTemplate($t, false);
+            }
+            echo '</div>';
+        } else if (isset($template->type) && $template->type !== 'plus-minus') {
+            echo '<i class="fa fa-chevron-right has-children"></i>';
+            echo '<div class="note-template-children">';
+            if ($template->type === 'alpha') {
+                echo '<textarea class="form-control form-control-sm"></textarea>';
+            } else {
+                echo '<input type="' . $template->type . '" class="form-control form-control-sm">';
+            }
+            echo '</div>';
+        }
+        echo '</div>';
+    }
+}
+
+if(!function_exists('renderNoteTemplates')) {
+    function renderNoteTemplates($path)
+    {
+        $templates = json_decode(file_get_contents($path));
+        foreach ($templates->templates as $template) {
+            renderNoteTemplate($template, true);
+        }
+    }
+}
+
+if(!function_exists('renderNoteExamTemplates')) {
+    function renderNoteExamTemplates($parentPath, $childPath)
+    {
+        $templates = json_decode(file_get_contents($parentPath));
+        $templates = $templates->templates;
+
+        // override as needed with what is in template set
+        if(file_exists($childPath)) {
+            $orTemplates = json_decode(file_get_contents($parentPath));
+            $orTemplates = $orTemplates->templates;
+            for ($i = 0; $i < count($templates); $i++) {
+                for ($j = 0; $j < count($orTemplates); $j++) {
+                    if($templates[$i]->text === $orTemplates[$j]->text) {
+                        $templates[$i] = $orTemplates[$j];
+                    }
+                }
+            }
+        }
+
+        foreach ($templates as $template) {
+            renderNoteTemplate($template, true);
+        }
+    }
+}
+
+if(!function_exists('getVal')) {
+    function getVal($object, $prop)
+    {
+        if (isset($object->$prop)) {
+            return $object->$prop;
+        } else {
+            return '';
+        }
+    }
+}
+
+if(!function_exists('appTZtoPHPTZ')) {
+    function appTZtoPHPTZ($_timezone)
+    {
+        switch ($_timezone) {
+            case 'ALASKA':
+                $timezone = "US/Alaska";
+                break;
+            case 'CENTRAL':
+                $timezone = "US/Central";
+                break;
+            case 'HAWAII':
+                $timezone = "US/Hawaii";
+                break;
+            case 'MOUNTAIN':
+                $timezone = "US/Mountain";
+                break;
+            case 'PACIFIC':
+                $timezone = "US/Pacific";
+                break;
+            case 'PUERTO_RICO':
+                $timezone = "America/Puerto_Rico";
+                break;
+            default:
+                $timezone = "US/Eastern";
+                break;
+        }
+        return $timezone;
+    }
+}
+
+if(!function_exists('convertToTimezone')) {
+    function convertToTimezone($_dateTime, $_targetTimezone, $_sourceTimezone = 'UTC', $_returnRaw = false)
+    {
+        if (!$_dateTime) return $_dateTime;
+        $date = new \DateTime($_dateTime, new \DateTimeZone($_sourceTimezone));
+        $date->setTimezone(new \DateTimeZone(appTZtoPHPTZ($_targetTimezone)));
+        return $_returnRaw ? $date : $date->format('Y-m-d H:i:s');
+    }
+}
+
+if(!function_exists('minutes_to_hhmm')) {
+    function minutes_to_hhmm($_minutes)
+    {
+        $h = intval(floor($_minutes / 60));
+        $m = $_minutes;
+        if($h > 0) {
+            $m = $_minutes - $h * 60;
+        }
+        $h = ($h < 10 ? '0' : '') . $h;
+        $m = ($m < 10 ? '0' : '') . $m;
+        return $h . ':' . $m;
+    }
+}
+if(!function_exists('vsValue')) {
+    function vsValue($_v, $patient = null, $_direct = false)
+    {
+        if ($_direct) {
+            return $_v ? $_v : '<span class="font-weight-normal text-info font-italic">empty</span>';
+        }
+        return @($patient->{$_v}) ? $patient->{$_v} : '<span class="font-weight-normal text-info font-italic">empty</span>';
+    }
+}
+if(!function_exists('vsElement')) {
+    function vsElement($_v, $type, $name, $patient, $class = '')
+    {
+        return '<input type="' . $type . '" class="form-control form-control-sm min-width-unset rounded-0 ' . $class . '" ' .
+            'name="' . $name . '" ' .
+            'value="' . (@($patient->{$_v}) ? $patient->{$_v} : '') . '">';
+    }
+}
+
+if(!function_exists('vsRoElement')) {
+    function vsRoElement($_v, $type, $name, $patient, $class = '')
+    {
+        return '<input type="' . $type . '" readonly class="form-control form-control-sm min-width-unset rounded-0 ' . $class . '" ' .
+            'name="' . $name . '" ' .
+            'value="' . (@($patient->{$_v}) ? $patient->{$_v} : '') . '">';
+    }
+}
+
+if(!function_exists('str_compact')) {
+    function str_compact($_str)
+    {
+        return preg_replace("/[^a-zA-Z0-9]/", "", strip_tags($_str));
+    }
+}
+
+if(!function_exists('noteMethodDisplay')) {
+    function noteMethodDisplay($method)
+    {
+        if($method === 'IN_CLINIC') return 'In-Clinic';
+        $method = str_replace('_', ' ', $method);
+        return ucwords(strtolower($method));
+    }
+}
+
+if(!function_exists('friendly_timezone')) {
+    function friendly_timezone($tz) {
+        $map = [
+            'EASTERN' => 'EST',
+            'CENTRAL' => 'CST',
+            'MOUNTAIN' => 'MST',
+            'PACIFIC' => 'PST',
+            'ALASKA' => 'AST',
+            'HAWAII' => 'HST',
+            'PUERTO_RICO' => 'PR'
+        ];
+        return $map[$tz] ?? str_replace("_", " ", $tz);
+    }
+}
+
+if(!function_exists('segment_template_summary_value_display')) {
+    function segment_template_summary_value_display($value, $default='________', $class = '') {
+        if($value && strlen($value)) return '<span class="segment-template-summary-value '.$class.'">' . $value . '</span>';
+        return $default;
+    }
+}

+ 219 - 0
app/Http/Controllers/AdminController.php

@@ -0,0 +1,219 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Appointment;
+use App\Models\BDTDevice;
+use App\Models\CareMonth;
+use App\Models\Client;
+use App\Models\ClientBDTDevice;
+use App\Models\ClientInfoLine;
+use App\Models\Erx;
+use App\Models\Facility;
+use App\Models\Handout;
+use App\Models\IncomingReport;
+use App\Models\MBClaim;
+use App\Models\MBPayer;
+use App\Models\Note;
+use App\Models\NoteTemplate;
+use App\Models\Pro;
+use App\Models\Product;
+use App\Models\ProProAccess;
+use App\Models\SectionTemplate;
+use App\Models\Shipment;
+use App\Models\SupplyOrder;
+use App\Models\Ticket;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\File;
+use App\Models\Bill;
+use App\Models\ClientSMS;
+
+use Illuminate\Support\Facades\Http;
+use PDF;
+
+class AdminController extends Controller
+{
+
+    public function patients(Request $request)
+    {
+        $filters = $request->all();
+        $patients = Client::whereNull('shadow_pro_id');
+
+        // filters
+        /*
+        array:18 [▼
+          "age_category" => "LESS_THAN"
+          "age_value_1" => "34"
+          "age_value_2" => null
+          "sex" => "M"
+          "bmi_category" => "BETWEEN"
+          "bmi_value_1" => "20"
+          "bmi_value_2" => "25"
+          "last_visit_category" => "LESS_THAN"
+          "last_visit_value_1" => "2021-10-14"
+          "last_visit_value_2" => null
+          "next_appointment_category" => "LESS_THAN"
+          "next_appointment_value_1" => "2021-10-15"
+          "status" => "ACTIVE"
+          "last_weighed_in_category" => "EXACTLY"
+          "last_weighed_in_value_1" => "2021-10-07"
+          "last_bp_category" => "BETWEEN"
+          "last_bp_value_1" => "2021-10-01"
+          "last_bp_value_2" => "2021-10-31"
+        ]
+        */
+
+        if ($request->input('name')) {
+            $name = trim($request->input('name'));
+            if ($name) {
+                $patients = $patients->where(function ($q) use ($name) {
+                    $q->where('name_first', 'ILIKE', '%' . $name . '%')
+                        ->orWhere('name_last', 'ILIKE', '%' . $name . '%');
+                });
+            }
+        }
+
+        if ($request->input('mcp')) {
+            $mcp = Pro::where('uid', trim($request->input('mcp')))->first();
+            if ($mcp) {
+                $patients = $patients->where('mcp_pro_id', $mcp->id);
+            }
+        }
+
+        if ($request->input('na')) {
+            $na = Pro::where('uid', trim($request->input('na')))->first();
+            if ($na) {
+                $patients = $patients->where('default_na_pro_id', $na->id);
+            }
+        }
+
+        if ($request->input('next_appointment_category')) {
+            if($request->input('next_appointment_category') == 'NONE'){
+                $patients = $patients->whereNull('next_mcp_appointment_id');
+            }
+        }
+
+        if ($request->input('chart_number_ends_with')) {
+            if($request->input('chart_number_ends_with') == 'ZERO'){
+                $patients = $patients->where('chart_number', 'ILIKE' , '%0');
+            }else{
+                $patients = $patients->where('chart_number', 'ILIKE' , '%'.$request->input('chart_number_ends_with'));
+            }
+        }
+
+        $this->filterMultiQuery($request, $patients, 'age_in_years', 'age_category', 'age_value_1', 'age_value_2');
+        $this->filterSimpleQuery($request, $patients, 'sex', 'sex');
+        $this->filterMultiQuery($request, $patients, 'usual_bmi_max', 'bmi_category', 'bmi_value_1', 'bmi_value_2');
+        $this->filterMultiQuery($request, $patients, 'most_recent_weight_at', 'last_weighed_in_category', 'last_weighed_in_value_1', 'last_weighed_in_value_2');
+        $this->filterMultiQuery($request, $patients, 'most_recent_bp_at', 'last_bp_category', 'last_bp_value_1', 'last_bp_value_2');
+
+        switch($request->input('status')) {
+            case 'ACTIVE':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', true);
+                break;
+            case 'AWAITING_VISIT':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', false);
+                break;
+            case 'INACTIVE':
+                $patients->where('is_active', '<>', true);
+                break;
+        }
+
+        $initiative = $request->input('initiative');
+        if($initiative){
+            $wildCardedInitiative = '%'.$initiative.'%';
+            $patients->where('initiative', 'ilike', $wildCardedInitiative);
+        }
+
+        $include_test_records = $request->input('include_test_records');
+        if(!$include_test_records){
+            $patients = $patients->where(function ($q) {
+                $q->whereNull('client_engagement_status_category')
+                    ->orWhere('client_engagement_status_category', '<>', 'DUMMY');
+            });
+        }
+
+        $insurance = $request->get('insurance');
+        if($insurance){
+            if($insurance === 'MEDICARE'){
+                $patients = $patients->where('is_part_b_primary', 'YES');
+            }else{
+                $patients = $patients->where('is_part_b_primary', '!=', 'YES');
+            }
+        }
+
+        $patients = $patients->orderBy('created_at', 'DESC')->paginate(25);
+        return view('app.admin.patients', compact('patients', 'filters'));
+    }
+
+    public function notes(Request $request)
+    {
+        $notes = Note::paginate(5);
+
+        // SELECT * FROM note WHERE client_id IN (SELECT id FROM client WHERE mcp_pro_id = :me.id);
+
+        return view('app.mcp.notes', compact('notes'));
+    }
+
+    public function notes_pending_summary_suggestion(Request $request){
+        $pro = $this->performer->pro;
+        $data = [
+            'records' => $pro->get_notes_pending_summary_suggestion_as_admin()
+        ];
+        return view('app.admin.notes_pending_summary_suggestion', $data);
+    }
+
+    public function notes_rejected_summary_suggestion(Request $request){
+        $pro = $this->performer->pro;
+        $data = [
+            'records' => $pro->get_notes_rejected_summary_suggestion_as_admin()
+        ];
+        return view('app.admin.notes_rejected_summary_suggestion', $data);
+    }
+
+
+    public function appointments(Request $request)
+    {
+        $appointments = Appointment::paginate(5);
+        return view('app.mcp.appointments', compact('appointments'));
+    }
+
+    public function bills(Request $request)
+    {
+        $bills = Bill::paginate(5);
+        return view('app.mcp.bills', compact('bills'));
+    }
+
+    public function erx_and_orders(Request $request)
+    {
+        $erxAndOrders = Erx::paginate(5);
+        return view('app.mcp.erx_and_orders', compact('erxAndOrders'));
+    }
+
+    public function reports(Request $request)
+    {
+        $data = [];
+        return view('app.mcp.reports', $data);
+    }
+
+    public function supply_orders(Request $request)
+    {
+        $supplyOrders = SupplyOrder::paginate(5);
+        return view('app.mcp.supply_orders', compact('supplyOrders'));
+    }
+
+    public function getCreateNewPatientScriptTemplate(Request $request){
+        $template = $request->get('template');
+        if(!$template) return $this->fail('No script template');
+
+        $path = resource_path() . '/views/app/patient/create-patient/scripts/' . $template . '.blade.php';
+        
+        if(!File::exists($path)) return $this->fail('Invalid script template');
+
+        $templateContent = file_get_contents($path);
+        return $this->pass($templateContent);
+    }
+
+
+}

+ 306 - 0
app/Http/Controllers/AppointmentController.php

@@ -0,0 +1,306 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Helpers\TimeLine;
+use App\Models\Appointment;
+use App\Models\BDTDevice;
+use App\Models\CareMonth;
+use App\Models\Client;
+use App\Models\ClientInfoLine;
+use App\Models\Facility;
+use App\Models\NoteTemplate;
+use App\Models\Pro;
+use App\Models\ProGeneralAvailability;
+use App\Models\ProSpecificAvailability;
+use App\Models\ProSpecificUnavailability;
+use App\Models\SectionTemplate;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\File;
+
+class AppointmentController extends Controller
+{
+    public function events(Request $request)
+    {
+
+        $performerPro = null;
+        if($this->performer && $this->performer->pro) {
+            $performerPro = $this->performer->pro;
+        }
+
+        if(empty($request->get('proIds'))) {
+            $proIds = [$performerPro->id];
+        }
+        else {
+            $proIds = explode(',', $request->get('proIds'));
+        }
+
+        $start = $request->get('start');
+        $end = $request->get('end');
+        $clientId = $request->get('clientId');
+        $timeZone = $request->get('timeZone');
+
+        // get appointments
+        $appointments = Appointment
+            ::where('start_time', '>=', $start)
+            ->where('start_time', '<=', $end);
+
+        // for dna, limit to accessible pros
+        if($performerPro && $performerPro->isDefaultNA()) {
+            $teams = $performerPro->teamsWhereAssistant;
+            foreach ($teams as $team) {
+                if(!in_array($team->mcp_pro_id, $proIds)) {
+                    $proIds[] = $team->mcp_pro_id;
+                }
+            }
+        }
+
+        if(count($proIds)) {
+            $appointments = $appointments
+                ->where(function ($query) use ($proIds, $clientId) {
+                    $query
+                        ->whereIn('pro_id', $proIds)
+                        ->orWhere('client_id', $clientId)
+                        ->orWhereRaw('client_id IN (SELECT shadow_client_id FROM pro WHERE id IN (' . implode(',', $proIds) . '))');
+                });
+            $appointments = $appointments->get();
+        }
+        else {
+            $appointments = [];
+        }
+
+        $events = [];
+        foreach ($appointments as $appointment) {
+
+            // default - as used from patient calendar
+            $title = ($appointment->client->id != $clientId ? '* ' : '') . $appointment->pro->displayName() .
+                " (" . strtolower($appointment->status) . ")";
+
+            if($clientId == "-1") {
+
+                $shortStatus = '';
+                switch($appointment->status) {
+                    case 'PENDING':
+                        $shortStatus = 'new';
+                        break;
+                    case 'CONFIRMED':
+                        $shortStatus = 'conf';
+                        break;
+                    case 'CANCELLED':
+                        $shortStatus = 'canc';
+                        break;
+                    case 'COMPLETED':
+                        $shortStatus = 'done';
+                        break;
+                }
+
+                if(count($proIds) === 1) {
+                    $title = "($shortStatus) " . $appointment->client->displayName();
+                }
+                else {
+                    $title = '[' . $appointment->pro->initials()  . '] ' .
+                        "($shortStatus) " . $appointment->client->displayName();
+                }
+            }
+
+            $events[] = [
+                "type" => "appointment",
+                "title" => $title,
+                "_title" => $appointment->title,
+                "description" => $appointment->description,
+                "clientName" => $appointment->client->displayName(),
+                "appointmentUid" => $appointment->uid,
+                "clientUid" => $appointment->client->uid,
+                "proId" => $appointment->pro->id,
+                "proUid" => $appointment->pro->uid,
+                "isTrainingEvent" => !!$appointment->client->shadowOfPro,
+                "proName" => $appointment->pro->displayName(),
+                "start" => convertToTimezone($appointment->start_time, $timeZone),
+                "end" => convertToTimezone($appointment->end_time, $timeZone),
+                "clientOnly" => !in_array($appointment->pro->id, $proIds),
+                "otherClient" => ($appointment->client->id != $clientId),
+                "status" => $appointment->status,
+                "editable" => true
+            ];
+        }
+
+        if(count($proIds)) {
+
+            // get availability
+            $genAvail = ProGeneralAvailability
+                ::where('is_cancelled', false)
+                ->whereIn('pro_id', $proIds)
+                ->get();
+            $specAvail = ProSpecificAvailability
+                ::where('is_cancelled', false)
+                ->whereIn('pro_id', $proIds)
+                ->where(function ($query) use ($start, $end) {
+                    $query
+                        ->where(function ($query2) use ($start, $end) {
+                            $query2
+                                ->where('start_time', '>=', $start)
+                                ->where('start_time', '<=', $end);
+                        })
+                        ->orWhere(function ($query2) use ($start, $end) {
+                            $query2
+                                ->where('end_time', '>=', $start)
+                                ->where('end_time', '<=', $end);
+                        });
+                })
+                ->get();
+            $specUnavail = ProSpecificUnavailability
+                ::where('is_cancelled', false)
+                ->whereIn('pro_id', $proIds)
+                ->where(function ($query) use ($start, $end) {
+                    $query
+                        ->where(function ($query2) use ($start, $end) {
+                            $query2
+                                ->where('start_time', '>=', $start)
+                                ->where('start_time', '<=', $end);
+                        })
+                        ->orWhere(function ($query2) use ($start, $end) {
+                            $query2
+                                ->where('end_time', '>=', $start)
+                                ->where('end_time', '<=', $end);
+                        });
+                })
+                ->get();
+
+            // logic
+            // 1. enumerate days between start and end (inclusive)
+            // 2. for each pro
+            // 3. calculate pairs of start-end of availability
+
+            // 1. enumerate days between start and end (inclusive)
+            $phpTZ = appTZtoPHPTZ($timeZone);
+            $phpTZObject = new \DateTimeZone($phpTZ);
+            $startDate = new \DateTime($start, $phpTZObject);
+            $availabilityStartDate = new \DateTime(date('Y-m-d 00:00:00'), $phpTZObject);
+            $endDate = new \DateTime($end, $phpTZObject);
+            $endDate->setTime(23, 59, 59);
+            $period = new \DatePeriod($availabilityStartDate, \DateInterval::createFromDateString('1 day'), $endDate);
+            $days = [];
+            foreach ($period as $day) {
+                $days[] = [
+                    "day" => strtoupper($day->format("l")), // SUNDAY, etc.
+                    "date" => $day->format("Y-m-d"), // 2020-10-04, etc.
+                ];
+            }
+
+            foreach ($proIds as $proId) {
+
+                $proTimeLine = new TimeLine($startDate, $endDate);
+
+                $pro = Pro::where('id', $proId)->first();
+
+                $proGenAvail = $genAvail->filter(function ($record) use ($proId) {
+                    return $record->pro_id == $proId;
+                });
+
+                // if no gen avail, assume mon to fri, 9 to 7
+                /*if (!count($proGenAvail)) {
+                    $dayNames = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY'];
+                    foreach ($dayNames as $dayName) {
+                        $item = new \stdClass();
+                        $item->day_of_week = $dayName;
+                        $item->timezone = $timeZone;
+                        $item->start_time = '09:00:00';
+                        $item->end_time = '18:00:00';
+                        $proGenAvail->push($item);
+                    }
+                }*/
+
+                $proSpecAvail = $specAvail->filter(function ($record) use ($proId) {
+                    return $record->pro_id == $proId;
+                });
+
+                $proSpecUnavail = $specUnavail->filter(function ($record) use ($proId) {
+                    return $record->pro_id == $proId;
+                });
+
+                // general availability
+                foreach ($days as $day) {
+
+                    $proGenAvailForTheDay = $proGenAvail->filter(function ($record) use ($day) {
+                        return $record->day_of_week === $day["day"];
+                    });
+                    foreach ($proGenAvailForTheDay as $ga) {
+
+                        $gaStart = new \DateTime($day["date"], new \DateTimeZone(appTZtoPHPTZ($ga->timezone)));
+                        $parts = explode(":", $ga->start_time);
+                        $gaStart->setTime(intval($parts[0]), intval($parts[1]), intval($parts[2]));
+                        $gaStart->setTimezone($phpTZObject);
+
+                        $gaEnd = new \DateTime($day["date"], new \DateTimeZone(appTZtoPHPTZ($ga->timezone)));
+                        $parts = explode(":", $ga->end_time);
+                        $gaEnd->setTime(intval($parts[0]), intval($parts[1]), intval($parts[2]));
+                        $gaEnd->setTimezone($phpTZObject);
+
+                        $proTimeLine->addAvailability($gaStart, $gaEnd);
+                    }
+
+                }
+
+                // specific availability
+                foreach ($proSpecAvail as $sa) {
+                    $saStart = new \DateTime($sa->start_time, new \DateTimeZone(appTZtoPHPTZ($sa->timezone)));
+                    $saStart->setTimezone($phpTZObject);
+                    $saEnd = new \DateTime($sa->end_time, new \DateTimeZone(appTZtoPHPTZ($sa->timezone)));
+                    $saEnd->setTimezone($phpTZObject);
+                    $proTimeLine->addAvailability($saStart, $saEnd);
+                }
+
+                // specific unavailability
+                foreach ($proSpecUnavail as $sua) {
+                    $suaStart = new \DateTime($sua->start_time, new \DateTimeZone(appTZtoPHPTZ($sua->timezone)));
+                    $suaStart->setTimezone($phpTZObject);
+                    $suaEnd = new \DateTime($sua->end_time, new \DateTimeZone(appTZtoPHPTZ($sua->timezone)));
+                    $suaEnd->setTimezone($phpTZObject);
+                    $proTimeLine->removeAvailability($suaStart, $suaEnd);
+                }
+
+                // make already booked slots unavailable
+                $proAppointments = $appointments->filter(function ($record) use ($proId, $pro) {
+                    return ($record->pro_id == $proId || $record->client_id == $pro->shadow_client_id) && !in_array($record->status, ['CANCELLED', 'COMPLETED']);
+                });
+                foreach ($proAppointments as $appointment) {
+                    if ($appointment->start_time && $appointment->end_time) {
+                        $appStart = convertToTimezone($appointment->start_time, $timeZone, 'UTC', true);
+                        $appEnd = convertToTimezone($appointment->end_time, $timeZone, 'UTC', true);
+                        $proTimeLine->removeAvailability($appStart, $appEnd);
+                    }
+                }
+
+                foreach ($proTimeLine->available as $item) {
+
+                    $eStart = new \DateTime('@' . $item->start);
+                    $eStart->setTimezone($phpTZObject);
+
+                    $eEnd = new \DateTime('@' . $item->end);
+                    $eEnd->setTimezone($phpTZObject);
+
+                    $events[] = [
+                        "type" => "availability",
+                        "title" => $pro->displayName() . " (available)",
+                        "proId" => $pro->id,
+                        "proUid" => $pro->uid,
+                        "proName" => $pro->displayName(),
+                        "start" => $eStart->format('Y-m-d H:i:s'),
+                        "end" => $eEnd->format('Y-m-d H:i:s'),
+                        "editable" => false
+                    ];
+
+                }
+
+            }
+
+        }
+
+        return json_encode($events);
+    }
+
+    public function appointmentConfirmationHistory(Request $request, Appointment $appointment) {
+        return view('app.patient.partials.appointment-confirmation-history', compact('appointment'));
+    }
+
+}

+ 39 - 0
app/Http/Controllers/CareMonthController.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\CareMonth;
+use App\Models\CareMonthEntry;
+use App\Models\Client;
+use App\Models\Pro;
+use App\Models\SectionTemplate;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\File;
+
+class CareMonthController extends Controller
+{
+    public function dashboard(Request $request, Client $patient, CareMonth $careMonth )
+    {
+        $files = File::allFiles(resource_path('views/app/entry-templates/cm'));
+        $cmTemplates = [];
+        foreach ($files as $file) {
+            $tName = str_replace(".blade.php", "", $file->getFilename());
+            if($tName !== 'default' && strpos($tName, '-narration') === FALSE) {
+                $cmTemplates[] = $tName;
+            }
+        }
+
+        $files = File::allFiles(resource_path('views/app/entry-templates/rm'));
+        $rmTemplates = [];
+        foreach ($files as $file) {
+            $tName = str_replace(".blade.php", "", $file->getFilename());
+            if($tName !== 'default' && strpos($tName, '-narration') === FALSE) {
+                $rmTemplates[] = $tName;
+            }
+        }
+
+        $pros = $this->pros;
+        return view('app.patient.care-month.dashboard', compact('patient', 'careMonth', 'pros', 'cmTemplates', 'rmTemplates'));
+    }
+
+}

+ 44 - 0
app/Http/Controllers/ClauseArgController.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\ClauseArg;
+use Illuminate\Http\Request;
+use App\Models\Clause;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\DB;
+use Ramsey\Uuid\Uuid;
+use App\Models\StatTree;
+use App\Models\StatTreeLine;
+use App\Models\StatTreeLineClause;
+
+class ClauseArgController extends Controller
+{
+
+    // eps
+    public function create(Request $request) {
+        $clauseArg = new ClauseArg();
+        $nextId = DB::select("select nextval('clause_arg_id_seq')");
+        $clauseArg->id = $nextId[0]->nextval;
+        $clauseArg->clause_id = $request->input('clauseId');
+        $clauseArg->arg_text = $request->input('argText');
+        $clauseArg->field_type = $request->input('fieldType');
+        $clauseArg->save();
+        return $this->pass();
+    }
+    public function update(Request $request) {
+        $clauseArg = ClauseArg::where('id', $request->input('id'))->first();
+        if(!$clauseArg) return $this->fail('Clause arg not found!');
+        $clauseArg->arg_text = $request->input('argText');
+        $clauseArg->field_type = $request->input('fieldType');
+        $clauseArg->save();
+        return $this->pass();
+    }
+    public function remove(Request $request) {
+        $clauseArg = ClauseArg::where('id', $request->input('id'))->first();
+        if(!$clauseArg) return $this->fail('Clause arg not found!');
+        DB::select("delete from clause_arg where id = {$clauseArg->id}");
+        return $this->pass();
+    }
+
+}

+ 99 - 0
app/Http/Controllers/ClauseController.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use App\Models\Clause;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\DB;
+use Ramsey\Uuid\Uuid;
+use App\Models\StatTree;
+use App\Models\StatTreeLine;
+use App\Models\StatTreeLineClause;
+
+class ClauseController extends Controller
+{
+
+    public function list(){
+        $clauses = Clause::all();
+        return view('app.stat-tree.clauses.list', compact('clauses'));
+    }
+
+    public function dashboard(Clause $clause){
+        return view('app.stat-tree.clauses.single', compact('clause'));
+    }
+
+    // view
+    public function replaceAllPage(Request $request){
+        return view('app.stat-tree.clauses.replace-all');
+    }
+
+    // process
+    public function replaceAll(Request $request){
+
+        $data = $request->get('data');
+        
+        $rows = json_decode($data);
+
+        Clause::truncate();
+        //Should we truncate related data?
+        StatTree::truncate();
+        StatTreeLine::truncate();
+        StatTreeLineClause::truncate();
+
+        for($i = 0; $i < count($rows); $i++){
+            $row = $rows[$i];
+            $clause = new Clause(); 
+
+            $nextId = DB::select("select nextval('clause_id_seq')");
+            $clause->id = $nextId[0]->nextval;
+            $clause->uid = Uuid::uuid4();
+
+            $clause->model = $row[0];
+            $clause->question = $row[1];
+            $clause->answer = $row[2];
+            $clause->label = $row[1] . ($row[2] && $row[2] != '-' ? ' ' . $row[2] : '');
+            $clause->clause_text = $row[3];
+            $clause->position_index = $i;
+            $clause->save();
+        }
+        
+        return $this->pass();
+    }
+
+    // eps
+    public function create(Request $request) {
+        $clause = new Clause();
+        $nextId = DB::select("select nextval('clause_id_seq')");
+        $clause->id = $nextId[0]->nextval;
+        $clause->uid = Uuid::uuid4();
+        $clause->model = $request->input('model');
+        $clause->question = $request->input('question');
+        $clause->answer = $request->input('answer');
+        $clause->label = $request->input('label');
+        $clause->clause_text = $request->input('clauseText');
+        $positionIndex = DB::select('select max(position_index) from clause');
+        $clause->position_index = is_numeric($positionIndex[0]->max) ? $positionIndex[0]->max + 1 : 1;
+        $clause->save();
+        return $this->pass();
+    }
+    public function update(Request $request) {
+        $clause = Clause::where('uid', $request->input('uid'))->first();
+        if(!$clause) return $this->fail('Clause not found!');
+        $clause->model = $request->input('model');
+        $clause->question = $request->input('question');
+        $clause->answer = $request->input('answer');
+        $clause->label = $request->input('label');
+        $clause->clause_text = $request->input('clauseText');
+        $clause->save();
+        return $this->pass();
+    }
+    public function remove(Request $request) {
+        $clause = Clause::where('uid', $request->input('uid'))->first();
+        if(!$clause) return $this->fail('Clause not found!');
+        // TODO: allow only if this clause isn't used anywhere
+        DB::select("delete from clause where id = {$clause->id}");
+        return $this->pass();
+    }
+
+}

+ 135 - 0
app/Http/Controllers/Controller.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\AppSession;
+use App\Models\Note;
+use App\Models\NoteTemplate;
+use App\Models\Pro;
+use App\Models\ProProAccess;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Controller as BaseController;
+use Illuminate\Support\Facades\Cookie;
+
+class Controller extends BaseController
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+
+    protected $performer = null;
+
+    protected $pro = null;
+
+    protected $pros = null;
+
+    public function __construct()
+    {
+        $this->performer = get_current_session();
+        if($this->performer && $this->performer->pro) {
+            $this->pro = $this->performer->pro;
+            view()->share('pro', $this->performer->pro);
+        }
+        view()->share('performer', $this->performer);
+
+        $this->pros = [];
+
+        /*$this->pros =  Pro::all();
+        if($this->pro && $this->pro->pro_type != 'ADMIN'){
+            $accessiblePros = ProProAccess::where('owner_pro_id', $this->pro->id);
+            $accessibleProIds = [];
+            foreach($accessiblePros as $accessiblePro){
+                $accessibleProIds[] = $accessiblePro->id;
+            }
+            $accessibleProIds[] = $this->pro->id;
+
+            $this->pros = Pro::whereIn('id', $accessibleProIds)->get();
+        }*/
+
+        view()->share('pros',$this->pros);
+        // view()->share('notes', Note::all());
+
+        // $noteTemplates = NoteTemplate::all();
+        // view()->share('noteTemplates', $noteTemplates);
+
+        $initiatives = ['LHI', 'BHI'];
+        view()->share('intiatives', $initiatives);
+
+        $rmCodes = ['RMB','RM20', 'RM40','RM60'];
+
+        view()->share('rmCodes', $rmCodes);
+    }
+
+    public function performer(){
+        $sessionKey = Cookie::get('sessionKey');
+        if ($sessionKey == null){
+            throw new \Exception('No session key in cookie.');
+        }
+        $performer = AppSession::where('session_key', $sessionKey)->first();
+        return $performer;
+    }
+
+    public function getMyClientIds(){
+        if($this->pro == null){
+            return [];
+        }
+        return $this->pro->getMyClientIds();
+    }
+
+    public function pass($data = null): array
+    {
+        return [
+            'success' => true,
+            'data' => $data,
+        ];
+    }
+    public function fail($message): array
+    {
+        return [
+            'success' => false,
+            'message' => $message
+        ];
+    }
+
+    public function filterMultiQuery(Request $request, $query, $columnName, $keyName, $valueName1, $valueName2) {
+        switch($request->input($keyName)) {
+            case 'EXACTLY':
+                if($request->input($valueName1)) {
+                    $query->where($columnName, $request->input($valueName1));
+                }
+                break;
+            case 'LESS_THAN':
+                if($request->input($valueName1)) {
+                    $query->where($columnName, '<', $request->input($valueName1));
+                }
+                break;
+            case 'GREATER_THAN':
+                if($request->input($valueName1)) {
+                    $query->where($columnName, '>', $request->input($valueName1));
+                }
+                break;
+            case 'BETWEEN':
+                if($request->input($valueName1) && $request->input($valueName2)) {
+                    $query
+                        ->where($columnName, '>=', $request->input($valueName1))
+                        ->where($columnName, '<=', $request->input($valueName2));
+                }
+                break;
+            case 'NOT_BETWEEN':
+                if($request->input($valueName1) && $request->input($valueName2)) {
+                    $query
+                        ->where(function ($q) use ($request, $columnName, $valueName1, $valueName2) {
+                            $q->where($columnName, '<', $request->input($valueName1))
+                                ->orWhere($columnName, '>', $request->input($valueName2));
+                        });
+                }
+                break;
+        }
+    }
+    public function filterSimpleQuery(Request $request, $query, $columnName, $valueName) {
+        if($request->input($valueName)) {
+            $query->where($columnName, $request->input($valueName));
+        }
+    }
+}

+ 282 - 0
app/Http/Controllers/DnaController.php

@@ -0,0 +1,282 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Appointment;
+use App\Models\BDTDevice;
+use App\Models\Bill;
+use App\Models\CareMonth;
+use App\Models\Client;
+use App\Models\ClientBDTDevice;
+use App\Models\ClientInfoLine;
+use App\Models\Erx;
+use App\Models\Facility;
+use App\Models\Handout;
+use App\Models\IncomingReport;
+use App\Models\MBClaim;
+use App\Models\MBPayer;
+use App\Models\Note;
+use App\Models\NoteTemplate;
+use App\Models\Pro;
+use App\Models\Product;
+use App\Models\ProProAccess;
+use App\Models\ProTeam;
+use App\Models\ProTransaction;
+use App\Models\SectionTemplate;
+use App\Models\Shipment;
+use App\Models\SupplyOrder;
+use App\Models\Ticket;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\File;
+
+use Illuminate\Support\Facades\Http;
+use PDF;
+
+class DnaController extends Controller
+{
+
+    public function patients(Request $request)
+    {
+        $filters = $request->all();
+        $patients = Client::whereNull('shadow_pro_id')->where('default_na_pro_id', $this->performer->pro->id);
+
+        if ($request->input('name')) {
+            $name = trim($request->input('name'));
+            if ($name) {
+                $patients = $patients->where(function ($q) use ($name) {
+                    $q->where('name_first', 'ILIKE', '%' . $name . '%')
+                        ->orWhere('name_last', 'ILIKE', '%' . $name . '%');
+                });
+            }
+        }
+
+        $this->filterMultiQuery($request, $patients, 'age_in_years', 'age_category', 'age_value_1', 'age_value_2');
+        $this->filterSimpleQuery($request, $patients, 'sex', 'sex');
+        $this->filterMultiQuery($request, $patients, 'usual_bmi_max', 'bmi_category', 'bmi_value_1', 'bmi_value_2');
+        $this->filterMultiQuery($request, $patients, 'most_recent_completed_mcp_note_date', 'last_visit_category', 'last_visit_value_1', 'last_visit_value_2'); 
+        $this->filterMultiQuery($request, $patients, 'next_mcp_appointment_date', 'next_appointment_category', 'next_appointment_value_1', 'next_appointment_value_2');
+
+        switch($request->input('status')) {
+            case 'ACTIVE':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', true);
+                break;
+            case 'AWAITING_VISIT':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', false);
+                break;
+            case 'INACTIVE':
+                $patients->where('is_active', '<>', true);
+                break;
+        }
+        $patients = $patients->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.dna.patients', compact('patients', 'filters'));
+    }
+
+    public function encounters(Request $request)
+    {
+        $filters = $request->all();
+        $notes = Note::query();
+        $notes = $notes->where('ally_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $notes, 'effective_time', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $notes, 'new_or_fu_or_na', 'new_or_fu_or_na');
+        $notes = $notes->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.dna.encounters', compact('notes', 'filters'));
+    }
+
+    public function notes(Request $request)
+    {
+        $data = [];
+        return view('app.dna.notes', $data);
+    }
+
+    public function appointments(Request $request)
+    {
+        $filters = $request->all();
+        $appointments = Appointment::select('appointment.*')
+                        ->join('client', 'client.id', '=', 'appointment.client_id')
+                        ->where('client.default_na_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $appointments, 'raw_date', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $appointments, 'status', 'status');
+        $appointments = $appointments->orderBy('end_time', 'DESC')->paginate(20);
+        return view('app.dna.appointments', compact('appointments', 'filters'));
+    }
+
+    public function careMonths(Request $request){
+        $filters = $request->all();
+        $careMonths = CareMonth::select('care_month.*')
+                        ->join('client', 'client.id', '=', 'care_month.client_id')
+                        ->where('client.default_na_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $careMonths, 'raw_date', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $careMonths, 'status', 'status');
+        $careMonths = $careMonths->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.dna.care-months', compact('careMonths', 'filters'));
+    }
+
+    public function financialTransactions(Request $request){
+        $filters = $request->all();
+        $financialTransactions = ProTransaction::select('pro_transaction.*')
+                                ->join('bill', 'bill.id', '=', 'pro_transaction.bill_id')
+                                ->where('bill.na_pro_id', $this->performer->pro->id);
+        $financialTransactions = $financialTransactions->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.dna.financial-transactions', compact('financialTransactions', 'filters'));
+    }
+    public function myBills(Request $request){
+        $performerProID = $this->performer->pro->id;
+        $filters = $request->all();
+        $bills = Bill::where(function($q) use ($performerProID){
+            return $q->where('na_pro_id', '=', $performerProID )->orWhere('generic_pro_id', '=', $performerProID);
+        });
+        $this->filterMultiQuery($request, $bills, 'effective_date', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterMultiQuery($request, $bills, 'na_expected_payment_amount', 'amount_category', 'amount_value_1', 'amount_value_2');
+        
+        $status = $request->get('status');
+        if($status){
+            if($status === 'VERIFIED') $bills = $bills->where('is_verified', true);
+            if($status === 'ACTIVE') $bills = $bills->where('is_cancelled', false);
+            if($status === 'CANCELLED') $bills = $bills->where('is_cancelled', true);
+            if($status === 'SUBMITTED') $bills = $bills->where('is_submitted', true);
+        }
+        
+        $bills = $bills->orderBy('effective_date', 'DESC')->paginate(20);
+        return view('app.dna.my-bills', compact('bills', 'filters'));
+    }
+
+    public function myClinicalTeams(Request $request){
+        $filters = $request->all();
+        $teams = ProTeam::where('assistant_pro_id', $this->performer->pro->id);        
+        $teams = $teams->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.dna.my-clinical-teams', compact('teams', 'filters'));
+    }
+
+    public function bills(Request $request)
+    {
+        $data = [];
+        return view('app.dna.bills', $data);
+    }
+
+    public function erx_and_orders(Request $request)
+    {
+        $data = [];
+        return view('app.dna.erx_and_orders', $data);
+    }
+
+    public function reports(Request $request)
+    {
+        $data = [];
+        return view('app.dna.reports', $data);
+    }
+
+    public function supply_orders(Request $request)
+    {
+        $data = [];
+        return view('app.dna.supply_orders', $data);
+    }
+
+    public function new_patients_awaiting_visit(Request $request){
+        $data = [];
+        return view('app.dna.new_patients_awaiting_visit', $data);
+    }
+    public function notes_pending_signature(Request $request){
+        $data = [];
+        return view('app.dna.notes_pending_signature', $data);
+    }
+    public function notes_pending_billing(Request $request){
+        $data = [];
+        return view('app.dna.notes_pending_billing', $data);
+    }
+    public function reports_pending_signature(Request $request){
+        $data = [];
+        return view('app.dna.reports_pending_signature', $data);
+    }
+    public function patients_without_appointments(Request $request){
+        $data = [];
+        return view('app.dna.patients_without_appointments', $data);
+    }
+    public function patients_overdue_for_visit(Request $request){
+        $data = [];
+        return view('app.dna.patients_overdue_for_visit', $data);
+    }
+    public function cancelled_appointments_pending_review(Request $request){
+        $data = [];
+        return view('app.dna.cancelled_appointments_pending_review', $data);
+    }
+    public function cancelled_bills_pending_review(Request $request){
+        $data = [];
+        return view('app.dna.cancelled_bills_pending_review', $data);
+    }
+    public function cancelled_supply_orders_pending_review(Request $request){
+        $data = [];
+        return view('app.dna.cancelled_supply_orders_pending_review', $data);
+    }
+    public function erx_and_orders_pending_signature(Request $request){
+        $data = [];
+        return view('app.dna.erx_and_orders_pending_signature', $data);
+    }
+    public function supply_orders_pending_signature(Request $request){
+        $data = [];
+        return view('app.dna.supply_orders_pending_signature', $data);
+    }
+
+    //From the new spec 
+    public function myPatients(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->patientsRecordsAsDna();
+        return view('app.dna.dashboard.patients', compact('records'));
+    }
+    
+    public function patientsAwaitingMcpVisit(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->patientsAwaitingMcpVisitRecordsAsDna();
+        return view('app.dna.dashboard.patients_awaiting_mcp_visit', compact('records'));
+    }
+    
+    public function patientsWithoutAppointment(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->patientsWithoutAppointmentRecordsAsDna();
+        return view('app.dna.dashboard.patients_without_appointment', compact('records'));
+    }
+    
+    public function encountersPendingMyReview(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->encountersPendingMyReviewRecordsAsDna();
+        return view('app.dna.dashboard.encounters_pending_my_review', compact('records'));
+    }
+    
+    public function encountersInProgress(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->encountersInProgressRecordsAsDna();
+        return view('app.dna.dashboard.encounters_in_progress', compact('records'));
+    }
+    
+    public function appointmentsPendingConfirmation(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->appointmentsPendingConfirmationRecordsAsDna();
+        return view('app.dna.dashboard.appointments_pending_confirmation', compact('records'));
+    }
+    
+    public function cancelledAppointmentsPendingAck(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->cancelledAppointmentsPendingAckRecordsAsDna();
+        return view('app.dna.dashboard.cancelled_appointments_pending_ack', compact('records'));
+    }
+    
+    public function reportsPendingAck(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->reportsPendingAckRecordsAsDna();
+        return view('app.dna.dashboard.reports_pending_ack', compact('records'));
+    }
+    
+    public function supplyOrdersPendingMyAck(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->supplyOrdersPendingMyAckRecordsAsDna();
+        return view('app.dna.dashboard.supply_orders_pending_my_ack', compact('records'));
+    }
+    
+    public function supplyOrdersPendingHcpApproval(Request $request){
+        $pro = $this->performer->pro; 
+        $records = $pro->supplyOrdersPendingHcpApprovalRecordsAsDna();
+        return view('app.dna.dashboard.supply_orders_pending_hcp_approval', compact('records'));
+    }
+    
+
+}

+ 758 - 0
app/Http/Controllers/FDBPGController.php

@@ -0,0 +1,758 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Client;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+
+class FDBPGController extends Controller
+{
+
+    public function rx(Request $request)
+    {
+        return view('app.fdb-pg.fdb-rx');
+    }
+
+    // 1. medication suggest
+    public function medSuggest(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select(
+            "SELECT med_name_id, med_name FROM rminmid1_med_name WHERE med_status_cd = '0' AND med_name ILIKE :term ORDER BY med_name",
+            ['term' => '%' . $term . '%']
+        );
+        return view('app.fdb-pg.fdb-med-suggest', compact('matches'));
+    }
+
+    // 1.1 medication suggest (json response)
+    public function medSuggestJSON(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select(
+            "SELECT med_name_id, med_name as text FROM rminmid1_med_name WHERE med_status_cd = '0' AND med_name ILIKE :term ORDER BY med_name",
+            ['term' => '%' . $term . '%']
+        );
+        return json_encode([
+            "success" => true,
+            "data" => $matches
+        ]);
+    }
+
+    // 2. routed meds from men name
+    public function routedMeds(Request $request)
+    {
+        $medNameID = $request->input('med-name-id') ? trim($request->input('med-name-id')) : '';
+        if (empty($medNameID)) return '';
+        $matches = DB::connection('pgsql_fdb')->select(
+            "SELECT routed_med_id, med_routed_med_id_desc FROM rmirmid1_routed_med WHERE med_status_cd = '0' AND med_name_id = :medNameID ORDER BY med_routed_med_id_desc",
+            ['medNameID' => $medNameID]
+        );
+        return json_encode($matches);
+    }
+
+    // 3. routed dosage from routed med
+    public function routedDosages(Request $request)
+    {
+        $routedMedID = $request->input('routed-med-id') ? trim($request->input('routed-med-id')) : '';
+        if (empty($routedMedID)) return '';
+        $matches = DB::connection('pgsql_fdb')->select(
+            "SELECT routed_dosage_form_med_id, med_routed_df_med_id_desc FROM rmidfid1_routed_dose_form_med WHERE med_status_cd = '0' AND routed_med_id = :routedMedID ORDER BY med_routed_df_med_id_desc",
+            ['routedMedID' => $routedMedID]
+        );
+        return json_encode($matches);
+    }
+
+    // 4. strengths from routed med
+    public function meds(Request $request)
+    {
+        $dosageFormMedId = $request->input('dosage-form-med-id') ? trim($request->input('dosage-form-med-id')) : '';
+        if (empty($dosageFormMedId)) return '';
+        $matches = DB::connection('pgsql_fdb')->select(
+            "SELECT medid, med_medid_desc, gcn_seqno FROM rmiid1_med WHERE med_status_cd = '0' AND routed_dosage_form_med_id = :dosageFormMedId ORDER BY med_medid_desc",
+            ['dosageFormMedId' => $dosageFormMedId]
+        );
+        return json_encode($matches);
+    }
+
+    // ** med suggest V2 ** //
+    public function medSuggestV2JSON(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select("
+SELECT r_med.medid,
+       r_med.med_medid_desc as text,
+       r_med.gcn_seqno,
+       r_dosage_form.routed_dosage_form_med_id,
+       r_route.routed_med_id
+FROM rmiid1_med r_med
+         JOIN rmidfid1_routed_dose_form_med r_dosage_form
+              ON r_med.routed_dosage_form_med_id = r_dosage_form.routed_dosage_form_med_id
+         JOIN rmirmid1_routed_med r_route ON r_dosage_form.routed_med_id = r_route.routed_med_id
+WHERE r_med.med_status_cd = '0'
+  AND r_med.med_medid_desc ILIKE :term
+ORDER BY REPLACE(r_med.med_medid_desc, '-', 'x')",
+            ['term' => $term . '%']
+        );
+        return json_encode([
+            "success" => true,
+            "data" => $matches
+        ]);
+    }
+
+
+    // side effects for a given rx
+    public function sideEffects(Request $request)
+    {
+        $gcnSeqNo = $request->input('gcn-seq-no') ? trim($request->input('gcn-seq-no')) : '';
+        if (empty($gcnSeqNo)) return '';
+        $sides = DB::connection('pgsql_fdb')->select("
+SELECT r1.side, sm.side_freq, sm.side_sev, sm.dxid, dx.dxid_desc56
+FROM rsidegc0_gcnseqno_link r1
+    JOIN rsidema3_mstr sm ON r1.side = sm.side
+    JOIN rfmldx0_dxid dx ON sm.dxid = dx.dxid
+WHERE r1.gcn_seqno = :gcnSeqNo
+ORDER BY sm.side_sev DESC, sm.side_freq ASC
+            ",
+            ['gcnSeqNo' => $gcnSeqNo]
+        );
+        return view('app.fdb-pg.fdb-side-effects', compact('sides'));
+    }
+
+    // ger prec for a given rx
+    public function geriatricPrecautions(Request $request)
+    {
+        $gcnSeqNo = $request->input('gcn-seq-no') ? trim($request->input('gcn-seq-no')) : '';
+        if (empty($gcnSeqNo)) return '';
+        $precautions = DB::connection('pgsql_fdb')->select("
+SELECT r1.geri_code, gm.geri_sl, gm.geri_desc, gm.geri_narrative
+FROM rgerigc0_geri_gcnseqno_link r1
+    JOIN rgerima1_geri_mstr gm ON r1.geri_code = gm.geri_code
+WHERE r1.gcn_seqno = :gcnSeqNo
+ORDER BY gm.geri_desc
+            ",
+            ['gcnSeqNo' => $gcnSeqNo]
+        );
+        return view('app.fdb-pg.fdb-geriatric-precautions', compact('precautions'));
+    }
+
+    // indication of a given rx
+    public function indications(Request $request)
+    {
+        $gcnSeqNo = $request->input('gcn-seq-no') ? trim($request->input('gcn-seq-no')) : '';
+        if (empty($gcnSeqNo)) return '';
+        $indications = DB::connection('pgsql_fdb')->select("
+SELECT r1.indcts, r2.indcts_sn, r2.indcts_lbl, r2.dxid, r2.proxy_ind, r3.dxid_desc56
+FROM rindmgc0_indcts_gcnseqno_link r1
+    JOIN rindmma2_indcts_mstr r2 ON r1.indcts = r2.indcts
+    JOIN rfmldx0_dxid r3 ON r2.dxid = r3.dxid
+WHERE r1.gcn_seqno = :gcnSeqNo
+ORDER BY r3.dxid_desc56
+            ",
+            ['gcnSeqNo' => $gcnSeqNo]
+        );
+        return view('app.fdb-pg.fdb-indications', compact('indications'));
+    }
+
+    // contra-indications of a given rx
+    public function contraindications(Request $request)
+    {
+        $routedMedID = $request->input('routed-med-id') ? trim($request->input('routed-med-id')) : '';
+        if (empty($routedMedID)) return '';
+        $contraindications = DB::connection('pgsql_fdb')->select("
+SELECT r1.ddxcn, r2.dxid, r2.ddxcn_sl, r3.dxid_desc56
+FROM rddcmrm0_routed_med_link r1
+    JOIN rddcmma1_contra_mstr r2 ON r1.ddxcn = r2.ddxcn
+    JOIN rfmldx0_dxid r3 ON r2.dxid = r3.dxid
+WHERE r1.routed_med_id = :routedMedID
+ORDER BY r2.ddxcn_sl
+            ",
+            ['routedMedID' => $routedMedID]
+        );
+        return view('app.fdb-pg.fdb-contraindications', compact('contraindications'));
+    }
+
+    // dx suggest
+    public function dxSuggest(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select("
+SELECT distinct(r1.dxid), r1.dxid_desc56
+FROM rfmldx0_dxid r1
+JOIN rfmlsyn0_dxid_syn r2 ON r1.dxid = r2.dxid
+WHERE (r1.dxid_desc56 ILIKE :term OR r1.dxid_desc100 ILIKE :term OR r2.dxid_syn_desc56 ILIKE :term OR r2.dxid_syn_desc100 ILIKE :term)
+ORDER BY r1.dxid_desc56
+",
+            ['term' => '%' . $term . '%']
+        );
+        return view('app.fdb-pg.fdb-dx-suggest', compact('matches'));
+    }
+
+    public function dxSuggestJSON(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select("
+SELECT distinct(r1.dxid), r1.dxid_desc56 as text
+FROM rfmldx0_dxid r1
+JOIN rfmlsyn0_dxid_syn r2 ON r1.dxid = r2.dxid
+WHERE (r1.dxid_desc56 ILIKE :term OR r1.dxid_desc100 ILIKE :term OR r2.dxid_syn_desc56 ILIKE :term OR r2.dxid_syn_desc100 ILIKE :term)
+ORDER BY r1.dxid_desc56
+",
+            ['term' => '%' . $term . '%']
+        );
+        return response()->json(['success'=>true, 'data'=>$matches]);
+    }
+
+    public function dxICDsForDxID(Request $request) {
+        $matches = DB::connection('pgsql_fdb')->select("
+select r1.search_icd_cd, r1.icd_cd_type, r2.icd_desc
+from RFMLISR1_ICD_SEARCH r1
+left join RFMLINM1_ICD_DESC r2 on r1.search_icd_cd = r2.icd_cd
+where r1.related_dxid = :dxid
+  and (/*r1.icd_cd_type = '01' or */r1.icd_cd_type = '05')
+  and r1.fml_clin_code = '01'
+  /*and fml_nav_code = '01'*/
+",
+            ['dxid' => $request->input('dxid')]
+        );
+        return response()->json($matches);
+    }
+
+    // dx suggest v2 - from ICD tables
+    public function dxSuggestV2JSON(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select("
+SELECT DISTINCT ON (r1.search_icd_cd) search_icd_cd as sub_text, r1.icd_cd_type, r2.icd_desc as text, r1.related_dxid as dxid, r1.fml_clin_code, r1.fml_nav_code, r3.dxid_desc56
+FROM RFMLISR1_ICD_SEARCH r1
+JOIN RFMLINM1_ICD_DESC r2 ON r1.search_icd_cd = r2.icd_cd
+LEFT OUTER JOIN RFMLDX0_DXID r3 ON r1.related_dxid = r3.dxid
+WHERE r1.icd_cd_type = '05'
+    AND r1.fml_clin_code = '01'
+    AND (r2.icd_desc ILIKE :term OR r1.search_icd_cd ILIKE :term)
+ORDER BY r1.search_icd_cd ASC, r1.fml_nav_code ASC
+",
+            ['term' => '%' . $term . '%']
+        );
+        return response()->json(['success'=>true, 'data'=>$matches]);
+    }
+
+    // allergy suggest
+    public function allergySuggest(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select("
+SELECT r1.dam_concept_id, r1.dam_concept_id_typ, r1.dam_concept_id_desc
+FROM rdamca0_concept r1
+WHERE (r1.dam_concept_id_desc ILIKE :term)
+ORDER BY r1.dam_concept_id_desc
+",
+            ['term' => $term . '%']
+        );
+        return view('app.fdb-pg.fdb-allergy-suggest', compact('matches'));
+    }
+
+    // allergy suggest (json response)
+    public function allergySuggestJSON(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $matches = DB::connection('pgsql_fdb')->select("
+SELECT r1.dam_concept_id, r1.dam_concept_id_typ, r1.dam_concept_id_desc as text,
+r2.dam_concept_id_typ_desc as sub_text
+FROM rdamca0_concept r1 join rdamcd0_picklist_con_typ_desc r2 on r1.dam_concept_id_typ = r2.dam_concept_id_typ
+WHERE (r1.dam_concept_id_desc ILIKE :term)
+ORDER BY r1.dam_concept_id_desc
+",
+            ['term' => $term . '%']
+        );
+        return json_encode([
+            "success" => true,
+            "data" => $matches
+        ]);
+    }
+
+    // drug <-> allergy match making
+    public function drugAllergies(Request $request) {
+
+        // override
+        if($request->input('test')) {
+            $x = new \stdClass();
+            $x->allergen = 'Pollen';
+            $x->dam_concept_id_typ = 6;
+            $allergies = [$x];
+            $y = new \stdClass();
+            $y->rx = 'Brufen';
+            $rx = [$y];
+        }
+        else {
+            $input = json_decode($request->input('data'));
+            $allergies = $input->allergies;
+            $rx = $input->rx;
+        }
+
+        $output = [];
+
+        /*
+        for each allergy
+            if dam_concept_id_typ = 1 // allergen-group-id
+                // https://docs.fdbhealth.com/display/MKDOCUS/Screening+an+NDC+for+a+DAM_ALRGN_GRP+Allergen+-+Illustration+of+Scenario+C
+                ...
+            elseif dam_concept_id_typ = 2 // medication-name-id
+                // https://docs.fdbhealth.com/display/MKDOCUS/Screening+an+NDC+for+a+MED_NAME_ID+Allergen+-+Illustration+of+Scenario+B
+                ...
+            elseif dam_concept_id_typ = 6 // base-ingredient-id
+                // https://docs.fdbhealth.com/display/MKDOCUS/Screening+an+NDC+for+an+Ingredient+Allergen+-+Illustration+of+Scenario+A
+                ...
+            endif
+        endfor
+        */
+
+        foreach ($allergies as $allergy) {
+            foreach ($rx as $rxItem) {
+                if($allergy->dam_concept_id_typ == 6) { // ingredient
+                    if ($this->drugAllergyIngredientAllergenVsSingleRx($allergy, $rxItem)) {
+                        $output[] = "<b>{$rxItem->rx}</b> can cause allergic reactions since the patient is allergic to <b>{$allergy->allergen}</b>.";
+                    }
+                }
+                else if($allergy->dam_concept_id_typ == 2) { // medication
+                    if ($this->drugAllergyMedicationAllergenVsSingleRx($allergy, $rxItem)) {
+                        $output[] = "<b>{$rxItem->rx}</b> can cause allergic reactions since the patient is allergic to <b>{$allergy->allergen}</b>.";
+                    }
+                }
+                else if($allergy->dam_concept_id_typ == 1) { // allergen group
+                    if ($this->drugAllergyGroupAllergenVsSingleRx($allergy, $rxItem)) {
+                        $output[] = "<b>{$rxItem->rx}</b> can cause allergic reactions since the patient is allergic to <b>{$allergy->allergen}</b>.";
+                    }
+                }
+            }
+        }
+
+        return implode("<br>", $output);
+
+    }
+
+        private function drugAllergyIngredientAllergenVsSingleRx($_allergen, $_rx) {
+
+            $matches = DB::connection('pgsql_fdb')->select("
+    (
+        -- ingredients from medication
+        SELECT R1.related_hic_seqn as hic_seqn, R2.hic_desc
+        FROM RHICHCR0_HIC_HIC_LINK R1
+                 JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+        WHERE R1.hic_seqn IN (
+            SELECT S2.dam_alrgn_hic_seqn
+            FROM RMEDMHL0_MED_HICLSEQNO_LINK S1
+                     JOIN RDAMHHA0_HIC_HICL_ALG_LINK S2 ON S1.hicl_seqno = S2.hicl_seqno
+            WHERE S1.med_concept_id = :medid
+              AND S1.med_concept_id_typ = 3
+        )
+    )
+    INTERSECT
+    (
+        -- all ingredients directly and related from allergens
+        (
+            SELECT R1.related_hic_seqn as hic_seqn, R2.hic_desc
+            FROM RHICHCR0_HIC_HIC_LINK R1
+                     JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+            WHERE R1.hic_seqn = :allergenConceptID
+        )
+        UNION
+        -- all ingredients via related dam allergen groups
+        (
+            SELECT r3.hic_seqn, r4.hic_desc
+            FROM RDAMGHC0_HIC_ALRGN_GRP_LINK R1
+                     JOIN rdamagd1_alrgn_grp_desc R2 on R1.dam_alrgn_grp = R2.dam_alrgn_grp
+                     JOIN RDAMGHC0_HIC_ALRGN_GRP_LINK R3 on R1.dam_alrgn_grp = R3.dam_alrgn_grp
+                     JOIN RHICD5_HIC_DESC R4 on r3.hic_seqn = r4.hic_seqn
+            WHERE R1.hic_seqn = :allergenConceptID
+              AND R2.dam_alrgn_grp_status_cd = 0
+            ORDER BY r3.hic_seqn
+        )
+    )
+    ",
+                ['medid' => $_rx->medid, 'allergenConceptID' => $_allergen->dam_concept_id]
+            );
+
+            return !!count($matches);
+        }
+
+        private function drugAllergyMedicationAllergenVsSingleRx($_allergen, $_rx) {
+
+            $matches = DB::connection('pgsql_fdb')->select("
+    (
+        -- ingredients from medication
+        SELECT R1.related_hic_seqn as hic_seqn
+        FROM RHICHCR0_HIC_HIC_LINK R1
+                 JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+        WHERE R1.hic_seqn IN (
+            SELECT S2.dam_alrgn_hic_seqn
+            FROM RMEDMHL0_MED_HICLSEQNO_LINK S1
+                     JOIN RDAMHHA0_HIC_HICL_ALG_LINK S2 ON S1.hicl_seqno = S2.hicl_seqno
+            WHERE S1.med_concept_id = :medid
+              AND S1.med_concept_id_typ = 3
+        )
+    )
+    INTERSECT
+    (
+        -- all ingredients directly and related from allergens
+        SELECT R1.dam_alrgn_hic_seqn as hic_seqn
+        FROM RDAMHHA0_HIC_HICL_ALG_LINK R1
+        WHERE R1.hicl_seqno IN (
+            SELECT R1.hicl_seqno
+            FROM RMEDMHL0_MED_HICLSEQNO_LINK R1
+            WHERE R1.med_concept_id_typ = 1
+              AND R1.med_concept_id = :allergenConceptID
+              AND MED_CONCEPT_HICL_SRC_CD = 0
+        )
+    )
+    ",
+                ['medid' => $_rx->medid, 'allergenConceptID' => $_allergen->dam_concept_id]
+            );
+
+            return !!count($matches);
+        }
+
+        private function drugAllergyGroupAllergenVsSingleRx($_allergen, $_rx) {
+
+        $matches = DB::connection('pgsql_fdb')->select("
+(
+    -- ingredients from medication
+    SELECT R1.related_hic_seqn as hic_seqn
+    FROM RHICHCR0_HIC_HIC_LINK R1
+             JOIN RHICD5_HIC_DESC R2 ON R1.related_hic_seqn = R2.hic_seqn
+    WHERE R1.hic_seqn IN (
+        SELECT S2.dam_alrgn_hic_seqn
+        FROM RMEDMHL0_MED_HICLSEQNO_LINK S1
+                 JOIN RDAMHHA0_HIC_HICL_ALG_LINK S2 ON S1.hicl_seqno = S2.hicl_seqno
+        WHERE S1.med_concept_id = :medid
+          AND S1.med_concept_id_typ = 3
+    )
+)
+INTERSECT
+(
+    -- ingredients from medication allergen
+    (
+        SELECT R1.hic_seqn as hic_seqn
+        FROM RDAMGHC0_HIC_ALRGN_GRP_LINK R1
+        WHERE R1.DAM_ALRGN_GRP = :allergenConceptID
+    )
+    UNION
+    (
+        SELECT distinct s1.hic_seqn
+        FROM RDAMXHC0_HIC_ALRGN_XSENSE_LINK S1
+        WHERE S1.dam_alrgn_xsense IN (
+            SELECT R1.dam_alrgn_xsense
+            FROM RDAMGX0_ALRGN_GRP_XSENSE_LINK R1
+                     JOIN RDAMCSD1_XSENSIT_ALLERGY_DESC R2 on R1.dam_alrgn_xsense = R2.dam_alrgn_xsense
+            WHERE R1.dam_alrgn_grp = :allergenConceptID
+              AND R2.dam_alrgn_xsense_status_cd = 0
+        )
+    )
+)
+",
+            ['medid' => $_rx->medid, 'allergenConceptID' => $_allergen->dam_concept_id]
+        );
+
+        return !!count($matches);
+    }
+
+    // drug <-> drug match making
+    public function drugDrugInteraction(Request $request) {
+
+        if($request->input('test')) {
+            // override
+            $rx = json_decode(json_encode([
+                [
+                    "gcn_seqno" => "45190",
+                    "med_name_id" => "18604",
+                    "medid" => "234539",
+                    "routed_dosage_form_med_id" => "95130",
+                    "routed_med_id" => "19876",
+                    "rx" => "Zyprexa Zydis",
+                ],
+                [
+                    "gcn_seqno" => "49853",
+                    "med_name_id" => "26164",
+                    "medid" => "400058",
+                    "routed_dosage_form_med_id" => "36194",
+                    "routed_med_id" => "32562",
+                    "rx" => "Orfadin",
+                ],
+            ]));
+        }
+        else {
+            $input = json_decode($request->input('data'));
+            $rx = $input->rx;
+        }
+
+        if(count($rx) < 2) return "";
+
+        $leftIndex = 0;
+
+        $output = [];
+
+        for ($i=$leftIndex; $i<count($rx) - 1; $i++) {
+            for ($j=$i + 1; $j<count($rx); $j++) {
+                $output[] = $this->drugDrugInteractionSinglePair($rx[$i], $rx[$j]);
+            }
+        }
+
+        $output = array_filter($output, function($_x) {
+            return !!$_x;
+        });
+
+        return implode("<br>", $output);
+    }
+
+        private function drugDrugInteractionSinglePair($_rx1, $_rx2) {
+
+            $output = [];
+
+            // get active ingredient DDI_CODEX values for drug 1 and 2
+            $rx1ActiveDdiCodex = $this->getActiveDdiCodexFromGcnSeqNo($_rx1->gcn_seqno);
+            $rx2ActiveDdiCodex = $this->getActiveDdiCodexFromGcnSeqNo($_rx2->gcn_seqno);
+            if(!$rx1ActiveDdiCodex || !$rx2ActiveDdiCodex || !count($rx1ActiveDdiCodex) || !count($rx2ActiveDdiCodex)) return "";
+
+    //        dump($rx1_DDI_CODEX);
+    //        dump($rx2_DDI_CODEX);
+
+            // get inactive ingredient DDI_CODEX values for drug 1 and 2
+            // to get this we need to first get the NDCs of the drugs
+            $rx1Ndc = $this->getNdcFromMedId($_rx1->medid);
+            $rx2Ndc = $this->getNdcFromMedId($_rx2->medid);
+
+    //        dump($rx1Ndc);
+    //        dump($rx2Ndc);
+
+            $rx1InactiveDdiCodex = $this->getInactiveDdiCodexFromNdc($rx1Ndc);
+            $rx2InactiveDdiCodex = $this->getInactiveDdiCodexFromNdc($rx2Ndc);
+            // if(!$rx1InactiveDdiCodex || !$rx2InactiveDdiCodex || !count($rx1InactiveDdiCodex) || !count($rx2InactiveDdiCodex)) return "";
+
+    //        dump($rx1InactiveDdiCodex);
+    //        dump($rx2InactiveDdiCodex);
+
+            // get ddi codex - monox pairs for drug 1 & 2
+            $rx1ActiveDdiCodexMonoxPairs = $this->getDdiCodexMonoxPairs($rx1ActiveDdiCodex);
+            $rx1InactiveDdiCodexMonoxPairs = $this->getDdiCodexMonoxPairs($rx1InactiveDdiCodex);
+            $rx2ActiveDdiCodexMonoxPairs = $this->getDdiCodexMonoxPairs($rx2ActiveDdiCodex);
+            $rx2InactiveDdiCodexMonoxPairs = $this->getDdiCodexMonoxPairs($rx2InactiveDdiCodex);
+
+    //        dump($rx1ActiveDdiCodexMonoxPairs);
+    //        dump($rx1InactiveDdiCodexMonoxPairs);
+    //        dump($rx2ActiveDdiCodexMonoxPairs);
+    //        dump($rx2InactiveDdiCodexMonoxPairs);
+
+            // compare 1-active to 2-active and 2-inactive
+            $activeCatches = [];
+            foreach ($rx1ActiveDdiCodexMonoxPairs as $compareLeft) {
+                foreach ($rx2ActiveDdiCodexMonoxPairs as $compareRight) {
+                    if($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                        $activeCatches[] = $compareLeft->ddi_codex;
+                    }
+                }
+                foreach ($rx2InactiveDdiCodexMonoxPairs as $compareRight) {
+                    if($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                        $activeCatches[] = $compareLeft->ddi_codex;
+                    }
+                }
+            }
+
+            // compare 1-inactive to 2-active and 2-inactive
+            $inactiveCatches = [];
+            foreach ($rx1InactiveDdiCodexMonoxPairs as $compareLeft) {
+                foreach ($rx2ActiveDdiCodexMonoxPairs as $compareRight) {
+                    if($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                        $inactiveCatches[] = $compareLeft->ddi_codex;
+                    }
+                }
+                foreach ($rx2InactiveDdiCodexMonoxPairs as $compareRight) {
+                    if($compareLeft->ddi_monox == $compareRight->ddi_monox && $compareLeft->ddi_codex != $compareRight->ddi_codex) {
+                        $inactiveCatches[] = $compareLeft->ddi_codex;
+                    }
+                }
+            }
+
+            if(count($activeCatches)) {
+                $output[] = "<b>{$_rx2->rx}</b> interacts with one or more active ingredients in <b>{$_rx1->rx}</b>.";
+            }
+            if(count($inactiveCatches)) {
+                $output[] = "<b>{$_rx2->rx}</b> interacts with one or more inactive ingredients in <b>{$_rx1->rx}</b>.";
+            }
+
+            // TODO: find out and show the names of the actual ingredients causing interaction
+
+            return implode("<br>", $output);
+        }
+
+        private function getActiveDdiCodexFromGcnSeqNo($_gcnSeqNo) {
+            $ddiCodexArray = [];
+            $query = DB::connection('pgsql_fdb')->select("
+    SELECT r1.ddi_codex
+    FROM RADIMGC4_GCNSEQNO_LINK r1
+    WHERE r1.gcn_seqno = :gcnSeqNo
+                ",
+                ['gcnSeqNo' => $_gcnSeqNo]
+            );
+            if(count($query)) {
+                $ddiCodexArray = array_map(function($_x) {
+                    return $_x->ddi_codex;
+                }, $query);
+            }
+            return $ddiCodexArray;
+        }
+
+        private function getNdcFromMedId($_medId) {
+            $ndcArray = [];
+            $query = DB::connection('pgsql_fdb')->select("
+    select ndc from rmindc1_ndc_medid where medid = :medId
+                ",
+                ['medId' => $_medId]
+            );
+            if(count($query)) {
+                $ndcArray = array_map(function($_x) {
+                    return $_x->ndc;
+                }, $query);
+            }
+            return $ndcArray;
+        }
+
+        private function getInactiveDdiCodexFromNdc($_ndc) {
+            $ddiCodexArray = [];
+            $query = DB::connection('pgsql_fdb')->select("
+    SELECT distinct r1.ddi_codex
+    FROM RDDIMIN0_NDC_INACTV_DDIM_LINK r1
+    WHERE r1.ddi_ndc IN (" . implode(',', array_map(function($_x) {return "'" . $_x . "'";}, $_ndc)) . ")
+                "
+            );
+            if(count($query)) {
+                $ddiCodexArray = array_map(function($_x) {
+                    return $_x->ddi_codex;
+                }, $query);
+            }
+            return $ddiCodexArray;
+        }
+
+        private function getDdiCodexMonoxPairs($_ddiCodexArray) {
+        $ddiCodexMonoxPairsArray = [];
+        if(count($_ddiCodexArray)) {
+            $ddiCodexMonoxPairsArray = DB::connection('pgsql_fdb')->select("
+SELECT r1.ddi_codex, r1.ddi_monox
+FROM RADIMMA5_MSTR r1
+WHERE r1.ddi_codex IN (" . implode(',', array_map(function($_x) {return "'" . $_x . "'";}, $_ddiCodexArray)) . ")
+            "
+            );
+        }
+        return $ddiCodexMonoxPairsArray;
+    }
+
+    // drug <-> drug coadministration notes
+    public function drugCoadministration(Request $request) {
+        $gcnSeqnos = $request->input('gcn-seqnos') ? trim($request->input('gcn-seqnos')) : '';
+        if (empty($gcnSeqnos)) return '';
+        //$gcnSeqnos = explode(",", $gcnSeqnos);
+
+        $coadministration = DB::connection('pgsql_fdb')->select("
+SELECT distinct r1.coadmin_dosing_text
+FROM radige0_ddi_gcnseqno_except r1
+WHERE r1.side_a_gcn_seqno in ($gcnSeqnos) AND r1.side_b_gcn_seqno in ($gcnSeqnos)
+            "
+        );
+        return view('app.fdb-pg.fdb-coadministration', compact('coadministration'));
+    }
+
+    // duplicate therapy indications
+    public function duplicateTherapy(Request $request) {
+
+        if($request->input('test')) {
+            // override
+            $rx = json_decode(json_encode([
+                [
+                    "gcn_seqno" => "4376",
+                    "med_name_id" => "1076",
+                    "medid" => "172480",
+                    "routed_dosage_form_med_id" => "5870",
+                    "routed_med_id" => "1082",
+                    "rx" => "aspirin 325",
+                ],
+                [
+                    "gcn_seqno" => "4377",
+                    "med_name_id" => "1076",
+                    "medid" => "216092",
+                    "routed_dosage_form_med_id" => "5870",
+                    "routed_med_id" => "1082",
+                    "rx" => "aspirin 500",
+                ],
+            ]));
+        }
+        else {
+            $input = json_decode($request->input('data'));
+            $rx = $input->rx;
+        }
+
+        $dptClasses = [];
+        foreach ($rx as $rxItem) {
+            $rxItem->dpt = $this->getDptClassFromGcnSeqNo($rxItem->gcn_seqno);
+        }
+
+        // dd($rx);
+
+        $leftIndex = 0;
+        $matches = [];
+        for ($i=$leftIndex; $i<count($rx) - 1; $i++) {
+            for ($j=$i + 1; $j<count($rx); $j++) {
+                $compareResult = $this->compareDPTs($rx[$i]->dpt, $rx[$j]->dpt);
+                foreach ($compareResult as $c) {
+                    $matches[] = "<b>{$rx[$i]->rx}</b> and <b>{$rx[$j]->rx}</b> both participate in the duplicate therapy class <b>{$c->dpt_class_desc}</b> (duplicates allowed: {$c->dpt_allowance})";
+                }
+            }
+        }
+
+        return "<ol class='pl-0 ml-3'>" . implode("", array_map(function($_x) {
+                return "<li class='mb-2'>" . $_x . "</li>";
+            }, $matches)) . "</ol>";
+    }
+
+        private function getDptClassFromGcnSeqNo($_gcnSeqNo) {
+        return DB::connection('pgsql_fdb')->select("
+    SELECT distinct r1.dpt_class_id, r2.dpt_allowance, r2.dpt_class_desc
+    FROM RDPTGC0_GCNSEQNO_LINK r1
+    JOIN RDPTCL0_CLASS_ID r2 on r1.dpt_class_id = r2.dpt_class_id
+    WHERE r1.gcn_seqno = :gcnSeqNo
+                ",
+            ['gcnSeqNo' => $_gcnSeqNo]
+        );
+    }
+
+        private function compareDPTs($_dptArray1, $_dptArray2) {
+        $output = [];
+        for ($i = 0; $i < count($_dptArray1); $i++) {
+            for ($j = 0; $j < count($_dptArray2); $j++) {
+                if ($_dptArray1[$i]->dpt_class_id == $_dptArray2[$j]->dpt_class_id) {
+                    $output[] = json_decode(json_encode([
+                        "dpt_allowance" => $_dptArray1[$i]->dpt_allowance,
+                        "dpt_class_desc" => $_dptArray1[$i]->dpt_class_desc
+                    ]));
+                }
+            }
+        }
+        return $output;
+    }
+
+    public function rxVigilance(Request $request, Client $patient) {
+        return view('app.fdb-pg.rx-vigilance', compact('patient'));
+    }
+    public function dxVigilance(Request $request, Client $patient) {
+        return view('app.fdb-pg.dx-vigilance', compact('patient'));
+    }
+    public function allergyVigilance(Request $request, Client $patient) {
+        return view('app.fdb-pg.allergy-vigilance', compact('patient'));
+    }
+}

+ 106 - 0
app/Http/Controllers/GuestController.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Appointment;
+use App\Models\CareMonth;
+use App\Models\CareMonthEntry;
+use App\Models\Client;
+use App\Models\Handout;
+use App\Models\HandoutClient;
+use App\Models\Pro;
+use App\Models\Section;
+use App\Models\SectionTemplate;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\File;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Response;
+
+class GuestController extends Controller
+{
+    public function section(Request $request, $guestAccessCode )
+    {
+
+        $section = Section::where('guest_access_code', $guestAccessCode)->first();
+        abort_if(!$section, 404, 'Invalid access code');
+        abort_if(!$section->is_active, 404, 'Invalid access code');
+        abort_if($section->guest_access_level == 'NONE', 401, 'Invalid access code');
+
+        $patient = null;
+        if($section->note){
+            abort_if($section->note->is_signed_by_hcp, 401, 'Note is already signed.');
+            $patient = $section->note->client;
+        }else{
+            $patient = $section->client;
+        }
+
+       return view('app.guest.section', compact('patient','section', 'guestAccessCode'));
+    }
+
+    public function handout(Request $request, $handoutClientUid )
+    {
+
+        $handoutClient = HandoutClient::where('uid', $handoutClientUid)->first();
+        abort_if((!$handoutClient || !$handoutClient->is_active), 404, 'Invalid access code');
+
+        $handout = Handout::where('id', $handoutClient->handout_id)->first();
+        abort_if((!$handout || !$handout->is_active), 404, 'Invalid access code');
+
+        return Response::download(
+            $handout->pdf_file_path,
+            $handout->internal_name . '.pdf',
+            ['Content-Type: application/pdf']
+        );
+    }
+
+    public function appointmentConfirmation(Request $request, $appointmentUid )
+    {
+        $appointment = Appointment::where('uid', $appointmentUid)->first();
+        abort_if(!count($appointment->confirmationRequests), 404, 'No confirmation requests on this appointment.');
+        abort_if(!$appointment, 404, 'Invalid url');
+        abort_if($appointment->status == 'COMPLETED', 404, 'Appointment has been completed');
+
+       return view('app.guest.appointment-confirmation', compact('appointment'));
+    }
+
+    public function processAppointmentConfirmation(Request $request){
+        $appointmentUid = $request->get('appointment_uid');
+        $appointment = Appointment::where('uid', $appointmentUid)->first();
+        abort_if(!count($appointment->confirmationRequests), 404, 'No confirmation requests on this appointment.');
+        abort_if(!$appointment, 404, 'Invalid url');
+        abort_if($appointment->status == 'COMPLETED', 404, 'Appointment has been completed');
+
+        $decision = $request->get('decision');
+        $memo = $request->get('memo');
+        $response = null;
+        $data = [
+            'uid' => $appointment->uid,
+            'memo' => $memo,
+            'confirmationDecisionEnum' => ($decision == 'REJECT' ? 'CANCELLED' : 'CONFIRMED')
+        ];
+
+        $url = '/appointment/putConfirmationDecision';
+
+        $response = $this->calljava($request, $url, $data);
+
+        if($response['success']){
+            return redirect()->back()->with('success', true);
+        }
+        return redirect()->back()->with('error', true);
+    }
+
+    // TODO move to utility
+    private function callJava($request, $endPoint, $data)
+    {
+        $url =  config('stag.backendUrl') . $endPoint;
+        
+        $response = Http::asForm()
+            ->withHeaders([
+                'secret' => 'superman'
+            ])
+            ->post($url, $data)
+            ->json();
+        return $response;
+    }
+
+}

+ 2075 - 0
app/Http/Controllers/HomeController.php

@@ -0,0 +1,2075 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Lib\Backend;
+use App\Models\Appointment;
+use App\Models\AppointmentConfirmationDecision;
+use App\Models\AppSession;
+use App\Models\CareMonth;
+use App\Models\ClientMemo;
+use App\Models\ClientProChange;
+use App\Models\ClientSMS;
+use App\Models\Facility;
+use App\Models\IncomingReport;
+use App\Models\MBPayer;
+use App\Models\ProProAccess;
+use App\Models\SupplyOrder;
+use App\Models\Ticket;
+use DateTime;
+
+use App\Models\Client;
+use App\Models\Bill;
+use App\Models\Measurement;
+use App\Models\Note;
+use App\Models\Pro;
+use App\Models\ProTransaction;
+use GuzzleHttp\Cookie\CookieJar;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cookie;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Http;
+use App\Models\OutgoingEmailTemplate;
+
+class HomeController extends Controller
+{
+
+    public function nop() {
+        return json_encode(["success" => true]);
+    }
+
+    public function confirmSmsAuthToken(Request $request)
+    {
+        return view('app/confirm_sms_auth_token');
+    }
+
+    public function setPassword(Request $request)
+    {
+        return view('app/set_password');
+    }
+
+    public function setSecurityQuestions(Request $request)
+    {
+        return view('app/set_security_questions');
+    }
+
+    public function postConfirmSmsAuthToken(Request $request)
+    {
+
+        try {
+
+            $url = config('stag.backendUrl') . '/session/confirmSmsAuthToken';
+
+            $data = [
+                'cellNumber' => $request->input('cellNumber'),
+                'token' => $request->input('token'),
+            ];
+
+            $response = Http::asForm()
+                ->withHeaders(['sessionKey' => $request->cookie('sessionKey')])
+                ->post($url, $data)
+                ->json();
+
+            if (!isset($response['success']) || !$response['success']) {
+                $message = 'API error';
+                if (isset($response['error'])) {
+                    $message = $response['error'];
+                    if (isset($response['path'])) $message .= ': ' . $response['path'];
+                } else if (isset($response['message'])) $message = $response['message'];
+                return redirect('/confirm_sms_auth_token')
+                    ->withInput()
+                    ->with('message', $message);
+            }
+
+            return redirect('/');
+        } catch (\Exception $e) {
+            return redirect()->back()
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+
+    public function resendSmsAuthToken(Request $request)
+    {
+
+        try {
+
+            $url =  config('stag.backendUrl') . '/session/resendSmsAuthToken';
+
+            $data = [];
+
+            $response = Http::asForm()
+                ->withHeaders(['sessionKey' => $request->cookie('sessionKey')])
+                ->post($url, $data)
+                ->json();
+
+            if (!isset($response['success']) || !$response['success']) {
+                $message = 'API error';
+                if (isset($response['error'])) {
+                    $message = $response['error'];
+                    if (isset($response['path'])) $message .= ': ' . $response['path'];
+                } else if (isset($response['message'])) $message = $response['message'];
+                return redirect('/confirm_sms_auth_token')
+                    ->withInput()
+                    ->with('message', $message);
+            }
+
+            return redirect()->back()->withInput()->with('message', "SMS Auth Token sent.");
+        } catch (\Exception $e) {
+            return redirect()->back()
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+
+    public function postSetPassword(Request $request)
+    {
+        try {
+
+            $url =  config('stag.backendUrl') . '/pro/selfPutPassword';
+
+            $data = [
+                'newPassword' => $request->input('newPassword'),
+                'newPasswordConfirmation' => $request->input('newPasswordConfirmation'),
+            ];
+
+            $response = Http::asForm()
+                ->withHeaders(['sessionKey' => $request->cookie('sessionKey')])
+                ->post($url, $data)
+                ->json();
+
+            if (!isset($response['success']) || !$response['success']) {
+                $message = 'API error';
+                if (isset($response['error'])) {
+                    $message = $response['error'];
+                    if (isset($response['path'])) $message .= ': ' . $response['path'];
+                } else if (isset($response['message'])) $message = $response['message'];
+                return redirect('/set_password')
+                    ->withInput()
+                    ->with('message', $message);
+            }
+
+            return redirect('/');
+        } catch (\Exception $e) {
+            return redirect()->back()
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+    public function postSetSecurityQuestions(Request $request)
+    {
+
+        try {
+
+            $url = env('BACKEND_URL', 'http://localhost:8080/api') . '/pro/selfPutSecurityQuestions';
+
+            $data = [
+                'securityQuestion1' => $request->input('securityQuestion1'),
+                'securityAnswer1' => $request->input('securityAnswer1'),
+                'securityQuestion2' => $request->input('securityQuestion2'),
+                'securityAnswer2' => $request->input('securityAnswer2'),
+            ];
+
+            $response = Http::asForm()
+                ->withHeaders(['sessionKey' => $request->cookie('sessionKey')])
+                ->post($url, $data)
+                ->json();
+
+            if (!isset($response['success']) || !$response['success']) {
+                $message = 'API error';
+                if (isset($response['error'])) {
+                    $message = $response['error'];
+                    if (isset($response['path'])) $message .= ': ' . $response['path'];
+                } else if (isset($response['message'])) $message = $response['message'];
+                return redirect('/set_password')
+                    ->withInput()
+                    ->with('message', $message);
+            }
+
+            return redirect('/');
+        } catch (\Exception $e) {
+            return redirect()->back()
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+
+    public function dashboard_MCP(Request $request){
+
+
+        $keyNumbers = [];
+
+        // Patients // SELECT * FROM client WHERE mcp_pro_id = :me.id;
+        // New Patients Awaiting Visit // SELECT * FROM client WHERE mcp_pro_id = :me.id AND hasMcpDoneOnboardingVisit != 'YES';
+        // Notes Pending Signature // SELECT * FROM note WHERE hcp_pro_id = :me.id AND is_cancelled IS NOT TRUE AND has_hcp_signed IS NOT TRUE;
+        // Notes Pending Billing // SELECT * FROM note WHERE hcp_pro_id = :me.id AND is_cancelled IS NOT TRUE AND has_hcp_signed IS TRUE AND is_billing_marked_done IS FALSE;
+        // Reports Pending Signature // SELECT * FROM incoming_report WHERE hcp_pro_id = :me.id AND isEntryError IS NOT TRUE AND hasHcpProSigned IS NOT TRUE;
+        // Patients w/o Appointments // SELECT * FROM client WHERE mcp_pro_id = :me.id AND client.next_mcp_appointment_date < today();
+        // Patients Overdue for Visit // SELECT * FROM client WHERE mcp_pro_id = :me.id AND client.next_expected_mcp_visit_date < today();
+        // Cancelled Appts. Pending Review // SELECT * FROM appointment WHERE hcp_pro_id = :me.id AND status = 'REJECTED' AND wasAcknowledgedByAppointmentPro IS NOT TRUE;
+        // Cancelled Bills Pending Review // SELECT * FROM bill WHERE bill_service_type = 'NOTE' AND is_cancelled IS TRUE AND isCancellationAcknowledged IS FALSE;
+        // Cancelled Supply Orders Pending Review // SELECT * FROM supply_order WHERE signed_by_pro_id = :me.id AND is_cancelled IS TRUE AND isCancellationAcknowledged IS NOT TRUE;
+        // ERx & Orders Pending Signature // SELECT * FROM erx WHERE hcp_pro_id = :me.id AND pro_declared_status <> 'CANCELLED' AND hasHcpProSigned IS NOT TRUE;
+        // Supply Orders Pending Signature // SELECT supply_order.id FROM supply_order WHERE signed_by_pro_id IS NOT TRUE AND is_cancelled IS NOT TRUE AND created_by_pro_id = :me.id;
+
+        $performer = $this->performer();
+        $pro = $performer->pro;
+        $performerProID = $performer->pro->id;
+
+        $keyNumbers  = [];
+
+        $queryClients = $this->performer()->pro->getAccessibleClientsQuery();
+
+        $pendingNotesToSign = Note
+            ::where(function ($query) use ($performerProID) {
+                $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->orWhere(function ($query) use ($performerProID) {
+                $query->where('ally_pro_id', $performerProID)->where('is_signed_by_ally', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->count();
+        $keyNumbers['pendingNotesToSign'] = $pendingNotesToSign;
+
+        // notes pending mcp sign (applicable to dnas only)
+        $pendingNotesToSignMCP = Note
+            ::where(function ($query) use ($performerProID) {
+                $query->where('ally_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->count();
+        $keyNumbers['$pendingNotesToSignMCP'] = $pendingNotesToSignMCP;
+
+        $pendingNotesToSignAllySigned = Note::where(function ($query) use ($performerProID) {
+            $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_signed_by_ally', true)->where('is_cancelled', false)->where('is_core_note', false);;
+        })->count();
+        $keyNumbers['pendingNotesToSignAllySigned'] = $pendingNotesToSignAllySigned;
+
+
+        $signedNotesWithoutBills = Note::where(function ($query) use ($performerProID) {
+            $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', true)->where('is_cancelled', false);
+        })->whereDoesntHave('bills')->count();
+
+        $keyNumbers['signedNotesWithoutBills'] = $signedNotesWithoutBills;
+
+        // open tickets
+        $keyNumbers['numOpenTickets'] = Ticket::where('is_open', true)
+            ->where(function ($q) use ($performerProID) {
+                $q->where('assigned_pro_id', $performerProID)
+                    ->orWhere('manager_pro_id', $performerProID)
+                    ->orWhere('ordering_pro_id', $performerProID)
+                    ->orWhere('initiating_pro_id', $performerProID);
+            })
+            ->count();
+
+        // unacknowledged cancelled bills for authed pro
+        $keyNumbers['unacknowledgedCancelledBills'] = Bill::where('hcp_pro_id', $performerProID)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+            ->count();
+
+        // unacknowledged cancelled supply orders for authed pro
+        $keyNumbers['unacknowledgedCancelledSupplyOrders'] = SupplyOrder::where('signed_by_pro_id', $performerProID)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+            ->count();
+
+        // unsigned supply orders created by authed pro
+        $keyNumbers['unsignedSupplyOrders'] = SupplyOrder
+            ::where('is_cancelled', false)
+            ->where('is_signed_by_pro', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session WHERE pro_id = ?)', [$performerProID])
+            ->count();
+
+        // patientsHavingBirthdayToday
+        $queryClients = $this->performer()->pro->getAccessibleClientsQuery();
+        $keyNumbers['patientsHavingBirthdayToday'] = $queryClients
+            ->whereRaw('EXTRACT(DAY from dob) = ?', [date('d')])
+            ->whereRaw('EXTRACT(MONTH from dob) = ?', [date('m')])
+            ->count();
+
+        $reimbursement = [];
+        $reimbursement["currentBalance"] =  $performer->pro->balance;
+        $reimbursement["nextPaymentDate"] = '--';
+        $lastPayment = ProTransaction::where('pro_id', $performerProID)->where('plus_or_minus', 'PLUS')->orderBy('created_at', 'DESC')->first();
+        if ($lastPayment) {
+            $reimbursement["lastPayment"] =  $lastPayment->amount;
+            $reimbursement["lastPaymentDate"] = $lastPayment->created_at;
+        } else {
+            $reimbursement["lastPayment"] = '--';
+            $reimbursement["lastPaymentDate"] = '--';
+        }
+
+        //if today is < 15th, next payment is 15th, else nextPayment is
+        $today = strtotime(date('Y-m-d'));
+        $todayDate = date('j', $today);
+
+        $todayMonth =  date('m', $today);
+        $todayYear = date('Y', $today);
+        if ($todayDate < 15) {
+            $nextPaymentDate = new DateTime();
+            $nextPaymentDate->setDate($todayYear, $todayMonth, 15);
+            $reimbursement['nextPaymentDate'] = $nextPaymentDate->format('m/d/Y');
+        } else {
+            $nextPaymentDate = new \DateTime();
+            $lastDayOfMonth = date('t', $today);
+            $nextPaymentDate->setDate($todayYear, $todayMonth, $lastDayOfMonth);
+            $reimbursement['nextPaymentDate'] = $nextPaymentDate->format('m/d/Y');
+        }
+
+        //expectedPay
+        $expectedForHcp = DB::select(DB::raw("SELECT coalesce(SUM(hcp_expected_payment_amount),0) as expected_pay FROM bill WHERE hcp_pro_id = :performerProID  AND has_hcp_been_paid = false AND is_signed_by_hcp IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForCm = DB::select(DB::raw("SELECT coalesce(SUM(cm_expected_payment_amount),0) as expected_pay  FROM bill WHERE cm_pro_id = :performerProID  AND has_cm_been_paid = false AND is_signed_by_cm IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForRme = DB::select(DB::raw("SELECT coalesce(SUM(rme_expected_payment_amount),0) as expected_pay  FROM bill WHERE rme_pro_id = :performerProID  AND has_rme_been_paid = false AND is_signed_by_rme IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForRmm = DB::select(DB::raw("SELECT coalesce(SUM(rmm_expected_payment_amount),0) as expected_pay  FROM bill WHERE rmm_pro_id = :performerProID  AND has_rmm_been_paid = false AND is_signed_by_rmm IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForNa = DB::select(DB::raw("SELECT coalesce(SUM(generic_pro_expected_payment_amount),0) as expected_pay  FROM bill WHERE generic_pro_id = :performerProID  AND has_generic_pro_been_paid = false AND is_signed_by_generic_pro IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+
+        $totalExpectedAmount =  $expectedForHcp + $expectedForCm + $expectedForRme + $expectedForRmm + $expectedForNa;
+        $reimbursement['nextPaymentAmount'] =  $totalExpectedAmount;
+
+        $milliseconds = strtotime(date('Y-m-d')) . '000';
+
+        // bills & claims
+        $businessNumbers = [];
+
+        // Notes with bills to resolve
+        $businessNumbers['notesWithBillsToResolve'] = Note::where('is_cancelled', '!=', true)
+            ->where('is_bill_closed', '!=', true)
+            ->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND is_cancelled = false AND is_verified = false) > 0')
+            ->count();
+
+        // Notes pending bill closure
+        $businessNumbers['notesPendingBillingClosure'] = Note::where('is_cancelled', '!=', true)
+            ->where('is_bill_closed', '!=', true)
+            ->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND (is_cancelled = true OR is_verified = true)) = 0')
+            ->count();
+
+        // incoming reports not signed
+        $incomingReports = IncomingReport::where('hcp_pro_id', $performerProID)
+            ->where('has_hcp_pro_signed', false)
+            ->where('is_entry_error', false)
+            ->orderBy('created_at', 'ASC')
+            ->get();
+        // erx, labs & imaging that are not closed
+        $tickets = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->orderBy('created_at', 'ASC')
+            ->get();
+        $supplyOrders = SupplyOrder::where('is_cleared_for_shipment', false)
+            ->where('is_cancelled', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session where pro_id = ?)', [$performer->pro->id])
+            ->orderBy('created_at', 'ASC')
+            ->get();
+
+        $numERx = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'erx')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numLabs = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'lab')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numImaging = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'imaging')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numSupplyOrders = SupplyOrder::where('is_cleared_for_shipment', false)
+            ->where('is_cancelled', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session where pro_id = ?)', [$performer->pro->id])
+            ->count();
+
+        $newMCPAssociations = ClientProChange
+            ::where('new_pro_id', $performerProID)
+            ->where('responsibility_type', 'MCP')
+            ->whereNull('current_client_pro_change_decision_id')
+            ->get();
+
+        $newNAAssociations = ClientProChange
+            ::where('new_pro_id', $performerProID)
+            ->where('responsibility_type', 'DEFAULT_NA')
+            ->whereNull('current_client_pro_change_decision_id')
+            ->get();
+
+        // unstamped client memos
+        // for mcp
+        $mcpClientMemos = DB::select(
+            DB::raw("
+SELECT c.uid as client_uid, c.name_first, c.name_last,
+       cm.uid, cm.content, cm.created_at
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.mcp_pro_id = {$performerProID} AND
+      cm.mcp_stamp_id IS NULL
+      AND (is_admin_only IS FALSE OR is_admin_only IS NULL)
+ORDER BY cm.created_at DESC
+OFFSET 0 LIMIT 10
+            ")
+        );
+        $mcpClientMemosCount = DB::select(
+            DB::raw("
+SELECT count(c.uid)
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.mcp_pro_id = {$performerProID} AND
+      cm.mcp_stamp_id IS NULL
+      AND (is_admin_only IS FALSE OR is_admin_only IS NULL)
+            ")
+        );
+        if($mcpClientMemosCount && count($mcpClientMemosCount)) {
+            $mcpClientMemosCount = $mcpClientMemosCount[0]->count;
+        }
+
+        // for na
+        $naClientMemos = DB::select(
+            DB::raw("
+SELECT c.uid as client_uid, c.name_first, c.name_last,
+       cm.uid, cm.content, cm.created_at
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.default_na_pro_id = {$performerProID} AND
+      cm.default_na_stamp_id IS NULL
+ORDER BY cm.created_at DESC
+            ")
+        );
+
+        $keyNumbers['rmBillsToSign'] = Bill
+            ::where('is_cancelled', false)
+            ->where('cm_or_rm', 'RM')
+            ->where(function ($q) use ($performerProID) {
+                $q
+                    ->where(function ($q2) use ($performerProID) {
+                        $q2->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rme_pro_id', $performerProID)->where('is_signed_by_rme', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rmm_pro_id', $performerProID)->where('is_signed_by_rmm', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('generic_pro_id', $performerProID)->where('is_signed_by_generic_pro', false);
+                    });
+            })
+            ->count();
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.number_of_days_with_remote_measurements < 16 OR care_month.number_of_days_with_remote_measurements IS NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithLT16MD'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.number_of_days_with_remote_measurements >= 16 AND care_month.number_of_days_with_remote_measurements IS NOT NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithGTE16MD'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.has_anyone_interacted_with_client_about_rm_outside_note = TRUE AND care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NOT NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithWhomCommDone'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.has_anyone_interacted_with_client_about_rm_outside_note = FALSE OR care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithWhomCommNotDone'] = $count[0]->cnt;
+
+        // num measurements that need stamping
+        $keyNumbers['measurementsToBeStamped'] = $this->performer()->pro->getUnstampedMeasurementsFromCurrentMonth(true, null, null);
+
+        if($performer->pro->pro_type === 'ADMIN') {
+
+            // patients without coverage information
+            $keyNumbers['patientsWithoutCoverageInformation'] = DB::select(DB::raw("
+SELECT count(DISTINCT (cl.id)) as cnt
+FROM client cl
+WHERE cl.shadow_pro_id IS NULL AND cl.latest_client_primary_coverage_id IS NULL -- no coverage record"
+            ))[0]->cnt;
+
+            // patients pending coverage verification
+            $keyNumbers['patientsPendingCoverageVerification'] = DB::select(DB::raw("
+SELECT count(DISTINCT (cl.id)) as cnt
+FROM client cl
+         LEFT JOIN client_primary_coverage cpc ON cl.latest_client_primary_coverage_id = cpc.id
+WHERE cl.shadow_pro_id IS NULL
+    AND (cl.latest_client_primary_coverage_id IS NOT NULL -- coverage exists, but status is null or unknown
+    AND (
+               (cpc.plan_type = 'MEDICARE' AND (cpc.is_partbprimary = 'UNKNOWN' OR cpc.is_partbprimary IS NULL))
+               OR
+               (cpc.plan_type != 'MEDICARE' AND
+                (cpc.manual_determination_category = 'UNKNOWN' OR cpc.manual_determination_category IS NULL))
+           ))"
+            ))[0]->cnt;
+
+        }
+
+        $incomingSmsMessagesPendingReply = DB::select("
+            SELECT cs.* ,c.uid as client_uid, c.name_first as client_name_first, c.name_last as client_name_last FROM client_sms cs LEFT JOIN client c ON c.id = cs.client_id
+            WHERE cs.is_reply_needed = 'YES' AND c.mcp_pro_id = :mcp_pro_id AND incoming_or_outgoing = 'INCOMING'
+            AND (cs.created_at > c.last_sms_sent_to_client_at OR c.last_sms_sent_to_client_at IS NULL)
+            ORDER BY created_at DESC
+        ", ['mcp_pro_id' => $performer->pro->id]);
+
+        $careMonthsWithMeasurementsPendingStamping = CareMonth::select('id')
+            ->where('mcp_pro_id', $this->performer->pro->id)
+            ->where('rm_num_measurements_not_stamped_by_mcp', '>', 0)
+            ->whereNotNull('rm_num_measurements_not_stamped_by_mcp')
+            ->orderBy('created_at', 'DESC')
+            ->get()
+            ->map(function($_x) {
+                return $_x->id;
+            })
+            ->toArray();
+
+        $measurementsPendingStamping = Measurement::whereIn('care_month_id', $careMonthsWithMeasurementsPendingStamping)
+            ->orderBy('created_at', 'DESC')
+            ->whereNotNull('ts')
+            ->whereNotIn('label', ['SBP', 'DBP'])
+            ->where('is_cellular_zero', '<>', true)
+            ->where('is_active', true)
+            ->where('has_been_stamped_by_mcp', false)
+            ->whereNotNull('client_bdt_measurement_id')
+            ->paginate(15);
+
+        return view('app/dashboard-mcp', compact('keyNumbers', 'reimbursement', 'milliseconds',
+            'businessNumbers',
+            'incomingReports', 'tickets', 'supplyOrders',
+            'numERx', 'numLabs', 'numImaging', 'numSupplyOrders',
+            'newMCPAssociations', 'newNAAssociations',
+            'measurementsPendingStamping', 'careMonthsWithMeasurementsPendingStamping',
+            'mcpClientMemos', 'mcpClientMemosCount', 'naClientMemos', 'incomingSmsMessagesPendingReply'));
+    }
+
+    public function dashboard_HCP(Request $request){
+        //TODO provide hcp specific data. Currently is based on MCP context
+        $keyNumbers = [];
+
+        // Patients // SELECT * FROM client WHERE mcp_pro_id = :me.id;
+        // New Patients Awaiting Visit // SELECT * FROM client WHERE mcp_pro_id = :me.id AND hasMcpDoneOnboardingVisit != 'YES';
+        // Notes Pending Signature // SELECT * FROM note WHERE hcp_pro_id = :me.id AND is_cancelled IS NOT TRUE AND has_hcp_signed IS NOT TRUE;
+        // Notes Pending Billing // SELECT * FROM note WHERE hcp_pro_id = :me.id AND is_cancelled IS NOT TRUE AND has_hcp_signed IS TRUE AND is_billing_marked_done IS FALSE;
+        // Reports Pending Signature // SELECT * FROM incoming_report WHERE hcp_pro_id = :me.id AND isEntryError IS NOT TRUE AND hasHcpProSigned IS NOT TRUE;
+        // Patients w/o Appointments // SELECT * FROM client WHERE mcp_pro_id = :me.id AND client.next_mcp_appointment_date < today();
+        // Patients Overdue for Visit // SELECT * FROM client WHERE mcp_pro_id = :me.id AND client.next_expected_mcp_visit_date < today();
+        // Cancelled Appts. Pending Review // SELECT * FROM appointment WHERE hcp_pro_id = :me.id AND status = 'REJECTED' AND wasAcknowledgedByAppointmentPro IS NOT TRUE;
+        // Cancelled Bills Pending Review // SELECT * FROM bill WHERE bill_service_type = 'NOTE' AND is_cancelled IS TRUE AND isCancellationAcknowledged IS FALSE;
+        // Cancelled Supply Orders Pending Review // SELECT * FROM supply_order WHERE signed_by_pro_id = :me.id AND is_cancelled IS TRUE AND isCancellationAcknowledged IS NOT TRUE;
+        // ERx & Orders Pending Signature // SELECT * FROM erx WHERE hcp_pro_id = :me.id AND pro_declared_status <> 'CANCELLED' AND hasHcpProSigned IS NOT TRUE;
+        // Supply Orders Pending Signature // SELECT supply_order.id FROM supply_order WHERE signed_by_pro_id IS NOT TRUE AND is_cancelled IS NOT TRUE AND created_by_pro_id = :me.id;
+
+        $performer = $this->performer();
+        $pro = $performer->pro;
+        $performerProID = $performer->pro->id;
+
+        $keyNumbers  = [];
+
+        $queryClients = $this->performer()->pro->getAccessibleClientsQuery();
+
+        $pendingNotesToSign = Note
+            ::where(function ($query) use ($performerProID) {
+                $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->orWhere(function ($query) use ($performerProID) {
+                $query->where('ally_pro_id', $performerProID)->where('is_signed_by_ally', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->count();
+        $keyNumbers['pendingNotesToSign'] = $pendingNotesToSign;
+
+        // notes pending mcp sign (applicable to dnas only)
+        $pendingNotesToSignMCP = Note
+            ::where(function ($query) use ($performerProID) {
+                $query->where('ally_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->count();
+        $keyNumbers['$pendingNotesToSignMCP'] = $pendingNotesToSignMCP;
+
+        $pendingNotesToSignAllySigned = Note::where(function ($query) use ($performerProID) {
+            $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_signed_by_ally', true)->where('is_cancelled', false)->where('is_core_note', false);;
+        })->count();
+        $keyNumbers['pendingNotesToSignAllySigned'] = $pendingNotesToSignAllySigned;
+
+
+        $signedNotesWithoutBills = Note::where(function ($query) use ($performerProID) {
+            $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', true)->where('is_cancelled', false);
+        })->whereDoesntHave('bills')->count();
+
+        $keyNumbers['signedNotesWithoutBills'] = $signedNotesWithoutBills;
+
+        // open tickets
+        $keyNumbers['numOpenTickets'] = Ticket::where('is_open', true)
+            ->where(function ($q) use ($performerProID) {
+                $q->where('assigned_pro_id', $performerProID)
+                    ->orWhere('manager_pro_id', $performerProID)
+                    ->orWhere('ordering_pro_id', $performerProID)
+                    ->orWhere('initiating_pro_id', $performerProID);
+            })
+            ->count();
+
+        // unacknowledged cancelled bills for authed pro
+        $keyNumbers['unacknowledgedCancelledBills'] = Bill::where('hcp_pro_id', $performerProID)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+            ->count();
+
+        // unacknowledged cancelled supply orders for authed pro
+        $keyNumbers['unacknowledgedCancelledSupplyOrders'] = SupplyOrder::where('signed_by_pro_id', $performerProID)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+            ->count();
+
+        // unsigned supply orders created by authed pro
+        $keyNumbers['unsignedSupplyOrders'] = SupplyOrder
+            ::where('is_cancelled', false)
+            ->where('is_signed_by_pro', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session WHERE pro_id = ?)', [$performerProID])
+            ->count();
+
+        // patientsHavingBirthdayToday
+        $queryClients = $this->performer()->pro->getAccessibleClientsQuery();
+        $keyNumbers['patientsHavingBirthdayToday'] = $queryClients
+            ->whereRaw('EXTRACT(DAY from dob) = ?', [date('d')])
+            ->whereRaw('EXTRACT(MONTH from dob) = ?', [date('m')])
+            ->count();
+
+        $reimbursement = [];
+        $reimbursement["currentBalance"] =  $performer->pro->balance;
+        $reimbursement["nextPaymentDate"] = '--';
+        $lastPayment = ProTransaction::where('pro_id', $performerProID)->where('plus_or_minus', 'PLUS')->orderBy('created_at', 'DESC')->first();
+        if ($lastPayment) {
+            $reimbursement["lastPayment"] =  $lastPayment->amount;
+            $reimbursement["lastPaymentDate"] = $lastPayment->created_at;
+        } else {
+            $reimbursement["lastPayment"] = '--';
+            $reimbursement["lastPaymentDate"] = '--';
+        }
+
+        //if today is < 15th, next payment is 15th, else nextPayment is
+        $today = strtotime(date('Y-m-d'));
+        $todayDate = date('j', $today);
+
+        $todayMonth =  date('m', $today);
+        $todayYear = date('Y', $today);
+        if ($todayDate < 15) {
+            $nextPaymentDate = new DateTime();
+            $nextPaymentDate->setDate($todayYear, $todayMonth, 15);
+            $reimbursement['nextPaymentDate'] = $nextPaymentDate->format('m/d/Y');
+        } else {
+            $nextPaymentDate = new \DateTime();
+            $lastDayOfMonth = date('t', $today);
+            $nextPaymentDate->setDate($todayYear, $todayMonth, $lastDayOfMonth);
+            $reimbursement['nextPaymentDate'] = $nextPaymentDate->format('m/d/Y');
+        }
+
+        //expectedPay
+        $expectedForHcp = DB::select(DB::raw("SELECT coalesce(SUM(hcp_expected_payment_amount),0) as expected_pay FROM bill WHERE hcp_pro_id = :performerProID  AND has_hcp_been_paid = false AND is_signed_by_hcp IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForCm = DB::select(DB::raw("SELECT coalesce(SUM(cm_expected_payment_amount),0) as expected_pay  FROM bill WHERE cm_pro_id = :performerProID  AND has_cm_been_paid = false AND is_signed_by_cm IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForRme = DB::select(DB::raw("SELECT coalesce(SUM(rme_expected_payment_amount),0) as expected_pay  FROM bill WHERE rme_pro_id = :performerProID  AND has_rme_been_paid = false AND is_signed_by_rme IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForRmm = DB::select(DB::raw("SELECT coalesce(SUM(rmm_expected_payment_amount),0) as expected_pay  FROM bill WHERE rmm_pro_id = :performerProID  AND has_rmm_been_paid = false AND is_signed_by_rmm IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForNa = DB::select(DB::raw("SELECT coalesce(SUM(generic_pro_expected_payment_amount),0) as expected_pay  FROM bill WHERE generic_pro_id = :performerProID  AND has_generic_pro_been_paid = false AND is_signed_by_generic_pro IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+
+        $totalExpectedAmount =  $expectedForHcp + $expectedForCm + $expectedForRme + $expectedForRmm + $expectedForNa;
+        $reimbursement['nextPaymentAmount'] =  $totalExpectedAmount;
+
+        $milliseconds = strtotime(date('Y-m-d')) . '000';
+
+        // bills & claims
+        $businessNumbers = [];
+
+        // Notes with bills to resolve
+        $businessNumbers['notesWithBillsToResolve'] = Note::where('is_cancelled', '!=', true)
+            ->where('is_bill_closed', '!=', true)
+            ->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND is_cancelled = false AND is_verified = false) > 0')
+            ->count();
+
+        // Notes pending bill closure
+        $businessNumbers['notesPendingBillingClosure'] = Note::where('is_cancelled', '!=', true)
+            ->where('is_bill_closed', '!=', true)
+            ->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND (is_cancelled = true OR is_verified = true)) = 0')
+            ->count();
+
+        // incoming reports not signed
+        $incomingReports = IncomingReport::where('hcp_pro_id', $performerProID)
+            ->where('has_hcp_pro_signed', false)
+            ->where('is_entry_error', false)
+            ->orderBy('created_at', 'ASC')
+            ->get();
+        // erx, labs & imaging that are not closed
+        $tickets = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->orderBy('created_at', 'ASC')
+            ->get();
+        $supplyOrders = SupplyOrder::where('is_cleared_for_shipment', false)
+            ->where('is_cancelled', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session where pro_id = ?)', [$performer->pro->id])
+            ->orderBy('created_at', 'ASC')
+            ->get();
+
+        $numERx = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'erx')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numLabs = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'lab')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numImaging = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'imaging')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numSupplyOrders = SupplyOrder::where('is_cleared_for_shipment', false)
+            ->where('is_cancelled', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session where pro_id = ?)', [$performer->pro->id])
+            ->count();
+
+        $newMCPAssociations = ClientProChange
+            ::where('new_pro_id', $performerProID)
+            ->where('responsibility_type', 'MCP')
+            ->whereNull('current_client_pro_change_decision_id')
+            ->get();
+
+        $newNAAssociations = ClientProChange
+            ::where('new_pro_id', $performerProID)
+            ->where('responsibility_type', 'DEFAULT_NA')
+            ->whereNull('current_client_pro_change_decision_id')
+            ->get();
+
+        // unstamped client memos
+        // for mcp
+        $mcpClientMemos = DB::select(
+            DB::raw("
+SELECT c.uid as client_uid, c.name_first, c.name_last,
+       cm.uid, cm.content, cm.created_at
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.mcp_pro_id = {$performerProID} AND
+      cm.mcp_stamp_id IS NULL
+      AND (is_admin_only IS FALSE OR is_admin_only IS NULL)
+ORDER BY cm.created_at DESC
+OFFSET 0 LIMIT 10
+            ")
+        );
+        $mcpClientMemosCount = DB::select(
+            DB::raw("
+SELECT count(c.uid)
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.mcp_pro_id = {$performerProID} AND
+      cm.mcp_stamp_id IS NULL
+      AND (is_admin_only IS FALSE OR is_admin_only IS NULL)
+            ")
+        );
+        if($mcpClientMemosCount && count($mcpClientMemosCount)) {
+            $mcpClientMemosCount = $mcpClientMemosCount[0]->count;
+        }
+
+        // for na
+        $naClientMemos = DB::select(
+            DB::raw("
+SELECT c.uid as client_uid, c.name_first, c.name_last,
+       cm.uid, cm.content, cm.created_at
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.default_na_pro_id = {$performerProID} AND
+      cm.default_na_stamp_id IS NULL
+ORDER BY cm.created_at DESC
+            ")
+        );
+
+        $keyNumbers['rmBillsToSign'] = Bill
+            ::where('is_cancelled', false)
+            ->where('cm_or_rm', 'RM')
+            ->where(function ($q) use ($performerProID) {
+                $q
+                    ->where(function ($q2) use ($performerProID) {
+                        $q2->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rme_pro_id', $performerProID)->where('is_signed_by_rme', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rmm_pro_id', $performerProID)->where('is_signed_by_rmm', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('generic_pro_id', $performerProID)->where('is_signed_by_generic_pro', false);
+                    });
+            })
+            ->count();
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.number_of_days_with_remote_measurements < 16 OR care_month.number_of_days_with_remote_measurements IS NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithLT16MD'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.number_of_days_with_remote_measurements >= 16 AND care_month.number_of_days_with_remote_measurements IS NOT NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithGTE16MD'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.has_anyone_interacted_with_client_about_rm_outside_note = TRUE AND care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NOT NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithWhomCommDone'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.has_anyone_interacted_with_client_about_rm_outside_note = FALSE OR care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithWhomCommNotDone'] = $count[0]->cnt;
+
+        // num measurements that need stamping
+        $keyNumbers['measurementsToBeStamped'] = $this->performer()->pro->getUnstampedMeasurementsFromCurrentMonth(true, null, null);
+
+        if($performer->pro->pro_type === 'ADMIN') {
+
+            // patients without coverage information
+            $keyNumbers['patientsWithoutCoverageInformation'] = DB::select(DB::raw("
+SELECT count(DISTINCT (cl.id)) as cnt
+FROM client cl
+WHERE cl.shadow_pro_id IS NULL AND cl.latest_client_primary_coverage_id IS NULL -- no coverage record"
+            ))[0]->cnt;
+
+            // patients pending coverage verification
+            $keyNumbers['patientsPendingCoverageVerification'] = DB::select(DB::raw("
+SELECT count(DISTINCT (cl.id)) as cnt
+FROM client cl
+         LEFT JOIN client_primary_coverage cpc ON cl.latest_client_primary_coverage_id = cpc.id
+WHERE cl.shadow_pro_id IS NULL
+    AND (cl.latest_client_primary_coverage_id IS NOT NULL -- coverage exists, but status is null or unknown
+    AND (
+               (cpc.plan_type = 'MEDICARE' AND (cpc.is_partbprimary = 'UNKNOWN' OR cpc.is_partbprimary IS NULL))
+               OR
+               (cpc.plan_type != 'MEDICARE' AND
+                (cpc.manual_determination_category = 'UNKNOWN' OR cpc.manual_determination_category IS NULL))
+           ))"
+            ))[0]->cnt;
+
+        }
+
+        $incomingSmsMessagesPendingReply = DB::select("
+            SELECT cs.* ,c.uid as client_uid, c.name_first as client_name_first, c.name_last as client_name_last FROM client_sms cs LEFT JOIN client c ON c.id = cs.client_id
+            WHERE cs.is_reply_needed = 'YES' AND c.mcp_pro_id = :mcp_pro_id AND incoming_or_outgoing = 'INCOMING'
+            AND (cs.created_at > c.last_sms_sent_to_client_at OR c.last_sms_sent_to_client_at IS NULL)
+            ORDER BY created_at DESC
+        ", ['mcp_pro_id' => $performer->pro->id]);
+
+        $careMonthsWithMeasurementsPendingStamping = CareMonth::select('id')
+            ->where('mcp_pro_id', $this->performer->pro->id)
+            ->where('rm_num_measurements_not_stamped_by_mcp', '>', 0)
+            ->whereNotNull('rm_num_measurements_not_stamped_by_mcp')
+            ->orderBy('created_at', 'DESC')
+            ->get()
+            ->map(function($_x) {
+                return $_x->id;
+            })
+            ->toArray();
+
+        $measurementsPendingStamping = Measurement::whereIn('care_month_id', $careMonthsWithMeasurementsPendingStamping)
+            ->orderBy('created_at', 'DESC')
+            ->whereNotNull('ts')
+            ->whereNotIn('label', ['SBP', 'DBP'])
+            ->where('is_cellular_zero', '<>', true)
+            ->where('is_active', true)
+            ->where('has_been_stamped_by_mcp', false)
+            ->whereNotNull('client_bdt_measurement_id')
+            ->paginate(15);
+
+        return view('app/dashboard-hcp', compact('keyNumbers', 'reimbursement', 'milliseconds',
+            'businessNumbers',
+            'incomingReports', 'tickets', 'supplyOrders',
+            'numERx', 'numLabs', 'numImaging', 'numSupplyOrders',
+            'newMCPAssociations', 'newNAAssociations',
+            'measurementsPendingStamping', 'careMonthsWithMeasurementsPendingStamping',
+            'mcpClientMemos', 'mcpClientMemosCount', 'naClientMemos', 'incomingSmsMessagesPendingReply'));
+    }
+
+    public function dashboard_DNA(Request $request){
+        $performer = $this->performer();
+        $pro = $performer->pro;
+        $performerProID = $performer->pro->id;
+
+        $milliseconds = strtotime(date('Y-m-d')) . '000'; //required by the calendar
+        return view('app/dashboard-dna', compact( 'milliseconds'));
+    }
+
+    public function dashboard_ADMIN(Request $request){
+        $keyNumbers = [];
+
+        // Patients // SELECT * FROM client WHERE mcp_pro_id = :me.id;
+        // New Patients Awaiting Visit // SELECT * FROM client WHERE mcp_pro_id = :me.id AND hasMcpDoneOnboardingVisit != 'YES';
+        // Notes Pending Signature // SELECT * FROM note WHERE hcp_pro_id = :me.id AND is_cancelled IS NOT TRUE AND has_hcp_signed IS NOT TRUE;
+        // Notes Pending Billing // SELECT * FROM note WHERE hcp_pro_id = :me.id AND is_cancelled IS NOT TRUE AND has_hcp_signed IS TRUE AND is_billing_marked_done IS FALSE;
+        // Reports Pending Signature // SELECT * FROM incoming_report WHERE hcp_pro_id = :me.id AND isEntryError IS NOT TRUE AND hasHcpProSigned IS NOT TRUE;
+        // Patients w/o Appointments // SELECT * FROM client WHERE mcp_pro_id = :me.id AND client.next_mcp_appointment_date < today();
+        // Patients Overdue for Visit // SELECT * FROM client WHERE mcp_pro_id = :me.id AND client.next_expected_mcp_visit_date < today();
+        // Cancelled Appts. Pending Review // SELECT * FROM appointment WHERE hcp_pro_id = :me.id AND status = 'REJECTED' AND wasAcknowledgedByAppointmentPro IS NOT TRUE;
+        // Cancelled Bills Pending Review // SELECT * FROM bill WHERE bill_service_type = 'NOTE' AND is_cancelled IS TRUE AND isCancellationAcknowledged IS FALSE;
+        // Cancelled Supply Orders Pending Review // SELECT * FROM supply_order WHERE signed_by_pro_id = :me.id AND is_cancelled IS TRUE AND isCancellationAcknowledged IS NOT TRUE;
+        // ERx & Orders Pending Signature // SELECT * FROM erx WHERE hcp_pro_id = :me.id AND pro_declared_status <> 'CANCELLED' AND hasHcpProSigned IS NOT TRUE;
+        // Supply Orders Pending Signature // SELECT supply_order.id FROM supply_order WHERE signed_by_pro_id IS NOT TRUE AND is_cancelled IS NOT TRUE AND created_by_pro_id = :me.id;
+
+        $performer = $this->performer();
+        $pro = $performer->pro;
+        $performerProID = $performer->pro->id;
+
+        $keyNumbers  = [];
+
+        $queryClients = $this->performer()->pro->getAccessibleClientsQuery();
+
+        $pendingNotesToSign = Note
+            ::where(function ($query) use ($performerProID) {
+                $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->orWhere(function ($query) use ($performerProID) {
+                $query->where('ally_pro_id', $performerProID)->where('is_signed_by_ally', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->count();
+        $keyNumbers['pendingNotesToSign'] = $pendingNotesToSign;
+
+        // notes pending mcp sign (applicable to dnas only)
+        $pendingNotesToSignMCP = Note
+            ::where(function ($query) use ($performerProID) {
+                $query->where('ally_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_cancelled', false)->where('is_core_note', false);
+            })
+            ->count();
+        $keyNumbers['$pendingNotesToSignMCP'] = $pendingNotesToSignMCP;
+
+        $pendingNotesToSignAllySigned = Note::where(function ($query) use ($performerProID) {
+            $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false)->where('is_signed_by_ally', true)->where('is_cancelled', false)->where('is_core_note', false);;
+        })->count();
+        $keyNumbers['pendingNotesToSignAllySigned'] = $pendingNotesToSignAllySigned;
+
+
+        $signedNotesWithoutBills = Note::where(function ($query) use ($performerProID) {
+            $query->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', true)->where('is_cancelled', false);
+        })->whereDoesntHave('bills')->count();
+
+        $keyNumbers['signedNotesWithoutBills'] = $signedNotesWithoutBills;
+
+        // open tickets
+        $keyNumbers['numOpenTickets'] = Ticket::where('is_open', true)
+            ->where(function ($q) use ($performerProID) {
+                $q->where('assigned_pro_id', $performerProID)
+                    ->orWhere('manager_pro_id', $performerProID)
+                    ->orWhere('ordering_pro_id', $performerProID)
+                    ->orWhere('initiating_pro_id', $performerProID);
+            })
+            ->count();
+
+        // unacknowledged cancelled bills for authed pro
+        $keyNumbers['unacknowledgedCancelledBills'] = Bill::where('hcp_pro_id', $performerProID)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+            ->count();
+
+        // unacknowledged cancelled supply orders for authed pro
+        $keyNumbers['unacknowledgedCancelledSupplyOrders'] = SupplyOrder::where('signed_by_pro_id', $performerProID)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+            ->count();
+
+        // unsigned supply orders created by authed pro
+        $keyNumbers['unsignedSupplyOrders'] = SupplyOrder
+            ::where('is_cancelled', false)
+            ->where('is_signed_by_pro', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session WHERE pro_id = ?)', [$performerProID])
+            ->count();
+
+        // patientsHavingBirthdayToday
+        $queryClients = $this->performer()->pro->getAccessibleClientsQuery();
+        $keyNumbers['patientsHavingBirthdayToday'] = $queryClients
+            ->whereRaw('EXTRACT(DAY from dob) = ?', [date('d')])
+            ->whereRaw('EXTRACT(MONTH from dob) = ?', [date('m')])
+            ->count();
+
+        $reimbursement = [];
+        $reimbursement["currentBalance"] =  $performer->pro->balance;
+        $reimbursement["nextPaymentDate"] = '--';
+        $lastPayment = ProTransaction::where('pro_id', $performerProID)->where('plus_or_minus', 'PLUS')->orderBy('created_at', 'DESC')->first();
+        if ($lastPayment) {
+            $reimbursement["lastPayment"] =  $lastPayment->amount;
+            $reimbursement["lastPaymentDate"] = $lastPayment->created_at;
+        } else {
+            $reimbursement["lastPayment"] = '--';
+            $reimbursement["lastPaymentDate"] = '--';
+        }
+
+        //if today is < 15th, next payment is 15th, else nextPayment is
+        $today = strtotime(date('Y-m-d'));
+        $todayDate = date('j', $today);
+
+        $todayMonth =  date('m', $today);
+        $todayYear = date('Y', $today);
+        if ($todayDate < 15) {
+            $nextPaymentDate = new DateTime();
+            $nextPaymentDate->setDate($todayYear, $todayMonth, 15);
+            $reimbursement['nextPaymentDate'] = $nextPaymentDate->format('m/d/Y');
+        } else {
+            $nextPaymentDate = new \DateTime();
+            $lastDayOfMonth = date('t', $today);
+            $nextPaymentDate->setDate($todayYear, $todayMonth, $lastDayOfMonth);
+            $reimbursement['nextPaymentDate'] = $nextPaymentDate->format('m/d/Y');
+        }
+
+        //expectedPay
+        $expectedForHcp = DB::select(DB::raw("SELECT coalesce(SUM(hcp_expected_payment_amount),0) as expected_pay FROM bill WHERE hcp_pro_id = :performerProID  AND has_hcp_been_paid = false AND is_signed_by_hcp IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForCm = DB::select(DB::raw("SELECT coalesce(SUM(cm_expected_payment_amount),0) as expected_pay  FROM bill WHERE cm_pro_id = :performerProID  AND has_cm_been_paid = false AND is_signed_by_cm IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForRme = DB::select(DB::raw("SELECT coalesce(SUM(rme_expected_payment_amount),0) as expected_pay  FROM bill WHERE rme_pro_id = :performerProID  AND has_rme_been_paid = false AND is_signed_by_rme IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForRmm = DB::select(DB::raw("SELECT coalesce(SUM(rmm_expected_payment_amount),0) as expected_pay  FROM bill WHERE rmm_pro_id = :performerProID  AND has_rmm_been_paid = false AND is_signed_by_rmm IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+        $expectedForNa = DB::select(DB::raw("SELECT coalesce(SUM(generic_pro_expected_payment_amount),0) as expected_pay  FROM bill WHERE generic_pro_id = :performerProID  AND has_generic_pro_been_paid = false AND is_signed_by_generic_pro IS TRUE AND is_cancelled = false"), ['performerProID' => $performerProID])[0]->expected_pay;
+
+        $totalExpectedAmount =  $expectedForHcp + $expectedForCm + $expectedForRme + $expectedForRmm + $expectedForNa;
+        $reimbursement['nextPaymentAmount'] =  $totalExpectedAmount;
+
+        $milliseconds = strtotime(date('Y-m-d')) . '000';
+
+        // bills & claims
+        $businessNumbers = [];
+
+        // Notes with bills to resolve
+        $businessNumbers['notesWithBillsToResolve'] = Note::where('is_cancelled', '!=', true)
+            ->where('is_bill_closed', '!=', true)
+            ->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND is_cancelled = false AND is_verified = false) > 0')
+            ->count();
+
+        // Notes pending bill closure
+        $businessNumbers['notesPendingBillingClosure'] = Note::where('is_cancelled', '!=', true)
+            ->where('is_bill_closed', '!=', true)
+            ->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND (is_cancelled = true OR is_verified = true)) = 0')
+            ->count();
+
+        // incoming reports not signed
+        $incomingReports = IncomingReport::where('hcp_pro_id', $performerProID)
+            ->where('has_hcp_pro_signed', false)
+            ->where('is_entry_error', false)
+            ->orderBy('created_at', 'ASC')
+            ->get();
+        // erx, labs & imaging that are not closed
+        $tickets = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->orderBy('created_at', 'ASC')
+            ->get();
+        $supplyOrders = SupplyOrder::where('is_cleared_for_shipment', false)
+            ->where('is_cancelled', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session where pro_id = ?)', [$performer->pro->id])
+            ->orderBy('created_at', 'ASC')
+            ->get();
+
+        $numERx = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'erx')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numLabs = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'lab')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numImaging = Ticket::where('ordering_pro_id', $performerProID)
+            ->where('category', 'imaging')
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->count();
+        $numSupplyOrders = SupplyOrder::where('is_cleared_for_shipment', false)
+            ->where('is_cancelled', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session where pro_id = ?)', [$performer->pro->id])
+            ->count();
+
+        $newMCPAssociations = ClientProChange
+            ::where('new_pro_id', $performerProID)
+            ->where('responsibility_type', 'MCP')
+            ->whereNull('current_client_pro_change_decision_id')
+            ->get();
+
+        $newNAAssociations = ClientProChange
+            ::where('new_pro_id', $performerProID)
+            ->where('responsibility_type', 'DEFAULT_NA')
+            ->whereNull('current_client_pro_change_decision_id')
+            ->get();
+
+        $proApptUpdates = AppointmentConfirmationDecision
+            ::select('appointment_confirmation_decision.uid', 'client.name_first', 'client.name_last', 'appointment.start_time')
+            ->rightJoin('appointment', 'appointment.id', '=', 'appointment_confirmation_decision.appointment_id')
+            ->rightJoin('client', 'client.id', '=', 'appointment.client_id')
+            ->where('appointment_confirmation_decision.was_acknowledged_by_appointment_pro', false)
+            ->where('appointment.status', '!=', 'CREATED')
+            ->where('appointment.status', '!=', 'COMPLETED')
+            ->where('appointment.status', '!=', 'ABANDONED')
+            ->where('appointment.pro_id', $performerProID)
+            ->where('client.mcp_pro_id', $performerProID)
+            ->orderBy('appointment.start_time', 'DESC')
+            ->get();
+
+        $naApptUpdates = AppointmentConfirmationDecision
+            ::select('appointment_confirmation_decision.uid', 'client.name_first', 'client.name_last', 'pro.name_first as pro_name_first', 'pro.name_last as pro_name_last', 'appointment.start_time')
+            ->rightJoin('appointment', 'appointment.id', '=', 'appointment_confirmation_decision.appointment_id')
+            ->rightJoin('client', 'client.id', '=', 'appointment.client_id')
+            ->rightJoin('pro', 'pro.id', '=', 'appointment.pro_id')
+            ->where('appointment_confirmation_decision.was_acknowledged_by_client_default_na', false)
+            ->where('appointment.status', '!=', 'CREATED')
+            ->where('appointment.status', '!=', 'COMPLETED')
+            ->where('appointment.status', '!=', 'ABANDONED')
+            ->where('client.default_na_pro_id', $performerProID)
+            ->orderBy('appointment.start_time', 'DESC')
+            ->get();
+
+//        $naApptUpdates = AppointmentConfirmationDecision
+//            ::join('appointment', 'appointment.id', '=', 'appointment_confirmation_decision.appointment_id')
+//            ->join('client', 'client.id', '=', 'appointment.client_id')
+//            ->where('client.default_na_pro_id', $performerProID)
+//            ->where('appointment_confirmation_decision.was_acknowledged_by_client_default_na', false)
+//            ->orderBy('appointment.start_time DESC')
+//            ->get();
+
+        // unstamped client memos
+        // for mcp
+        $mcpClientMemos = DB::select(
+            DB::raw("
+SELECT c.uid as client_uid, c.name_first, c.name_last,
+       cm.uid, cm.content, cm.created_at
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.mcp_pro_id = {$performerProID} AND
+      cm.mcp_stamp_id IS NULL
+ORDER BY cm.created_at DESC
+            ")
+        );
+        // for na
+        $naClientMemos = DB::select(
+            DB::raw("
+SELECT c.uid as client_uid, c.name_first, c.name_last,
+       cm.uid, cm.content, cm.created_at
+FROM client c join client_memo cm on c.id = cm.client_id
+WHERE
+      c.default_na_pro_id = {$performerProID} AND
+      cm.default_na_stamp_id IS NULL
+ORDER BY cm.created_at DESC
+            ")
+        );
+
+        $keyNumbers['rmBillsToSign'] = Bill
+            ::where('is_cancelled', false)
+            ->where('cm_or_rm', 'RM')
+            ->where(function ($q) use ($performerProID) {
+                $q
+                    ->where(function ($q2) use ($performerProID) {
+                        $q2->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rme_pro_id', $performerProID)->where('is_signed_by_rme', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rmm_pro_id', $performerProID)->where('is_signed_by_rmm', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('generic_pro_id', $performerProID)->where('is_signed_by_generic_pro', false);
+                    });
+            })
+            ->count();
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.number_of_days_with_remote_measurements < 16 OR care_month.number_of_days_with_remote_measurements IS NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithLT16MD'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.number_of_days_with_remote_measurements >= 16 AND care_month.number_of_days_with_remote_measurements IS NOT NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithGTE16MD'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.has_anyone_interacted_with_client_about_rm_outside_note = TRUE AND care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NOT NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithWhomCommDone'] = $count[0]->cnt;
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(client.id) as cnt FROM client join care_month on care_month.client_id = client.id
+WHERE ((client.mcp_pro_id = {$performer->pro->id}) OR (client.rmm_pro_id = {$performer->pro->id})
+          OR (client.rme_pro_id = {$performer->pro->id}) OR (client.default_na_pro_id = {$performer->pro->id}))
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from now())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from now())
+  AND (care_month.has_anyone_interacted_with_client_about_rm_outside_note = FALSE OR care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NULL)
+"
+            )
+        );
+        $keyNumbers['rmPatientsWithWhomCommNotDone'] = $count[0]->cnt;
+
+        // num measurements that need stamping
+        $keyNumbers['measurementsToBeStamped'] = $this->performer()->pro->getUnstampedMeasurementsFromCurrentMonth(true, null, null);
+
+        if($performer->pro->pro_type === 'ADMIN') {
+
+            // patients without coverage information
+            $keyNumbers['patientsWithoutCoverageInformation'] = DB::select(DB::raw("
+SELECT count(DISTINCT (cl.id)) as cnt
+FROM client cl
+WHERE cl.shadow_pro_id IS NULL AND cl.latest_client_primary_coverage_id IS NULL -- no coverage record"
+            ))[0]->cnt;
+
+            // patients pending coverage verification
+            $keyNumbers['patientsPendingCoverageVerification'] = DB::select(DB::raw("
+SELECT count(DISTINCT (cl.id)) as cnt
+FROM client cl
+         LEFT JOIN client_primary_coverage cpc ON cl.latest_client_primary_coverage_id = cpc.id
+WHERE cl.shadow_pro_id IS NULL
+    AND (cl.latest_client_primary_coverage_id IS NOT NULL -- coverage exists, but status is null or unknown
+    AND (
+               (cpc.plan_type = 'MEDICARE' AND (cpc.is_partbprimary = 'UNKNOWN' OR cpc.is_partbprimary IS NULL))
+               OR
+               (cpc.plan_type != 'MEDICARE' AND
+                (cpc.manual_determination_category = 'UNKNOWN' OR cpc.manual_determination_category IS NULL))
+           ))"
+            ))[0]->cnt;
+
+        }
+
+        return view('app/dashboard-admin', compact('keyNumbers', 'reimbursement', 'milliseconds',
+            'businessNumbers',
+            'incomingReports', 'tickets', 'supplyOrders',
+            'numERx', 'numLabs', 'numImaging', 'numSupplyOrders',
+            'newMCPAssociations', 'newNAAssociations',
+            'mcpClientMemos', 'naClientMemos',
+            'proApptUpdates', 'naApptUpdates'));
+    }
+
+    public function dashboard(Request $request)
+    {
+        $performer = $this->performer();
+        $pro = $performer->pro;
+       
+        if($pro->pro_type === 'ADMIN'){
+            return $this->dashboard_ADMIN($request);
+        }elseif($pro->is_enrolled_as_mcp && $pro->is_considered_for_mcp_assignment) {
+            return $this->dashboard_MCP($request);
+        }elseif($pro->is_hcp){
+            return $this->dashboard_HCP($request); //TODO for HCP
+        }else{
+            return $this->dashboard_DNA($request);
+        }
+    }
+
+
+    public function dashboardMeasurementsTab(Request $request, $page = 1) {
+
+        $performer = $this->performer();
+
+        $myClientIDs = [];
+        if ($performer->pro->pro_type != 'ADMIN') {
+            $myClientIDs = $this->getMyClientIds();
+            $myClientIDs = implode(", ", $myClientIDs);
+        }
+
+        $ifNotAdmin = " AND (
+            client.mcp_pro_id = {$performer->pro->id}
+            OR client.rmm_pro_id = {$performer->pro->id}
+            OR client.rme_pro_id = {$performer->pro->id}
+            OR client.physician_pro_id = {$performer->pro->id}
+            OR client.id in (SELECT client_id FROM client_pro_access WHERE is_active AND pro_id = {$performer->pro->id})
+            OR client.id in (SELECT client_id FROM appointment WHERE status NOT IN ('CANCELLED') AND pro_id = {$performer->pro->id})
+        )";
+
+        $numMeasurements = DB::select(
+            DB::raw(
+                "
+SELECT count(measurement.id) as cnt
+FROM measurement
+         join client on measurement.client_id = client.id
+WHERE measurement.label NOT IN ('SBP', 'DBP')
+  AND (measurement.is_cellular_zero = FALSE or measurement.is_cellular_zero IS NULL)
+  AND measurement.is_active IS TRUE
+  AND measurement.has_been_stamped_by_mcp IS FALSE
+  AND measurement.ts IS NOT NULL
+  AND measurement.client_bdt_measurement_id IS NOT NULL
+  AND (measurement.status IS NULL OR (measurement.status <> 'ACK' AND measurement.status <> 'INVALID_ACK'))
+  AND EXTRACT(MONTH from measurement.created_at) = EXTRACT(MONTH from NOW())
+  AND EXTRACT(YEAR from measurement.created_at) = EXTRACT(YEAR from NOW())
+" .
+                (
+                $performer->pro->pro_type != 'ADMIN' ? $ifNotAdmin : ''
+  )
+            )
+        );
+
+        $numMeasurements = $numMeasurements[0]->cnt;
+
+        $measurements = DB::select(
+            DB::raw(
+                "
+SELECT measurement.uid as uid,
+       care_month.uid as care_month_uid,
+       care_month.start_date as care_month_start_date,
+       measurement.label,
+       measurement.value,
+       measurement.sbp_mm_hg,
+       measurement.dbp_mm_hg,
+       measurement.numeric_value,
+       measurement.value_pulse,
+       measurement.value_irregular,
+       measurement.ts,
+       client.id as client_id,
+       client.mcp_pro_id,
+       client.default_na_pro_id,
+       client.rmm_pro_id,
+       client.rme_pro_id,
+       client.uid as client_uid,
+       client.name_last,
+       client.name_first,
+       care_month.rm_total_time_in_seconds
+FROM measurement
+         join client on measurement.client_id = client.id
+         join care_month on client.id = care_month.client_id
+WHERE measurement.label NOT IN ('SBP', 'DBP')
+  AND (measurement.is_cellular_zero = FALSE or measurement.is_cellular_zero IS NULL)
+  AND measurement.is_active IS TRUE
+  AND measurement.has_been_stamped_by_mcp IS FALSE
+  AND measurement.ts IS NOT NULL
+  AND measurement.client_bdt_measurement_id IS NOT NULL
+  AND (measurement.status IS NULL OR (measurement.status <> 'ACK' AND measurement.status <> 'INVALID_ACK'))
+  AND EXTRACT(MONTH from measurement.created_at) = EXTRACT(MONTH from NOW())
+  AND EXTRACT(YEAR from measurement.created_at) = EXTRACT(YEAR from NOW())
+  AND EXTRACT(MONTH from care_month.start_date) = EXTRACT(MONTH from NOW())
+  AND EXTRACT(YEAR from care_month.start_date) = EXTRACT(YEAR from NOW())
+" .
+                (
+                $performer->pro->pro_type != 'ADMIN' ? $ifNotAdmin : ''
+  )
+            ) .
+            " ORDER BY measurement.ts DESC LIMIT 20 OFFSET " . (($page - 1) * 20)
+
+        );
+
+        return view('app.dashboard.measurements', compact('numMeasurements', 'measurements', 'page'));
+    }
+
+    public function dashboardAppointmentDates(Request $request, $from, $to) {
+        $performer = $this->performer();
+        $performerProID = $performer->pro->id;
+        $isAdmin = ($performer->pro->pro_type === 'ADMIN');
+
+        $results = DB::table('appointment')->select('raw_date')->distinct()->where("start_time", '>=', $from)->where("start_time", '<=', $to.' 23:59:00+00');
+
+        $results = $results->leftJoin('client AS c', 'c.id', '=', 'appointment.id');
+
+        if(!$isAdmin) {
+            $results = $results->where(function($query) use ($performerProID){
+                return $query->where('appointment.pro_id', $performerProID)
+                        ->orWhere('c.default_na_pro_id', $performerProID);
+            });
+        }
+
+        $results = $results->get();
+
+        $dates = [];
+        foreach ($results as $result) {
+            // $dates[] = strtotime($result->raw_date) . '000';
+            $dates[] = $result->raw_date;
+        }
+
+//        foreach ($results as $result) {
+//            $results->dateYMD = date('Y-m-d', strtotime($result->raw_date));
+//        }
+
+        return json_encode($dates);
+    }
+
+    public function dashboardAppointments(Request $request, $from, $to) {
+
+        $performer = $this->performer();
+        $performerProID = $performer->pro->id;
+        $isAdmin = ($performer->pro->pro_type === 'ADMIN');
+
+        // $appointments = Appointment::where("start_time", '>=', $from)->where("start_time", '<=', $to.' 23:59:00+00');
+        $appointments = Appointment::where("raw_date", '=', $from);
+
+        if(!$isAdmin) {
+            $appointments = $appointments->where(function($q) use ($performerProID) {
+                return $q->where("pro_id", $performerProID)
+                    ->orWhereHas('client', function($clientQuery) use ($performerProID){
+                        return $clientQuery->where('default_na_pro_id', $performerProID);
+                    });
+            });
+        }
+
+        $appointments = $appointments
+
+	    // ->whereRaw('status NOT IN (\'CANCELLED\', \'COMPLETED\')')
+            ->orderBy('start_time', 'asc')
+            ->get();
+
+        foreach ($appointments as $appointment) {
+            $date = explode(" ", $appointment->start_time)[0];
+            $appointment->milliseconds = strtotime($date) . '000';
+            $appointment->newStatus = $appointment->status;
+
+            $appointment->dateYMD = date('Y-m-d', strtotime($appointment->raw_date));
+            $appointment->clientName = $appointment->client->displayName();
+            $appointment->clientInitials = substr($appointment->client->name_first, 0, 1) . substr($appointment->client->name_last, 0, 1);
+            $appointment->isClientShadowOfPro = $appointment->client->shadow_pro_id ? true : false;
+            $appointment->proInitials = substr($appointment->pro->name_first, 0, 1) . substr($appointment->pro->name_last, 0, 1);
+            $appointment->friendlyStartTime = friendly_time($appointment->raw_start_time);
+            $appointment->friendlyEndTime = friendly_time($appointment->raw_end_time);
+            $appointment->clientSummary = friendly_date_time($appointment->client->dob, false) . ' (' .
+                $appointment->client->age_in_years . ' y.o' .
+                ($appointment->client->sex ? ' ' . $appointment->client->sex : '') .
+                ')';
+            $appointment->clientAge = $appointment->client->age_in_years;
+            $appointment->clientSex = $appointment->client->sex;
+
+            $appointment->started = false;
+            $appointment->inHowManyHours = date_diff(date_create('now'), date_create($appointment->start_time), false)
+                ->format('%R%h h, %i m');
+            if ($appointment->inHowManyHours[0] === '-') {
+                $appointment->inHowManyHours = substr($appointment->inHowManyHours, 1) . ' ago';
+                $appointment->started = true;
+            } else {
+                $appointment->inHowManyHours = 'Appt. in ' . substr($appointment->inHowManyHours, 1);
+            }
+            $appointment->clientUid = $appointment->client->uid;
+            $appointment->proUid = $appointment->pro->uid;
+            $appointment->proName = $appointment->pro->displayName();
+
+            // insurance information
+            $appointment->coverage = $appointment->client->getPrimaryCoverageStatus();
+
+            unset($appointment->client);
+            unset($appointment->pro);
+            unset($appointment->detail_json);
+        }
+
+        return json_encode($appointments);
+    }
+
+    public function dashboardAppointmentsDisplay(Request $request, $from, $to) {
+        $performer = $this->performer();
+        $performerProID = $performer->pro->id;
+        $isAdmin = ($performer->pro->pro_type === 'ADMIN');
+
+        // $appointments = Appointment::where("start_time", '>=', $from)->where("start_time", '<=', $to.' 23:59:00+00');
+        $appointments = Appointment::where("raw_date", '=', $from);
+
+        if(!$isAdmin) {
+            $appointments = $appointments->where(function($q) use ($performerProID) {
+                return $q->where("pro_id", $performerProID)
+                    ->orWhereHas('client', function($clientQuery) use ($performerProID){
+                        return $clientQuery->where('default_na_pro_id', $performerProID);
+                    });
+            });
+        }
+
+        $appointments = $appointments
+            ->orderBy('start_time', 'asc')
+            ->orderBy('end_time', 'asc')
+            ->get();
+
+        foreach ($appointments as $appointment) {
+            $date = explode(" ", $appointment->start_time)[0];
+            $appointment->milliseconds = strtotime($date) . '000';
+            $appointment->newStatus = $appointment->status;
+
+            $appointment->dateYMD = date('Y-m-d', strtotime($appointment->raw_date));
+            $appointment->clientName = $appointment->client->displayName();
+            $appointment->clientInitials = substr($appointment->client->name_first, 0, 1) . substr($appointment->client->name_last, 0, 1);
+            $appointment->isClientShadowOfPro = $appointment->client->shadow_pro_id ? true : false;
+            $appointment->proInitials = substr($appointment->pro->name_first, 0, 1) . substr($appointment->pro->name_last, 0, 1);
+            $appointment->friendlyStartTime = friendly_time($appointment->raw_start_time);
+            $appointment->friendlyEndTime = friendly_time($appointment->raw_end_time);
+            $appointment->clientSummary = friendly_date_time($appointment->client->dob, false) . ' (' .
+                $appointment->client->age_in_years . ' y.o' .
+                ($appointment->client->sex ? ' ' . $appointment->client->sex : '') .
+                ')';
+            $appointment->clientAge = $appointment->client->age_in_years;
+            $appointment->clientSex = $appointment->client->sex;
+
+            $appointment->started = false;
+            $appointment->inHowManyHours = date_diff(date_create('now'), date_create($appointment->start_time), false)
+                ->format('%R%h h, %i m');
+            if ($appointment->inHowManyHours[0] === '-') {
+                $appointment->inHowManyHours = substr($appointment->inHowManyHours, 1) . ' ago';
+                $appointment->started = true;
+            } else {
+                $appointment->inHowManyHours = 'Appt. in ' . substr($appointment->inHowManyHours, 1);
+            }
+            $appointment->clientUid = $appointment->client->uid;
+            $appointment->proUid = $appointment->pro->uid;
+            $appointment->proName = $appointment->pro->displayName();
+
+            // insurance information
+            $appointment->coverage = $appointment->client->getPrimaryCoverageStatus();
+
+            // bg color
+            $appointment->bgColor = 'bg-white';
+            if($appointment->status === 'COMPLETED') {
+                $appointment->bgColor = 'event-bg-green';
+            }
+            else if($appointment->status === 'CANCELLED') {
+                $appointment->bgColor = 'event-bg-gray';
+            }
+
+            unset($appointment->client);
+            unset($appointment->pro);
+            unset($appointment->detail_json);
+        }
+        return view('app.mcp.dashboard.appointments-list', compact('appointments', 'from', 'to'));
+    }
+
+    public function dashboardMeasurements(Request $request, $filter) {
+        $measurements = $this->performer()->pro->getMeasurements($filter === 'NEED_ACK');
+        return json_encode($measurements);
+    }
+
+    public function patients(Request $request, $filter = '')
+    {
+
+        $performer = $this->performer();
+        $query = $performer->pro->getAccessibleClientsQuery();
+
+        $q = trim($request->input('q'));
+        if(!empty($q)) {
+            $query = $query->where(function ($query) use ($q) {
+                $query->where('name_first', 'ILIKE', "%$q%")
+                    ->orWhere('name_last', 'ILIKE', "%$q%")
+                    ->orWhere('email_address', 'ILIKE', "%$q%")
+                    ->orWhere('tags', 'ILIKE', "%$q%");
+            });
+        }
+
+        switch ($filter) {
+            case 'not-yet-seen':
+                $query = $query
+                    ->where(function ($query) use ($performer) {
+                        $query
+                            ->where(function ($query) use ($performer) {     // own patient and primary OB visit pending
+                                $query->where('mcp_pro_id', $performer->pro->id)
+                                    ->where('has_mcp_done_onboarding_visit', '<>', 'YES');
+                            })
+                            ->orWhere(function ($query) use ($performer) {   // mcp of any client program and program OB pending
+                                $query->select(DB::raw('COUNT(id)'))
+                                    ->from('client_program')
+                                    ->whereColumn('client_id', 'client.id')
+                                    ->where('mcp_pro_id', $performer->pro->id)
+                                    ->where('has_mcp_done_onboarding_visit', '<>', 'YES');
+                            }, '>=', 1);
+                    });
+                break;
+
+            case 'having-birthday-today':
+                $query = $query
+                    ->whereRaw('EXTRACT(DAY from dob) = ?', [date('d')])
+                    ->whereRaw('EXTRACT(MONTH from dob) = ?', [date('m')]);
+                break;
+
+                // more cases can be added as needed
+            default:
+                break;
+        }
+        $patients = $query->orderBy('created_at', 'desc')->paginate(50);
+
+        // patient acquisition chart (admin only)
+        $patientAcquisitionData = null;
+        if($performer->pro->pro_type === 'ADMIN') {
+            $startDate = date_sub(date_create(), date_interval_create_from_date_string("1 month"));
+            $startDate = date_format($startDate, "Y-m-d");
+            $patientAcquisitionData = DB::select(DB::raw(
+                "SELECT count(id) as count, DATE(created_at at time zone 'utc' at time zone 'est') as date " .
+                "FROM client " .
+                "WHERE shadow_pro_id IS NULL " .
+                "GROUP BY DATE(created_at at time zone 'utc' at time zone 'est') " .
+                "ORDER BY DATE(created_at at time zone 'utc' at time zone 'est') DESC " .
+                "LIMIT 30"));
+        }
+
+        return view('app/patients', compact('patients', 'filter', 'patientAcquisitionData'));
+
+    }
+
+    public function patientsSuggest(Request $request)
+    {
+
+        $pro = $this->pro;
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        $originalTerm = $term;
+        if (empty($term)) return '';
+
+        // if multiple words in query, check for all (max 2)
+        $term2 = '';
+        if(strpos($term, ' ') !== FALSE) {
+            $terms = explode(' ', $term);
+            $term = trim($terms[0]);
+            $term2 = trim($terms[1]);
+        }
+
+        $phoneNumberTerm = preg_replace("/[^0-9]/", "", $originalTerm );
+        if($phoneNumberTerm == ""){  //to avoid search with blank string
+            $phoneNumberTerm = $term;
+        }
+
+        $clientQuery= Client::whereNull('shadow_pro_id')
+            ->where(function ($q) use ($term, $phoneNumberTerm) {
+                $q->where('name_first', 'ILIKE', '%' . $term . '%')
+                    ->orWhere('name_last', 'ILIKE', '%' . $term . '%')
+                    ->orWhere('cell_number', 'ILIKE', '%' . $phoneNumberTerm . '%')
+                    ->orWhere('phone_home', 'ILIKE', '%' . $phoneNumberTerm . '%');
+            });
+
+        if(!empty($term2)) {
+            $clientQuery = $clientQuery->where(function ($q) use ($term2, $phoneNumberTerm) {
+                $q->where('name_first', 'ILIKE', '%' . $term2 . '%')
+                    ->orWhere('name_last', 'ILIKE', '%' . $term2 . '%')
+                    ->orWhere('cell_number', 'ILIKE', '%' . $phoneNumberTerm . '%')
+                    ->orWhere('phone_home', 'ILIKE', '%' . $phoneNumberTerm . '%');
+            });
+        }
+
+        if(!($pro->pro_type === 'ADMIN' && $pro->can_see_any_client_via_search)) {
+            $clientQuery->where(function ($q) use ($pro) {
+                if($pro->pro_type === 'ADMIN' || $pro->is_enrolled_as_mcp) {
+                    $q->whereIn('id', $pro->getMyClientIds(true))->orWhereNull('mcp_pro_id');
+                }
+                else {
+                    $q->whereIn('id', $pro->getMyClientIds(true));
+                }
+            });
+        }
+
+        $clients = $clientQuery->get();
+        return view('app/patient-suggest', compact('clients'));
+    }
+
+    public function pharmacySuggest(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $term = strtolower($term);
+        $pharmacies = Facility::where('facility_type', 'Pharmacy')
+            ->where(function ($q) use ($term) {
+                $q->orWhereRaw('LOWER(name::text) LIKE ?', ['%' . $term . '%'])
+                    ->orWhereRaw('LOWER(address_line1::text) LIKE ?', ['%' . $term . '%'])
+                    ->orWhereRaw('LOWER(address_line2::text) LIKE ?', ['%' . $term . '%'])
+                    ->orWhereRaw('LOWER(address_city::text) LIKE ?', ['%' . $term . '%'])
+                    ->orWhereRaw('LOWER(address_state::text) LIKE ?', ['%' . $term . '%'])
+                    ->orWhereRaw('LOWER(phone::text) LIKE ?', ['%' . $term . '%'])
+                    ->orWhereRaw('LOWER(address_zip::text) LIKE ?', ['%' . $term . '%']);
+            });
+        if($request->input('city')) {
+            $pharmacies = $pharmacies->whereRaw('LOWER(address_city::text) LIKE ?', ['%' . strtolower($request->input('city')) . '%']);
+        }
+        if($request->input('state')) {
+            $pharmacies = $pharmacies->whereRaw('LOWER(address_state::text) LIKE ?', ['%' . strtolower($request->input('state')) . '%']);
+        }
+        if($request->input('zip')) {
+            $pharmacies = $pharmacies->whereRaw('LOWER(address_zip::text) LIKE ?', ['%' . strtolower($request->input('zip')) . '%']);
+        }
+        $pharmacies = $pharmacies
+            ->orderBy('name', 'asc')
+            ->orderBy('address_line1', 'asc')
+            ->orderBy('address_city', 'asc')
+            ->orderBy('address_state', 'asc')
+            ->get();
+        return view('app/pharmacy-suggest', compact('pharmacies'));
+    }
+
+    public function facilitySuggestJSON(Request $request)
+    {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+
+        $terms = explode(' ', $term);
+        $terms = array_filter($terms, function($_x) {
+            return !!trim($_x);
+        });
+
+        $whereCondition = [];
+        $whereParams = [];
+        for ($i = 0; $i < count($terms); $i++) {
+            $whereCondition[] = "(name ILIKE :term{$i} OR address_city ILIKE :term{$i} OR address_state ILIKE :term{$i} OR address_zip ILIKE :term{$i})";
+            $whereParams["term{$i}"] = '%' . $terms[$i] . '%';
+        }
+        $whereCondition = implode(" AND ", $whereCondition);
+
+        $matches = DB::select(
+            "SELECT (name || ' ' || address_city || ' ' || address_state || ' ' || address_zip) as text,
+       address_line1 as text2,
+       name, address_city as city, address_state as state, address_zip as zip, phone, fax FROM facility
+       WHERE {$whereCondition} ORDER BY name", $whereParams);
+        return json_encode([
+            "success" => true,
+            "data" => $matches
+        ]);
+    }
+
+    public function proSuggest(Request $request) {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $term = strtolower($term);
+
+        // if multiple words in query, check for all (max 2)
+        $term2 = '';
+        if(strpos($term, ' ') !== FALSE) {
+            $terms = explode(' ', $term);
+            $term = trim($terms[0]);
+            $term2 = trim($terms[1]);
+        }
+
+        $pros = Pro::where(function ($q) use ($term) {
+            $q->orWhereRaw('LOWER(name_first::text) LIKE ?', ['%' . $term . '%'])
+                ->orWhereRaw('LOWER(name_last::text) LIKE ?', ['%' . $term . '%'])
+                ->orWhereRaw('cell_number LIKE ?', ['%' . $term . '%']);
+        });
+
+        if(!empty($term2)) {
+            $pros = $pros->where(function ($q) use ($term2) {
+                $q->orWhereRaw('LOWER(name_first::text) LIKE ?', ['%' . $term2 . '%'])
+                    ->orWhereRaw('LOWER(name_last::text) LIKE ?', ['%' . $term2 . '%'])
+                    ->orWhereRaw('cell_number LIKE ?', ['%' . $term2 . '%']);
+            });
+        }
+
+        $type = $request->input('type') ? trim($request->input('type')) : '';
+        if(!!$type) {
+            switch(strtolower($type)) {
+                case 'hcp':
+                    $pros->where('is_hcp', true);
+                    break;
+                case 'default-na': // TODO: fix condition for NA
+                    $pros->where('is_hcp', false)->where('pro_type', '!=', 'ADMIN');
+                    break;
+                case 'admin':
+                    $pros->where('pro_type', 'ADMIN');
+                    break;
+                case 'non-admin':
+                    $pros->where('pro_type', '!=', 'ADMIN');
+                    break;
+            }
+        }
+
+        if($this->performer->pro && $this->performer->pro->pro_type != 'ADMIN'){
+            $accessiblePros = ProProAccess::where('owner_pro_id', $this->performer->pro->id);
+            $accessibleProIds = [];
+            foreach($accessiblePros as $accessiblePro){
+                $accessibleProIds[] = $accessiblePro->id;
+            }
+            $accessibleProIds[] = $this->performer->pro->id;
+
+            // for dna, add pros accessible via pro teams
+            if($this->performer->pro->isDefaultNA()) {
+                $teams = $this->performer->pro->teamsWhereAssistant;
+                foreach ($teams as $team) {
+                    if(!in_array($team->mcp_pro_id, $accessibleProIds)) {
+                        $accessibleProIds[] = $team->mcp_pro_id;
+                    }
+                }
+            }
+
+            $pros->whereIn('id', $accessibleProIds);
+        }
+        $suggestedPros = $pros->orderBy('name_last')->orderBy('name_first')->get();
+
+        // for calendar select2
+        if($request->input('json')) {
+            $jsonPros = $suggestedPros->map(function($_pro) {
+                return [
+                    "uid" => $_pro->uid,
+                    "id" => $_pro->id,
+                    "text" => $_pro->displayName(),
+                    "initials" => $_pro->initials(),
+                ];
+            });
+            return json_encode([
+                "results" => $jsonPros
+            ]);
+        }
+
+        return view('app/pro-suggest', compact('suggestedPros'));
+    }
+
+    public function canAccessPatient(Request $request, $uid) {
+        return json_encode([
+            "success" => true,
+            "data" => $this->performer->pro->canAccess($uid)
+        ]);
+    }
+
+    public function proDisplayName(Request $request, Pro $pro) {
+        return $pro ? $pro->displayName() : '';
+    }
+
+    public function unmappedSMS(Request $request, $filter = '')
+    {
+        $proID = $this->performer()->pro->id;
+        if ($this->performer()->pro->pro_type === 'ADMIN') {
+            $query = Client::where('id', '>', 0);
+        } else {
+            $query = Client::where(function ($q) use ($proID) {
+                $q->where('mcp_pro_id', $proID)
+                    ->orWhere('cm_pro_id', $proID)
+                    ->orWhere('rmm_pro_id', $proID)
+                    ->orWhere('rme_pro_id', $proID)
+                    ->orWhereRaw('id IN (SELECT client_id FROM client_pro_access WHERE is_active AND pro_id = ?)', [$proID]);
+            });
+        }
+        $patients = $query->orderBy('name_last', 'asc')->orderBy('name_first', 'asc')->get();
+        $unmappedSMS = ClientSMS::where('client_id', null)->where('incoming_or_outgoing', 'INCOMING')->paginate(20);
+        return view('app/unmapped-sms', compact('unmappedSMS', 'patients'));
+    }
+
+    public function newPatient(Request $request)
+    {
+        $mbPayers = MBPayer::all();
+        return view('app/new-patient', compact('mbPayers'));
+    }
+
+    public function newNonMcnPatient(Request $request)
+    {
+        $mbPayers = MBPayer::all();
+        return view('app/new-non-mcn-patient', compact('mbPayers'));
+    }
+
+    public function mc(Request $request, $fragment = "")
+    {
+        $page = "/";
+        if ($fragment) {
+            $page = '/' . $fragment;
+        }
+        return view('app/mc', compact('page'));
+    }
+
+    public function blank(Request $request)
+    {
+        return view('app/blank');
+    }
+
+    public function noteTemplateSet(Request $request, $section, $template)
+    {
+        return view('app/patient/note/_template', [
+            "sectionInternalName" => $section,
+            "templateName" => $template
+        ]);
+    }
+
+    public function noteExamTemplateSet(Request $request, $exam, $template)
+    {
+        return view('app/patient/note/_template-exam', [
+            "exam" => $exam,
+            "sectionInternalName" => 'exam-' . $exam . '-detail',
+            "templateName" => $template
+        ]);
+    }
+
+    public function logInAs(Request $request)
+    {
+        if($this->pro->pro_type != 'ADMIN'){
+            return redirect()->to(route('dashboard'));
+        }
+
+        // dummy condition to get the chain-ability going
+        $pros =  Pro::where('id', '>', 0);
+
+        if($request->input('q')) {
+            $nameQuery = '%' . $request->input('q') . '%';
+            $pros = $pros->where(function ($query) use ($nameQuery) {
+                $query->where('name_first', 'ILIKE', $nameQuery)
+                    ->orWhere('name_last', 'ILIKE', $nameQuery)
+                    ->orWhere('email_address', 'ILIKE', $nameQuery)
+                    ->orWhere('cell_number', 'ILIKE', $nameQuery);
+            });
+        }
+
+        if($request->input('sort') && $request->input('dir')) {
+            $pros = $pros->orderBy($request->input('sort'), $request->input('dir'));
+        }
+        else {
+            $pros = $pros->orderBy('name_last', 'asc');
+        }
+
+        $pros = $pros->paginate(20);
+
+        return view('app/log-in-as', ['logInAsPros' => $pros]);
+    }
+
+    public function processLogInAs(Request $request)
+    {
+
+        $api = new Backend();
+
+        try {
+            $apiResponse = $api->post('session/proLogInAs', [
+                'proUid' => $request->post('proUid')
+            ],
+            [
+                'sessionKey'=>$this->performer()->session_key
+            ]);
+
+            $data = json_decode($apiResponse->getContents());
+
+            if (!property_exists($data, 'success') || !$data->success) {
+                return redirect()->to(route('log-in-as'))->with('message', $data->message)
+                    ->withInput($request->input());
+            }
+
+            Cookie::queue('sessionKey', $data->data->sessionKey);
+
+            return redirect('/mc');
+        } catch (\Exception $e) {
+            return redirect()->to(route('log-in-as'))
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+
+    public function backToAdminPro(Request $request){
+        $adminPerformerId = $this->performer->logged_in_as_pro_from_admin_pro_app_session_id;
+        $adminPerformer = AppSession::where('id', $adminPerformerId)->first();
+        $url = "/session/pro_log_in_with_session_key/".$adminPerformer->session_key;
+        $api = new Backend();
+        try {
+            $apiResponse = $api->post($url, []);
+            $data = json_decode($apiResponse->getContents());
+
+            if (!property_exists($data, 'success') || !$data->success) {
+                return redirect()->to(route('logout'));
+            }
+
+
+            Cookie::queue('sessionKey', $data->data->sessionKey);
+
+            return redirect(route('dashboard'));
+
+        } catch (\Exception $e) {
+            return redirect(route('dashboard'));
+        }
+    }
+
+    public function getTicket(Request $request, Ticket $ticket) {
+        $ticket->data = json_decode($ticket->data);
+//        $ticket->created_at = friendly_date_time($ticket->created_at);
+        $ticket->assignedPro;
+        $ticket->managerPro;
+        $ticket->orderingPro;
+        $ticket->initiatingPro;
+        return json_encode($ticket);
+    }
+
+    public function genericBill(Request $request, $entityType, $entityUid) {
+        $patient = null;
+        if ($entityType && $entityUid) {
+            try {
+                $entityClass = "\\App\\Models\\" . $entityType;
+                $entity = $entityClass::where('uid', $entityUid)->first();
+                if ($entity->client) {
+                    $patient = $entity->client;
+                }
+            } catch (\Exception $e) {
+            }
+        }
+        return view('app.generic-bills.inline', ['class' => 'p-3 border-top mt-3', 'entityType' => $entityType, 'entityUid' => $entityUid, 'patient' => $patient]);
+    }
+
+    public function outgoingEmailTemplates(Request $request){
+        $templates = OutgoingEmailTemplate::where('is_active', true)->orderBy('default_subject_tpl', 'ASC')->get();
+        return $this->pass($templates);
+    }
+}

+ 208 - 0
app/Http/Controllers/LoginController.php

@@ -0,0 +1,208 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Middleware\RedirectAuthenticatedPro;
+use App\Lib\Backend;
+use App\Models\Pro;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cookie;
+use Exception;
+use Illuminate\Support\Facades\Http;
+
+class LoginController extends Controller
+{
+    public function __construct()
+    {
+        $this->middleware('pro.auth.redirect')->except('logout');
+    }
+
+
+    public function showLoginForm(Request $request)
+    {
+        return view('app/login');
+    }
+
+    public function showRequestPasswordReset(Request $request)
+    {
+        return view('app/request_password_reset');
+    }
+
+    public function processRequestPasswordReset(Request $request)
+    {
+        try {
+
+            $url =  config('stag.backendUrl') . '/pro/requestPasswordReset';
+
+            $data = [
+                'cellNumber' => $request->input('cellNumber'),
+            ];
+
+            $response = Http::asForm()
+                ->post($url, $data)
+                ->json();
+
+            if(!isset($response['success']) || !$response['success']){
+                $message = 'API error';
+                if(isset($response['error'])) {
+                    $message = $response['error'];
+                    if(isset($response['path'])) $message .= ': ' . $response['path'];
+                }
+                else if(isset($response['message'])) $message = $response['message'];
+                return redirect('/request_password_reset')
+                    ->withInput()
+                    ->with('message', $message);
+            }
+
+            // load pro and set security questions in the session
+            $guestPro = Pro::where('cell_number', $request->input('cellNumber'))->first();
+            $request->session()->put('sq1', $guestPro->security_question_1);
+            $request->session()->put('sq2', $guestPro->security_question_2);
+
+            return redirect('/self_reset_password');
+
+        } catch (\Exception $e) {
+            return redirect()->back()
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+
+    public function showSelfResetPassword(Request $request)
+    {
+        return view('app/self_reset_password');
+    }
+
+    public function processSelfResetPassword(Request $request)
+    {
+        try {
+
+            $url =  config('stag.backendUrl') . '/pro/selfResetPassword';
+
+            $data = [
+                'cellNumber' => $request->input('cellNumber'),
+                'passwordResetToken' => $request->input('passwordResetToken'),
+                'securityQuestionAnswer1' => $request->input('securityQuestionAnswer1'),
+                'securityQuestionAnswer2' => $request->input('securityQuestionAnswer2'),
+                'password' => $request->input('password'),
+                'passwordConfirmation' => $request->input('passwordConfirmation'),
+            ];
+
+            $response = Http::asForm()
+                ->post($url, $data)
+                ->json();
+
+            if(!isset($response['success']) || !$response['success']){
+                $message = 'API error';
+                if(isset($response['error'])) {
+                    $message = $response['error'];
+                    if(isset($response['path'])) $message .= ': ' . $response['path'];
+                }
+                else if(isset($response['message'])) $message = $response['message'];
+                return redirect('/self_reset_password')
+                    ->withInput()
+                    ->with('message', $message);
+            }
+
+            $request->session()->remove('sq1');
+            $request->session()->remove('sq2');
+
+            return redirect('/login');
+
+        } catch (\Exception $e) {
+            return redirect()->back()
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+
+
+    public function login(Request $request)
+    {
+        $this->validate($request, [
+            'cell-number' => 'required',
+            'password' => 'required'
+        ]);
+
+        $api = new Backend();
+
+        try {
+            $apiResponse = $api->post('session/proLogInWithPassword', [
+                'cellNumber' => $request->post('cell-number'),
+                'password' => $request->post('password')
+            ]);
+            $data = json_decode($apiResponse->getContents());
+
+            if (!property_exists($data, 'success') || !$data->success) {
+                return back()->with('message', 'Invalid login credentials.')
+                    ->withInput($request->input());
+            }
+
+            Cookie::queue('sessionKey', $data->data->sessionKey);
+
+            return redirect('/mc');
+
+        } catch (\Exception $e) {
+            return redirect()->back()
+                ->with('message', 'Unable to process your request at the moment. Please try again later.')
+                ->withInput($request->input());
+        }
+    }
+
+
+    public function logout(Request $request)
+    {
+        $api = new Backend();
+
+        try {
+            //$apiResponse = $api->get('session/logOut?sessionKey=' . $request->cookie('sessionKey'));
+            $apiResponse = $api->sendRequest(
+                'session/logOut',
+                'GET',
+                [
+                    'headers'  => [
+                        'sessionKey' => $request->cookie('sessionKey')
+                    ]
+                ]
+            );
+
+            $data = json_decode($apiResponse->getContents());
+
+            if (!property_exists($data, 'success') || !$data->success) {
+                //TODO: throw message to log
+                throw new Exception('Failed to log out of backend');
+            }
+        } catch (Exception $e) {
+            // TODO: Log message
+            // TODO: Never fail on logout. Just delete cookie.
+        } finally {
+            Cookie::queue(Cookie::forget('sessionKey'));
+        }
+
+
+        return redirect()->to(config('stag.authUrl').'/logout');
+    }
+
+    public function loginWithSessionKey($sessionKey, $appAccessUID=null, Request $request){
+        $url = "/session/pro_log_in_with_session_key/${sessionKey}";
+        if(!!$appAccessUID) {
+            $url .= "/$appAccessUID";
+        }
+        $api = new Backend();
+        try {
+            $apiResponse = $api->post($url, []);
+            $data = json_decode($apiResponse->getContents());
+
+            if (!property_exists($data, 'success') || !$data->success) {
+                return redirect('/mc');
+            }
+            
+            Cookie::queue('sessionKey', $data->data->sessionKey);
+
+            return redirect('/mc');
+
+        } catch (\Exception $e) {
+            return redirect('/mc');
+        }
+    }
+}

+ 421 - 0
app/Http/Controllers/McpController.php

@@ -0,0 +1,421 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Appointment;
+use App\Models\BDTDevice;
+use App\Models\CareMonth;
+use App\Models\Client;
+use App\Models\ClientBDTDevice;
+use App\Models\ClientInfoLine;
+use App\Models\Erx;
+use App\Models\Facility;
+use App\Models\Handout;
+use App\Models\IncomingReport;
+use App\Models\MBClaim;
+use App\Models\MBPayer;
+use App\Models\Note;
+use App\Models\NoteTemplate;
+use App\Models\Pro;
+use App\Models\Product;
+use App\Models\ProProAccess;
+use App\Models\SectionTemplate;
+use App\Models\Shipment;
+use App\Models\SupplyOrder;
+use App\Models\Ticket;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\File;
+use App\Models\Bill;
+use App\Models\ClientSMS;
+use App\Models\AccountInvite;
+use App\Models\ClientMemo;
+use Illuminate\Support\Facades\Http;
+use PDF;
+
+class McpController extends Controller
+{
+
+    public function patients(Request $request)
+    {
+        $filters = $request->all();
+        $patients = Client::whereNull('shadow_pro_id');
+        
+        //TODO: implement in admin controller 
+        if($this->performer->pro->pro_type != 'ADMIN'){
+            $patients->where('mcp_pro_id', $this->performer->pro->id);
+        }
+        
+
+        // filters
+        /*
+        array:18 [▼
+          "age_category" => "LESS_THAN"
+          "age_value_1" => "34"
+          "age_value_2" => null
+          "sex" => "M"
+          "bmi_category" => "BETWEEN"
+          "bmi_value_1" => "20"
+          "bmi_value_2" => "25"
+          "last_visit_category" => "LESS_THAN"
+          "last_visit_value_1" => "2021-10-14"
+          "last_visit_value_2" => null
+          "next_appointment_category" => "LESS_THAN"
+          "next_appointment_value_1" => "2021-10-15"
+          "status" => "ACTIVE"
+          "last_weighed_in_category" => "EXACTLY"
+          "last_weighed_in_value_1" => "2021-10-07"
+          "last_bp_category" => "BETWEEN"
+          "last_bp_value_1" => "2021-10-01"
+          "last_bp_value_2" => "2021-10-31"
+        ]
+        */
+
+        if ($request->input('name')) {
+            $name = trim($request->input('name'));
+            if ($name) {
+                $patients = $patients->where(function ($q) use ($name) {
+                    $q->where('name_first', 'ILIKE', '%' . $name . '%')
+                        ->orWhere('name_last', 'ILIKE', '%' . $name . '%');
+                });
+            }
+        }
+
+        $this->filterMultiQuery($request, $patients, 'age_in_years', 'age_category', 'age_value_1', 'age_value_2');
+        $this->filterSimpleQuery($request, $patients, 'sex', 'sex');
+        $this->filterMultiQuery($request, $patients, 'usual_bmi_max', 'bmi_category', 'bmi_value_1', 'bmi_value_2');
+        $this->filterMultiQuery($request, $patients, 'most_recent_weight_at', 'last_weighed_in_category', 'last_weighed_in_value_1', 'last_weighed_in_value_2');
+        $this->filterMultiQuery($request, $patients, 'most_recent_bp_at', 'last_bp_category', 'last_bp_value_1', 'last_bp_value_2');
+
+        switch($request->input('status')) {
+            case 'ACTIVE':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', true);
+                break;
+            case 'AWAITING_VISIT':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', false);
+                break;
+            case 'INACTIVE':
+                $patients->where('is_active', '<>', true);
+                break;
+        }
+
+        $initiative = $request->input('initiative');
+        if($initiative){
+            $wildCardedInitiative = '%'.$initiative.'%';
+            $patients->where('initiative', 'ilike', $wildCardedInitiative);
+        }
+
+        $include_test_records = $request->input('include_test_records');
+        if(!$include_test_records){
+            $patients = $patients->where(function ($q) {
+                $q->whereNull('client_engagement_status_category')
+                    ->orWhere('client_engagement_status_category', '<>', 'DUMMY');
+            });
+        }
+
+        $patients = $patients->orderBy('created_at', 'DESC')->paginate(50);
+        return view('app.mcp.patients', compact('patients', 'filters'));
+    }
+
+    public function notes(Request $request)
+    {
+        $filters = $request->all();
+        $notes = Note::query();
+        $notes = $notes->where('hcp_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $notes, 'effective_time', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $notes, 'new_or_fu_or_na', 'new_or_fu_or_na');
+        $notes = $notes->orderBy('created_at', 'DESC')->paginate(20);
+
+
+        return view('app.mcp.notes', compact('notes','filters'));
+    }
+
+    public function appointments(Request $request)
+    {
+        $filters = $request->all();
+        $appointments = Appointment::where('pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $appointments, 'raw_date', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $appointments, 'status', 'status');
+        $appointments = $appointments->orderBy('end_time', 'DESC')->paginate(20);
+        return view('app.mcp.appointments', compact('appointments', 'filters'));
+    }
+
+    public function bills(Request $request)
+    {
+        $filters = $request->all();
+        $bills = Bill::where('hcp_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $bills, 'created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $status = $request->get('status');
+        if($status){
+            if($status == 'CANCELLED') $bills = $bills->where('is_cancelled', true);
+            if($status == 'NOT_CANCELLED') $bills = $bills->where('is_cancelled', false);
+        }
+        $bills = $bills->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.mcp.bills', compact('bills', 'filters'));
+    }
+
+    public function clients_bdt_devices(Request $request){
+        $filters = $request->all();
+
+        $devices = ClientBDTDevice::select('client_bdt_device.*')
+        ->join('client', 'client.id', '=', 'client_bdt_device.client_id')
+        ->where('client.mcp_pro_id', $this->performer->pro->id);
+
+        $this->filterMultiQuery($request, $devices, 'client_bdt_device.created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $status = $request->get('status');
+        if($status){
+            if($status === 'ACTIVE') $devices = $devices->where('client_bdt_device.is_active', true);
+            if($status === 'DEACTIVATED') $devices = $devices->where('client_bdt_device.is_active', false);
+        }
+        $devices = $devices->orderBy('created_at', 'DESC')->paginate(20);
+
+        return view('app.mcp.clients_bdt_devices', compact('devices', 'filters'));
+    }
+
+    public function memos(Request $request){
+        $filters = $request->all();
+
+        $memos = ClientMemo::select('client_memo.*')
+        ->join('client', 'client.id', '=', 'client_memo.client_id')
+        ->where('client.mcp_pro_id', $this->performer->pro->id);
+
+        $this->filterMultiQuery($request, $memos, 'client_memo.created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $memos, 'category', 'category');
+        $memos = $memos->orderBy('created_at', 'DESC')->paginate(20);
+
+        return view('app.mcp.memos', compact('memos', 'filters'));
+    }
+
+    public function erx_and_orders(Request $request)
+    {
+        $filters = $request->all();
+        $erxAndOrders = Erx::query();
+        $erxAndOrders = $erxAndOrders->where('hcp_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $erxAndOrders, 'created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $erxAndOrders, 'pro_declared_status', 'status');
+
+        $erxAndOrders = $erxAndOrders->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.mcp.erx_and_orders', compact('erxAndOrders', 'filters'));
+    }
+
+    public function reports(Request $request)
+    {
+        $filters = $request->all();
+        $reports = IncomingReport::where('hcp_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $reports, 'report_date', 'date_category', 'date_value_1', 'date_value_2');
+        $status = $request->get('status');
+        if($status){
+            if($status == 'SIGNED') $reports = $reports->where('has_hcp_pro_signed', true);
+            if($status == 'NOT_SIGNED') $reports = $reports->where('has_hcp_pro_signed', false);
+        }
+
+        $reports = $reports->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.mcp.reports', compact('reports', 'filters'));
+    }
+
+    public function supply_orders(Request $request)
+    {
+        $filters = $request->all();
+        $supplyOrders = SupplyOrder::select('supply_order.*')->where('supply_order.signed_by_pro_id', $this->performer->pro->id);
+        $this->filterMultiQuery($request, $supplyOrders, 'created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $status = $request->get('status');
+        if($status){
+            if($status == 'CLEARED_FOR_SHIPMENT'){
+                $supplyOrders = $supplyOrders->where('is_cleared_for_shipment', true);
+            }elseif($status == 'NOT_CLEARED_FOR_SHIPMENT'){
+                $supplyOrders = $supplyOrders->where('is_cleared_for_shipment', false);
+            }elseif($status == 'CANCELLED'){
+                $supplyOrders = $supplyOrders->where('is_cancelled', true);
+            }else{
+             $supplyOrders = $supplyOrders->join('shipment', 'shipment.id', '=', 'supply_order.shipment_id')->where('shipment.status', $status);
+            }
+
+        }
+        $supplyOrders = $supplyOrders->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.mcp.supply_orders', compact('supplyOrders', 'filters'));
+    }
+
+    public function client_messages(Request $request)
+    {
+        $filters = $request->all();
+
+        $clientMessages = ClientSMS::select('client_sms.*')
+        ->join('client', 'client.id', '=', 'client_sms.client_id')
+        ->where('client.mcp_pro_id', $this->performer->pro->id);
+
+        $this->filterMultiQuery($request, $clientMessages, 'client_sms.created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $clientMessages, 'sms_status', 'sms_status');
+
+        $clientMessages = $clientMessages->orderBy('client_sms.created_at', 'DESC');
+        $clientMessages = $clientMessages->paginate(20);
+        return view('app.mcp.client_messages', compact('clientMessages', 'filters'));
+    }
+
+    public function patients_accounts_invites(Request $request){
+        $filters = $request->all();
+
+        $accountInvites = AccountInvite::select('account_invite.*')
+                            ->join('client', 'client.id', '=', 'account_invite.for_client_id');
+        $accountInvites = $accountInvites->where('client.mcp_pro_id', $this->performer->pro->id);
+
+        $this->filterMultiQuery($request, $accountInvites, 'account_invite.created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $accountInvites, 'account_invite.status', 'status');
+
+        $accountInvites = $accountInvites->orderBy('created_at', 'DESC')->paginate(20);
+
+        return view('app.mcp.patients-accounts-invites', compact('accountInvites', 'filters'));
+    }
+
+    public function new_patients_awaiting_visit(Request $request){
+        $data = [
+            'records' => Client::where('mcp_pro_id', $this->performer->pro->id)
+                ->where('has_mcp_done_onboarding_visit', '!=', 'YES')
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.new_patients_awaiting_visit', $data);
+    }
+    public function notes_pending_signature(Request $request){
+        $data = [
+            'records' => Note::where('hcp_pro_id', $this->performer->pro->id)
+                ->where('is_cancelled', '<>', true)
+                ->where('is_signed_by_hcp', '<>', true)
+                ->where('is_core_note', false)
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.notes_pending_signature', $data);
+    }
+
+    public function notes_pending_summary_suggestion(Request $request){
+        $pro = $this->performer->pro;
+        $data = [
+            'records' => $pro->get_notes_pending_summary_suggestion_as_mcp()
+        ];
+        return view('app.mcp.notes_pending_summary_suggestion', $data);
+    }
+
+    public function notes_rejected_summary_suggestion(Request $request){
+        $pro = $this->performer->pro;
+        $data = [
+            'records' => $pro->get_notes_rejected_summary_suggestion_as_mcp()
+        ];
+        return view('app.mcp.notes_rejected_summary_suggestion', $data);
+    }
+
+    public function notes_pending_billing(Request $request){
+        $data = [
+            'records' => Note::where('hcp_pro_id', $this->performer->pro->id)
+                ->where('is_cancelled', '<>', true)
+                ->where('is_signed_by_hcp', true)
+                ->where('is_billing_marked_done', '<>', true)
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.notes_pending_billing', $data);
+    }
+    public function bills_pending_signature(Request $request){
+        $data = [
+            'records' => Bill::where('bill_service_type', '<>', 'CARE_MONTH')->where(function ($query) {
+                $query->where('hcp_pro_id', $this->performer->pro->id)->where('is_signed_by_hcp', false)->where('is_cancelled', false);
+            })
+                ->orWhere(function ($query) {
+                    $query->where('cm_pro_id', $this->performer->pro->id)->where('is_signed_by_cm', false)->where('is_cancelled', false);
+                })->orWhere(function ($query) {
+                    $query->where('rme_pro_id', $this->performer->pro->id)->where('is_signed_by_rme', false)->where('is_cancelled', false);
+                })->orWhere(function ($query) {
+                    $query->where('rmm_pro_id', $this->performer->pro->id)->where('is_signed_by_rmm', false)->where('is_cancelled', false);
+                })->orWhere(function ($query) {
+                    $query->where('generic_pro_id', $this->performer->pro->id)->where('is_signed_by_generic_pro', false)->where('is_cancelled', false);
+                })
+                ->orderBy('created_at', 'DESC')
+                ->get()
+        ];
+        return view('app.mcp.bills_pending_signature', $data);
+    }
+    public function reports_pending_signature(Request $request){
+        $data = [
+            'records' => IncomingReport::where('hcp_pro_id', $this->performer->pro->id)
+                ->where('has_hcp_pro_signed', '<>', true)
+                ->where('is_entry_error', '<>', true)
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.reports_pending_signature', $data);
+    }
+    public function patients_without_appointments(Request $request){
+        $patients = $this->performer->pro->get_patients_without_appointment_query()->paginate(20);
+        return view('app.mcp.patients_without_appointments', compact('patients'));
+    }
+    public function patients_overdue_for_visit(Request $request){
+        $patients = $this->performer->pro->get_patients_overdue_for_visit_query()->paginate(20);
+        return view('app.mcp.patients_overdue_for_visit', compact('patients'));
+    }
+    public function cancelled_appointments_pending_review(Request $request){
+        $data = [];
+        return view('app.mcp.cancelled_appointments_pending_review', $data);
+    }
+    public function cancelled_bills_pending_review(Request $request){
+        $data = [
+            'records' => Bill::where('hcp_pro_id', $this->performer->pro->id)
+                ->where('bill_service_type', 'NOTE')
+                ->where('is_cancelled', true)
+                ->where('is_cancellation_acknowledged', '<>', true)
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.cancelled_bills_pending_review', $data);
+    }
+    public function cancelled_supply_orders_pending_review(Request $request){
+        $data = [
+            'records' => SupplyOrder::where('signed_by_pro_id', $this->performer->pro->id)
+                ->where('is_cancelled', true)
+                ->where('is_cancellation_acknowledged', '<>', true)
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.cancelled_supply_orders_pending_review', $data);
+    }
+    public function erx_and_orders_pending_signature(Request $request){
+        $data = [
+            'records' => Erx::where('hcp_pro_id', $this->performer->pro->id)
+                ->where('pro_declared_status', '<>', 'CANCELLED')
+                ->where('has_hcp_pro_signed', '<>', true)
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.erx_and_orders_pending_signature', $data);
+    }
+    public function supply_orders_pending_signature(Request $request){
+        $data = [
+            'records' => SupplyOrder::where('created_by_pro_id', $this->performer->pro->id)
+                ->whereNull('signed_by_pro_id')
+                ->where('is_cancelled', '<>', true)
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.supply_orders_pending_signature', $data);
+    }
+    public function supply_orders_awaiting_shipment(Request $request){
+        $data = [
+            'records' => SupplyOrder::where('created_by_pro_id', $this->performer->pro->id)
+                ->where('is_signed_by_pro', true)
+                ->where('is_cleared_for_shipment', true)
+                ->whereNull('shipment_id')
+                ->orderBy('created_at')
+                ->get()
+        ];
+        return view('app.mcp.supply_orders_awaiting_shipment', $data);
+    }
+    public function measurements_pending_stamping(Request $request){
+        $data = [
+            'records' => CareMonth::where('mcp_pro_id', $this->performer->pro->id)
+                ->where('rm_num_measurements_not_stamped_by_mcp', '>', 0)
+                ->whereNotNull('rm_num_measurements_not_stamped_by_mcp')
+                ->orderBy('created_at', 'DESC')
+                ->paginate(15)
+        ];
+        return view('app.mcp.measurements_pending_stamping', $data);
+    }
+
+}

+ 415 - 0
app/Http/Controllers/NoteController.php

@@ -0,0 +1,415 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\AppSession;
+use App\Models\Page;
+use App\Models\Point;
+use App\Models\Pro;
+use App\Models\SupplyOrder;
+use App\Models\Ticket;
+use Illuminate\Http\Request;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Blade;
+use Illuminate\Support\Facades\Http;
+
+use App\Models\Note;
+use App\Models\Client;
+use App\Models\Section;
+use App\Models\SectionTemplate;
+use App\Models\Segment;
+use App\Models\SegmentTemplate;
+
+class NoteController extends Controller
+{
+
+    public function dashboard(Request $request, Client $patient, Note $note)
+    {
+        $pros = $this->pros;
+        $noteSections = $note->sections;
+        $allSections = SectionTemplate::where('is_active', true)->get();
+        foreach ($allSections as $section) {
+            $section->used = false;
+            foreach ($noteSections as $noteSection) {
+                if ($noteSection->sectionTemplate->id === $section->id) {
+                    $section->used = true;
+                    $section->section_uid = $noteSection->uid;
+                    break;
+                }
+            }
+        }
+
+        // load tickets created on note->effective_date for patient
+        $ticketsOnNote = Ticket::where('client_id', $patient->id)
+            ->where('is_entry_error', false)
+            ->where('note_id', $note->id)
+            ->get();
+
+        // other open tickets as of today
+        $otherOpenTickets = Ticket::where('client_id', $patient->id)
+            ->where('is_entry_error', false)
+            ->where('is_open', true)
+            ->where(function ($query) use ($note) {
+                $query->where('note_id', '<>', $note->id)->orWhereNull('note_id'); // weird, but just the <> isn't working!
+            })
+            ->get();
+
+        // load supplyOrders created on note->effective_date for patient
+        $supplyOrdersOnNote = SupplyOrder::where('client_id', $patient->id)
+            ->where('is_cancelled', false)
+            ->where('note_id', $note->id)
+            ->get();
+
+        // other open supplyOrders as of today
+        $otherOpenSupplyOrders = SupplyOrder::where('client_id', $patient->id)
+            ->where('is_cancelled', false)
+            ->where('note_id', '<>', $note->id)
+            ->get();
+
+        return view('app.patient.note.dashboard', compact('patient', 'note',
+            'allSections',
+            'ticketsOnNote', 'otherOpenTickets',
+            'supplyOrdersOnNote', 'otherOpenSupplyOrders'));
+    }
+
+    public function signConfirmation(Request $request, Client $patient, Note $note) {
+        return view('app.patient.note.sign-confirmation', compact('patient', 'note'));
+    }
+
+    public function renderNote($noteUid, Request $request)
+    {
+
+        $note = Note::where('uid', $noteUid)->first();
+        $client = Client::where('id', $note->client_id)->first();
+
+        return view('client/note', compact('note', 'client'));
+    }
+
+    public function sectionView(Request $request, Client $patient, Note $note, Section $section, $form, Page $page = null) {
+        return view("app.patient.page-sections." . $section->sectionTemplate->internal_name . "." . $form,
+            compact('patient', 'note', 'section', 'page'));
+    }
+
+    public function getHtmlForSegment($segmentUid, $sessionKey){
+
+        $summaryHtml = '';
+        $editHtml = '';
+
+        try {
+
+            $performer = AppSession::where('session_key', $sessionKey)->first();
+            if (!$performer || !$performer->is_active) {
+                return response()->json([
+                    'success' => false,
+                    'message' => 'Invalid session key'
+                ]);
+            }
+            $pro = $performer->pro;
+
+            $segment = Segment::where('uid', $segmentUid)->first();
+
+            $recalculatedHtml = $segment->getRecalculatedHtml($performer, $sessionKey);
+
+        } catch (\Throwable $e) {
+            return response()->json([
+                'success' => false,
+                'message' => $e->getMessage()
+            ]);
+        }
+
+        return response()->json([
+            'success'=>true,
+            'summaryHtml' => $recalculatedHtml['summaryHtml'],
+            'editHtml' => $recalculatedHtml['editHtml'],
+        ]);
+    }
+
+    // JAVA ONLY
+    // ... if hcpProId is passed, get from request
+    public function getDefaultValueForSection($patientID, $sectionTemplateID)
+    {
+        $contentData = [];
+        $summaryHtml = '';
+        $patient = Client::where('id', $patientID)->first();
+        $sectionTemplate = SectionTemplate::where('id', $sectionTemplateID)->first();
+
+        if ($sectionTemplate->is_canvas) {
+            if (file_exists(resource_path('views/app/patient/canvas-sections/' . $sectionTemplate->internal_name . '/default.php'))) {
+
+                // for canvas section where we have pro mapped data, use hcpProId
+                $hcpPro = null;
+                if(\request()->input('hcpProUid')) {
+                    $hcpPro = Pro::where('uid', \request()->input('hcpProUid'))->first();
+                }
+
+                $note = null;
+                if(\request()->input('noteUid')) {
+                    $note = Note::where('uid', \request()->input('noteUid'))->first();
+                }
+
+                // default should simply assign to $contentData
+                include(resource_path('views/app/patient/canvas-sections/' . $sectionTemplate->internal_name . '/default.php'));
+
+                ob_start();
+                include(resource_path('views/app/patient/canvas-sections/' . $sectionTemplate->internal_name . '/summary.php'));
+                $summaryHtml = ob_get_contents();
+                ob_end_clean();
+            }
+        } else {
+            if (file_exists(storage_path('sections/' . $sectionTemplate->internal_name . '/default.php'))) {
+                // default should simply assign to $contentData and $summaryHtml as needed
+                include(storage_path('sections/' . $sectionTemplate->internal_name . '/default.php'));
+            }
+        }
+        return [
+            'contentData' => $contentData,
+            'summaryHtml' => $summaryHtml
+        ];
+    }
+
+    public function processFormSubmit(Request $request)
+    {
+        // guest_access_code, section_uid, data
+        // REMEMBER, if this is an hcp scoped canvas section, data will not be the ENTIRE node...
+        // ... it will only be the hcp scope within that node
+
+        $guestAccessCode = $request->get('guest_access_code');
+        if($guestAccessCode){
+            //its from guest
+            $sectionForToken = Section::where('guest_access_code', $guestAccessCode)->first();
+            abort_if(!$sectionForToken, 401, 'Unauthorized');
+        }else{
+            //its not from guest so require performer
+            abort_if(!$this->performer, 401, 'Unauthorized');
+            abort_if(!$this->performer->is_active, 401, 'Unauthorized');
+        }
+
+        // TODO require
+        $section_uid =  $request->get('section_uid');
+
+        $section = Section::where('uid', $section_uid)->first();
+        $note = Note::where('id', $section->note_id)->first();
+        $client = null;
+        if($note){
+            $client = Client::where('id', $note->client_id)->first();
+        }else{
+            $client = Client::where('id', $section->client_id)->first();
+        }
+
+        $patient = $client;
+        $sectionTemplate = SectionTemplate::where('id', $section->section_template_id)->first();
+
+        $newContentData = [];
+        $newSummaryHtml = "";
+
+        $sectionInternalName = $sectionTemplate->internal_name;
+        if ($sectionTemplate->is_canvas) {
+
+            $key = $sectionTemplate->internal_name;
+
+            // Because sectionTemplate is_canvas, any update to the section will require updating the canvas.
+            // ... there are TWO possibilities.
+            // ...... 1) if !is_hcp_scoped, then what comes in from the section simply swaps out the entire node
+            // ...... 2) if is_hcp_scoped, then what comes in from the section is incoprorated into that scope in the node
+
+            $newCanvasNodeData = null;
+            if($sectionTemplate->is_hcp_scoped){
+                $currentCanvasData = json_decode($client->canvas_data, true);
+                $currentCanvasDataNode = isset($currentCanvasData[$key]) ? $currentCanvasData[$key] : [];
+                $currentCanvasDataNode[$note->hcpPro->id] = json_decode($request->get('data'), true);
+                $newCanvasNodeData = json_encode($currentCanvasDataNode);
+            }else{
+                $newCanvasNodeData = $request->get('data');
+            }
+
+            $response = null;
+            $data = [
+                'uid' => $client->uid,
+                'noteUid'=> $note?$note->uid:null,
+                'key' => $key,
+                'data' => $newCanvasNodeData
+            ];
+
+            $response = $this->calljava($request, '/client/updateCanvasData', $data, $guestAccessCode);
+            //TODO: handle $response->success == false
+
+            if($note){
+                $client = Client::where('id', $note->client_id)->first();
+            }else{
+                $client = Client::where('id', $section->client_id)->first();
+            }
+
+            $patient = $client;
+            if (file_exists(resource_path("views/app/patient/canvas-sections/{$sectionInternalName}/processor.php"))) {
+                include(resource_path("views/app/patient/canvas-sections/{$sectionInternalName}/processor.php"));
+            } else {
+                $newContentData = json_decode($request->get('data'), true);
+            }
+
+            ob_start();
+            include(resource_path("views/app/patient/canvas-sections/{$sectionInternalName}/summary.php"));
+            $newSummaryHtml = ob_get_contents();
+            ob_end_clean();
+            // TODO call Java to update the canvas
+        } elseif (file_exists(storage_path('sections/' . $sectionTemplate->internal_name . '/form.blade.php'))) {
+
+            include(storage_path('sections/' . $sectionTemplate->internal_name . '/processor.php'));
+
+            ob_start();
+            include(storage_path('sections/' . $sectionTemplate->internal_name . '/summary.php'));
+            $newSummaryHtml = ob_get_contents();
+            ob_end_clean();
+        } else {
+
+            $newContentData = json_decode($request->get('data'), true);
+            if (isset($newContentData['value'])) {
+                $newSummaryHtml = $newContentData['value'];
+            }
+        }
+
+        $response = null;
+        $data = [
+            'uid' => $section->uid,
+            'contentData' => json_encode($newContentData),
+            'summaryHtml' => $newSummaryHtml
+        ];
+        $response = $this->calljava($request, '/section/update', $data, $guestAccessCode);
+        return [
+            'success' => $response['success'],
+            'newSummaryHtml' => $newSummaryHtml
+        ];
+    }
+
+    // edit hpi (structured)
+    public function editHPI(Note $note, Point $point) {
+        return view('app.patient.note.edit-hpi', compact('note', 'point'));
+    }
+
+    public function hpiLog(Note $note, Point $point) {
+        return view('app.patient.note.hpi-log', compact('note', 'point'));
+    }
+
+    // review log
+    public function reviewLog(Point $point) {
+        return view('app.patient.note.review-log', compact('point'));
+    }
+
+    // plan log
+    public function planLog(Point $point) {
+        return view('app.patient.note.plan-log', compact('point'));
+    }
+
+    // print/pdf
+    public function downloadAsPdf(Request $request, Note $note) {
+        $patient = $note->client;
+        if($request->input('html')) {
+            return view('app.patient.note.pdf', compact('note', 'patient'));
+        }
+        else {
+            $pdf = \PDF::loadView('app.patient.note.pdf', compact('note', 'patient'));
+            return $pdf->stream($note->created_at .'_' . 'note.pdf');
+        }
+    }
+
+    public function generateCC(Request $request, Note $note) {
+        $client = $note->client;
+        return view('app.patient.segment-templates.chief_complaint.generate', compact('note', 'client'));
+    }
+
+    public function segmentSummary(Request $request, Segment $segment) {
+        return '<div class="mrv-content border-top px-3 pt-2 mt-3">' . @$segment->summary_html . '</div>';
+    }
+
+    public function chartSegmentView(Request $request, Client $patient, $segmentInternalName, $view) {
+        return view("app.patient.segment-templates.{$segmentInternalName}.{$view}", [
+            'patient' => $patient,
+            'note' => $patient->coreNote,
+            'segmentInternalName' => $segmentInternalName,
+            'closeOnSave' => true
+        ]);
+    }
+
+    public function noteSegmentView(Request $request, Client $patient, Note $note, Segment $segment, $segmentInternalName, $view) {
+        return view("app.patient.segment-templates.{$segmentInternalName}.{$view}", [
+            'patient' => $patient,
+            'note' => $note,
+            'segment' => $segment,
+            'segmentInternalName' => $segmentInternalName
+        ]);
+    }
+
+    public function rhsSidebar(Request $request, Client $patient, Note $note) {
+        return view('app.patient.note.rhs-sidebar', compact('patient', 'note'));
+    }
+
+    public function medicationsCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.medications-center', compact('patient', 'note'));
+    }
+
+    public function medicationsReconcile(Request $request, Client $patient, Note $note) {
+        return view('app.patient.medications-reconcile', compact('patient', 'note'));
+    }
+
+    public function problemsQuickAdd(Request $request, Client $patient, Note $note) {
+        return view('app.patient.problems-quick-add', compact('patient', 'note'));
+    }
+
+    public function problemsCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.problems-center', compact('patient', 'note'));
+    }
+
+    public function goalsCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.goals-center', compact('patient', 'note'));
+    }
+
+    public function allergiesCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.allergies-center', compact('patient', 'note'));
+    }
+
+    public function careteamCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.careteam-center', compact('patient', 'note'));
+    }
+
+    public function supplementsCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.supplements-center', compact('patient', 'note'));
+    }
+
+    public function supplementsReconcile(Request $request, Client $patient, Note $note) {
+        return view('app.patient.supplements-reconcile', compact('patient', 'note'));
+    }
+
+    public function nutritionCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.nutrition-center', compact('patient', 'note'));
+    }
+
+    public function exerciseCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.exercise-center', compact('patient', 'note'));
+    }
+
+    public function behaviorCenter(Request $request, Client $patient, Note $note) {
+        return view('app.patient.behavior-center', compact('patient', 'note'));
+    }
+
+    public function ccmAgreement(Request $request, Note $note) {
+        return view('app.patient.note.ccm-agreement', compact('note'));
+    }
+
+    public function rpmAgreement(Request $request, Note $note) {
+        return view('app.patient.note.rpm-agreement', compact('note'));
+    }
+
+    // TODO move to utility
+    private function callJava($request, $endPoint, $data, $guestAccessCode = null)
+    {
+        $url =  config('stag.backendUrl') . $endPoint;
+        $response = Http::asForm()
+            ->withHeaders([
+                'sessionKey' => $request->cookie('sessionKey'),
+                'guestAccessCode' => $guestAccessCode
+            ])
+            ->post($url, $data)
+            ->json();
+        return $response;
+    }
+
+}

+ 628 - 0
app/Http/Controllers/PatientController.php

@@ -0,0 +1,628 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Appointment;
+use App\Models\BDTDevice;
+use App\Models\CareMonth;
+use App\Models\Client;
+use App\Models\ClientBDTDevice;
+use App\Models\ClientInfoLine;
+use App\Models\ClientProAccess;
+use App\Models\Erx;
+use App\Models\Facility;
+use App\Models\Handout;
+use App\Models\IncomingReport;
+use App\Models\MBClaim;
+use App\Models\MBPayer;
+use App\Models\Note;
+use App\Models\NoteTemplate;
+use App\Models\Pro;
+use App\Models\Product;
+use App\Models\ProProAccess;
+use App\Models\SectionTemplate;
+use App\Models\Shipment;
+use App\Models\SupplyOrder;
+use App\Models\Ticket;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\File;
+
+use Illuminate\Support\Facades\Http;
+use PDF;
+
+class PatientController extends Controller
+{
+
+    public function claimsResolver(Request $request, Client $patient)
+    {
+        $notes = $patient->notesAscending;
+        $hcpSignedNotesCount = 0;
+        foreach($notes as $note){
+            if($note->is_signed_by_hcp){
+                $hcpSignedNotesCount += 1;
+            }
+        }
+        $data = [
+            'dog' => 'bark',
+            'patient' => $patient,
+            'hcpSignedNotesCount' => $hcpSignedNotesCount
+        ];
+        return view('app.patient.claims-resolver', $data);
+    }
+
+    public function dashboard(Request $request, Client $patient )
+    {
+        $mcpPros = Pro::where('is_enrolled_as_mcp', true)->get();
+        $facilities = []; // Facility::where('is_active', true)->get();
+
+        // get assigned devices
+        $assignedDeviceIDs = DB::select(DB::raw("SELECT device_id from client_bdt_device where is_active = true"));
+        $assignedDeviceIDs = array_map(function($_x) {
+            return $_x->device_id;
+        }, $assignedDeviceIDs);
+
+        // get all except assigned ones
+        $devices = BDTDevice::where('is_active', true)
+            ->whereNotIn('id', $assignedDeviceIDs)
+            ->orderBy('imei', 'asc')
+            ->get();
+
+        $assignedDeviceIDs = null;
+        unset($assignedDeviceIDs);
+
+        $dxInfoLines = ClientInfoLine::where('client_id', $patient->id)
+            ->where('category', 'dx')
+            ->where('is_removed', false)
+            ->orderBy('content_text', 'asc')
+            ->get();
+        return view('app.patient.dashboard',
+            compact('patient', 'facilities', 'devices', 'dxInfoLines'));
+    }
+
+    public function canvasMigrate(Request $request, Client $patient )
+    {
+        $mcpPros = Pro::where('is_enrolled_as_mcp', true)->get();
+        $facilities = []; // Facility::where('is_active', true)->get();
+
+        // get assigned devices
+        $assignedDeviceIDs = DB::select(DB::raw("SELECT device_id from client_bdt_device where is_active = true"));
+        $assignedDeviceIDs = array_map(function($_x) {
+            return $_x->device_id;
+        }, $assignedDeviceIDs);
+
+        // get all except assigned ones
+        $devices = BDTDevice::where('is_active', true)
+            ->whereNotIn('id', $assignedDeviceIDs)
+            ->orderBy('imei', 'asc')
+            ->get();
+
+        $assignedDeviceIDs = null;
+        unset($assignedDeviceIDs);
+
+        $dxInfoLines = ClientInfoLine::where('client_id', $patient->id)
+            ->where('category', 'dx')
+            ->where('is_removed', false)
+            ->orderBy('content_text', 'asc')
+            ->get();
+        return view('app.patient.canvas-migrate',
+            compact('patient', 'facilities', 'devices', 'dxInfoLines'));
+    }
+
+    public function canvas(Request $request, Client $patient){
+        return view('app.patient.canvas_dump', compact('patient'));
+    }
+
+    public function actionItems(Request $request, Client $patient )
+    {
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items', compact('patient', 'facilities'));
+    }
+
+    public function actionItemsErx(Request $request, Client $patient, $filter = 'open')
+    {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-erx', compact('patient', 'facilities', 'filter', 'allPros'));
+    }
+
+    public function actionItemsLab(Request $request, Client $patient, $filter = 'open')
+    {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-lab', compact('patient', 'facilities', 'filter', 'allPros'));
+    }
+
+    public function actionItemsImaging(Request $request, Client $patient, $filter = 'open')
+    {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-imaging', compact('patient', 'facilities', 'filter', 'allPros'));
+    }
+
+    public function actionItemsEquipment(Request $request, Client $patient, $filter = 'open')
+    {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-equipment', compact('patient', 'facilities', 'filter', 'allPros'));
+    }
+
+    public function actionItemsOther(Request $request, Client $patient, $filter = 'open')
+    {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-other', compact('patient', 'facilities', 'filter', 'allPros'));
+    }
+
+    public function actionItemsErxSingle(Request $request, Client $patient, Ticket $ticket) {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-erx-single', compact('patient', 'facilities', 'allPros', 'ticket'));
+    }
+    public function actionItemsLabSingle(Request $request, Client $patient, Ticket $ticket) {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-lab-single', compact('patient', 'facilities', 'allPros', 'ticket'));
+    }
+    public function actionItemsImagingSingle(Request $request, Client $patient, Ticket $ticket) {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-imaging-single', compact('patient', 'facilities', 'allPros', 'ticket'));
+    }
+    public function actionItemsEquipmentSingle(Request $request, Client $patient, Ticket $ticket) {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-equipment-single', compact('patient', 'facilities', 'allPros', 'ticket'));
+    }
+    public function actionItemsOtherSingle(Request $request, Client $patient, Ticket $ticket) {
+        $allPros = Pro::all();
+        $facilities = []; // Facility::where('is_active', true)->get();
+        return view('app.patient.action-items-other-single', compact('patient', 'facilities', 'allPros', 'ticket'));
+    }
+
+    public function intake(Request $request, Client $patient )
+    {
+        $files = File::allFiles(resource_path('views/app/intake-templates'));
+        $templates = [];
+        foreach ($files as $file) {
+            $templates[] = str_replace(".blade.php", "", $file->getFilename());
+        }
+        return view('app.patient.intake', compact('patient', 'templates'));
+    }
+
+    public function carePlan(Request $request, Client $patient )
+    {
+        return view('app.patient.care-plan', compact('patient'));
+    }
+
+    public function medications(Request $request, Client $patient )
+    {
+        $infoLines = ClientInfoLine::where('client_id', $patient->id)
+            ->where('category', 'rx')
+            ->where('is_removed', false)
+            ->orderBy('content_text', 'asc')
+            ->get();
+        return view('app.patient.medications', compact('patient', 'infoLines'));
+    }
+
+    public function dxAndFocusAreas(Request $request, Client $patient )
+    {
+        $dxInfoLines = ClientInfoLine::where('client_id', $patient->id)
+            ->where('category', 'dx')
+            ->where('is_removed', false)
+            ->orderBy('content_text', 'asc')
+            ->get();
+        return view('app.patient.dx-and-focus-areas', compact('patient', 'dxInfoLines'));
+    }
+
+    public function careTeam(Request $request, Client $patient )
+    {
+        $infoLines = ClientInfoLine::where('client_id', $patient->id)
+            ->where('category', 'care_team')
+            ->where('is_removed', false)
+            ->get();
+        return view('app.patient.care-team', compact('patient', 'infoLines'));
+    }
+
+    public function devices(Request $request, Client $patient )
+    {
+        // get assigned devices
+        $assignedDeviceIDs = DB::select(DB::raw("SELECT device_id from client_bdt_device where is_active = true"));
+        $assignedDeviceIDs = array_map(function($_x) {
+            return $_x->device_id;
+        }, $assignedDeviceIDs);
+
+        // get all except assigned ones
+        $devices = BDTDevice::where('is_active', true)
+            ->whereNotIn('id', $assignedDeviceIDs)
+            ->orderBy('imei', 'asc')
+            ->get();
+
+        $assignedDeviceIDs = null;
+        unset($assignedDeviceIDs);
+
+        return view('app.patient.devices', compact('patient', 'devices'));
+    }
+
+    public function measurements(Request $request, Client $patient )
+    {
+        return view('app.patient.measurements', compact('patient'));
+    }
+
+    public function labsAndStudies(Request $request, Client $patient )
+    {
+        return view('app.patient.labs-and-studies', compact('patient'));
+    }
+
+    public function history(Request $request, Client $patient )
+    {
+        $infoLines = ClientInfoLine::where('client_id', $patient->id)
+            ->where('category', 'LIKE', 'history_%')
+            ->where('is_removed', false)
+            ->get();
+        return view('app.patient.history', compact('patient', 'infoLines'));
+    }
+
+    public function memos(Request $request, Client $patient )
+    {
+        return view('app.patient.memos', compact('patient'));
+    }
+
+    public function memosThread(Request $request, Client $patient )
+    {
+        return view('app.patient.memos-thread', compact('patient'));
+    }
+
+    public function messagesThread(Request $request, Client $patient )
+    {
+        return view('app.patient.messages-thread', compact('patient'));
+    }
+
+    public function sms(Request $request, Client $patient )
+    {
+        return view('app.patient.sms', compact('patient'));
+    }
+
+    public function smsNumbers(Request $request, Client $patient )
+    {
+        return view('app.patient.sms-numbers', compact('patient'));
+    }
+
+    public function immunizations(Request $request, Client $patient )
+    {
+        return view('app.patient.immunizations', compact('patient'));
+    }
+
+    public function allergies(Request $request, Client $patient )
+    {
+        $infoLines = ClientInfoLine::where('client_id', $patient->id)
+            ->where('category', 'allergy')
+            ->where('is_removed', false)
+            ->get();
+        return view('app.patient.allergies', compact('patient', 'infoLines'));
+    }
+
+    public function notes(Request $request, Client $patient, $filter = 'active')
+    {
+        $pros = $this->pros;
+        return view('app.patient.notes', compact('patient','pros', 'filter'));
+    }
+
+    public function genericBills(Request $request, Client $patient)
+    {
+        return view('app.patient.generic-bills', compact('patient'));
+    }
+
+    public function rmSetup(Request $request, Client $patient)
+    {
+        return view('app.patient.rm-setup', compact('patient'));
+    }
+
+    public function handouts(Request $request, Client $patient )
+    {
+        $handouts = Handout::where('is_active', true)->get();
+        return view('app.patient.handouts', compact('patient', 'handouts'));
+    }
+
+    public function settings(Request $request, Client $patient )
+    {
+        return view('app.patient.settings', compact('patient'));
+    }
+
+    public function smsReminders(Request $request, Client $patient )
+    {
+        return view('app.patient.sms-reminders', compact('patient'));
+    }
+
+    public function measurementConfirmationNumbers(Request $request, Client $patient )
+    {
+        return view('app.patient.measurement-confirmation-numbers', compact('patient'));
+    }
+
+    public function pros(Request $request, Client $patient )
+    {
+        return view('app.patient.pros', compact('patient'));
+    }
+
+    public function account(Request $request, Client $patient )
+    {
+        return view('app.patient.account', compact('patient'));
+    }
+
+    public function careChecklist(Request $request, Client $patient )
+    {
+        return view('app.patient.care-checklist', compact('patient'));
+    }
+
+    public function documents(Request $request, Client $patient )
+    {
+        return view('app.patient.documents', compact('patient'));
+    }
+
+    public function incomingReports(Request $request, Client $patient, IncomingReport $currentReport = null)
+    {
+        return view('app.patient.incoming-reports', compact('patient', 'currentReport'));
+    }
+
+    public function education(Request $request, Client $patient )
+    {
+        return view('app.patient.education', compact('patient'));
+    }
+
+    public function messaging(Request $request, Client $patient )
+    {
+        return view('app.patient.messaging', compact('patient'));
+    }
+
+    public function duplicate(Request $request, Client $patient )
+    {
+        return view('app.patient.duplicate', compact('patient'));
+    }
+
+    public function careMonths(Request $request, Client $patient )
+    {
+        $careMonths = CareMonth::where('client_id', $patient->id)->orderBy('start_date', 'desc')->get();
+        $notes = Note::where('is_cancelled', false)->get();
+        return view('app.patient.care-months', compact('patient', 'careMonths', 'notes'));
+    }
+
+    public function presence(Request $request, Client $patient )
+    {
+        return json_encode([
+            "online" => $patient->is_online
+        ]);
+    }
+
+    public function embedSection(Request $request, Client $patient, $section, $selectable) {
+        return view('app.patient.partials.' . $section, compact('patient', 'selectable'));
+    }
+
+    public function calendar(Request $request, Client $patient, Appointment $currentAppointment) {
+
+        $pros = [];
+
+        if($this->pro && $this->pro->pro_type != 'ADMIN') {
+
+            $accessiblePros = ProProAccess::where('owner_pro_id', $this->pro->id)->get();
+
+            $accessibleProIds = [];
+            foreach($accessiblePros as $accessiblePro){
+                $accessibleProIds[] = $accessiblePro->accessible_pro_id;
+            }
+            $accessibleProIds[] = $this->pro->id;
+
+            // for dna, add pros accessible via pro teams
+            if($this->performer->pro->isDefaultNA()) {
+                $teams = $this->performer->pro->teamsWhereAssistant;
+                foreach ($teams as $team) {
+                    if(!in_array($team->mcp_pro_id, $accessibleProIds)) {
+                        $accessibleProIds[] = $team->mcp_pro_id;
+                    }
+                }
+            }
+
+            $pros = Pro::whereIn('id', $accessibleProIds)->get();
+        }
+
+        $dateLastWeek = date_sub(date_create(), date_interval_create_from_date_string("14 days"));
+        $dateLastWeek = date_format($dateLastWeek, "Y-m-d");
+        $appointments = Appointment::where('client_id', $patient->id)
+            ->orderBy('raw_date', 'desc')->orderBy('raw_start_time', 'desc')
+            ->where('raw_date', '>=', $dateLastWeek)
+            ->get();
+        $appointmentProIDs = $appointments->map(function($_item) {
+            return $_item->pro_id;
+        });
+        $appointmentPros = Pro::whereIn('id', $appointmentProIDs)->get();
+
+        return view('app.patient.appointment-calendar',
+            compact('pros', 'patient', 'currentAppointment', 'appointments', 'appointmentPros'));
+    }
+
+    public function programs(Request $request, Client $patient, $filter = '') {
+        $pros = $this->pros;
+        return view('app.patient.programs', compact('patient', 'pros', 'filter'));
+    }
+
+    public function flowsheets(Request $request, Client $patient, $filter = '') {
+        $pros = $this->pros;
+        return view('app.patient.flowsheets', compact('patient', 'pros', 'filter'));
+    }
+
+    public function vitalsSettings(Request $request, Client $patient) {
+        return view('app.patient.vitals-settings', compact('patient'));
+    }
+
+    public function vitalsGraph(Request $request, Client $patient, $filter = '') {
+        $pros = $this->pros;
+        return view('app.patient.vitals-graph', compact('patient', 'pros', 'filter'));
+    }
+
+    public function tickets(Request $request, Client $patient, $type = '', String $currentTicket = '') {
+        $pros = $this->pros;
+        $allPros = Pro::all();
+        $qlTicket = $currentTicket;
+        if(!!$currentTicket) {
+            $qlTicket = Ticket::where('uid', $currentTicket)->first();
+            if($qlTicket) {
+                $currentTicket = $qlTicket;
+            }
+        }
+        return view('app.patient.tickets', compact('patient', 'pros', 'allPros', 'type', 'currentTicket'));
+    }
+
+    public function prescriptions(Request $request, Client $patient, String $type = '', String $currentErx = '') {
+        if(!!$currentErx) {
+            $currentErx = Erx::where('uid', $currentErx)->first();
+        }
+        $note = $patient->coreNote;
+        return view('app.patient.prescriptions.index', compact('patient', 'type', 'currentErx', 'note'));
+    }
+
+    public function prescriptionsPopup(Request $request, Client $patient, String $type = '', String $currentErx = '') {
+        if(!!$currentErx) {
+            $currentErx = Erx::where('uid', $currentErx)->first();
+        }
+        $note = null;
+        if($request->input('noteUid')) {
+            $note = Note::where('uid', $request->input('noteUid'))->first();
+        }
+        return view('app.patient.prescriptions-popup.list-popup', compact('patient', 'type', 'currentErx', 'note'));
+    }
+
+    public function prescriptionsList(Request $request, Client $patient, String $type = '', String $currentErx = '') {
+        if(!!$currentErx) {
+            $currentErx = Erx::where('uid', $currentErx)->first();
+        }
+        $note = null;
+        if($request->input('noteUid')) {
+            $note = Note::where('uid', $request->input('noteUid'))->first();
+        }
+        return view('app.patient.prescriptions.list', compact('patient', 'type', 'currentErx', 'note'));
+    }
+
+    public function downloadPrescriptionAsPdf(Request $request, Erx $prescription){
+        if($request->input('html')) {
+            return view('app.patient.prescriptions.pdf.pdf-preview', compact('prescription'));
+        }
+        else {
+            $pdf = PDF::loadView('app.patient.prescriptions.pdf.pdf-preview', compact('prescription'));
+            return $pdf->download($prescription->created_at .'_' . 'erx.pdf');
+        }
+    }
+
+    public function transmitPrescription(Request $request, Erx $prescription){
+
+        // re-generate pdf with cover sheet as first page and save it to FS
+        $filePath = config('app.temp_dir') . "/{$prescription->uid}.pdf";
+        $pdf = PDF::loadView('app.patient.prescriptions.pdf.pdf-preview-with-cover-sheet', compact('prescription'));
+        $pdf->save($filePath);
+
+        // send it along with the rest of the params to /api/erx/transmit [multi-part POST]
+        $url =  config('stag.backendUrl') . '/erx/transmit';
+        $params = [
+            "uid" => $request->input('uid'),
+            "toWho" => $request->input('toWho'),
+            "toEmail" => $request->input('toEmail'),
+            "toFaxNumber" => $request->input('toFaxNumber'),
+            "toFaxNumberAttentionLine" => $request->input('toFaxNumberAttentionLine'),
+            "toFaxNumberCoverSheetMemo" => $request->input('toFaxNumberCoverSheetMemo'),
+        ];
+        if($request->input('copyToPatient')) {
+            $params["copyToPatientFaxNumber"] = $request->input('copyToPatientFaxNumber');
+            $params["copyToPatientEmail"] = $request->input('copyToPatientEmail');
+        }
+        $pdf = fopen($filePath, 'r');
+        $response = Http
+            ::attach('pdfSystemFile', $filePath, "{$prescription->uid}.pdf")
+            ->withHeaders(['sessionKey' => $request->cookie('sessionKey')])
+            ->post($url, $params)
+            ->json();
+        return $response;
+
+    }
+
+    public function supplyOrders(Request $request, Client $patient, SupplyOrder $supplyOrder = null)
+    {
+        $products = Product::where('is_active', true)->orderBy('created_at', 'desc')->get();
+        return view('app.patient.supply-orders', compact('patient', 'supplyOrder', 'products'));
+    }
+
+    public function shipments(Request $request, Client $patient, Shipment $shipment = null)
+    {
+        return view('app.patient.shipments', compact('patient', 'shipment'));
+    }
+
+    public function appointments(Request $request, Client $patient, $forPro = 'all', $status = 'all') {
+        $pros = $this->pros;
+        $appointments = $patient->appointmentsForProByStatus($forPro, strtoupper($status));
+        $appointmentProIDs = $appointments->map(function($_item) {
+            return $_item->pro_id;
+        });
+        $appointmentPros = Pro::whereIn('id', $appointmentProIDs)->get();
+        return view('app.patient.appointments',
+            compact('patient', 'pros', 'appointments', 'appointmentPros', 'forPro', 'status'));
+    }
+
+    public function mcpRequests(Request $request, Client $patient) {
+        return view('app.patient.mcp-requests', compact('patient'));
+    }
+
+    public function eligibleRefreshes(Request $request, Client $patient) {
+        return view('app.patient.eligible-refreshes', compact('patient'));
+    }
+
+    public function insuranceCoverage(Request $request, Client $patient) {
+        $mbPayers = MBPayer::all();
+        return view('app.patient.insurance-coverage', compact('patient', 'mbPayers'));
+    }
+
+    public function clientPrimaryCoverages(Request $request, Client $patient) {
+        $mbPayers = MBPayer::all();
+        return view('app.patient.client-primary-coverages', compact('patient', 'mbPayers'));
+    }
+
+    public function primaryCoverage(Request $request, Client $patient) {
+        $mbPayers = MBPayer::all();
+        return view('app.patient.primary-coverage', compact('patient', 'mbPayers'));
+    }
+
+    public function primaryCoverageForm(Request $request, Client $patient) {
+        $mbPayers = MBPayer::all();
+        return view('app.patient.primary-coverage-form', compact('patient', 'mbPayers'));
+    }
+
+    public function primaryCoverageManualDeterminationModal(Request $request, Client $patient) {
+        if($patient->latestClientPrimaryCoverage->plan_type === 'MEDICARE'){
+            return view('app.patient.primary-coverage-manual-determination-medicare-modal', compact('patient'));
+        }
+        if($patient->latestClientPrimaryCoverage->plan_type === 'MEDICAID'){
+            return view('app.patient.primary-coverage-manual-determination-medicaid-modal', compact('patient'));
+        }
+        if($patient->latestClientPrimaryCoverage->plan_type === 'COMMERCIAL'){
+            return view('app.patient.primary-coverage-manual-determination-commercial-modal', compact('patient'));
+        }
+
+        return "Plan Type is missing!";
+    }
+
+    public function mbClaim(Request $request, MBClaim $mbClaim) {
+        return view('app.patient.mb-claim-single', compact('mbClaim'));
+    }
+
+    public function accounts(Request $request, Client $patient) {
+        return view('app.patient.accounts', compact('patient'));
+    }
+
+    public function careMonthMatrix(Request $request, CareMonth $careMonth) {
+        return view('app.patient.care-month.matrix', [
+            'patient' => $careMonth->patient,
+            'careMonth' => $careMonth,
+        ]);
+    }
+
+    public function clientProAccess(Request $request, Client $patient) {
+        $rows = ClientProAccess::where('client_id', $patient->id)->get();
+        return view('app.patient.client-pro-access', compact('patient', 'rows'));
+    }
+}

+ 28 - 0
app/Http/Controllers/PayerController.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
+
+use App\Models\Payer;
+use Illuminate\Support\Facades\DB;
+
+class PayerController extends Controller
+{
+	public function searchPayerV2JSON(Request $request)
+	{
+		$term = $request->input('term') ? trim($request->input('term')) : '';
+		if (empty($term)) return '';
+		$matches = DB::select(
+			"
+			SELECT id, uid, (name || ' (' || COALESCE(availity_payer_id, '--') || ')') as text FROM payer WHERE name ILIKE :term OR memo ILIKE :term", 
+			['term' => '%' . $term . '%']
+		);
+
+		return json_encode([
+			"success" => true,
+			"data" => $matches
+		]);
+	}
+}

+ 2640 - 0
app/Http/Controllers/PracticeManagementController.php

@@ -0,0 +1,2640 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\AppSession;
+use App\Models\BillingReport;
+use App\Models\CareMonth;
+use App\Models\ClaimEDI;
+use App\Models\Company;
+use App\Models\Handout;
+use App\Models\MBClaim;
+use App\Models\Measurement;
+use App\Models\Bill;
+use App\Models\Claim;
+use App\Models\Client;
+use App\Models\McpRequest;
+use App\Models\McCodeCheck;
+use App\Models\Note;
+use App\Models\Pack;
+use App\Models\Pro;
+use App\Models\Product;
+use App\Models\ProFavorite;
+use App\Models\ProGeneralAvailability;
+use App\Models\ProProAccess;
+use App\Models\ProRate;
+use App\Models\ProSpecificAvailability;
+use App\Models\ProSpecificUnavailability;
+use App\Models\ProTeam;
+use App\Models\ProTextShortcut;
+use App\Models\ProTransaction;
+use App\Models\Shipment;
+use App\Models\SupplyOrder;
+use App\Models\Team;
+use App\Models\Ticket;
+use App\Models\AccountInvite;
+use App\Models\ClientMeasurementDaysPerMonth;
+use App\Models\ClientBDTDevice;
+use App\Models\ClientMemo;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Http;
+use PDF;
+use DateTime;
+use DateTimeZone;
+use Illuminate\Http\Request;
+use App\Models\SegmentTemplate;
+use App\Models\VisitTemplate;
+use App\Models\VisitTemplateSegmentTemplate;
+
+class PracticeManagementController extends Controller
+{
+
+    public function rpmMatrix(Request $request)
+    {
+        $proID = $this->performer()->pro->id;
+        $isAdmin = $this->performer()->pro->pro_type == 'ADMIN';
+        $query = Client::whereNull('shadow_pro_id');
+        if(!$isAdmin) $query->where('mcp_pro_id', '=', $proID);
+        $clients = $query->orderByRaw('most_recent_cellular_measurement_at desc nulls last')
+            ->paginate(50);
+        return view ('app.practice-management.rpm-matrix', compact('clients'));
+    }
+
+    public function mcCodeChecks(Request $request)
+	{
+		$checks = McCodeCheck::orderBy('next_eligible_date_professional', 'asc')->get();
+		return view ('app.practice-management.mc-code-checks', compact('checks'));
+	}
+
+    public function remoteMonitoringReport(Request $request)
+	{
+		$rows = null;
+		$proID = $this->performer()->pro->id;
+		$isAdmin = $this->performer()->pro->pro_type == 'ADMIN';
+		//$rows = $isAdmin ? ClientMeasurementDaysPerMonth::all() : ClientMeasurementDaysPerMonth::where('mcp_pro_id', $proID)->orderBy('year_month', 'asc')->orderBy('num_of_days_with_measurement', 'asc')->get();
+        $query = CareMonth::whereNotNull('mcp_pro_id')
+            ->where('number_of_days_with_remote_measurements', '>=', 16)
+            ->where('days_between_most_recent_mcp_note_date_and_end_of_care_month', '<=', 90);
+	if(!$isAdmin) $query->where('mcp_pro_id', '=', $proID);
+
+    if($request->get('show_billing_not_closed_only')){
+        $query->where(function($q){
+            return $q->where('is_bill_closed', false)->orWhereNull('is_bill_closed');
+        });
+    }
+
+
+	$rows = $query->orderByRaw(DB::raw('start_date DESC'))->paginate(100) ;
+        return view ('app.practice-management.remote-monitoring-report', compact('rows', 'isAdmin'));
+	}
+
+    public function billingReport(Request $request)
+    {
+
+        $claimStatus = $request->get('status');
+        
+        $rows = BillingReport::paginate(50);
+     
+        if($claimStatus){
+            if($claimStatus == 'NO_CLAIMS'){
+                $rows = BillingReport::whereHas('note', function($noteQuery) use ($claimStatus){
+                    return $noteQuery->has('claims', '=', 0);
+                })->whereHas('client', function($clientQuery){
+                    return $clientQuery->where('client_engagement_status_category','<>' ,'DUMMY');
+                })->orderBy('note_date', 'desc')->paginate(50);
+            }else{
+                $rows =  BillingReport::whereHas('note', function($noteQuery) use ($claimStatus){
+                    return $noteQuery->whereHas('claims', function($claimQuery) use ($claimStatus) {
+                        return $claimQuery->where('status', $claimStatus);
+                    });
+                })->whereHas('client', function($clientQuery){
+                    return $clientQuery->where('client_engagement_status_category','<>' ,'DUMMY');
+                })->orderBy('note_date', 'desc')->paginate(50);
+            }
+        }else {
+            $rows =  BillingReport::whereHas('client', function($clientQuery){
+                return $clientQuery->where('client_engagement_status_category','<>' ,'DUMMY');
+            })->orderBy('note_date', 'desc')->paginate(50);    
+        }
+
+        $claimStatuses = DB::select('SELECT distinct status FROM claim ORDER BY status DESC');
+
+        return view('app.practice-management.billing-report', compact('rows', 'claimStatuses'));
+    }
+
+    public function dashboard(Request $request)
+    {
+        return view('app.practice-management.dashboard');
+    }
+
+    public function rates(Request $request, $selectedProUid = 'all')
+    {
+        $proUid = $selectedProUid ? $selectedProUid : 'all';
+        $rates = ProRate::where('is_active', true);
+        if ($proUid !== 'all') {
+            $selectedPro = Pro::where('uid', $proUid)->first();
+            $rates = $rates->where('pro_id', $selectedPro->id);
+        }
+        $rates = $rates->orderBy('pro_id', 'asc')->get();
+        $pros = $this->pros;
+        return view('app.practice-management.rates', compact('rates', 'pros', 'selectedProUid'));
+    }
+
+    public function previousBills(Request $request)
+    {
+        return view('app.practice-management.previous-bills');
+    }
+
+    public function financialTransactions(Request $request)
+    {
+        $pro = $this->performer()->pro;
+        $transactions = null;
+        if($pro->pro_type === 'ADMIN') {
+            $transactions = ProTransaction::whereNotNull('id');
+        }
+        else {
+            $transactions = ProTransaction::where('pro_id', $pro->id);
+        }
+
+        $filter = $request->input('p');
+        if ($filter) {
+            $filterPro = Pro::where('uid', $filter)->first();
+            if($filterPro) {
+                $transactions = $transactions->where('pro_id', '=', $filterPro->id);
+            }
+        }
+
+        $filter = $request->input('t');
+        if ($filter) {
+            $transactions = $transactions->where('plus_or_minus', '=', $filter);
+        }
+
+        $filter = $request->input('c');
+        if ($filter) {
+            $transactions = $transactions->where('company_id', '=', $filter);
+        }
+
+        $filter = $request->input('bs');
+        if ($filter) {
+            $transactions = $transactions->where('created_at', '>=', $filter);
+        }
+
+        $filter = $request->input('be');
+        if ($filter) {
+            $transactions = $transactions->where('created_at', '<=', $filter);
+        }
+
+        $transactions = $transactions->orderBy('created_at', 'desc')->paginate();
+        $companies = Company::where('is_active', true)->orderBy('name')->get();
+        return view('app.practice-management.financial-transactions', compact('transactions', 'companies'));
+    }
+
+    public function pendingBillsToSign(Request $request)
+    {
+        return view('app.practice-management.pending-bills-to-sign');
+    }
+
+    public function HR(Request $request)
+    {
+        return view('app.practice-management.hr');
+    }
+
+    public function directDepositSettings(Request $request)
+    {
+        return view('app.practice-management.direct-deposit-settings');
+    }
+
+    public function w9(Request $request)
+    {
+        return view('app.practice-management.w9');
+    }
+
+    public function contract(Request $request)
+    {
+        return view('app.practice-management.contract');
+    }
+
+    public function notes(Request $request, $filter = '')
+    {
+        $proID = $this->performer()->pro->id;
+        $query = Note::where('hcp_pro_id', $proID);
+        switch ($filter) {
+            case 'not-yet-signed':
+                $query = $query->where('is_signed_by_hcp', false);
+                break;
+
+            case 'not-yet-signed-but-ally-signed':
+                $query = $query->where('is_signed_by_hcp', false)->where('is_signed_by_ally', true);
+                break;
+
+            case 'without-bills':
+                $query = $query->where('is_signed_by_hcp', true)->where('is_cancelled', false)->whereDoesntHave('bills');
+
+                break;
+
+            // more cases can be added as needed
+            default:
+                break;
+        }
+        $notes = $query->orderBy('created_at', 'desc')->get();
+        return view('app.practice-management.notes', compact('notes', 'filter'));
+    }
+
+    public function dnaNotesPendingMcpSign(Request $request)
+    {
+        $proID = $this->performer()->pro->id;
+        $notes = Note::where('ally_pro_id', $proID)
+            ->where('is_signed_by_hcp', false)
+            ->where('is_cancelled', false)
+            ->orderBy('created_at', 'desc')
+            ->get();
+        return view('app.practice-management.dna-notes-pending-mcp-sign', compact('notes'));
+    }
+
+    public function naBillableSignedNotes(Request $request)
+    {
+
+        $notes = Note
+            ::where('is_signed_by_hcp', TRUE)
+            ->where('ally_pro_id', $this->performer()->pro->id)
+            ->where('is_cancelled', FALSE)
+            ->whereRaw("
+            (
+                SELECT count(bill.id)
+                FROM bill WHERE
+                      bill.is_cancelled = FALSE AND
+                      bill.generic_pro_id = {$this->performer()->pro->id} AND
+                      bill.note_id = note.id
+            ) = 0
+            ");
+
+        $notes = $notes->orderBy('created_at', 'desc')->get();
+        return view('app.practice-management.na-billable-signed-notes', compact('notes'));
+    }
+
+    public function bills(Request $request, $filter = '')
+    {
+        $proID = $this->performer()->pro->id;
+        $query = Bill::where('is_cancelled', false)->where('bill_service_type', '<>', 'CARE_MONTH');
+        switch ($filter) {
+            case 'not-yet-signed':
+                $query = $query
+                    ->where(function ($q) use ($proID) {
+                        $q->where(function ($q2) use ($proID) {
+                            $q2->where('hcp_pro_id', $proID)->where('is_signed_by_hcp', false);
+                        })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('cm_pro_id', $proID)->where('is_signed_by_cm', false);
+                            })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('rme_pro_id', $proID)->where('is_signed_by_rme', false);
+                            })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('rmm_pro_id', $proID)->where('is_signed_by_rmm', false);
+                            })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('generic_pro_id', $proID)->where('is_signed_by_generic_pro', false);
+                            });
+                    });
+                break;
+
+            case 'previous':
+                $query = $query
+                    ->where(function ($q) use ($proID) {
+                        $q->where(function ($q2) use ($proID) {
+                            $q2->where('hcp_pro_id', $proID)->where('is_signed_by_hcp', true);
+                        })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('cm_pro_id', $proID)->where('is_signed_by_cm', true);
+                            })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('rme_pro_id', $proID)->where('is_signed_by_rme', true);
+                            })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('rmm_pro_id', $proID)->where('is_signed_by_rmm', true);
+                            });
+                    });
+                break;
+
+            // more cases can be added as needed
+            default:
+                $query = $query
+                    ->where(function ($q) use ($proID) {
+                        $q->where(function ($q2) use ($proID) {
+                            $q2->where('hcp_pro_id', $proID);
+                        })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('cm_pro_id', $proID);
+                            })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('rme_pro_id', $proID);
+                            })
+                            ->orWhere(function ($q2) use ($proID) {
+                                $q2->where('rmm_pro_id', $proID);
+                            });
+                    });
+                break;
+        }
+        $bills = $query->orderBy('created_at', 'desc')->get();
+        return view('app.practice-management.bills', compact('bills', 'filter'));
+    }
+
+    public function rmBillsToSign(Request $request)
+    {
+        $performerProID = $this->performer()->pro->id;
+        $bills = Bill::where('is_cancelled', false)->where('cm_or_rm', 'RM')
+            ->where(function ($q) use ($performerProID) {
+                $q
+                    ->where(function ($q2) use ($performerProID) {
+                        $q2->where('hcp_pro_id', $performerProID)->where('is_signed_by_hcp', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rme_pro_id', $performerProID)->where('is_signed_by_rme', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('rmm_pro_id', $performerProID)->where('is_signed_by_rmm', false);
+                    })
+                    ->orWhere(function ($q2) use ($performerProID) {
+                        $q2->where('generic_pro_id', $performerProID)->where('is_signed_by_generic_pro', false);
+                    });
+            })
+            ->orderBy('effective_date', 'desc')
+            ->get();
+
+        return view('app.practice-management.rm-bills-to-sign', compact('bills'));
+    }
+
+    public function unacknowledgedCancelledBills(Request $request)
+    {
+        $bills = Bill::where('hcp_pro_id', $this->performer()->pro->id)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+            ->orderBy('created_at', 'desc')
+            ->get();
+        return view('app.practice-management.unacknowledged-cancelled-bills', compact('bills'));
+    }
+
+    public function myTickets(Request $request, $filter = 'open')
+    {
+        $performer = $this->performer();
+        $myTickets = Ticket::where(function ($q) use ($performer) {
+            $q->where('assigned_pro_id', $performer->pro_id)
+                ->orWhere('manager_pro_id', $performer->pro_id)
+                ->orWhere('ordering_pro_id', $performer->pro_id)
+                ->orWhere('initiating_pro_id', $performer->pro_id);
+        });
+        if ($filter === 'open') {
+            $myTickets = $myTickets->where('is_open', true);
+        } else if ($filter === 'closed') {
+            $myTickets = $myTickets->where('is_open', false);
+        }
+        $myTickets = $myTickets->orderBy('created_at', 'desc')->get();
+        return view('app.practice-management.my-tickets', compact('myTickets', 'filter'));
+    }
+
+    public function myTextShortcuts(Request $request)
+    {
+
+        $personalShortcuts = DB::table('pro_text_shortcut')
+            ->leftJoin('pro', 'pro_text_shortcut.pro_id', '=', 'pro.id')
+            ->select(
+                'pro_text_shortcut.uid',
+                'pro_text_shortcut.shortcut',
+                'pro_text_shortcut.text',
+                'pro.name_first',
+                'pro.name_last'
+            )
+            ->where('pro_text_shortcut.is_removed', false);
+
+        if($this->performer()->pro->pro_type !== 'ADMIN') {
+            $personalShortcuts = $personalShortcuts->where('pro_id', $this->performer()->pro_id);
+        }
+
+        $personalShortcuts = $personalShortcuts
+            ->orderBy('pro.name_last')
+            ->orderBy('pro.name_first')
+            ->orderBy('pro_text_shortcut.shortcut')
+            ->get();
+
+        $globalTextShortcuts = DB::table('pro_text_shortcut')
+            ->select(
+                'pro_text_shortcut.uid',
+                'pro_text_shortcut.shortcut',
+                'pro_text_shortcut.text'
+            )
+            ->whereNull('pro_id')
+            ->where('pro_text_shortcut.is_removed', false)
+            ->orderBy('pro_text_shortcut.shortcut')
+            ->get();
+
+        return view('app.practice-management.my-text-shortcuts', compact('personalShortcuts', 'globalTextShortcuts'));
+    }
+
+    public function myFavorites(Request $request, $filter = 'all')
+    {
+        $performer = $this->performer();
+        $myFavorites = ProFavorite::where('pro_id', $performer->pro_id)->where('is_removed', false);
+        if ($filter !== 'all') {
+            $myFavorites = $myFavorites->where('category', $filter);
+        }
+        $myFavorites = $myFavorites
+            ->whereIn('category', ['allergy', 'medication', 'problem'])
+            ->orderBy('category', 'asc')
+            ->orderBy('position_index', 'asc')
+            ->get();
+        return view('app.practice-management.my-favorites', compact('myFavorites', 'filter'));
+    }
+
+    public function patientsWithoutCoverage(Request $request, $filter = 'all')
+    {
+        $performer = $this->performer();
+
+        $sql = "SELECT cl.uid, cl.name_first, cl.name_last FROM client cl ";
+
+        $joinClause = 'LEFT JOIN client_primary_coverage cpc ON cl.latest_client_primary_coverage_id = cpc.id ';
+        $withoutCondition = "cl.latest_client_primary_coverage_id IS NULL";
+        $pendingCondition = "
+(cl.latest_client_primary_coverage_id IS NOT NULL -- coverage exists, but status is null or unknown
+    AND (
+               (cpc.plan_type = 'MEDICARE' AND (cpc.is_partbprimary = 'UNKNOWN' OR cpc.is_partbprimary IS NULL))
+               OR
+               (cpc.plan_type != 'MEDICARE' AND
+                (cpc.manual_determination_category = 'UNKNOWN' OR cpc.manual_determination_category IS NULL))
+           ))";
+
+        switch ($filter) {
+            case 'without-coverage-information':
+                $sql .= 'WHERE cl.shadow_pro_id IS NULL AND ';
+                $sql .= $withoutCondition;
+                break;
+            case 'pending-coverage-verification':
+                $sql .= $joinClause . 'WHERE cl.shadow_pro_id IS NULL AND ';
+                $sql .= $pendingCondition;
+                break;
+            default:
+                $sql .= $joinClause . 'WHERE cl.shadow_pro_id IS NULL AND ';
+                $sql .= '(' . $withoutCondition . ' OR ' . $pendingCondition . ')';
+                break;
+        }
+
+        $sql .= ' ORDER BY cl.name_first ASC';
+
+        $pendingCoverage = DB::select(DB::raw($sql));
+
+        return view('app.practice-management.patients-without-coverage', compact('pendingCoverage', 'filter'));
+    }
+
+    public function proAvailability(Request $request, $proUid = null)
+    {
+        $performer = $this->performer();
+        $pro = $performer->pro;
+
+        if ($proUid) {
+            $pro = Pro::where('uid', $proUid)->first();
+        }
+
+        if ($request->get('pro_uid')) {
+            $proUid = $request->get('pro_uid');
+            $pro = Pro::where('uid', $proUid)->first();
+        }
+
+        $selectedProUid = $pro->uid;
+
+        $pros = $this->pros;
+
+        $generalAvailabilitiesList = ProGeneralAvailability::where('pro_id', $pro->id)->where('is_cancelled', false)->orderBy('created_at', 'asc')->get();
+        $generalAvailabilities = [
+            'MONDAY' => [],
+            'TUESDAY' => [],
+            'WEDNESDAY' => [],
+            'THURSDAY' => [],
+            'FRIDAY' => [],
+            'SATURDAY' => [],
+            'SUNDAY' => [],
+        ];
+        foreach ($generalAvailabilitiesList as $ga) {
+            if ($ga->day_of_week == 'MONDAY') {
+                $generalAvailabilities['MONDAY'][] = $ga;
+            }
+            if ($ga->day_of_week == 'TUESDAY') {
+                $generalAvailabilities['TUESDAY'][] = $ga;
+            }
+            if ($ga->day_of_week == 'WEDNESDAY') {
+                $generalAvailabilities['WEDNESDAY'][] = $ga;
+            }
+            if ($ga->day_of_week == 'THURSDAY') {
+                $generalAvailabilities['THURSDAY'][] = $ga;
+            }
+            if ($ga->day_of_week == 'FRIDAY') {
+                $generalAvailabilities['FRIDAY'][] = $ga;
+            }
+            if ($ga->day_of_week == 'SATURDAY') {
+                $generalAvailabilities['SATURDAY'][] = $ga;
+            }
+            if ($ga->day_of_week == 'SUNDAY') {
+                $generalAvailabilities['SUNDAY'][] = $ga;
+            }
+        }
+
+        $specificAvailabilities = ProSpecificAvailability::where('pro_id', $pro->id)->where('is_cancelled', false)->orderBy('start_time')->get();
+        $specificUnavailabilities = ProSpecificUnavailability::where('pro_id', $pro->id)->where('is_cancelled', false)->orderBy('start_time', 'asc')->get();
+
+        //events for the calendar
+        $startDate = date('Y-m-d', strtotime("sunday -1 week"));
+        $endDateTime = new DateTime($startDate);
+        $endDateTime->modify('+6 day');
+        $endDate = $endDateTime->format("Y-m-d");
+
+        $eventsData = $pro->getAvailabilityEvents($startDate, $endDate);
+        $events = json_encode($eventsData);
+
+        return view(
+            'app.practice-management.pro-availability',
+            compact(
+                'pros',
+                'generalAvailabilities',
+                'specificAvailabilities',
+                'specificUnavailabilities',
+                'events',
+                'selectedProUid'
+            )
+        );
+    }
+
+    public function loadAvailability(Request $request, $proUid)
+    {
+        $performer = $this->performer();
+        $pro = $performer->pro;
+        $startDate = $request->get('start');
+        $endDate = $request->get('end');
+
+        $selectedPro = Pro::where('uid', $proUid)->first();
+
+        return $selectedPro->getAvailabilityEvents($startDate, $endDate, $request->input('tz') ? $request->input('tz') : 'EASTERN');
+    }
+
+    public function proAvailabilityFilter(Request $request)
+    {
+        $proUid = $request->get('proUid');
+        return ['success' => true, 'data' => $proUid];
+    }
+
+    // video call page (RHS)
+    // generic call handle (no uid)
+    // specific call handle (uid of client)
+    public function meet(Request $request, $uid = false)
+    {
+        $session = AppSession::where('session_key', $request->cookie('sessionKey'))->first();
+        $client = !empty($uid) ? Client::where('uid', $uid)->first() : null;
+        if (!empty($client)) {
+            return view('app.video.call-minimal', compact('session', 'client'));
+        }
+        return view('app.video.call-agora-v2', compact('session', 'client'));
+    }
+
+    // check video page
+    public function checkVideo(Request $request, $uid)
+    {
+        $session = AppSession::where('session_key', $request->cookie('sessionKey'))->first();
+        $client = !empty($uid) ? Client::where('uid', $uid)->first() : null;
+        $publish = false;
+        return view('app.video.check-video-minimal', compact('session', 'client'));
+    }
+
+    public function getParticipantInfo(Request $request)
+    {
+        $sid = intval($request->get('uid')) - 1000000;
+        $session = AppSession::where('id', $sid)->first();
+        $result = [
+            "type" => '',
+            "name" => ''
+        ];
+        if ($session) {
+            $result["type"] = $session->session_type;
+            switch ($session->session_type) {
+                case 'PRO':
+                    $pro = Pro::where('id', $session->pro_id)->first();
+                    $result["name"] = $pro->displayName();
+                    break;
+                case 'CLIENT':
+                    $client = Client::where('id', $session->client_id)->first();
+                    $result["name"] = $client->displayName();
+                    break;
+            }
+        }
+        return json_encode($result);
+    }
+
+    // ajax ep used by the video page
+    // this is needed bcoz meet() is used not
+    // just for the client passed to the view
+    public function getOpentokSessionKey(Request $request, $uid)
+    {
+        $client = Client::where('uid', $uid)->first();
+        return json_encode(["data" => $client ? $client->opentok_session_id : '']);
+    }
+
+    // poll to check if there are patients with active mcp requests
+    public function getPatientsInQueue(Request $request)
+    {
+
+        $myInitiatives = $this->performer->pro->initiatives;
+        if ($myInitiatives) {
+            $myInitiatives = strtoupper($myInitiatives);
+        }
+        $myInitiativesList = explode('|', $myInitiatives);
+
+        $myForeignLanguages = $this->performer->pro->foreign_languages;
+        if ($myForeignLanguages) {
+            $myForeignLanguages = strtoupper($myForeignLanguages);
+        }
+        $myForeignLanguagesList = explode('|', $myForeignLanguages);
+
+        $clients = Client::whereNotNull('active_mcp_request_id')->where(function ($query) use ($myInitiativesList) {
+            $query->whereNull('initiative')->orWhereIn('initiative', $myInitiativesList);
+        })
+            ->where(function ($query) use ($myForeignLanguagesList) {
+                $query->whereNull('preferred_foreign_language')->orWhereIn('preferred_foreign_language', $myForeignLanguagesList);
+            })->limit(3)->get();
+        $results = [];
+
+
+        foreach ($clients as $client) {
+            $results[] = [
+                'clientUid' => $client->uid,
+                'name' => $client->displayName(),
+                'initials' => substr($client->name_first, 0, 1) . substr($client->name_last, 0, 1)
+            ];
+        }
+
+        return json_encode($results);
+
+    }
+
+    public function currentWork(Request $request)
+    {
+        return view('app/current-work');
+    }
+
+    public function calendar(Request $request, $proUid = null)
+    {
+        $pros = Pro::all();
+        if ($this->pro && $this->pro->pro_type != 'ADMIN') {
+            $accessiblePros = ProProAccess::where('owner_pro_id', $this->pro->id);
+            $accessibleProIds = [];
+            foreach ($accessiblePros as $accessiblePro) {
+                $accessibleProIds[] = $accessiblePro->id;
+            }
+            $accessibleProIds[] = $this->pro->id;
+            $pros = Pro::whereIn('id', $accessibleProIds)->get();
+        }
+        return view('app.practice-management.calendar', compact('pros'));
+    }
+
+    public function cellularDeviceManager(Request $request, $proUid = null)
+    {
+        $proUid = $proUid ? $proUid : $request->get('pro-uid');
+        $performerPro = $this->performer->pro;
+        $targetPro = null;
+        $expectedForHcp = null;
+        if ($performerPro->pro_type == 'ADMIN') {
+            $targetPro = Pro::where('uid', $proUid)->first();
+        } else {
+            $targetPro = $performerPro;
+        }
+        $clients = [];
+        if ($targetPro) {
+            $clients = Client::where('mcp_pro_id', $targetPro->id)->orderBy('created_at', 'desc')->paginate(20);
+        } else {
+            $clients = Client::orderBy('created_at', 'desc')->paginate(20);
+        }
+        return view('app.practice-management.cellular-device-manager', compact('clients', 'targetPro', 'proUid'));
+    }
+
+    public function treatmentServiceUtil(Request $request)
+    {
+        $view_treatment_service_utilization_org = DB::select(DB::raw("SELECT * FROM view_treatment_service_utilization_org ORDER BY effective_date DESC"));
+        $view_treatment_service_utilization = DB::select(DB::raw("SELECT * FROM view_treatment_service_utilization ORDER BY effective_date DESC, total_hrs DESC"));
+        $view_treatment_service_utilization_by_patient = DB::select(DB::raw("SELECT * FROM view_treatment_service_utilization_by_patient ORDER BY pro_lname ASC, pro_fname ASC, hcp_pro_id ASC, total_hrs DESC"));
+        return view('app.practice-management.treatment-services-util', compact(
+            'view_treatment_service_utilization_org',
+            'view_treatment_service_utilization',
+            'view_treatment_service_utilization_by_patient'));
+    }
+
+    public function processingBillMatrix(Request $request, $proUid = null)
+    {
+        $proUid = $proUid ? $proUid : $request->get('pro-uid');
+        $performerPro = $this->performer->pro;
+        $targetPro = null;
+        if ($performerPro->pro_type == 'ADMIN') {
+            $targetPro = Pro::where('uid', $proUid)->first();
+        } else {
+            $targetPro = $performerPro;
+        }
+
+        $bills = Bill::where('is_cancelled', false);
+
+        if(!$request->input('t') || $request->input('t') === 'hcp') {
+            $bills = $bills
+                ->where('has_hcp_been_paid', false)
+                ->where('hcp_expected_payment_amount', '>', 0)
+                ->where('is_signed_by_hcp', true);
+            if ($targetPro) {
+                $bills = $bills->where('hcp_pro_id', $targetPro->id);
+            }
+            if($request->input('c')) {
+                $bills = $bills->whereRaw('(SELECT company_id from company_pro where id = hcp_company_pro_id) = ' . $request->input('c'));
+            }
+        }
+        else if($request->input('t') === 'na') {
+            $bills = $bills
+                ->where('has_generic_pro_been_paid', false)
+                ->where('generic_pro_expected_payment_amount', '>', 0)
+                ->where('is_signed_by_generic_pro', true);
+            if ($targetPro) {
+                $bills = $bills->where('generic_pro_id', $targetPro->id);
+            }
+            if($request->input('c')) {
+                $bills = $bills->whereRaw('(SELECT company_id from company_pro where id = generic_company_pro_id) = ' . $request->input('c'));
+            }
+        }
+
+        $filter = $request->input('f');
+        switch ($filter) {
+            case 'verified':
+                $bills = $bills->where('is_verified', true);
+                break;
+            case 'not-verified':
+                $bills = $bills->where('is_verified', false);
+                break;
+        }
+
+        $filter = $request->input('bs');
+        if ($filter) {
+            $bills = $bills->where('balance_post_date', '>=', $filter);
+        }
+
+        $filter = $request->input('be');
+        if ($filter) {
+            $bills = $bills->where('balance_post_date', '<=', $filter);
+        }
+
+        $filter = $request->input('s');
+        if ($filter) {
+            $bills = $bills->where('code', '=', $filter);
+        }
+
+        $bills = $bills->orderBy('effective_date', 'desc')->paginate();
+
+        $companies = Company::where('is_active', true)->orderBy('name')->get();
+
+        $codes = DB::select(DB::raw("SELECT code, count(*) as count FROM bill WHERE is_cancelled IS FALSE GROUP BY code ORDER BY code"));
+
+        $viewData = [
+            'bills' => $bills,
+            'targetPro' => $targetPro,
+            'performerPro' => $performerPro,
+            'proUid' => $proUid,
+            'companies' => $companies,
+            'codes' => $codes
+        ];
+        return view('app.practice-management.processing-bill-matrix', $viewData);
+    }
+
+    public function hcpNoteActivity(Request $request)
+    {
+
+        $filters = $request->all();
+
+        $from_date = $request->get('from_date');
+//        if(!$from_date) {
+//            $from_date = '2010-01-01';
+//        }
+        $to_date = $request->get('to_date');
+//        if(!$to_date){
+//            $to_date = '2900-12-31';
+//        }
+        
+
+        //TODO sanitize inputs
+
+        $proUid =  $request->get('pro-uid');
+
+        $now = Carbon::now();
+        $startOfCurrentMonth = $now->startOfMonth('Y-m-d');
+        $startOfCurrentMonth = $startOfCurrentMonth->toDateString();
+
+        $endOfCurrentMonth  = $now->endOfMonth()->format('Y-m-d');
+
+        $rows = DB::table('pro')
+        ->selectRaw(" *,
+            (SELECT id FROM note WHERE note.hcp_pro_id = pro.id AND is_cancelled = false ORDER BY created_at DESC LIMIT 1) as last_note_id, 
+            (SELECT note.created_at FROM note WHERE note.hcp_pro_id = pro.id AND note.is_cancelled = false ORDER BY note.created_at DESC LIMIT 1) as last_note_created_at,
+            (SELECT COUNT(*) FROM note WHERE note.hcp_pro_id = pro.id AND note.is_cancelled = false AND created_at::DATE >= '${startOfCurrentMonth}'::DATE AND created_at::DATE <= '${endOfCurrentMonth}'::DATE ) AS notes_this_month,
+            (SELECT COUNT(*) FROM client WHERE client.mcp_pro_id = pro.id AND client.client_engagement_status_category <> 'DUMMY' AND client.client_engagement_status_category <> 'DUPLICATE') as total_client_count,
+            (SELECT COUNT(*) FROM client WHERE client.mcp_pro_id = pro.id AND client.client_engagement_status_category <> 'DUMMY' AND client.client_engagement_status_category <> 'DUPLICATE' AND client.most_recent_completed_mcp_note_date >= (NOW() - interval '60 days')::DATE) as active_client_count
+            " . (!!$from_date && !!$to_date ? ",(SELECT COUNT(*) FROM note WHERE note.hcp_pro_id = pro.id AND note.is_cancelled = false AND created_at::DATE >= '${from_date}'::DATE AND created_at::DATE <= '${to_date}'::DATE  ) AS notes_this_period" : ""));
+
+        $prosIDs = $request->get('pros_ids');
+        if($prosIDs){
+            $pros = 
+            $rows = $rows->whereIn('pro.id', $prosIDs);
+        }
+
+        $sortBy = $request->input('sort_by') ?: 'name_first';
+        $sortDir = $request->input('sort_dir') ?: 'ASC';
+
+        $rows = $rows->orderByRaw("$sortBy $sortDir NULLS LAST");
+
+        $rows = $rows->paginate(50);
+
+
+        return view('app.practice-management.hcp-note-activity', compact('rows', 'filters'));
+    }
+
+
+    public function proFinancials(Request $request, $proUid = null)
+    {
+        $proUid = $proUid ? $proUid : $request->get('pro-uid');
+        $performerPro = $this->performer->pro;
+        $targetPro = null;
+        if ($performerPro->pro_type == 'ADMIN') {
+            $targetPro = Pro::where('uid', $proUid)->first();
+        } else {
+            $targetPro = $performerPro;
+        }
+        $fPros = Pro::whereNotNull('balance');
+        if($targetPro) {
+            $fPros = $fPros->where('uid', $targetPro->uid);
+        }
+        $fPros = $fPros
+            ->where('balance', '>', 0)
+            ->orderBy('name_last')
+            ->orderBy('name_first')
+            ->paginate();
+        return view('app.practice-management.pro-financials', compact('fPros', 'targetPro'));
+    }
+
+    public function hcpBillMatrix(Request $request, $proUid = null)
+    {
+        $proUid = $proUid ? $proUid : $request->get('pro-uid');
+        $performerPro = $this->performer->pro;
+        $targetPro = null;
+        $allPros = [];
+        $expectedForHcp = null;
+
+        if ($performerPro->pro_type == 'ADMIN') {
+            $allPros = Pro::all();
+            $targetPro = Pro::where('uid', $proUid)->first();
+        } else {
+            $targetPro = $performerPro;
+        }
+        $rows = [];
+        if ($targetPro) {
+            $rows = DB::select(DB::raw("SELECT * FROM aemish_bill_report WHERE hcp_pro_id = :targetProID"), ['targetProID' => $targetPro->id]);
+        } else {
+            $rows = DB::select(DB::raw("SELECT * FROM aemish_bill_report"));
+        }
+        return view('app.practice-management.hcp-bill-matrix', compact('rows', 'allPros', 'expectedForHcp', 'targetPro', 'proUid'));
+    }
+
+    public function billingManager(Request $request, $proUid = null)
+    {
+        $proUid = $proUid ? $proUid : $request->get('pro-uid');
+        $performerPro = $this->performer->pro;
+        $targetPro = null;
+        $allPros = [];
+        $expectedForHcp = null;
+        if ($performerPro->pro_type == 'ADMIN') {
+            $allPros = Pro::all();
+            $targetPro = Pro::where('uid', $proUid)->first();
+        } else {
+            $targetPro = $performerPro;
+        }
+        $notes = [];
+        if ($targetPro) {
+            $expectedForHcp = DB::select(DB::raw("SELECT coalesce(SUM(hcp_expected_payment_amount),0) as expected_pay FROM bill WHERE hcp_pro_id = :targetProID  AND is_signed_by_hcp IS TRUE AND is_cancelled = false"), ['targetProID' => $targetPro->id])[0]->expected_pay;
+            $notes = Note::where('hcp_pro_id', $targetPro->id);
+        } else {
+            $notes = Note::where('id', '>', 0);
+        }
+
+        if($request->input('date')) {
+            $notes = $notes->where('effective_dateest', $request->input('date'));
+        }
+
+        $filters = [];
+        $filters['bills_created'] = $request->input('bills_created');
+        $filters['is_billing_marked_done'] = $request->input('is_billing_marked_done');
+        $filters['bills_resolved'] = $request->input('bills_resolved');
+        $filters['bills_closed'] = $request->input('bills_closed');
+        $filters['claims_created'] = $request->input('claims_created');
+        $filters['claims_closed'] = $request->input('claims_closed');
+
+        if ($filters['bills_created']) {
+            $notes->where(
+                'bill_total_expected',
+                ($filters['bills_created'] === 'yes' ? '>' : '<='),
+                0);
+        }
+
+        if ($filters['is_billing_marked_done']) {
+            $notes->where(
+                'is_billing_marked_done',
+                ($filters['is_billing_marked_done'] === 'yes' ? '=' : '!='),
+                true);
+        }
+
+        if ($filters['bills_resolved']) {
+            $notes->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id) > 0'); // have bills
+            if ($filters['bills_resolved'] === 'yes') {
+                $notes->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND (is_cancelled = false  AND is_verified = false) OR (is_cancelled = TRUE AND is_cancellation_acknowledged = FALSE)) > 0');
+            } elseif ($filters['bills_resolved'] === 'no') {
+                $notes->whereRaw('(SELECT count(id) FROM bill WHERE note_id = note.id AND ((is_cancelled = true AND is_cancellation_acknowledged = true) OR is_verified = true)) = 0');
+            }
+        }
+
+        if ($filters['bills_closed']) {
+            $notes->where(
+                'is_bill_closed',
+                ($filters['bills_closed'] === 'yes' ? '=' : '!='),
+                true);
+        }
+
+        if ($filters['claims_created']) {
+            $notes->where(
+                'claim_total_expected',
+                ($filters['claims_created'] === 'yes' ? '>' : '<='),
+                0);
+        }
+
+        if ($filters['claims_closed']) {
+            $notes->where(
+                'is_claim_closed',
+                ($filters['claims_closed'] === 'yes' ? '=' : '!='),
+                true);
+        }
+
+        $notes = $notes->orderBy('effective_dateest', 'desc')->paginate(20);
+
+        return view('app.practice-management.billing-manager', compact('notes', 'allPros', 'expectedForHcp', 'targetPro', 'proUid', 'filters'));
+    }
+
+    public function remoteMonitoring(Request $request) {
+
+        $performer = $this->performer();
+
+        abort_if($performer->pro->pro_type !== 'ADMIN' && !$performer->pro->can_view_rm_matrix, 403);
+
+        $ym = ($request->input('y') ?: 'Y') . '-' . ($request->input('m') ?: 'm');
+        $careMonthStart = date($ym . '-01');
+
+        $rc = $request->input('rc') ?: 1;
+        $rc2 = $request->input('rc2') ?: 2;
+
+        $conditions = $this->rpmConditions($performer, $rc, $rc2);
+
+        $patients = DB::select(
+            DB::raw(
+                "
+SELECT client.name_first, 
+       client.name_last, 
+       client.uid as client_uid, 
+       client.dob,
+       client.is_enrolled_in_rm,
+       client.most_recent_completed_mcp_note_date,
+       care_month.uid as care_month_uid,
+       care_month.id as care_month_id,
+       care_month.start_date,
+       care_month.rm_total_time_in_seconds_by_mcp,
+       care_month.number_of_days_with_remote_measurements,
+       care_month.has_anyone_interacted_with_client_about_rm_outside_note,
+       care_month.rm_num_measurements_not_stamped_by_mcp,
+       care_month.rm_num_measurements_not_stamped_by_non_hcp,
+       care_month.rm_num_measurements_not_stamped_by_rmm,
+       care_month.rm_num_measurements_not_stamped_by_rme,
+       client.mcp_pro_id,
+       client.default_na_pro_id,
+       client.rmm_pro_id,
+       client.rme_pro_id,
+       client.cell_number,
+       client.most_recent_cellular_bp_dbp_mm_hg,
+       client.most_recent_cellular_bp_sbp_mm_hg,
+       client.most_recent_cellular_bp_measurement_at,      
+       client.most_recent_cellular_weight_value,
+       client.most_recent_cellular_weight_measurement_at
+FROM care_month join client on care_month.client_id = client.id
+WHERE
+      client.mcp_pro_id = {$performer->pro->id}
+      AND EXTRACT(MONTH from care_month.start_date) = " . ($request->input('m') ?: 'EXTRACT(MONTH from now())') . "
+      AND EXTRACT(YEAR from care_month.start_date) = " . ($request->input('y') ?: 'EXTRACT(YEAR from now())') . "
+      " . (count($conditions) > 0 ? 'AND ' . implode(" AND ", $conditions) : '') . "
+ORDER BY care_month.number_of_days_with_remote_measurements DESC NULLS LAST, client.name_first, client.name_last
+"
+            )
+        );
+
+        $timestamp = strtotime(date('Y-m-d'));
+        $daysRemaining = (int)date('t', $timestamp) - (int)date('j', $timestamp);
+
+        return view('app.practice-management.remote-monitoring', compact('patients', 'daysRemaining', 'careMonthStart'));
+    }
+
+    public function remoteMonitoringCount(Request $request) {
+
+        $performer = $this->performer();
+
+        $ym = ($request->input('y') ?: 'Y') . '-' . ($request->input('m') ?: 'm');
+        $careMonthStart = date($ym . '-01');
+
+        $rc = $request->input('rc') ?: 1;
+        $rc2 = $request->input('rc2') ?: 2;
+
+        $conditions = $this->rpmConditions($performer, $rc, $rc2);
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(*)
+FROM care_month join client on care_month.client_id = client.id
+WHERE
+      client.mcp_pro_id = {$performer->pro->id}
+      AND EXTRACT(MONTH from care_month.start_date) = " . ($request->input('m') ?: 'EXTRACT(MONTH from now())') . "
+      AND EXTRACT(YEAR from care_month.start_date) = " . ($request->input('y') ?: 'EXTRACT(YEAR from now())') . "
+      " . (count($conditions) > 0 ? 'AND ' . implode(" AND ", $conditions) : '')
+            )
+        );
+
+        return $count[0]->count;
+    }
+
+    public function remoteMonitoringAdmin(Request $request) {
+
+        $ym = ($request->input('y') ?: 'Y') . '-' . ($request->input('m') ?: 'm');
+        $careMonthStart = date($ym . '-01');
+
+        $rc = $request->input('rc') ?: 1;
+        $rc2 = $request->input('rc2') ?: 2;
+
+        $conditions = $this->rpmConditionsAdmin($this->performer(), $rc, $rc2);
+
+        $patients = DB::select(
+            DB::raw(
+                "
+SELECT client.name_first, 
+       client.name_last, 
+       client.uid as client_uid, 
+       client.dob,
+       client.is_enrolled_in_rm,
+       client.most_recent_completed_mcp_note_date,
+       care_month.uid as care_month_uid,
+       care_month.id as care_month_id,
+       care_month.start_date,
+       care_month.rm_total_time_in_seconds_by_mcp,
+       care_month.number_of_days_with_remote_measurements,
+       care_month.has_anyone_interacted_with_client_about_rm_outside_note,
+       care_month.rm_num_measurements_not_stamped_by_mcp,
+       care_month.rm_num_measurements_not_stamped_by_non_hcp,
+       care_month.rm_num_measurements_not_stamped_by_rmm,
+       care_month.rm_num_measurements_not_stamped_by_rme,
+       client.mcp_pro_id,
+       client.default_na_pro_id,
+       client.rmm_pro_id,
+       client.rme_pro_id,
+       client.cell_number,
+       client.most_recent_cellular_bp_dbp_mm_hg,
+       client.most_recent_cellular_bp_sbp_mm_hg,
+       client.most_recent_cellular_bp_measurement_at,      
+       client.most_recent_cellular_weight_value,
+       client.most_recent_cellular_weight_measurement_at
+FROM care_month join client on care_month.client_id = client.id
+WHERE
+      client.shadow_pro_id is null AND client.is_enrolled_in_rm = 'YES'
+      AND EXTRACT(MONTH from care_month.start_date) = " . ($request->input('m') ?: 'EXTRACT(MONTH from now())') . "
+      AND EXTRACT(YEAR from care_month.start_date) = " . ($request->input('y') ?: 'EXTRACT(YEAR from now())') . "
+      " . (count($conditions) > 0 ? 'AND ' . implode(" AND ", $conditions) : '') . "
+ORDER BY care_month.number_of_days_with_remote_measurements DESC NULLS LAST, client.name_first, client.name_last
+"
+            )
+        );
+
+        $timestamp = strtotime(date('Y-m-d'));
+        $daysRemaining = (int)date('t', $timestamp) - (int)date('j', $timestamp);
+
+        return view('app.practice-management.remote-monitoring-admin', compact('patients', 'daysRemaining', 'careMonthStart'));
+    }
+
+    public function remoteMonitoringAdminCount(Request $request) {
+
+        $ym = ($request->input('y') ?: 'Y') . '-' . ($request->input('m') ?: 'm');
+        $careMonthStart = date($ym . '-01');
+
+        $rc = $request->input('rc') ?: 1;
+        $rc2 = $request->input('rc2') ?: 2;
+
+        $conditions = $this->rpmConditionsAdmin($this->performer(), $rc, $rc2);
+
+        $count = DB::select(
+            DB::raw(
+                "
+SELECT count(*)
+FROM care_month join client on care_month.client_id = client.id
+WHERE
+      client.shadow_pro_id is null AND client.is_enrolled_in_rm = 'YES'
+      AND EXTRACT(MONTH from care_month.start_date) = " . ($request->input('m') ?: 'EXTRACT(MONTH from now())') . "
+      AND EXTRACT(YEAR from care_month.start_date) = " . ($request->input('y') ?: 'EXTRACT(YEAR from now())') . "
+      " . (count($conditions) > 0 ? 'AND ' . implode(" AND ", $conditions) : '')
+            )
+        );
+
+        return $count[0]->count;
+    }
+
+    private function rpmConditions($performer, $rc, $rc2) {
+        $conditions = [];
+
+        $c_isMCP = "client.mcp_pro_id = {$performer->pro->id}";
+        $c_enrolledInRPM = "client.is_enrolled_in_rm = 'YES'";
+        $c_hasDevice = "(SELECT COUNT(client_bdt_device.id) FROM client_bdt_device JOIN bdt_device bd on client_bdt_device.device_id = bd.id WHERE client_bdt_device.client_id = client.id) > 0";
+        $c_lastVisitBefore90Days = "DATE_PART('day', care_month.start_date::timestamp - client.most_recent_completed_mcp_note_date::timestamp) > 90";
+        $c_lastVisitWithin90Days = "DATE_PART('day', care_month.start_date::timestamp - client.most_recent_completed_mcp_note_date::timestamp) <= 90";
+        $c_notSpokenToThisMonth = "(care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NULL OR care_month.has_anyone_interacted_with_client_about_rm_outside_note = FALSE)";
+        $c_spokenToThisMonth = "care_month.has_anyone_interacted_with_client_about_rm_outside_note = TRUE";
+        $c_hasUnstamped = "care_month.rm_num_measurements_not_stamped_by_mcp > 0";
+        $c_hasNoUnstamped = "care_month.rm_num_measurements_not_stamped_by_mcp = 0";
+        $c_lt16MeasurementDays = "care_month.number_of_days_with_remote_measurements < 16";
+        $c_gte16MeasurementDays = "care_month.number_of_days_with_remote_measurements >= 16";
+        $c_subscribedToSMS = "client.send_sms_on_bdt_measurement = TRUE";
+        $c_deviceUsed = "(client.most_recent_cellular_bp_measurement_at IS NOT NULL OR most_recent_cellular_weight_measurement_at IS NOT NULL)";
+        $c_lt20BillingMinutes = "care_month.rm_total_time_in_seconds_by_mcp < 1200";
+        $c_gte20BillingMinutes = "care_month.rm_total_time_in_seconds_by_mcp >= 1200";
+
+        switch ($rc) {
+            case 1:
+                $conditions = [$c_isMCP];
+                break;
+            case 2:
+                $conditions = [$c_isMCP, $c_enrolledInRPM];
+                break;
+            case 3:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_hasDevice];
+                break;
+            case 4:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_hasDevice, ($rc2 == 1 ? $c_lastVisitBefore90Days : $c_lastVisitWithin90Days)];
+                break;
+            case 5:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, ($rc2 == 1 ? $c_notSpokenToThisMonth : $c_spokenToThisMonth)];
+                break;
+            case 6:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, $c_spokenToThisMonth, ($rc2 == 1 ? $c_hasUnstamped : $c_hasNoUnstamped)];
+                break;
+            case 7:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, $c_spokenToThisMonth, ($rc2 == 1 ? $c_lt16MeasurementDays : $c_gte16MeasurementDays)];
+                break;
+            case 10:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, $c_spokenToThisMonth, $c_gte16MeasurementDays, ($rc2 == 1 ? $c_lt20BillingMinutes : $c_gte20BillingMinutes)];
+                break;
+            case 8:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_subscribedToSMS];
+                break;
+            case 9:
+                $conditions = [$c_isMCP, $c_enrolledInRPM, $c_deviceUsed];
+                break;
+        }
+
+        return $conditions;
+    }
+
+    private function rpmConditionsAdmin($performer, $rc, $rc2) {
+        $conditions = [];
+
+        $c_enrolledInRPM = "client.is_enrolled_in_rm = 'YES'";
+        $c_hasDevice = "(SELECT COUNT(client_bdt_device.id) FROM client_bdt_device JOIN bdt_device bd on client_bdt_device.device_id = bd.id WHERE client_bdt_device.client_id = client.id) > 0";
+        $c_lastVisitBefore90Days = "DATE_PART('day', care_month.start_date::timestamp - client.most_recent_completed_mcp_note_date::timestamp) > 90";
+        $c_lastVisitWithin90Days = "DATE_PART('day', care_month.start_date::timestamp - client.most_recent_completed_mcp_note_date::timestamp) <= 90";
+        $c_notSpokenToThisMonth = "(care_month.has_anyone_interacted_with_client_about_rm_outside_note IS NULL OR care_month.has_anyone_interacted_with_client_about_rm_outside_note = FALSE)";
+        $c_spokenToThisMonth = "care_month.has_anyone_interacted_with_client_about_rm_outside_note = TRUE";
+        $c_hasUnstamped = "care_month.rm_num_measurements_not_stamped_by_mcp > 0";
+        $c_hasNoUnstamped = "care_month.rm_num_measurements_not_stamped_by_mcp = 0";
+        $c_lt16MeasurementDays = "care_month.number_of_days_with_remote_measurements < 16";
+        $c_gte16MeasurementDays = "care_month.number_of_days_with_remote_measurements >= 16";
+        $c_subscribedToSMS = "client.send_sms_on_bdt_measurement = TRUE";
+        $c_deviceUsed = "(client.most_recent_cellular_bp_measurement_at IS NOT NULL OR most_recent_cellular_weight_measurement_at IS NOT NULL)";
+        $c_lt20BillingMinutes = "care_month.rm_total_time_in_seconds_by_mcp < 1200";
+        $c_gte20BillingMinutes = "care_month.rm_total_time_in_seconds_by_mcp >= 1200";
+
+        switch ($rc) {
+            case 2:
+                $conditions = [$c_enrolledInRPM];
+                break;
+            case 3:
+                $conditions = [$c_enrolledInRPM, $c_hasDevice];
+                break;
+            case 4:
+                $conditions = [$c_enrolledInRPM, $c_hasDevice, ($rc2 == 1 ? $c_lastVisitBefore90Days : $c_lastVisitWithin90Days)];
+                break;
+            case 5:
+                $conditions = [$c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, ($rc2 == 1 ? $c_notSpokenToThisMonth : $c_spokenToThisMonth)];
+                break;
+            case 6:
+                $conditions = [$c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, $c_spokenToThisMonth, ($rc2 == 1 ? $c_hasUnstamped : $c_hasNoUnstamped)];
+                break;
+            case 7:
+                $conditions = [$c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, $c_spokenToThisMonth, ($rc2 == 1 ? $c_lt16MeasurementDays : $c_gte16MeasurementDays)];
+                break;
+            case 10:
+                $conditions = [$c_enrolledInRPM, $c_hasDevice, $c_lastVisitWithin90Days, $c_spokenToThisMonth, $c_gte16MeasurementDays, ($rc2 == 1 ? $c_lt20BillingMinutes : $c_gte20BillingMinutes)];
+                break;
+            case 8:
+                $conditions = [$c_enrolledInRPM, $c_subscribedToSMS];
+                break;
+            case 9:
+                $conditions = [$c_enrolledInRPM, $c_deviceUsed];
+                break;
+        }
+
+        return $conditions;
+    }
+
+    public function remoteMonitoringMeasurements(Request $request, CareMonth $careMonth) {
+
+        $performer = $this->performer();
+
+        $measurements = DB::select(
+            DB::raw(
+                "
+SELECT measurement.label,
+    measurement.ts,
+    measurement.effective_date,
+    measurement.sbp_mm_hg,
+    measurement.dbp_mm_hg,
+    measurement.value_pulse,
+    measurement.value_irregular,
+    measurement.numeric_value,
+    measurement.value,
+    measurement.uid,
+    client.name_first,
+    client.name_last,
+    client.mcp_pro_id,
+    client.default_na_pro_id,
+    client.rmm_pro_id,
+    client.rme_pro_id
+FROM measurement RIGHT JOIN client on measurement.client_id = client.id
+WHERE
+    client.id = {$careMonth->client_id}
+    AND measurement.label IS NOT NULL
+    AND measurement.label NOT IN ('SBP', 'DBP')
+    AND (measurement.is_cellular_zero = FALSE or measurement.is_cellular_zero IS NULL)
+    AND measurement.is_removed IS FALSE
+    AND measurement.client_bdt_measurement_id IS NOT NULL
+    AND measurement.care_month_id = {$careMonth->id}
+    AND (
+        (client.mcp_pro_id = {$performer->pro->id} AND measurement.has_been_stamped_by_mcp IS FALSE)
+        OR (client.rmm_pro_id = {$performer->pro->id} AND measurement.has_been_stamped_by_rmm IS FALSE)
+        OR (client.rme_pro_id = {$performer->pro->id} AND measurement.has_been_stamped_by_rme IS FALSE)
+        OR (client.default_na_pro_id = {$performer->pro->id} AND measurement.has_been_stamped_by_non_hcp IS FALSE)
+    )
+ORDER BY ts DESC
+"
+            )
+        );
+
+        return view('app.practice-management.remote-monitoring-measurements', compact('careMonth', 'measurements'));
+    }
+
+    public function billMatrix(Request $request)
+    {
+        $bClients = [];
+        $bHCPPros = [];
+        $bNAPros = [];
+
+        $filters = [];
+        $filters['client'] = $request->input('client');
+        $filters['service'] = $request->input('service');
+        $filters['hcp'] = $request->input('hcp');
+        $filters['hcp_paid'] = $request->input('hcp_paid');
+        $filters['expected_op'] = $request->input('expected_op');
+        $filters['expected_value'] = $request->input('expected_value');
+        $filters['paid_op'] = $request->input('paid_op');
+        $filters['paid_value'] = $request->input('paid_value');
+        $filters['bal_post_date_op'] = $request->input('bal_post_date_op');
+        $filters['bal_post_date_value'] = $request->input('bal_post_date_value');
+        $filters['hcp_sign'] = $request->input('hcp_sign');
+        $filters['verified'] = $request->input('verified');
+        $filters['cancelled'] = $request->input('cancelled');
+
+        $bills = Bill::orderBy('effective_date')->paginate();
+        return view('app.practice-management.bill-matrix', compact('bills', 'bClients', 'bHCPPros', 'filters'));
+    }
+
+    public function medicarePartBClaims(Request $request)
+    {
+
+        $medicarePartBOnly = $request->get("medicare_part_b");
+
+        $allClaims = Claim::where('was_submitted', false)->orWhere('was_submitted', null)->orderBy('created_at', 'desc')->get();
+
+        //Only medicare claims
+        $claims = [];
+        foreach ($allClaims as $claim) {
+            if ($claim->client != null && $claim->client->getPrimaryCoverageStatus() == 'YES' && !$claim->edi) {
+                $claims[] = $claim;
+            }
+        }
+
+        $claimEDIs = ClaimEDI::all();
+
+        return view('app.practice-management.medicare-partb-claims', compact('claims', 'claimEDIs'));
+    }
+
+    // Generate PDF
+    public function downloadClaims()
+    {
+        $claims = Claim::where('was_submitted', false)->orWhere('was_submitted', null)->orderBy('created_at', 'desc')->limit(100)->get();
+        view()->share('claims', $claims);
+
+        $pdf = PDF::loadView('app.practice-management.claims-pdf', $claims);
+
+        return $pdf->download('pdf_file.pdf');
+    }
+
+    public function tickets(Request $request, $proUid = null)
+    {
+        $tickets = Ticket::orderBy('created_at', 'desc')->paginate();
+        return view('app.practice-management.tickets', compact('tickets'));
+    }
+
+    public function supplyOrders(Request $request)
+    {
+
+        // counts
+        $counts = $this->getSupplyOrderCounts();
+
+        // so clients
+        $soClientIDs = DB::table('supply_order')->select('client_id')->distinct()->get()->toArray();
+        $soClientIDs = array_map(function ($_x) {
+            return $_x->client_id;
+        }, $soClientIDs);
+        $soClients = Client::whereIn('id', $soClientIDs)->get();
+
+        // so products
+        $soProductIDs = DB::table('supply_order')->select('product_id')->distinct()->get()->toArray();
+        $soProductIDs = array_map(function ($_x) {
+            return $_x->product_id;
+        }, $soProductIDs);
+        $soProducts = Product::whereIn('id', $soProductIDs)->get();
+
+        $filters = [];
+        $filters['client'] = $request->input('client');
+        $filters['product'] = $request->input('product');
+        $filters['reason'] = $request->input('reason');
+        $filters['cu_memo'] = $request->input('cu_memo');
+        $filters['pro_sign'] = $request->input('pro_sign');
+        $filters['client_sign'] = $request->input('client_sign');
+        $filters['shipment'] = $request->input('shipment');
+        $filters['lot_number'] = $request->input('lot_number');
+        $filters['imei'] = $request->input('imei');
+        $filters['cancelled'] = $request->input('cancelled');
+
+        $supplyOrders = SupplyOrder::where('id', '>', 0);
+
+        // apply filters
+        if ($filters['client']) $supplyOrders->where('client_id', $filters['client']);
+        if ($filters['product']) $supplyOrders->where('product_id', $filters['product']);
+        if ($filters['reason']) $supplyOrders->where('reason', 'ILIKE', '%' . $filters['reason'] . '%');
+        if ($filters['cu_memo']) $supplyOrders->where('cu_memo', 'ILIKE', '%' . $filters['cu_memo'] . '%');
+        if ($filters['pro_sign']) $supplyOrders->where('is_signed_by_pro', ($filters['pro_sign'] === 'signed'));
+
+        if ($filters['client_sign']) {
+            if ($filters['client_sign'] === 'signed')
+                $supplyOrders->where('is_signed_by_client', true);
+            elseif ($filters['client_sign'] === 'waived')
+                $supplyOrders->where('is_client_signature_waived', true);
+            else
+                $supplyOrders->where('is_client_signature_waived', false)->where('is_signed_by_client', false);
+        }
+
+        if ($filters['shipment']) {
+            if ($filters['shipment'] === 'not_cleared_for_shipment')
+                $supplyOrders->whereNull('shipment_id')->where('is_cleared_for_shipment', false);
+            elseif ($filters['shipment'] === 'cleared_for_shipment')
+                $supplyOrders->whereNull('shipment_id')->where('is_cleared_for_shipment', true);
+            else
+                $supplyOrders
+                    ->whereNotNull('shipment_id')
+                    ->whereRaw('(SELECT status FROM shipment WHERE id = shipment_id LIMIT 1) = ?', [$filters['shipment']]);
+        }
+
+        if ($filters['lot_number']) $supplyOrders->where('lot_number', 'ILIKE', '%' . $filters['lot_number'] . '%');
+        if ($filters['imei']) $supplyOrders->where('imei', 'ILIKE', '%' . $filters['imei'] . '%');
+        if ($filters['cancelled']) $supplyOrders->where('is_cancelled', ($filters['cancelled'] === 'cancelled'));
+
+        $supplyOrders = $supplyOrders->orderBy('created_at', 'desc')->paginate();
+
+        return view('app.practice-management.supply-orders',
+            compact('supplyOrders', 'filters',
+                'soClients', 'soProducts', 'counts'
+            )
+        );
+    }
+
+    public function shipments(Request $request, $filter = null)
+    {
+
+        // counts
+        $counts = $this->getShipmentCounts();
+
+        // so clients
+        $shClientIDs = DB::table('shipment')->select('client_id')->distinct()->get()->toArray();
+        $shClientIDs = array_map(function ($_x) {
+            return $_x->client_id;
+        }, $shClientIDs);
+        $shClients = Client::whereIn('id', $shClientIDs)->get();
+
+        $shipments = Shipment::where('id', '>', 0);
+
+        $filters = [];
+        $filters['client'] = $request->input('client');
+        $filters['courier'] = $request->input('courier');
+        $filters['tracking_num'] = $request->input('tracking_num');
+        $filters['label'] = $request->input('label');
+        $filters['status'] = $request->input('status');
+        $filters['cancelled'] = $request->input('cancelled');
+
+        if ($filters['client']) $shipments->where('client_id', $filters['client']);
+        if ($filters['courier']) $shipments->where('courier', 'ILIKE', '%' . $filters['courier'] . '%');
+        if ($filters['tracking_num']) $shipments->where('tracking_number', 'ILIKE', '%' . $filters['tracking_num'] . '%');
+        if ($filters['label']) {
+            if ($filters['label'] === 'yes')
+                $shipments->whereNotNull('label_system_file_id');
+            else
+                $shipments->whereNull('label_system_file_id');
+        }
+        if ($filters['status']) $shipments->where('status', $filters['status']);
+        if ($filters['cancelled']) $shipments->where('is_cancelled', ($filters['cancelled'] === 'cancelled'));
+
+        $shipments = $shipments->orderBy('created_at', 'desc')->paginate();
+        return view('app.practice-management.shipments', compact('shipments', 'filters', 'shClients', 'counts'));
+    }
+
+    public function cellularMeasurements(Request $request)
+    {
+        $measurements = Measurement::orderBy('ts', 'desc')->whereNotNull('ts')->paginate();
+        return view('app.practice-management.cellular-measurements', compact('measurements'));
+    }
+
+    // v2 supply-orders & shipments management (wh)
+
+    public function supplyOrdersReadyToShip(Request $request)
+    {
+        $counts = $this->getSupplyOrderCounts();
+        $supplyOrders = SupplyOrder
+            ::where('is_cleared_for_shipment', true)
+            ->where('is_cancelled', false)
+            ->whereNull('shipment_id')
+            ->join('client', 'client.id', '=', 'supply_order.client_id')
+            ->orderBy('client.name_last', 'ASC')
+            ->orderBy('client.name_first', 'ASC')
+            ->orderBy('supply_order.client_id', 'ASC')
+            ->orderBy('supply_order.mailing_address_full', 'ASC')
+            ->orderBy('supply_order.created_at', 'ASC')
+            ->select('supply_order.*')
+            ->paginate();
+        return view('app.practice-management.supply-orders-ready-to-ship', compact('supplyOrders', 'counts'));
+    }
+
+    public function supplyOrdersShipmentUnderway(Request $request)
+    {
+        $counts = $this->getSupplyOrderCounts();
+        $supplyOrders = SupplyOrder
+            ::where('is_cancelled', false)
+            ->whereNotNull('shipment_id')
+            ->orderBy('client_id', 'ASC')
+            ->orderBy('mailing_address_full', 'ASC')
+            ->orderBy('created_at', 'ASC')
+            ->paginate();
+        return view('app.practice-management.supply-orders-shipment-underway', compact('supplyOrders', 'counts'));
+    }
+
+    public function supplyOrdersHanging(Request $request)
+    {
+        $counts = $this->getSupplyOrderCounts();
+        $supplyOrders = SupplyOrder
+            ::select('supply_order.*')
+            ->leftJoin('shipment', function($join) {
+                $join->on('supply_order.shipment_id', '=', 'shipment.id');
+            })
+            ->where('shipment.status', 'CANCELLED')
+            ->where('supply_order.is_cancelled', false)
+            ->orderBy('supply_order.client_id', 'ASC')
+            ->orderBy('supply_order.mailing_address_full', 'ASC')
+            ->orderBy('supply_order.created_at', 'ASC')
+            ->paginate();
+        return view('app.practice-management.supply-orders-hanging', compact('supplyOrders', 'counts'));
+    }
+
+    public function supplyOrdersCancelledButUnacknowledged(Request $request)
+    {
+        $supplyOrders = SupplyOrder::where('signed_by_pro_id', $this->performer()->pro->id)
+            ->where('is_cancelled', true)
+            ->where('is_cancellation_acknowledged', false)
+	        ->orderBy('created_at', 'desc')
+            ->paginate();
+        return view('app.practice-management.supply-orders-cancelled-but-unacknowledged', compact('supplyOrders'));
+    }
+
+    public function supplyOrdersUnsigned(Request $request)
+    {
+        $supplyOrders = SupplyOrder
+            ::where('is_cancelled', false)
+            ->where('is_signed_by_pro', false)
+            ->whereRaw('created_by_session_id IN (SELECT id FROM app_session WHERE pro_id = ?)', [$this->performer()->pro->id])
+            ->orderBy('created_at', 'desc')
+            ->paginate();
+        return view('app.practice-management.supply-orders-unsigned', compact('supplyOrders'));
+    }
+
+    private function getSupplyOrderCounts()
+    {
+        return [
+            "supplyOrders" => SupplyOrder::count(),
+            "supplyOrdersReadyToShip" => SupplyOrder
+                ::where('is_cleared_for_shipment', true)
+                ->where('is_cancelled', false)
+                ->whereNull('shipment_id')->count(),
+            "supplyOrdersShipmentUnderway" => SupplyOrder
+                ::where('is_cancelled', false)
+                ->whereNotNull('shipment_id')->count(),
+            "supplyOrdersHanging" => SupplyOrder
+                ::leftJoin('shipment', function($join) {
+                    $join->on('supply_order.shipment_id', '=', 'shipment.id');
+                })
+                ->where('shipment.status', 'CANCELLED')
+                ->where('supply_order.is_cancelled', false)
+                ->count(),
+        ];
+    }
+
+    public function shipmentsReadyToPrint(Request $request)
+    {
+        $counts = $this->getShipmentCounts();
+        $shipments = Shipment
+            ::where('is_cancelled', false)
+            ->where('status', 'CREATED')
+            ->orderBy('created_at', 'ASC')
+            ->paginate();
+        return view('app.practice-management.shipments-ready-to-print', compact('shipments', 'counts'));
+    }
+
+    public function shipmentsShipmentUnderway(Request $request)
+    {
+        $counts = $this->getShipmentCounts();
+        $shipments = Shipment
+            ::where('is_cancelled', false)
+            ->where('status', 'PRINTED')
+            ->orderBy('created_at', 'ASC')
+            ->paginate();
+        return view('app.practice-management.shipments-waiting-for-picker', compact('shipments', 'counts'));
+    }
+
+    private function getShipmentCounts()
+    {
+        return [
+            "shipments" => Shipment::count(),
+            "shipmentsReadyToPrint" => Shipment
+                ::where('is_cancelled', false)
+                ->where('status', 'CREATED')
+                ->count(),
+            "shipmentsWaitingForPicker" => Shipment
+                ::where('is_cancelled', false)
+                ->where('status', 'PRINTED')
+                ->count()
+        ];
+    }
+
+    public function shipment(Request $request, Shipment $shipment)
+    {
+        return view('app.practice-management.shipment', compact('shipment'));
+    }
+
+    public function shipmentsMultiPrint(Request $request, $ids)
+    {
+        $ids = array_map(function ($_x) {
+            return intval($_x);
+        }, explode("|", $ids));
+        $shipments = Shipment::whereIn('id', $ids)->get();
+        return view('app.practice-management.shipments-multi-print', compact('shipments'));
+    }
+
+    public function patientClaimSummary(Request $request, $proUid = null)
+    {
+
+        $notesTotal = DB::select(DB::raw("SELECT COUNT(*) FROM note WHERE is_cancelled IS NOT TRUE"))[0]->count;
+        $notesTotalWithBillingClosed = DB::select(DB::raw("SELECT COUNT(*) FROM note WHERE is_cancelled IS NOT TRUE AND is_bill_closed IS TRUE"))[0]->count;
+        $notesTotalWithClaimingClosed = DB::select(DB::raw("SELECT COUNT(*) FROM note WHERE is_cancelled IS NOT TRUE AND is_claim_closed IS TRUE"))[0]->count;
+
+        $notes3rdPartyTotal = DB::select(DB::raw("SELECT COUNT(*) FROM note n LEFT JOIN client c ON n.client_id = c.id WHERE n.is_cancelled IS NOT TRUE AND c.is_part_b_primary <> 'YES'"))[0]->count;
+        $notes3rdPartyTotalWithBillingClosed = DB::select(DB::raw("SELECT COUNT(*) FROM note n LEFT JOIN client c ON n.client_id = c.id WHERE n.is_cancelled IS NOT TRUE AND n.is_bill_closed IS TRUE AND c.is_part_b_primary <> 'YES'"))[0]->count;
+        $notes3rdPartyTotalWithClaimingClosed = DB::select(DB::raw("SELECT COUNT(*) FROM note n LEFT JOIN client c ON n.client_id = c.id WHERE n.is_cancelled IS NOT TRUE AND n.is_claim_closed IS TRUE AND c.is_part_b_primary <> 'YES'"))[0]->count;
+
+        $patientsTotal = DB::select(DB::raw("SELECT COUNT(*) FROM client WHERE is_active IS TRUE AND 0 NOT IN (SELECT c FROM (SELECT COUNT(*) c FROM note WHERE is_cancelled IS NOT TRUE AND note.client_id = client.id) x)"))[0]->count;
+        $patientsTotalWithBillingClosed = DB::select(DB::raw("SELECT COUNT(*) FROM client WHERE is_active IS TRUE AND 0 NOT IN (SELECT c FROM (SELECT COUNT(*) c FROM note WHERE is_cancelled IS NOT TRUE AND note.client_id = client.id) y) AND 0 IN (SELECT c FROM (SELECT COUNT(*) c FROM note WHERE is_cancelled IS NOT TRUE AND is_bill_closed IS NOT TRUE AND note.client_id = client.id) x)"))[0]->count;
+        $patientsTotalWithClaimingClosed = DB::select(DB::raw("SELECT COUNT(*) FROM client WHERE is_active IS TRUE AND 0 NOT IN (SELECT c FROM (SELECT COUNT(*) c FROM note WHERE is_cancelled IS NOT TRUE AND note.client_id = client.id) y) AND 0 IN (SELECT c FROM (SELECT COUNT(*) c FROM note WHERE is_cancelled IS NOT TRUE AND is_claim_closed IS NOT TRUE AND note.client_id = client.id) x)"))[0]->count;
+
+
+        $performerPro = $this->performer->pro;
+        $allPros = [];
+        if ($performerPro->pro_type == 'ADMIN') {
+            $allPros = Pro::all();
+        } else {
+            $allPros = [$performerPro];
+        }
+
+        //Patient | MCP | # Notes Total | # Notes without Billing Closed | # Notes without Claiming Closed
+        $patientsQuery = Client::where('is_dummy', '=', false)
+            ->whereNull('shadow_pro_id')
+            ->select('id', 'uid', 'name_first', 'name_last', 'mcp_pro_id', 'is_part_b_primary', 'medicare_advantage_plan',
+                DB::raw("(SELECT name_first||' '||name_last FROM pro where pro.id = client.mcp_pro_id) as mcp"),
+                DB::raw("(SELECT uid FROM pro where pro.id = mcp_pro_id) as mcp_pro_uid"),
+                DB::raw("(SELECT COUNT(*) FROM note where note.client_id = client.id) as notes_total"),
+                DB::raw("(SELECT COUNT(*) FROM note where note.client_id = client.id AND is_bill_closed IS NOT true) as notes_without_billing_closed"),
+                DB::raw("(SELECT COUNT(*) FROM note where note.client_id = client.id AND is_claim_closed IS NOT true) as notes_without_claiming_closed")
+            )->orderBy('is_part_b_primary', 'asc')->orderBy('notes_without_claiming_closed', 'desc');
+
+        if ($proUid) {
+            $mcpPro = Pro::where('uid', $proUid)->first();
+            if ($mcpPro) {
+                $patientsQuery->where('client.mcp_pro_id', '=', $mcpPro->id);
+            }
+        }
+
+        $patientsQuery->whereRaw('(SELECT COUNT(*) FROM note where note.client_id = client.id) > 0');
+
+        $patientsQuery->orderBy('notes_total', 'DESC');
+
+        $patients = $patientsQuery->paginate(50);
+
+        $data = [
+            'patients' => $patients,
+            'proUid' => $proUid,
+            'allPros' => $allPros,
+            'notesTotal' => $notesTotal,
+            'notesTotalWithBillingClosed' => $notesTotalWithBillingClosed,
+            'notesTotalWithClaimingClosed' => $notesTotalWithClaimingClosed,
+
+            'notes3rdPartyTotal' => $notes3rdPartyTotal,
+            'notes3rdPartyTotalWithBillingClosed' => $notes3rdPartyTotalWithBillingClosed,
+            'notes3rdPartyTotalWithClaimingClosed' => $notes3rdPartyTotalWithClaimingClosed,
+
+            'patientsTotal' => $patientsTotal,
+            'patientsTotalWithBillingClosed' => $patientsTotalWithBillingClosed,
+            'patientsTotalWithClaimingClosed' => $patientsTotalWithClaimingClosed
+
+        ];
+
+        return view('app.practice-management.patient-claim-summary', $data);
+    }
+
+    public function claims(Request  $request){
+        $status = $request->get('status');
+        $claims = [];
+        if(!$status){
+            $claims = Claim::orderBy('created_at', 'DESC')->paginate();
+        }else{
+            $claims = Claim::where('status', $status)->orderBy('created_at', 'DESC')->paginate();
+        }
+        return  view('app.practice-management.claims', compact('claims', 'status'));
+    }
+
+    public function processClaims(Request  $request) {
+        $status = '';
+
+        $q = $request->input('q') ? $request->input('q') : '';
+        $from = $request->input('from') ? $request->input('from') : '1900-01-01';
+        $to = $request->input('to') ? $request->input('to') : '2100-01-01';
+
+        $params = [
+            'q' => '%' . $q . '%',
+            'from' => $from,
+            'to' => $to
+        ];
+
+        $hcpPro = $request->input('hcp') ? Pro::where('uid', $request->input('hcp'))->first() : null;
+        if($hcpPro) {
+            $params['hcp'] = $hcpPro->id;
+        }
+
+        $claims = DB::select(DB::raw("
+SELECT claim.uid AS uid,
+       DATE(claim.created_at) AS created,
+       claim.status,
+       client.uid AS client_uid,
+       client.cell_number AS client_phone,
+       (client.name_last || ' ' || client.name_first) AS client ,
+       client.chart_number AS client_chart_number,
+       cp.id AS claim_pro_id,
+       (cp.name_last || ' ' || cp.name_first) AS claim_pro,
+       sp.id AS status_pro_id,
+       (sp.name_last || ' ' || sp.name_first) AS status_pro,
+       note.uid AS note_uid,
+       note.method,
+       note.new_or_fu_or_na,
+       care_month.uid AS care_month_uid,
+       -- claim.status_updated_at,
+       (DATE(claim.status_updated_at) || ' ' ||
+            LPAD(EXTRACT(hour FROM claim.status_updated_at)::text, 2, '0') || ':' ||
+            LPAD(EXTRACT(minute FROM claim.status_updated_at)::text, 2, '0')) AS status_updated_at,
+       (SELECT string_agg(claim_line.cpt, ', ') FROM claim_line where claim_id = claim.id) AS cpts,
+       (SELECT COUNT(claim_line_icd.id) FROM claim_line_icd WHERE claim_line_id IN (SELECT id FROM claim_line WHERE claim_id = claim.id)) AS icds,
+       ROUND(claim.expected_total, 3) as expected_total
+FROM claim
+    join client on claim.client_id = client.id
+    join pro cp on claim.pro_id = cp.id
+    left join note on claim.note_id = note.id
+    left join care_month on claim.care_month_id = care_month.id
+    left join app_session on claim.status_updated_by_session_id = app_session.id
+    left join pro sp on app_session.pro_id = sp.id
+--WHERE claim.status IS NULL OR claim.status = 'NEW'
+WHERE (claim.status is NULL OR claim.status NOT IN ('CANCELLED', 'ABANDONED'" . ($request->input('show-submitted') ? "" : ", 'SUBMITTED'") . "))
+-- AND claim.current_version_id IS NOT NULL
+AND (client.name_first ILIKE :q OR
+     client.name_last ILIKE :q OR
+     client.chart_number ILIKE :q OR
+     client.mcn ILIKE :q)
+AND (claim.created_at >= :from AND claim.created_at <= :to)
+" . ($hcpPro ? "AND claim.pro_id = :hcp" : '') . "
+AND claim.id IN (SELECT mb_claim.claim_id FROM mb_claim)
+ORDER BY claim.created_at DESC
+--OFFSET 0 LIMIT 15
+"), $params);
+
+        if($request->input('json')) {
+            return json_encode($claims);
+        }
+
+        return  view('app.practice-management.process-claims', compact('claims', 'status'));
+    }
+
+    public function rmLaunchAndClean(Request  $request) {
+
+        $keyNumbers = [];
+
+        $keyNumbers['careMonthsEligibleForBillGeneration_RM30_HCP_PLUS_40'] = CareMonth::where('is_bill_closed', false)
+            ->whereNotNull('company_pro_id')
+            ->whereNotNull('company_pro_payer_id')
+            ->whereNotNull('company_location_id')
+            ->whereRaw('(SELECT count(id) FROM care_month_cm_rm_reason WHERE care_month_id = care_month.id) > 0')
+            ->where('rm_total_time_in_seconds_by_mcp', '>=', 1800)
+            ->where('rm_total_time_in_seconds', '>=', 4200) // at 4200 (70 minutes, it becomes eligible for plus40)
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )
+            ->whereRaw('is_rm_time_waived IS NOT TRUE')
+            ->whereRaw('(has_anyone_interacted_with_client_about_rm_outside_note IS  TRUE OR has_mcp_rm_interacted_by_note IS  TRUE )')
+            ->count();
+
+        $keyNumbers['careMonthsEligibleForBillGeneration_RM30_HCP_PLUS_40_ifHadInteraction'] = CareMonth::where('is_bill_closed', false)
+            ->whereNotNull('company_pro_id')
+            ->whereNotNull('company_pro_payer_id')
+            ->whereNotNull('company_location_id')
+            ->whereRaw('(SELECT count(id) FROM care_month_cm_rm_reason WHERE care_month_id = care_month.id) > 0')
+            ->where('rm_total_time_in_seconds_by_mcp', '>=', 1800)
+            ->where('rm_total_time_in_seconds', '>=', 4200) // at 4200 (70 minutes, it becomes eligible for plus40)
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )
+            ->whereRaw('is_rm_time_waived IS NOT TRUE')
+            ->whereRaw('(has_anyone_interacted_with_client_about_rm_outside_note IS NOT TRUE AND has_mcp_rm_interacted_by_note IS NOT TRUE AND is_rm_interaction_waived IS NOT TRUE )')
+            ->count();
+
+        $keyNumbers['careMonthsEligibleForBillGeneration_RM30_HCP_PLUS_20'] = CareMonth::where('is_bill_closed', false)
+            ->whereNotNull('company_pro_id')
+            ->whereNotNull('company_pro_payer_id')
+            ->whereNotNull('company_location_id')
+            ->whereRaw('(SELECT count(id) FROM care_month_cm_rm_reason WHERE care_month_id = care_month.id) > 0')
+            ->where('rm_total_time_in_seconds_by_mcp', '>=', 1800)
+            ->where('rm_total_time_in_seconds', '<', 4200)
+            ->where('rm_total_time_in_seconds', '>=', 3000) // at 3000 (50 minutes, it becomes eligible for plus20)
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )
+            ->whereRaw('is_rm_time_waived IS NOT TRUE')
+            ->whereRaw('(has_anyone_interacted_with_client_about_rm_outside_note IS TRUE OR has_mcp_rm_interacted_by_note IS TRUE)')
+            ->count();
+
+        $keyNumbers['careMonthsEligibleForBillGeneration_RM30_HCP_PLUS_20_ifHadInteraction'] = CareMonth::where('is_bill_closed', false)
+            ->whereNotNull('company_pro_id')
+            ->whereNotNull('company_pro_payer_id')
+            ->whereNotNull('company_location_id')
+            ->whereRaw('(SELECT count(id) FROM care_month_cm_rm_reason WHERE care_month_id = care_month.id) > 0')
+            ->where('rm_total_time_in_seconds_by_mcp', '>=', 1800)
+            ->where('rm_total_time_in_seconds', '<', 4200)
+            ->where('rm_total_time_in_seconds', '>=', 3000) // at 3000 (50 minutes, it becomes eligible for plus20)
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )
+            ->whereRaw('is_rm_time_waived IS NOT TRUE')
+            ->whereRaw('(has_anyone_interacted_with_client_about_rm_outside_note IS NOT TRUE AND has_mcp_rm_interacted_by_note IS NOT TRUE AND is_rm_interaction_waived IS NOT TRUE )')
+            ->count();
+
+        $keyNumbers['careMonthsEligibleForBillGeneration_RM30_HCP'] = CareMonth::where('is_bill_closed', false)
+            ->whereNotNull('company_pro_id')
+            ->whereNotNull('company_pro_payer_id')
+            ->whereNotNull('company_location_id')
+            ->whereRaw('(SELECT count(id) FROM care_month_cm_rm_reason WHERE care_month_id = care_month.id) > 0')
+            ->where('rm_total_time_in_seconds_by_mcp', '>=', 1800)
+            ->whereRaw('is_rm_time_waived IS NOT TRUE')
+            ->whereRaw('(rm_total_time_in_seconds < 3000 OR (rm_total_time_in_seconds >= 3000 AND is_rm_interaction_waived IS TRUE))')
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )
+            ->count();
+
+        $keyNumbers['careMonthsEligibleForBillGeneration_RMB'] = CareMonth::where('is_bill_closed', false)
+            ->whereNotNull('company_pro_id')
+            ->whereNotNull('company_pro_payer_id')
+            ->whereNotNull('company_location_id')
+            ->whereRaw('(SELECT count(id) FROM care_month_cm_rm_reason WHERE care_month_id = care_month.id) > 0')
+            ->whereRaw('is_rm_time_waived IS TRUE')
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )
+            ->count();
+
+        $keyNumbers['careMonthsWith16OrMoreMeasurementDays'] = CareMonth::where('is_bill_closed', false)
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )
+            ->count();
+
+        $careMonthsWith16PlusMeasurements = CareMonth::where('is_bill_closed', false)
+            ->where('number_of_days_with_remote_measurements', '>=', 16 )->get();
+
+
+        return  view('app.practice-management.rm-launch-and-clean', compact('keyNumbers', 'careMonthsWith16PlusMeasurements'));
+    }
+
+    public function processNotes(Request  $request) {
+
+        $mode = $request->input('mode') ? $request->input('mode') : '1';
+        if(($mode < 1 || $mode > 5) && $mode != 8) $mode = 1;
+
+	DB::enableQueryLog(); // Enable query log
+
+	$test = Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isGood')::text = 'true'")
+                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', true)
+                ->where('is_claim_closed', true)
+                ->whereRaw("((SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') = 0)")
+                ->count();
+
+	// var_dump($test);
+
+	// dd(DB::getQueryLog()); // Show results of log
+
+	// exit;
+
+        $counts = [
+            "picked" => Note::where('is_cancelled', false)
+                ->whereNotNull('current_note_pickup_for_processing_id')
+                ->count(),
+            "bad" => Note::where('is_cancelled', false)
+                ->whereRaw("((detail_json)::json->>'isBad' = 'true')")
+                ->count(),
+            // not yet signed
+            "mode-1" => Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', false)
+                ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+//                ->whereNull('current_note_pickup_for_processing_id')
+                ->count(),
+            // billing not marked done
+            "mode-2" => Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', true)
+                //->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+//                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', false)
+                ->count(),
+            // billing not closed
+            "mode-3" => Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+//                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', false)
+                ->count(),
+            // claiming not closed
+            "mode-4" => Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+//                ->whereNull('current_note_pickup_for_processing_id')
+                ->whereRaw("(((detail_json)::json->>'isGood')::text <> 'true' OR ((detail_json)::json->>'isGood')::text IS NULL)")
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', true)
+                ->where('is_claim_closed', false)
+                ->count(),
+            // has unsubmitted claims
+            "mode-5" => Note::where('is_cancelled', false)
+
+		/*->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', true)
+                ->where('is_claim_closed', true)
+                ->whereRaw("(SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') > 0")
+*/
+
+                ->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+//                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', true)
+                // ->whereRaw("(((detail_json)::json->>'isGood')::text <> 'true')")
+                ->where('is_claim_closed', true)
+                ->whereRaw("((SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') > 0)")
+
+		->count(),
+ // has unsubmitted claims MARKED GOOD!
+            "mode-8" => Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isGood')::text = 'true'")
+                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', true)
+                ->where('is_claim_closed', false)
+                ->whereRaw("((SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') = 0)")
+		->count(),
+            // all good
+            "mode-6" => Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+//                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', true)
+                ->where('is_claim_closed', true)
+                ->whereRaw("((SELECT count(id) FROM claim WHERE note_id = note.id) > 0)")
+                ->whereRaw("((SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') = 0)")
+                ->count(),
+            "mode-7" => Note::where('is_cancelled', false)
+                ->whereRaw("(detail_json)::json->>'isBad' = 'true'")
+                ->count(),
+        ];
+
+        return  view('app.practice-management.process-notes', compact('mode', 'counts'));
+    }
+
+    public function notesProcessingCenter(Request  $request) {
+
+        $notes = Note::where('is_cancelled', false);
+
+        // filters
+        $proUid = $request->input('proUid');
+        if($proUid) {
+            $fPro = Pro::where('uid', $proUid)->first();
+            if($fPro) {
+                $notes = $notes->where('hcp_pro_id', $fPro->id);
+            }
+        }
+        $proSigned = $request->input('proSigned');
+        switch($proSigned) {
+            case 'yes': $notes = $notes->where('is_signed_by_hcp', true); break;
+            case 'no': $notes = $notes->where('is_signed_by_hcp', '!=', true); break;
+        }
+        $billingMarkedDone = $request->input('billingMarkedDone');
+        switch($billingMarkedDone) {
+            case 'yes': $notes = $notes->where('is_billing_marked_done', true); break;
+            case 'no': $notes = $notes->where('is_billing_marked_done', '!=', true); break;
+        }
+        $billingClosed = $request->input('billingClosed');
+        switch($billingClosed) {
+            case 'yes': $notes = $notes->where('is_bill_closed', true); break;
+            case 'no': $notes = $notes->where('is_bill_closed', '!=', true); break;
+        }
+        $claimingClosed = $request->input('claimingClosed');
+        switch($claimingClosed) {
+            case 'yes': $notes = $notes->where('is_claim_closed', true); break;
+            case 'no': $notes = $notes->where('is_claim_closed', '!=', true); break;
+        }
+        $allClaimsSubmitted = $request->input('allClaimsSubmitted');
+        if($allClaimsSubmitted) {
+            // TODO
+        }
+        $goodBad = $request->input('goodBad');
+        switch($goodBad) {
+            case 'good': $notes = $notes->whereRaw("(detail_json)::json->>'isBad' = 'false'"); break;
+            case 'bad': $notes = $notes->whereRaw("(detail_json)::json->>'isBad' = 'true'"); break;
+            case 'unclassified': $notes = $notes->whereRaw("(detail_json)::json->>'isBad' is null"); break;
+        }
+        $startDate = $request->input('startDate');
+        if($startDate) {
+            $notes = $notes->where('effective_dateest', '>=', $startDate);
+        }
+        $endDate = $request->input('endDate');
+        if($endDate) {
+            $notes = $notes->where('effective_dateest', '<=', $endDate);
+        }
+        $mcPartB = $request->input('mcPartB');
+        if($mcPartB) {
+            // TODO
+        }
+
+        $notes = $notes->orderBy('effective_dateest')->paginate(10);
+
+        return  view('app.practice-management.notes-processing-center', compact('notes'));
+    }
+
+    public function getNextNote(Request $request, $mode)
+    {
+        $note = null;
+        switch (+$mode) {
+            case 1:
+                $note = Note::where('is_cancelled', false)
+                    ->where('is_signed_by_hcp', false)
+                    ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+                    ->whereNull('current_note_pickup_for_processing_id')
+                    ->orderBy('effective_dateest', 'DESC')
+                    ->first();
+                break;
+            case 2:
+                $note = Note::where('is_cancelled', false)
+                    ->where('is_signed_by_hcp', true)
+                    ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+                    ->whereNull('current_note_pickup_for_processing_id')
+                    ->where('is_billing_marked_done', false)
+                    ->orderBy('effective_dateest', 'DESC')
+                    ->first();
+                break;
+            case 3:
+                $note = Note::where('is_cancelled', false)
+                    ->where('is_signed_by_hcp', true)
+                    ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+                    ->whereNull('current_note_pickup_for_processing_id')
+                    ->where('is_billing_marked_done', true)
+                    ->where('is_bill_closed', false)
+                    ->orderBy('effective_dateest', 'DESC')
+                    ->first();
+                break;
+            case 4: // claiming not closed
+		DB::enableQueryLog(); // Enable query log
+
+		$note = Note::where('is_cancelled', false)
+                    ->where('is_signed_by_hcp', true)
+                    ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+                    ->whereNull('current_note_pickup_for_processing_id')
+                    ->where('is_billing_marked_done', true)
+                    ->where('is_bill_closed', true)
+                    ->whereRaw("(((detail_json)::json->>'isGood')::text <> 'true' OR ((detail_json)::json->>'isGood')::text IS NULL)")
+                    ->where('is_claim_closed', false)
+                    ->orderBy('effective_dateest', 'DESC')
+                    ->first();
+
+//		DB::enableQueryLog(); // Enable query log
+
+// Your Eloquent query executed by using get()
+
+// dd(DB::getQueryLog()); // Show results of log
+
+                break;
+            case 5:
+                $note = Note::where('is_cancelled', false)
+                    ->where('is_signed_by_hcp', true)
+                    ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+                    ->whereNull('current_note_pickup_for_processing_id')
+                    ->where('is_billing_marked_done', true)
+                    ->where('is_bill_closed', true)
+                    ->where('is_claim_closed', true)
+                    ->whereRaw("(SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') > 0")
+                    ->orderBy('effective_dateest', 'DESC')
+                    ->first();
+                break;
+	    case 8:
+		 $note = Note::where('is_cancelled', false)
+                ->where('is_signed_by_hcp', true)
+                ->whereRaw("((detail_json)::json->>'isGood')::text = 'true'")
+                ->whereNull('current_note_pickup_for_processing_id')
+                ->where('is_billing_marked_done', true)
+                ->where('is_bill_closed', true)
+                ->where('is_claim_closed', false)
+                ->whereRaw("((SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') = 0)")
+                    ->orderBy('effective_dateest', 'DESC')
+                    ->first();
+                break;
+        }
+        if($note) {
+            $note->client_uid = $note->client->uid;
+        }
+        return json_encode($note);
+    }
+
+    public function pickedNotes(Request  $request) {
+        $counts = [
+            "unpicked" => Note::where('is_cancelled', false)
+                ->whereNull('current_note_pickup_for_processing_id')
+                ->count(),
+        ];
+        $notes = Note::where('is_cancelled', false)
+                ->whereNotNull('current_note_pickup_for_processing_id')
+                ->orderBy('effective_dateest', 'ASC')
+                ->paginate();
+        return  view('app.practice-management.picked-notes', compact('counts', 'notes'));
+    }
+
+    public function badNotes(Request  $request) {
+        $counts = [
+            "unpicked" => Note::where('is_cancelled', false)
+                ->whereNull('current_note_pickup_for_processing_id')
+                ->count(),
+        ];
+        $notes = Note::where('is_cancelled', false)
+            ->whereRaw("(detail_json)::json->>'isBad' = 'true'")
+            ->orderBy('effective_dateest', 'ASC')
+            ->paginate();
+        return  view('app.practice-management.bad-notes', compact('counts', 'notes'));
+    }
+
+    public function doneNotes(Request  $request) {
+        $counts = [
+            "unpicked" => Note::where('is_cancelled', false)
+                ->whereNull('current_note_pickup_for_processing_id')
+                ->count(),
+        ];
+        $notes = Note::where('is_cancelled', false)
+            ->where('is_signed_by_hcp', true)
+            ->whereRaw("((detail_json)::json->>'isBad' is null OR ((detail_json)::json->>'isBad')::text != 'true')")
+            ->whereNull('current_note_pickup_for_processing_id')
+            ->where('is_billing_marked_done', true)
+            ->where('is_bill_closed', true)
+            ->where('is_claim_closed', true)
+            ->whereRaw("(SELECT count(id) FROM claim WHERE note_id = note.id) > 0")
+            ->whereRaw("(SELECT count(id) FROM claim WHERE note_id = note.id AND is_cancelled IS FALSE AND status != 'CANCELLED' AND status != 'SUBMITTED') = 0")
+            ->orderBy('effective_dateest', 'ASC')
+            ->paginate();
+        return  view('app.practice-management.done-notes', compact('counts', 'notes'));
+    }
+
+    public function currentMbClaim(Request $request, $claimUid) {
+        $claim = Claim::where('uid', $claimUid)->first();
+        return json_encode(MBClaim::where('claim_version_id', $claim->currentVersion->id)->first());
+    }
+
+    public function currentClaimLines(Request $request, $claimUid) {
+        $claim = Claim::where('uid', $claimUid)->first();
+        if($request->input('json')) {
+            foreach ($claim->lines as $line) {
+                $line->expected_total = round($line->expected_total, 3);
+                $x = $line->claimLineIcds;
+            }
+            return json_encode($claim->lines);
+        }
+        return  view('app.practice-management._claim-lines', compact('claim'));
+    }
+
+    public function packsMultiPrint(Request $request) {
+        $packs = Pack
+            ::select('pack.*')
+            ->leftJoin('shipment', function($join) {
+                $join->on('pack.shipment_id', '=', 'shipment.id');
+            })
+            ->whereNotIn('shipment.status', ['CANCELLED', 'DISPATCHED'])
+            ->where(function ($query) {
+                $query->where('pack.status', '<>', 'DELETED')->orWhereNull('pack.status'); // weird, but just the <> isn't working!
+            })
+            ->whereNotNull('pack.label_system_file_id')
+            ->orderBy('pack.created_at', 'ASC')
+            ->get();
+        return view('app.practice-management.packs-multi-print', compact('packs'));
+    }
+
+    public function packsMultiPDF(Request $request, $ids) {
+        $ids = array_map(function ($_x) {
+            return intval($_x);
+        }, explode("|", $ids));
+        $packs = Pack::whereIn('id', $ids)->get();
+    }
+
+    public function handouts(Request $request) {
+        $handouts = Handout::orderBy('display_name')->get();
+        return view('app.practice-management.handouts', compact('handouts'));
+    }
+
+    private function callJava($request, $endPoint, $data)
+    {
+        $url =  config('stag.backendUrl') . $endPoint;
+        $response = Http::asForm()
+            ->withHeaders([
+                'sessionKey' => $request->cookie('sessionKey')
+            ])
+            ->post($url, $data)
+            ->body();
+        dd($response);
+        return $response;
+    }
+
+    public function genericBills(Request $request)
+    {
+        return view('app.practice-management.generic-bills');
+    }
+
+    public function billsUnderProcessing(Request $request)
+    {
+
+        $bills = Bill::where('is_cancelled', false)
+            ->where(function ($query) {   // mcp of any client program and program OB pending
+                $query
+                    ->where(function ($_query) {
+                        $_query->where('hcp_pro_id', $this->pro->id)
+                            ->where('hcp_expected_payment_amount', '>', 0)
+                            ->where('has_hcp_been_paid', false)
+                            ->where('is_signed_by_hcp', true);
+                    })
+                    ->orWhere(function ($_query) {
+                        $_query->where('cm_pro_id', $this->pro->id)
+                            ->where('cm_expected_payment_amount', '>', 0)
+                            ->where('has_cm_been_paid', false)
+                            ->where('is_signed_by_cm', true);
+                    })
+                    ->orWhere(function ($_query) {
+                        $_query->where('rme_pro_id', $this->pro->id)
+                            ->where('rme_expected_payment_amount', '>', 0)
+                            ->where('has_rme_been_paid', false)
+                            ->where('is_signed_by_rme', true);
+                    })
+                    ->orWhere(function ($_query) {
+                        $_query->where('rmm_pro_id', $this->pro->id)
+                            ->where('rmm_expected_payment_amount', '>', 0)
+                            ->where('has_rmm_been_paid', false)
+                            ->where('is_signed_by_rmm', true);
+                    })
+                    ->orWhere(function ($_query) {
+                        $_query->where('generic_pro_id', $this->pro->id)
+                            ->where('generic_pro_expected_payment_amount', '>', 0)
+                            ->where('has_generic_pro_been_paid', false)
+                            ->where('is_signed_by_generic_pro', true);
+                    });
+            })
+            ->orderBy('created_at', 'DESC')
+            ->paginate();
+
+        return view('app.practice-management.bills-under-processing', compact('bills'));
+
+    }
+
+    public function careMonthReport(Request $request) {
+        $m = $request->input('m');
+        $y = $request->input('y');
+        if(!$m || !$y) {
+            $date = date('Y-m-01');
+        }
+        else {
+            $date = date('Y-m-d', strtotime("$m/1/$y"));
+        }
+
+        $pro = $this->pro;
+
+        $records = DB::select("
+SELECT cm.id                                as care_month_id,
+       cm.uid                               as care_month_uid,
+       c.id                                 as client_id,
+       c.uid                                as client_uid,
+       cm.number_of_days_with_remote_measurements,
+       cm.rm_total_time_in_seconds_by_mcp,
+       cm.rm_total_time_in_seconds_by_rme_pro,
+       cm.rm_total_time_in_seconds_by_rmm_pro,
+       cm.mcp_pro_id,
+       cm.rme_pro_id,
+       cm.rmm_pro_id,
+       (c.name_first || ' ' || c.name_last) as client_name
+FROM care_month cm
+         JOIN client c on cm.client_id = c.id
+WHERE cm.start_date = :startDate
+  AND (cm.mcp_pro_id = :proID OR
+       cm.rme_pro_id = :proID OR
+       cm.rmm_pro_id = :proID)
+ORDER BY c.name_last, c.name_first
+            ",
+            ['startDate' => $date, 'proID' => $pro->id]
+        );
+
+        return view('app.practice-management.care-month-report', compact('records', 'date'));
+    }
+
+    public function myTeams(Request $request) {
+        $pro = $this->pro;
+
+        // get all teams where the authed-pro is the assistant pro
+        $teams = ProTeam::where('assistant_pro_id', $pro->id)
+            ->where('is_active', true)
+            ->orderBy('slug')
+            ->get();
+
+        return view('app.practice-management.my-teams', compact('teams'));
+    }
+
+    public function patientsAccountsInvites(Request $request){
+        $filters = $request->all();
+
+        $accountInvites = AccountInvite::select('account_invite.*')
+                            ->join('client', 'client.id', '=', 'account_invite.for_client_id');
+        if($this->performer->pro->pro_type !== 'ADMIN'){
+            $accountInvites = $accountInvites->where('client.mcp_pro_id', $this->performer->pro->id);
+        }
+
+        $this->filterMultiQuery($request, $accountInvites, 'account_invite.created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $accountInvites, 'account_invite.status', 'status');
+
+        $accountInvites = $accountInvites->orderBy('created_at', 'DESC')->paginate(20);
+
+        return view('app.practice-management.patients-accounts-invites', compact('accountInvites','filters'));
+    }
+
+    public function clientsBdtDevices(Request $request){
+        $filters = $request->all();
+
+        $devices = ClientBDTDevice::select(
+            'client_bdt_device.*',
+            DB::raw('(SELECT COUNT(*) FROM bdt_measurement WHERE bdt_measurement.bdt_device_id = client_bdt_device.device_id AND is_cellular_zero IS FALSE) as num_of_measurements'))
+           // DB::raw('(SELECT ts_date_time FROM bdt_measurement WHERE bdt_measurement.bdt_device_id = client_bdt_device.device_id AND is_cellular_zero IS FALSE ORDER BY ts_date_time DESC LIMIT 1) as most_recent_none_zero_measurement_at'))
+        ->join('client', 'client.id', '=', 'client_bdt_device.client_id');
+
+        if($this->performer->pro->pro_type !== 'ADMIN'){
+            $devices = $devices->where('client.mcp_pro_id', $this->performer->pro->id);
+        }
+
+        $this->filterMultiQuery($request, $devices, 'client_bdt_device.most_recent_measurement_at', 'date_category', 'date_value_1', 'date_value_2');
+        
+        // $dateKeyName = $request->get('date_category');
+        // $date1 = $request->get('date_value_1');
+        // $date2 = $request->get('date_value_2');
+        // $dateColumnName = "(SELECT (MAX(ts_date_time))::DATE FROM bdt_measurement WHERE bdt_measurement.bdt_device_id = client_bdt_device.device_id AND is_cellular_zero IS FALSE)";
+        // switch($dateKeyName) {
+        //     case 'EXACTLY':
+        //         if($date1) {
+        //             $devices->whereRaw($dateColumnName." = '$date1'::DATE");
+        //         }
+        //         break;
+        //     case 'LESS_THAN':
+        //         if($date1) {
+        //             $devices->whereRaw($dateColumnName. "< '$date1'::DATE" );
+        //         }
+        //         break;
+        //     case 'GREATER_THAN':
+        //         if($date1) {
+        //             $devices->whereRaw($dateColumnName. "> '$date1'::DATE");
+        //         }
+        //         break;
+        //     case 'BETWEEN':
+        //         if($date1 && $date2) {
+        //             $devices
+        //                 ->whereRaw($dateColumnName. ">= '$date1'::DATE")
+        //                 ->whereRaw($dateColumnName. "<= '$date2'::DATE");
+        //         }
+        //         break;
+        //     case 'NOT_BETWEEN':
+        //         if($date2 && $date1) {
+        //             $devices
+        //                 ->where(function ($q) use ($request, $dateColumnName, $date1, $date2) {
+        //                     $q->whereRaw($dateColumnName. "< '$date1'::DATE")
+        //                         ->orWhereRaw($dateColumnName. "> '$date2'::DATE");
+        //                 });
+        //         }
+        //         break;
+        // }
+
+        $keyName = $request->get('measurement_count_category');
+        $value1 = $request->get('measurement_count_value_1');
+        $value2 = $request->get('measurement_count_value_2');
+        $columnName = "(SELECT COUNT(*) FROM bdt_measurement WHERE bdt_measurement.bdt_device_id = client_bdt_device.device_id AND is_cellular_zero IS FALSE)";
+        switch($keyName) {
+            case 'EXACTLY':
+                if($value1) {
+                    $devices->whereRaw($columnName.'='.$value1);
+                }
+                break;
+            case 'LESS_THAN':
+                if($value1) {
+                    $devices->whereRaw($columnName. '<' .$value1);
+                }
+                break;
+            case 'GREATER_THAN':
+                if($value1) {
+                    $devices->whereRaw($columnName. '>'. $value1);
+                }
+                break;
+            case 'BETWEEN':
+                if($value1 && $value2) {
+                    $devices
+                        ->whereRaw($columnName. '>=' . $value1)
+                        ->whereRaw($columnName. '<=' . $value1);
+                }
+                break;
+            case 'NOT_BETWEEN':
+                if($value1 && $value2) {
+                    $devices
+                        ->where(function ($q) use ($request, $columnName, $value1, $value2) {
+                            $q->whereRaw($columnName. '<'. $value1)
+                                ->orWhereRaw($columnName. '>'.$value2);
+                        });
+                }
+                break;
+        }
+        
+        $status = $request->get('status');
+        if($status){
+            if($status === 'ACTIVE') $devices = $devices->where('client_bdt_device.is_active', true);
+            if($status === 'DEACTIVATED') $devices = $devices->where('client_bdt_device.is_active', false);
+        }
+
+        $devices = $devices->orderBy('num_of_measurements', 'DESC')->paginate(20);
+        return view('app.practice-management.clients_bdt_devices', compact('devices','filters'));
+    }
+
+    public function memos(Request $request){
+        $filters = $request->all();
+
+        $memos = ClientMemo::select('client_memo.*')
+        ->join('client', 'client.id', '=', 'client_memo.client_id');
+
+        if($this->performer->pro->pro_type !== 'ADMIN'){
+            $memos = $memos->where('client.mcp_pro_id', $this->performer->pro->id);
+        }
+
+        $this->filterMultiQuery($request, $memos, 'client_memo.created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $this->filterSimpleQuery($request, $memos, 'category', 'category');
+        $memos = $memos->orderBy('created_at', 'DESC')->paginate(20);
+
+        return view('app.practice-management.memos', compact('memos', 'filters'));
+    }
+
+    public function segmentTemplates(Request $request){
+        $segmentTemplates = SegmentTemplate::query();
+        $segmentTemplates = $segmentTemplates->orderBy('created_at', 'DESC');
+        
+        $responseType = $request->get('response_type');
+        if($responseType && $responseType == 'json'){
+            $segmentTemplates = $segmentTemplates->where('is_active', true)->get();
+            return $this->pass($segmentTemplates);
+        }
+
+        $segmentTemplates = $segmentTemplates->paginate(30);
+        return view('app.practice-management.segment-templates.index', compact('segmentTemplates'));
+    }
+
+    public function visitTemplates(Request $request){
+        $visitTemplates = VisitTemplate::query();
+        $visitTemplates = $visitTemplates->orderBy('created_at', 'DESC');
+
+        $responseType = $request->get('response_type');
+        if($responseType && $responseType == 'json'){
+            $visitTemplates = $visitTemplates->where('is_active', true)->get();
+            return $this->pass($visitTemplates);
+        }
+
+        $visitTemplates = $visitTemplates->paginate(30);
+        return view('app.practice-management.visit-templates.index', compact('visitTemplates'));
+    }
+
+    public function visitTemplate(Request $request, VisitTemplate $visitTemplate){
+        $visitTemplateSegmentTemplates = VisitTemplateSegmentTemplate::query();
+        $visitTemplateSegmentTemplates = $visitTemplateSegmentTemplates->where('visit_template_id', $visitTemplate->id);
+        $visitTemplateSegmentTemplates = $visitTemplateSegmentTemplates->orderBy('position_index', 'DESC');
+        $visitTemplateSegmentTemplates = $visitTemplateSegmentTemplates->paginate(30);
+        return view('app.practice-management.visit-templates.visit-template-segment-templates.index', compact('visitTemplate','visitTemplateSegmentTemplates'));
+    }
+
+    public function clientCcmRmStatus(Request $request){
+        $filters = $request->all();
+        $patients = Client::whereNull('shadow_pro_id');
+
+        // filters
+        /*
+        array:18 [▼
+          "age_category" => "LESS_THAN"
+          "age_value_1" => "34"
+          "age_value_2" => null
+          "sex" => "M"
+          "bmi_category" => "BETWEEN"
+          "bmi_value_1" => "20"
+          "bmi_value_2" => "25"
+          "last_visit_category" => "LESS_THAN"
+          "last_visit_value_1" => "2021-10-14"
+          "last_visit_value_2" => null
+          "next_appointment_category" => "LESS_THAN"
+          "next_appointment_value_1" => "2021-10-15"
+          "status" => "ACTIVE"
+          "last_weighed_in_category" => "EXACTLY"
+          "last_weighed_in_value_1" => "2021-10-07"
+          "last_bp_category" => "BETWEEN"
+          "last_bp_value_1" => "2021-10-01"
+          "last_bp_value_2" => "2021-10-31"
+        ]
+        */
+
+        $this->filterMultiQuery($request, $patients, 'age_in_years', 'age_category', 'age_value_1', 'age_value_2');
+        $this->filterSimpleQuery($request, $patients, 'sex', 'sex');
+        $this->filterMultiQuery($request, $patients, 'usual_bmi', 'bmi_category', 'bmi_value_1', 'bmi_value_2');
+        $this->filterMultiQuery($request, $patients, 'most_recent_weight_at', 'last_weighed_in_category', 'last_weighed_in_value_1', 'last_weighed_in_value_2');
+        $this->filterMultiQuery($request, $patients, 'most_recent_bp_at', 'last_bp_category', 'last_bp_value_1', 'last_bp_value_2');
+
+        switch($request->input('status')) {
+            case 'ACTIVE':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', true);
+                break;
+            case 'AWAITING_VISIT':
+                $patients->where('is_active', true)->where('has_mcp_done_onboarding_visit', false);
+                break;
+            case 'INACTIVE':
+                $patients->where('is_active', '<>', true);
+                break;
+        }
+
+
+        if($request->input('is_eligible_for_cm')){
+            $patients->where('is_eligible_for_cm', '=', $request->input('is_eligible_for_cm'));
+        }
+
+        if($request->input('is_enrolled_in_cm')){
+            $patients->where('is_enrolled_in_cm', '=', $request->input('is_enrolled_in_cm'));
+        }
+
+        if($request->input('has_cm_setup_been_performed')){
+            $patients->where('has_cm_setup_been_performed', '=', $request->input('has_cm_setup_been_performed')=='YES'? true : false);
+        }
+
+        if($request->input('is_eligible_for_rm')){
+            $patients->where('is_eligible_for_rm', '=', $request->input('is_eligible_for_rm'));
+        }
+
+        if($request->input('is_enrolled_in_rm')){
+            $patients->where('is_enrolled_in_rm', '=', $request->input('is_enrolled_in_rm'));
+        }
+
+        if($request->input('has_rm_setup_been_performed')){
+            $patients->where('has_rm_setup_been_performed', '=', $request->input('has_rm_setup_been_performed') =='YES'? true : false);
+        }
+
+        $patients = $patients->orderBy('created_at', 'DESC')->paginate(20);
+        return view('app.admin.client-ccm-rm-status', compact('patients', 'filters'));
+    }
+
+    public function rmActionReport(Request $request){
+
+        $filters = $request->all();
+    
+        $careMonthStartDate = $request->get('care_month_start_date');
+        
+        if(!$careMonthStartDate){
+            $careMonthStartDate = '2022-01-01';
+        }
+
+        $lastDateOfMonth = new DateTime(date("Y-m-t", strtotime(get_current_date())));
+        $today = new DateTime(get_current_date());
+        $diff = $lastDateOfMonth->diff($today);
+        $daysBetweenNowAndEndmonth = $diff->days;
+        $minRequiredMeasurements = 16 - $daysBetweenNowAndEndmonth -1;
+
+        $numOfMeasurements = $request->get('num_of_measurements'); //16_or_more, 12_or_more
+        $hasRecentVisit = $request->get('has_recent_visit'); //yes no
+        $hasBeenSpokenToThisMonth = $request->get('has_been_spoken_to'); //yes no 
+
+        $cmQuery = CareMonth::where('start_date', $careMonthStartDate);
+
+        //remove dummies
+        $cmQuery = $cmQuery->whereHas('client', function($clientQuery) {
+            return $clientQuery->where('client_engagement_status_category','<>', 'DUMMY')
+            ->orWhere('client_engagement_status_category', '=', null);
+        });
+
+        if($numOfMeasurements){
+            if($numOfMeasurements  == '16_or_more'){
+                $cmQuery = $cmQuery->where('number_of_days_with_remote_measurements', '>=', 16);
+            }
+            if($numOfMeasurements  == 'min_or_more'){
+                $cmQuery = $cmQuery->where('number_of_days_with_remote_measurements', '>=', $minRequiredMeasurements)
+                ->where('number_of_days_with_remote_measurements', '<', 16);
+            }
+
+            if($numOfMeasurements  == 'less_than_min'){
+                $cmQuery = $cmQuery->where('number_of_days_with_remote_measurements', '<', $minRequiredMeasurements);
+            }
+        }
+
+        if($hasRecentVisit){
+            if($hasRecentVisit == 'YES'){
+                $cmQuery = $cmQuery->whereHas('client', function($clientQuery) {
+                    return $clientQuery->whereRaw("most_recent_completed_mcp_note_date::DATE >= ((NOW() - interval '90 days')::DATE)");
+                });
+            }else{
+                $cmQuery = $cmQuery->whereHas('client', function($clientQuery) {
+                    return $clientQuery->whereRaw("most_recent_completed_mcp_note_date::DATE < ((NOW() - interval '90 days')::DATE)");
+                });
+            }
+        }
+
+        if($hasBeenSpokenToThisMonth){
+            if($hasBeenSpokenToThisMonth  == 'YES'){
+                $cmQuery = $cmQuery->where('has_anyone_interacted_with_client_about_rm_outside_note', '=', true);
+            }else{
+                $cmQuery = $cmQuery->where('has_anyone_interacted_with_client_about_rm_outside_note', '=', false);
+            }
+           
+        }
+
+        $rows = $cmQuery->paginate(50);
+
+        return view('app.practice-management.rm-action-report', compact('rows', 'filters', 'minRequiredMeasurements'));
+
+    }
+
+   
+}

+ 697 - 0
app/Http/Controllers/StatTreeController.php

@@ -0,0 +1,697 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Pro;
+use App\Models\StatTreeLineClauseArg;
+use App\Models\StatTreeLineReportColumn;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
+use App\Models\Clause;
+use App\Models\StatTree;
+use App\Models\StatTreeLine;
+use App\Models\StatTreeLineClause;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\DB;
+use Ramsey\Uuid\Uuid;
+
+class StatTreeController extends Controller
+{
+    public function list()
+    {
+        $statTrees = StatTree::orderByRaw('is_template DESC NULLS LAST')->orderBy('name')->get();
+        return view('app.stat-tree.stat-trees.list', compact('statTrees'));
+    }
+
+    public function createPage()
+    {
+        return view('app.stat-tree.stat-trees.create');
+    }
+    public function dashboard(StatTree $statTree)
+    {
+        return view('app.stat-tree.stat-trees.sub.dashboard', compact('statTree'));
+    }
+    public function dashboard2(StatTree $statTree)
+    {
+        return view('app.stat-tree.stat-trees.sub.dashboard2', compact('statTree'));
+    }
+
+    public function create(Request $request)
+    {
+        $validatedData = Validator::make($request->all(), [
+			'name' => 'required|string|unique:stat_tree',
+            'model' => 'required|string',
+            'slug' => 'required|string|unique:stat_tree'
+		]);
+
+		if ($validatedData->fails()) return $this->fail($validatedData->errors()->first());
+        [
+            'name' => $name,
+            'model' => $model,
+            'slug' => $slug
+        ] = $request->all();
+
+        $nextId = DB::select("select nextval('stat_tree_id_seq')");
+        $statTree = new StatTree; 
+        $statTree->id = $nextId[0]->nextval;
+        $statTree->uid = Uuid::uuid4();
+        $statTree->name = $name;
+        $statTree->model = $model;
+        $statTree->slug = $slug;
+
+        $statTree->is_template = (bool)$request->input('isTemplate');
+
+        if($request->input('proUid')) {
+            $pro = Pro::where('uid', $request->input('proUid'))->first();
+            if($pro) {
+                $statTree->pro_id = $pro->id;
+            }
+        }
+        $statTree->pro_scope_clause = $request->input('proScopeClause');
+
+        $statTree->save();
+
+        return $this->pass($statTree->uid);
+    }
+
+    public function updateBasic(Request $request) {
+        $statTree = StatTree::where('uid', $request->input('uid'))->first();
+        if(!$statTree) return $this->fail('Stat tree not found!');
+        $statTree->name = $request->input('name');
+        $statTree->model = $request->input('model');
+        $statTree->slug = $request->input('slug');
+        $statTree->is_template = (bool)$request->input('isTemplate');
+        if($request->input('proUid')) {
+            $pro = Pro::where('uid', $request->input('proUid'))->first();
+            if($pro) {
+                $statTree->pro_id = $pro->id;
+            }
+        }
+        $statTree->pro_scope_clause = $request->input('proScopeClause');
+        $statTree->save();
+        return $this->pass();
+    }
+
+    public function remove(Request $request) {
+        $statTree = StatTree::where('uid', $request->input('uid'))->first();
+        if(!$statTree) return $this->pass();
+        DB::statement("DELETE FROM stat_tree_line WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause_arg WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause_arg_value WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_report_column WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree WHERE id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        return $this->pass();
+    }
+
+    public function instantiate(Request $request, StatTree $statTree) {
+        if(!$statTree->is_template) {
+            return $this->fail("State tree is not a template!");
+        }
+        $pro = Pro::where('uid', $request->input('proUid'))->first();
+        if(!$pro) {
+            return $this->fail("Pro not found!");
+        }
+        $instance = $statTree->instantiateForPro($pro);
+        return $this->pass($instance->uid);
+    }
+
+    public function clone(Request $request, StatTree $statTree) {
+        $instance = $statTree->clone($request->input('name'));
+        return $this->pass($instance->uid);
+    }
+
+    private function traverseLines($_lines, &$result) {
+        foreach ($_lines as $line) {
+            $result[] = $line;
+            if(count($line->children)) {
+                $this->traverseLines($line->children, $result);
+            }
+        }
+    }
+
+    public function edit(Request $request, StatTree $statTree) {
+        $linesFlat = [];
+        if($request->input('multi-pro')) {
+            $this->traverseLines($statTree->rootLines, $linesFlat);
+        }
+        return view('app.stat-tree.stat-trees.sub.edit', compact('statTree', 'linesFlat'));
+    }
+
+    public function replaceAllLines(Request $request){
+
+        $parents = [];
+
+        $columns = '';
+
+        $statTreeID = (int) $request->get('statTreeID');
+        $data = $request->get('data');
+        $rows = json_decode($data, true);
+
+        $statTree = StatTree::where('id', $statTreeID)->first();
+        $model = $statTree->model;
+        DB::beginTransaction();
+
+        // cleanup junk
+        DB::statement("DELETE FROM stat_tree_line WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause_arg WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause_arg_value WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_report_column WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+
+        for($x = 0; $x < count($rows); $x++){
+            $row = $rows[$x];
+            $nextStatLineId = DB::select("select nextval('stat_tree_line_id_seq')");
+            $statTreeLine = new StatTreeLine;
+            $statTreeLine->id = $nextStatLineId[0]->nextval;
+            $statTreeLine->uid = Uuid::uuid4();
+            $statTreeLine->stat_tree_id = $statTree->id;
+            $statTreeLine->tree_order_position_index = $x;
+            $statTreeLine->last_refresh_count = null;
+            $statTreeLine->tsv_text_for_report_columns = null;
+            $statTreeLine->save();
+
+            $allClauses = [];
+            for($i = 0; $i < count($row); $i++){
+                $cell = $row[$i];
+                if(!$cell || empty($cell)) continue;
+
+                $parts = explode('==>', $cell);
+                $cell = $parts[0];
+                $columns = count($parts) > 1 ? $parts[1] : '';
+
+                $clause = Clause::where('label', $cell)->where('model', 'ilike', $model)->first();
+                if(!$clause){
+                    DB::rollBack();
+                    return $this->fail('No clause record found for ' . $cell);
+                }
+                $nextStatLineClauseId = DB::select("select nextval('stat_tree_line_clause_id_seq')");
+                $statTreeLineClause = new StatTreeLineClause;
+                $statTreeLineClause->id = $nextStatLineClauseId[0]->nextval;
+                $statTreeLineClause->uid = Uuid::uuid4();
+                $statTreeLineClause->stat_tree_line_id = $statTreeLine->id;
+                $statTreeLineClause->clause_id = $clause->id;
+                $statTreeLineClause->clause_label = $cell;
+                $statTreeLineClause->position_index = $i;
+                $statTreeLineClause->detail_json = json_encode(['model' => $model]);
+                $statTreeLineClause->stat_tree_id = $statTree->id;
+                $statTreeLineClause->save();
+
+                // check if the clause has any clauseArgs, if yes, create stat-tree-line-clause-arg and value records for them
+                if(count($clause->clauseArgs)) {
+                    foreach ($clause->clauseArgs as $clauseArg) {
+                        $statTreeLineClauseArg = new StatTreeLineClauseArg();
+                        $nextId = DB::select("select nextval('stat_tree_line_clause_arg_id_seq')");
+                        $statTreeLineClauseArg->id = $nextId[0]->nextval;
+                        $statTreeLineClauseArg->stat_tree_line_clause_id = $statTreeLineClause->id;
+                        $statTreeLineClauseArg->clause_arg_id = $clauseArg->id;
+                        $statTreeLineClauseArg->access_level = 'ADMIN';
+                        $statTreeLineClauseArg->stat_tree_id = $statTree->id;
+                        $statTreeLineClauseArg->default_value = null;
+
+                        // TODO: need to copy clause arg values from parent node for all except last clause
+
+                        $statTreeLineClauseArg->save();
+                    }
+                }
+
+                $allClauses[] = $cell;
+
+                // fill report columns (if last clause)
+                if($i === count($row) - 1 && !!$columns) {
+                    $columns = explode(",", $columns);
+                    for ($j = 0; $j < count($columns); $j++) {
+                        $parts = explode("|", $columns[$j]);
+                        $column = new StatTreeLineReportColumn();
+                        $nextId = DB::select("select nextval('stat_tree_line_report_column_id_seq')");
+                        $column->id = $nextId[0]->nextval;
+                        $column->uid = Uuid::uuid4();
+                        $column->stat_tree_line_id = $statTreeLine->id;
+                        $column->label = $parts[1];
+                        $column->display_key = $parts[0];
+                        $positionIndex = DB::select("select max(position_index) from stat_tree_line_report_column where stat_tree_line_id = {$column->stat_tree_line_id}");
+                        $column->position_index = is_numeric($positionIndex[0]->max) ? $positionIndex[0]->max + 1 : 1;
+                        $column->stat_tree_id = $statTree->id;
+                        $column->save();
+                    }
+                }
+
+            }
+            $parents[implode("|", $allClauses)] = $statTreeLine;
+
+            // if child, find and set parent
+            $nonEmpty = [];
+            for($i = 0; $i < count($row); $i++){
+                if($row[$i] && !empty($row[$i])) $nonEmpty[] = $row[$i];
+            }
+            if(count($nonEmpty) > 1) {
+                $parentClauses = [];
+                for($i = 0; $i < count($nonEmpty) - 1; $i++){
+                    $cell = $nonEmpty[$i];
+                    $parts = explode('==>', $cell);
+                    $cell = $parts[0];
+                    $columns = count($parts) > 1 ? $parts[1] : '';
+                    $clause = Clause::where('label', $cell)->where('model', 'ilike', $model)->first();
+                    if(!$clause){
+                        DB::rollBack();
+                        return $this->fail('No clause record found for ' . $cell);
+                    }
+                    $parentClauses[] = $clause->label;
+                }
+                $parentClauses = implode("|", $parentClauses);
+                if(@$parents[$parentClauses]) {
+                    $statTreeLine->parent_stat_tree_line_id = $parents[$parentClauses]->id;
+                    $statTreeLine->save();
+                }
+            }
+
+        }
+        DB::commit();
+        return $this->pass();
+    }
+
+    public function clausesJSON(Request $request, StatTree $statTree) {
+        $clauses = Clause::where('model', $statTree->model)->orderBy('position_index')->get();
+        $nodes = [];
+        foreach ($clauses as $clause) {
+
+            $children = [];
+
+            // clause text child
+            $children[] = [
+                "text" => $clause->clause_text,
+                "icon" => "fa fa-laptop-code text-primary text-sm",
+                "state" => [
+                    "opened" => false,
+                    "disabled" => false,
+                    "selected" => false,
+                ],
+                "li_attr" => [
+                    "type" => "clause_text"
+                ],
+                "a_attr" => [
+                    "title" => $clause->clause_text,
+                ],
+                "data" => [
+                    "type" => "clause_text"
+                ]
+            ];
+
+            // clause arg children
+            $argsSystem = [];
+            foreach ($clause->clauseArgs as $clauseArg) {
+                $children[] = [
+                    "text" => $clauseArg->arg_text . '<span class="text-secondary text-sm ml-2">' . $clauseArg->field_type . '</span>',
+                    "icon" => "fa fa-cubes text-info",
+                    "state" => [
+                        "opened" => false,
+                        "disabled" => false,
+                        "selected" => false,
+                    ],
+                    "data" => [
+                        "type" => "clause_arg",
+                        "id" => $clauseArg->id,
+                        "uid" => $clauseArg->uid,
+                        "argText" => $clauseArg->arg_text,
+                        "fieldType" => $clauseArg->field_type,
+                        "clauseId" => $clause->id
+                    ]
+                ];
+                $argsSystem[] = [
+                    "arg_text" => $clauseArg->arg_text,
+                    "field_type" => $clauseArg->field_type,
+                    "default_value" => null,
+                    "access_level" => null
+                ];
+            }
+
+            $nodes[] = [
+                "text" => $clause->label,
+                "state" => [
+                    "opened" => false,
+                    "disabled" => false,
+                    "selected" => false,
+                ],
+                "children" => $children,
+                "data" => [
+                    "type" => "clause",
+                    "id" => $clause->id,
+                    "uid" => $clause->uid,
+                    "model" => $clause->model,
+                    "question" => $clause->question,
+                    "answer" => $clause->answer,
+                    "label" => $clause->label,
+                    "clauseText" => $clause->clause_text,
+                    "clauseId" => $clause->id,
+                    "args" => $argsSystem
+                ],
+                "a_attr" => [
+                    "title" => $clause->clause_text,
+                ],
+            ];
+        }
+
+        return json_encode($nodes);
+    }
+
+    private function cleanupClause($clauseText)
+    {
+        //Dont include empty clauses, i.e ()
+        preg_match('#\((.*?)\)#', $clauseText, $match);
+        $content = @$match[1];
+        if (!$content || empty($content)) return null;
+        return $content;
+    }
+
+    private function applyStatTreeLineQueryClauses(StatTreeLine $statTreeLine, $proUid = false)
+    {
+        $model = $statTreeLine->statTree->model;
+        $clauses = [];
+        foreach ($statTreeLine->lineClauses as $lineClause) {
+            $clauseText = $lineClause->clause->clause_text;
+
+            // apply arg values
+            foreach ($lineClause->clause->clauseArgs as $clauseArg) {
+
+                $value = null;
+                foreach ($lineClause->lineClauseArgs as $lineClauseArg) {
+                    if($lineClauseArg->clause_arg_id === $clauseArg->id) {
+                        $value = $lineClauseArg->default_value;
+                    }
+                }
+
+                if(!is_null($value)) {
+                    $clauseText = str_replace(
+                        ':' . $clauseArg->arg_text,                         // search for :xxx
+                        "'" . $value . "'::" . $clauseArg->field_type,      // replace with '$value'::$field_type
+                        $clauseText);
+                }
+            }
+
+            $isValid = $this->cleanupClause($clauseText);
+            if ($isValid) {
+                array_push($clauses, $clauseText);
+            }
+        }
+
+        // if stat tree bound to a pro, apply pro_scope_clause
+        if(!$proUid) {
+            if($statTreeLine->statTree->pro && $statTreeLine->statTree->pro_scope_clause) {
+                $clauses[] = str_replace('@PRO_ID', $statTreeLine->statTree->pro->id, $statTreeLine->statTree->pro_scope_clause);
+            }
+        }
+        else {
+            $mvPro = Pro::where('uid', $proUid)->first();
+            if($statTreeLine->statTree->pro_scope_clause) {
+                $clauses[] = str_replace('@PRO_ID', $mvPro->id, $statTreeLine->statTree->pro_scope_clause);
+            }
+            $query = 'SELECT COUNT(*) FROM '.$model.' WHERE '. implode(" AND ", $clauses);
+        }
+
+        $query = 'SELECT COUNT(*) FROM '.$model.' WHERE '. implode(" AND ", $clauses);
+        try {
+            $result = DB::select($query);
+        }
+        catch (\Exception $ex) {
+            $result = 'error';
+        }
+        return $result;
+    }
+
+    public function linesJSON(Request $request, StatTree $statTree) {
+
+        // refresh counts
+        $lines = $statTree->lines;
+        foreach ($lines as $line) {
+            $query = $this->applyStatTreeLineQueryClauses($line);
+            if ($query && $query !== 'error') {
+                $line->last_refresh_count = $query[0]->count;
+            }
+            else {
+                $line->last_refresh_count = -1;
+            }
+            $line->save();
+        }
+
+        $nodes = [];
+        foreach ($statTree->rootLines as $rootLine) {
+            $nodes[] = $this->lineObject($rootLine);
+        }
+        return json_encode($nodes);
+    }
+
+    public function getCountsForPro(Request $request) {
+        $statTreeUid = $request->get('uid');
+        if (!$statTreeUid) return $this->fail('No specified stat tree!');
+        $statTree = StatTree::where('uid',  $statTreeUid)->first();
+        $lines = $statTree->lines;
+        $result = [];
+        foreach ($lines as $line) {
+            $query = $this->applyStatTreeLineQueryClauses($line, $request->input('proUid'));
+            if ($query && $query !== 'error') {
+                $result[$line->uid] = $query[0]->count;
+            }
+            else {
+                $result[$line->uid] = -1;
+            }
+        }
+        return json_encode($result);
+    }
+
+    private function lineObject(StatTreeLine  $line) {
+
+        // columns
+        $columns = [];
+        foreach ($line->reportColumns as $column) {
+            $columns[] = [
+                "label" => $column->label,
+                "display_key" => $column->display_key,
+                "type" => $column->field_type
+            ];
+        }
+
+        // clause
+        $lineClause = $line->displayLineClause();
+        $args = [];
+        $argsLabel = [];
+        foreach($lineClause->lineClauseArgs as $stlcArg) {
+            if($stlcArg->clauseArg) {
+                $args[] = [
+                    "arg_text" => $stlcArg->clauseArg->arg_text,
+                    "field_type" => $stlcArg->clauseArg->field_type,
+                    "default_value" => $stlcArg->default_value,
+                    "access_level" => $stlcArg->access_level
+                ];
+                $argsLabel[] = $stlcArg->clauseArg->arg_text . ': ' . $stlcArg->default_value;
+            }
+        }
+        $clause = [
+            "clause_id" => $lineClause->clause_id,
+            "clause_label" => $lineClause->clause_label,
+            "position_index" => $lineClause->position_index,
+            "args" => $args
+        ];
+
+        $children = [];
+        foreach ($line->children as $child) {
+            $children[] = $this->lineObject($child);
+        }
+        return [
+            "text" => '<span class="stat-tree-anchor">' . $line->displayLabel() . '</span>' .
+                (count($argsLabel) ? '<span class="text-info text-sm ml-2">[' . implode(', ', $argsLabel) . ']</span>' : '') .
+                '<span class="ml-2 text-secondary line-count-label">(' .
+                (is_null($line->last_refresh_count) ? '…' :
+                    ($line->last_refresh_count === -1 ?
+                        '<span class="text-danger text-sm font-weight-bold"><i class="fa fa-exclamation-triangle"></i> Query error / missing arg values</span>' :
+                        $line->last_refresh_count)) .
+                ')</span>',
+            "state" => [
+                "opened" => true,
+                "disabled" => false,
+                "selected" => false,
+            ],
+            "children" => $children,
+            "data" => [
+                "type" => "stat_tree_line",
+                "id" => $line->id,
+                "uid" => $line->uid,
+                "displayLabel" => $line->displayLabel(),
+                "extendedLabel" => $line->displayLabel() . (count($argsLabel) ? ' [' . implode(', ', $argsLabel) . ']' : ''),
+                "lastRefreshCount" => $line->last_refresh_count,
+                "treeOrderPositionIndex" => $line->tree_order_position_index,
+                "columns" => $columns,
+                "clause" => $clause
+            ],
+            "a_attr" => [
+                "title" => $line->displayLineClause()->clause->clause_text
+            ]
+        ];
+    }
+
+    public function replaceAllLinesJSON(Request $request) {
+
+        $statTree = StatTree::where('uid', $request->get('uid'))->first();
+        if(!$statTree) return $this->fail("Stat tree does not exist!");
+
+        $lines = json_decode($request->get('data'));
+
+        DB::beginTransaction();
+
+        // cleanup junk
+        DB::statement("DELETE FROM stat_tree_line WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause_arg WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_clause_arg_value WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+        DB::statement("DELETE FROM stat_tree_line_report_column WHERE stat_tree_id = :stat_tree_id", ['stat_tree_id' => $statTree->id]);
+
+        // process
+        for ($i=0; $i<count($lines); $i++) {
+            $result = $this->saveStatTreeLine($lines[$i], $i, null, $statTree);
+            if($result !== TRUE) {
+                DB::rollBack();
+                return $result;
+            }
+        }
+
+        DB::commit();
+        return $this->pass();
+    }
+
+    private function saveStatTreeLine($line, $position, $parentLine, $statTree) {
+
+        // saved tree line
+        $nextId = DB::select("select nextval('stat_tree_line_id_seq')");
+        $statTreeLine = new StatTreeLine;
+        $statTreeLine->id = $nextId[0]->nextval;
+        $statTreeLine->uid = Uuid::uuid4();
+        $statTreeLine->stat_tree_id = $statTree->id;
+        $statTreeLine->tree_order_position_index = $position;
+        $statTreeLine->last_refresh_count = null;
+        $statTreeLine->tsv_text_for_report_columns = null;
+        $statTreeLine->parent_stat_tree_line_id = $parentLine ? $parentLine->id : null;
+        $statTreeLine->save();
+
+        // -- clauses -- START
+
+        // copy parent line clauses
+        $maxParentClausePositionIndex = 0;
+        if($parentLine && count($parentLine->lineClauses)) {
+            foreach ($parentLine->lineClauses as $parentLineClause) {
+                $nextId = DB::select("select nextval('stat_tree_line_clause_id_seq')");
+                $statTreeLineClause = new StatTreeLineClause();
+                $statTreeLineClause->id = $nextId[0]->nextval;
+                $statTreeLineClause->uid = Uuid::uuid4();
+                $statTreeLineClause->stat_tree_line_id = $statTreeLine->id; // this stat tree line
+                $statTreeLineClause->clause_id = $parentLineClause->clause_id;
+                $statTreeLineClause->clause_label = $parentLineClause->clause_label;
+                $statTreeLineClause->position_index = $parentLineClause->position_index;
+                if($statTreeLineClause->position_index > $maxParentClausePositionIndex) {
+                    $maxParentClausePositionIndex = $statTreeLineClause->position_index;
+                }
+                $statTreeLineClause->stat_tree_id = $statTree->id;
+                $statTreeLineClause->save();
+
+                // copy clause args for parent line clauses
+                foreach ($parentLineClause->lineClauseArgs as $parentLineClauseArg) {
+                    $statTreeLineClauseArg = new StatTreeLineClauseArg();
+                    $nextId = DB::select("select nextval('stat_tree_line_clause_arg_id_seq')");
+                    $statTreeLineClauseArg->id = $nextId[0]->nextval;
+                    $statTreeLineClauseArg->stat_tree_line_clause_id = $statTreeLineClause->id; // this line cause
+                    $statTreeLineClauseArg->clause_arg_id = $parentLineClauseArg->clause_arg_id;
+                    $statTreeLineClauseArg->default_value = $parentLineClauseArg->default_value;
+                    $statTreeLineClauseArg->access_level = $parentLineClauseArg->access_level;
+                    $statTreeLineClauseArg->stat_tree_id = $statTree->id;
+                    $statTreeLineClauseArg->save();
+                }
+            }
+        }
+
+        // create own line clause (from clause.clause_label)
+        $clause = Clause::where('label', $line->clause->clause_label)->where('model', 'ilike', $statTree->model)->first();
+        if(!$clause) {
+            return $this->fail('No clause record found for ' . $line->clause->clause_label);
+        }
+        $nextId = DB::select("select nextval('stat_tree_line_clause_id_seq')");
+        $statTreeLineClause = new StatTreeLineClause();
+        $statTreeLineClause->id = $nextId[0]->nextval;
+        $statTreeLineClause->uid = Uuid::uuid4();
+        $statTreeLineClause->stat_tree_line_id = $statTreeLine->id; // this stat tree line
+        $statTreeLineClause->clause_id = $clause->id;
+        $statTreeLineClause->clause_label = $line->clause->clause_label;
+        $statTreeLineClause->position_index = $maxParentClausePositionIndex + 1;
+        $statTreeLineClause->stat_tree_id = $statTree->id;
+        $statTreeLineClause->save();
+
+        // create args for own line clause
+        foreach ($clause->clauseArgs as $clauseArg) {
+            $statTreeLineClauseArg = new StatTreeLineClauseArg();
+            $nextId = DB::select("select nextval('stat_tree_line_clause_arg_id_seq')");
+            $statTreeLineClauseArg->id = $nextId[0]->nextval;
+            $statTreeLineClauseArg->stat_tree_line_clause_id = $statTreeLineClause->id;
+            $statTreeLineClauseArg->clause_arg_id = $clauseArg->id;
+            $statTreeLineClauseArg->default_value = null;
+            $statTreeLineClauseArg->access_level = 'ADMIN';
+            for ($j=0; $j<count($line->clause->args); $j++) { // find the arg matching text and type from line->args and use that
+                if($line->clause->args[$j]->arg_text === $clauseArg->arg_text &&
+                    $line->clause->args[$j]->field_type === $clauseArg->field_type) {
+                    $statTreeLineClauseArg->default_value = $line->clause->args[$j]->default_value;
+                    $statTreeLineClauseArg->access_level = $line->clause->args[$j]->access_level;
+                    break;
+                }
+            }
+            $statTreeLineClauseArg->stat_tree_id = $statTree->id;
+            $statTreeLineClauseArg->save();
+        }
+
+        // -- clauses -- END
+
+
+        // columns
+        for ($i=0; $i<count($line->columns); $i++) {
+            $column = new StatTreeLineReportColumn();
+            $nextId = DB::select("select nextval('stat_tree_line_report_column_id_seq')");
+            $column->id = $nextId[0]->nextval;
+            $column->uid = Uuid::uuid4();
+            $column->stat_tree_line_id = $statTreeLine->id;
+            $column->label = $line->columns[$i]->label;
+            $column->display_key = $line->columns[$i]->display_key;
+            $column->field_type = $line->columns[$i]->type;
+            $column->position_index = $i;
+            $column->stat_tree_id = $statTree->id;
+            $column->save();
+        }
+
+        // child lines
+        for ($i=0; $i<count($line->children); $i++) {
+            $result = $this->saveStatTreeLine($line->children[$i], $i, $statTreeLine, $statTree);
+            if($result !== TRUE) {
+                DB::rollBack();
+                return $result;
+            }
+        }
+
+        return TRUE;
+    }
+
+    public function refreshTreeCountQueries(Request $request)
+    {
+        $statTreeID = $request->get('statTreeID');
+        if (!$statTreeID) return $this->fail('No specified stat tree!');
+
+        $statTree = StatTree::where('id', $statTreeID)->first();
+        if (!$statTree) return $this->fail('Invalid stat tree!');
+        $lines = $statTree->lines;
+        foreach ($lines as $line) {
+            $query = $this->applyStatTreeLineQueryClauses($line);
+            if ($query && $query !== 'error') {
+                $line->last_refresh_count = $query[0]->count;
+            }
+            else {
+                $line->last_refresh_count = -1;
+            }
+            $line->save();
+        }
+
+        return $this->pass();
+    }
+}

+ 320 - 0
app/Http/Controllers/StatTreeLineController.php

@@ -0,0 +1,320 @@
+<?php
+
+namespace App\Http\Controllers;
+
+
+use App\Models\Clause;
+use App\Models\StatTreeLineClause;
+use Illuminate\Http\Request;
+use App\Models\StatTree;
+use App\Models\StatTreeLine;
+use App\Models\Client;
+use App\Models\Pro;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Facades\DB;
+use Ramsey\Uuid\Uuid;
+
+class StatTreeLineController extends Controller
+{
+    public function list()
+    {
+        $statTreeLines = StatTreeLine::all();
+        return view('app.stat-tree-lines.list', compact('statTreeLines'));
+    }
+    public function dashboard(StatTreeLine $statTreeLine)
+    {
+        $result = $this->applyStatTreeLineQueryClauses($statTreeLine);
+        if (!$result) {
+            $response = 'Invalid query or model/table name';
+        } else {
+            $response = [
+                'type' => 'count',
+                'sql' => $result->toSql(),
+                'result' => $result->count()
+            ];
+        }
+
+        return view('app.stat-tree.stat-tree-lines.single', compact('statTreeLine', 'response'));
+    }
+
+    public function columnSuggest(Request $request) {
+        $term = $request->input('term') ? trim($request->input('term')) : '';
+        if (empty($term)) return '';
+        $table = $request->input('table') ? trim($request->input('table')) : '';
+        if (empty($table)) return '';
+        $table = strtolower($table);
+
+        $tables = [];
+        $columns = [];
+
+        // if single-term without spaces, single table
+        if(strpos($table, ' ') === FALSE) {
+            $tables[] = $table;
+        }
+        else {
+            $table = explode(" ", $table);
+            $tables[] = $table[0];
+            for ($i = 1; $i < count($table); $i++) {
+                if($table[$i] == 'join' && $i < count($table) - 1) {
+                    $tables[] = $table[$i+1];
+                    $i++;
+                }
+            }
+        }
+
+        for ($i = 0; $i < count($tables); $i++) {
+            $cols = DB::getSchemaBuilder()->getColumnListing($tables[$i]);
+            sort($cols);
+            $matches = array_filter($cols, function($_x) use ($term) {
+                return strpos($_x, $term) !== FALSE;
+            });
+            $matches = array_map(function($_x) use ($tables, $i) {
+                return [
+                    "text" => $tables[$i] . '.' . $_x,
+                    "label" => sanitize_state_name($_x),
+                    "type" => DB::getSchemaBuilder()->getColumnType($tables[$i], $_x)
+                ];
+            }, $matches);
+            $columns = array_merge($columns, $matches);
+        }
+
+        $columns = array_values($columns);
+        $columns = json_decode(json_encode($columns));
+        // dd($matches);
+        return json_encode([
+            "success" => true,
+            "data" => $columns
+        ]);
+    }
+
+    public function viewData(Request $request, StatTreeLine $line) {
+        $total = 0;
+        $rows = [];
+        $columns = [];
+        $selectColumns = [];
+        $paginator = null;
+        foreach ($line->reportColumns as $reportColumn) {
+            $columns[] = [
+                "label" => $reportColumn->label,
+                "column" => $reportColumn->display_key,
+                "type" => $reportColumn->field_type,
+                "as" => "v_{$reportColumn->id}"
+            ];
+            $selectColumns[] = "{$reportColumn->display_key} as v_{$reportColumn->id}";
+        }
+        if(count($line->reportColumns)) {
+            $result = $this->queryStatTreeLineData($line, $selectColumns, $columns, $request);
+            $total = $result[0];
+            $rows = $result[1];
+            $paginator = new LengthAwarePaginator($rows, $total, $request->input('per_page') ?: 20, $request->input('page') ?: 1);
+            $paginator->setPath(route('practice-management.statTreeLines.view-data', compact('line')));
+        }
+        return view('app.stat-tree.stat-tree-lines.view-data', compact('line', 'total', 'rows', 'columns', 'paginator'));
+    }
+
+    public function refreshCountQuery(Request $request)
+    {
+        $statTreeLineID = $request->get('statTreeLineID');
+        if (!$statTreeLineID) return $this->fail('No specified stat tree line!');
+
+        $statTreeLine = StatTreeLine::where('id', $statTreeLineID)->first();
+        if (!$statTreeLine) return $this->fail('Invalid stat tree line!');
+        $query = $this->applyStatTreeLineQueryClauses($statTreeLine);  
+        if ($query) {
+            $statTreeLine->last_refresh_count = $query[0]->count;
+            $statTreeLine->save();
+            return $this->pass($statTreeLine->last_refresh_count);
+        }else{
+            return $this->fail('Invalid query or model/table name');
+        }
+    }
+
+    protected function applyStatTreeLineQueryClauses(StatTreeLine $statTreeLine)
+    {
+
+        $model = $statTreeLine->statTree->model;
+        // $query = null;
+        // if (strcasecmp($model, 'client') == 0) {
+        //     $query = Client::query();
+        // }
+        // if (strcasecmp($model, 'pro') == 0) {
+        //     $query = Pro::query();
+        // }
+        // if (!$query) return null;
+
+
+
+
+        $clauses = [];
+        foreach ($statTreeLine->lineClauses as $lineClause) {
+            $clauseText = $lineClause->clause->clause_text;
+            $isValid = $this->cleanupClause($clauseText);
+            if ($isValid) {
+                array_push($clauses, $clauseText);
+            }
+        }
+
+        $query = 'SELECT COUNT(*) FROM '.$model.' WHERE '. implode(" AND ", $clauses);
+        
+        return DB::select($query);
+    }
+
+    protected function queryStatTreeLineData(StatTreeLine $statTreeLine, $selectColumns, $columns, Request $request)
+    {
+
+        $model = $statTreeLine->statTree->model;
+
+        $clauses = [];
+        foreach ($statTreeLine->lineClauses as $lineClause) {
+            $clauseText = $lineClause->clause->clause_text;
+
+            // apply arg values
+            foreach ($lineClause->clause->clauseArgs as $clauseArg) {
+
+                $value = null;
+                foreach ($lineClause->lineClauseArgs as $lineClauseArg) {
+                    if($lineClauseArg->clause_arg_id === $clauseArg->id) {
+                        $value = $lineClauseArg->default_value;
+                    }
+                }
+
+                if(!is_null($value)) {
+                    $clauseText = str_replace(
+                        ':' . $clauseArg->arg_text,                         // search for :xxx
+                        "'" . $value . "'::" . $clauseArg->field_type,      // replace with '$value'::$field_type
+                        $clauseText);
+                }
+            }
+
+            $isValid = $this->cleanupClause($clauseText);
+            if ($isValid) {
+                array_push($clauses, $clauseText);
+            }
+        }
+
+        // if stat tree bound to a pro, apply pro_scope_clause
+        if($statTreeLine->statTree->pro && $statTreeLine->statTree->pro_scope_clause) {
+            $clauses[] = str_replace('@PRO_ID', $statTreeLine->statTree->pro->id, $statTreeLine->statTree->pro_scope_clause);
+        }
+
+        // filters from view-data UI
+        foreach ($columns as $column) {
+            if($request->input($column['as'] . '_op')) {
+                switch($column['type']) {
+                    case 'integer':
+                    case 'bigint':
+                    case 'decimal':
+                    case 'bool':
+                    case 'boolean':
+                        if($request->input($column['as'] . '_value')) {
+                            $clauses[] = "{$column['column']} " . $request->input($column['as'] . '_op') . ' ' . $request->input($column['as'] . '_value');
+                        }
+                        break;
+                    case 'string':
+                    case 'text':
+                    case 'varchar':
+                        if($request->input($column['as'] . '_value')) {
+                            if($request->input($column['as'] . '_op') === '=' || $request->input($column['as'] . '_op') === '!=') {
+                                $clauses[] = "{$column['column']} " . $request->input($column['as'] . '_op') . ' ' . $request->input($column['as'] . '_value');
+                            }
+                            elseif($request->input($column['as'] . '_op') === 'ILIKE' || $request->input($column['as'] . '_op') === 'NOT ILIKE') {
+                                $clauses[] = "{$column['column']} " . $request->input($column['as'] . '_op') . ' ' . "'%" . $request->input($column['as'] . '_value') . "%'";
+                            }
+                        }
+                        break;
+                    case 'date':
+                    case 'datetime':
+                        if($request->input($column['as'] . '_value_start')) {
+                            $clauses[] = "{$column['column']} >= " . "'" . $request->input($column['as'] . '_value_start') . "'";
+                        }
+                        if($request->input($column['as'] . '_value_end')) {
+                            $clauses[] = "{$column['column']} <= " . "'" . $request->input($column['as'] . '_value_end') . "'";
+                        }
+                        break;
+                }
+            }
+        }
+
+        $result = null;
+
+        // get count for paginator
+        try {
+            $query = 'SELECT COUNT(*) FROM '.$model.' WHERE '. implode(" AND ", $clauses);
+            $result = DB::select($query);
+            $total = $result[0]->count;
+
+            $page = $request->input('page') ?: 1;
+            $perPage = $request->input('per_page') ?: 20;
+            $offset = ($page - 1) * $perPage;
+            $sort = '';
+            if($request->input('sort_by') && $request->input('sort_by')) {
+                $sort = "ORDER BY " . $request->input('sort_by') . " " . $request->input('sort_dir') . " NULLS LAST";
+            }
+            $query = 'SELECT ' . implode(", ", $selectColumns) . ' FROM '.$model.' WHERE '. implode(" AND ", $clauses) . " {$sort} OFFSET {$offset} LIMIT {$perPage}";
+            $result = DB::select($query);
+            $result = [$total, $result];
+        }
+        catch (\Exception $ex) {
+            $result = 'error';
+        }
+        return $result;
+    }
+
+    protected function cleanupClause($clauseText)
+    {
+        //Dont include empty clauses, i.e ()
+        preg_match('#\((.*?)\)#', $clauseText, $match);
+        $content = @$match[1];
+        if (!$content || empty($content)) return null;
+        return $content;
+    }
+
+    // eps
+    public function create(Request $request) {
+
+        // stat tree line
+        $statTreeLine = new StatTreeLine();
+        $nextId = DB::select("select nextval('stat_tree_line_id_seq')");
+        $statTreeLine->id = $nextId[0]->nextval;
+        $statTreeLine->uid = Uuid::uuid4();
+        $statTreeLine->stat_tree_id = $request->input('statTreeId');
+        $statTreeLine->parent_stat_tree_line_id = $request->input('parentStatTreeLineId');
+        $positionIndex = DB::select("select max(tree_order_position_index) from stat_tree_line where stat_tree_id = {$statTreeLine->stat_tree_id}" .
+            ($statTreeLine->parent_stat_tree_line_id ? " AND parent_stat_tree_line_id = {$statTreeLine->parent_stat_tree_line_id}" : '')
+        );
+        $statTreeLine->tree_order_position_index = is_numeric($positionIndex[0]->max) ? $positionIndex[0]->max + 1 : 1;
+        $statTreeLine->save();
+
+        // stat tree line clauses (all of parent's clauses + own)
+        if($statTreeLine->parent) {
+            foreach($statTreeLine->parent->lineClauses as $lineClause) {
+                $statTreeLineClause = new StatTreeLineClause();
+                $nextId = DB::select("select nextval('stat_tree_line_clause_id_seq')");
+                $statTreeLineClause->id = $nextId[0]->nextval;
+                $statTreeLineClause->uid = Uuid::uuid4();
+                $statTreeLineClause->stat_tree_line_id = $statTreeLine->id;
+                $statTreeLineClause->clause_id = $lineClause->clause_id;
+                $statTreeLineClause->clause_label = $lineClause->clause_label;
+                $statTreeLineClause->save();
+            }
+        }
+        $statTreeLineClause = new StatTreeLineClause();
+        $nextId = DB::select("select nextval('stat_tree_line_clause_id_seq')");
+        $statTreeLineClause->id = $nextId[0]->nextval;
+        $statTreeLineClause->uid = Uuid::uuid4();
+        $statTreeLineClause->stat_tree_line_id = $statTreeLine->id;
+        $clause = Clause::where('id', $request->input('clauseId'))->first();
+        $statTreeLineClause->clause_id = $clause->id;
+        $statTreeLineClause->clause_label = $clause->label;
+        $statTreeLineClause->save();
+
+        return $this->pass();
+    }
+    public function remove(Request $request) {
+        $statTreeLine = StatTreeLine::where('uid', $request->input('uid'))->first();
+        if(!$statTreeLine) return $this->fail('Stat tree line not found!');
+        // TODO: disallow if this has children
+        DB::select("delete from stat_tree_line where id = {$statTreeLine->id}");
+        return $this->pass();
+    }
+}

+ 48 - 0
app/Http/Controllers/StatTreeLineReportColumnController.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Http\Controllers;
+
+
+use App\Models\Clause;
+use App\Models\StatTreeLineClause;
+use App\Models\StatTreeLineReportColumn;
+use Illuminate\Http\Request;
+use App\Models\StatTree;
+use App\Models\StatTreeLine;
+use App\Models\Client;
+use App\Models\Pro;
+use Illuminate\Support\Facades\DB;
+use Ramsey\Uuid\Uuid;
+
+class StatTreeLineReportColumnController extends Controller
+{
+
+    // eps
+    public function create(Request $request) {
+        $column = new StatTreeLineReportColumn();
+        $nextId = DB::select("select nextval('stat_tree_line_report_column_id_seq')");
+        $column->id = $nextId[0]->nextval;
+        $column->uid = Uuid::uuid4();
+        $column->stat_tree_line_id = $request->input('statTreeLineId');
+        $column->label = $request->input('label');
+        $column->display_key = $request->input('displayKey');
+        $positionIndex = DB::select("select max(position_index) from stat_tree_line_report_column where stat_tree_line_id = {$column->stat_tree_line_id}");
+        $column->position_index = is_numeric($positionIndex[0]->max) ? $positionIndex[0]->max + 1 : 1;
+        $column->save();
+        return $this->pass();
+    }
+    public function update(Request $request) {
+        $column = StatTreeLineReportColumn::where('uid', $request->input('uid'))->first();
+        if(!$column) return $this->fail('Column not found!');
+        $column->label = $request->input('label');
+        $column->display_key = $request->input('displayKey');
+        $column->save();
+        return $this->pass();
+    }
+    public function remove(Request $request) {
+        $column = StatTreeLineReportColumn::where('uid', $request->input('uid'))->first();
+        if(!$column) return $this->fail('Column not found!');
+        DB::select("delete from stat_tree_line_report_column where id = {$column->id}");
+        return $this->pass();
+    }
+}

+ 30 - 0
app/Http/Controllers/TicketController.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Ticket;
+use Illuminate\Http\Request;
+
+use PDF;
+
+class TicketController extends Controller
+{
+
+    public function downloadAsPdf(Request $request, Ticket $ticket){
+        if($request->input('html')) {
+            return view('app.ticket.pdf-preview', compact('ticket'));
+        }
+        else {
+            $pdf = PDF::loadView('app.ticket.pdf-preview', compact('ticket'));
+            return $pdf->download($ticket->created_at .'_' . 'order.pdf');
+        }
+    }
+
+    public function getTicketFaxes(Request $request, Ticket $ticket) {
+        $result = $ticket->faxes->toArray();
+        foreach ($result as $k => $row) {
+            $result[$k]["sent_at"] = friendly_date_time($row["sent_at"]);
+        }
+        return json_encode($result);
+    }
+}

+ 74 - 0
app/Http/Kernel.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace App\Http;
+
+use Illuminate\Foundation\Http\Kernel as HttpKernel;
+
+class Kernel extends HttpKernel
+{
+    /**
+     * The application's global HTTP middleware stack.
+     *
+     * These middleware are run during every request to your application.
+     *
+     * @var array
+     */
+    protected $middleware = [
+        // \App\Http\Middleware\TrustHosts::class,
+        \App\Http\Middleware\TrustProxies::class,
+        \Fruitcake\Cors\HandleCors::class,
+        \App\Http\Middleware\CheckForMaintenanceMode::class,
+        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
+        \App\Http\Middleware\TrimStrings::class,
+        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
+    ];
+
+    /**
+     * The application's route middleware groups.
+     *
+     * @var array
+     */
+    protected $middlewareGroups = [
+        'web' => [
+            \App\Http\Middleware\EncryptCookies::class,
+            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+            \Illuminate\Session\Middleware\StartSession::class,
+            // \Illuminate\Session\Middleware\AuthenticateSession::class,
+            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+            \App\Http\Middleware\VerifyCsrfToken::class,
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        ],
+
+        'api' => [
+            'throttle:60,1',
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        ],
+    ];
+
+    /**
+     * The application's route middleware.
+     *
+     * These middleware may be assigned to groups or used individually.
+     *
+     * @var array
+     */
+    protected $routeMiddleware = [
+        'auth' => \App\Http\Middleware\Authenticate::class,
+        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
+        'can' => \Illuminate\Auth\Middleware\Authorize::class,
+        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
+        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
+        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
+        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+        'pro.auth' => \App\Http\Middleware\ProAuthenticated::class,
+        'pro.auth.redirect' => \App\Http\Middleware\RedirectAuthenticatedPro::class,
+        'pro.auth.admin' => \App\Http\Middleware\EnsureAdminPro::class,
+        'pro.auth.na' => \App\Http\Middleware\EnsureNaPro::class,
+        'pro.auth.mcp' => \App\Http\Middleware\EnsureMcpPro::class,
+        'pro.auth.can-access-patient' => \App\Http\Middleware\EnsureProCanAccessPatient::class,
+        'client.not-shadow-of-pro' => \App\Http\Middleware\EnsureClientIsNotShadowOfPro::class,
+    ];
+}

+ 21 - 0
app/Http/Middleware/Authenticate.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Auth\Middleware\Authenticate as Middleware;
+
+class Authenticate extends Middleware
+{
+    /**
+     * Get the path the user should be redirected to when they are not authenticated.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return string|null
+     */
+    protected function redirectTo($request)
+    {
+        if (! $request->expectsJson()) {
+            return route('login');
+        }
+    }
+}

+ 17 - 0
app/Http/Middleware/CheckForMaintenanceMode.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
+
+class CheckForMaintenanceMode extends Middleware
+{
+    /**
+     * The URIs that should be reachable while maintenance mode is enabled.
+     *
+     * @var array
+     */
+    protected $except = [
+        //
+    ];
+}

+ 18 - 0
app/Http/Middleware/EncryptCookies.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
+
+class EncryptCookies extends Middleware
+{
+    /**
+     * The names of the cookies that should not be encrypted.
+     *
+     * @var array
+     */
+    protected $except = [
+        //
+        'sessionKey'
+    ];
+}

+ 30 - 0
app/Http/Middleware/EnsureAdminPro.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\AppSession;
+use Closure;
+
+class EnsureAdminPro
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $sessionKey = $request->cookie('sessionKey');
+        $appSession = AppSession::where('session_key', $sessionKey)->where('is_active', true)->first();
+        $authenticated = $sessionKey && $appSession && $appSession->pro && $appSession->pro->pro_type == 'ADMIN';
+       
+        if (!$authenticated) {
+            //return redirect('/');
+            return abort(403);
+        }
+
+        return $next($request);
+    }
+}

+ 27 - 0
app/Http/Middleware/EnsureClientIsNotShadowOfPro.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\AppSession;
+use Closure;
+
+class EnsureClientIsNotShadowOfPro
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $patient = \request()->route('patient');
+        if(!!$patient) {
+            if(!!$patient->shadow_pro_id) {
+                abort(403);
+            }
+        }
+        return $next($request);
+    }
+}

+ 29 - 0
app/Http/Middleware/EnsureMcpPro.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\AppSession;
+use Closure;
+
+class EnsureMcpPro
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $sessionKey = $request->cookie('sessionKey');
+        $appSession = AppSession::where('session_key', $sessionKey)->where('is_active', true)->first();
+        $authenticated = $sessionKey && $appSession && $appSession->pro && $appSession->pro->is_enrolled_as_mcp;
+
+        if (!$authenticated) {
+            return abort(403);
+        }
+
+        return $next($request);
+    }
+}

+ 29 - 0
app/Http/Middleware/EnsureNaPro.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\AppSession;
+use Closure;
+
+class EnsureNaPro
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $sessionKey = $request->cookie('sessionKey');
+        $appSession = AppSession::where('session_key', $sessionKey)->where('is_active', true)->first();
+        $authenticated = $sessionKey && $appSession && $appSession->pro && $appSession->pro->is_considered_for_dna;
+
+        if (!$authenticated) {
+            return abort(403);
+        }
+
+        return $next($request);
+    }
+}

+ 37 - 0
app/Http/Middleware/EnsureProCanAccessPatient.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\AppSession;
+use Closure;
+
+class EnsureProCanAccessPatient
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $sessionKey = $request->cookie('sessionKey');
+        $appSession = AppSession::where('session_key', $sessionKey)->where('is_active', true)->first();
+        $authenticated = $sessionKey && $appSession && $appSession->pro;
+       
+        if (!$authenticated) {
+            abort(403);
+        }
+
+        $patient = \request()->route('patient');
+
+        if(!!$patient) {
+            if(!$appSession->pro->canAccess($patient->uid)) {
+                abort(403);
+            }
+        }
+
+        return $next($request);
+    }
+}

+ 51 - 0
app/Http/Middleware/ProAuthenticated.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\AppSession;
+use Closure;
+use Illuminate\Support\Facades\Http;
+
+class ProAuthenticated
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request $request
+     * @param  \Closure $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $sessionKey = $request->cookie('sessionKey');
+        $appSession = AppSession::where('session_key', $sessionKey)->where('is_active', true)->first();
+        $authenticated = $sessionKey && $appSession;
+       //TODO: confirm app_access
+        if (!$authenticated) {
+            $authUrl = config('stag.authUrl');
+            if(!$authUrl){
+                echo('AUTH_URL is not specified.');
+                exit;
+            }
+            return redirect($authUrl);
+        }
+
+        //log session activity 
+        $this->logSessionActivity($sessionKey);
+
+        return $next($request);
+    }
+
+    private function logSessionActivity($sessionKey)
+    {
+        
+        $url =  config('stag.backendUrl') . '/session/ping';
+        $response = Http::asForm()
+            ->withHeaders([
+                'sessionKey' => $sessionKey
+            ])
+            ->post($url,[])
+            ->body();
+        return $response;
+    }
+}

+ 28 - 0
app/Http/Middleware/RedirectAuthenticatedPro.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\AppSession;
+use Closure;
+
+class RedirectAuthenticatedPro
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request $request
+     * @param  \Closure $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $sessionKey = $request->cookie('sessionKey');
+        $appSession = AppSession::where('session_key', $sessionKey)->where('is_active', true)->first();
+
+        if($appSession && $appSession->pro) {
+            return redirect()->route('dashboard');
+        }
+
+        return $next($request);
+    }
+}

+ 27 - 0
app/Http/Middleware/RedirectIfAuthenticated.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Providers\RouteServiceProvider;
+use Closure;
+use Illuminate\Support\Facades\Auth;
+
+class RedirectIfAuthenticated
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @param  string|null  $guard
+     * @return mixed
+     */
+    public function handle($request, Closure $next, $guard = null)
+    {
+        if (Auth::guard($guard)->check()) {
+            return redirect(RouteServiceProvider::HOME);
+        }
+
+        return $next($request);
+    }
+}

+ 18 - 0
app/Http/Middleware/TrimStrings.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
+
+class TrimStrings extends Middleware
+{
+    /**
+     * The names of the attributes that should not be trimmed.
+     *
+     * @var array
+     */
+    protected $except = [
+        'password',
+        'password_confirmation',
+    ];
+}

+ 20 - 0
app/Http/Middleware/TrustHosts.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Http\Middleware\TrustHosts as Middleware;
+
+class TrustHosts extends Middleware
+{
+    /**
+     * Get the host patterns that should be trusted.
+     *
+     * @return array
+     */
+    public function hosts()
+    {
+        return [
+            $this->allSubdomainsOfApplicationUrl(),
+        ];
+    }
+}

+ 23 - 0
app/Http/Middleware/TrustProxies.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Fideloper\Proxy\TrustProxies as Middleware;
+use Illuminate\Http\Request;
+
+class TrustProxies extends Middleware
+{
+    /**
+     * The trusted proxies for this application.
+     *
+     * @var array|string|null
+     */
+    protected $proxies;
+
+    /**
+     * The headers that should be used to detect proxies.
+     *
+     * @var int
+     */
+    protected $headers = Request::HEADER_X_FORWARDED_ALL;
+}

+ 23 - 0
app/Http/Middleware/VerifyCsrfToken.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
+use Illuminate\Support\Facades\Redirect;
+
+class VerifyCsrfToken extends Middleware
+{
+    /**
+     * The URIs that should be excluded from CSRF verification.
+     *
+     * @var array
+     */
+    protected $except = [
+        "/process_form_submit",
+        "/availability/load",
+        '/practice-management/stat-tree/*',
+        '/practice-management/stat-tree-line/*',
+        '/practice-management/clause/replace-all'
+    ];
+    
+}

+ 62 - 0
app/Lib/Backend.php

@@ -0,0 +1,62 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: tatu
+ * Date: 6/22/20
+ * Time: 9:58 PM
+ */
+
+namespace App\Lib;
+
+
+use GuzzleHttp\Client as Guzzle;
+use GuzzleHttp\Cookie\CookieJar;
+use Psr\Http\Message\StreamInterface;
+use GuzzleHttp\Exception\ClientException;
+
+
+class Backend
+{
+    protected $url;
+
+    public function __construct()
+    {
+        $this->url = config('stag.backendUrl');
+    }
+
+
+    public function post(string $url, array $data, $headers = null)
+    {
+        return $this->sendRequest($url, 'POST', ['form_params' => $data, 'headers'=>$headers]);
+    }
+
+    public function get(string $url,  array $data = [])
+    {
+        return $this->sendRequest($url, 'GET', $data);
+    }
+
+
+    /**
+     * @param string $url
+     * @param string $method HTTP request method eg POST, GET.
+     * @param array $options
+     * @return StreamInterface
+     * @throws \GuzzleHttp\Exception\GuzzleException
+     */
+    public function sendRequest(string $url, string $method,  array $options = []) : StreamInterface
+    {
+        $url = sprintf('%s/%s', rtrim($this->url, '/'), trim($url, '/'));
+
+        $response = null;
+        $client = new Guzzle();
+
+        try {
+            $response = $client->request($method, $url, $options);
+        }
+        catch (ClientException $clientException)  {
+            $response = $clientException->getResponse();
+        }
+
+        return $response->getBody();
+    }
+}

+ 11 - 0
app/Models/Account.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class Account extends Model
+{
+    protected $table = 'account';
+
+}

+ 19 - 0
app/Models/AccountClient.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class AccountClient extends Model
+{
+    protected $table = 'account_client';
+
+    public function client() {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function account() {
+        return $this->hasOne(Account::class, 'id', 'account_id');
+    }
+
+}

+ 21 - 0
app/Models/AccountInvite.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class AccountInvite extends Model
+{
+
+    protected $table = 'account_invite';
+
+    public function getRouteKeyName()
+    {
+        return 'access_token';
+    }
+
+    public function client() {
+        return $this->hasOne(Client::class, 'id', 'for_client_id');
+    }
+}

+ 32 - 0
app/Models/ActionItem.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ActionItem extends Model
+{
+    protected $table = 'action_item';
+
+    public function facility()
+    {
+        return $this->belongsTo(Facility::class, 'to_facility_id', 'id');
+    }
+
+    public function client()
+    {
+        return $this->belongsTo(Client::class, 'client_id', 'id');
+    }
+
+
+    public function pro()
+    {
+        return $this->belongsTo(Pro::class, 'ally_pro_id', 'id');
+    }
+
+
+    public function actionItemContentUpdates()
+    {
+        return $this->hasMany(ActionItemContentUpdate::class, 'action_item_id');
+    }
+}

+ 13 - 0
app/Models/ActionItemContentUpdate.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ActionItemContentUpdate extends Model
+{
+    public function actionItem()
+    {
+        return $this->belongsTo(ActionItem::class, 'action_item_id');
+    }
+}

+ 15 - 0
app/Models/ActionItemFax.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ActionItemFax extends Model
+{
+    protected $table = 'action_item_fax';
+
+    public function actionItem()
+    {
+        return $this->belongsTo(ActionItem::class, 'action_item_id');
+    }
+}

+ 10 - 0
app/Models/ActionItemStatusUpdate.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ActionItemStatusUpdate extends Model
+{
+    protected $table = 'action_item_status_update';
+}

+ 10 - 0
app/Models/Amendment.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class Amendment extends Model
+{
+    protected $table = 'amendment';
+}

+ 10 - 0
app/Models/AmendmentDecision.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class AmendmentDecision extends Model
+{
+    protected $table = 'amendment_decision';
+}

+ 21 - 0
app/Models/AppSession.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class AppSession extends Model
+{
+    protected $table = 'app_session';
+
+
+    public function pro()
+    {
+        return $this->hasOne(Pro::class, 'id', 'pro_id');
+    }
+
+    public function client()
+    {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+}

+ 35 - 0
app/Models/Appointment.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\HasOne;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class Appointment extends Model
+{
+    protected $table = 'appointment';
+
+    public function client() {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function pro() {
+        return $this->hasOne(Pro::class, 'id', 'pro_id');
+    }
+
+    public function confirmationRequests() {
+        return $this->hasMany(AppointmentConfirmationRequest::class, 'appointment_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function confirmationDecisions() {
+        return $this->hasMany(AppointmentConfirmationDecision::class, 'appointment_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function latestConfirmationDecision() {
+        return $this->hasOne(AppointmentConfirmationDecision::class, 'id', 'latest_confirmation_decision_id');
+    }
+
+}

+ 17 - 0
app/Models/AppointmentConfirmationDecision.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\HasOne;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class AppointmentConfirmationDecision extends Model
+{
+    protected $table = 'appointment_confirmation_decision';
+
+    public function session() {
+        return $this->hasOne(AppSession::class, 'id', 'created_by_session_id');
+    }
+
+}

+ 13 - 0
app/Models/AppointmentConfirmationRequest.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\HasOne;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class AppointmentConfirmationRequest extends Model
+{
+    protected $table = 'appointment_confirmation_request';
+
+}

+ 9 - 0
app/Models/BDTDevice.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace App\Models;
+
+class BDTDevice extends Model
+{
+    protected $table = 'bdt_device';
+
+}

+ 18 - 0
app/Models/BDTMeasurement.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Models;
+
+class BDTMeasurement extends Model
+{
+    protected $table = 'bdt_measurement';
+
+    public function device() {
+        return $this->hasOne(BDTDevice::class, 'id', 'device_id');
+    }
+
+    public function clientBDTMeasurements()
+    {
+        return $this->hasMany(ClientBDTMeasurement::class, 'bdt_measurement_id', 'id');
+    }
+
+}

+ 43 - 0
app/Models/Bill.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class Bill extends Model
+{
+    protected $table = 'bill';
+
+    public function careMonth() {
+        return $this->belongsTo(CareMonth::class);
+    }
+
+    public function note() {
+        return $this->belongsTo(Note::class);
+    }
+
+    public function client()
+    {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function hcp() {
+        return $this->hasOne(Pro::class, 'id', 'hcp_pro_id');
+    }
+
+    public function ally() {
+        return $this->hasOne(Pro::class, 'id', 'na_pro_id');
+    }
+
+    public function genericPro() {
+        return $this->hasOne(Pro::class, 'id', 'generic_pro_id');
+    }
+
+    public function hcpCompanyPro() {
+        return $this->hasOne(CompanyPro::class, 'id', 'hcp_company_pro_id');
+    }
+
+    public function genericCompanyPro() {
+        return $this->hasOne(CompanyPro::class, 'id', 'generic_company_pro_id');
+    }
+}

+ 10 - 0
app/Models/BillCareMonthEntry.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class BillCareMonthEntry extends Model
+{
+    protected $table = 'bill_care_month_entry';
+}

+ 10 - 0
app/Models/BillStatusUpdate.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class BillStatusUpdate extends Model
+{
+    protected $table = 'bill_status_update';
+}

+ 34 - 0
app/Models/BillingReport.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\HasOne;
+use Illuminate\Support\Collection;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class BillingReport extends Model
+{
+    protected $table = 'billing_report';
+
+    public function clientDisplayName()
+    {
+        return $this->client_last . ', ' . $this->client_first;
+    }
+
+    public function proDisplayName()
+    {
+        return $this->pro_last . ', ' . $this->pro_first;
+    }
+
+    public function note()
+    {
+        return $this->hasOne(Note::class, 'id', 'note_id');
+    }
+
+    public function client()
+    {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+}

+ 117 - 0
app/Models/CareMonth.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+use Illuminate\Support\Collection;
+
+class CareMonth extends Model
+{
+    protected $table = 'care_month';
+
+    public function patient(){
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function client(){
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function mcp(){
+        return $this->hasOne(Pro::class, 'id', 'mcp_pro_id');
+    }
+
+    public function cmPro(){
+        return $this->hasOne(Pro::class, 'id', 'cm_pro_id');
+    }
+
+    public function rmmPro(){
+        return $this->hasOne(Pro::class, 'id', 'rmm_pro_id');
+    }
+
+    public function rmePro(){
+        return $this->hasOne(Pro::class, 'id', 'rme_pro_id');
+    }
+
+    public function entries() {
+        return $this->hasMany(CareMonthEntry::class, 'care_month_id', 'id');
+    }
+
+    public function bills() {
+        return $this->hasMany(Bill::class, 'care_month_id', 'id');
+    }
+
+    public function claims() {
+        return $this->hasMany(Claim::class, 'care_month_id', 'id');
+    }
+
+    public function getBillsOfType($_type) {
+        $bills = $this->bills;
+        $targetBills = new Collection();
+        foreach ($bills as $bill) {
+            if($bill->cm_or_rm === $_type) {
+                $targetBills->add($bill);
+            }
+        }
+        return $targetBills;
+    }
+
+    public function rmBill(){
+        return $this->hasOne(Bill::class, 'id', 'rm_bill_id');
+    }
+
+    public function companyPro()
+    {
+        return $this->hasOne(CompanyPro::class, 'id', 'company_pro_id');
+    }
+
+    public function company()
+    {
+        return $this->hasOne(Company::class, 'id', 'company_id');
+    }
+
+    public function companyProPayer()
+    {
+        return $this->hasOne(CompanyProPayer::class, 'id', 'company_pro_payer_id');
+    }
+
+    public function companyLocation()
+    {
+        return $this->hasOne(CompanyLocation::class, 'id', 'company_location_id');
+    }
+
+
+    public function cmReasons()
+    {
+        return $this->hasMany(CareMonthCmRmReason::class, 'care_month_id', 'id')
+            ->where('cm_or_rm', 'CM')
+            ->orderBy('position_index', 'ASC')
+            ->orderBy('code', 'ASC');
+    }
+
+    public function rmReasons()
+    {
+        return $this->hasMany(CareMonthCmRmReason::class, 'care_month_id', 'id')
+            ->where('cm_or_rm', 'RM')
+            ->orderBy('position_index', 'ASC')
+            ->orderBy('code', 'ASC');
+    }
+
+    public function rmSetupClaim()
+    {
+        return $this->hasOne(Claim::class, 'id', 'rm_setup_claim_id')
+            ->where('status', '<>', 'CANCELLED');
+    }
+
+    public function mostRecentMcpNote()
+    {
+        return $this->hasOne(Note::class, 'id', 'most_recent_mcp_note_id');
+    }
+
+    public function note()
+    {
+        return $this->hasOne(Note::class, 'id', 'note_id');
+    }
+
+}

+ 10 - 0
app/Models/CareMonthCmRmReason.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class CareMonthCmRmReason extends Model
+{
+    protected $table = 'care_month_cm_rm_reason';
+}

+ 18 - 0
app/Models/CareMonthEntry.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class CareMonthEntry extends Model
+{
+    protected $table = 'care_month_entry';
+
+    public function careMonth() {
+        return $this->belongsTo(CareMonth::class, 'care_month_id', 'id');
+    }
+
+    public function pro() {
+        return $this->hasOne(Pro::class, 'id', 'pro_id');
+    }
+}

+ 52 - 0
app/Models/Claim.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Claim extends Model
+{
+
+    protected $table = 'claim';
+
+    public function lines(){
+        return $this->hasMany(ClaimLine::class, 'claim_id', 'id');
+    }
+
+    public function client(){
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function edi(){
+        return $this->hasOne(ClaimEDI::class, 'id', 'claim_edi_id');
+    }
+
+    public function currentVersion(){
+        return $this->hasOne(ClaimVersion::class, 'id', 'current_version_id');
+    }
+
+    public function mbClaims(){
+        return $this->hasMany(MBClaim::class, 'claim_id', 'id')->orderBy('created_at', 'DESC');
+    }
+
+    public function company(){
+        return $this->hasOne(Company::class, 'id', 'company_id');
+    }
+
+    public function companyPro(){
+        return $this->hasOne(CompanyPro::class, 'id', 'company_pro_id');
+    }
+
+    public function primaryCompany(){
+        return $this->hasOne(Company::class, 'id', 'primary_company_id');
+    }
+
+    public function companyLocation(){
+        return $this->hasOne(CompanyLocation::class, 'id', 'company_location_id');
+    }
+
+    public function primaryPayer(){
+        return $this->hasOne(Payer::class, 'id', 'primary_payer_id');
+    }
+
+}

+ 16 - 0
app/Models/ClaimEDI.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ClaimEDI extends Model
+{
+
+    protected $table = 'claim_edi';
+
+    public function claims(){
+        return $this->hasMany(Claim::class, 'claim_edi_id', 'id');
+    }
+
+}

+ 25 - 0
app/Models/ClaimLine.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ClaimLine extends Model
+{
+
+    protected $table = 'claim_line';
+
+    public function claimLineIcds(){
+        return $this->hasMany(ClaimLineIcd::class, 'claim_line_id', 'id');
+    }
+
+    public function icds(){
+        $icdList = [];
+        foreach($this->claimLineIcds as $icd){
+            $icdList[] = $icd->code;
+        }
+
+        return implode (", ", $icdList);
+    }
+
+}

+ 12 - 0
app/Models/ClaimLineIcd.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ClaimLineIcd extends Model
+{
+
+    protected $table = 'claim_line_icd';
+
+}

+ 12 - 0
app/Models/ClaimVersion.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ClaimVersion extends Model
+{
+
+    protected $table = 'claim_version';
+
+}

+ 16 - 0
app/Models/Clause.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Clause extends Model
+{    
+    protected $table = 'clause';
+    public $timestamps = false;
+
+    public function clauseArgs() {
+        return $this->hasMany(ClauseArg::class, 'clause_id', 'id')->orderBy('id');
+    }
+}

+ 13 - 0
app/Models/ClauseArg.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class ClauseArg extends Model
+{    
+    protected $table = 'clause_arg';
+    public $timestamps = false;
+
+}

+ 891 - 0
app/Models/Client.php

@@ -0,0 +1,891 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\HasOne;
+use Illuminate\Support\Collection;
+
+# use Illuminate\Database\Eloquent\Model;
+
+use Illuminate\Support\Facades\DB;
+
+class Client extends Model
+{
+    protected $table = 'client';
+
+    public function primaryCoverages()
+    {
+        return $this->hasMany(ClientPrimaryCoverage::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function latestClientPrimaryCoverage(){
+        return $this->hasOne(ClientPrimaryCoverage::class, 'id', 'latest_client_primary_coverage_id');
+    }
+	public function latestNewClientPrimaryCoverage(){
+        return $this->hasOne(ClientPrimaryCoverage::class, 'id', 'latest_new_client_primary_coverage_id');
+    }
+	public function latestAutoRefreshClientPrimaryCoverage(){
+        return $this->hasOne(ClientPrimaryCoverage::class, 'id', 'latest_auto_refresh_client_primary_coverage_id');
+    }
+	public function latestManualClientPrimaryCoverage(){
+        return $this->hasOne(ClientPrimaryCoverage::class, 'id', 'latest_manual_client_primary_coverage_id');
+    }
+	public function temporaryOutsiderNewClientPrimaryCoverage(){
+        return $this->hasOne(ClientPrimaryCoverage::class, 'id', 'temporary_outsider_new_client_primary_coverage_id');
+    }
+
+    public function displayName()
+    {
+        return $this->name_last . ', ' . $this->name_first;
+    }
+
+    public function mcp()
+    {
+        return $this->hasOne(Pro::class, 'id', 'mcp_pro_id');
+    }
+
+    public function rd()
+    {
+        return $this->hasOne(Pro::class, 'id', 'rd_pro_id');
+    }
+
+    public function pcp()
+    {
+        return $this->hasOne(Pro::class, 'id', 'physician_pro_id');
+    }
+
+    public function cm()
+    {
+        return $this->hasOne(Pro::class, 'id', 'cm_pro_id');
+    }
+
+    public function rmm()
+    {
+        return $this->hasOne(Pro::class, 'id', 'rmm_pro_id');
+    }
+
+    public function rme()
+    {
+        return $this->hasOne(Pro::class, 'id', 'rme_pro_id');
+    }
+
+    public function rms()
+    {
+        return $this->hasOne(Pro::class, 'id', 'rms_pro_id');
+    }
+
+    public function rmg()
+    {
+        return $this->hasOne(Pro::class, 'id', 'rmg_pro_id');
+    }
+
+    public function defaultNaPro()
+    {
+        return $this->hasOne(Pro::class, 'id', 'default_na_pro_id');
+    }
+
+    public function creator()
+    {
+        return $this->hasOne(Pro::class, 'id', 'created_by_pro_id');
+    }
+
+    public function prosInMeetingWith()
+    {
+        return Pro::where('in_meeting_with_client_id', $this->id)->get();
+    }
+
+    public function notes()
+    {
+        return $this->hasMany(Note::class, 'client_id', 'id')
+           // ->where('is_core_note', false)
+            ->orderBy('effective_dateest', 'desc')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function prescriptions()
+    {
+        return $this->hasMany(Erx::class, 'client_id', 'id')
+            ->where(function ($q) {
+                $q->whereNull('pro_declared_status')
+                    ->orWhere('pro_declared_status', '<>', 'CANCELLED');
+            })
+            ->orderBy('created_at', 'desc')
+            ->orderByRaw('note_id DESC NULLS LAST');
+    }
+
+    public function prescriptionsCreatedInNote($note)
+    {
+        return Erx::where('client_id', $this->id)
+            ->where('note_id', $note->id)
+            ->orderBy('created_at', 'desc')
+            ->where(function ($q) {
+                $q->whereNull('pro_declared_status')
+                    ->orWhere('pro_declared_status', '<>', 'CANCELLED');
+            })
+            ->get();
+    }
+
+    public function notesAscending()
+    {
+        return $this->hasMany(Note::class, 'client_id', 'id')
+            ->orderBy('effective_dateest', 'asc')
+            ->orderBy('created_at', 'desc');;
+    }
+
+    public function mcCodeChecks(){
+
+	// $tables = DB::select("SELECT * FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'");
+
+	// foreach ($tables as $table) {
+	// dump($table);
+// }
+
+// die();
+	return $this->hasMany(McCodeCheck::class, 'client_id', 'id')->orderBy('created_at', 'asc');
+    }
+
+    public function activeNotes()
+    {
+        return $this->hasMany(Note::class, 'client_id', 'id')
+            ->where('is_cancelled', false)
+            ->where('id', '<>', $this->core_note_id)
+            ->orderBy('effective_dateest', 'desc')
+            ->orderBy('created_at', 'desc');;
+    }
+
+    public function cancelledNotes()
+    {
+        return $this->hasMany(Note::class, 'client_id', 'id')
+            ->where('is_cancelled', true)
+            ->where('id', '<>', $this->core_note_id)
+            ->orderBy('effective_dateest', 'desc')
+            ->orderBy('created_at', 'desc');;
+    }
+
+    public function sections()
+    {
+        return $this->hasMany(Section::class, 'client_id', 'id')
+            ->where('is_active', true)
+            ->orderBy('created_at', 'asc');
+    }
+
+    public function handouts()
+    {
+        $mappings = HandoutClient::where('client_id', $this->id)->get();
+        $handouts = new Collection();
+        foreach ($mappings as $mapping) {
+            $handout = Handout::where('id', $mapping->handout_id)->first();
+            $handout->handout_client_uid = $mapping->uid;
+            $handouts->add($handout);
+        }
+        $handouts = $handouts->sortBy('created_at');
+        return $handouts;
+    }
+
+    public function duplicateOf()
+    {
+        return $this->hasOne(Client::class, 'id', 'duplicate_of_client_id');
+    }
+
+    public function actionItems()
+    {
+        return $this->hasMany(ActionItem::class, 'client_id', 'id')
+            ->orderBy('action_item_category', 'asc')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function infoLines()
+    {
+        return $this->hasMany(ClientInfoLine::class, 'client_id', 'id')->orderBy('created_at', 'desc');
+    }
+
+    public function measurements()
+    {
+        return $this->hasMany(Measurement::class, 'client_id', 'id')
+            /*->distinct('label')*/
+            ->where('is_active', true)
+            ->orderByRaw('ts DESC NULLS LAST');
+    }
+
+    public function recentMeasurements()
+    {
+        return $this->hasMany(Measurement::class, 'client_id', 'id')
+            ->where('is_active', true)
+            ->whereNotNull('label')
+            ->where('label', '<>', 'SBP')
+            ->where('label', '<>', 'DBP')
+            ->where('is_cellular_zero', false)
+            ->orderByRaw('ts DESC NULLS LAST')
+            ->offset(0)->limit(20);
+    }
+
+    public function nonZeroMeasurements()
+    {
+        return $this->hasMany(Measurement::class, 'client_id', 'id')
+            /*->distinct('label')*/
+            ->where('is_active', true)
+            ->where('is_cellular_zero', false)
+            ->orderBy('effective_date', 'desc');
+    }
+
+    public function getNonZeroBpMeasurements(){
+        return $this->hasMany(Measurement::class, 'client_id', 'id')
+            /*->distinct('label')*/
+            ->where('is_active', true)
+            ->where('label', '=', 'BP')
+            ->where('sbp_mm_hg', '>', 0)
+            ->where('dbp_mm_hg', '>', 0)
+            ->orderBy('ts', 'desc');
+    }
+
+    public function getNonZeroWeightMeasurements(){
+        return $this->hasMany(Measurement::class, 'client_id', 'id')
+            /*->distinct('label')*/
+            ->where('is_active', true)
+            ->where('label', '=', 'Wt. (lbs.)')
+            ->where('numeric_value', '>', 0)
+            ->orderBy('ts', 'desc');
+    }
+
+    public function currentCareMonth()
+    {
+        $cmStartDate = strtotime(date('Y-m-d'));
+        $month = date("n", $cmStartDate);
+        $year = date("Y", $cmStartDate);
+        return CareMonth
+            ::where('client_id', $this->id)
+            ->whereRaw('EXTRACT(MONTH FROM start_date) = ?', [$month])
+            ->whereRaw('EXTRACT(YEAR FROM start_date) = ?', [$year])
+            ->first();
+    }
+
+    public function measurementsInCareMonth(CareMonth $careMonth)
+    {
+        $cmStartDate = strtotime($careMonth->start_date);
+        $month = date("n", $cmStartDate);
+        $year = date("Y", $cmStartDate);
+        $measurements = Measurement
+            ::where('client_id', $this->id)
+            ->whereRaw('EXTRACT(MONTH FROM effective_date) = ?', [$month])
+            ->whereRaw('EXTRACT(YEAR FROM effective_date) = ?', [$year])
+            ->where('is_active', true)
+            ->orderBy('ts', 'desc')
+            ->get();
+        return $measurements;
+    }
+
+    public function allMeasurements()
+    {
+        return $this->hasMany(Measurement::class, 'client_id', 'id')
+            ->where('is_active', true)
+            ->whereNull('parent_measurement_id')
+            ->orderBy('label', 'asc')
+            ->orderBy('effective_date', 'desc');
+    }
+
+    public function smses()
+    {
+        return $this->hasMany(ClientSMS::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function documents()
+    {
+        return $this->hasMany(ClientDocument::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function incomingReports() {
+        return $this->hasMany(IncomingReport::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function smsNumbers() {
+        return $this->hasMany(ClientSMSNumber::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function nextMcpAppointment()
+    {
+        return $this->hasOne(Appointment::class, 'id', 'next_mcp_appointment_id');
+    }
+
+    public function lastMcpAppointment()
+    {
+        return $this->hasOne(Appointment::class, 'id', 'previous_mcp_appointment_id');
+    }
+
+    public function lastMeasurementOfType($_type) {
+        return Measurement::where('client_id', $this->id)
+            ->whereNotNull('bdt_measurement_id')
+            ->whereNotNull('ts')
+            ->where('is_cellular_zero', false)
+            ->where('is_active', true)
+            ->where('label', '=', $_type)
+            ->orderBy('ts', 'desc')
+            ->first();
+    }
+
+    public function appointments()
+    {
+        return $this->hasMany(Appointment::class, 'client_id', 'id')
+            ->orderBy('start_time', 'desc');
+    }
+
+    public function appointmentsForProByStatus($forPro = 'all', $status = 'all')
+    {
+        $appointments = Appointment::where('client_id', $this->id);
+        if($forPro !== 'all') {
+            $forPro = Pro::where('uid', $forPro)->first();
+            $appointments = $appointments->where('pro_id', $forPro->id);
+        }
+        if($status !== 'ALL') {
+            $appointments = $appointments->where('status', $status);
+        }
+        $appointments = $appointments->orderBy('raw_date', 'desc')->orderBy('raw_start_time', 'desc');
+        return $appointments->get();
+    }
+
+    public function upcomingAppointments()
+    {
+        return $this->hasMany(Appointment::class, 'client_id', 'id')
+            ->where('raw_date', '>=', date('Y-m-d'))
+            ->whereIn('status', ['PENDING', 'CONFIRMED'])
+            ->orderBy('start_time', 'desc')
+            ->limit(5);
+    }
+
+    public function nextAppointment() {
+        return Appointment
+            ::where('client_id', $this->id)
+            ->where('raw_date', '>=', DB::raw('NOW()'))
+            ->whereIn('status', ['PENDING', 'CONFIRMED'])
+            ->orderBy('start_time')
+            ->first();
+    }
+
+    public function appointmentsFromLastWeek()
+    {
+
+        $dateLastWeek = date_sub(date_create(), date_interval_create_from_date_string("14 days"));
+        $dateLastWeek = date_format($dateLastWeek, "Y-m-d");
+        return $this->hasMany(Appointment::class, 'client_id', 'id')
+            ->where('raw_date', '>=', $dateLastWeek)
+            ->orderBy('start_time', 'desc');
+    }
+
+    public function memos()
+    {
+        return $this->hasMany(ClientMemo::class, 'client_id', 'id')
+            ->where('is_cancelled', false)
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function devices()
+    {
+        return $this->hasMany(ClientBDTDevice::class, 'client_id', 'id')
+            ->where('is_active', true)
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function deactivatedDevices()
+    {
+        return $this->hasMany(ClientBDTDevice::class, 'client_id', 'id')
+            ->where('is_active', false)
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function hasDevice($_device)
+    {
+        $count = ClientBDTDevice::where('client_id', $this->id)
+            ->where('device_id', $_device->id)
+            ->where('is_active', true)
+            ->count();
+        return !!$count;
+    }
+
+    public function deviceMeasurements()
+    {
+        return $this->hasMany(ClientBDTMeasurement::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function activeMcpRequest()
+    {
+        return $this->hasOne(McpRequest::class, 'id', 'active_mcp_request_id');
+    }
+
+    public function clientPrograms()
+    {
+        return $this->hasMany(ClientProgram::class, 'client_id', 'id')
+            ->where('is_active', true)
+            ->orderBy('title', 'desc');
+    }
+
+    public function tickets()
+    {
+        return $this->hasMany(Ticket::class, 'client_id', 'id')
+            ->orderBy('is_open', 'desc')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function mcpDisplayName()
+    {
+    }
+
+    public function rmeDisplayName()
+    {
+    }
+
+    public function supplyOrderForCellularBPDevice() {
+        return SupplyOrder::where('product_id', 1)
+            ->where('is_cancelled', false)
+            ->where('client_id', $this->id)
+            ->orderBy('id', 'desc')
+            ->first();
+    }
+
+    public function supplyOrderForCellularWeightScale() {
+        return SupplyOrder::where('product_id', 2)
+            ->where('is_cancelled', false)
+            ->where('client_id', $this->id)
+            ->orderBy('id', 'desc')
+            ->first();
+    }
+
+    public function firstCellularBPDevice()
+    {
+        $devices = $this->devices;
+        $x = null;
+        foreach($devices as $device){
+            if($device->device->category == 'BP'){
+                $x = $device;
+                break;
+            }
+        }
+        return $x;
+    }
+
+    public function getFirstCellularBPMeasurementAt()
+    {
+    }
+
+    public function getLatestCellularBPMeasurementAt()
+    {
+    }
+
+    public function getTotalCellularBPMeasurements()
+    {
+    }
+
+    public function firstCellularWeightDevice()
+    {
+        $devices = $this->devices;
+        $x = null;
+        foreach($devices as $device){
+            if($device->device->category == 'WEIGHT'){
+                $x = $device;
+                break;
+            }
+        }
+        return $x;
+    }
+
+    public function getFirstCellularWeightMeasurementAt()
+    {
+    }
+
+    public function getLatestCellularWeightMeasurementAt()
+    {
+    }
+
+    public function getTotalCellularWeightMeasurements()
+    {
+    }
+
+    public function prosWithAccess()
+    {
+
+        $pros = [];
+
+        // directly associated pros
+        $pro = $this->mcp;
+        if ($pro && $pro->id) $pros[] = ["pro" => $pro->displayName(), "association" => 'MCP'];
+        $pro = $this->pcp;
+        if ($pro && $pro->id) $pros[] = ["pro" => $pro->displayName(), "association" => 'PCP (Physician)'];
+        $pro = $this->cm;
+        if ($pro && $pro->id) $pros[] = ["pro" => $pro->displayName(), "association" => 'CM'];
+        $pro = $this->rmm;
+        if ($pro && $pro->id) $pros[] = ["pro" => $pro->displayName(), "association" => 'RMM'];
+        $pro = $this->rme;
+        if ($pro && $pro->id) $pros[] = ["pro" => $pro->displayName(), "association" => 'RME'];
+        $pro = $this->defaultNaPro;
+        if ($pro && $pro->id) $pros[] = ["pro" => $pro->displayName(), "association" => 'Care Coordinator'];
+
+        // via client pro access
+        $cpAccesses = ClientProAccess::where('client_id', $this->id)->where('is_active', true)->get();
+        foreach ($cpAccesses as $cpAccess) {
+            if (!$cpAccess->pro) continue;
+            $pros[] = ["pro" => $cpAccess->pro->displayName(), "association" => $cpAccess->reason_category. ' - Via Client Pro Access', 'isClientProAccess'=>true, 'clientProAccess'=>$cpAccess];
+        }
+
+        // via appointments
+        $appointments = Appointment::where('client_id', $this->id)->get();
+        foreach ($appointments as $appointment) {
+            if (!$appointment->pro) continue;
+            $pros[] = ["pro" => $appointment->pro->displayName(), "association" => 'Via Appointment: ' . $appointment->raw_date];
+        }
+
+        // via client program
+        $clientPrograms = ClientProgram::where('client_id', $this->id)->where('is_active', true)->get();
+        foreach ($clientPrograms as $clientProgram) {
+            if ($clientProgram->mcp)
+                $pros[] = ["pro" => $clientProgram->mcp->displayName(), "association" => 'Program MCP: ' . $clientProgram->title];
+            if ($clientProgram->manager)
+                $pros[] = ["pro" => $clientProgram->manager->displayName(), "association" => 'Program Manager: ' . $clientProgram->title];
+        }
+
+        // sort by pro name
+        $name = array_column($pros, 'pro');
+        array_multisort($name, SORT_ASC, $pros);
+
+        return $pros;
+
+    }
+
+    public function mcpRequests()
+    {
+        return $this->hasMany(McpRequest::class, 'for_client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function eligibleRefreshes()
+    {
+        return $this->hasMany(ClientEligibleRefresh::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function mbPayerValidationResults()
+    {
+        return $this->hasMany(ClientMBPayerValidationResult::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function payer()
+    {
+        return $this->hasOne(MBPayer::class, 'id', 'mb_payer_id');
+    }
+
+    public function supplyOrders()
+    {
+        return $this->hasMany(SupplyOrder::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function activeSupplyOrders()
+    {
+        return $this->hasMany(SupplyOrder::class, 'client_id', 'id')
+            ->where('is_cancelled', false)
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function cancelledSupplyOrders()
+    {
+        return $this->hasMany(SupplyOrder::class, 'client_id', 'id')
+            ->where('is_cancelled', true)
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function readyToShipSupplyOrders()
+    {
+        return $this->hasMany(SupplyOrder::class, 'client_id', 'id')
+            ->where('is_cancelled', false)
+            ->where('is_cleared_for_shipment', true)
+            ->whereNull('shipment_id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function shipments()
+    {
+        return $this->hasMany(Shipment::class, 'client_id', 'id')
+            ->where('is_cancelled', false)
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function numSignedNotes() {
+        return Note::where('client_id', $this->id)
+            ->where('is_cancelled', false)
+            ->where('is_signed_by_hcp', true)
+            ->count();
+    }
+
+    public function smsReminders()
+    {
+        return $this->hasMany(SimpleSMSReminder::class, 'client_id', 'id')
+            ->orderBy('created_at', 'asc');
+    }
+
+    public function measurementConfirmationNumbers()
+    {
+        return $this->hasMany(MeasurementConfirmationNumber::class, 'client_id', 'id')
+            ->orderBy('created_at', 'asc');
+    }
+
+    public function shadowOfPro() {
+        return $this->hasOne(Pro::class, 'id', 'shadow_pro_id');
+    }
+
+    public function clientTags()
+    {
+        return $this->hasMany(ClientTag::class, 'client_id', 'id')
+            ->where('is_cancelled', false)
+            ->orderBy('tag', 'asc');
+    }
+
+    public function medicalTeam()
+    {
+        return $this->hasMany(ClientProAccess::class, 'client_id', 'id')
+            ->where('is_active', true)
+            ->whereNotNull('pro_id')
+            ->orderBy('created_at', 'asc');
+    }
+
+    public function accountInvites()
+    {
+        return $this->hasMany(AccountInvite::class, 'for_client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+    public function linkedAccounts()
+    {
+        return $this->hasMany(AccountClient::class, 'client_id', 'id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function pages($_type, $_returnShadows, $_note) {
+        return Page
+            ::where('client_id', $this->id)
+            ->where('note_id', $_note->id)
+            ->where('category', $_type)
+            ->where('is_shadow', !!$_returnShadows)
+            ->orderBy('key', 'ASC')
+            ->get();
+    }
+
+    public function firstPageByCategoryAndKey($_category, $_key) {
+        return Page
+            ::where('client_id', $this->id)
+            ->where('category', $_category)
+            ->where('key', $_key)
+            ->first();
+    }
+
+    public function cmReasons()
+    {
+        return $this->hasMany(ClientCmRmReason::class, 'client_id', 'id')
+            ->where('cm_or_rm', 'CM')
+            ->where('is_removed', false)
+            ->orderBy('position_index', 'ASC')
+            ->orderBy('code', 'ASC');
+    }
+
+    public function rmReasons()
+    {
+        return $this->hasMany(ClientCmRmReason::class, 'client_id', 'id')
+            ->where('cm_or_rm', 'RM')
+            ->where('is_removed', false)
+            ->orderBy('position_index', 'ASC')
+            ->orderBy('code', 'ASC');
+    }
+
+    public function cmSetupNote()
+    {
+        return $this->hasOne(Note::class, 'id', 'cm_setup_note_id');
+    }
+
+    public function rmSetupCareMonth()
+    {
+        return $this->hasOne(CareMonth::class, 'id', 'rm_setup_care_month_id');
+    }
+
+    public function defaultMcpCompanyPro()
+    {
+        return $this->hasOne(CompanyPro::class, 'id', 'default_mcp_company_pro_id');
+    }
+
+    public function defaultMcpCompanyProPayer()
+    {
+        return $this->hasOne(CompanyProPayer::class, 'id', 'default_mcp_company_pro_payer_id');
+    }
+
+    public function defaultMcpCompanyLocation()
+    {
+        return $this->hasOne(CompanyLocation::class, 'id', 'default_mcp_company_location_id');
+    }
+
+    public function hasNewNoteForPro($_pro) {
+        $count = Note::where('client_id', $this->id)->where('hcp_pro_id', $_pro->id)->where('is_cancelled', false)->where('new_or_fu_or_na', 'NEW')->count();
+        return !!$count;
+    }
+
+    public function systemSourcePro()
+    {
+        return $this->hasOne(Pro::class, 'id', 'system_source_pro_id');
+    }
+
+    public function systemSourceProTeam()
+    {
+        return $this->hasOne(ProTeam::class, 'id', 'system_source_pro_team_id');
+    }
+
+    public function adminEngagementAssessmentStatus(){
+        return $this->hasOne(Status::class, 'id', 'admin_engagement_assessment_status_id');
+    }
+
+    public function mcpEngagementAssessmentStatus(){
+        return $this->hasOne(Status::class, 'id', 'mcp_engagement_assessment_status_id');
+    }
+
+    public function defaultNaEngagementAssessmentStatus(){
+        return $this->hasOne(Status::class, 'id', 'default_na_engagement_assessment_status_id');
+    }
+
+    public function clientSelfSatisfactionStatus(){
+        return $this->hasOne(Status::class, 'id', 'client_self_satisfaction_status_id');
+    }
+
+
+    public function recentNotes($_pro = null) {
+        $notes = Note::where('client_id', $this->id)->where('is_cancelled', false);
+        if($_pro) {
+            $notes = $notes->where('hcp_pro_id', $_pro->id);
+        }
+        $notes = $notes->orderBy('effective_dateest', 'DESC')->limit(5)->get();
+        return $notes;
+    }
+
+    public function cmMeasurementsMatrix($_careMonth, $pro = null) {
+
+        $days = [];
+
+        $matches = DB::select(
+            "
+SELECT m.id   AS measurement_id,
+       m.uid  AS measurement_uid,
+       cm.id  AS care_month_id,
+       cm.uid AS care_month_uid,
+       m.label,
+       m.value,
+       m.dbp_mm_hg,
+       m.sbp_mm_hg,
+       m.value_pulse,
+       m.value_irregular,
+       m.numeric_value,
+       m.effective_date,
+       m.ts,
+       m.has_been_stamped_by_mcp,
+       m.has_been_stamped_by_non_hcp
+FROM measurement m
+         JOIN care_month cm ON m.care_month_id = cm.id
+WHERE m.care_month_id = :careMonthID
+        AND m.label NOT IN ('SBP', 'DBP')
+        AND m.bdt_measurement_id IS NOT NULL
+        AND m.is_active IS TRUE
+        AND (m.is_cellular_zero = FALSE or m.is_cellular_zero IS NULL)
+        AND m.ts IS NOT NULL
+        AND m.client_bdt_measurement_id IS NOT NULL
+ORDER BY m.ts DESC
+            ",
+            ['careMonthID' => $_careMonth->id]
+        );
+
+        foreach ($matches as $match) {
+            $time = (floor($match->ts / 1000));
+            $realTimezone = resolve_timezone('EASTERN');
+            $date = new \DateTime("@$time");
+            $date->setTimezone(new \DateTimeZone($realTimezone));
+            $match->date = $date->format("m/d/Y");
+            $match->dateYMD = $date->format("Y-m-d");
+            $match->time = $date->format("h:i A");
+
+            // get existing entries for listing
+            $match->entries = CareMonthEntry::where('care_month_id', $match->care_month_id)
+                ->where('is_removed', false)
+                ->where('effective_date', $match->dateYMD);
+
+            if(!!$pro) {
+                $match->entries = $match->entries->where('pro_id', $pro->id);
+            }
+
+            $match->entries = $match->entries->orderBy('created_at')->get();
+
+            if(!isset($days[$match->date])) {
+                $days[$match->date] = [];
+            }
+            $days[$match->date][] = $match;
+        }
+
+        return $days;
+
+    }
+
+    public function getPrimaryCoverage()
+    {
+        $coverage = $this->latestClientPrimaryCoverage;
+        return $coverage;
+    }
+
+    // return value will be YES, NO or UNKNOWN
+    public function getPrimaryCoverageStatus() {
+        $coverage = $this->getPrimaryCoverage();
+        if(!$coverage) return 'NO';
+        return $coverage->getStatus();
+    }
+
+    public function getMcpAssignedOn() {
+        $change = ClientProChange::where('client_id', $this->id)
+            ->where('new_pro_id', $this->mcp_pro_id)
+            ->where('responsibility_type', 'MCP')
+            ->orderBy('created_at', 'DESC')
+            ->first();
+        if(!!$change) {
+            return friendlier_date($change->created_at);
+        }
+        return '-';
+    }
+
+    public function coreNote(){
+        return $this->hasOne(Note::class, 'id', 'core_note_id');
+    }
+
+    public function mostRecentCompletedMcpNote(){
+        return $this->hasOne(Note::class, 'id', 'most_recent_completed_mcp_note_id');
+    }
+
+    
+    public function nonCoreVisitNotes() {
+        return $this->hasMany(Note::class, 'client_id', 'id')
+            ->where('id', '<>', $this->core_note_id)
+            ->whereNotNull('visit_template_id')
+            ->orderBy('created_at', 'desc');
+    }
+
+    public function mostRecentWeightMeasurement(){        
+        return $this->hasOne(Measurement::class, 'id', 'most_recent_weight_measurement_id');
+    }
+
+    public function hasBPDevice() {
+        $cbds = ClientBDTDevice::where('client_id', $this->id)->get();
+        foreach ($cbds as $cbd) {
+            if($cbd->is_active && !!$cbd->device && $cbd->device->is_active && $cbd->device->category === 'BP') return true;
+        }
+        return false;
+    }
+    public function hasWeightScaleDevice() {
+        $cbds = ClientBDTDevice::where('client_id', $this->id)->get();
+        foreach ($cbds as $cbd) {
+            if($cbd->is_active && !!$cbd->device && $cbd->device->is_active && $cbd->device->category === 'WEIGHT') return true;
+        }
+        return false;
+    }
+}

+ 10 - 0
app/Models/ClientAllyUpdate.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientAllyUpdate extends Model
+{
+    protected $table = 'client_ally_update';
+}

+ 34 - 0
app/Models/ClientBDTDevice.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Models;
+
+class ClientBDTDevice extends Model
+{
+    protected $table = 'client_bdt_device';
+
+    public function device() {
+        return $this->hasOne(BDTDevice::class, 'id', 'device_id');
+    }
+
+    public function client() {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function mostRecentMeasurement() {
+        return $this->hasOne(ClientBDTMeasurement::class, 'id', 'most_recent_client_bdt_measurement_id');
+    }
+
+
+    public function lastDeviceMeasurement() {
+        return BDTMeasurement::select('bdt_measurement.created_at', 'measurement.label', 'measurement.sbp_mm_hg', 'measurement.dbp_mm_hg', 'measurement.numeric_value')
+            ->join('client_bdt_measurement', 'client_bdt_measurement.bdt_measurement_id', '=', 'bdt_measurement.id')
+            ->join('measurement', 'measurement.client_bdt_measurement_id', '=', 'client_bdt_measurement.id')
+            ->where('bdt_measurement.is_cellular_zero', '<>', true)
+            ->whereNotNull('bdt_measurement.ts')
+            ->where('client_bdt_measurement.client_id', $this->client_id)
+            ->where('bdt_measurement.imei', $this->device->imei)
+            ->orderBy('bdt_measurement.created_at', 'DESC')
+            ->first();
+    }
+
+}

+ 17 - 0
app/Models/ClientBDTMeasurement.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Models;
+
+class ClientBDTMeasurement extends Model
+{
+    protected $table = 'client_bdt_measurement';
+
+    public function measurement() {
+        return $this->hasOne(Measurement::class, 'id', 'measurement_id');
+    }
+
+    public function client() {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+}

+ 11 - 0
app/Models/ClientCanvasDataCustomItem.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientCanvasDataCustomItem extends Model
+{
+    protected $table = 'client_canvas_data_custom_item';
+
+}

+ 10 - 0
app/Models/ClientCmRmReason.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientCmRmReason extends Model
+{
+    protected $table = 'client_cm_rm_reason';
+}

+ 10 - 0
app/Models/ClientDocument.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientDocument extends Model
+{
+    protected $table = 'client_document';
+}

+ 19 - 0
app/Models/ClientEligibleRefresh.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Models;
+
+use Carbon\Carbon;
+use DateTime;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientEligibleRefresh extends Model
+{
+    protected $table = 'client_eligible_refresh';
+
+    public function medicareResult()
+    {
+        return $this->hasOne(MedicareResult::class, 'id', 'medicare_result_id');
+    }
+
+}

+ 28 - 0
app/Models/ClientInfoLine.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientInfoLine extends Model
+{
+    protected $table = 'client_info_line';
+
+    public function createdBySession() {
+        return $this->hasOne(AppSession::class, 'id', 'created_by_session_id');
+    }
+
+    public function contentDetail($prop = null) {
+        $x = json_decode($this->content_detail);
+	if(!$prop){        
+		return $x;	
+	}else{
+		$x = json_decode($this->content_detail, true);
+		if(isset($x[$prop])){
+			return $x[$prop];
+		}else{
+			return '';
+		}
+	}
+    }
+}

+ 10 - 0
app/Models/ClientInfoLineUpdate.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientInfoLineUpdate extends Model
+{
+    protected $table = 'client_info_line_update';
+}

+ 19 - 0
app/Models/ClientMBPayerValidationResult.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App\Models;
+
+use Carbon\Carbon;
+use DateTime;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientMBPayerValidationResult extends Model
+{
+    protected $table = 'client_mb_payer_validation_result';
+
+    public function mbPayer()
+    {
+        return $this->hasOne(MBPayer::class, 'id', 'mb_payer_id');
+    }
+
+}

+ 10 - 0
app/Models/ClientMcpUpdate.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientMcpUpdate extends Model
+{
+    //
+}

+ 15 - 0
app/Models/ClientMeasurementDaysPerMonth.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\HasOne;
+use Illuminate\Support\Collection;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientMeasurementDaysPerMonth extends Model
+{
+
+    protected $table = 'client_measurement_days_per_month';
+
+}

+ 31 - 0
app/Models/ClientMemo.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
+
+class ClientMemo extends Model
+{
+    protected $table = 'client_memo';
+
+    public function createdBy(): HasOne
+    {
+        return $this->hasOne(AppSession::class, 'id', 'created_by_session_id');
+    }
+
+    public function client(): HasOne
+    {
+        return $this->hasOne(Client::class, 'id', 'client_id');
+    }
+
+    public function updates(): HasMany{
+        return $this->hasMany(ClientMemoUpdate::class, 'client_memo_id', 'id')->orderBy('created_at', 'DESC');
+    }
+
+    public function stamp(): HasOne
+    {
+        return $this->hasOne(Stamp::class, 'client_memo_id', 'id');
+    }
+
+}

+ 17 - 0
app/Models/ClientMemoUpdate.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasOne;
+
+class ClientMemoUpdate extends Model
+{
+    protected $table = 'client_memo_update';
+
+    public function createdBy(): HasOne
+    {
+        return $this->hasOne(AppSession::class, 'id', 'created_by_session_id');
+    }
+
+}

+ 10 - 0
app/Models/ClientPointUpdate.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ClientPointUpdate extends Model
+{
+    //
+}

+ 73 - 0
app/Models/ClientPrimaryCoverage.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class ClientPrimaryCoverage extends Model
+{    
+    protected $table = 'client_primary_coverage';
+
+    public function getStatus() {
+        $status = 'NO';
+
+        // if medicare, check is_partbprimary
+        if($this->plan_type === 'MEDICARE') {
+            $status = $this->is_partbprimary;
+        }
+        else {
+            if(!$this->is_manually_determined) {
+                // AUTO determination of non-medicare not yet supported
+                $status = 'NO';
+            }
+            else {
+                switch($this->manual_determination_category) {
+                    case 'COVERED':
+                        $status = 'YES';
+                        break;
+                    case 'NOT_COVERED':
+                    case 'INVALID':
+                        $status = 'NO';
+                        break;
+                    default:
+                        $status = $this->manual_determination_category;
+                        break;
+                }
+            }
+        }
+
+        return $status;
+    }
+
+    public function insuranceDisplayName(){
+        $coverageName = $this->toString();
+        if(stripos($coverageName, 'medicare') !== false) return 'Medicare';
+        if(stripos($coverageName, 'medicaid') !== false) return 'Medicaid';
+        if(stripos($coverageName, 'commercial') !== false) return 'Commercial';
+        return null;
+
+    }
+
+    public function toString() {
+        $parts = [];
+        $parts[] = $this->plan_type;
+        if($this->plan_type === 'MEDICARE') {
+            if($this->is_partbprimary === 'YES') {
+                $parts[] = 'Part B';
+            }
+        }
+        else {
+            if(@$this->plan_name) $parts[] = ' / ' . $this->plan_name;
+            if(@$this->plan_identifier) $parts[] = ' / ' . $this->plan_identifier;
+        }
+        return implode(" ", $parts);
+    }
+
+    public function payer(){
+        return $this->hasOne(Payer::class, 'id', 'commercial_payer_id');
+    }
+    public function mcdPayer(){
+        return $this->hasOne(Payer::class, 'id', 'mcd_payer_id');
+    }
+}

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