v0.5.1: Первая версия
This commit is contained in:
commit
8491a6daa0
11 files changed
+1095
No files matched your search
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
const D = __DIR__ . '/data/';
|
||||
const LIST_TYPES = ['Коробка', 'Мешок', 'Полка'];
|
||||
const COL_TYPES = ['text', 'number', 'date', 'select', 'check'];
|
||||
const STD_IDS = ['name', 'qty', 'cat', 'loc'];
|
||||
const ONLINE_SEC = 120;
|
||||
|
||||
if (!function_exists('mb_substr')) { function mb_substr($s, $a, $l = null) { preg_match_all('/./us', (string)$s, $m); return implode('', array_slice($m[0], $a, $l)); } }
|
||||
if (!function_exists('mb_strlen')) { function mb_strlen($s) { return (int)preg_match_all('/./us', (string)$s); } }
|
||||
if (!function_exists('mb_strtolower')) {
|
||||
function mb_strtolower($s) {
|
||||
static $m = null;
|
||||
if ($m === null) $m = array_combine(preg_split('//u', 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ', -1, PREG_SPLIT_NO_EMPTY), preg_split('//u', 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя', -1, PREG_SPLIT_NO_EMPTY));
|
||||
return strtr(strtolower((string)$s), $m);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiError extends Exception {}
|
||||
function fail(string $m, int $c = 400): void { throw new ApiError($m, $c); }
|
||||
|
||||
ini_set('display_errors', '0');
|
||||
set_error_handler(function ($no, $msg, $file, $line) { throw new ErrorException($msg, 0, $no, $file, $line); });
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
set_exception_handler(function ($e) {
|
||||
if (!($e instanceof ApiError)) error_log('inventory api: ' . $e);
|
||||
http_response_code($e instanceof ApiError ? ($e->getCode() ?: 400) : 500);
|
||||
echo json_encode(['error' => $e instanceof ApiError ? $e->getMessage() : 'Ошибка сервера (подробности в логе PHP)'], JSON_UNESCAPED_UNICODE);
|
||||
});
|
||||
|
||||
function out($d, int $c = 200): void { http_response_code($c); echo json_encode($d, JSON_UNESCAPED_UNICODE); exit; }
|
||||
function rd(string $f, $def) { $p = D . $f; if (!is_file($p)) return $def; $j = json_decode((string)file_get_contents($p), true); return $j === null ? $def : $j; }
|
||||
function wr(string $f, $d): void {
|
||||
$p = D . $f; $t = $p . '.tmp';
|
||||
file_put_contents($t, json_encode($d, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
rename($t, $p);
|
||||
}
|
||||
function str($v, int $max): string { return is_scalar($v) ? mb_substr(trim((string)$v), 0, $max) : ''; }
|
||||
function new_id(string $p = ''): string { return $p . bin2hex(random_bytes(5)); }
|
||||
|
||||
function load_ref(): array {
|
||||
$r = rd('ref.json', []);
|
||||
if (!isset($r['dicts'])) {
|
||||
$r = ['dicts' => [
|
||||
['id' => 'cats', 'name' => 'Категории', 'values' => array_values($r['cats'] ?? ['Категория 1', 'Категория 2', 'Категория 3'])],
|
||||
['id' => 'locs', 'name' => 'Расположения', 'values' => array_values($r['locs'] ?? ['Склад', 'Дом', 'Воронеж'])],
|
||||
], 'columns' => []];
|
||||
}
|
||||
$r['columns'] = $r['columns'] ?? [];
|
||||
if (dict_idx($r, 'ltypes') < 0) $r['dicts'][] = ['id' => 'ltypes', 'name' => 'Типы списков', 'values' => LIST_TYPES];
|
||||
return $r;
|
||||
}
|
||||
function std_cols(): array {
|
||||
return [
|
||||
['id' => 'name', 'name' => 'Название', 'type' => 'text', 'dict' => null],
|
||||
['id' => 'qty', 'name' => 'Кол-во', 'type' => 'number', 'dict' => null],
|
||||
['id' => 'cat', 'name' => 'Категория', 'type' => 'select', 'dict' => 'cats'],
|
||||
['id' => 'loc', 'name' => 'Расположение', 'type' => 'select', 'dict' => 'locs'],
|
||||
];
|
||||
}
|
||||
function cols_map(array $ref): array {
|
||||
$m = [];
|
||||
foreach (array_merge(std_cols(), $ref['columns']) as $c) $m[$c['id']] = $c;
|
||||
return $m;
|
||||
}
|
||||
function ltypes(array $ref): array { $i = dict_idx($ref, 'ltypes'); return $i < 0 ? LIST_TYPES : $ref['dicts'][$i]['values']; }
|
||||
function ref_values(array $ref, string $id): array { $i = dict_idx($ref, $id); return $i < 0 ? [] : $ref['dicts'][$i]['values']; }
|
||||
function dict_idx(array $ref, string $id): int {
|
||||
foreach ($ref['dicts'] as $i => $d) if ($d['id'] === $id) return $i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function norm_list(array $l): array {
|
||||
if (!isset($l['kind'])) {
|
||||
$l['kind'] = 'standard';
|
||||
$l['cols'] = STD_IDS;
|
||||
$l['ltype'] = (($l['type'] ?? '') === 'box') ? 'Коробка' : '';
|
||||
$items = [];
|
||||
foreach ($l['items'] ?? [] as $it) {
|
||||
$items[] = ['id' => (string)($it['id'] ?? new_id()), 'v' => [
|
||||
'name' => (string)($it['name'] ?? ''), 'qty' => (string)($it['qty'] ?? ''),
|
||||
'cat' => (string)($it['cat'] ?? ''), 'loc' => (string)($it['loc'] ?? '')]];
|
||||
}
|
||||
$l['items'] = $items;
|
||||
unset($l['type']);
|
||||
}
|
||||
$l['ltype'] = $l['ltype'] ?? '';
|
||||
return $l;
|
||||
}
|
||||
function load_lists(): array { return array_map('norm_list', rd('lists.json', [])); }
|
||||
function list_idx(array $lists, string $id): int {
|
||||
foreach ($lists as $i => $l) if ($l['id'] === $id) return $i;
|
||||
return -1;
|
||||
}
|
||||
function same_cols(array $a, array $b): bool {
|
||||
$a = $a['cols']; $b = $b['cols']; sort($a); sort($b);
|
||||
return $a === $b;
|
||||
}
|
||||
|
||||
function map_items(array $items, array $dst, array $ref): array {
|
||||
$out = [];
|
||||
foreach ($items as $it) {
|
||||
$v = [];
|
||||
foreach ($dst['cols'] as $c) $v[$c] = (string)($it['v'][$c] ?? '');
|
||||
if ($dst['kind'] === 'standard') {
|
||||
if ($v['name'] === '') $v['name'] = 'Без названия';
|
||||
if ($v['cat'] === '') $v['cat'] = (string)(ref_values($ref, 'cats')[0] ?? '');
|
||||
if ($v['loc'] === '') $v['loc'] = (string)(ref_values($ref, 'locs')[0] ?? '');
|
||||
}
|
||||
$out[] = ['id' => new_id(), 'v' => $v];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
function clean_val(array $col, $v, array $ref): string {
|
||||
$v = is_scalar($v) ? trim((string)$v) : '';
|
||||
switch ($col['type']) {
|
||||
case 'text':
|
||||
return mb_substr($v, 0, 200);
|
||||
case 'number':
|
||||
if ($v === '') return '';
|
||||
if (!is_numeric($v) || strlen($v) > 20) fail('«' . $col['name'] . '»: нужно число');
|
||||
return $v;
|
||||
case 'date':
|
||||
if ($v === '') return '';
|
||||
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $v, $m) || !checkdate((int)$m[2], (int)$m[3], (int)$m[1])) fail('«' . $col['name'] . '»: неверная дата');
|
||||
return $v;
|
||||
case 'check':
|
||||
return ($v === '1' || $v === 'true') ? '1' : '';
|
||||
case 'select':
|
||||
if ($v === '') return '';
|
||||
$di = dict_idx($ref, (string)$col['dict']);
|
||||
if ($di < 0 || !in_array($v, $ref['dicts'][$di]['values'], true)) fail('«' . $col['name'] . '»: значения «' . mb_substr($v, 0, 40) . '» нет в справочнике');
|
||||
return $v;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function clean_row($raw, array $cols, array $cm, array $ref, string $kind): array {
|
||||
$raw = is_array($raw) ? $raw : [];
|
||||
$v = []; $any = false;
|
||||
foreach ($cols as $c) {
|
||||
$v[$c] = clean_val($cm[$c], $raw[$c] ?? '', $ref);
|
||||
if ($v[$c] !== '') $any = true;
|
||||
}
|
||||
if ($kind === 'standard') {
|
||||
if ($v['name'] === '') fail('У предмета должно быть название');
|
||||
if ($v['cat'] === '' || $v['loc'] === '') fail('Укажите категорию и расположение');
|
||||
} elseif (!$any) fail('Строка пустая');
|
||||
return $v;
|
||||
}
|
||||
function clean_list(array $l, array $ref, ?array $old): array {
|
||||
$id = (string)($l['id'] ?? '');
|
||||
if (!preg_match('/^[a-z0-9]{3,20}$/', $id)) fail('Некорректный список');
|
||||
$cm = cols_map($ref);
|
||||
$lt = str($l['ltype'] ?? '', 60);
|
||||
$oldLt = $old ? (string)($old['ltype'] ?? '') : '';
|
||||
$ltype = ($lt === '' || in_array($lt, ltypes($ref), true) || $lt === $oldLt) ? $lt : $oldLt;
|
||||
if ($old) {
|
||||
$kind = $old['kind']; $cols = $old['cols'];
|
||||
} else {
|
||||
$kind = (($l['kind'] ?? '') === 'custom') ? 'custom' : 'standard';
|
||||
if ($kind === 'standard') $cols = STD_IDS;
|
||||
else {
|
||||
$cols = array_values(array_unique(array_map('strval', (array)($l['cols'] ?? []))));
|
||||
if (!$cols || count($cols) > 12) fail('Выберите от 1 до 12 колонок');
|
||||
foreach ($cols as $c) if (!isset($cm[$c])) fail('Неизвестная колонка');
|
||||
}
|
||||
}
|
||||
$src = $l['items'] ?? null;
|
||||
if (!is_array($src) || count($src) > 5000) fail('Некорректный список предметов');
|
||||
$items = [];
|
||||
foreach ($src as $it) {
|
||||
$iid = substr(preg_replace('/[^a-z0-9]/', '', strtolower((string)($it['id'] ?? ''))), 0, 20);
|
||||
$items[] = ['id' => $iid !== '' ? $iid : new_id(), 'v' => clean_row($it['v'] ?? [], $cols, $cm, $ref, $kind)];
|
||||
}
|
||||
return ['id' => $id, 'name' => str($l['name'] ?? '', 100) ?: 'Список', 'kind' => $kind, 'ltype' => $ltype,
|
||||
'cols' => $cols, 'final' => !empty($l['final']), 'items' => $items];
|
||||
}
|
||||
|
||||
function touch_presence(string $u): void {
|
||||
$pr = rd('presence.json', []); $now = time();
|
||||
if (($pr[$u] ?? 0) > $now - 15) return;
|
||||
$pr[$u] = $now;
|
||||
foreach ($pr as $k => $t) if ($t < $now - 86400) unset($pr[$k]);
|
||||
wr('presence.json', $pr);
|
||||
}
|
||||
|
||||
function session_info(): array {
|
||||
return ['user' => ['login' => $_SESSION['u'], 'role' => $_SESSION['role']], 'csrf' => $_SESSION['csrf'], 'mustChange' => !empty($_SESSION['must'])];
|
||||
}
|
||||
|
||||
session_name('inv');
|
||||
session_set_cookie_params(['httponly' => true, 'samesite' => 'Strict', 'secure' => !empty($_SERVER['HTTPS'])]);
|
||||
session_start();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') out(['error' => 'POST only'], 405);
|
||||
$in = json_decode((string)file_get_contents('php://input'), true) ?: [];
|
||||
$a = (string)($in['a'] ?? '');
|
||||
|
||||
$lock = fopen(D . '.lock', 'c'); flock($lock, LOCK_EX);
|
||||
|
||||
if ($a === 'login') {
|
||||
$login = mb_strtolower(str($in['login'] ?? '', 50));
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '?';
|
||||
$buckets = [[hash('sha256', "$ip|$login"), 5], [hash('sha256', $ip), 20]];
|
||||
$at = rd('attempts.json', []); $now = time();
|
||||
foreach ($at as $k => $v) if (($v['t'] ?? 0) < $now - 3600 && ($v['until'] ?? 0) < $now) unset($at[$k]);
|
||||
foreach ($buckets as [$k]) if (($at[$k]['until'] ?? 0) > $now) { wr('attempts.json', $at); out(['error' => 'Слишком много попыток. Подождите несколько минут.'], 429); }
|
||||
$users = rd('users.json', ['users' => []]); $u = null; $ui = -1;
|
||||
foreach ($users['users'] as $i => $x) if ($x['login'] === $login) { $u = $x; $ui = $i; }
|
||||
$pw = (string)($in['password'] ?? '');
|
||||
$ok = password_verify($pw, $u['hash'] ?? password_hash('dummy', PASSWORD_DEFAULT)) && $u !== null;
|
||||
if (!$ok) {
|
||||
foreach ($buckets as [$k, $lim]) {
|
||||
$n = ($at[$k]['n'] ?? 0) + 1;
|
||||
$at[$k] = ['n' => $n, 't' => $now, 'until' => $n >= $lim ? $now + 600 : 0];
|
||||
if ($n >= $lim) $at[$k]['n'] = 0;
|
||||
}
|
||||
wr('attempts.json', $at); usleep(500000);
|
||||
out(['error' => 'Неверный логин или пароль'], 401);
|
||||
}
|
||||
foreach ($buckets as [$k]) unset($at[$k]);
|
||||
wr('attempts.json', $at);
|
||||
if (password_needs_rehash($u['hash'], PASSWORD_DEFAULT)) { $users['users'][$ui]['hash'] = password_hash($pw, PASSWORD_DEFAULT); wr('users.json', $users); }
|
||||
session_regenerate_id(true);
|
||||
$_SESSION = ['u' => $u['login'], 'role' => $u['role'], 'csrf' => bin2hex(random_bytes(16)), 'must' => !empty($u['must_change'])];
|
||||
touch_presence($u['login']);
|
||||
out(session_info());
|
||||
}
|
||||
|
||||
if (empty($_SESSION['u'])) out(['error' => 'auth'], 401);
|
||||
touch_presence((string)$_SESSION['u']);
|
||||
if ($a === 'me') out(session_info());
|
||||
if (!hash_equals($_SESSION['csrf'], (string)($_SERVER['HTTP_X_CSRF'] ?? ''))) out(['error' => 'CSRF'], 403);
|
||||
$admin = $_SESSION['role'] === 'admin';
|
||||
if (!empty($_SESSION['must']) && !in_array($a, ['logout', 'passwd'], true)) fail('Сначала смените пароль', 403);
|
||||
$ref = load_ref();
|
||||
|
||||
switch ($a) {
|
||||
case 'ping':
|
||||
out(['ok' => true]);
|
||||
|
||||
case 'logout':
|
||||
$pr = rd('presence.json', []); unset($pr[$_SESSION['u']]); wr('presence.json', $pr);
|
||||
$_SESSION = []; session_destroy(); out(['ok' => true]);
|
||||
|
||||
case 'passwd':
|
||||
$users = rd('users.json', ['users' => []]);
|
||||
$new = (string)($in['new'] ?? '');
|
||||
$oldPw = (string)($in['old'] ?? '');
|
||||
if (mb_strlen($new) < 8) fail('Новый пароль — не короче 8 символов');
|
||||
if ($new === $oldPw) fail('Новый пароль должен отличаться от текущего');
|
||||
foreach ($users['users'] as $i => $x) if ($x['login'] === $_SESSION['u']) {
|
||||
if (!password_verify($oldPw, $x['hash'])) fail('Неверный текущий пароль', 403);
|
||||
$users['users'][$i]['hash'] = password_hash($new, PASSWORD_DEFAULT);
|
||||
unset($users['users'][$i]['must_change']);
|
||||
wr('users.json', $users); $_SESSION['must'] = false; out(['ok' => true]);
|
||||
}
|
||||
fail('Пользователь не найден', 404);
|
||||
|
||||
case 'state':
|
||||
$r = ['ref' => $ref, 'std' => std_cols(), 'listTypes' => ltypes($ref)];
|
||||
if (!$admin) $r['lists'] = load_lists();
|
||||
out($r);
|
||||
|
||||
case 'users':
|
||||
if (!$admin) fail('forbidden', 403);
|
||||
$pr = rd('presence.json', []); $now = time(); $res = [];
|
||||
foreach (rd('users.json', ['users' => []])['users'] as $x)
|
||||
$res[] = ['login' => $x['login'], 'role' => $x['role'] ?? 'user', 'online' => ($pr[$x['login']] ?? 0) > $now - ONLINE_SEC];
|
||||
out(['users' => $res]);
|
||||
|
||||
case 'user_add':
|
||||
if (!$admin) fail('forbidden', 403);
|
||||
$nl = mb_strtolower(str($in['login'] ?? '', 30)); $np = (string)($in['password'] ?? '');
|
||||
if (!preg_match('/^[a-z0-9_.-]{2,30}$/', $nl)) fail('Логин: 2–30 символов, латинские буквы, цифры и знаки _ . -');
|
||||
if (mb_strlen($np) < 8) fail('Пароль — не короче 8 символов');
|
||||
$users = rd('users.json', ['users' => []]);
|
||||
foreach ($users['users'] as $x) if ($x['login'] === $nl) fail('Такой логин уже есть');
|
||||
if (count($users['users']) >= 50) fail('Слишком много пользователей');
|
||||
$users['users'][] = ['login' => $nl, 'role' => 'user', 'hash' => password_hash($np, PASSWORD_DEFAULT)];
|
||||
wr('users.json', $users); out(['ok' => true]);
|
||||
|
||||
case 'list_save':
|
||||
if ($admin) fail('forbidden', 403);
|
||||
$lists = load_lists();
|
||||
$id = (string)($in['list']['id'] ?? '');
|
||||
$i = list_idx($lists, $id);
|
||||
$new = clean_list((array)($in['list'] ?? []), $ref, $i >= 0 ? $lists[$i] : null);
|
||||
if ($i >= 0) $lists[$i] = $new; else array_unshift($lists, $new);
|
||||
wr('lists.json', $lists); out(['ok' => true, 'list' => $new]);
|
||||
|
||||
case 'list_delete':
|
||||
if ($admin) fail('forbidden', 403);
|
||||
wr('lists.json', array_values(array_filter(load_lists(), fn($x) => $x['id'] !== ($in['id'] ?? ''))));
|
||||
out(['ok' => true]);
|
||||
|
||||
case 'transfer':
|
||||
if ($admin) fail('forbidden', 403);
|
||||
$lists = load_lists();
|
||||
$si = list_idx($lists, (string)($in['from'] ?? ''));
|
||||
if ($si < 0) fail('Исходный список не найден', 404);
|
||||
$ids = array_map('strval', (array)($in['ids'] ?? []));
|
||||
$picked = array_values(array_filter($lists[$si]['items'], fn($it) => in_array($it['id'], $ids, true)));
|
||||
if (!$picked) fail('Не выбрано ни одного предмета');
|
||||
$copy = fn() => array_map(fn($it) => ['id' => new_id(), 'v' => $it['v']], $picked);
|
||||
$move = !empty($in['move']);
|
||||
if ($move && !empty($lists[$si]['final'])) fail('Исходный список закрыт итогом — перенос с удалением невозможен');
|
||||
$to = (array)($in['to'] ?? []);
|
||||
if (isset($to['existing'])) {
|
||||
$ti = list_idx($lists, (string)$to['existing']);
|
||||
if ($ti < 0 || $ti === $si) fail('Список назначения не найден', 404);
|
||||
if (!empty($lists[$ti]['final'])) fail('Список назначения закрыт итогом');
|
||||
if (!in_array('name', $lists[$si]['cols'], true) || !in_array('name', $lists[$ti]['cols'], true)) fail('В обоих списках должна быть колонка «Название»');
|
||||
if (!same_cols($lists[$si], $lists[$ti]) && empty($in['confirm'])) fail('Колонки списков отличаются, нужно подтверждение');
|
||||
if (count($lists[$ti]['items']) + count($picked) > 5000) fail('Слишком много предметов в списке');
|
||||
$lists[$ti]['items'] = array_merge($lists[$ti]['items'], map_items($picked, $lists[$ti], $ref));
|
||||
$target = $lists[$ti]['id'];
|
||||
} else {
|
||||
$name = str($to['name'] ?? '', 100);
|
||||
$lt = (string)($to['ltype'] ?? '');
|
||||
if ($name === '') fail('Укажите название списка');
|
||||
if (!in_array($lt, ltypes($ref), true)) fail('Выберите тип списка');
|
||||
$nl = ['id' => new_id('l'), 'name' => $name, 'kind' => $lists[$si]['kind'], 'ltype' => $lt,
|
||||
'cols' => $lists[$si]['cols'], 'final' => false, 'items' => $copy()];
|
||||
array_unshift($lists, $nl); $si++;
|
||||
$target = $nl['id'];
|
||||
}
|
||||
if ($move) {
|
||||
$lists[$si]['items'] = array_values(array_filter($lists[$si]['items'], fn($it) => !in_array($it['id'], $ids, true)));
|
||||
}
|
||||
wr('lists.json', $lists);
|
||||
out(['ok' => true, 'target' => $target, 'lists' => $lists]);
|
||||
|
||||
case 'dict_op':
|
||||
if (!$admin) fail('forbidden', 403);
|
||||
$op = (string)($in['op'] ?? ''); $did = (string)($in['id'] ?? '');
|
||||
$v = str($in['value'] ?? '', 60); $old = (string)($in['old'] ?? '');
|
||||
if ($op === 'add_dict') {
|
||||
if ($v === '') fail('Введите название справочника');
|
||||
foreach ($ref['dicts'] as $d) if (mb_strtolower($d['name']) === mb_strtolower($v)) fail('Справочник с таким названием уже есть');
|
||||
if (count($ref['dicts']) >= 50) fail('Слишком много справочников');
|
||||
$ref['dicts'][] = ['id' => new_id('d'), 'name' => $v, 'values' => []];
|
||||
wr('ref.json', $ref); out(['ref' => $ref]);
|
||||
}
|
||||
$di = dict_idx($ref, $did);
|
||||
if ($di < 0) fail('Справочник не найден', 404);
|
||||
$vals = $ref['dicts'][$di]['values'];
|
||||
$core = in_array($did, ['cats', 'locs', 'ltypes'], true);
|
||||
$usedCols = array_keys(array_filter(cols_map($ref), fn($c) => $c['type'] === 'select' && $c['dict'] === $did));
|
||||
$lists = load_lists();
|
||||
switch ($op) {
|
||||
case 'rename_dict':
|
||||
if ($v === '') fail('Введите название');
|
||||
foreach ($ref['dicts'] as $d) if ($d['id'] !== $did && mb_strtolower($d['name']) === mb_strtolower($v)) fail('Справочник с таким названием уже есть');
|
||||
$ref['dicts'][$di]['name'] = $v; break;
|
||||
case 'delete_dict':
|
||||
if ($core) fail('Встроенный справочник удалить нельзя');
|
||||
if ($usedCols) fail('Справочник используется в колонках — сначала удалите или измените их');
|
||||
array_splice($ref['dicts'], $di, 1); break;
|
||||
case 'add_value':
|
||||
if ($v === '' || in_array($v, $vals, true)) fail('Пустое или повторяющееся значение');
|
||||
$ref['dicts'][$di]['values'][] = $v; break;
|
||||
case 'rename_value':
|
||||
$i = array_search($old, $vals, true);
|
||||
if ($i === false || $v === '' || ($v !== $old && in_array($v, $vals, true))) fail('Пустое или повторяющееся значение');
|
||||
$ref['dicts'][$di]['values'][$i] = $v;
|
||||
foreach ($lists as &$l) foreach ($l['items'] as &$it) foreach ($usedCols as $c) if (($it['v'][$c] ?? null) === $old) $it['v'][$c] = $v;
|
||||
unset($l, $it);
|
||||
if ($did === 'ltypes') foreach ($lists as &$l) if (($l['ltype'] ?? '') === $old) $l['ltype'] = $v;
|
||||
unset($l); wr('lists.json', $lists); break;
|
||||
case 'delete_value':
|
||||
$i = array_search($old, $vals, true);
|
||||
if ($i === false) fail('Не найдено', 404);
|
||||
if ($core && count($vals) < 2) fail('Нельзя удалить последнее значение');
|
||||
if ($did === 'ltypes') foreach ($lists as $l) if (($l['ltype'] ?? '') === $old) fail("Тип «{$old}» назначен спискам (например, «{$l['name']}»). Сначала смените им тип.");
|
||||
foreach ($lists as $l) foreach ($l['items'] as $it) foreach ($usedCols as $c) if (($it['v'][$c] ?? null) === $old) fail("«{$old}» используется в предметах");
|
||||
array_splice($ref['dicts'][$di]['values'], (int)$i, 1); break;
|
||||
default: fail('op');
|
||||
}
|
||||
wr('ref.json', $ref); out(['ref' => $ref]);
|
||||
|
||||
case 'col_op':
|
||||
if (!$admin) fail('forbidden', 403);
|
||||
$op = (string)($in['op'] ?? ''); $cid = (string)($in['id'] ?? '');
|
||||
$name = str($in['name'] ?? '', 60); $type = (string)($in['type'] ?? ''); $dict = $in['dict'] ?? null;
|
||||
$lists = load_lists();
|
||||
$usedIn = function (string $id) use ($lists) { return array_values(array_map(fn($l) => $l['name'], array_filter($lists, fn($l) => in_array($id, $l['cols'], true)))); };
|
||||
$checkName = function () use ($ref, $name, $cid) {
|
||||
if ($name === '') fail('Введите название колонки');
|
||||
foreach (cols_map($ref) as $c) if ($c['id'] !== $cid && mb_strtolower($c['name']) === mb_strtolower($name)) fail('Колонка с таким названием уже есть');
|
||||
};
|
||||
$checkType = function () use ($ref, $type, &$dict) {
|
||||
if (!in_array($type, COL_TYPES, true)) fail('Неверный тип колонки');
|
||||
if ($type === 'select') { if ((string)$dict === 'ltypes' || dict_idx($ref, (string)$dict) < 0) fail('Выберите справочник для списка'); $dict = (string)$dict; }
|
||||
else $dict = null;
|
||||
};
|
||||
if ($op === 'add') {
|
||||
$checkName(); $checkType();
|
||||
if (count($ref['columns']) >= 50) fail('Слишком много колонок');
|
||||
$ref['columns'][] = ['id' => new_id('c'), 'name' => $name, 'type' => $type, 'dict' => $dict];
|
||||
} else {
|
||||
$ci = -1; foreach ($ref['columns'] as $i => $c) if ($c['id'] === $cid) $ci = $i;
|
||||
if ($ci < 0) fail('Колонка не найдена', 404);
|
||||
if ($op === 'update') {
|
||||
$checkName();
|
||||
$c = $ref['columns'][$ci];
|
||||
$dict = $dict === '' ? null : $dict;
|
||||
if ($type !== $c['type'] || ($type === 'select' && (string)$dict !== (string)$c['dict'])) {
|
||||
if ($u = $usedIn($cid)) fail('Тип колонки нельзя менять: она уже используется в списках (' . implode(', ', array_slice($u, 0, 3)) . ')');
|
||||
$checkType();
|
||||
$ref['columns'][$ci]['type'] = $type; $ref['columns'][$ci]['dict'] = $dict;
|
||||
}
|
||||
$ref['columns'][$ci]['name'] = $name;
|
||||
} elseif ($op === 'delete') {
|
||||
if ($u = $usedIn($cid)) fail('Колонка используется в списках: ' . implode(', ', array_slice($u, 0, 3)));
|
||||
array_splice($ref['columns'], $ci, 1);
|
||||
} else fail('op');
|
||||
}
|
||||
wr('ref.json', $ref); out(['ref' => $ref]);
|
||||
|
||||
default: fail('unknown');
|
||||
}
|
||||
Reference in new issue
Block a user