480 lines
14 KiB
PHP
480 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Patient;
|
|
use App\Models\Visit;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
class OsurgImportService
|
|
{
|
|
|
|
public function import(string $filePath): array
|
|
{
|
|
$report = [];
|
|
|
|
if (! file_exists($filePath)) {
|
|
return ['success' => false, 'report' => ["فایل پیدا نشد: $filePath"]];
|
|
}
|
|
|
|
@set_time_limit(600);
|
|
@ini_set('memory_limit', '512M');
|
|
|
|
try {
|
|
$report[] = "خواندن فایل: $filePath";
|
|
|
|
[$patients, $oldIdToIcno] = $this->parsePatients($filePath);
|
|
$report[] = 'بیماران خواندهشده: ' . count($patients);
|
|
|
|
$visits = $this->parseVisits($filePath);
|
|
$report[] = 'ویزیتهای خواندهشده: ' . count($visits);
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
[$pCreated, $pUpdated, $icnoToNewId] = $this->importPatients($patients);
|
|
$report[] = "بیماران — جدید: $pCreated | بهروزرسانی: $pUpdated";
|
|
|
|
$oldIntIdToNewId = $this->buildOldIntIdMap($oldIdToIcno, $icnoToNewId);
|
|
|
|
[$vCreated, $vSkipped] = $this->importVisits($visits, $oldIntIdToNewId, $icnoToNewId);
|
|
$report[] = "ویزیتها — جدید: $vCreated | ردشده: $vSkipped";
|
|
|
|
DB::commit();
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
throw $e;
|
|
}
|
|
|
|
return ['success' => true, 'report' => $report];
|
|
} catch (\Throwable $e) {
|
|
return ['success' => false, 'report' => array_merge($report, ['خطا: ' . $e->getMessage()])];
|
|
}
|
|
}
|
|
|
|
private function parsePatients(string $filePath): array
|
|
{
|
|
$rows = [];
|
|
$oldIdToIcno = [];
|
|
|
|
foreach ($this->streamInsertRows($filePath, 'patients') as [$columns, $values]) {
|
|
$row = array_combine($columns, $values);
|
|
|
|
$oldId = (string) ($row['ID'] ?? '');
|
|
$icno = $row['icno'] ?? '';
|
|
|
|
if ($icno !== '' && $oldId !== '') {
|
|
$oldIdToIcno[$oldId] = $icno;
|
|
}
|
|
|
|
$rows[] = $row;
|
|
}
|
|
|
|
return [$rows, $oldIdToIcno];
|
|
}
|
|
|
|
|
|
private function parseVisits(string $filePath): array
|
|
{
|
|
$rows = [];
|
|
foreach ($this->streamInsertRows($filePath, 'visits') as [$columns, $values]) {
|
|
$rows[] = array_combine($columns, $values);
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
|
|
private function importPatients(array $patients): array
|
|
{
|
|
$created = 0;
|
|
$updated = 0;
|
|
$icnoToNewId = [];
|
|
|
|
$colMap = [
|
|
'icno' => 'icno',
|
|
'first_name' => 'first_name',
|
|
'last_name' => 'last_name',
|
|
'father_name' => 'father_name',
|
|
'gender' => 'gender',
|
|
'age' => 'age',
|
|
'job' => 'job',
|
|
'marital_status' => 'marital_status',
|
|
'education' => 'education',
|
|
'hand_phone' => 'hand_phone',
|
|
'home_phone' => 'home_phone',
|
|
'work_phone' => 'work_phone',
|
|
'other_phone' => 'other_phone',
|
|
'home_address' => 'home_address',
|
|
'work_address' => 'work_address',
|
|
'refered_by' => 'refered_by',
|
|
'referal_reason' => 'referal_reason',
|
|
'doc_id' => 'doc_id',
|
|
'insurance' => 'insurance',
|
|
'insurance_no' => 'insurance_no',
|
|
'is_undercare' => 'is_undercare',
|
|
'undercare_reason' => 'undercare_reason',
|
|
'is_usingdrug' => 'is_usingdrug',
|
|
'underdrug_reason' => 'underdrug_reason',
|
|
'has_alergyto' => 'has_alergyto',
|
|
'alergy_reason' => 'alergy_reason',
|
|
'description' => 'description',
|
|
'birth_date' => 'birth_date',
|
|
'blood_pressure' => 'blood_pressure',
|
|
'blood_sugar' => 'blood_sugar',
|
|
'surgery_before' => 'surgery_before',
|
|
'sergery_after' => 'sergery_after',
|
|
'photos_before' => 'photos_before',
|
|
'photos_after' => 'photos_after',
|
|
'videos' => 'videos',
|
|
'current_illness_1' => 'current_illness_1',
|
|
'current_illness_2' => 'current_illness_2',
|
|
];
|
|
|
|
$jsonArrayFields = [
|
|
'photos_before', 'photos_after', 'videos',
|
|
'has_alergyto', 'current_illness_1', 'current_illness_2',
|
|
];
|
|
|
|
foreach ($patients as $raw) {
|
|
$icno = trim($raw['icno'] ?? '');
|
|
if ($icno === '' || $icno === 'NULL') {
|
|
continue;
|
|
}
|
|
|
|
$row = [];
|
|
foreach ($colMap as $oldCol => $newCol) {
|
|
$val = $raw[$oldCol] ?? null;
|
|
$val = ($val === 'NULL' || $val === null) ? null : $val;
|
|
|
|
if ($val !== null && in_array($newCol, $jsonArrayFields, true)) {
|
|
$val = $this->normaliseJsonArray($val);
|
|
}
|
|
|
|
$row[$newCol] = $val;
|
|
}
|
|
|
|
$row['audio_files'] = null;
|
|
$row['icno'] = $icno;
|
|
|
|
|
|
$createdAt = $this->toGregorianDatetime($raw['log_datetime'] ?? null);
|
|
$updatedAt = $this->toGregorianDatetime($raw['modify_datetime'] ?? null);
|
|
$row['created_at'] = $createdAt ?? now();
|
|
$row['updated_at'] = $updatedAt ?? $createdAt ?? now();
|
|
|
|
|
|
$row['created_by'] = null;
|
|
$row['updated_by'] = null;
|
|
|
|
$existing = DB::table('patients')->where('icno', $icno)->first();
|
|
if ($existing) {
|
|
DB::table('patients')->where('icno', $icno)->update($row);
|
|
$icnoToNewId[$icno] = $existing->id;
|
|
$updated++;
|
|
} else {
|
|
$newId = DB::table('patients')->insertGetId($row);
|
|
$icnoToNewId[$icno] = $newId;
|
|
$created++;
|
|
}
|
|
}
|
|
|
|
return [$created, $updated, $icnoToNewId];
|
|
}
|
|
|
|
|
|
private function importVisits(array $visits, array $oldIntIdToNewId, array $icnoToNewId): array
|
|
{
|
|
$created = 0;
|
|
$skipped = 0;
|
|
|
|
$colMap = [
|
|
'visit_date' => 'visit_date',
|
|
'visit_time' => 'visit_time',
|
|
'treatment' => 'treatment',
|
|
'treatment_cost' => 'treatment_cost',
|
|
'payment_type' => 'payment_type',
|
|
'paid_amount' => 'paid_amount',
|
|
'pos_ul' => 'pos_ul',
|
|
'pos_ll' => 'pos_ll',
|
|
'pos_ur' => 'pos_ur',
|
|
'pos_lr' => 'pos_lr',
|
|
'treatment_description'=> 'treatment_description',
|
|
'doctor' => 'doctor',
|
|
];
|
|
|
|
$intFields = ['treatment', 'treatment_cost', 'payment_type', 'paid_amount',
|
|
'pos_ul', 'pos_ll', 'pos_ur', 'pos_lr', 'doctor'];
|
|
|
|
foreach ($visits as $raw) {
|
|
$oldPatientRef = (string) ($raw['patient'] ?? '');
|
|
|
|
$newPatientId = $this->resolvePatientId($oldPatientRef, $oldIntIdToNewId, $icnoToNewId);
|
|
if ($newPatientId === null) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
$row = ['patient' => $newPatientId];
|
|
foreach ($colMap as $oldCol => $newCol) {
|
|
$val = $raw[$oldCol] ?? null;
|
|
$val = ($val === 'NULL' || $val === null || $val === '') ? null : $val;
|
|
$row[$newCol] = $val;
|
|
}
|
|
|
|
if (! empty($row['visit_date'])) {
|
|
$row['visit_date'] = $this->jalaliToGregorian($row['visit_date']);
|
|
}
|
|
|
|
$row['visit_time'] = null;
|
|
|
|
foreach ($intFields as $f) {
|
|
if (isset($row[$f])) {
|
|
$row[$f] = (int) $row[$f];
|
|
}
|
|
}
|
|
|
|
$row['created_at'] = now();
|
|
$row['updated_at'] = now();
|
|
$row['created_by'] = null;
|
|
$row['updated_by'] = null;
|
|
|
|
DB::table('visits')->insert($row);
|
|
$created++;
|
|
}
|
|
|
|
return [$created, $skipped];
|
|
}
|
|
|
|
|
|
private function jalaliToGregorian(?string $jalali): ?string
|
|
{
|
|
if (empty($jalali) || $jalali === 'NULL') {
|
|
return null;
|
|
}
|
|
try {
|
|
$normalised = str_replace('-', '/', trim($jalali));
|
|
return \Morilog\Jalali\Jalalian::fromFormat('Y/m/d', $normalised)
|
|
->toCarbon()
|
|
->format('Y-m-d');
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
private function toGregorianDatetime(?string $value): ?string
|
|
{
|
|
if (empty($value) || $value === 'NULL') {
|
|
return null;
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
private function buildOldIntIdMap(array $oldIdToIcno, array $icnoToNewId): array
|
|
{
|
|
$map = [];
|
|
foreach ($oldIdToIcno as $oldId => $icno) {
|
|
if (isset($icnoToNewId[$icno])) {
|
|
$map[$oldId] = $icnoToNewId[$icno];
|
|
}
|
|
}
|
|
return $map;
|
|
}
|
|
|
|
|
|
private function resolvePatientId(string $ref, array $oldIntIdToNewId, array $icnoToNewId): ?int
|
|
{
|
|
if ($ref === '' || $ref === 'NULL') {
|
|
return null;
|
|
}
|
|
if (isset($oldIntIdToNewId[$ref])) {
|
|
return $oldIntIdToNewId[$ref];
|
|
}
|
|
if (isset($icnoToNewId[$ref])) {
|
|
return $icnoToNewId[$ref];
|
|
}
|
|
// Live DB fallback
|
|
$p = Patient::where('icno', $ref)->first();
|
|
return $p?->id;
|
|
}
|
|
|
|
|
|
private function normaliseJsonArray(?string $value): ?string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
$decoded = json_decode($value, true);
|
|
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
$paths = array_values(array_filter(array_map(function ($item) {
|
|
if (is_string($item)) {
|
|
return $item;
|
|
}
|
|
if (is_array($item) && isset($item['name'])) {
|
|
return $item['name'];
|
|
}
|
|
return null;
|
|
}, $decoded)));
|
|
|
|
return json_encode($paths, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
}
|
|
|
|
// plain string — wrap it
|
|
return json_encode([$value], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
|
|
|
|
private function streamInsertRows(string $filePath, string $table): \Generator
|
|
{
|
|
$handle = fopen($filePath, 'r');
|
|
$pattern = '/^INSERT INTO `' . preg_quote($table, '/') . '` \((.+?)\) VALUES\s*$/i';
|
|
|
|
$columns = [];
|
|
$inInsert = false;
|
|
$rowBuffer = '';
|
|
|
|
while (($raw = fgets($handle)) !== false) {
|
|
$line = rtrim($raw);
|
|
|
|
if (preg_match($pattern, $line, $m)) {
|
|
$inInsert = true;
|
|
$columns = $this->parseColumnList($m[1]);
|
|
$rowBuffer = '';
|
|
continue;
|
|
}
|
|
|
|
if ($inInsert && preg_match('/^INSERT INTO `([^`]+)`/i', $line, $m2)
|
|
&& strtolower($m2[1]) !== strtolower($table)) {
|
|
$inInsert = false;
|
|
$rowBuffer = '';
|
|
continue;
|
|
}
|
|
|
|
if (! $inInsert) {
|
|
continue;
|
|
}
|
|
|
|
if ($line === ';') {
|
|
$inInsert = false;
|
|
$rowBuffer = '';
|
|
continue;
|
|
}
|
|
|
|
if ($line === '' || str_starts_with($line, '--')) {
|
|
continue;
|
|
}
|
|
|
|
$rowBuffer .= ($rowBuffer !== '' ? "\n" : '') . $line;
|
|
|
|
if ($this->isCompleteValueRow($rowBuffer)) {
|
|
$values = $this->parseValueRow($rowBuffer);
|
|
if (count($values) === count($columns)) {
|
|
yield [$columns, $values];
|
|
}
|
|
$rowBuffer = '';
|
|
}
|
|
}
|
|
|
|
fclose($handle);
|
|
}
|
|
|
|
private function parseColumnList(string $colStr): array
|
|
{
|
|
$cols = [];
|
|
foreach (explode(',', $colStr) as $c) {
|
|
$cols[] = trim(trim($c), '`');
|
|
}
|
|
return $cols;
|
|
}
|
|
|
|
private function isCompleteValueRow(string $buf): bool
|
|
{
|
|
if (! str_starts_with($buf, '(')) {
|
|
return false;
|
|
}
|
|
$count = 0;
|
|
$escaped = false;
|
|
$len = strlen($buf);
|
|
for ($i = 0; $i < $len; $i++) {
|
|
if ($escaped) { $escaped = false; continue; }
|
|
if ($buf[$i] === '\\') { $escaped = true; continue; }
|
|
if ($buf[$i] === "'") { $count++; }
|
|
}
|
|
return ($count % 2 === 0) && (bool) preg_match('/\),?;?\s*$/', $buf);
|
|
}
|
|
|
|
|
|
private function parseValueRow(string $row): array
|
|
{
|
|
$row = trim($row);
|
|
$row = preg_replace('/[,;]\s*$/', '', $row);
|
|
if (str_starts_with($row, '(')) {
|
|
$row = substr($row, 1);
|
|
}
|
|
if (str_ends_with($row, ')')) {
|
|
$row = substr($row, 0, -1);
|
|
}
|
|
|
|
$values = [];
|
|
$current = '';
|
|
$inStr = false;
|
|
$escaped = false;
|
|
$len = strlen($row);
|
|
|
|
for ($i = 0; $i < $len; $i++) {
|
|
$ch = $row[$i];
|
|
|
|
if ($escaped) {
|
|
$current .= $ch;
|
|
$escaped = false;
|
|
continue;
|
|
}
|
|
|
|
if ($ch === '\\') {
|
|
$current .= $ch;
|
|
$escaped = true;
|
|
continue;
|
|
}
|
|
|
|
if ($ch === "'" && ! $inStr) {
|
|
$inStr = true;
|
|
continue;
|
|
}
|
|
|
|
if ($ch === "'" && $inStr) {
|
|
$inStr = false;
|
|
continue;
|
|
}
|
|
|
|
if ($ch === ',' && ! $inStr) {
|
|
$values[] = $this->unescapeSqlString($current);
|
|
$current = '';
|
|
continue;
|
|
}
|
|
|
|
$current .= $ch;
|
|
}
|
|
|
|
$values[] = $this->unescapeSqlString($current);
|
|
|
|
return $values;
|
|
}
|
|
|
|
private function unescapeSqlString(string $val): string
|
|
{
|
|
$val = trim($val);
|
|
if ($val === 'NULL') {
|
|
return 'NULL';
|
|
}
|
|
return str_replace(
|
|
['\\n', '\\r', '\\t', "\\'", '\\\\', '\\"'],
|
|
["\n", "\r", "\t", "'", '\\', '"'],
|
|
$val
|
|
);
|
|
}
|
|
}
|