Files
ladill-care/app/Services/Care/SpecialtyClinicalRecordService.php
T
isaaccladandCursor 56a663a777
Deploy Ladill Care / deploy (push) Successful in 26s
Replace Maternity with Women's Health (OB/GYN) and configurable service lines.
Keeps Fertility as a separate specialty, adds OB/GYN and fertility staff roles, and migrates live org settings from maternity → womens_health.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 09:48:19 +00:00

788 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\Care;
use App\Models\Organization;
use App\Models\SpecialtyClinicalRecord;
use App\Models\Visit;
use Illuminate\Support\Arr;
/**
* Visit-scoped specialty clinical records (structured JSON payloads + alerts).
*/
class SpecialtyClinicalRecordService
{
/**
* Map workspace tab → record_type for modules that have clinical forms.
*
* @return array<string, string>
*/
public function recordTypesForModule(string $moduleKey): array
{
$configured = config("care_specialty_clinical.record_types.{$moduleKey}");
if (is_array($configured) && $configured !== []) {
return $configured;
}
return [
'clinical_notes' => 'clinical_note',
];
}
public function recordTypeForTab(string $moduleKey, string $tab): ?string
{
return $this->recordTypesForModule($moduleKey)[$tab] ?? null;
}
/**
* Field schemas for forms (AI-ready structured data).
*
* @return list<array{name: string, label: string, type: string, options?: list<string>, required?: bool, rows?: int}>
*/
public function fieldsFor(string $moduleKey, string $recordType): array
{
$fields = config("care_specialty_clinical.fields.{$moduleKey}.{$recordType}");
if (is_array($fields) && $fields !== []) {
return $fields;
}
return [
['name' => 'notes', 'label' => 'Clinical notes', 'type' => 'textarea', 'rows' => 4, 'required' => true],
];
}
public function findForVisit(Visit $visit, string $moduleKey, string $recordType): ?SpecialtyClinicalRecord
{
$keys = [$moduleKey];
if ($moduleKey === 'womens_health') {
$keys[] = 'maternity'; // legacy clinical rows
}
return SpecialtyClinicalRecord::owned($visit->owner_ref)
->where('visit_id', $visit->id)
->whereIn('module_key', $keys)
->where('record_type', $recordType)
->orderByDesc('id')
->first();
}
/**
* @param array<string, mixed> $payload
*/
public function upsert(
Organization $organization,
Visit $visit,
string $moduleKey,
string $recordType,
array $payload,
string $ownerRef,
?string $actorRef = null,
?string $stageCode = null,
string $status = SpecialtyClinicalRecord::STATUS_ACTIVE,
): SpecialtyClinicalRecord {
$visit->loadMissing('patient');
$sanitized = $this->sanitizePayload($moduleKey, $recordType, $payload);
$alerts = $this->deriveAlerts($moduleKey, $recordType, $sanitized);
$record = $this->findForVisit($visit, $moduleKey, $recordType);
$attributes = [
'owner_ref' => $ownerRef,
'organization_id' => $organization->id,
'branch_id' => $visit->branch_id,
'patient_id' => $visit->patient_id,
'consultation_id' => $visit->consultations()->orderByDesc('id')->value('id'),
'module_key' => $moduleKey,
'record_type' => $recordType,
'status' => $status,
'stage_code' => $stageCode,
'payload' => $sanitized,
'alerts' => $alerts,
'recorded_by' => $actorRef ?? $ownerRef,
'recorded_at' => now(),
];
if ($record) {
$record->update($attributes);
return $record->fresh();
}
return SpecialtyClinicalRecord::create(array_merge($attributes, [
'visit_id' => $visit->id,
]));
}
/**
* @param array<string, mixed> $input
* @return array<string, mixed>
*/
public function sanitizePayload(string $moduleKey, string $recordType, array $input): array
{
$allowed = collect($this->fieldsFor($moduleKey, $recordType))->pluck('name')->all();
$out = [];
foreach ($allowed as $name) {
if (! array_key_exists($name, $input)) {
continue;
}
$value = $input[$name];
if (is_string($value)) {
$value = trim($value);
}
if ($value === '' || $value === null) {
continue;
}
if ($value === '1' || $value === '0' || $value === 'true' || $value === 'false') {
// keep as string for form round-trip; normalize booleans below via field type
}
$out[$name] = $value;
}
foreach ($this->fieldsFor($moduleKey, $recordType) as $field) {
$name = $field['name'];
if (! array_key_exists($name, $out)) {
continue;
}
if (($field['type'] ?? '') === 'boolean') {
$out[$name] = filter_var($out[$name], FILTER_VALIDATE_BOOLEAN);
}
if (($field['type'] ?? '') === 'number') {
$out[$name] = is_numeric($out[$name]) ? 0 + $out[$name] : null;
if ($out[$name] === null) {
unset($out[$name]);
}
}
}
return $out;
}
/**
* @param array<string, mixed> $payload
* @return list<array{code: string, severity: string, message: string}>
*/
public function deriveAlerts(string $moduleKey, string $recordType, array $payload): array
{
$alerts = [];
if ($moduleKey === 'emergency' && $recordType === 'triage') {
$acuity = (string) ($payload['acuity'] ?? '');
if (str_starts_with($acuity, '1') || str_starts_with($acuity, '2')) {
$alerts[] = [
'code' => 'er.high_acuity',
'severity' => 'critical',
'message' => 'High-acuity triage — prioritise resus / emergency bay.',
];
}
if (($payload['airway_ok'] ?? true) === false
|| ($payload['breathing_ok'] ?? true) === false
|| ($payload['circulation_ok'] ?? true) === false) {
$alerts[] = [
'code' => 'er.abc_compromise',
'severity' => 'critical',
'message' => 'ABC compromise flagged on triage.',
];
}
$pain = (int) ($payload['pain_score'] ?? 0);
if ($pain >= 8) {
$alerts[] = [
'code' => 'er.severe_pain',
'severity' => 'warning',
'message' => 'Severe pain score (≥8).',
];
}
}
if ($moduleKey === 'blood_bank' && $recordType === 'blood_request') {
$urgency = (string) ($payload['urgency'] ?? '');
if (str_contains(strtolower($urgency), 'emergency') || str_contains(strtolower($urgency), 'massive')) {
$alerts[] = [
'code' => 'bb.massive_transfusion',
'severity' => 'critical',
'message' => 'Emergency / massive transfusion request.',
];
} elseif (str_contains(strtolower($urgency), 'urgent')) {
$alerts[] = [
'code' => 'bb.urgent_request',
'severity' => 'warning',
'message' => 'Urgent blood product request.',
];
}
if (($payload['crossmatch_status'] ?? '') === 'Incompatible') {
$alerts[] = [
'code' => 'bb.incompatible',
'severity' => 'critical',
'message' => 'Cross-match incompatible — do not issue.',
];
}
}
if ($moduleKey === 'blood_bank' && $recordType === 'inventory_note') {
if (! empty($payload['low_stock_alert'])) {
$alerts[] = [
'code' => 'bb.low_stock',
'severity' => 'warning',
'message' => 'Blood products flagged as low stock.',
];
}
foreach (['whole_blood_units', 'packed_rbc_units', 'platelet_units', 'ffp_units', 'cryo_units'] as $key) {
if (isset($payload[$key]) && (int) $payload[$key] <= 2) {
$alerts[] = [
'code' => 'bb.unit_critical',
'severity' => 'warning',
'message' => str_replace('_', ' ', $key).' at or below 2 units.',
];
}
}
}
if ($moduleKey === 'blood_bank' && $recordType === 'transfusion_note') {
$reaction = strtolower((string) ($payload['reaction'] ?? ''));
if (str_contains($reaction, 'severe') || str_contains($reaction, 'stop')) {
$alerts[] = [
'code' => 'bb.transfusion_reaction',
'severity' => 'critical',
'message' => 'Severe transfusion reaction — stop and escalate.',
];
}
if (($payload['vitals_ok'] ?? true) === false) {
$alerts[] = [
'code' => 'bb.vitals_concern',
'severity' => 'warning',
'message' => 'Transfusion vitals not acceptable.',
];
}
}
if ($moduleKey === 'ophthalmology' && in_array($recordType, ['eye_exam', 'refraction'], true)) {
$iopOd = isset($payload['iop_od']) ? (float) $payload['iop_od'] : null;
$iopOs = isset($payload['iop_os']) ? (float) $payload['iop_os'] : null;
if (($iopOd !== null && $iopOd >= 30) || ($iopOs !== null && $iopOs >= 30)) {
$alerts[] = [
'code' => 'eye.critical_iop',
'severity' => 'critical',
'message' => 'Critical IOP (≥30 mmHg) — urgent glaucoma / acute review.',
];
} elseif (($iopOd !== null && $iopOd >= 24) || ($iopOs !== null && $iopOs >= 24)) {
$alerts[] = [
'code' => 'eye.elevated_iop',
'severity' => 'warning',
'message' => 'Elevated IOP (≥24 mmHg).',
];
}
foreach (['va_od', 'va_os'] as $vaKey) {
$va = strtolower(trim((string) ($payload[$vaKey] ?? '')));
if ($va === '') {
continue;
}
if (preg_match('/\b(cf|hm|pl|nlp|counting|hand\s*motion|perception)\b/i', $va)
|| preg_match('/^6\/(60|90|120)\b/', $va)
|| preg_match('/^20\/(200|400)\b/', $va)) {
$alerts[] = [
'code' => 'eye.low_vision',
'severity' => 'warning',
'message' => 'Low visual acuity recorded ('.$vaKey.').',
];
break;
}
}
}
if ($moduleKey === 'ophthalmology' && $recordType === 'eye_procedure') {
$outcome = strtolower((string) ($payload['outcome'] ?? ''));
if (str_contains($outcome, 'complication') || str_contains($outcome, 'aborted')) {
$alerts[] = [
'code' => 'eye.procedure_concern',
'severity' => 'warning',
'message' => 'Procedure outcome needs review (complication or aborted).',
];
}
}
if ($moduleKey === 'physiotherapy' && $recordType === 'pt_assessment') {
$pain = (int) ($payload['pain_score'] ?? 0);
if ($pain >= 8) {
$alerts[] = [
'code' => 'pt.severe_pain',
'severity' => 'warning',
'message' => 'Severe pain score (≥8) on physio assessment.',
];
}
$red = trim((string) ($payload['red_flags'] ?? ''));
if ($red !== '') {
$alerts[] = [
'code' => 'pt.red_flags',
'severity' => 'critical',
'message' => 'Physio red flags recorded — review urgently.',
];
}
}
if ($moduleKey === 'womens_health' && $recordType === 'obstetric_exam') {
$danger = trim((string) ($payload['danger_signs'] ?? ''));
if ($danger !== '') {
$alerts[] = [
'code' => 'mat.danger_signs',
'severity' => 'critical',
'message' => 'Maternity danger signs flagged.',
];
}
$bp = (string) ($payload['bp'] ?? '');
if (preg_match('/(\d{2,3})\s*[\/]\s*(\d{2,3})/', $bp, $m)) {
$sys = (int) $m[1];
$dia = (int) $m[2];
if ($sys >= 160 || $dia >= 110) {
$alerts[] = [
'code' => 'mat.severe_hypertension',
'severity' => 'critical',
'message' => 'Severe hypertension on obstetric exam.',
];
} elseif ($sys >= 140 || $dia >= 90) {
$alerts[] = [
'code' => 'mat.hypertension',
'severity' => 'warning',
'message' => 'Elevated BP on obstetric exam.',
];
}
}
}
if ($moduleKey === 'womens_health' && $recordType === 'fetal_notes') {
$fhr = isset($payload['fetal_heart_rate']) ? (int) $payload['fetal_heart_rate'] : null;
if ($fhr !== null && ($fhr < 110 || $fhr > 160)) {
$alerts[] = [
'code' => 'mat.abnormal_fhr',
'severity' => 'critical',
'message' => 'Fetal heart rate outside 110160 bpm.',
];
}
$movements = strtolower((string) ($payload['fetal_movements'] ?? ''));
if (str_contains($movements, 'reduced') || str_contains($movements, 'absent')) {
$alerts[] = [
'code' => 'mat.reduced_movements',
'severity' => 'critical',
'message' => 'Reduced or absent fetal movements.',
];
}
}
if ($moduleKey === 'womens_health' && $recordType === 'mat_plan') {
$risk = strtolower((string) ($payload['risk_category'] ?? ''));
if (str_contains($risk, 'high')) {
$alerts[] = [
'code' => 'mat.high_risk',
'severity' => 'warning',
'message' => 'High-risk maternity care plan.',
];
}
}
if ($moduleKey === 'radiology' && $recordType === 'imaging_request') {
$urgency = strtolower((string) ($payload['urgency'] ?? ''));
if (str_contains($urgency, 'stat')) {
$alerts[] = [
'code' => 'rad.stat',
'severity' => 'critical',
'message' => 'STAT imaging request.',
];
} elseif (str_contains($urgency, 'urgent')) {
$alerts[] = [
'code' => 'rad.urgent',
'severity' => 'warning',
'message' => 'Urgent imaging request.',
];
}
}
if ($moduleKey === 'radiology' && $recordType === 'imaging_report') {
if (! empty($payload['critical_result'])) {
$alerts[] = [
'code' => 'rad.critical_result',
'severity' => 'critical',
'message' => 'Critical / unexpected imaging finding recorded.',
];
}
}
if ($moduleKey === 'cardiology' && $recordType === 'cardiac_exam') {
$nyha = (string) ($payload['nyha_class'] ?? '');
if (in_array($nyha, ['III', 'IV'], true)) {
$alerts[] = [
'code' => 'car.nyha_high',
'severity' => $nyha === 'IV' ? 'critical' : 'warning',
'message' => 'NYHA class '.$nyha.' on cardiac exam.',
];
}
$ecg = strtolower((string) ($payload['ecg_findings'] ?? ''));
if (str_contains($ecg, 'stemi') || str_contains($ecg, 'st elevation') || str_contains($ecg, 'vt') || str_contains($ecg, 'vf')) {
$alerts[] = [
'code' => 'car.ecg_critical',
'severity' => 'critical',
'message' => 'Critical ECG findings recorded.',
];
}
}
if ($moduleKey === 'dermatology' && $recordType === 'skin_exam') {
$urgency = (string) ($payload['urgency'] ?? '');
if (in_array($urgency, ['High', 'Suspected malignancy'], true)) {
$alerts[] = [
'code' => 'der.urgency_high',
'severity' => $urgency === 'Suspected malignancy' ? 'critical' : 'warning',
'message' => 'Skin exam urgency: '.$urgency.'.',
];
}
}
if ($moduleKey === 'ambulance' && $recordType === 'amb_scene') {
$acuity = (string) ($payload['acuity'] ?? '');
if (in_array($acuity, ['Critical', 'Urgent'], true)) {
$alerts[] = [
'code' => 'amb.acuity_high',
'severity' => $acuity === 'Critical' ? 'critical' : 'warning',
'message' => 'Scene acuity: '.$acuity.'.',
];
}
if (array_key_exists('circulation_ok', $payload) && in_array($payload['circulation_ok'], [false, 0, '0'], true)) {
$alerts[] = [
'code' => 'amb.circulation_unstable',
'severity' => 'critical',
'message' => 'Circulation marked unstable on scene.',
];
}
}
if ($moduleKey === 'ambulance' && $recordType === 'amb_en_route') {
if (! empty($payload['deterioration'])) {
$alerts[] = [
'code' => 'amb.deterioration',
'severity' => 'critical',
'message' => 'Patient deterioration noted en route.',
];
}
}
if ($moduleKey === 'podiatry' && $recordType === 'foot_exam') {
$risk = (string) ($payload['diabetes'] ?? '');
if (in_array($risk, ['High', 'Active ulcer'], true)) {
$alerts[] = [
'code' => 'pod.diabetes_high',
'severity' => $risk === 'Active ulcer' ? 'critical' : 'warning',
'message' => 'Diabetic foot risk: '.$risk.'.',
];
}
}
if ($moduleKey === 'fertility' && $recordType === 'fert_exam') {
$priority = (string) ($payload['priority'] ?? '');
if (in_array($priority, ['Urgent', 'Time-sensitive'], true)) {
$alerts[] = [
'code' => 'fer.priority_high',
'severity' => $priority === 'Urgent' ? 'critical' : 'warning',
'message' => 'Fertility priority: '.$priority.'.',
];
}
}
if ($moduleKey === 'pediatrics' && $recordType === 'pediatric_exam') {
$concern = (string) ($payload['growth_concern'] ?? '');
if ($concern !== '' && $concern !== 'None') {
$alerts[] = [
'code' => 'ped.growth_concern',
'severity' => in_array($concern, ['Wasting', 'Underweight'], true) ? 'critical' : 'warning',
'message' => 'Growth concern: '.$concern.'.',
];
}
$temp = (float) ($payload['temperature_c'] ?? 0);
if ($temp >= 39.0) {
$alerts[] = [
'code' => 'ped.fever_high',
'severity' => 'critical',
'message' => 'High fever recorded ('.$temp.'°C).',
];
} elseif ($temp >= 38.0) {
$alerts[] = [
'code' => 'ped.fever',
'severity' => 'warning',
'message' => 'Fever recorded ('.$temp.'°C).',
];
}
}
if ($moduleKey === 'orthopedics' && $recordType === 'ortho_exam') {
$nv = strtolower((string) ($payload['neurovascular'] ?? ''));
if (str_contains($nv, 'vascular compromise')) {
$alerts[] = [
'code' => 'ort.nv_vascular',
'severity' => 'critical',
'message' => 'Vascular compromise on ortho exam.',
];
} elseif (str_contains($nv, 'deficit')) {
$alerts[] = [
'code' => 'ort.nv_deficit',
'severity' => 'warning',
'message' => 'Neurovascular deficit recorded.',
];
}
}
if ($moduleKey === 'ent' && $recordType === 'ent_exam') {
if (! empty($payload['airway_concern'])) {
$alerts[] = [
'code' => 'ent.airway_concern',
'severity' => 'critical',
'message' => 'Airway concern flagged on ENT exam.',
];
}
}
if ($moduleKey === 'oncology' && $recordType === 'onc_staging') {
$ecog = (string) ($payload['performance_status'] ?? '');
if (in_array($ecog, ['3', '4'], true)) {
$alerts[] = [
'code' => 'onc.ecog_high',
'severity' => 'warning',
'message' => 'High ECOG performance status ('.$ecog.') — review treatment fitness.',
];
}
}
if ($moduleKey === 'vaccination' && $recordType === 'vax_eligibility') {
if (empty($payload['cleared']) && ! empty($payload['vaccine'])) {
$alerts[] = [
'code' => 'vax.not_cleared',
'severity' => 'critical',
'message' => 'Vaccine requested but patient is not cleared to vaccinate.',
];
}
if (! empty($payload['fever_today'])) {
$alerts[] = [
'code' => 'vax.fever',
'severity' => 'warning',
'message' => 'Fever recorded on vaccination eligibility screen.',
];
}
}
if ($moduleKey === 'vaccination' && $recordType === 'vax_observation') {
$aefi = strtolower((string) ($payload['aefi'] ?? ''));
if (str_contains($aefi, 'severe')) {
$alerts[] = [
'code' => 'vax.aefi_severe',
'severity' => 'critical',
'message' => 'Severe AEFI recorded — escalate and document follow-up.',
];
}
}
if ($moduleKey === 'pathology' && $recordType === 'path_request') {
$urgency = strtolower((string) ($payload['urgency'] ?? ''));
if (str_contains($urgency, 'urgent') || str_contains($urgency, 'intra')) {
$alerts[] = [
'code' => 'path.urgent',
'severity' => 'warning',
'message' => 'Urgent / intra-op pathology request — prioritise processing.',
];
}
}
if ($moduleKey === 'pathology' && $recordType === 'path_specimen') {
$adequacy = strtolower((string) ($payload['adequacy'] ?? ''));
if (str_contains($adequacy, 'inadequate')) {
$alerts[] = [
'code' => 'path.inadequate',
'severity' => 'critical',
'message' => 'Specimen marked inadequate — notify requesting clinician.',
];
}
}
if ($moduleKey === 'infusion' && $recordType === 'infusion_assessment') {
if (empty($payload['fit_for_infusion']) && ! empty($payload['indication'])) {
$alerts[] = [
'code' => 'inf.not_fit',
'severity' => 'critical',
'message' => 'Indication recorded but patient not marked fit for infusion.',
];
}
}
if ($moduleKey === 'infusion' && in_array($recordType, ['infusion_session', 'infusion_monitoring'], true)) {
$reaction = strtolower((string) ($payload['reactions'] ?? $payload['reaction'] ?? ''));
if (str_contains($reaction, 'severe') || str_contains($reaction, 'stop')) {
$alerts[] = [
'code' => 'inf.reaction',
'severity' => 'critical',
'message' => 'Infusion reaction / stop flagged — review interventions.',
];
}
}
if ($moduleKey === 'renal' && $recordType === 'renal_exam') {
$fluid = strtolower((string) ($payload['fluid_status'] ?? ''));
if (str_contains($fluid, 'overload')) {
$alerts[] = [
'code' => 'ren.fluid_overload',
'severity' => 'warning',
'message' => 'Fluid overload recorded on renal exam.',
];
}
}
if ($moduleKey === 'surgery' && $recordType === 'surg_plan') {
if (empty($payload['consent_obtained']) && ! empty($payload['procedure_planned'])) {
$alerts[] = [
'code' => 'sur.consent_missing',
'severity' => 'critical',
'message' => 'Procedure planned without documented consent.',
];
}
}
if ($moduleKey === 'psychiatry' && $recordType === 'risk_assessment') {
$level = strtolower((string) ($payload['overall_risk'] ?? ''));
if (str_contains($level, 'imminent')) {
$alerts[] = [
'code' => 'psy.risk_imminent',
'severity' => 'critical',
'message' => 'Imminent risk — escalate immediately.',
];
} elseif (str_contains($level, 'high')) {
$alerts[] = [
'code' => 'psy.risk_high',
'severity' => 'critical',
'message' => 'High psychiatric risk recorded.',
];
}
$selfHarm = strtolower((string) ($payload['self_harm'] ?? ''));
if (str_contains($selfHarm, 'intent') || str_contains($selfHarm, 'attempt')) {
$alerts[] = [
'code' => 'psy.self_harm',
'severity' => 'critical',
'message' => 'Self-harm intent or recent attempt recorded.',
];
}
}
if ($moduleKey === 'child_welfare' && $recordType === 'cwc_assessment') {
$flag = strtolower((string) ($payload['safeguarding_flag'] ?? ''));
if ($flag === 'escalated') {
$alerts[] = [
'code' => 'cwc.safeguarding',
'severity' => 'critical',
'message' => 'Safeguarding escalated — follow facility protocol.',
];
} elseif ($flag === 'concern') {
$alerts[] = [
'code' => 'cwc.safeguarding',
'severity' => 'warning',
'message' => 'Safeguarding concern recorded on CWC assessment.',
];
}
$nutrition = strtolower((string) ($payload['nutrition_risk'] ?? ''));
if ($nutrition === 'high') {
$alerts[] = [
'code' => 'cwc.nutrition_risk',
'severity' => 'warning',
'message' => 'High nutrition risk on child welfare assessment.',
];
}
$growth = strtolower((string) ($payload['growth_status'] ?? ''));
if (in_array($growth, ['wasted', 'underweight'], true)) {
$alerts[] = [
'code' => 'cwc.growth',
'severity' => 'warning',
'message' => 'Abnormal growth status: '.$payload['growth_status'].'.',
];
}
}
return $alerts;
}
/**
* @return list<SpecialtyClinicalRecord>
*/
public function forVisit(Visit $visit, string $moduleKey): array
{
return SpecialtyClinicalRecord::owned($visit->owner_ref)
->where('visit_id', $visit->id)
->where('module_key', $moduleKey)
->orderBy('record_type')
->get()
->all();
}
/**
* @return list<array{code: string, severity: string, message: string}>
*/
public function alertsForVisit(Visit $visit, string $moduleKey): array
{
$alerts = [];
foreach ($this->forVisit($visit, $moduleKey) as $record) {
foreach ($record->activeAlerts() as $alert) {
$alerts[] = $alert;
}
}
return $alerts;
}
public function countOpenByType(Organization $organization, string $moduleKey, string $recordType, ?int $branchId = null): int
{
return SpecialtyClinicalRecord::owned($organization->owner_ref)
->where('organization_id', $organization->id)
->where('module_key', $moduleKey)
->where('record_type', $recordType)
->whereIn('status', [SpecialtyClinicalRecord::STATUS_DRAFT, SpecialtyClinicalRecord::STATUS_ACTIVE])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->count();
}
/**
* Validation rules derived from field schema.
*
* @return array<string, list<string>>
*/
public function validationRules(string $moduleKey, string $recordType): array
{
$rules = [];
foreach ($this->fieldsFor($moduleKey, $recordType) as $field) {
$name = 'payload.'.$field['name'];
$fieldRules = [];
if (! empty($field['required'])) {
$fieldRules[] = 'required';
} else {
$fieldRules[] = 'nullable';
}
$fieldRules[] = match ($field['type'] ?? 'text') {
'number' => 'numeric',
'boolean' => 'boolean',
'textarea', 'text', 'select' => 'string',
default => 'string',
};
if (($field['type'] ?? '') === 'select' && ! empty($field['options'])) {
$fieldRules[] = 'in:'.implode(',', $field['options']);
}
if (in_array($field['type'] ?? '', ['text', 'textarea'], true)) {
$fieldRules[] = 'max:5000';
}
$rules[$name] = $fieldRules;
}
return $rules;
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
public function payloadFromRequest(array $validated): array
{
return Arr::get($validated, 'payload', []);
}
}