first commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\DB;
|
||||
use App\Core\View;
|
||||
|
||||
class AdminAdController
|
||||
{
|
||||
private function requireAdmin(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ((int)($user['is_admin'] ?? 0) !== 1) {
|
||||
http_response_code(403);
|
||||
echo 'Доступ запрещен';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$groupCount = (int)$pdo->query("SELECT COUNT(*) FROM ad_groups")->fetchColumn();
|
||||
$userCount = (int)$pdo->query("SELECT COUNT(*) FROM user_ad_groups")->fetchColumn();
|
||||
|
||||
View::render('admin/ad/index', [
|
||||
'groupCount' => $groupCount,
|
||||
'userGroupCount' => $userCount,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
// MVP: берем уже залогиненных пользователей и их ad_groups из users
|
||||
$stmt = $pdo->query("
|
||||
SELECT id, ad_groups
|
||||
FROM users
|
||||
WHERE ad_groups IS NOT NULL
|
||||
AND ad_groups <> ''
|
||||
");
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
$pdo->exec("DELETE FROM user_ad_groups");
|
||||
|
||||
foreach ($users as $user) {
|
||||
$groups = json_decode((string)$user['ad_groups'], true);
|
||||
|
||||
if (!is_array($groups)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($groups as $groupName) {
|
||||
$groupName = trim((string)$groupName);
|
||||
if ($groupName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO ad_groups (name, created_at, updated_at)
|
||||
VALUES (?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
updated_at = NOW()
|
||||
");
|
||||
$stmt->execute([$groupName]);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id FROM ad_groups WHERE name = ? LIMIT 1");
|
||||
$stmt->execute([$groupName]);
|
||||
$groupId = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($groupId > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT IGNORE INTO user_ad_groups (user_id, group_id, created_at)
|
||||
VALUES (?, ?, NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
(int)$user['id'],
|
||||
$groupId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
$_SESSION['success'] = 'Синхронизация AD-групп завершена';
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
$_SESSION['error'] = 'Ошибка синхронизации AD: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
header('Location: /admin/ad');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\DB;
|
||||
use App\Core\View;
|
||||
|
||||
class AdminBoardController
|
||||
{
|
||||
private function requireAdmin(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ((int)($user['is_admin'] ?? 0) !== 1) {
|
||||
http_response_code(403);
|
||||
echo 'Доступ запрещен';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->query("
|
||||
SELECT id, name, code, description, is_active, show_on_home, created_at
|
||||
FROM boards
|
||||
ORDER BY id DESC
|
||||
");
|
||||
$boards = $stmt->fetchAll();
|
||||
|
||||
View::render('admin/boards/index', [
|
||||
'boards' => $boards,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
View::render('admin/boards/create', [
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if ($name === '' || $code === '') {
|
||||
$_SESSION['error'] = 'Заполните название и код доски';
|
||||
header('Location: /admin/boards/create');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO boards (name, code, description, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$name,
|
||||
$code,
|
||||
$description !== '' ? $description : null,
|
||||
$isActive,
|
||||
]);
|
||||
$showOnHome = isset($_POST['show_on_home']) ? 1 : 0;
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
public function showModal(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($boardId <= 0) {
|
||||
http_response_code(400);
|
||||
echo 'Board ID is required';
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
b.*,
|
||||
COUNT(t.id) AS task_count,
|
||||
SUM(CASE WHEN t.status = 'NEW' THEN 1 ELSE 0 END) AS new_count,
|
||||
SUM(CASE WHEN t.status = 'IN_PROGRESS' THEN 1 ELSE 0 END) AS in_progress_count,
|
||||
SUM(CASE WHEN t.status = 'REVIEW' THEN 1 ELSE 0 END) AS review_count,
|
||||
SUM(CASE WHEN t.status = 'DONE' THEN 1 ELSE 0 END) AS done_count,
|
||||
SUM(CASE WHEN t.status = 'CANCELED' THEN 1 ELSE 0 END) AS canceled_count,
|
||||
SUM(CASE WHEN t.status = 'OVERDUE' THEN 1 ELSE 0 END) AS overdue_count
|
||||
FROM boards b
|
||||
LEFT JOIN tasks t ON t.board_id = b.id
|
||||
WHERE b.id = ?
|
||||
GROUP BY b.id
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Board not found';
|
||||
return;
|
||||
}
|
||||
|
||||
View::render('admin/boards/modal', [
|
||||
'board' => $board,
|
||||
'user' => Auth::user(),
|
||||
], null);
|
||||
}
|
||||
public function access(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($boardId <= 0) {
|
||||
http_response_code(400);
|
||||
echo 'Board ID is required';
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, name, code, description
|
||||
FROM boards
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Доска не найдена';
|
||||
return;
|
||||
}
|
||||
|
||||
$groups = $pdo->query("
|
||||
SELECT id, name
|
||||
FROM ad_groups
|
||||
ORDER BY name ASC
|
||||
")->fetchAll();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT group_id
|
||||
FROM board_access_groups
|
||||
WHERE board_id = ?
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$selectedGroupIds = array_map('intval', array_column($stmt->fetchAll(), 'group_id'));
|
||||
|
||||
View::render('admin/boards/access', [
|
||||
'board' => $board,
|
||||
'groups' => $groups,
|
||||
'selectedGroupIds' => $selectedGroupIds,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveAccess(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_POST['board_id'] ?? 0);
|
||||
$groupIds = $_POST['group_ids'] ?? [];
|
||||
|
||||
if ($boardId <= 0) {
|
||||
$_SESSION['error'] = 'Некорректная доска';
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM board_access_groups WHERE board_id = ?");
|
||||
$stmt->execute([$boardId]);
|
||||
|
||||
if (is_array($groupIds)) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO board_access_groups (board_id, group_id, created_at)
|
||||
VALUES (?, ?, NOW())
|
||||
");
|
||||
|
||||
foreach ($groupIds as $groupId) {
|
||||
$groupId = (int)$groupId;
|
||||
if ($groupId > 0) {
|
||||
$stmt->execute([$boardId, $groupId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
$_SESSION['success'] = 'Права доступа к доске сохранены';
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
$_SESSION['error'] = 'Ошибка сохранения прав: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
header('Location: /admin/boards/access?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
public function statuses(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($boardId <= 0) {
|
||||
http_response_code(400);
|
||||
echo 'Board ID is required';
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, name, code FROM boards WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Доска не найдена';
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM board_statuses
|
||||
WHERE board_id = ?
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$statuses = $stmt->fetchAll();
|
||||
|
||||
View::render('admin/boards/statuses/index', [
|
||||
'board' => $board,
|
||||
'statuses' => $statuses,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function createStatus(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['board_id'] ?? 0);
|
||||
|
||||
$pdo = DB::connection();
|
||||
$stmt = $pdo->prepare("SELECT id, name, code FROM boards WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Доска не найдена';
|
||||
return;
|
||||
}
|
||||
|
||||
View::render('admin/boards/statuses/create', [
|
||||
'board' => $board,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeStatus(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_POST['board_id'] ?? 0);
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$color = trim($_POST['color'] ?? 'secondary');
|
||||
$sortOrder = (int)($_POST['sort_order'] ?? 100);
|
||||
$isDone = isset($_POST['is_done']) ? 1 : 0;
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if ($boardId <= 0 || $code === '' || $name === '') {
|
||||
$_SESSION['error'] = 'Заполните обязательные поля статуса';
|
||||
header('Location: /admin/boards/statuses/create?board_id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO board_statuses (
|
||||
board_id, code, name, color, sort_order, is_done, is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$boardId,
|
||||
$code,
|
||||
$name,
|
||||
$color,
|
||||
$sortOrder,
|
||||
$isDone,
|
||||
$isActive,
|
||||
]);
|
||||
|
||||
header('Location: /admin/boards/statuses?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function editStatus(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$statusId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
$pdo = DB::connection();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT bs.*, b.name AS board_name, b.code AS board_code
|
||||
FROM board_statuses bs
|
||||
INNER JOIN boards b ON b.id = bs.board_id
|
||||
WHERE bs.id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$statusId]);
|
||||
$status = $stmt->fetch();
|
||||
|
||||
if (!$status) {
|
||||
http_response_code(404);
|
||||
echo 'Статус не найден';
|
||||
return;
|
||||
}
|
||||
|
||||
View::render('admin/boards/statuses/edit', [
|
||||
'status' => $status,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateStatus(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$statusId = (int)($_POST['id'] ?? 0);
|
||||
$code = trim($_POST['code'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$color = trim($_POST['color'] ?? 'secondary');
|
||||
$sortOrder = (int)($_POST['sort_order'] ?? 100);
|
||||
$isDone = isset($_POST['is_done']) ? 1 : 0;
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if ($statusId <= 0 || $code === '' || $name === '') {
|
||||
$_SESSION['error'] = 'Заполните обязательные поля';
|
||||
header('Location: /admin');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT board_id FROM board_statuses WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$statusId]);
|
||||
$boardId = (int)$stmt->fetchColumn();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE board_statuses
|
||||
SET
|
||||
code = ?,
|
||||
name = ?,
|
||||
color = ?,
|
||||
sort_order = ?,
|
||||
is_done = ?,
|
||||
is_active = ?,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$code,
|
||||
$name,
|
||||
$color,
|
||||
$sortOrder,
|
||||
$isDone,
|
||||
$isActive,
|
||||
$statusId,
|
||||
]);
|
||||
|
||||
header('Location: /admin/boards/statuses?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
public function delete(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($boardId <= 0) {
|
||||
$_SESSION['error'] = 'Доска не найдена';
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, name, code
|
||||
FROM boards
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
$_SESSION['error'] = 'Доска не найдена';
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM boards WHERE id = ?");
|
||||
$stmt->execute([$boardId]);
|
||||
|
||||
$_SESSION['success'] = 'Доска "' . ($board['name'] ?? '') . '" удалена вместе со связанными данными';
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
public function edit(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($boardId <= 0) {
|
||||
http_response_code(400);
|
||||
echo 'Board ID is required';
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, name, code, description, is_active, show_on_home
|
||||
FROM boards
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Доска не найдена';
|
||||
return;
|
||||
}
|
||||
|
||||
View::render('admin/boards/edit', [
|
||||
'board' => $board,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_POST['id'] ?? 0);
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$code = trim((string)($_POST['code'] ?? ''));
|
||||
$description = trim((string)($_POST['description'] ?? ''));
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if ($boardId <= 0 || $name === '' || $code === '') {
|
||||
$_SESSION['error'] = 'Заполните название и код доски';
|
||||
header('Location: /admin/boards/edit?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE boards
|
||||
SET
|
||||
name = ?,
|
||||
code = ?,
|
||||
description = ?,
|
||||
is_active = ?,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$name,
|
||||
$code,
|
||||
$description !== '' ? $description : null,
|
||||
$isActive,
|
||||
$boardId,
|
||||
]);
|
||||
|
||||
$_SESSION['success'] = 'Доска обновлена';
|
||||
} catch (\PDOException $e) {
|
||||
$_SESSION['error'] = 'Не удалось обновить доску: ' . $e->getMessage();
|
||||
header('Location: /admin/boards/edit?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
$showOnHome = isset($_POST['show_on_home']) ? 1 : 0;
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
public function saveAll(): void
|
||||
{
|
||||
$pdo = DB::connection();
|
||||
|
||||
$boardId = (int)$_POST['board_id'];
|
||||
|
||||
// === ОБНОВЛЕНИЕ ДОСКИ ===
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE boards SET
|
||||
name = ?,
|
||||
code = ?,
|
||||
description = ?,
|
||||
is_active = ?,
|
||||
show_on_home = ?
|
||||
WHERE id = ?
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$_POST['name'],
|
||||
$_POST['code'],
|
||||
$_POST['description'] ?? null,
|
||||
isset($_POST['is_active']) ? 1 : 0,
|
||||
isset($_POST['show_on_home']) ? 1 : 0,
|
||||
$boardId
|
||||
]);
|
||||
|
||||
// === КАСТОМНЫЕ ПОЛЯ ===
|
||||
if (!empty($_POST['fields'])) {
|
||||
foreach ($_POST['fields'] as $field) {
|
||||
|
||||
if (!empty($field['delete'])) {
|
||||
$pdo->prepare("DELETE FROM board_fields WHERE id = ?")
|
||||
->execute([$field['id']]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$pdo->prepare("
|
||||
UPDATE board_fields
|
||||
SET name = ?, field_type = ?
|
||||
WHERE id = ?
|
||||
")->execute([
|
||||
$field['name'],
|
||||
$field['type'],
|
||||
$field['id']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// === НОВЫЕ ПОЛЯ ===
|
||||
if (!empty($_POST['new_fields'])) {
|
||||
foreach ($_POST['new_fields'] as $f) {
|
||||
if (empty($f['name'])) continue;
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO board_fields (board_id, code, name)
|
||||
VALUES (?, ?, ?)
|
||||
")->execute([
|
||||
$boardId,
|
||||
strtolower(preg_replace('/[^a-z0-9]/', '_', $f['name'])),
|
||||
$f['name']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: ' . $_SERVER['HTTP_REFERER']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\DB;
|
||||
use App\Core\View;
|
||||
|
||||
class AdminBoardFieldController
|
||||
{
|
||||
private function requireAdmin(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ((int)($user['is_admin'] ?? 0) !== 1) {
|
||||
http_response_code(403);
|
||||
echo 'Доступ запрещен';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, name, code FROM boards WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Доска не найдена';
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM board_fields
|
||||
WHERE board_id = ?
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$fields = $stmt->fetchAll();
|
||||
|
||||
View::render('admin/boards/fields/index', [
|
||||
'board' => $board,
|
||||
'fields' => $fields,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['board_id'] ?? 0);
|
||||
|
||||
$pdo = DB::connection();
|
||||
$stmt = $pdo->prepare("SELECT id, name, code FROM boards WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Доска не найдена';
|
||||
return;
|
||||
}
|
||||
|
||||
View::render('admin/boards/fields/create', [
|
||||
'board' => $board,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_POST['board_id'] ?? 0);
|
||||
$code = trim((string)($_POST['code'] ?? ''));
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$fieldType = trim((string)($_POST['field_type'] ?? 'text'));
|
||||
$isRequired = isset($_POST['is_required']) ? 1 : 0;
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
$sortOrder = (int)($_POST['sort_order'] ?? 100);
|
||||
|
||||
$settings = null;
|
||||
if ($fieldType === 'select') {
|
||||
$rawOptions = trim((string)($_POST['select_options'] ?? ''));
|
||||
$options = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $rawOptions))));
|
||||
$settings = json_encode(['options' => $options], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
if ($boardId <= 0 || $code === '' || $name === '') {
|
||||
$_SESSION['error'] = 'Заполни обязательные поля';
|
||||
header('Location: /admin/boards/fields/create?board_id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO board_fields (
|
||||
board_id, code, name, field_type, is_required, sort_order, is_active, settings_json, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$boardId,
|
||||
$code,
|
||||
$name,
|
||||
$fieldType,
|
||||
$isRequired,
|
||||
$sortOrder,
|
||||
$isActive,
|
||||
$settings,
|
||||
]);
|
||||
|
||||
header('Location: /admin/boards/fields?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function edit(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$fieldId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
$pdo = DB::connection();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT bf.*, b.name AS board_name, b.code AS board_code
|
||||
FROM board_fields bf
|
||||
INNER JOIN boards b ON b.id = bf.board_id
|
||||
WHERE bf.id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$fieldId]);
|
||||
$field = $stmt->fetch();
|
||||
|
||||
if (!$field) {
|
||||
http_response_code(404);
|
||||
echo 'Поле не найдено';
|
||||
return;
|
||||
}
|
||||
|
||||
View::render('admin/boards/fields/edit', [
|
||||
'field' => $field,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$fieldId = (int)($_POST['id'] ?? 0);
|
||||
$code = trim((string)($_POST['code'] ?? ''));
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$fieldType = trim((string)($_POST['field_type'] ?? 'text'));
|
||||
$isRequired = isset($_POST['is_required']) ? 1 : 0;
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
$sortOrder = (int)($_POST['sort_order'] ?? 100);
|
||||
|
||||
$settings = null;
|
||||
if ($fieldType === 'select') {
|
||||
$rawOptions = trim((string)($_POST['select_options'] ?? ''));
|
||||
$options = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $rawOptions))));
|
||||
$settings = json_encode(['options' => $options], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
if ($fieldId <= 0 || $code === '' || $name === '') {
|
||||
$_SESSION['error'] = 'Заполни обязательные поля';
|
||||
header('Location: /admin');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT board_id FROM board_fields WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$fieldId]);
|
||||
$boardId = (int)$stmt->fetchColumn();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE board_fields
|
||||
SET
|
||||
code = ?,
|
||||
name = ?,
|
||||
field_type = ?,
|
||||
is_required = ?,
|
||||
sort_order = ?,
|
||||
is_active = ?,
|
||||
settings_json = ?,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$code,
|
||||
$name,
|
||||
$fieldType,
|
||||
$isRequired,
|
||||
$sortOrder,
|
||||
$isActive,
|
||||
$settings,
|
||||
$fieldId,
|
||||
]);
|
||||
|
||||
header('Location: /admin/boards/fields?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
public function delete(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$fieldId = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($fieldId <= 0) {
|
||||
$_SESSION['error'] = 'Поле не найдено';
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, board_id
|
||||
FROM board_fields
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$fieldId]);
|
||||
$field = $stmt->fetch();
|
||||
|
||||
if (!$field) {
|
||||
$_SESSION['error'] = 'Поле не найдено';
|
||||
header('Location: /admin/boards');
|
||||
exit;
|
||||
}
|
||||
|
||||
$boardId = (int)$field['board_id'];
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM board_fields WHERE id = ?");
|
||||
$stmt->execute([$fieldId]);
|
||||
|
||||
$_SESSION['success'] = 'Поле удалено';
|
||||
header('Location: /admin/boards/fields?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\DB;
|
||||
use App\Core\View;
|
||||
|
||||
class AdminBoardSourceController
|
||||
{
|
||||
private function requireAdmin(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ((int)($user['is_admin'] ?? 0) !== 1) {
|
||||
http_response_code(403);
|
||||
echo 'Доступ запрещен';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private function extractSpreadsheetId(string $url): string
|
||||
{
|
||||
if (preg_match('~/spreadsheets/d/([a-zA-Z0-9-_]+)~', $url, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return trim($url);
|
||||
}
|
||||
|
||||
public function edit(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, name, code FROM boards WHERE id = ? LIMIT 1");
|
||||
$stmt->execute([$boardId]);
|
||||
$board = $stmt->fetch();
|
||||
|
||||
if (!$board) {
|
||||
http_response_code(404);
|
||||
echo 'Доска не найдена';
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM board_sources
|
||||
WHERE board_id = ?
|
||||
AND source_type = 'google_sheet'
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$source = $stmt->fetch();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, code, name
|
||||
FROM board_fields
|
||||
WHERE board_id = ?
|
||||
AND is_active = 1
|
||||
ORDER BY sort_order ASC, id ASC
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$customFields = $stmt->fetchAll();
|
||||
|
||||
$mappings = [];
|
||||
if ($source) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT target_type, target_key, source_column_name
|
||||
FROM board_source_mappings
|
||||
WHERE board_source_id = ?
|
||||
AND is_active = 1
|
||||
");
|
||||
$stmt->execute([$source['id']]);
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$mappings[$row['target_type'] . ':' . $row['target_key']] = $row['source_column_name'];
|
||||
}
|
||||
}
|
||||
|
||||
$baseFields = [
|
||||
['key' => 'creator_name', 'name' => 'Постановщик'],
|
||||
['key' => 'assignee_name', 'name' => 'Ответственный'],
|
||||
['key' => 'name', 'name' => 'Наименование'],
|
||||
['key' => 'description', 'name' => 'Описание задачи'],
|
||||
['key' => 'status', 'name' => 'Статус'],
|
||||
['key' => 'priority', 'name' => 'Приоритет'],
|
||||
['key' => 'crm_id', 'name' => 'CRM ID'],
|
||||
['key' => 'completed_flag', 'name' => 'Выполнено'],
|
||||
['key' => 'task_created_at', 'name' => 'Дата постановки'],
|
||||
['key' => 'planned_at', 'name' => 'Дата план'],
|
||||
['key' => 'completed_at', 'name' => 'Дата факт'],
|
||||
];
|
||||
|
||||
View::render('admin/boards/source/edit', [
|
||||
'board' => $board,
|
||||
'source' => $source,
|
||||
'baseFields' => $baseFields,
|
||||
'customFields' => $customFields,
|
||||
'mappings' => $mappings,
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
$boardId = (int)($_POST['board_id'] ?? 0);
|
||||
$spreadsheetUrl = trim((string)($_POST['spreadsheet_url'] ?? ''));
|
||||
$sheetName = trim((string)($_POST['sheet_name'] ?? ''));
|
||||
$syncMode = trim((string)($_POST['sync_mode'] ?? 'import_export'));
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if ($boardId <= 0 || $spreadsheetUrl === '' || $sheetName === '') {
|
||||
$_SESSION['error'] = 'Заполни ссылку и имя вкладки';
|
||||
header('Location: /admin/boards/source?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
$spreadsheetId = $this->extractSpreadsheetId($spreadsheetUrl);
|
||||
|
||||
if ($spreadsheetId === '') {
|
||||
$_SESSION['error'] = 'Не удалось определить Spreadsheet ID';
|
||||
header('Location: /admin/boards/source?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
$pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM board_sources
|
||||
WHERE board_id = ?
|
||||
AND source_type = 'google_sheet'
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$boardId]);
|
||||
$sourceId = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($sourceId > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE board_sources
|
||||
SET
|
||||
spreadsheet_url = ?,
|
||||
source_key = ?,
|
||||
sheet_name = ?,
|
||||
sync_mode = ?,
|
||||
is_active = ?,
|
||||
updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$spreadsheetUrl,
|
||||
$spreadsheetId,
|
||||
$sheetName,
|
||||
$syncMode,
|
||||
$isActive,
|
||||
$sourceId,
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO board_sources (
|
||||
board_id,
|
||||
source_type,
|
||||
spreadsheet_url,
|
||||
source_key,
|
||||
sheet_name,
|
||||
sync_mode,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, 'google_sheet', ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$boardId,
|
||||
$spreadsheetUrl,
|
||||
$spreadsheetId,
|
||||
$sheetName,
|
||||
$syncMode,
|
||||
$isActive,
|
||||
]);
|
||||
|
||||
$sourceId = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM board_source_mappings WHERE board_source_id = ?");
|
||||
$stmt->execute([$sourceId]);
|
||||
|
||||
$baseMappings = $_POST['mapping_base'] ?? [];
|
||||
if (is_array($baseMappings)) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO board_source_mappings (
|
||||
board_source_id,
|
||||
target_type,
|
||||
target_key,
|
||||
source_column_name,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, 'base', ?, ?, 1, NOW(), NOW())
|
||||
");
|
||||
|
||||
foreach ($baseMappings as $key => $columnName) {
|
||||
$columnName = trim((string)$columnName);
|
||||
if ($columnName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([$sourceId, $key, $columnName]);
|
||||
}
|
||||
}
|
||||
|
||||
$customMappings = $_POST['mapping_custom'] ?? [];
|
||||
if (is_array($customMappings)) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO board_source_mappings (
|
||||
board_source_id,
|
||||
target_type,
|
||||
target_key,
|
||||
source_column_name,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, 'custom', ?, ?, 1, NOW(), NOW())
|
||||
");
|
||||
|
||||
foreach ($customMappings as $key => $columnName) {
|
||||
$columnName = trim((string)$columnName);
|
||||
if ($columnName === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([$sourceId, $key, $columnName]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
$_SESSION['success'] = 'Интеграция Google Sheets сохранена';
|
||||
} catch (\Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
$_SESSION['error'] = 'Ошибка сохранения интеграции: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
header('Location: /admin/boards/source?id=' . $boardId);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\View;
|
||||
|
||||
class AdminController
|
||||
{
|
||||
protected function requireAdmin(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if ((int)($user['is_admin'] ?? 0) !== 1) {
|
||||
http_response_code(403);
|
||||
echo 'Доступ запрещен';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
$this->requireAdmin();
|
||||
|
||||
View::render('admin/index', [
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\DB;
|
||||
use App\Core\View;
|
||||
use App\Services\Auth\LdapService;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
public function showLogin(): void
|
||||
{
|
||||
View::render('auth/login', [], 'guest');
|
||||
}
|
||||
|
||||
public function login(): void
|
||||
{
|
||||
$login = trim($_POST['login'] ?? '');
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
|
||||
if ($login === '' || $password === '') {
|
||||
$_SESSION['error'] = 'Введите логин и пароль';
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$ldapService = new LdapService();
|
||||
$ldapUser = $ldapService->authenticate($login, $password);
|
||||
|
||||
if ($ldapUser === false) {
|
||||
$_SESSION['error'] = 'Неверный логин или пароль';
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE login = ? LIMIT 1");
|
||||
$stmt->execute([$ldapUser['login']]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO users (login, display_name, email, is_admin, ad_groups, last_login_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW(), NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$ldapUser['login'],
|
||||
$ldapUser['display_name'],
|
||||
$ldapUser['email'],
|
||||
$ldapUser['is_admin'] ? 1 : 0,
|
||||
json_encode($ldapUser['groups'], JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE users
|
||||
SET display_name = ?, email = ?, is_admin = ?, ad_groups = ?, last_login_at = NOW(), updated_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([
|
||||
$ldapUser['display_name'],
|
||||
$ldapUser['email'],
|
||||
$ldapUser['is_admin'] ? 1 : 0,
|
||||
json_encode($ldapUser['groups'], JSON_UNESCAPED_UNICODE),
|
||||
$user['id'],
|
||||
]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM users WHERE login = ? LIMIT 1");
|
||||
$stmt->execute([$ldapUser['login']]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
Auth::logout();
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\DB;
|
||||
use App\Services\WebSocket\EventPublisher;
|
||||
|
||||
class CommentController
|
||||
{
|
||||
public function store(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$taskId = (int)($_POST['task_id'] ?? 0);
|
||||
$text = trim((string)($_POST['text'] ?? ''));
|
||||
|
||||
if ($taskId <= 0 || $text === '') {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Комментарий не заполнен'], 400);
|
||||
}
|
||||
|
||||
$_SESSION['error'] = 'Комментарий не заполнен';
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
// 1. Добавляем комментарий
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO task_comments (
|
||||
task_id,
|
||||
user_id,
|
||||
text,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$taskId,
|
||||
Auth::user()['id'],
|
||||
$text,
|
||||
]);
|
||||
|
||||
// 2. Получаем мета
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
COUNT(*) AS comment_count,
|
||||
MAX(id) AS last_comment_id
|
||||
FROM task_comments
|
||||
WHERE task_id = ?
|
||||
");
|
||||
$stmt->execute([$taskId]);
|
||||
$commentMeta = $stmt->fetch();
|
||||
|
||||
// 3. Получаем задачу
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, name, assignee_id
|
||||
FROM tasks
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$taskId]);
|
||||
$task = $stmt->fetch();
|
||||
|
||||
// 4. WS — обновление счетчиков
|
||||
$publisher = new EventPublisher();
|
||||
$publisher->publish([
|
||||
'type' => 'comment_added',
|
||||
'task_id' => $taskId,
|
||||
'comment_count' => (int)($commentMeta['comment_count'] ?? 0),
|
||||
'last_comment_id' => (int)($commentMeta['last_comment_id'] ?? 0),
|
||||
'author_user_id' => (int)Auth::user()['id'],
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
// 5. Уведомление ответственному
|
||||
if ($task && !empty($task['assignee_id'])) {
|
||||
|
||||
$assigneeId = (int)$task['assignee_id'];
|
||||
$currentUserId = (int)Auth::user()['id'];
|
||||
|
||||
if ($assigneeId !== $currentUserId) {
|
||||
|
||||
// запись в БД (если есть таблица notifications)
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO notifications (
|
||||
user_id,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
data_json,
|
||||
is_read,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, 0, NOW())
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
$assigneeId,
|
||||
'task_comment_added',
|
||||
'Новый комментарий',
|
||||
'Задача: ' . (string)$task['name'],
|
||||
json_encode(['task_id' => $taskId], JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
|
||||
// WS уведомление
|
||||
$publisher->publish([
|
||||
'type' => 'notification_created',
|
||||
'user_id' => $assigneeId,
|
||||
'task_id' => $taskId,
|
||||
'title' => 'Новый комментарий',
|
||||
'message' => 'Задача: ' . (string)$task['name'],
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Ответ
|
||||
if ($this->isAjax()) {
|
||||
$this->json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
'comment_count' => (int)($commentMeta['comment_count'] ?? 0),
|
||||
'last_comment_id' => (int)($commentMeta['last_comment_id'] ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
private function isAjax(): bool
|
||||
{
|
||||
return strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
private function json(array $data, int $statusCode = 200): void
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\View;
|
||||
|
||||
class DashboardController
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
View::render('dashboard/index', [
|
||||
'user' => Auth::user(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Services\NotificationService;
|
||||
|
||||
class NotificationController
|
||||
{
|
||||
public function list(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
http_response_code(403);
|
||||
echo 'Unauthorized';
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new NotificationService();
|
||||
$items = $service->getLatest((int)Auth::user()['id'], 20);
|
||||
$unreadCount = $service->getUnreadCount((int)Auth::user()['id']);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'items' => $items,
|
||||
'unread_count' => $unreadCount,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function readAll(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
http_response_code(403);
|
||||
echo 'Unauthorized';
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new NotificationService();
|
||||
$service->markAllRead((int)Auth::user()['id']);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['success' => true], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\DB;
|
||||
|
||||
class TaskFileController
|
||||
{
|
||||
public function upload(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$taskId = (int)($_POST['task_id'] ?? 0);
|
||||
|
||||
if ($taskId <= 0 || empty($_FILES['file'])) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Файл не выбран'], 400);
|
||||
}
|
||||
|
||||
$_SESSION['error'] = 'Файл не выбран';
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Ошибка загрузки файла'], 400);
|
||||
}
|
||||
|
||||
$_SESSION['error'] = 'Ошибка загрузки файла';
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
|
||||
$uploadDir = __DIR__ . '/../../storage/uploads/tasks/' . $taskId;
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
}
|
||||
|
||||
$originalName = (string)$file['name'];
|
||||
$tmpName = (string)$file['tmp_name'];
|
||||
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
$storedName = uniqid('task_', true) . ($extension ? '.' . $extension : '');
|
||||
$targetPath = $uploadDir . '/' . $storedName;
|
||||
|
||||
if (!move_uploaded_file($tmpName, $targetPath)) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Не удалось сохранить файл'], 500);
|
||||
}
|
||||
|
||||
$_SESSION['error'] = 'Не удалось сохранить файл';
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
|
||||
$relativePath = 'storage/uploads/tasks/' . $taskId . '/' . $storedName;
|
||||
$fileSize = (int)filesize($targetPath);
|
||||
$mimeType = mime_content_type($targetPath) ?: null;
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO task_files (
|
||||
task_id,
|
||||
uploaded_by,
|
||||
original_name,
|
||||
stored_name,
|
||||
file_path,
|
||||
file_size,
|
||||
mime_type,
|
||||
created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$taskId,
|
||||
Auth::user()['id'],
|
||||
$originalName,
|
||||
$storedName,
|
||||
$relativePath,
|
||||
$fileSize,
|
||||
$mimeType,
|
||||
]);
|
||||
|
||||
if ($this->isAjax()) {
|
||||
$this->json([
|
||||
'success' => true,
|
||||
'task_id' => $taskId,
|
||||
]);
|
||||
}
|
||||
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
|
||||
public function download(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$fileId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($fileId <= 0) {
|
||||
http_response_code(404);
|
||||
echo 'Файл не найден';
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM task_files
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$fileId]);
|
||||
$file = $stmt->fetch();
|
||||
|
||||
if (!$file) {
|
||||
http_response_code(404);
|
||||
echo 'Файл не найден';
|
||||
return;
|
||||
}
|
||||
|
||||
$absolutePath = __DIR__ . '/../../' . $file['file_path'];
|
||||
|
||||
if (!is_file($absolutePath)) {
|
||||
http_response_code(404);
|
||||
echo 'Файл не найден на диске';
|
||||
return;
|
||||
}
|
||||
|
||||
header('Content-Description: File Transfer');
|
||||
header('Content-Type: ' . ($file['mime_type'] ?: 'application/octet-stream'));
|
||||
header('Content-Disposition: attachment; filename="' . basename((string)$file['original_name']) . '"');
|
||||
header('Content-Length: ' . filesize($absolutePath));
|
||||
header('Pragma: public');
|
||||
|
||||
readfile($absolutePath);
|
||||
exit;
|
||||
}
|
||||
public function view(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$fileId = (int)($_GET['id'] ?? 0);
|
||||
|
||||
if ($fileId <= 0) {
|
||||
http_response_code(404);
|
||||
echo 'Файл не найден';
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM task_files
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$fileId]);
|
||||
$file = $stmt->fetch();
|
||||
|
||||
if (!$file) {
|
||||
http_response_code(404);
|
||||
echo 'Файл не найден';
|
||||
return;
|
||||
}
|
||||
|
||||
$absolutePath = __DIR__ . '/../../' . $file['file_path'];
|
||||
|
||||
if (!is_file($absolutePath)) {
|
||||
http_response_code(404);
|
||||
echo 'Файл не найден на диске';
|
||||
return;
|
||||
}
|
||||
|
||||
$mimeType = $file['mime_type'] ?: mime_content_type($absolutePath) ?: 'application/octet-stream';
|
||||
|
||||
header('Content-Type: ' . $mimeType);
|
||||
header('Content-Length: ' . filesize($absolutePath));
|
||||
header('Content-Disposition: inline; filename="' . basename((string)$file['original_name']) . '"');
|
||||
|
||||
readfile($absolutePath);
|
||||
exit;
|
||||
}
|
||||
public function delete(): void
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Unauthorized'], 403);
|
||||
}
|
||||
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
|
||||
$fileId = (int)($_POST['id'] ?? 0);
|
||||
|
||||
if ($fileId <= 0) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Файл не найден'], 404);
|
||||
}
|
||||
|
||||
$_SESSION['error'] = 'Файл не найден';
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = DB::connection();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT *
|
||||
FROM task_files
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$fileId]);
|
||||
$file = $stmt->fetch();
|
||||
|
||||
if (!$file) {
|
||||
if ($this->isAjax()) {
|
||||
$this->json(['success' => false, 'message' => 'Файл не найден'], 404);
|
||||
}
|
||||
|
||||
$_SESSION['error'] = 'Файл не найден';
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
|
||||
$absolutePath = __DIR__ . '/../../' . $file['file_path'];
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM task_files WHERE id = ?");
|
||||
$stmt->execute([$fileId]);
|
||||
|
||||
if (is_file($absolutePath)) {
|
||||
@unlink($absolutePath);
|
||||
}
|
||||
|
||||
if ($this->isAjax()) {
|
||||
$this->json([
|
||||
'success' => true,
|
||||
'file_id' => $fileId,
|
||||
]);
|
||||
}
|
||||
|
||||
header('Location: /tasks');
|
||||
exit;
|
||||
}
|
||||
private function isAjax(): bool
|
||||
{
|
||||
return strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
private function json(array $data, int $statusCode = 200): void
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user