mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
270 lines
8.8 KiB
PHP
270 lines
8.8 KiB
PHP
<?php
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------------------
|
|
| AVE.cms
|
|
|--------------------------------------------------------------------------------------
|
|
| @package AVE.cms
|
|
| @file adminx/modules/Registrations/Model.php
|
|
| @author AVE.cms <support@ave-cms.ru>
|
|
| @copyright 2007-2026 (c) AVE.cms
|
|
| @link https://ave-cms.ru
|
|
| @version 3.3
|
|
*/
|
|
|
|
namespace App\Adminx\Registrations;
|
|
|
|
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
|
|
|
use DB;
|
|
|
|
/**
|
|
* Реестр регистрационных удостоверений Росздравнадзора (РУ) на медизделия.
|
|
*
|
|
* Юридически обязателен для продажи медтехники. Мягкая связь с товаром
|
|
* (document_id nullable) — жёсткий FK не нужен, товар может быть удалён/заменён.
|
|
*/
|
|
class Model
|
|
{
|
|
/** Статусы РУ: code => [label, color]. */
|
|
public static function statuses()
|
|
{
|
|
return array(
|
|
'active' => array('label' => 'Действует', 'color' => 'green'),
|
|
'suspended' => array('label' => 'Приостановлено', 'color' => 'amber'),
|
|
'expired' => array('label' => 'Истекло', 'color' => 'gray'),
|
|
'cancelled' => array('label' => 'Аннулировано', 'color' => 'red'),
|
|
);
|
|
}
|
|
|
|
/** Классы риска медизделий по номенклатуре Росздравнадзора. */
|
|
public static function riskClasses()
|
|
{
|
|
return array('1', '2a', '2b', '3');
|
|
}
|
|
|
|
public static function normalizeStatus($status, $fallback = 'active')
|
|
{
|
|
$status = trim((string) $status);
|
|
return array_key_exists($status, self::statuses()) ? $status : $fallback;
|
|
}
|
|
|
|
public static function normalizeRisk($risk)
|
|
{
|
|
$risk = trim((string) $risk);
|
|
return in_array($risk, self::riskClasses(), true) ? $risk : '';
|
|
}
|
|
|
|
public static function table()
|
|
{
|
|
return Schema::table('registration_certificates');
|
|
}
|
|
|
|
/** Список с фильтрами: status, risk_class, q (номер/владелец/изделие). */
|
|
public static function all(array $filters = array())
|
|
{
|
|
$sql = 'SELECT * FROM ' . self::table() . ' WHERE 1=1';
|
|
$args = array();
|
|
|
|
$status = isset($filters['status']) ? self::normalizeStatusFilter($filters['status']) : '';
|
|
if ($status !== '') {
|
|
$sql .= ' AND status = %s';
|
|
$args[] = $status;
|
|
}
|
|
|
|
$risk = isset($filters['risk_class']) ? self::normalizeRisk($filters['risk_class']) : '';
|
|
if ($risk !== '') {
|
|
$sql .= ' AND risk_class = %s';
|
|
$args[] = $risk;
|
|
}
|
|
|
|
$q = isset($filters['q']) ? trim((string) $filters['q']) : '';
|
|
if ($q !== '') {
|
|
$sql .= ' AND (number LIKE %ss OR holder LIKE %ss OR product_name LIKE %ss)';
|
|
$args[] = $q;
|
|
$args[] = $q;
|
|
$args[] = $q;
|
|
}
|
|
|
|
$sql .= ' ORDER BY updated_at DESC, id DESC';
|
|
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
|
|
|
|
return array_map(array(self::class, 'normalize'), $rows ?: array());
|
|
}
|
|
|
|
public static function summary()
|
|
{
|
|
$row = DB::query(
|
|
'SELECT COUNT(*) AS total,'
|
|
. ' SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) AS active,'
|
|
. ' SUM(CASE WHEN status <> %s AND status <> %s THEN 1 ELSE 0 END) AS inactive'
|
|
. ' FROM ' . self::table(),
|
|
'active',
|
|
'active',
|
|
'expired'
|
|
)->getAssoc();
|
|
|
|
// Действующие РУ со сроком, истекающим в ближайшие 60 дней.
|
|
$expiring = (int) DB::query(
|
|
'SELECT COUNT(*) FROM ' . self::table()
|
|
. ' WHERE status = %s AND perpetual = 0 AND valid_until > 0 AND valid_until < %i',
|
|
'active',
|
|
strtotime('+60 days')
|
|
)->getValue();
|
|
|
|
return array(
|
|
'total' => isset($row['total']) ? (int) $row['total'] : 0,
|
|
'active' => isset($row['active']) ? (int) $row['active'] : 0,
|
|
'inactive' => isset($row['inactive']) ? (int) $row['inactive'] : 0,
|
|
'expiring' => $expiring,
|
|
);
|
|
}
|
|
|
|
public static function one($id)
|
|
{
|
|
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE id = %i LIMIT 1', (int) $id)->getAssoc();
|
|
return $row ? self::normalize($row) : null;
|
|
}
|
|
|
|
public static function create(array $data)
|
|
{
|
|
$fields = self::sanitize($data);
|
|
$now = time();
|
|
$fields['created_at'] = $now;
|
|
$fields['updated_at'] = $now;
|
|
DB::Insert(self::table(), $fields);
|
|
return self::one((int) DB::insertId());
|
|
}
|
|
|
|
public static function update($id, array $data)
|
|
{
|
|
if (!self::one($id)) {
|
|
return null;
|
|
}
|
|
|
|
$fields = self::sanitize($data);
|
|
$fields['updated_at'] = time();
|
|
DB::Update(self::table(), $fields, 'id = %i', (int) $id);
|
|
return self::one($id);
|
|
}
|
|
|
|
public static function delete($id)
|
|
{
|
|
if (!self::one($id)) {
|
|
return false;
|
|
}
|
|
|
|
DB::Delete(self::table(), 'id = %i', (int) $id);
|
|
return true;
|
|
}
|
|
|
|
protected static function sanitize(array $data)
|
|
{
|
|
$number = trim((string) (isset($data['number']) ? $data['number'] : ''));
|
|
if ($number === '') {
|
|
throw new \RuntimeException('Укажите номер РУ');
|
|
}
|
|
|
|
$product = trim((string) (isset($data['product_name']) ? $data['product_name'] : ''));
|
|
if ($product === '') {
|
|
throw new \RuntimeException('Укажите наименование медизделия');
|
|
}
|
|
|
|
$perpetual = !empty($data['perpetual']) ? 1 : 0;
|
|
$validUntil = $perpetual ? 0 : self::dateToTs(isset($data['valid_until']) ? $data['valid_until'] : '');
|
|
|
|
return array(
|
|
'number' => mb_substr($number, 0, 120, 'UTF-8'),
|
|
'holder' => mb_substr(trim((string) (isset($data['holder']) ? $data['holder'] : '')), 0, 255, 'UTF-8'),
|
|
'product_name' => mb_substr($product, 0, 255, 'UTF-8'),
|
|
'risk_class' => self::normalizeRisk(isset($data['risk_class']) ? $data['risk_class'] : ''),
|
|
'status' => self::normalizeStatus(isset($data['status']) ? $data['status'] : 'active'),
|
|
'document_id' => self::nullableId(isset($data['document_id']) ? $data['document_id'] : null),
|
|
'scan_url' => self::url(isset($data['scan_url']) ? $data['scan_url'] : ''),
|
|
'issued_at' => self::dateToTs(isset($data['issued_at']) ? $data['issued_at'] : ''),
|
|
'valid_until' => $validUntil,
|
|
'perpetual' => $perpetual,
|
|
'note' => mb_substr(trim((string) (isset($data['note']) ? $data['note'] : '')), 0, 2000, 'UTF-8'),
|
|
);
|
|
}
|
|
|
|
public static function normalize($row)
|
|
{
|
|
$row = (array) $row;
|
|
$statuses = self::statuses();
|
|
$status = self::normalizeStatus(isset($row['status']) ? $row['status'] : '');
|
|
|
|
$row['id'] = (int) $row['id'];
|
|
$row['number'] = (string) $row['number'];
|
|
$row['holder'] = (string) $row['holder'];
|
|
$row['product_name'] = (string) $row['product_name'];
|
|
$row['risk_class'] = (string) $row['risk_class'];
|
|
$row['status'] = $status;
|
|
$row['status_label'] = $statuses[$status]['label'];
|
|
$row['status_color'] = $statuses[$status]['color'];
|
|
$row['document_id'] = isset($row['document_id']) && $row['document_id'] !== null ? (int) $row['document_id'] : null;
|
|
$row['scan_url'] = (string) $row['scan_url'];
|
|
$row['issued_at'] = (int) $row['issued_at'];
|
|
$row['valid_until'] = (int) $row['valid_until'];
|
|
$row['perpetual'] = (int) $row['perpetual'] === 1;
|
|
$row['issued_label'] = self::tsLabel($row['issued_at']);
|
|
$row['issued_input'] = self::tsInput($row['issued_at']);
|
|
$row['valid_input'] = self::tsInput($row['valid_until']);
|
|
$row['valid_label'] = $row['perpetual'] ? 'бессрочно' : self::tsLabel($row['valid_until']);
|
|
$row['is_expiring'] = !$row['perpetual'] && $row['valid_until'] > 0
|
|
&& $row['valid_until'] < strtotime('+60 days') && $status === 'active';
|
|
$row['note'] = (string) $row['note'];
|
|
return $row;
|
|
}
|
|
|
|
protected static function normalizeStatusFilter($status)
|
|
{
|
|
$status = trim((string) $status);
|
|
return array_key_exists($status, self::statuses()) ? $status : '';
|
|
}
|
|
|
|
protected static function nullableId($value)
|
|
{
|
|
$value = (int) $value;
|
|
return $value > 0 ? $value : null;
|
|
}
|
|
|
|
protected static function url($value)
|
|
{
|
|
$value = trim((string) $value);
|
|
if ($value === '') {
|
|
return '';
|
|
}
|
|
|
|
// Разрешаем только http(s) и локальные пути; прочее отбрасываем.
|
|
if (preg_match('#^https?://#i', $value) || strpos($value, '/') === 0) {
|
|
return mb_substr($value, 0, 500, 'UTF-8');
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
protected static function dateToTs($value)
|
|
{
|
|
$value = trim((string) $value);
|
|
if ($value === '') {
|
|
return 0;
|
|
}
|
|
|
|
$ts = strtotime($value);
|
|
return $ts === false ? 0 : $ts;
|
|
}
|
|
|
|
protected static function tsLabel($ts)
|
|
{
|
|
$ts = (int) $ts;
|
|
return $ts > 0 ? date('d.m.Y', $ts) : '';
|
|
}
|
|
|
|
protected static function tsInput($ts)
|
|
{
|
|
$ts = (int) $ts;
|
|
return $ts > 0 ? date('Y-m-d', $ts) : '';
|
|
}
|
|
}
|