288 lines
12 KiB
PHP
288 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Database;
|
|
use PDO;
|
|
|
|
final class AdminRepository
|
|
{
|
|
private PDO $db;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = Database::connection();
|
|
}
|
|
|
|
public function dashboardCounts(): array
|
|
{
|
|
return [
|
|
'posts' => (int) $this->db->query("SELECT COUNT(*) FROM posts WHERE deleted_at IS NULL")->fetchColumn(),
|
|
'published' => (int) $this->db->query("SELECT COUNT(*) FROM posts WHERE status = 'published' AND deleted_at IS NULL")->fetchColumn(),
|
|
'inquiries' => (int) $this->db->query("SELECT COUNT(*) FROM inquiries WHERE deleted_at IS NULL")->fetchColumn(),
|
|
'pending' => (int) $this->db->query("SELECT COUNT(*) FROM inquiries WHERE status IN ('pending','in_progress') AND deleted_at IS NULL")->fetchColumn(),
|
|
];
|
|
}
|
|
|
|
public function adminCount(): int
|
|
{
|
|
return (int) $this->db->query('SELECT COUNT(*) FROM admins')->fetchColumn();
|
|
}
|
|
|
|
public function createAdmin(string $username, string $displayName, string $password): void
|
|
{
|
|
$statement = $this->db->prepare(
|
|
'INSERT INTO admins (username, password_hash, display_name)
|
|
VALUES (:username, :password_hash, :display_name)'
|
|
);
|
|
$statement->execute([
|
|
'username' => $username,
|
|
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
|
'display_name' => $displayName,
|
|
]);
|
|
}
|
|
|
|
public function posts(): array
|
|
{
|
|
return $this->db->query(
|
|
"SELECT id, title, slug, category, status, published_at, updated_at
|
|
FROM posts WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 100"
|
|
)->fetchAll();
|
|
}
|
|
|
|
public function inquiries(): array
|
|
{
|
|
return $this->db->query(
|
|
"SELECT id, ticket, name, phone, inquiry_type, title, status, created_at
|
|
FROM inquiries WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 100"
|
|
)->fetchAll();
|
|
}
|
|
|
|
public function findPost(int $id): ?array
|
|
{
|
|
$statement = $this->db->prepare(
|
|
'SELECT posts.*,
|
|
(SELECT file_path FROM post_images WHERE post_id = posts.id AND caption NOT LIKE \'@gtsit:inline:%\' ORDER BY sort_order, id LIMIT 1) AS image_path,
|
|
(SELECT alt_text FROM post_images WHERE post_id = posts.id AND caption NOT LIKE \'@gtsit:inline:%\' ORDER BY sort_order, id LIMIT 1) AS image_alt
|
|
FROM posts WHERE posts.id = :id AND posts.deleted_at IS NULL LIMIT 1'
|
|
);
|
|
$statement->execute(['id' => $id]);
|
|
$post = $statement->fetch();
|
|
if (!$post) {
|
|
return null;
|
|
}
|
|
|
|
$media = $this->postMedia($id);
|
|
$post['gallery_images'] = $media['gallery'];
|
|
$post['inline_images'] = $media['inline'];
|
|
return $post;
|
|
}
|
|
|
|
public function postMedia(int $postId): array
|
|
{
|
|
$statement = $this->db->prepare(
|
|
'SELECT id, file_path, alt_text, caption, sort_order
|
|
FROM post_images WHERE post_id = :post_id ORDER BY sort_order, id'
|
|
);
|
|
$statement->execute(['post_id' => $postId]);
|
|
$gallery = [];
|
|
$inline = [];
|
|
foreach ($statement->fetchAll() as $image) {
|
|
if (preg_match('/^@gtsit:inline:(\d+)$/', (string) $image['caption'], $matches) === 1) {
|
|
$image['token'] = (int) $matches[1];
|
|
$inline[] = $image;
|
|
continue;
|
|
}
|
|
$gallery[] = $image;
|
|
}
|
|
return ['gallery' => $gallery, 'inline' => $inline];
|
|
}
|
|
|
|
public function savePost(array $data, ?int $id = null): int
|
|
{
|
|
$publishedAt = $data['status'] === 'published'
|
|
? ($data['published_at'] ?: date('Y-m-d H:i:s'))
|
|
: null;
|
|
|
|
if ($id === null) {
|
|
$temporarySlug = 'pending-' . bin2hex(random_bytes(12));
|
|
$statement = $this->db->prepare(
|
|
'INSERT INTO posts (title, slug, category, excerpt, body, author, status, published_at)
|
|
VALUES (:title, :slug, :category, :excerpt, :body, :author, :status, :published_at)'
|
|
);
|
|
$statement->execute([
|
|
'title'=>$data['title'],'slug'=>$temporarySlug,'category'=>$data['category'],
|
|
'excerpt'=>$data['excerpt'],'body'=>$data['body'],'author'=>$data['author'],
|
|
'status'=>$data['status'],'published_at'=>$publishedAt,
|
|
]);
|
|
$postId = (int) $this->db->lastInsertId();
|
|
$updateSlug = $this->db->prepare('UPDATE posts SET slug = :slug WHERE id = :id');
|
|
$updateSlug->execute(['slug' => 'post-' . $postId, 'id' => $postId]);
|
|
return $postId;
|
|
}
|
|
|
|
$statement = $this->db->prepare(
|
|
'UPDATE posts SET title=:title, slug=:slug, category=:category, excerpt=:excerpt,
|
|
body=:body, author=:author, status=:status, published_at=:published_at WHERE id=:id AND deleted_at IS NULL'
|
|
);
|
|
$statement->execute([
|
|
'title'=>$data['title'],'slug'=>'post-' . $id,'category'=>$data['category'],
|
|
'excerpt'=>$data['excerpt'],'body'=>$data['body'],'author'=>$data['author'],
|
|
'status'=>$data['status'],'published_at'=>$publishedAt,'id'=>$id,
|
|
]);
|
|
return $id;
|
|
}
|
|
|
|
public function savePostWithImage(array $data, ?int $id, ?string $imagePath, string $imageAlt): array
|
|
{
|
|
$oldPaths = [];
|
|
$this->db->beginTransaction();
|
|
try {
|
|
$postId = $this->savePost($data, $id);
|
|
if ($imagePath !== null) {
|
|
$select = $this->db->prepare('SELECT file_path FROM post_images WHERE post_id = :post_id FOR UPDATE');
|
|
$select->execute(['post_id' => $postId]);
|
|
$oldPaths = array_column($select->fetchAll(), 'file_path');
|
|
|
|
$delete = $this->db->prepare('DELETE FROM post_images WHERE post_id = :post_id');
|
|
$delete->execute(['post_id' => $postId]);
|
|
$insert = $this->db->prepare(
|
|
'INSERT INTO post_images (post_id, file_path, alt_text, sort_order)
|
|
VALUES (:post_id, :file_path, :alt_text, 0)'
|
|
);
|
|
$insert->execute(['post_id'=>$postId, 'file_path'=>$imagePath, 'alt_text'=>$imageAlt]);
|
|
} elseif ($id !== null) {
|
|
$update = $this->db->prepare(
|
|
'UPDATE post_images SET alt_text = :alt_text WHERE post_id = :post_id AND sort_order = 0'
|
|
);
|
|
$update->execute(['post_id'=>$postId, 'alt_text'=>$imageAlt]);
|
|
}
|
|
$this->db->commit();
|
|
return ['id' => $postId, 'old_paths' => $oldPaths];
|
|
} catch (\Throwable $exception) {
|
|
if ($this->db->inTransaction()) {
|
|
$this->db->rollBack();
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
public function savePostWithMedia(
|
|
array $data,
|
|
?int $id,
|
|
array $galleryImages,
|
|
array $inlineImages,
|
|
array $deleteImageIds,
|
|
array $imageAlts
|
|
): array {
|
|
$oldPaths = [];
|
|
$this->db->beginTransaction();
|
|
try {
|
|
$postId = $this->savePost($data, $id);
|
|
$existing = $this->db->prepare(
|
|
'SELECT id, file_path, caption FROM post_images WHERE post_id = :post_id FOR UPDATE'
|
|
);
|
|
$existing->execute(['post_id' => $postId]);
|
|
$existingImages = $existing->fetchAll();
|
|
$existingIds = array_map('intval', array_column($existingImages, 'id'));
|
|
|
|
foreach ($imageAlts as $imageId => $altText) {
|
|
$imageId = (int) $imageId;
|
|
if (!in_array($imageId, $existingIds, true)) continue;
|
|
$update = $this->db->prepare(
|
|
'UPDATE post_images SET alt_text = :alt_text WHERE id = :id AND post_id = :post_id'
|
|
);
|
|
$update->execute(['alt_text' => $altText, 'id' => $imageId, 'post_id' => $postId]);
|
|
}
|
|
|
|
$deleteIds = array_values(array_intersect($existingIds, array_map('intval', $deleteImageIds)));
|
|
if ($deleteIds !== []) {
|
|
foreach ($existingImages as $existingImage) {
|
|
if (in_array((int) $existingImage['id'], $deleteIds, true)) {
|
|
$oldPaths[] = (string) $existingImage['file_path'];
|
|
}
|
|
}
|
|
$placeholders = implode(',', array_fill(0, count($deleteIds), '?'));
|
|
$delete = $this->db->prepare("DELETE FROM post_images WHERE post_id = ? AND id IN ({$placeholders})");
|
|
$delete->execute([$postId, ...$deleteIds]);
|
|
}
|
|
|
|
$galleryOrder = 0;
|
|
foreach ($existingImages as $existingImage) {
|
|
if (!in_array((int) $existingImage['id'], $deleteIds, true)
|
|
&& !str_starts_with((string) $existingImage['caption'], '@gtsit:inline:')) {
|
|
$galleryOrder++;
|
|
}
|
|
}
|
|
$insert = $this->db->prepare(
|
|
'INSERT INTO post_images (post_id, file_path, alt_text, caption, sort_order)
|
|
VALUES (:post_id, :file_path, :alt_text, :caption, :sort_order)'
|
|
);
|
|
foreach ($galleryImages as $image) {
|
|
$insert->execute([
|
|
'post_id' => $postId,
|
|
'file_path' => $image['path'],
|
|
'alt_text' => $image['alt'],
|
|
'caption' => '@gtsit:gallery',
|
|
'sort_order' => $galleryOrder++,
|
|
]);
|
|
}
|
|
foreach ($inlineImages as $image) {
|
|
$insert->execute([
|
|
'post_id' => $postId,
|
|
'file_path' => $image['path'],
|
|
'alt_text' => $image['alt'],
|
|
'caption' => '@gtsit:inline:' . $image['token'],
|
|
'sort_order' => 1000 + (int) $image['token'],
|
|
]);
|
|
}
|
|
|
|
$this->db->commit();
|
|
return ['id' => $postId, 'old_paths' => $oldPaths];
|
|
} catch (\Throwable $exception) {
|
|
if ($this->db->inTransaction()) {
|
|
$this->db->rollBack();
|
|
}
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
public function deletePost(int $id): void
|
|
{
|
|
$statement = $this->db->prepare('UPDATE posts SET deleted_at = NOW() WHERE id = :id');
|
|
$statement->execute(['id' => $id]);
|
|
}
|
|
|
|
public function findInquiry(int $id): ?array
|
|
{
|
|
$statement = $this->db->prepare('SELECT * FROM inquiries WHERE id = :id AND deleted_at IS NULL LIMIT 1');
|
|
$statement->execute(['id' => $id]);
|
|
$inquiry = $statement->fetch();
|
|
return $inquiry ?: null;
|
|
}
|
|
|
|
public function updateInquiry(int $id, string $status, string $reply): void
|
|
{
|
|
$statement = $this->db->prepare(
|
|
'UPDATE inquiries SET status = :new_status, reply = :reply,
|
|
replied_at = CASE WHEN :has_reply = 1 THEN NOW() ELSE replied_at END,
|
|
closed_at = CASE
|
|
WHEN :closed_status = \'closed\' AND closed_at IS NULL THEN NOW()
|
|
WHEN :open_status <> \'closed\' THEN NULL
|
|
ELSE closed_at
|
|
END
|
|
WHERE id = :id AND deleted_at IS NULL'
|
|
);
|
|
$statement->execute([
|
|
'new_status' => $status,
|
|
'closed_status' => $status,
|
|
'open_status' => $status,
|
|
'reply' => $reply === '' ? null : $reply,
|
|
'has_reply' => $reply === '' ? 0 : 1,
|
|
'id' => $id,
|
|
]);
|
|
}
|
|
}
|