899 lines
37 KiB
PHP
899 lines
37 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/admin_bootstrap.php';
|
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/file_api_client.php';
|
|
|
|
$ADMIN_TASK_TITLE_MAX = 100;
|
|
$ADMIN_TASK_COMMENT_MAX = 2000;
|
|
$ADMIN_TASK_ATTACHMENTS_MAX = 10;
|
|
$ADMIN_TASK_ATTACHMENT_MAX_BYTES = 20 * 1024 * 1024;
|
|
|
|
$pdo = admin_get_pdo();
|
|
$auth = admin_require_auth($pdo);
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
|
|
|
function admin_task_files_table_exists(PDO $pdo): bool
|
|
{
|
|
static $cached = null;
|
|
if ($cached !== null) {
|
|
return $cached;
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t');
|
|
$stmt->execute([':t' => 'admin_task_files']);
|
|
$cached = ((int)$stmt->fetchColumn() > 0);
|
|
|
|
if (!$cached) {
|
|
$pdo->exec(
|
|
'CREATE TABLE IF NOT EXISTS admin_task_files ('
|
|
. 'id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,'
|
|
. 'task_id BIGINT UNSIGNED NOT NULL,'
|
|
. 'file_name VARCHAR(255) NOT NULL,'
|
|
. 'file_mime VARCHAR(255) NULL,'
|
|
. 'file_size BIGINT UNSIGNED NULL,'
|
|
. 'file_path VARCHAR(500) NOT NULL COMMENT \'Ścieżka na dysku relative to files_base_dir\','
|
|
. 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,'
|
|
. 'PRIMARY KEY (id),'
|
|
. 'KEY idx_task_id (task_id),'
|
|
. 'KEY idx_created_at (created_at),'
|
|
. 'CONSTRAINT fk_admin_task_files_task FOREIGN KEY (task_id) REFERENCES admin_tasks(id) ON DELETE CASCADE'
|
|
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
|
);
|
|
|
|
$stmt->execute([':t' => 'admin_task_files']);
|
|
$cached = ((int)$stmt->fetchColumn() > 0);
|
|
}
|
|
} catch (Throwable $e) {
|
|
$cached = false;
|
|
}
|
|
|
|
return $cached;
|
|
}
|
|
|
|
function admin_task_comments_table_exists(PDO $pdo): bool
|
|
{
|
|
static $cached = null;
|
|
if ($cached !== null) {
|
|
return $cached;
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :t');
|
|
$stmt->execute([':t' => 'admin_task_comments']);
|
|
$cached = ((int)$stmt->fetchColumn() > 0);
|
|
|
|
if (!$cached) {
|
|
$pdo->exec(
|
|
'CREATE TABLE IF NOT EXISTS admin_task_comments ('
|
|
. 'id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,'
|
|
. 'task_id BIGINT UNSIGNED NOT NULL,'
|
|
. 'user_id INT NOT NULL,'
|
|
. 'username VARCHAR(100) NOT NULL,'
|
|
. 'comment TEXT NOT NULL,'
|
|
. 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,'
|
|
. 'updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,'
|
|
. 'PRIMARY KEY (id),'
|
|
. 'KEY idx_task_id (task_id),'
|
|
. 'KEY idx_created_at (created_at),'
|
|
. 'KEY idx_user_id (user_id),'
|
|
. 'CONSTRAINT fk_admin_task_comments_task FOREIGN KEY (task_id) REFERENCES admin_tasks(id) ON DELETE CASCADE'
|
|
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci'
|
|
);
|
|
|
|
$stmt->execute([':t' => 'admin_task_comments']);
|
|
$cached = ((int)$stmt->fetchColumn() > 0);
|
|
}
|
|
} catch (Throwable $e) {
|
|
$cached = false;
|
|
}
|
|
|
|
return $cached;
|
|
}
|
|
|
|
function admin_task_get_comments_count_map(PDO $pdo, array $taskIds): array
|
|
{
|
|
$map = [];
|
|
if (empty($taskIds) || !admin_task_comments_table_exists($pdo)) {
|
|
return $map;
|
|
}
|
|
|
|
$taskIds = array_values(array_unique(array_map('intval', $taskIds)));
|
|
$taskIds = array_values(array_filter($taskIds, static fn($id) => $id > 0));
|
|
if (empty($taskIds)) {
|
|
return $map;
|
|
}
|
|
|
|
$ph = [];
|
|
$bind = [];
|
|
foreach ($taskIds as $i => $id) {
|
|
$k = ':id' . $i;
|
|
$ph[] = $k;
|
|
$bind[$k] = $id;
|
|
}
|
|
|
|
$sql = 'SELECT task_id, COUNT(*) AS cnt FROM admin_task_comments WHERE task_id IN (' . implode(', ', $ph) . ') GROUP BY task_id';
|
|
$stmt = $pdo->prepare($sql);
|
|
foreach ($bind as $k => $v) {
|
|
$stmt->bindValue($k, $v, PDO::PARAM_INT);
|
|
}
|
|
$stmt->execute();
|
|
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($rows as $r) {
|
|
$taskId = (int)($r['task_id'] ?? 0);
|
|
if ($taskId > 0) {
|
|
$map[$taskId] = (int)($r['cnt'] ?? 0);
|
|
}
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
function admin_task_assert_exists(PDO $pdo, int $taskId): void
|
|
{
|
|
$stmt = $pdo->prepare('SELECT id FROM admin_tasks WHERE id = :id LIMIT 1');
|
|
$stmt->execute([':id' => $taskId]);
|
|
if (!(bool)$stmt->fetchColumn()) {
|
|
admin_json_error('Nie znaleziono notatki', 404);
|
|
}
|
|
}
|
|
|
|
function admin_task_list_comments(PDO $pdo, int $taskId): array
|
|
{
|
|
if (!admin_task_comments_table_exists($pdo)) {
|
|
return [];
|
|
}
|
|
|
|
$stmt = $pdo->prepare(
|
|
'SELECT id, task_id, user_id, username, comment, created_at, updated_at '
|
|
. 'FROM admin_task_comments WHERE task_id = :task_id ORDER BY id ASC'
|
|
);
|
|
$stmt->execute([':task_id' => $taskId]);
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
|
}
|
|
|
|
function admin_task_detect_mime(string $tmpPath, string $originalName, string $browserMime = ''): string
|
|
{
|
|
$browserMime = strtolower(trim(explode(';', $browserMime)[0] ?? ''));
|
|
|
|
$detectedMime = '';
|
|
if (function_exists('finfo_open')) {
|
|
$fi = finfo_open(FILEINFO_MIME_TYPE);
|
|
if ($fi) {
|
|
$detectedMime = strtolower(trim((string)(finfo_file($fi, $tmpPath) ?: '')));
|
|
finfo_close($fi);
|
|
}
|
|
}
|
|
|
|
$ext = strtolower((string)pathinfo($originalName, PATHINFO_EXTENSION));
|
|
$extMimeMap = [
|
|
'md' => 'text/markdown',
|
|
'markdown' => 'text/markdown',
|
|
'txt' => 'text/plain',
|
|
'pdf' => 'application/pdf',
|
|
'zip' => 'application/zip',
|
|
'mp4' => 'video/mp4',
|
|
'mp3' => 'audio/mpeg',
|
|
'wav' => 'audio/wav',
|
|
'doc' => 'application/msword',
|
|
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'xls' => 'application/vnd.ms-excel',
|
|
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
];
|
|
$extMime = $ext !== '' && isset($extMimeMap[$ext]) ? $extMimeMap[$ext] : '';
|
|
|
|
foreach ([$detectedMime, $browserMime, $extMime] as $candidate) {
|
|
if ($candidate !== '' && $candidate !== 'application/octet-stream') {
|
|
return $candidate;
|
|
}
|
|
}
|
|
|
|
return $extMime !== '' ? $extMime : ($browserMime !== '' ? $browserMime : 'application/octet-stream');
|
|
}
|
|
|
|
function admin_task_collect_uploads(int $maxFileBytes): array
|
|
{
|
|
$uploads = [];
|
|
|
|
$uploadErrorToMessage = static function (int $err): string {
|
|
if ($err === UPLOAD_ERR_INI_SIZE || $err === UPLOAD_ERR_FORM_SIZE) {
|
|
return 'Plik przekracza limit uploadu serwera PHP (sprawdź upload_max_filesize i post_max_size).';
|
|
}
|
|
if ($err === UPLOAD_ERR_PARTIAL) {
|
|
return 'Plik został wysłany tylko częściowo.';
|
|
}
|
|
if ($err === UPLOAD_ERR_NO_TMP_DIR) {
|
|
return 'Brak katalogu tymczasowego na serwerze.';
|
|
}
|
|
if ($err === UPLOAD_ERR_CANT_WRITE) {
|
|
return 'Serwer nie może zapisać pliku na dysk.';
|
|
}
|
|
if ($err === UPLOAD_ERR_EXTENSION) {
|
|
return 'Upload został zatrzymany przez rozszerzenie PHP.';
|
|
}
|
|
return 'Błąd uploadu pliku.';
|
|
};
|
|
|
|
$append = static function ($name, $type, $size, $tmpName, $error) use (&$uploads, $maxFileBytes, $uploadErrorToMessage): void {
|
|
$err = isset($error) ? (int)$error : UPLOAD_ERR_NO_FILE;
|
|
if ($err === UPLOAD_ERR_NO_FILE) {
|
|
return;
|
|
}
|
|
|
|
if ($err !== UPLOAD_ERR_OK) {
|
|
admin_json_error($uploadErrorToMessage($err) . ' (kod: ' . $err . ')', 422);
|
|
}
|
|
|
|
$actualSize = isset($size) ? (int)$size : 0;
|
|
if ($actualSize > $maxFileBytes) {
|
|
$mb = (int)round($maxFileBytes / 1024 / 1024);
|
|
admin_json_error('Każdy załącznik może mieć maksymalnie ' . $mb . ' MB', 422);
|
|
}
|
|
|
|
$tmp = (string)($tmpName ?? '');
|
|
if ($tmp === '' || !is_uploaded_file($tmp)) {
|
|
admin_json_error('Nieprawidłowy upload pliku', 422);
|
|
}
|
|
|
|
// Wyślij plik do serwisu plików FastAPI, zapisze na dysku
|
|
$origName = (string)($name ?? 'plik');
|
|
$mimeType = admin_task_detect_mime($tmp, $origName, (string)($type ?? ''));
|
|
try {
|
|
$fileApiResult = get_file_api_client()->upload('admin_tasks', $tmp, $origName, $mimeType);
|
|
} catch (RuntimeException $e) {
|
|
$status = (int)$e->getCode();
|
|
if ($status < 400 || $status > 599) {
|
|
$status = 500;
|
|
}
|
|
admin_json_error('Błąd zapisu załącznika: ' . $e->getMessage(), $status);
|
|
}
|
|
|
|
$uploads[] = [
|
|
'file_name' => $origName,
|
|
'file_mime' => $mimeType,
|
|
'file_size' => isset($size) ? (int)$size : null,
|
|
'file_path' => (string)($fileApiResult['path'] ?? ''),
|
|
];
|
|
};
|
|
|
|
$fields = ['files', 'file'];
|
|
foreach ($fields as $field) {
|
|
if (empty($_FILES[$field]) || !is_array($_FILES[$field])) {
|
|
continue;
|
|
}
|
|
|
|
$upload = $_FILES[$field];
|
|
$isMulti = isset($upload['name']) && is_array($upload['name']);
|
|
|
|
if ($isMulti) {
|
|
$count = count($upload['name']);
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$append(
|
|
$upload['name'][$i] ?? null,
|
|
$upload['type'][$i] ?? null,
|
|
$upload['size'][$i] ?? null,
|
|
$upload['tmp_name'][$i] ?? null,
|
|
$upload['error'][$i] ?? UPLOAD_ERR_NO_FILE
|
|
);
|
|
}
|
|
} else {
|
|
$append(
|
|
$upload['name'] ?? null,
|
|
$upload['type'] ?? null,
|
|
$upload['size'] ?? null,
|
|
$upload['tmp_name'] ?? null,
|
|
$upload['error'] ?? UPLOAD_ERR_NO_FILE
|
|
);
|
|
}
|
|
}
|
|
|
|
return $uploads;
|
|
}
|
|
|
|
function admin_task_count_current_attachments(PDO $pdo, int $taskId): array
|
|
{
|
|
$legacyCount = 0;
|
|
$modernCount = 0;
|
|
|
|
$stmt = $pdo->prepare('SELECT (file_name IS NOT NULL AND file_name <> \'\') AS has_file FROM admin_tasks WHERE id = :id LIMIT 1');
|
|
$stmt->execute([':id' => $taskId]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($row && (int)($row['has_file'] ?? 0) === 1) {
|
|
$legacyCount = 1;
|
|
}
|
|
|
|
if (admin_task_files_table_exists($pdo)) {
|
|
$stmt = $pdo->prepare('SELECT COUNT(*) FROM admin_task_files WHERE task_id = :id');
|
|
$stmt->execute([':id' => $taskId]);
|
|
$modernCount = (int)$stmt->fetchColumn();
|
|
}
|
|
|
|
return [
|
|
'legacy' => $legacyCount,
|
|
'modern' => $modernCount,
|
|
'total' => $legacyCount + $modernCount,
|
|
];
|
|
}
|
|
|
|
function admin_task_parse_delete_file_ids($value): array
|
|
{
|
|
if ($value === null) {
|
|
return [];
|
|
}
|
|
|
|
if (is_string($value)) {
|
|
$value = trim($value);
|
|
if ($value === '') {
|
|
return [];
|
|
}
|
|
$value = explode(',', $value);
|
|
}
|
|
|
|
if (!is_array($value)) {
|
|
return [];
|
|
}
|
|
|
|
$ids = [];
|
|
foreach ($value as $item) {
|
|
$id = (int)$item;
|
|
if ($id > 0) {
|
|
$ids[] = $id;
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($ids));
|
|
}
|
|
|
|
function admin_task_insert_files(PDO $pdo, int $taskId, array $uploads): void
|
|
{
|
|
if (empty($uploads)) {
|
|
return;
|
|
}
|
|
|
|
$stmt = $pdo->prepare(
|
|
'INSERT INTO admin_task_files (task_id, file_name, file_mime, file_size, file_path) '
|
|
. 'VALUES (:task_id, :file_name, :file_mime, :file_size, :file_path)'
|
|
);
|
|
|
|
foreach ($uploads as $file) {
|
|
$stmt->bindValue(':task_id', $taskId, PDO::PARAM_INT);
|
|
$stmt->bindValue(':file_name', (string)$file['file_name'], PDO::PARAM_STR);
|
|
$stmt->bindValue(':file_mime', (string)$file['file_mime'], PDO::PARAM_STR);
|
|
$stmt->bindValue(':file_size', $file['file_size'] !== null ? (int)$file['file_size'] : null, $file['file_size'] !== null ? PDO::PARAM_INT : PDO::PARAM_NULL);
|
|
$stmt->bindValue(':file_path', (string)$file['file_path'], PDO::PARAM_STR);
|
|
$stmt->execute();
|
|
}
|
|
}
|
|
|
|
function admin_task_get_attachments_by_task(PDO $pdo, array $taskIds): array
|
|
{
|
|
$map = [];
|
|
if (empty($taskIds) || !admin_task_files_table_exists($pdo)) {
|
|
return $map;
|
|
}
|
|
|
|
$taskIds = array_values(array_unique(array_map('intval', $taskIds)));
|
|
$taskIds = array_values(array_filter($taskIds, static fn($id) => $id > 0));
|
|
if (empty($taskIds)) {
|
|
return $map;
|
|
}
|
|
|
|
$placeholders = [];
|
|
$params = [];
|
|
foreach ($taskIds as $i => $id) {
|
|
$k = ':id' . $i;
|
|
$placeholders[] = $k;
|
|
$params[$k] = $id;
|
|
}
|
|
|
|
$sql = 'SELECT id, task_id, file_name, file_mime, file_size FROM admin_task_files '
|
|
. 'WHERE task_id IN (' . implode(', ', $placeholders) . ') ORDER BY id ASC';
|
|
$stmt = $pdo->prepare($sql);
|
|
foreach ($params as $k => $v) {
|
|
$stmt->bindValue($k, $v, PDO::PARAM_INT);
|
|
}
|
|
$stmt->execute();
|
|
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($rows as $r) {
|
|
$taskId = (int)($r['task_id'] ?? 0);
|
|
if ($taskId <= 0) {
|
|
continue;
|
|
}
|
|
if (!isset($map[$taskId])) {
|
|
$map[$taskId] = [];
|
|
}
|
|
$fileId = (int)($r['id'] ?? 0);
|
|
$map[$taskId][] = [
|
|
'id' => $fileId,
|
|
'name' => (string)($r['file_name'] ?? ''),
|
|
'mime' => (string)($r['file_mime'] ?? ''),
|
|
'size' => isset($r['file_size']) ? (int)$r['file_size'] : null,
|
|
'download_url' => '/api/admin_task_file.php?file_id=' . $fileId,
|
|
];
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
if ($method === 'GET') {
|
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 50;
|
|
$limit = max(1, min(200, $limit));
|
|
|
|
try {
|
|
$stmt = $pdo->prepare(
|
|
'SELECT id, title, description, created_by, created_by_username, created_at, updated_at, '
|
|
. 'is_done, done_at, done_by, done_by_username, '
|
|
. '(file_name IS NOT NULL AND file_name <> \'\') AS has_file, file_name, file_mime, file_size '
|
|
. 'FROM admin_tasks '
|
|
. 'ORDER BY id DESC '
|
|
. 'LIMIT :limit'
|
|
);
|
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$taskIds = [];
|
|
foreach ($rows as $r) {
|
|
$taskIds[] = (int)($r['id'] ?? 0);
|
|
}
|
|
$attachmentsMap = admin_task_get_attachments_by_task($pdo, $taskIds);
|
|
$commentsCountMap = admin_task_get_comments_count_map($pdo, $taskIds);
|
|
|
|
foreach ($rows as &$r) {
|
|
$taskId = (int)($r['id'] ?? 0);
|
|
$attachments = $attachmentsMap[$taskId] ?? [];
|
|
|
|
if (!empty($r['file_name'])) {
|
|
$attachments[] = [
|
|
'id' => null,
|
|
'name' => (string)$r['file_name'],
|
|
'mime' => (string)($r['file_mime'] ?? ''),
|
|
'size' => isset($r['file_size']) ? (int)$r['file_size'] : null,
|
|
'download_url' => '/api/admin_task_file.php?id=' . $taskId,
|
|
'legacy' => true,
|
|
];
|
|
}
|
|
|
|
$r['attachments'] = $attachments;
|
|
$r['attachments_count'] = count($attachments);
|
|
$r['has_file'] = $r['attachments_count'] > 0;
|
|
$r['comments_count'] = (int)($commentsCountMap[$taskId] ?? 0);
|
|
|
|
if ($r['has_file'] && !empty($attachments[0])) {
|
|
$r['file_name'] = (string)($attachments[0]['name'] ?? $r['file_name']);
|
|
$r['file_mime'] = (string)($attachments[0]['mime'] ?? $r['file_mime']);
|
|
$r['file_size'] = isset($attachments[0]['size']) ? (int)$attachments[0]['size'] : $r['file_size'];
|
|
}
|
|
|
|
$r['is_done'] = (bool)((int)($r['is_done'] ?? 0));
|
|
$r['done_by'] = isset($r['done_by']) ? (int)$r['done_by'] : null;
|
|
}
|
|
unset($r);
|
|
|
|
admin_json_response([
|
|
'success' => true,
|
|
'data' => $rows,
|
|
'count' => count($rows),
|
|
]);
|
|
} catch (Throwable $e) {
|
|
$msg = (string)$e->getMessage();
|
|
$sqlState = ($e instanceof PDOException) ? (string)($e->getCode() ?? '') : '';
|
|
$isSchemaProblem = false;
|
|
|
|
// Common MySQL/PDO signals:
|
|
// - SQLSTATE[42S22]: Column not found (new columns not installed)
|
|
// - SQLSTATE[42S02]: Base table or view not found
|
|
if (stripos($msg, 'Unknown column') !== false) $isSchemaProblem = true;
|
|
if (stripos($msg, 'Base table or view not found') !== false) $isSchemaProblem = true;
|
|
if ($sqlState === '42S22' || $sqlState === '42S02') $isSchemaProblem = true;
|
|
|
|
if ($isSchemaProblem) {
|
|
admin_json_error('Baza danych nie ma jeszcze aktualnych kolumn/tabel dla notatek. Uruchom /administration/install_notes_chat.php i odśwież Dashboard.', 500);
|
|
}
|
|
|
|
admin_json_error('Błąd pobierania notatek', 500);
|
|
}
|
|
}
|
|
|
|
if ($method === 'POST') {
|
|
$contentType = (string)($_SERVER['CONTENT_TYPE'] ?? '');
|
|
|
|
// 1) JSON actions: update/delete/toggle_done
|
|
if (stripos($contentType, 'application/json') !== false) {
|
|
$body = admin_read_json_body();
|
|
$action = isset($body['action']) ? (string)$body['action'] : '';
|
|
$id = isset($body['id']) ? (int)$body['id'] : 0;
|
|
$taskId = isset($body['task_id']) ? (int)$body['task_id'] : 0;
|
|
$commentId = isset($body['comment_id']) ? (int)$body['comment_id'] : 0;
|
|
|
|
if ($action === '') {
|
|
admin_json_error('Nieprawidłowe żądanie (action)', 422);
|
|
}
|
|
|
|
if (in_array($action, ['delete', 'toggle_done', 'update'], true) && $id <= 0) {
|
|
admin_json_error('Nieprawidłowe żądanie (id)', 422);
|
|
}
|
|
if (in_array($action, ['list_comments', 'add_comment'], true) && $taskId <= 0) {
|
|
admin_json_error('Nieprawidłowe żądanie (task_id)', 422);
|
|
}
|
|
if ($action === 'delete_comment' && $commentId <= 0) {
|
|
admin_json_error('Nieprawidłowe żądanie (comment_id)', 422);
|
|
}
|
|
|
|
try {
|
|
if ($action === 'delete') {
|
|
$stmt = $pdo->prepare('DELETE FROM admin_tasks WHERE id = :id');
|
|
$stmt->execute([':id' => $id]);
|
|
admin_json_response(['success' => true]);
|
|
}
|
|
|
|
if ($action === 'toggle_done') {
|
|
$stmt = $pdo->prepare('SELECT is_done FROM admin_tasks WHERE id = :id');
|
|
$stmt->execute([':id' => $id]);
|
|
$cur = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$cur) {
|
|
admin_json_error('Nie znaleziono notatki', 404);
|
|
}
|
|
|
|
$currentDone = (bool)((int)($cur['is_done'] ?? 0));
|
|
$desired = null;
|
|
if (array_key_exists('is_done', $body)) {
|
|
$desired = (bool)$body['is_done'];
|
|
}
|
|
$newDone = $desired !== null ? $desired : !$currentDone;
|
|
|
|
if ($newDone) {
|
|
$up = $pdo->prepare(
|
|
'UPDATE admin_tasks '
|
|
. 'SET is_done = 1, done_at = CURRENT_TIMESTAMP, done_by = :uid, done_by_username = :u '
|
|
. 'WHERE id = :id'
|
|
);
|
|
$up->execute([
|
|
':uid' => (int)$auth['user_id'],
|
|
':u' => (string)$auth['username'],
|
|
':id' => $id,
|
|
]);
|
|
} else {
|
|
$up = $pdo->prepare(
|
|
'UPDATE admin_tasks '
|
|
. 'SET is_done = 0, done_at = NULL, done_by = NULL, done_by_username = NULL '
|
|
. 'WHERE id = :id'
|
|
);
|
|
$up->execute([':id' => $id]);
|
|
}
|
|
|
|
admin_json_response(['success' => true, 'is_done' => (bool)$newDone]);
|
|
}
|
|
|
|
if ($action === 'update') {
|
|
$stmt = $pdo->prepare('SELECT title, description FROM admin_tasks WHERE id = :id');
|
|
$stmt->execute([':id' => $id]);
|
|
$cur = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$cur) {
|
|
admin_json_error('Nie znaleziono notatki', 404);
|
|
}
|
|
|
|
$title = array_key_exists('title', $body) ? trim((string)$body['title']) : (string)($cur['title'] ?? '');
|
|
$description = array_key_exists('description', $body) ? trim((string)$body['description']) : (string)($cur['description'] ?? '');
|
|
|
|
if ($title === '') {
|
|
admin_json_error('Tytuł jest wymagany', 422);
|
|
}
|
|
if (mb_strlen($title) > $ADMIN_TASK_TITLE_MAX) {
|
|
admin_json_error('Tytuł jest zbyt długi (max ' . $ADMIN_TASK_TITLE_MAX . ' znaków)', 422);
|
|
}
|
|
|
|
$up = $pdo->prepare('UPDATE admin_tasks SET title = :title, description = :description WHERE id = :id');
|
|
$up->bindValue(':title', $title, PDO::PARAM_STR);
|
|
$up->bindValue(':description', $description !== '' ? $description : null, $description !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
$up->bindValue(':id', $id, PDO::PARAM_INT);
|
|
$up->execute();
|
|
admin_json_response(['success' => true]);
|
|
}
|
|
|
|
if ($action === 'list_comments') {
|
|
admin_task_assert_exists($pdo, $taskId);
|
|
$comments = admin_task_list_comments($pdo, $taskId);
|
|
admin_json_response(['success' => true, 'data' => $comments]);
|
|
}
|
|
|
|
if ($action === 'add_comment') {
|
|
admin_task_assert_exists($pdo, $taskId);
|
|
$comment = trim((string)($body['comment'] ?? ''));
|
|
if ($comment === '') {
|
|
admin_json_error('Treść komentarza jest wymagana', 422);
|
|
}
|
|
if (mb_strlen($comment) > $ADMIN_TASK_COMMENT_MAX) {
|
|
admin_json_error('Komentarz jest zbyt długi (max ' . $ADMIN_TASK_COMMENT_MAX . ' znaków)', 422);
|
|
}
|
|
|
|
if (!admin_task_comments_table_exists($pdo)) {
|
|
admin_json_error('Brak tabeli komentarzy tasków', 500);
|
|
}
|
|
|
|
$ins = $pdo->prepare(
|
|
'INSERT INTO admin_task_comments (task_id, user_id, username, comment) '
|
|
. 'VALUES (:task_id, :user_id, :username, :comment)'
|
|
);
|
|
$ins->execute([
|
|
':task_id' => $taskId,
|
|
':user_id' => (int)$auth['user_id'],
|
|
':username' => (string)$auth['username'],
|
|
':comment' => $comment,
|
|
]);
|
|
|
|
admin_json_response(['success' => true, 'id' => (int)$pdo->lastInsertId()], 201);
|
|
}
|
|
|
|
if ($action === 'delete_comment') {
|
|
if (!admin_task_comments_table_exists($pdo)) {
|
|
admin_json_error('Brak tabeli komentarzy tasków', 500);
|
|
}
|
|
|
|
$del = $pdo->prepare('DELETE FROM admin_task_comments WHERE id = :id');
|
|
$del->execute([':id' => $commentId]);
|
|
admin_json_response(['success' => true]);
|
|
}
|
|
|
|
admin_json_error('Nieznana akcja', 422);
|
|
} catch (Throwable $e) {
|
|
$msg = (string)$e->getMessage();
|
|
$sqlState = ($e instanceof PDOException) ? (string)($e->getCode() ?? '') : '';
|
|
$isSchemaProblem = false;
|
|
if (stripos($msg, 'Unknown column') !== false) $isSchemaProblem = true;
|
|
if (stripos($msg, 'Base table or view not found') !== false) $isSchemaProblem = true;
|
|
if ($sqlState === '42S22' || $sqlState === '42S02') $isSchemaProblem = true;
|
|
if ($isSchemaProblem) {
|
|
admin_json_error('Baza danych nie ma jeszcze aktualnych kolumn/tabel dla notatek. Uruchom /administration/install_notes_chat.php i spróbuj ponownie.', 500);
|
|
}
|
|
admin_json_error('Błąd operacji notatek', 500);
|
|
}
|
|
}
|
|
|
|
// 2) multipart/form-data: create OR update (with optional file)
|
|
$action = isset($_POST['action']) ? (string)$_POST['action'] : '';
|
|
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
|
|
|
$title = isset($_POST['title']) ? trim((string)$_POST['title']) : '';
|
|
$description = isset($_POST['description']) ? trim((string)$_POST['description']) : '';
|
|
$clearFile = isset($_POST['clear_file']) ? (bool)((int)$_POST['clear_file']) : false;
|
|
$deleteFileIds = admin_task_parse_delete_file_ids($_POST['delete_file_ids'] ?? null);
|
|
|
|
$newUploads = admin_task_collect_uploads($ADMIN_TASK_ATTACHMENT_MAX_BYTES);
|
|
$hasNewUpload = !empty($newUploads);
|
|
$hasFilesTable = admin_task_files_table_exists($pdo);
|
|
|
|
if (count($newUploads) > $ADMIN_TASK_ATTACHMENTS_MAX) {
|
|
admin_json_error('Maksymalnie ' . $ADMIN_TASK_ATTACHMENTS_MAX . ' załączników na raz', 422);
|
|
}
|
|
|
|
if (!$hasFilesTable && ($hasNewUpload || !empty($deleteFileIds))) {
|
|
admin_json_error('Obsługa załączników tasków wymaga aktualizacji bazy. Uruchom /administration/install_notes_chat.php i spróbuj ponownie.', 500);
|
|
}
|
|
|
|
try {
|
|
if ($action === 'update') {
|
|
if ($id <= 0) {
|
|
admin_json_error('Brak id do edycji', 422);
|
|
}
|
|
|
|
$stmt = $pdo->prepare('SELECT title, description, file_name FROM admin_tasks WHERE id = :id');
|
|
$stmt->execute([':id' => $id]);
|
|
$cur = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if (!$cur) {
|
|
admin_json_error('Nie znaleziono notatki', 404);
|
|
}
|
|
|
|
$newTitle = $title !== '' ? $title : (string)($cur['title'] ?? '');
|
|
$newDesc = array_key_exists('description', $_POST) ? $description : (string)($cur['description'] ?? '');
|
|
|
|
if ($newTitle === '') {
|
|
admin_json_error('Tytuł jest wymagany', 422);
|
|
}
|
|
if (mb_strlen($newTitle) > $ADMIN_TASK_TITLE_MAX) {
|
|
admin_json_error('Tytuł jest zbyt długi (max ' . $ADMIN_TASK_TITLE_MAX . ' znaków)', 422);
|
|
}
|
|
|
|
if ($hasFilesTable) {
|
|
$counts = admin_task_count_current_attachments($pdo, $id);
|
|
|
|
$deleteModernCount = 0;
|
|
if (!empty($deleteFileIds)) {
|
|
$ph = [];
|
|
$bind = [':task_id' => $id];
|
|
foreach ($deleteFileIds as $idx => $fid) {
|
|
$k = ':fid' . $idx;
|
|
$ph[] = $k;
|
|
$bind[$k] = (int)$fid;
|
|
}
|
|
$cntSql = 'SELECT COUNT(*) FROM admin_task_files WHERE task_id = :task_id AND id IN (' . implode(', ', $ph) . ')';
|
|
$cntStmt = $pdo->prepare($cntSql);
|
|
foreach ($bind as $k => $v) {
|
|
$cntStmt->bindValue($k, $v, PDO::PARAM_INT);
|
|
}
|
|
$cntStmt->execute();
|
|
$deleteModernCount = (int)$cntStmt->fetchColumn();
|
|
}
|
|
|
|
$legacyAfter = $clearFile ? 0 : ($counts['legacy'] > 0 ? 1 : 0);
|
|
$modernAfter = max(0, $counts['modern'] - $deleteModernCount) + count($newUploads);
|
|
$totalAfter = $legacyAfter + $modernAfter;
|
|
|
|
if ($totalAfter > $ADMIN_TASK_ATTACHMENTS_MAX) {
|
|
admin_json_error('Task może mieć maksymalnie ' . $ADMIN_TASK_ATTACHMENTS_MAX . ' załączników', 422);
|
|
}
|
|
}
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$fields = ['title = :title', 'description = :description'];
|
|
$params = [
|
|
':title' => $newTitle,
|
|
':description' => $newDesc !== '' ? $newDesc : null,
|
|
':id' => $id,
|
|
];
|
|
|
|
if ($clearFile) {
|
|
// Usuń ewentualny legacy plik z dysku
|
|
try {
|
|
$cfRow = $pdo->query('SELECT file_path FROM admin_tasks WHERE id = ' . (int)$id . ' LIMIT 1')->fetch(PDO::FETCH_ASSOC);
|
|
if (!empty($cfRow['file_path'])) {
|
|
get_file_api_client()->deleteFile(dirname((string)$cfRow['file_path']), basename((string)$cfRow['file_path']));
|
|
}
|
|
} catch (Throwable $ignored) { }
|
|
|
|
$fields[] = 'file_name = NULL';
|
|
$fields[] = 'file_mime = NULL';
|
|
$fields[] = 'file_size = NULL';
|
|
$fields[] = 'file_path = NULL';
|
|
} elseif (!$hasFilesTable && $hasNewUpload) {
|
|
$legacyFile = $newUploads[0];
|
|
$fields[] = 'file_name = :file_name';
|
|
$fields[] = 'file_mime = :file_mime';
|
|
$fields[] = 'file_size = :file_size';
|
|
$fields[] = 'file_path = :file_path';
|
|
$params[':file_name'] = $legacyFile['file_name'];
|
|
$params[':file_mime'] = $legacyFile['file_mime'];
|
|
$params[':file_size'] = $legacyFile['file_size'];
|
|
$params[':file_path'] = $legacyFile['file_path'];
|
|
}
|
|
|
|
$sql = 'UPDATE admin_tasks SET ' . implode(', ', $fields) . ' WHERE id = :id';
|
|
$up = $pdo->prepare($sql);
|
|
$up->bindValue(':title', (string)$params[':title'], PDO::PARAM_STR);
|
|
$up->bindValue(':description', $params[':description'], $params[':description'] !== null ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
$up->bindValue(':id', (int)$params[':id'], PDO::PARAM_INT);
|
|
if (array_key_exists(':file_name', $params)) {
|
|
$up->bindValue(':file_name', (string)$params[':file_name'], PDO::PARAM_STR);
|
|
$up->bindValue(':file_mime', (string)$params[':file_mime'], PDO::PARAM_STR);
|
|
$up->bindValue(':file_size', $params[':file_size'] !== null ? (int)$params[':file_size'] : null, $params[':file_size'] !== null ? PDO::PARAM_INT : PDO::PARAM_NULL);
|
|
$up->bindValue(':file_path', (string)$params[':file_path'], PDO::PARAM_STR);
|
|
}
|
|
$up->execute();
|
|
|
|
if ($hasFilesTable) {
|
|
if (!empty($deleteFileIds)) {
|
|
$ph = [];
|
|
$bind = [':task_id' => $id];
|
|
foreach ($deleteFileIds as $idx => $fid) {
|
|
$k = ':fid' . $idx;
|
|
$ph[] = $k;
|
|
$bind[$k] = (int)$fid;
|
|
}
|
|
|
|
// Pobierz ścieżki do usunięcia z dysku przed DELETE
|
|
$pathSql = 'SELECT file_path FROM admin_task_files WHERE task_id = :task_id AND id IN (' . implode(', ', $ph) . ') AND file_path IS NOT NULL';
|
|
$pathStmt = $pdo->prepare($pathSql);
|
|
foreach ($bind as $k => $v) {
|
|
$pathStmt->bindValue($k, $v, PDO::PARAM_INT);
|
|
}
|
|
$pathStmt->execute();
|
|
$pathsToDelete = $pathStmt->fetchAll(PDO::FETCH_COLUMN);
|
|
|
|
$delSome = $pdo->prepare('DELETE FROM admin_task_files WHERE task_id = :task_id AND id IN (' . implode(', ', $ph) . ')');
|
|
foreach ($bind as $k => $v) {
|
|
$delSome->bindValue($k, $v, PDO::PARAM_INT);
|
|
}
|
|
$delSome->execute();
|
|
|
|
// Usuń pliki z dysku po pomyślnym DELETE z bazy
|
|
foreach ($pathsToDelete as $fp) {
|
|
try {
|
|
get_file_api_client()->deleteFile(dirname((string)$fp), basename((string)$fp));
|
|
} catch (Throwable $ignored) { }
|
|
}
|
|
}
|
|
|
|
if ($hasNewUpload) {
|
|
admin_task_insert_files($pdo, $id, $newUploads);
|
|
}
|
|
}
|
|
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
$pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
|
|
admin_json_response(['success' => true]);
|
|
}
|
|
|
|
// create
|
|
if ($title === '') {
|
|
admin_json_error('Tytuł jest wymagany', 422);
|
|
}
|
|
|
|
if (mb_strlen($title) > $ADMIN_TASK_TITLE_MAX) {
|
|
admin_json_error('Tytuł jest zbyt długi (max ' . $ADMIN_TASK_TITLE_MAX . ' znaków)', 422);
|
|
}
|
|
|
|
if ($hasFilesTable && count($newUploads) > $ADMIN_TASK_ATTACHMENTS_MAX) {
|
|
admin_json_error('Task może mieć maksymalnie ' . $ADMIN_TASK_ATTACHMENTS_MAX . ' załączników', 422);
|
|
}
|
|
|
|
$pdo->beginTransaction();
|
|
try {
|
|
if ($hasFilesTable) {
|
|
$stmt = $pdo->prepare(
|
|
'INSERT INTO admin_tasks (title, description, created_by, created_by_username) '
|
|
. 'VALUES (:title, :description, :created_by, :created_by_username)'
|
|
);
|
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
|
$stmt->bindValue(':description', $description !== '' ? $description : null, $description !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
$stmt->bindValue(':created_by', (int)$auth['user_id'], PDO::PARAM_INT);
|
|
$stmt->bindValue(':created_by_username', (string)$auth['username'], PDO::PARAM_STR);
|
|
$stmt->execute();
|
|
|
|
$newId = (int)$pdo->lastInsertId();
|
|
if ($hasNewUpload) {
|
|
admin_task_insert_files($pdo, $newId, $newUploads);
|
|
}
|
|
} else {
|
|
$legacyFile = $hasNewUpload ? $newUploads[0] : null;
|
|
|
|
$stmt = $pdo->prepare(
|
|
'INSERT INTO admin_tasks (title, description, file_name, file_mime, file_size, file_path, created_by, created_by_username) '
|
|
. 'VALUES (:title, :description, :file_name, :file_mime, :file_size, :file_path, :created_by, :created_by_username)'
|
|
);
|
|
|
|
$stmt->bindValue(':title', $title, PDO::PARAM_STR);
|
|
$stmt->bindValue(':description', $description !== '' ? $description : null, $description !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
$stmt->bindValue(':file_name', $legacyFile['file_name'] ?? null, isset($legacyFile['file_name']) ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
$stmt->bindValue(':file_mime', $legacyFile['file_mime'] ?? null, isset($legacyFile['file_mime']) ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
$stmt->bindValue(':file_size', $legacyFile['file_size'] ?? null, isset($legacyFile['file_size']) ? PDO::PARAM_INT : PDO::PARAM_NULL);
|
|
$stmt->bindValue(':file_path', $legacyFile !== null ? (string)($legacyFile['file_path'] ?? '') : null, $legacyFile !== null ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
|
|
$stmt->bindValue(':created_by', (int)$auth['user_id'], PDO::PARAM_INT);
|
|
$stmt->bindValue(':created_by_username', (string)$auth['username'], PDO::PARAM_STR);
|
|
$stmt->execute();
|
|
|
|
$newId = (int)$pdo->lastInsertId();
|
|
}
|
|
|
|
$pdo->commit();
|
|
} catch (Throwable $e) {
|
|
$pdo->rollBack();
|
|
throw $e;
|
|
}
|
|
|
|
admin_json_response(['success' => true, 'id' => $newId], 201);
|
|
} catch (Throwable $e) {
|
|
$msg = (string)$e->getMessage();
|
|
$sqlState = ($e instanceof PDOException) ? (string)($e->getCode() ?? '') : '';
|
|
$isSchemaProblem = false;
|
|
if (stripos($msg, 'Unknown column') !== false) $isSchemaProblem = true;
|
|
if (stripos($msg, 'Base table or view not found') !== false) $isSchemaProblem = true;
|
|
if ($sqlState === '42S22' || $sqlState === '42S02') $isSchemaProblem = true;
|
|
if ($isSchemaProblem) {
|
|
admin_json_error('Baza danych nie ma jeszcze aktualnych kolumn/tabel dla notatek. Uruchom /administration/install_notes_chat.php i spróbuj ponownie.', 500);
|
|
}
|
|
admin_json_error('Błąd zapisu notatki', 500);
|
|
}
|
|
}
|
|
|
|
admin_json_error('Metoda niedozwolona', 405);
|