Просмотр исходного кода

Merge branch 'dev' of rav.triplestart.com:jmudaka/stagfe2

= 3 лет назад
Родитель
Сommit
7d9f7cddfa

+ 84 - 3
app/Http/Controllers/PracticeManagementController.php

@@ -2338,20 +2338,101 @@ ORDER BY c.name_last, c.name_first
     public function clientsBdtDevices(Request $request){
         $filters = $request->all();
 
-        $devices = ClientBDTDevice::select('client_bdt_device.*')
+        $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.created_at', 'date_category', 'date_value_1', 'date_value_2');
+        $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('created_at', 'DESC')->paginate(20);
+
+        $devices = $devices->orderBy('num_of_measurements', 'DESC')->paginate(20);
         return view('app.practice-management.clients_bdt_devices', compact('devices','filters'));
     }
 

+ 15 - 12
app/Http/Controllers/StatTreeController.php

@@ -92,6 +92,18 @@ class StatTreeController extends Controller
         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!");
@@ -126,15 +138,6 @@ class StatTreeController extends Controller
         return view('app.stat-tree.stat-trees.sub.edit', compact('statTree', 'linesFlat'));
     }
 
-    public function delete(Request $request){
-        $request->validate([
-            'id' => 'required'
-        ]);
-        $id = $request->get('id');
-        $statTree = StatTree::where('id', $id)->first();
-
-    }
-
     public function replaceAllLines(Request $request){
 
         $parents = [];
@@ -371,7 +374,7 @@ class StatTreeController extends Controller
             foreach ($lineClause->clause->clauseArgs as $clauseArg) {
 
                 $value = null;
-                foreach ($lineClause->statTreeLineClauseArgs as $lineClauseArg) {
+                foreach ($lineClause->lineClauseArgs as $lineClauseArg) {
                     if($lineClauseArg->clause_arg_id === $clauseArg->id) {
                         $value = $lineClauseArg->default_value;
                     }
@@ -469,7 +472,7 @@ class StatTreeController extends Controller
         // clause
         $lineClause = $line->displayLineClause();
         $args = [];
-        foreach($lineClause->statTreeLineClauseArgs as $stlcArg) {
+        foreach($lineClause->lineClauseArgs as $stlcArg) {
             if($stlcArg->clauseArg) {
                 $args[] = [
                     "arg_text" => $stlcArg->clauseArg->arg_text,
@@ -584,7 +587,7 @@ class StatTreeController extends Controller
                 $statTreeLineClause->save();
 
                 // copy clause args for parent line clauses
-                foreach ($parentLineClause->statTreeLineClauseArgs as $parentLineClauseArg) {
+                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;

+ 26 - 5
app/Http/Controllers/StatTreeLineController.php

@@ -10,6 +10,7 @@ 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;
 
@@ -86,9 +87,11 @@ class StatTreeLineController extends Controller
     }
 
     public function viewData(Request $request, StatTreeLine $line) {
+        $total = 0;
         $rows = [];
         $columns = [];
         $selectColumns = [];
+        $paginator = null;
         foreach ($line->reportColumns as $reportColumn) {
             $columns[] = [
                 "label" => $reportColumn->label,
@@ -98,9 +101,13 @@ class StatTreeLineController extends Controller
             $selectColumns[] = "{$reportColumn->display_key} as v_{$reportColumn->id}";
         }
         if(count($line->reportColumns)) {
-            $rows = $this->queryStatTreeLineData($line, $selectColumns);
+            $result = $this->queryStatTreeLineData($line, $selectColumns, $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', 'rows', 'columns'));
+        return view('app.stat-tree.stat-tree-lines.view-data', compact('line', 'total', 'rows', 'columns', 'paginator'));
     }
 
     public function refreshCountQuery(Request $request)
@@ -150,7 +157,7 @@ class StatTreeLineController extends Controller
         return DB::select($query);
     }
 
-    protected function queryStatTreeLineData(StatTreeLine $statTreeLine, $selectColumns)
+    protected function queryStatTreeLineData(StatTreeLine $statTreeLine, $selectColumns, Request $request)
     {
 
         $model = $statTreeLine->statTree->model;
@@ -163,7 +170,7 @@ class StatTreeLineController extends Controller
             foreach ($lineClause->clause->clauseArgs as $clauseArg) {
 
                 $value = null;
-                foreach ($lineClause->statTreeLineClauseArgs as $lineClauseArg) {
+                foreach ($lineClause->lineClauseArgs as $lineClauseArg) {
                     if($lineClauseArg->clause_arg_id === $clauseArg->id) {
                         $value = $lineClauseArg->default_value;
                     }
@@ -188,10 +195,24 @@ class StatTreeLineController extends Controller
             $clauses[] = str_replace('@PRO_ID', $statTreeLine->statTree->pro->id, $statTreeLine->statTree->pro_scope_clause);
         }
 
-        $query = 'SELECT ' . implode(", ", $selectColumns) . ' FROM '.$model.' WHERE '. implode(" AND ", $clauses);
+        $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';

+ 5 - 0
app/Models/ClientBDTDevice.php

@@ -14,6 +14,11 @@ class ClientBDTDevice extends Model
         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')

+ 1 - 1
app/Models/ClientBDTMeasurement.php

@@ -7,7 +7,7 @@ class ClientBDTMeasurement extends Model
     protected $table = 'client_bdt_measurement';
 
     public function measurement() {
-        return $this->hasOne(BDTMeasurement::class, 'id', 'bdt_measurement_id');
+        return $this->hasOne(Measurement::class, 'id', 'measurement_id');
     }
 
     public function client() {

+ 2 - 2
app/Models/StatTreeLine.php

@@ -67,7 +67,7 @@ class StatTreeLine extends Model
         $args = [];
         $displayLineClause = $this->displayLineClause();
         if(!!$displayLineClause) {
-            foreach($displayLineClause->statTreeLineClauseArgs as $stlcArg) {
+            foreach($displayLineClause->lineClauseArgs as $stlcArg) {
                 $args[] = [
                     "uid" => $stlcArg->uid,
                     "arg_text" => $stlcArg->clauseArg->arg_text,
@@ -106,7 +106,7 @@ class StatTreeLine extends Model
             $newStatTreeLineClause->save();
 
             // line clause args
-            foreach ($srcLineClause->statTreeLineClauseArgs as $srcLineClauseArg) {
+            foreach ($srcLineClause->lineClauseArgs as $srcLineClauseArg) {
                 $newStatTreeLineClauseArg = new StatTreeLineClauseArg();
                 $nextId = DB::select("select nextval('stat_tree_line_clause_arg_id_seq')");
                 $newStatTreeLineClauseArg->id = $nextId[0]->nextval;

+ 1 - 1
app/Models/StatTreeLineClause.php

@@ -14,7 +14,7 @@ class StatTreeLineClause extends Model
         return $this->hasOne(Clause::class, 'id', 'clause_id');
     }
 
-    public function statTreeLineClauseArgs() {
+    public function lineClauseArgs() {
         return $this->hasMany(StatTreeLineClauseArg::class, 'stat_tree_line_clause_id', 'id')->orderBy('id');
     }
 

+ 26 - 9
public/css/style.css

@@ -2761,6 +2761,9 @@ table.stag-compact-grid>tbody>tr>td [if-grid-view] {
     height: 100%;
     border-radius: 0;
 }
+.tree-column-header.has-changes {
+    background: #ffeeee !important;
+}
 .stat-tree-view .stat-tree-node {
     padding: 0.1rem 0.3rem;
 }
@@ -2814,6 +2817,18 @@ table.stag-compact-grid>tbody>tr>td [if-grid-view] {
 .stat-tree-view .jstree-anchor.has-drop-visualization.drop-76-100::after {
     color: red;
 }
+.mv-drop-0-25 {
+    color: grey;
+}
+.mv-drop-26-50 {
+    color: saddlebrown;
+}
+.mv-drop-51-75 {
+    color: #dc7024;
+}
+.mv-drop-76-100 {
+    color: red;
+}
 .multi-pro-view .line-count-label {
     display: none;
 }
@@ -2829,10 +2844,11 @@ table.stag-compact-grid>tbody>tr>td [if-grid-view] {
     color: #777;
     border: 0;
     border-left: 1px solid #e1e1e1;
-    width: 100px;
+    width: 120px;
 }
 .multi-pro-view .multi-pro-stats-table tr th .pro-label {
-    max-width: 100px;
+    min-width: 120px;
+    max-width: 120px;
     overflow: hidden;
     white-space: nowrap;
     text-overflow: ellipsis;
@@ -2841,7 +2857,7 @@ table.stag-compact-grid>tbody>tr>td [if-grid-view] {
     border-top: 0;
     padding: 0 5px;
     border-left: 1px solid #f1f1f1;
-    width: 100px;
+    width: 120px;
 }
 .multi-pro-view .multi-pro-stats-table tr th:last-child,
 .multi-pro-view .multi-pro-stats-table tr td:last-child {
@@ -2857,15 +2873,16 @@ table.stag-compact-grid>tbody>tr>td [if-grid-view] {
     padding-left: 4px;
 }
 .multi-pro-view .count-label .count {
-    min-width: 30px;
-    /*text-align: right;*/
-    margin-right: 5px;
+    white-space: nowrap;
+    width: 50%;
+    border-right: 1px solid #e7e7e7;
+    text-align: right;
+    padding-right: 5px;
 }
 .multi-pro-view .count-label .drop-percent {
-    width: 41px;
-    font-size: 80%;
+    font-size: 78%;
     opacity: 0.8;
-    text-align: right;
+    margin-left: 5px;
 }
 .multi-pro-view .multi-pro-view-stat-label {
     height: 22px;

+ 29 - 22
resources/views/app/practice-management/clients_bdt_devices.blade.php

@@ -18,13 +18,16 @@
             <table class="table table-sm table-striped border-top p-0 m-0">
                 <thead class="bg-light">
                     <tr>
-                        <th class="px-3 border-0">IMEI</th>
-                        <th class="px-3 border-0">Client</th>
-                        <th class="px-3 border-0">MCP</th>
-                        <th class="px-3 border-0">Created</th>
-                        <th class="px-3 border-0">Category</th>
-                        <th class="px-3 border-0">Last Measurement</th>
-                        <th class="px-3 border-0">Status</th>
+                        <th class="px-2 border-0">IMEI</th>
+                        <th class="px-2 border-0">Client</th>
+                        <th class="px-2 border-0">MCP</th>
+                        <th class="px-2 border-0">Created</th>
+                        <th class="px-2 border-0">Category</th>
+                        <th class="px-2 border-0"># of Non-zero Measurements</th>
+                        <!-- <th class="px-2 border-0">Most Recent Non-zero Measurement At</th> -->
+                        <th class="px-2 border-0">Most Recent Measurement At</th>
+                        <th class="px-2 border-0">Most Recent Measurement</th>
+                        <th class="px-2 border-0">Status</th>
                     </tr>
                 </thead>
                 <tbody>
@@ -44,23 +47,27 @@
                         <td>{{ $mcpName }}</td>
                         <td class="px-2 text-nowrap">{{ friendly_date_time($device->device->created_at) }}</td>
                         <td class="px-2">{{ $device->device->category }}</td>
+                        <td class="px-2">{{ $device->num_of_measurements}}</td>
+                        <!-- <td class="px-2">{{ friendly_date_time($device->most_recent_none_zero_measurement_at)}}</td> -->
+                        <td class="px-2">{{ friendly_date_time($device->most_recent_measurement_at)}}</td>
                         <td class="px-2 d-flex align-items-center">
-                            <?php $lastMeasurement = $device->lastDeviceMeasurement(); ?>
-                            @if($lastMeasurement)
-                            @if($lastMeasurement->is_cellular_zero)
-                            <i class="font-size-11 fa fa-rss"></i>
-                            @elseif($lastMeasurement->label === 'BP')
-                            {{ $lastMeasurement->sbp_mm_hg }} / {{ $lastMeasurement->dbp_mm_hg }}
-                            @elseif($lastMeasurement->label === 'Wt. (lbs.)')
-                            {{ round($lastMeasurement->numeric_value, 2) }} lbs
+                            <?php $lastMeasurement = $device->mostRecentMeasurement; ?>
+                            @if($lastMeasurement && $lastMeasurement->measurement)
+                                @if($lastMeasurement->measurement->is_cellular_zero)
+                                    <i class="font-size-11 fa fa-rss"></i>
+                                @elseif($device->device->category === 'BP')
+                                    {{ $lastMeasurement->measurement->sbp_mm_hg }} / {{ $lastMeasurement->measurement->dbp_mm_hg }}
+                                @elseif($device->device->category === 'WEIGHT')
+                                    {{ round($lastMeasurement->measurement->numeric_value, 2) }} lbs
+                                @else
+                                    {{ $lastMeasurement->measurement->value }}
+                                @endif
+                                <div class="ml-2">
+                                    <i class="far fa-calendar-check"></i> <span class="text-secondary">{{ friendly_date_time($lastMeasurement->ts_date_time) }}</span>
+                                </div>
                             @else
-                            {{ $lastMeasurement->value }}
-                            @endif
-                            <div class="ml-2">
-                                <i class="far fa-calendar-check"></i> <span class="text-secondary">{{ friendly_date_time($lastMeasurement->created_at) }}</span>
-                            </div>
-                            @else
-                            <small class="text-muted">-</small>
+                                <h1>No measurement</h1>
+                                <small class="text-muted">-</small>
                             @endif
                         </td>
                         <td>

+ 25 - 1
resources/views/app/practice-management/clients_bdt_devices_filters.blade.php

@@ -22,7 +22,7 @@
 	<!-- DATE	 -->
 	<div>
 		<div class="form-group">
-			<label>Date:</label>
+			<label>Most Recent Measurement Date:</label>
 			<select name="date_category" class="form-control input-sm" v-model="filters.date_category">
 				<option value="">All</option>
 				<option value="EXACTLY">Exactly</option>
@@ -41,6 +41,27 @@
 			</div>
 		</div>
 	</div>
+	<div>
+		<div class="form-group">
+			<label># of Non-zero Measurements:</label>
+			<select name="measurement_count_category" class="form-control input-sm" v-model="filters.measurement_count_category">
+				<option value="">All</option>
+				<option value="EXACTLY">Exactly</option>
+				<option value="LESS_THAN">Less Than</option>
+				<option value="GREATER_THAN">Greater Than</option>
+				<option value="BETWEEN">Between</option>
+				<option value="NOT_BETWEEN">Not Between</option>
+			</select>
+			<div v-show="filters.measurement_count_category" class="mt-2">
+				<div>
+					<input name="measurement_count_value_1" v-model="filters.measurement_count_value_1" type="number" class="form-control input-sm"/>
+				</div>
+				<div v-show="filters.measurement_count_category === 'BETWEEN' || filters.measurement_count_category === 'NOT_BETWEEN'" class="mt-2">
+					<input name="measurement_count_value_2" v-model="filters.measurement_count_value_2" type="number" class="form-control input-sm"/>
+				</div>
+			</div>
+		</div>
+	</div>
 	<!-- STATUS -->
 	<div>
 		<div class="form-group">
@@ -69,6 +90,9 @@ $allFilterKeys = [
 	'date_category',
 	'date_value_1',
 	'date_value_2',
+	'measurement_count_category',
+	'measurement_count_value_1',
+	'measurement_count_value_2',
 	'status'
 ];
 for ($i = 0; $i < count($allFilterKeys); $i++) {

+ 9 - 0
resources/views/app/stat-tree/_sort_header.blade.php

@@ -0,0 +1,9 @@
+<b class="{{request()->input('sort_by') === $key ? 'text-primary' : 'text-secondary'}}">{{$label}}</b>
+<span class="ml-2 d-inline-flex align-items-baseline">
+    <a href="{{ $route }}?{{queryLineExcept(['sort_by', 'sort_dir', 'page'])}}&sort_by={{$key}}&sort_dir=DESC" class="ml-1">
+        <i class="fa fa-arrow-up {{request()->input('sort_by') === $key && request()->input('sort_dir') === 'DESC' ? '' : 'on-hover-opaque text-secondary'}}"></i>
+    </a>
+    <a href="{{ $route }}?{{queryLineExcept(['sort_by', 'sort_dir', 'page'])}}&sort_by={{$key}}&sort_dir=ASC" class="ml-2">
+        <i class="fa fa-arrow-down {{request()->input('sort_by') === $key && request()->input('sort_dir') === 'ASC' ? '' : 'on-hover-opaque text-secondary'}}"></i>
+    </a>
+</span>

+ 16 - 2
resources/views/app/stat-tree/stat-tree-lines/view-data.blade.php

@@ -13,14 +13,27 @@
         @else
             <div class="mb-2 d-flex flex-wrap">
                 @foreach($line->lineClauses as $lineClause)
-                    <div class="border rounded px-2 py-1 mr-2 bg-light">{{$lineClause->clause_label}}</div>
+                    <div class="border rounded px-2 py-1 mr-2 bg-light">{{$lineClause->clause_label}}
+                        @if(count($lineClause->lineClauseArgs))
+                            <?php
+                            $argsLabel = [];
+                            foreach($lineClause->lineClauseArgs as $lineClauseArg) {
+                                $argLabel = $lineClauseArg->clauseArg->arg_text . ': ' . $lineClauseArg->default_value;
+                                $argsLabel[] = $argLabel;
+                            }
+                            ?>
+                            ({{implode(', ', $argsLabel)}})
+                        @endif
+                    </div>
                 @endforeach
             </div>
             <table class="table table-sm table-striped table-bordered">
                 <thead>
                 <tr>
                     @foreach($columns as $column)
-                    <th class="border-bottom-0 text-left">{{$column['label']}}</th>
+                    <th class="border-bottom-0 text-left">
+                        @include('app.stat-tree._sort_header', ['route' => route('practice-management.statTreeLines.view-data', compact('line')), 'label' => $column['label'], 'key' => $column['as']])
+                    </th>
                     @endforeach
                 </tr>
                 </thead>
@@ -34,6 +47,7 @@
                 @endforeach
                 </tbody>
             </table>
+            {!! $paginator->withQueryString()->links() !!}
         @endif
     @endif
 </div>

+ 13 - 0
resources/views/app/stat-tree/stat-trees/list.blade.php

@@ -126,6 +126,19 @@
                                                     </form>
                                                 </div>
                                             @endif
+                                            <span class="mx-2 text-secondary text-sm">|</span>
+                                            <div moe relative>
+                                                <a href="#" start show class="text-nowrap"><i class="text-danger on-hover-opaque fa fa-trash-alt"></i></a>
+                                                <form url="{{ route('practice-management.api.statTree.remove', $statTree) }}" right>
+                                                    @csrf
+                                                    <input type="hidden" name="uid" value="{{$statTree->uid}}">
+                                                    <p class="mb-2 font-weight-bold">Remove this tree?</p>
+                                                    <div class="d-flex align-items-center">
+                                                        <button class="btn btn-sm btn-danger mr-2" type="button" submit>Delete</button>
+                                                        <button class="btn btn-sm btn-default mr-2 border" type="button" cancel>Cancel</button>
+                                                    </div>
+                                                </form>
+                                            </div>
                                         </div>
                                     </td>
                                 </tr>

+ 201 - 11
resources/views/app/stat-tree/stat-trees/sub/edit.blade.php

@@ -270,7 +270,7 @@
                         <div class="row m-0 h-100">
                             <div class="{{$multiProView ? 'col-12' : 'col-6'}} tree-lhs-column border-right h-100 p-0">
                                 <div class="d-flex flex-column h-100">
-                                    <div class="d-flex align-items-center pl-2 height-35px border-bottom bg-light">
+                                    <div class="d-flex align-items-center pl-2 height-35px border-bottom bg-light tree-column-header">
                                         <h6 class="font-weight-bold m-0 text-secondary">{{$statTree->name}}</h6>
                                         <span class="text-danger d-none if-changed ml-2 text-sm">(modified *)</span>
                                         @if($multiProView)
@@ -294,7 +294,7 @@
                                         <a href="#" class="tree-collapse-all ml-2" title="Collapse All"><i class="fa fa-angle-double-up text-secondary"></i></a>
                                         <div class="ml-auto mr-2 d-none if-changed">
                                             <a href="#" class="btn btn-sm btn-primary text-white" id="btn-save-tree">Save</a>
-                                            <a href="#" class="ml-2 btn btn-sm btn-default border text-dark" onclick="return fastReload()">Reset</a>
+                                            <a href="#" class="ml-1 btn btn-sm btn-default border text-dark bg-white" onclick="return fastReload()">Reset</a>
                                         </div>
                                     </div>
                                     <div class="flex-grow-1 overflow-overlay-on-hover">
@@ -302,12 +302,12 @@
                                             <div class="stat-tree-view overflow-auto min-height-300px" id="stat-tree-edit-{{$statTree->id}}"></div>
                                         @else
                                             <div class="d-flex align-items-start">
-                                                <div class="">
+                                                <div class="mv-tree">
                                                     <div class="multi-pro-view-stat-label">Stat</div>
                                                     <div class="stat-tree-view overflow-auto" id="stat-tree-edit-{{$statTree->id}}"></div>
                                                 </div>
-                                                <div class="flex-grow-1">
-                                                    <table class="table table-sm multi-pro-stats-table">
+                                                <div class="overflow-auto mv-table">
+                                                    <table class="table table-sm multi-pro-stats-table w-auto m-0">
                                                         <thead>
                                                         <tr>
                                                             <th {{$statTree->pro ? 'data-pro-uid="' . $statTree->pro->uid . '"' : ''}}>
@@ -318,27 +318,25 @@
                                                                     <div class="pro-label" title="{{$multiPro->name_display}}">{{$multiPro->name_display}}</div>
                                                                 </th>
                                                             @endforeach
-                                                            <th></th>
                                                         </tr>
                                                         </thead>
                                                         <tbody>
                                                         @foreach($linesFlat as $line)
-                                                            <tr data-line-uid="{{$line->uid}}">
+                                                            <tr data-line-uid="{{$line->uid}}" {!! $line->parent ? 'data-parent-line-uid="' . $line->parent->uid . '"' : '' !!}>
                                                                 <td class="p-0">
                                                                     <div class="count-label">
                                                                         <div class="count">{{$line->last_refresh_count}}</div>
-                                                                        <div class="drop-percent">50%  ⤵</div>
+                                                                        <div class="drop-percent"></div>
                                                                     </div>
                                                                 </td>
                                                                 @foreach($multiPros as $multiPro)
                                                                     <td class="p-0" data-pro-uid="{{$multiPro->uid}}">
                                                                         <div class="count-label">
-                                                                            <div class="count">0</div>
-                                                                            <div class="drop-percent">0%  ⤵</div>
+                                                                            <div class="count"></div>
+                                                                            <div class="drop-percent"></div>
                                                                         </div>
                                                                     </td>
                                                                 @endforeach
-                                                                <td></td>
                                                             </tr>
                                                         @endforeach
                                                         </tbody>
@@ -533,14 +531,17 @@
                             let StatTree = {
                                 el: $('#stat-tree-edit-{{$statTree->id}}'),
                                 changed: false,
+                                copiedColumns: null,
 
                                 setDirty: function(_changed = true) {
                                     this.changed = _changed;
                                     if(_changed) {
                                         $('.if-changed').removeClass('d-none');
+                                        $('.tree-column-header').addClass('has-changes');
                                     }
                                     else {
                                         $('.if-changed').addClass('d-none');
+                                        $('.tree-column-header').removeClass('has-changes');
                                     }
                                 },
 
@@ -654,6 +655,41 @@
                                                                     },
                                                                     separator_after: true,
                                                                 },
+                                                                "copy_columns": {
+                                                                    "label": "<span class='text-sm'>Copy Columns</span>",
+                                                                    "_disabled": !(node.data && node.data.columns && node.data.columns.length),
+                                                                    "action": function (obj) {
+                                                                        let selected = StatTree.selectedNode();
+                                                                        if(selected) {
+                                                                            localStorage.stPasteBuffer_Columns = JSON.stringify(selected.data.columns);
+                                                                        }
+                                                                    },
+                                                                },
+                                                                "paste_columns": {
+                                                                    "label": "<span class='text-sm'>Paste Columns</span>",
+                                                                    "_disabled": !localStorage.stPasteBuffer_Columns,
+                                                                    "action": function (obj) {
+                                                                        let selected = StatTree.selectedNode();
+                                                                        if(selected) {
+                                                                            StatTree.setSelectedNodeColumns(JSON.parse(localStorage.stPasteBuffer_Columns));
+                                                                            StatTree.onSelected();
+                                                                            StatTree.setDirty();
+                                                                        }
+                                                                    },
+                                                                },
+                                                                "paste_columns_deep": {
+                                                                    "label": "<span class='text-sm'>Paste Columns Recursively</span>",
+                                                                    "_disabled": !localStorage.stPasteBuffer_Columns,
+                                                                    "action": function (obj) {
+                                                                        let selected = StatTree.selectedNode();
+                                                                        if(selected) {
+                                                                            StatTree.pasteColumnsDeep(selected, localStorage.stPasteBuffer_Columns);
+                                                                            StatTree.onSelected();
+                                                                            StatTree.setDirty();
+                                                                        }
+                                                                    },
+                                                                    separator_after: true,
+                                                                },
                                                                 "remove": {
                                                                     "label": "<span class='text-sm'>Remove</span>",
                                                                     "action": function (obj) {
@@ -662,6 +698,16 @@
                                                                             StatTree.el.jstree(true).delete_node(selected.id);
                                                                             StatTree.setDirty();
                                                                         }
+                                                                    },
+                                                                    separator_after: true,
+                                                                },
+                                                                "permutate_child_tree": {
+                                                                    "label": "<span class='text-sm'>Create Child Permutations</span>",
+                                                                    "action": function (obj) {
+                                                                        let selected = StatTree.selectedNode();
+                                                                        if(selected) {
+                                                                            StatTree.createChildTreePermutations(selected);
+                                                                        }
                                                                     }
                                                                 }
                                                             }
@@ -683,6 +729,120 @@
                                     }, 'json').then(hideMask);
                                 },
 
+                                createChildTreePermutations: function(_node) {
+
+                                    // permute: thanks: https://stackoverflow.com/a/37580979/921204
+                                    function _permute(permutation) {
+                                        var length = permutation.length,
+                                            result = [permutation.slice()],
+                                            c = new Array(length).fill(0),
+                                            i = 1, k, p;
+
+                                        while (i < length) {
+                                            if (c[i] < i) {
+                                                k = i % 2 && c[i];
+                                                p = permutation[i];
+                                                permutation[i] = permutation[k];
+                                                permutation[k] = p;
+                                                ++c[i];
+                                                i = 1;
+                                                result.push(permutation.slice());
+                                            } else {
+                                                c[i] = 0;
+                                                ++i;
+                                            }
+                                        }
+                                        return result;
+                                    }
+
+                                    // ensure 1 child for selected node
+                                    if(_node.children.length !== 1) {
+                                        toastr.error('Line must have exactly one child!');
+                                        return;
+                                    }
+
+                                    // ensure all nodes down the node in the tree have only one child (if any)
+                                    function _hasSingleChild(_id) {
+                                        let node = StatTree.el.jstree(true).get_node(_id);
+                                        if(node.children.length === 0) return true; // no children
+                                        if(node.children.length > 1) return false; // more than 1 child, stop already!
+                                        return _hasSingleChild(node.children[0]);
+                                    }
+                                    if(!_hasSingleChild(_node.id)) {
+                                        toastr.error('Single linear child tree structure needed under selected node!');
+                                        return;
+                                    }
+
+                                    // get an array of line "data" of the child tree
+                                    function _getData(_id, _dataArray, _displayLabelArray) {
+                                        let node = StatTree.el.jstree(true).get_node(_id);
+                                        _dataArray.push(node.data);
+                                        _displayLabelArray.push(node.data.displayLabel);
+                                        if(node.children.length) {
+                                            _getData(node.children[0], _dataArray, _displayLabelArray);
+                                        }
+                                    }
+                                    let dataArray = [], displayLabelArray = [];
+                                    _getData(_node.children[0], dataArray, displayLabelArray);
+                                    let dataMap = {};
+                                    for (let i = 0; i < dataArray.length; i++) {
+                                        dataMap[dataArray[i].displayLabel] = JSON.parse(JSON.stringify(dataArray[i]));
+                                        delete dataMap[dataArray[i].displayLabel].dropPercent;
+                                        delete dataMap[dataArray[i].displayLabel].id;
+                                        delete dataMap[dataArray[i].displayLabel].lastRefreshCount;
+                                        delete dataMap[dataArray[i].displayLabel].treeOrderPositionIndex;
+                                        delete dataMap[dataArray[i].displayLabel].uid;
+                                    }
+
+                                    // get all permutations
+                                    let permutations = _permute(displayLabelArray);
+
+                                    // create child tree for each permutation
+                                    function _childTreeFromArray(_array, _index = 0) {
+                                        let children = [];
+                                        if(_index < _array.length - 1) {
+                                            children.push(_childTreeFromArray(_array, _index + 1));
+                                        }
+                                        return {
+                                            text: '<span class="stat-tree-anchor">' + _array[_index] + '</span><span class="ml-2 text-secondary line-count-label">(…)</span>',
+                                            state: {
+                                                opened: true,
+                                                disabled: false,
+                                                selected: false,
+                                            },
+                                            children: children,
+                                            data: {
+                                                    type: "stat_tree_line",
+                                                    displayLabel: _array[_index],
+                                                    columns: dataMap[_array[_index]].columns,
+                                                    clause: dataMap[_array[_index]].clause
+                                            },
+                                            a_attr: {
+                                                title: dataMap[_array[_index]].clause.clause_text
+                                            }
+                                        };
+                                    }
+
+                                    // clear current children
+                                    this.el.jstree(true).delete_node(_node.children[0]);
+
+                                    // create new child trees
+                                    for (let i = 0; i < permutations.length; i++) {
+                                        let subTree = _childTreeFromArray(permutations[i]);
+                                        this.el.jstree(true).create_node(_node.id, JSON.parse(JSON.stringify(subTree)));
+                                    }
+
+                                    this.setDirty();
+
+                                },
+
+                                pasteColumnsDeep: function(_node, _strColumns) {
+                                    _node.data.columns = JSON.parse(_strColumns);
+                                    for (let i = 0; i < _node.children.length; i++) {
+                                        this.pasteColumnsDeep(this.el.jstree(true).get_node(_node.children[i]), _strColumns);
+                                    }
+                                },
+
                                 dropVisualize: function() {
                                     console.log('redrawn')
                                     function calculateDropPercent(node, parent = null) {
@@ -1137,6 +1297,34 @@
                                     for(let x in _data) {
                                         $('.multi-pro-stats-table tr[data-line-uid="' + x + '"] td[data-pro-uid="' + $(this).attr('data-pro-uid') + '"] .count').text(_data[x]);
                                     }
+
+                                    // calculate drop %
+                                    $('.multi-pro-stats-table tr[data-line-uid][data-parent-line-uid]').each(function() {
+                                        for (let i = 0; i < $(this).find('>td').length; i++) {
+                                            let parentCount = $('.multi-pro-stats-table tr[data-line-uid="' + $(this).attr('data-parent-line-uid') + '"]>td:eq(' + i + ') .count').first().text(),
+                                                myCount = $(this).find('>td:eq(' + i + ') .count').first().text();
+                                            if(parentCount && myCount && !Number.isNaN(+parentCount) && !Number.isNaN(+myCount) && +parentCount > 0) {
+                                                parentCount = +parentCount;
+                                                myCount = +myCount;
+                                                let element = $(this).find('>td:eq(' + i + ') .drop-percent').first(), cssClass = '';
+                                                let dropPercent = ((parentCount - myCount) / parentCount) * 100;
+                                                if(dropPercent > 75) {
+                                                    cssClass = 'mv-drop-76-100';
+                                                }
+                                                else if(dropPercent > 50 && dropPercent <= 75) {
+                                                    cssClass = 'mv-drop-51-75';
+                                                }
+                                                else if(dropPercent > 25 && dropPercent <= 50) {
+                                                    cssClass = 'mv-drop-26-50';
+                                                }
+                                                else if(dropPercent > 0 && dropPercent <= 25) {
+                                                    cssClass = 'mv-drop-0-25';
+                                                }
+                                                element.removeClass().addClass('drop-percent').addClass(cssClass).text(dropPercent.toFixed(1) + '% ⤵');
+                                            }
+                                        }
+                                    });
+
                                 }, 'json');
                             });
                             @endif
@@ -1152,6 +1340,8 @@
                             initVSplitter('stat-tree-edit-1', $('.clauses-column'), $('.tree-column'));
                             @if(!$multiProView)
                                 initVSplitter('stat-tree-edit-2', $('.tree-lhs-column'), $('.tree-rhs-column'));
+                            @else
+                                initVSplitter('stat-tree-edit-3', $('.mv-tree'), $('.mv-table'));
                             @endif
 
                         }

+ 1 - 1
routes/web.php

@@ -364,7 +364,7 @@ Route::middleware('pro.auth')->group(function () {
             //Stat Tree
             Route::name('statTree.')->prefix('stat-tree/')->group(function () {
                 Route::post('create', 'StatTreeController@create')->name('create');
-                Route::post('delete', 'StatTreeController@delete')->name('delete');
+                Route::post('remove', 'StatTreeController@remove')->name('remove');
                 Route::post('update-basic', 'StatTreeController@updateBasic')->name('updateBasic');
                 Route::post('refresh-count', 'StatTreeController@refreshCount')->name('refreshCount');
                 Route::post('replace-all-lines', 'StatTreeController@replaceAllLines')->name('replaceAllLines');