This commit is contained in:
Your Name
2026-10-17 16:08:24 +08:00
parent c67b3eff69
commit 56f85f7d89
57 changed files with 4170 additions and 57 deletions
@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\common\service\tcm\RevisitVisitSlotService;
/** 指派日期队列:先圈定患者,再累计其持有期间的有效复诊订单;不改变全局诊次。 */
final class RevisitCohortCalculator
{
public const TYPE_NEW = 'new';
public const TYPE_INHERIT = 'inherit';
/**
* 日志与订单分别按 (diagnosis_id, create_time, id) 升序。
* 新指派保留全局诊次;继承每次从二诊重新起算,同一秒订单归最新指派。
* 同一医助、诊单、诊次只计一次,避免反复继承把单诊接诊率推高至 100% 以上。
*/
public static function compute(
array $logRows,
iterable $orderRows,
array $offsetMap,
array $refundRejectSet,
array $rollbackKeySet,
int $startTs,
int $endTs,
int $observedAt,
string $type
): array {
$type = $type === self::TYPE_INHERIT ? self::TYPE_INHERIT : self::TYPE_NEW;
$timeline = [];
$pairs = [];
$lastTarget = [];
foreach ($logRows as $r) {
$did = (int) ($r['diagnosis_id'] ?? 0);
$aid = (int) ($r['to_assistant_id'] ?? 0);
$from = (int) ($r['from_assistant_id'] ?? 0);
$time = (int) ($r['create_time'] ?? 0);
if ($did <= 0 || $time > $observedAt) {
continue;
}
// 从未分配重新指派时,写入侧可能把 from 回填为原医助,形成 A→A。
// 以实际时间线判断持有人是否变化,保留释放后的重新承接。
$previous = $lastTarget[$did] ?? null;
if (($previous !== null && $previous === $aid) || ($previous === null && $from === $aid)) {
continue;
}
$lastTarget[$did] = $aid;
$isRollback = isset($rollbackKeySet[RevisitRateCalculator::rollbackKey(
$did, $time, (int) ($r['operator_admin_id'] ?? 0)
)]);
$eventType = (int) ($r['is_inherit'] ?? 0) === 1 || $isRollback
? self::TYPE_INHERIT : self::TYPE_NEW;
$selected = $aid > 0 && $time >= $startTs && $time <= $endTs
&& $eventType === $type && !isset($refundRejectSet[$did]);
$timeline[$did][] = [
'assistant_id' => $aid,
'time' => $time,
'selected' => $selected,
'type' => $eventType,
];
if (!$selected) {
continue;
}
if (!isset($pairs[$aid][$did])) {
$pairs[$aid][$did] = [
'diagnosis_id' => $did,
'assistant_id' => $aid,
'assign_type' => $type,
'assign_count' => 0,
'first_assign_time' => $time,
'last_assign_time' => $time,
'is_rollback' => 0,
];
}
$pairs[$aid][$did]['assign_count']++;
$pairs[$aid][$did]['last_assign_time'] = $time;
$pairs[$aid][$did]['is_rollback'] |= $isRollback ? 1 : 0;
}
$orders = [];
$curDid = 0;
$globalSeq = 0;
$segmentSeq = 0;
$ptr = 0;
$offset = 0;
$event = null;
foreach ($orderRows as $r) {
$did = (int) ($r['diagnosis_id'] ?? 0);
$ct = (int) ($r['create_time'] ?? 0);
if ($did <= 0 || $ct > $observedAt) {
continue;
}
if ($curDid !== $did) {
$curDid = $did;
$globalSeq = 0;
$segmentSeq = 0;
$ptr = 0;
$event = null;
$offset = RevisitVisitSlotService::resolveRevisitSlotStartOffset($did, $offsetMap);
}
$globalSeq++;
$tl = $timeline[$did] ?? [];
while ($ptr < \count($tl) && $tl[$ptr]['time'] <= $ct) {
$event = $tl[$ptr++];
$segmentSeq = 0;
}
if ($event === null) {
continue;
}
$segmentSeq++;
if (!$event['selected']) {
continue;
}
$slot = $type === self::TYPE_INHERIT ? $segmentSeq + 1 : $globalSeq + $offset;
if ($slot < 2 || $slot > RevisitRateCalculator::MAX_VISIT_SLOT) {
continue;
}
$aid = $event['assistant_id'];
// 重复承接同一患者时,明细保留首次达到该诊次的订单,与人数指标逐项对账。
if (isset($orders[$aid][$slot][$did])) {
continue;
}
$r['assistant_id'] = $aid;
$r['visit_slot'] = $slot;
$r['global_visit_slot'] = $globalSeq + $offset;
$r['cohort_assign_time'] = $event['time'];
$orders[$aid][$slot][$did] = $r;
}
return ['pairsByAssistant' => $pairs, 'ordersByAssistant' => $orders, 'cohort_type' => $type];
}
/** 部门与总计按诊单去重;每个诊次保留同一患者最早一笔订单。 */
public static function scopedOrders(array $calc, array $assistantIds, int $slot = 0): array
{
$out = [];
foreach ($assistantIds as $aid) {
foreach ($calc['ordersByAssistant'][$aid] ?? [] as $s => $byDiag) {
if ($slot > 0 && $s !== $slot) {
continue;
}
foreach ($byDiag as $did => $r) {
$previous = $out[$s][$did] ?? null;
if ($previous === null || [(int) $r['create_time'], (int) $r['id']]
< [(int) $previous['create_time'], (int) $previous['id']]) {
$out[$s][$did] = $r;
}
}
}
}
return $out;
}
public static function metricPack(array $calc, array $assistantIds, array $slots): array
{
$diagSet = [];
foreach ($assistantIds as $aid) {
foreach ($calc['pairsByAssistant'][$aid] ?? [] as $did => $_) {
$diagSet[$did] = true;
}
}
$count = \count($diagSet);
$inherit = $calc['cohort_type'] === self::TYPE_INHERIT;
$pack = [
'assigned_count' => $inherit ? 0 : $count,
'inherit_count' => $inherit ? $count : 0,
'assigned_total_count' => $count,
];
$orders = self::scopedOrders($calc, $assistantIds);
$all = array_sum(array_map('count', $orders));
foreach ($slots as $slot) {
$num = \count($orders[$slot] ?? []);
$pack['visit' . $slot . '_count'] = $num;
$pack['visit' . $slot . '_den'] = $count;
$pack['visit' . $slot . '_rate'] = $count > 0 ? round($num / $count * 100, 2) : null;
}
$pack['visit_all_count'] = $all;
$pack['visit_all_den'] = $count;
$pack['visit_all_rate'] = $count > 0 ? round($all / $count * 100, 2) : null;
return $pack;
}
/** 严格校验日期,避免非法日期被 strtotime 自动滚到次月。 */
public static function dateRange(array $params, int $now): array
{
$start = trim((string) ($params['start_date'] ?? ''));
$end = trim((string) ($params['end_date'] ?? ''));
if ($start === '' && $end === '') {
$month = trim((string) ($params['month'] ?? ''));
if (preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) === 1) {
$start = $month . '-01';
$end = date('Y-m-t', (int) strtotime($start));
} else {
$start = date('Y-m-01', $now);
$end = date('Y-m-d', $now);
}
}
foreach ([$start, $end] as $date) {
$parsed = \DateTimeImmutable::createFromFormat('!Y-m-d', $date);
if ($parsed === false || $parsed->format('Y-m-d') !== $date) {
throw new \InvalidArgumentException('请选择有效的开始日期和结束日期');
}
}
if ($start > $end) {
throw new \InvalidArgumentException('开始日期不能晚于结束日期');
}
return [$start, $end, (int) strtotime($start . ' 00:00:00'), (int) strtotime($end . ' 23:59:59')];
}
}
@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use app\adminapi\logic\dept\DeptLogic;
use app\common\service\tcm\RevisitVisitSlotService;
use think\facade\Db;
/** 新报表独立读取方法,保持原复诊率 Logic 源码及调用链不变。 */
trait RevisitCohortDataSource
{
private static function fetchRollbackKeySet(int $startTs, int $endTs): array
{
$rows = Db::name('tcm_diagnosis_auto_assign_log')
->where('rollback_time', '>=', $startTs)
->where('rollback_time', '<=', $endTs)
->where('diagnosis_id', '>', 0)
->field(['diagnosis_id', 'rollback_time', 'rollback_admin_id'])
->select()
->toArray();
$out = [];
foreach ($rows as $r) {
$out[RevisitRateCalculator::rollbackKey(
(int) ($r['diagnosis_id'] ?? 0),
(int) ($r['rollback_time'] ?? 0),
(int) ($r['rollback_admin_id'] ?? 0)
)] = true;
}
return $out;
}
private static function iterateOrderSeqRows(array $diagIds, array $fields): \Generator
{
foreach (array_chunk($diagIds, 2000) as $chunk) {
foreach (self::fetchOrderSeqRows($chunk, $fields) as $r) {
yield $r;
}
}
}
private static function buildAssistantDeptIndex(array $adminIds): array
{
if ($adminIds === []) {
return [[], []];
}
// admin_dept 为 (admin_id, dept_id) 联合主键、无自增 id;取最小 dept_id 作为归属部门保证确定性
$relRows = Db::name('admin_dept')
->whereIn('admin_id', $adminIds)
->order(['admin_id' => 'asc', 'dept_id' => 'asc'])
->field(['admin_id', 'dept_id'])
->select()
->toArray();
$deptNames = Db::name('dept')
->whereNull('delete_time')
->column('name', 'id');
$canonical = [];
foreach ($relRows as $r) {
$aid = (int) ($r['admin_id'] ?? 0);
$deptId = (int) ($r['dept_id'] ?? 0);
if ($aid <= 0 || $deptId <= 0 || isset($canonical[$aid])) {
continue;
}
if (!isset($deptNames[$deptId])) {
continue;
}
$canonical[$aid] = $deptId;
}
$names = [];
foreach ($deptNames as $id => $name) {
$names[(int) $id] = (string) $name;
}
return [$canonical, $names];
}
private static function resolveDeptFilterSet(mixed $raw): array
{
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($erSet === []) {
return [];
}
$deptFilterIds = self::parseDeptIds($raw);
if ($deptFilterIds === []) {
return $erSet;
}
$allowedRoots = [];
foreach ($deptFilterIds as $id) {
if (isset($erSet[$id])) {
$allowedRoots[] = $id;
}
}
if ($allowedRoots === []) {
return [];
}
$expanded = self::expandDeptSubtreeSet($allowedRoots);
$out = [];
foreach ($expanded as $id => $_) {
if (isset($erSet[$id])) {
$out[$id] = true;
}
}
return $out;
}
private static function parseDeptIds(mixed $raw): array
{
if ($raw === null || $raw === '' || $raw === []) {
return [];
}
$list = \is_array($raw) ? $raw : explode(',', (string) $raw);
return array_values(array_unique(array_filter(
array_map('intval', $list),
static fn (int $v): bool => $v > 0
)));
}
private static function expandDeptSubtreeSet(array $deptIds): array
{
$rows = Db::name('dept')
->whereNull('delete_time')
->field(['id', 'pid'])
->select()
->toArray();
$childrenByPid = [];
foreach ($rows as $r) {
$childrenByPid[(int) $r['pid']][] = (int) $r['id'];
}
$set = [];
$queue = $deptIds;
while ($queue !== []) {
$id = (int) array_shift($queue);
if ($id <= 0 || isset($set[$id])) {
continue;
}
$set[$id] = true;
foreach ($childrenByPid[$id] ?? [] as $childId) {
$queue[] = $childId;
}
}
return $set;
}
private static function fetchRefundOrRejectDiagnosisSet(array $diagIds): array
{
if ($diagIds === []) {
return [];
}
$out = [];
foreach (array_chunk($diagIds, 2000) as $chunk) {
$ids = Db::name('tcm_prescription_order')
->whereIn('diagnosis_id', $chunk)
->whereNull('delete_time')
->whereIn('fulfillment_status', [9, 10])
->group('diagnosis_id')
->column('diagnosis_id');
foreach ($ids as $id) {
$out[(int) $id] = true;
}
}
return $out;
}
private static function fetchOrderSeqRows(array $diagIds, array $fields): array
{
// 实现已搬至 RevisitVisitSlotService(诊次口径单点维护),此处保留委托以减少 diff
return RevisitVisitSlotService::fetchOrderSeqRows($diagIds, $fields);
}
private static function fetchRevisitSlotStartOffsetMap(array $diagIds): array
{
// 实现已搬至 RevisitVisitSlotService(诊次口径单点维护),此处保留委托以减少 diff
return RevisitVisitSlotService::fetchRevisitSlotStartOffsetMap($diagIds);
}
private static function fetchDiagnosisInfo(array $diagIds): array
{
if ($diagIds === []) {
return [];
}
$rows = Db::name('tcm_diagnosis')
->whereIn('id', $diagIds)
->field(['id', 'patient_name', 'phone'])
->select()
->toArray();
$out = [];
foreach ($rows as $r) {
$out[(int) $r['id']] = [
'patient_name' => trim((string) ($r['patient_name'] ?? '')),
'phone' => trim((string) ($r['phone'] ?? '')),
];
}
return $out;
}
private static function cohortDeptOptions(): array
{
$erSet = DeptLogic::getErCenterSubtreeDeptIdSet();
if ($erSet === []) {
return ['rows' => []];
}
$rows = Db::name('dept')
->whereNull('delete_time')
->whereIn('id', array_keys($erSet))
->field(['id', 'pid', 'name'])
->order('sort', 'desc')
->order('id', 'asc')
->select()
->toArray();
return [
'rows' => array_map(static fn (array $r): array => [
'id' => (int) $r['id'],
'pid' => (int) $r['pid'],
'name' => (string) $r['name'],
], $rows),
];
}
}
@@ -0,0 +1,250 @@
<?php
declare(strict_types=1);
namespace app\adminapi\logic\stats;
use think\facade\Db;
/** 指派 / 继承独立报表。旧按月计算器继续服务自动分配与个人业绩。 */
class RevisitCohortLogic
{
use RevisitCohortDataSource;
public static function overview(array $params, string $type = RevisitCohortCalculator::TYPE_NEW): array
{
$ctx = self::cohortContext($params, $type);
$calc = $ctx['calc'];
$aids = array_keys($calc['pairsByAssistant']);
$names = $aids === [] ? [] : Db::name('admin')->whereIn('id', $aids)->column('name', 'id');
$maxSlot = 4;
foreach ($calc['ordersByAssistant'] as $bySlot) {
foreach ($bySlot as $slot => $_) {
$maxSlot = max($maxSlot, $slot);
}
}
$slots = range(2, $maxSlot);
$byDept = [];
foreach ($aids as $aid) {
$deptId = $ctx['assistantDept'][$aid];
$byDept[$deptId][] = $aid;
}
$rows = [];
foreach ($byDept as $deptId => $deptAids) {
$children = [];
$deptName = (string) ($ctx['deptNames'][$deptId] ?? ('#' . $deptId));
foreach ($deptAids as $aid) {
$children[] = [
'row_key' => 'a' . $aid,
'is_dept' => 0,
'dept_id' => $deptId,
'dept_name' => $deptName,
'assistant_id' => $aid,
'assistant_name' => (string) ($names[$aid] ?? ('#' . $aid)),
] + RevisitCohortCalculator::metricPack($calc, [$aid], $slots);
}
usort($children, static fn (array $a, array $b): int =>
($b['assigned_total_count'] <=> $a['assigned_total_count'])
?: strcmp($a['assistant_name'], $b['assistant_name']));
$rows[] = [
'row_key' => 'd' . $deptId,
'is_dept' => 1,
'dept_id' => $deptId,
'dept_name' => $deptName,
'assistant_count' => \count($children),
'children' => $children,
] + RevisitCohortCalculator::metricPack($calc, $deptAids, $slots);
}
usort($rows, static fn (array $a, array $b): int =>
($b['assigned_total_count'] <=> $a['assigned_total_count'])
?: strcmp($a['dept_name'], $b['dept_name']));
return self::metadata($ctx) + [
'slots' => $slots,
'total' => RevisitCohortCalculator::metricPack($calc, $aids, $slots),
'rows' => $rows,
];
}
public static function assignLines(array $params, string $type = RevisitCohortCalculator::TYPE_NEW): array
{
$ctx = self::cohortContext($params, $type);
$aids = self::scopedAssistantIds($ctx, $params);
$byDiag = [];
foreach ($aids as $aid) {
foreach ($ctx['calc']['pairsByAssistant'][$aid] ?? [] as $did => $pair) {
if (!isset($byDiag[$did])) {
$byDiag[$did] = $pair + ['assistants' => []];
$byDiag[$did]['assign_count'] = 0;
}
$byDiag[$did]['assistants'][$aid] = true;
$byDiag[$did]['assign_count'] += $pair['assign_count'];
$byDiag[$did]['first_assign_time'] = min($byDiag[$did]['first_assign_time'], $pair['first_assign_time']);
$byDiag[$did]['last_assign_time'] = max($byDiag[$did]['last_assign_time'], $pair['last_assign_time']);
$byDiag[$did]['is_rollback'] |= $pair['is_rollback'];
}
}
$diagInfo = self::fetchDiagnosisInfo(array_keys($byDiag));
$names = $aids === [] ? [] : Db::name('admin')->whereIn('id', $aids)->column('name', 'id');
$rows = [];
foreach ($byDiag as $did => $pair) {
$assistantNames = [];
foreach ($pair['assistants'] as $aid => $_) {
$assistantNames[] = (string) ($names[$aid] ?? ('#' . $aid));
}
$rows[] = [
'diagnosis_id' => $did,
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
'assistant_names' => implode('、', $assistantNames),
'assign_count' => $pair['assign_count'],
'first_assign_time_text' => date('Y-m-d H:i:s', $pair['first_assign_time']),
'last_assign_time' => $pair['last_assign_time'],
'last_assign_time_text' => date('Y-m-d H:i:s', $pair['last_assign_time']),
'assign_type' => $ctx['calc']['cohort_type'],
'is_rollback' => $pair['is_rollback'],
];
}
usort($rows, static fn (array $a, array $b): int => $b['last_assign_time'] <=> $a['last_assign_time']);
return self::metadata($ctx) + ['count' => \count($rows), 'rows' => $rows];
}
public static function visitOrderLines(array $params, string $type = RevisitCohortCalculator::TYPE_NEW): array
{
$slot = (int) ($params['slot'] ?? 0);
if ($slot < 0 || $slot === 1 || $slot > RevisitRateCalculator::MAX_VISIT_SLOT) {
throw new \InvalidArgumentException('诊次须为 2~50,或 0(复诊合计)');
}
$ctx = self::cohortContext($params, $type);
$orders = RevisitCohortCalculator::scopedOrders($ctx['calc'], self::scopedAssistantIds($ctx, $params), $slot);
$flat = [];
$adminIds = [];
$diagIds = [];
foreach ($orders as $byDiag) {
foreach ($byDiag as $did => $r) {
$flat[] = $r;
$diagIds[$did] = true;
$adminIds[(int) $r['assistant_id']] = true;
if ((int) ($r['creator_id'] ?? 0) > 0) {
$adminIds[(int) $r['creator_id']] = true;
}
}
}
$diagInfo = self::fetchDiagnosisInfo(array_keys($diagIds));
$names = $adminIds === [] ? [] : Db::name('admin')->whereIn('id', array_keys($adminIds))->column('name', 'id');
$rows = [];
foreach ($flat as $r) {
$did = (int) $r['diagnosis_id'];
$aid = (int) $r['assistant_id'];
$cid = (int) ($r['creator_id'] ?? 0);
$rows[] = [
'order_id' => (int) $r['id'],
'order_no' => (string) ($r['order_no'] ?? ''),
'diagnosis_id' => $did,
'patient_name' => (string) ($diagInfo[$did]['patient_name'] ?? ''),
'patient_phone' => (string) ($diagInfo[$did]['phone'] ?? ''),
'amount' => round((float) ($r['amount'] ?? 0), 2),
'create_time' => (int) $r['create_time'],
'create_time_text' => date('Y-m-d H:i:s', (int) $r['create_time']),
'assistant_id' => $aid,
'assistant_name' => (string) ($names[$aid] ?? ('#' . $aid)),
'creator_name' => $cid > 0 ? (string) ($names[$cid] ?? ('#' . $cid)) : '—',
'visit_slot' => (int) $r['visit_slot'],
'global_visit_slot' => (int) $r['global_visit_slot'],
'cohort_assign_time_text' => date('Y-m-d H:i:s', (int) $r['cohort_assign_time']),
];
}
usort($rows, static fn (array $a, array $b): int => $b['create_time'] <=> $a['create_time']);
return self::metadata($ctx) + ['slot' => $slot, 'count' => \count($rows), 'rows' => $rows];
}
/** 保留部门下拉结构,补充二中心内医助;不影响复诊到期页对父类 deptOptions 的复用。 */
public static function deptOptions(): array
{
$options = self::cohortDeptOptions();
$deptSet = self::resolveDeptFilterSet(null);
if ($deptSet === []) {
return $options + ['assistants' => []];
}
$ids = Db::name('admin_dept')->whereIn('dept_id', array_keys($deptSet))->group('admin_id')->column('admin_id');
[$assistantDept] = self::buildAssistantDeptIndex($ids);
$admins = $ids === [] ? [] : Db::name('admin')->whereIn('id', $ids)->whereNull('delete_time')
->field(['id', 'name'])->order('name', 'asc')->select()->toArray();
$assistants = [];
foreach ($admins as $admin) {
$deptId = (int) ($assistantDept[(int) $admin['id']] ?? 0);
if (isset($deptSet[$deptId])) {
$assistants[] = ['id' => (int) $admin['id'], 'name' => (string) $admin['name'], 'dept_id' => $deptId];
}
}
return $options + ['assistants' => $assistants];
}
private static function cohortContext(array $params, string $type): array
{
$now = time();
[$start, $end, $startTs, $endTs] = RevisitCohortCalculator::dateRange($params, $now);
// 先按指派周期圈定诊单,再读取其完整时间线;避免小范围查询拉取全库历史日志。
$candidateQuery = Db::name('tcm_diagnosis_assign_log')->alias('lg')
->join('tcm_diagnosis dg', 'dg.id = lg.diagnosis_id AND dg.delete_time IS NULL', 'INNER')
->where('lg.create_time', '>=', $startTs)->where('lg.create_time', '<=', min($endTs, $now))
->where('lg.to_assistant_id', '>', 0)->where('lg.diagnosis_id', '>', 0);
$filterAid = (int) ($params['assistant_id'] ?? 0);
if ($filterAid > 0) {
$candidateQuery->where('lg.to_assistant_id', $filterAid);
}
$candidateIds = array_map('intval', $candidateQuery->group('lg.diagnosis_id')->column('lg.diagnosis_id'));
$logs = [];
foreach (array_chunk($candidateIds, 2000) as $chunk) {
$chunkLogs = Db::name('tcm_diagnosis_assign_log')->alias('lg')
->whereIn('lg.diagnosis_id', $chunk)->where('lg.create_time', '<=', $now)
->field(['lg.id', 'lg.diagnosis_id', 'lg.from_assistant_id', 'lg.to_assistant_id',
'lg.operator_admin_id', 'lg.create_time', 'lg.is_inherit'])
->order(['lg.diagnosis_id' => 'asc', 'lg.create_time' => 'asc', 'lg.id' => 'asc'])
->select()->toArray();
array_push($logs, ...$chunkLogs);
}
$calc = RevisitCohortCalculator::compute(
$logs,
self::iterateOrderSeqRows($candidateIds, ['o.id', 'o.order_no', 'o.diagnosis_id', 'o.create_time', 'o.amount', 'o.creator_id']),
self::fetchRevisitSlotStartOffsetMap($candidateIds),
self::fetchRefundOrRejectDiagnosisSet($candidateIds),
self::fetchRollbackKeySet($startTs, min($endTs, $now)),
$startTs, $endTs, $now, $type
);
[$assistantDept, $deptNames] = self::buildAssistantDeptIndex(array_keys($calc['pairsByAssistant']));
$deptSet = self::resolveDeptFilterSet($params['dept_ids'] ?? null);
foreach (array_keys($calc['pairsByAssistant']) as $aid) {
if (!isset($deptSet[$assistantDept[$aid] ?? 0]) || ($filterAid > 0 && $filterAid !== $aid)) {
unset($calc['pairsByAssistant'][$aid], $calc['ordersByAssistant'][$aid]);
}
}
return ['calc' => $calc, 'assistantDept' => $assistantDept, 'deptNames' => $deptNames,
'start_date' => $start, 'end_date' => $end, 'observed_until' => date('Y-m-d H:i:s', $now)];
}
private static function scopedAssistantIds(array $ctx, array $params): array
{
$aids = array_keys($ctx['calc']['pairsByAssistant']);
$scopeAid = (int) ($params['scope_assistant_id'] ?? 0);
if ($scopeAid > 0) {
return \in_array($scopeAid, $aids, true) ? [$scopeAid] : [];
}
if (isset($params['dept_id']) && $params['dept_id'] !== '') {
$deptId = (int) $params['dept_id'];
$aids = array_values(array_filter($aids, static fn (int $aid): bool => ($ctx['assistantDept'][$aid] ?? 0) === $deptId));
}
return $aids;
}
private static function metadata(array $ctx): array
{
return ['start_date' => $ctx['start_date'], 'end_date' => $ctx['end_date'],
'observed_until' => $ctx['observed_until'], 'cohort_type' => $ctx['calc']['cohort_type']];
}
}