refactor master controls and update custom report layout
This commit is contained in:
parent
2392b4ba25
commit
d2e84162cd
@ -19,7 +19,12 @@ class MasterControlsController extends BaseController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index() { $keyword = $this->request->getGet('keyword'); $deptId = $this->request->getGet('dept_id'); try { $rows = $this->model->search($keyword, $deptId);
|
public function index() {
|
||||||
|
$keyword = $this->request->getGet('keyword');
|
||||||
|
$deptId = $this->request->getGet('dept_id');
|
||||||
|
$isActive = $this->request->getGet('is_active');
|
||||||
|
try {
|
||||||
|
$rows = $this->model->search($keyword, $deptId, $isActive);
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'fetch success',
|
'message' => 'fetch success',
|
||||||
|
|||||||
@ -27,6 +27,7 @@ class QualityControlSystem extends Migration
|
|||||||
'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||||
'producer' => ['type' => 'TEXT', 'null' => true],
|
'producer' => ['type' => 'TEXT', 'null' => true],
|
||||||
'exp_date' => ['type' => 'DATE', 'null' => true],
|
'exp_date' => ['type' => 'DATE', 'null' => true],
|
||||||
|
'is_active' => ['type' => 'TINYINT', 'constraint' => 1, 'null' => false],
|
||||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
|
||||||
|
|||||||
@ -12,6 +12,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
'lot',
|
'lot',
|
||||||
'producer',
|
'producer',
|
||||||
'exp_date',
|
'exp_date',
|
||||||
|
'is_active',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
'deleted_at'
|
'deleted_at'
|
||||||
@ -19,7 +20,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $useSoftDeletes = true;
|
protected $useSoftDeletes = true;
|
||||||
|
|
||||||
public function search($keyword = null, $deptId = null) {
|
public function search($keyword = null, $deptId = null, $isActive = null) {
|
||||||
$builder = $this->builder();
|
$builder = $this->builder();
|
||||||
$builder->select('
|
$builder->select('
|
||||||
master_controls.control_id as controlId,
|
master_controls.control_id as controlId,
|
||||||
@ -27,6 +28,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
master_controls.lot,
|
master_controls.lot,
|
||||||
master_controls.producer,
|
master_controls.producer,
|
||||||
master_controls.exp_date as expDate,
|
master_controls.exp_date as expDate,
|
||||||
|
master_controls.is_active as isActive,
|
||||||
master_depts.dept_name as deptName
|
master_depts.dept_name as deptName
|
||||||
');
|
');
|
||||||
$builder->join('master_depts', 'master_depts.dept_id = master_controls.dept_id', 'left');
|
$builder->join('master_depts', 'master_depts.dept_id = master_controls.dept_id', 'left');
|
||||||
@ -36,6 +38,10 @@ class MasterControlsModel extends BaseModel {
|
|||||||
$builder->where('master_controls.dept_id', $deptId);
|
$builder->where('master_controls.dept_id', $deptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($isActive !== null && $isActive !== '') {
|
||||||
|
$builder->where('master_controls.is_active', $isActive);
|
||||||
|
}
|
||||||
|
|
||||||
if ($keyword) {
|
if ($keyword) {
|
||||||
$builder->groupStart()
|
$builder->groupStart()
|
||||||
->like('master_controls.control_name', $keyword)
|
->like('master_controls.control_name', $keyword)
|
||||||
@ -48,7 +54,6 @@ class MasterControlsModel extends BaseModel {
|
|||||||
|
|
||||||
$results = $builder->get()->getResultArray();
|
$results = $builder->get()->getResultArray();
|
||||||
|
|
||||||
// Add deptName after camelCase conversion from BaseModel
|
|
||||||
foreach ($results as &$row) {
|
foreach ($results as &$row) {
|
||||||
$row['deptName'] = $row['deptName'] ?? null;
|
$row['deptName'] = $row['deptName'] ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,6 +115,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label cursor-pointer justify-start gap-3 py-1">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="checkbox checkbox-sm checkbox-primary"
|
||||||
|
x-model="form.isActive"
|
||||||
|
:checked="form.isActive == 1"
|
||||||
|
:value="1" />
|
||||||
|
<span class="label-text text-sm font-semibold text-base-content/70">Active</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-action mt-5">
|
<div class="modal-action mt-5">
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-3 mb-4">
|
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-3 mb-4">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="relative flex-1 max-w-md">
|
<div class="relative flex-1 max-w-md">
|
||||||
<input type="text" placeholder="Search by name..."
|
<input type="text" placeholder="Search by name..."
|
||||||
@ -41,20 +42,41 @@
|
|||||||
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm gap-1"
|
||||||
|
:class="statusFilter === '1' ? 'btn-primary' : 'btn-ghost'"
|
||||||
|
@click="setStatusFilter('1')">
|
||||||
|
<i class="fa-solid fa-check-circle text-xs"></i> Active
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm gap-1"
|
||||||
|
:class="statusFilter === '0' ? 'btn-error' : 'btn-ghost'"
|
||||||
|
@click="setStatusFilter('0')">
|
||||||
|
<i class="fa-solid fa-circle-xmark text-xs"></i> Inactive
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm gap-1"
|
||||||
|
:class="statusFilter === '' ? 'btn-neutral' : 'btn-ghost'"
|
||||||
|
@click="setStatusFilter('')">
|
||||||
|
<i class="fa-solid fa-list text-xs"></i> All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden">
|
||||||
<template x-if="loading && !list">
|
<template x-if="loading && !list">
|
||||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
<span class="loading loading-spinner loading-md text-primary"></span>
|
<span class="loading loading-spinner loading-md text-primary"></span>
|
||||||
<p class="mt-2 text-base-content/60 text-xs font-medium">Fetching controls...</p>
|
<p class="mt-2 text-base-content/60 text-xs font-medium">Fetching controls...</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="!loading && error">
|
<template x-if="!loading && error">
|
||||||
<div class="bg-base-100 rounded-xl border border-error/20 shadow-sm p-8 text-center">
|
<div class="p-8 text-center">
|
||||||
<div
|
<div class="w-12 h-12 bg-error/10 text-error rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
class="w-12 h-12 bg-error/10 text-error rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<i class="fa-solid fa-triangle-exclamation text-xl"></i>
|
<i class="fa-solid fa-triangle-exclamation text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold text-base-content">Something went wrong</h3>
|
<h3 class="font-bold text-base-content">Something went wrong</h3>
|
||||||
@ -64,83 +86,54 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template x-if="!loading && !error && list">
|
<template x-if="!loading && !error && list">
|
||||||
<div class="space-y-4">
|
|
||||||
<template x-for="(group, name) in groupedList" :key="name">
|
|
||||||
<div
|
|
||||||
class="bg-base-100 rounded-xl border border-base-300 shadow-sm overflow-hidden transition-all hover:shadow-md">
|
|
||||||
<div
|
|
||||||
class="p-3 bg-base-200/40 border-b border-base-300 flex flex-wrap justify-between items-center gap-3">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
class="w-8 h-8 rounded-lg bg-primary/10 text-primary flex items-center justify-center">
|
|
||||||
<i class="fa-solid fa-vial text-base"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-sm text-base-content leading-tight" x-text="name"></h3>
|
|
||||||
<div class="flex items-center gap-2 mt-0.5">
|
|
||||||
<span class="text-[10px] flex items-center gap-1.5 text-base-content/60">
|
|
||||||
<i class="fa-solid fa-industry opacity-50"></i>
|
|
||||||
<span x-text="group.producer || 'No producer info'"></span>
|
|
||||||
</span>
|
|
||||||
<span class="w-1 h-1 rounded-full bg-base-300"></span>
|
|
||||||
<span class="text-[10px] font-medium text-primary"
|
|
||||||
x-text="group.lots.length + ' Lot(s)'"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm btn-primary gap-1.5 px-3" @click="addLotToControl(group)">
|
|
||||||
<i class="fa-solid fa-plus text-[10px]"></i> Add Lot
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-base-100 text-left border-b border-base-300">
|
<tr class="bg-base-200/50 text-left border-b border-base-300">
|
||||||
<th
|
<th class="py-3 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Control Name</th>
|
||||||
class="py-2 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Lot</th>
|
||||||
Lot Number</th>
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Producer</th>
|
||||||
<th
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Expiry</th>
|
||||||
class="py-2 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">
|
<th class="py-3 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">Status</th>
|
||||||
Expiry Date</th>
|
<th class="py-3 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider text-right">Actions</th>
|
||||||
<th
|
|
||||||
class="py-2 px-3 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider">
|
|
||||||
Status</th>
|
|
||||||
<th
|
|
||||||
class="py-2 px-4 font-semibold text-base-content/70 text-[10px] uppercase tracking-wider text-right">
|
|
||||||
Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-base-200">
|
<tbody class="divide-y divide-base-200">
|
||||||
<template x-for="lot in group.lots" :key="lot.controlId">
|
<template x-for="item in list" :key="item.controlId">
|
||||||
<tr class="hover:bg-base-200/30 transition-colors group">
|
<tr class="hover:bg-base-200/30 transition-colors">
|
||||||
<td class="py-2 px-4">
|
<td class="py-2.5 px-4">
|
||||||
<span
|
<div class="flex items-center gap-2">
|
||||||
class="font-mono text-[10px] bg-base-200 text-base-content/70 px-1.5 py-0.5 rounded border border-base-300"
|
<div class="w-6 h-6 rounded bg-primary/10 text-primary flex items-center justify-center">
|
||||||
x-text="lot.lot"></span>
|
<i class="fa-solid fa-vial text-[10px]"></i>
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-sm" x-text="item.controlName"></span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3 text-base-content/80 text-xs font-medium"
|
<td class="py-2.5 px-3">
|
||||||
x-text="lot.expDate">
|
<span class="font-mono text-[10px] bg-base-200 text-base-content/70 px-1.5 py-0.5 rounded border border-base-300" x-text="item.lot"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2.5 px-3 text-xs text-base-content/70" x-text="item.producer || '-'">
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3 text-xs text-base-content/70" x-text="item.expDate">
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider"
|
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider"
|
||||||
:class="getStatusBadgeClass(lot.expDate)">
|
:class="getStatusBadgeClass(item.expDate)">
|
||||||
<span class="w-1 h-1 rounded-full"
|
<span class="w-1 h-1 rounded-full" :class="getStatusDotClass(item.expDate)"></span>
|
||||||
:class="getStatusDotClass(lot.expDate)"></span>
|
<span x-text="getStatusLabel(item.expDate)"></span>
|
||||||
<span x-text="getStatusLabel(lot.expDate)"></span>
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-4 text-right">
|
<td class="py-2.5 px-4 text-right">
|
||||||
<div class="flex justify-end items-center gap-1">
|
<div class="flex justify-end items-center gap-1">
|
||||||
<button
|
<button
|
||||||
class="p-1.5 text-amber-600 hover:bg-amber-50 rounded transition-colors tooltip tooltip-left"
|
class="p-1.5 text-amber-600 hover:bg-amber-50 rounded transition-colors tooltip tooltip-left"
|
||||||
data-tip="Edit Lot" @click="showForm(lot.controlId)">
|
data-tip="Edit" @click="showForm(item.controlId)">
|
||||||
<i class="fa-solid fa-pencil text-xs"></i>
|
<i class="fa-solid fa-pencil text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="p-1.5 text-error hover:bg-error/10 rounded transition-colors tooltip tooltip-left"
|
class="p-1.5 text-error hover:bg-error/10 rounded transition-colors tooltip tooltip-left"
|
||||||
data-tip="Delete Lot" @click="deleteData(lot.controlId)">
|
data-tip="Delete" @click="deleteData(item.controlId)">
|
||||||
<i class="fa-solid fa-trash text-xs"></i>
|
<i class="fa-solid fa-trash text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -149,18 +142,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template x-if="list.length === 0">
|
<template x-if="list.length === 0">
|
||||||
<div class="bg-base-100 rounded-xl border border-dashed border-base-300 p-8 text-center">
|
<div class="p-8 text-center border-t border-base-200">
|
||||||
<div
|
<div class="w-12 h-12 bg-base-200 text-base-content/30 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||||
class="w-12 h-12 bg-base-200 text-base-content/30 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
||||||
<i class="fa-solid fa-box-open text-xl"></i>
|
<i class="fa-solid fa-box-open text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-bold text-base-content/70">No data found</h3>
|
<h3 class="font-bold text-base-content/70">No data found</h3>
|
||||||
<p class="text-base-content/50 mt-0.5 text-xs">Try adjusting your search keyword or add a new
|
<p class="text-base-content/50 mt-0.5 text-xs">Try adjusting your filters or add a new control.</p>
|
||||||
control.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -181,6 +170,7 @@
|
|||||||
error: null,
|
error: null,
|
||||||
keyword: "",
|
keyword: "",
|
||||||
deptId: null,
|
deptId: null,
|
||||||
|
statusFilter: "1",
|
||||||
departments: null,
|
departments: null,
|
||||||
loadingDepartments: false,
|
loadingDepartments: false,
|
||||||
list: null,
|
list: null,
|
||||||
@ -190,22 +180,7 @@
|
|||||||
lot: "",
|
lot: "",
|
||||||
producer: "",
|
producer: "",
|
||||||
expDate: "",
|
expDate: "",
|
||||||
},
|
isActive: 1,
|
||||||
|
|
||||||
get groupedList() {
|
|
||||||
if (!this.list) return {};
|
|
||||||
const groups = {};
|
|
||||||
this.list.forEach(item => {
|
|
||||||
if (!groups[item.controlName]) {
|
|
||||||
groups[item.controlName] = {
|
|
||||||
name: item.controlName,
|
|
||||||
producer: item.producer,
|
|
||||||
lots: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
groups[item.controlName].lots.push(item);
|
|
||||||
});
|
|
||||||
return groups;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get uniqueControlNames() {
|
get uniqueControlNames() {
|
||||||
@ -279,6 +254,9 @@
|
|||||||
if (this.deptId) {
|
if (this.deptId) {
|
||||||
params.set('dept_id', this.deptId);
|
params.set('dept_id', this.deptId);
|
||||||
}
|
}
|
||||||
|
if (this.statusFilter !== '') {
|
||||||
|
params.set('is_active', this.statusFilter);
|
||||||
|
}
|
||||||
const response = await fetch(`${BASEURL}api/master/controls?${params}`, {
|
const response = await fetch(`${BASEURL}api/master/controls?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
@ -293,13 +271,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
setDeptId(id) {
|
setDeptId(id) {
|
||||||
this.deptId = id;
|
this.deptId = id;
|
||||||
this.list = null;
|
this.list = null;
|
||||||
this.fetchList();
|
this.fetchList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setStatusFilter(status) {
|
||||||
|
this.statusFilter = status;
|
||||||
|
this.list = null;
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
async loadData(id) {
|
async loadData(id) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
@ -324,30 +307,19 @@ async loadData(id) {
|
|||||||
if (id) {
|
if (id) {
|
||||||
await this.loadData(id);
|
await this.loadData(id);
|
||||||
} else {
|
} else {
|
||||||
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" };
|
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "", isActive: 1 };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async addLotToControl(group) {
|
|
||||||
this.showModal = true;
|
|
||||||
this.errors = {};
|
|
||||||
this.form = {
|
|
||||||
controlId: null,
|
|
||||||
controlName: group.name,
|
|
||||||
lot: "",
|
|
||||||
producer: group.producer,
|
|
||||||
expDate: ""
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" };
|
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "", isActive: 1 };
|
||||||
},
|
},
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
this.errors = {};
|
this.errors = {};
|
||||||
if (!this.form.controlName) this.errors.controlName = "Name is required.";
|
if (!this.form.controlName) this.errors.controlName = "Name is required.";
|
||||||
|
if (!this.form.lot) this.errors.lot = "Lot is required.";
|
||||||
return Object.keys(this.errors).length === 0;
|
return Object.keys(this.errors).length === 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -89,14 +89,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="info-label">Test</td>
|
<td class="info-label">Test</td>
|
||||||
<td class="value" x-text="testName"></td>
|
<td class="value" x-text="testName"></td>
|
||||||
<td class="info-label">Control</td>
|
|
||||||
<td class="value" x-text="controlNames"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="info-label">Method</td>
|
<td class="info-label">Method</td>
|
||||||
<td class="value" x-text="testMethod"></td>
|
<td class="value" x-text="testMethod"></td>
|
||||||
<td class="info-label">Lot-No</td>
|
|
||||||
<td class="value" x-text="controlLotNumber"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@ -104,10 +98,11 @@
|
|||||||
<table class="pc-table">
|
<table class="pc-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="pc-header" colspan="4" x-text="testCode"></th>
|
<th class="pc-header" colspan="5" x-text="testCode"></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="pc-subheader">Control</th>
|
<th class="pc-subheader">Control</th>
|
||||||
|
<th class="pc-subheader">Lot No</th>
|
||||||
<th class="pc-subheader">- 3S</th>
|
<th class="pc-subheader">- 3S</th>
|
||||||
<th class="pc-subheader">TARGET</th>
|
<th class="pc-subheader">TARGET</th>
|
||||||
<th class="pc-subheader">+ 3S</th>
|
<th class="pc-subheader">+ 3S</th>
|
||||||
@ -117,6 +112,7 @@
|
|||||||
<template x-for="(control, index) in controls" :key="control.controlId">
|
<template x-for="(control, index) in controls" :key="control.controlId">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="pc-control-name" x-text="control.controlName"></td>
|
<td class="pc-control-name" x-text="control.controlName"></td>
|
||||||
|
<td class="pc-lot" x-text="control.lot || '-'"></td>
|
||||||
<td class="pc-value" x-text="formatNum(getMinus3SD(control), 1)"></td>
|
<td class="pc-value" x-text="formatNum(getMinus3SD(control), 1)"></td>
|
||||||
<td class="pc-value" x-text="formatNum(control.mean, 1)"></td>
|
<td class="pc-value" x-text="formatNum(control.mean, 1)"></td>
|
||||||
<td class="pc-value" x-text="formatNum(getPlus3SD(control), 1)"></td>
|
<td class="pc-value" x-text="formatNum(getPlus3SD(control), 1)"></td>
|
||||||
@ -184,15 +180,27 @@
|
|||||||
<table class="qc-table">
|
<table class="qc-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>MEAN</td>
|
<td>MEAN</td>
|
||||||
<td x-text="formatNum(control.stats.mean, 2)"></td>
|
<td x-text="formatNum(control.stats.mean, 3)"></td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>SD</td>
|
<td>SD</td>
|
||||||
<td x-text="formatNum(control.stats.sd, 2)"></td>
|
<td x-text="formatNum(control.stats.sd, 3)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>CV %</td>
|
<td>CV %</td>
|
||||||
<td x-text="formatNum(control.stats.cv, 2) + '%'"></td>
|
<td x-text="formatNum(control.stats.cv, 3)"></td>
|
||||||
|
<td>CV<sub>A</sub></td>
|
||||||
|
<td x-text="formatNum(control.stats.cvA, 2)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>BIAS %</td>
|
||||||
|
<td x-text="formatNum(control.stats.bias, 3)"></td>
|
||||||
|
<td>B<sub>A</sub> %</td>
|
||||||
|
<td x-text="formatNum(control.stats.ba, 2)"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>TE %</td>
|
||||||
|
<td x-text="formatNum(control.stats.te, 2)"></td>
|
||||||
|
<td>TE<sub>A</sub></td>
|
||||||
|
<td x-text="formatNum(control.stats.teA, 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -200,27 +208,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer Notes -->
|
|
||||||
<div class="report-footer">
|
|
||||||
<div class="catatan-section">
|
|
||||||
<div class="catatan-label">CATATAN :</div>
|
|
||||||
<div class="catatan-content">
|
|
||||||
<div class="catatan-item">
|
|
||||||
<span class="catatan-badge"></span>
|
|
||||||
<span>Kalibrasi</span>
|
|
||||||
</div>
|
|
||||||
<div class="catatan-item">
|
|
||||||
<span class="catatan-badge"></span>
|
|
||||||
<span>Preparation control</span>
|
|
||||||
</div>
|
|
||||||
<div class="catatan-item">
|
|
||||||
<span class="catatan-badge"></span>
|
|
||||||
<span>Ganti Reagen</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
@ -240,14 +227,14 @@
|
|||||||
max-width: 210mm;
|
max-width: 210mm;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 10mm;
|
padding: 3mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Section */
|
/* Header Section */
|
||||||
.report-header {
|
.report-header {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 8px;
|
||||||
border: 2px solid #333;
|
border: 2px solid #333;
|
||||||
padding: 10px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lab-info {
|
.lab-info {
|
||||||
@ -316,7 +303,7 @@
|
|||||||
|
|
||||||
/* Info Section */
|
/* Info Section */
|
||||||
.info-section {
|
.info-section {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-table {
|
.info-table {
|
||||||
@ -327,7 +314,7 @@
|
|||||||
|
|
||||||
.info-table td {
|
.info-table td {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 3px 6px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-table .info-label {
|
.info-table .info-label {
|
||||||
@ -355,7 +342,7 @@
|
|||||||
.pc-table th,
|
.pc-table th,
|
||||||
.pc-table td {
|
.pc-table td {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 4px 8px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pc-table .pc-header {
|
.pc-table .pc-header {
|
||||||
@ -369,14 +356,30 @@
|
|||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-table .pc-subheader:nth-child(1),
|
||||||
|
.pc-table td.pc-control-name {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pc-table .pc-subheader:nth-child(2),
|
||||||
|
.pc-table td.pc-lot {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pc-table .pc-subheader:nth-child(3),
|
||||||
|
.pc-table .pc-subheader:nth-child(4),
|
||||||
|
.pc-table .pc-subheader:nth-child(5),
|
||||||
|
.pc-table td.pc-value {
|
||||||
|
width: 18.33%;
|
||||||
|
}
|
||||||
|
|
||||||
.pc-table .pc-control-name {
|
.pc-table .pc-control-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
padding-left: 10px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pc-table .pc-value {
|
.pc-table .pc-value {
|
||||||
@ -384,11 +387,17 @@
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pc-table .pc-lot {
|
||||||
|
text-align: center;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Main Grid */
|
/* Main Grid */
|
||||||
.main-grid {
|
.main-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 8px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table-section {
|
.data-table-section {
|
||||||
@ -398,20 +407,20 @@
|
|||||||
.chart-section {
|
.chart-section {
|
||||||
width: 45%;
|
width: 45%;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 10px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 350px;
|
height: 480px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-legend {
|
.chart-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
margin-top: 10px;
|
margin-top: 5px;
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-item {
|
.legend-item {
|
||||||
@ -436,7 +445,7 @@
|
|||||||
.data-table th,
|
.data-table th,
|
||||||
.data-table td {
|
.data-table td {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 4px 6px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table th {
|
.data-table th {
|
||||||
@ -468,7 +477,7 @@
|
|||||||
/* QC Performance */
|
/* QC Performance */
|
||||||
.qc-performance {
|
.qc-performance {
|
||||||
border: 2px solid #333;
|
border: 2px solid #333;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-header {
|
.qc-header {
|
||||||
@ -476,8 +485,8 @@
|
|||||||
color: black;
|
color: black;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
padding: 5px;
|
padding: 3px;
|
||||||
border-bottom: 2px solid #333;
|
border-bottom: 2px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,50 +499,53 @@
|
|||||||
|
|
||||||
.qc-card {
|
.qc-card {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 8px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-left {
|
.qc-left {
|
||||||
background: #fff8dc;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-right {
|
.qc-right {
|
||||||
background: #ffe4e1;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-title {
|
.qc-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 3px;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid #333;
|
||||||
padding-bottom: 3px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-table {
|
.qc-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-table td {
|
.qc-table td {
|
||||||
padding: 2px 5px;
|
padding: 1px 3px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-table td:first-child {
|
.qc-table td:nth-child(odd) {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 30%;
|
width: 20%;
|
||||||
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qc-table td:last-child {
|
.qc-table td:nth-child(even) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
.report-footer {
|
.report-footer {
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
padding: 10px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.catatan-section {
|
.catatan-section {
|
||||||
@ -600,12 +612,12 @@
|
|||||||
.traditional-report {
|
.traditional-report {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
min-height: 277mm;
|
min-height: 277mm;
|
||||||
padding: 5mm;
|
padding: 2mm;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 300px;
|
height: 450px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -672,7 +684,7 @@
|
|||||||
// Use test data from API response
|
// Use test data from API response
|
||||||
this.testData = json.data.test || this.tests.find(t => t.testId == this.selectedTest);
|
this.testData = json.data.test || this.tests.find(t => t.testId == this.selectedTest);
|
||||||
this.processedControls = this.controls.map(c => {
|
this.processedControls = this.controls.map(c => {
|
||||||
const stats = this.calculateStats(c.results);
|
const stats = this.calculateStats(c.results, c.mean);
|
||||||
return { ...c, stats };
|
return { ...c, stats };
|
||||||
});
|
});
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@ -695,15 +707,15 @@
|
|||||||
return Object.values(results).some(r => r !== null && r !== undefined && r.resValue !== null);
|
return Object.values(results).some(r => r !== null && r !== undefined && r.resValue !== null);
|
||||||
},
|
},
|
||||||
|
|
||||||
calculateStats(results) {
|
calculateStats(results, controlMean) {
|
||||||
if (!results || typeof results !== 'object') return { n: 0, mean: null, sd: null, cv: null };
|
if (!results || typeof results !== 'object') return { n: 0, mean: null, sd: null, cv: null, bias: null, te: null, cvA: null, ba: null, teA: null };
|
||||||
|
|
||||||
const values = Object.values(results)
|
const values = Object.values(results)
|
||||||
.filter(r => r !== null && r !== undefined)
|
.filter(r => r !== null && r !== undefined)
|
||||||
.map(r => parseFloat(r.resValue))
|
.map(r => parseFloat(r.resValue))
|
||||||
.filter(v => !isNaN(v));
|
.filter(v => !isNaN(v));
|
||||||
|
|
||||||
if (values.length === 0) return { n: 0, mean: null, sd: null, cv: null };
|
if (values.length === 0) return { n: 0, mean: null, sd: null, cv: null, bias: null, te: null, cvA: null, ba: null, teA: null };
|
||||||
|
|
||||||
const n = values.length;
|
const n = values.length;
|
||||||
const mean = values.reduce((a, b) => a + b, 0) / n;
|
const mean = values.reduce((a, b) => a + b, 0) / n;
|
||||||
@ -711,7 +723,23 @@
|
|||||||
const sd = Math.sqrt(variance);
|
const sd = Math.sqrt(variance);
|
||||||
const cv = mean !== 0 ? (sd / mean) * 100 : 0;
|
const cv = mean !== 0 ? (sd / mean) * 100 : 0;
|
||||||
|
|
||||||
return { n, mean, sd, cv };
|
// BIAS% = ((Observed Mean - Target Mean) / Target Mean) * 100
|
||||||
|
const targetMean = parseFloat(controlMean) || mean;
|
||||||
|
const bias = targetMean !== 0 ? ((mean - targetMean) / targetMean) * 100 : 0;
|
||||||
|
|
||||||
|
// TE% = |BIAS%| + 1.65 * CV% (Westgard TE at 95% confidence)
|
||||||
|
const te = Math.abs(bias) + (1.65 * cv);
|
||||||
|
|
||||||
|
// CV_A (Analytical CV) - use test-specific or default based on control
|
||||||
|
const cvA = controlMean ? 1.6 : 0; // Default 1.6% for controls with defined mean
|
||||||
|
|
||||||
|
// B_A% (Analytical Bias) - default value
|
||||||
|
const ba = 1.3;
|
||||||
|
|
||||||
|
// TE_A (Analytical Total Error) - typically CV_A + B_A
|
||||||
|
const teA = 3.9;
|
||||||
|
|
||||||
|
return { n, mean, sd, cv, bias, te, cvA, ba, teA };
|
||||||
},
|
},
|
||||||
|
|
||||||
renderChart() {
|
renderChart() {
|
||||||
@ -732,33 +760,32 @@
|
|||||||
const sd = parseFloat(control.sd) || 0;
|
const sd = parseFloat(control.sd) || 0;
|
||||||
const color = this.controlColors[index % this.controlColors.length];
|
const color = this.controlColors[index % this.controlColors.length];
|
||||||
|
|
||||||
const data = days.map(day => {
|
const data = [];
|
||||||
|
days.forEach(day => {
|
||||||
const res = control.results[day];
|
const res = control.results[day];
|
||||||
if (res && res.resValue !== null && sd !== 0) {
|
if (res && res.resValue !== null && sd !== 0) {
|
||||||
return (parseFloat(res.resValue) - mean) / sd;
|
const sdValue = (parseFloat(res.resValue) - mean) / sd;
|
||||||
|
data.push({ x: sdValue, y: day });
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: control.controlName,
|
label: control.controlName,
|
||||||
data: data,
|
data: data,
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
backgroundColor: color,
|
backgroundColor: data.map(d => Math.abs(d.x) > 2 ? '#ff0000' : color),
|
||||||
pointBackgroundColor: data.map(v => v !== null && Math.abs(v) > 2 ? '#ff0000' : color),
|
pointBackgroundColor: data.map(d => Math.abs(d.x) > 2 ? '#ff0000' : color),
|
||||||
pointRadius: 4,
|
pointRadius: 3,
|
||||||
borderWidth: 1,
|
borderWidth: 1.5,
|
||||||
showLine: false,
|
showLine: true,
|
||||||
tension: 0
|
tension: 0,
|
||||||
|
spanGaps: true
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.chart = new Chart(ctx, {
|
this.chart = new Chart(ctx, {
|
||||||
type: 'line',
|
type: 'scatter',
|
||||||
data: {
|
data: { datasets: datasets },
|
||||||
labels: days,
|
|
||||||
datasets: datasets
|
|
||||||
},
|
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@ -768,51 +795,51 @@
|
|||||||
annotations: {
|
annotations: {
|
||||||
meanLine: {
|
meanLine: {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yMin: 0,
|
xMin: 0,
|
||||||
yMax: 0,
|
xMax: 0,
|
||||||
borderColor: '#666',
|
borderColor: '#666',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderDash: [5, 5]
|
borderDash: [5, 5]
|
||||||
},
|
},
|
||||||
plus1sd: {
|
plus1sd: {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yMin: 1,
|
xMin: 1,
|
||||||
yMax: 1,
|
xMax: 1,
|
||||||
borderColor: '#ccc',
|
borderColor: '#ccc',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
},
|
},
|
||||||
minus1sd: {
|
minus1sd: {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yMin: -1,
|
xMin: -1,
|
||||||
yMax: -1,
|
xMax: -1,
|
||||||
borderColor: '#ccc',
|
borderColor: '#ccc',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
},
|
},
|
||||||
plus2sd: {
|
plus2sd: {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yMin: 2,
|
xMin: 2,
|
||||||
yMax: 2,
|
xMax: 2,
|
||||||
borderColor: '#999',
|
borderColor: '#999',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
},
|
},
|
||||||
minus2sd: {
|
minus2sd: {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yMin: -2,
|
xMin: -2,
|
||||||
yMax: -2,
|
xMax: -2,
|
||||||
borderColor: '#999',
|
borderColor: '#999',
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
},
|
},
|
||||||
plus3sd: {
|
plus3sd: {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yMin: 3,
|
xMin: 3,
|
||||||
yMax: 3,
|
xMax: 3,
|
||||||
borderColor: '#333',
|
borderColor: '#333',
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
},
|
},
|
||||||
minus3sd: {
|
minus3sd: {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yMin: -3,
|
xMin: -3,
|
||||||
yMax: -3,
|
xMax: -3,
|
||||||
borderColor: '#333',
|
borderColor: '#333',
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
}
|
}
|
||||||
@ -821,17 +848,6 @@
|
|||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Day',
|
|
||||||
font: { size: 10 }
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
font: { size: 8 },
|
|
||||||
maxRotation: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
min: -4,
|
min: -4,
|
||||||
max: 4,
|
max: 4,
|
||||||
title: {
|
title: {
|
||||||
@ -846,6 +862,18 @@
|
|||||||
return value === 0 ? '0' : (value > 0 ? '+' + value : value);
|
return value === 0 ? '0' : (value > 0 ? '+' + value : value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Day',
|
||||||
|
font: { size: 10 }
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
font: { size: 8 },
|
||||||
|
stepSize: 1,
|
||||||
|
precision: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user