docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,358 @@
<?php
declare(strict_types=1);
namespace App\Application\Admin;
use App\Framework\Attributes\Route;
use App\Framework\Attributes\Auth;
use App\Framework\Core\Method;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\ViewResult;
use App\Framework\Http\JsonResult;
use App\Framework\Http\Redirect;
use App\Domain\PreSave\PreSaveCampaignRepository;
use App\Domain\PreSave\PreSaveRegistrationRepository;
use App\Domain\PreSave\PreSaveCampaign;
use App\Domain\PreSave\ValueObjects\CampaignStatus;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;
/**
* Admin Controller for Pre-Save Campaign Management
*/
#[Auth(strategy: 'ip', allowedIps: ['127.0.0.1', '::1'])]
final readonly class PreSaveCampaignController
{
public function __construct(
private PreSaveCampaignRepository $campaignRepository,
private PreSaveRegistrationRepository $registrationRepository
) {}
/**
* Show all campaigns overview
*/
#[Route(path: '/admin/presave/campaigns', method: Method::GET)]
public function index(HttpRequest $request): ViewResult
{
$campaigns = $this->campaignRepository->findAll();
return new ViewResult('admin/presave/campaigns/index', [
'campaigns' => $campaigns,
'stats' => $this->getGlobalStats()
]);
}
/**
* Show create campaign form
*/
#[Route(path: '/admin/presave/campaigns/create', method: Method::GET)]
public function create(): ViewResult
{
return new ViewResult('admin/presave/campaigns/create');
}
/**
* Store new campaign
*/
#[Route(path: '/admin/presave/campaigns', method: Method::POST)]
public function store(HttpRequest $request): Redirect
{
$data = $request->parsedBody->toArray();
// Validate required fields
$this->validateCampaignData($data);
// Parse track URLs from form
$trackUrls = [];
if (!empty($data['spotify_url'])) {
$trackUrls['spotify'] = $data['spotify_url'];
}
if (!empty($data['apple_music_url'])) {
$trackUrls['apple_music'] = $data['apple_music_url'];
}
if (!empty($data['tidal_url'])) {
$trackUrls['tidal'] = $data['tidal_url'];
}
// Create campaign
$campaign = PreSaveCampaign::create(
title: $data['title'],
artistName: $data['artist_name'],
coverImageUrl: $data['cover_image_url'],
description: $data['description'] ?? null,
releaseDate: strtotime($data['release_date']),
trackUrls: $trackUrls,
startDate: !empty($data['start_date']) ? strtotime($data['start_date']) : null
);
$this->campaignRepository->save($campaign);
return Redirect::to('/admin/presave/campaigns')
->withFlashMessage('success', 'Campaign created successfully!');
}
/**
* Show campaign details with registrations
*/
#[Route(path: '/admin/presave/campaigns/{id}', method: Method::GET)]
public function show(HttpRequest $request): ViewResult
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$registrations = $this->registrationRepository->findByCampaign($campaignId);
$stats = $this->getCampaignStats($campaign, $registrations);
return new ViewResult('admin/presave/campaigns/show', [
'campaign' => $campaign,
'registrations' => $registrations,
'stats' => $stats
]);
}
/**
* Show edit campaign form
*/
#[Route(path: '/admin/presave/campaigns/{id}/edit', method: Method::GET)]
public function edit(HttpRequest $request): ViewResult
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
return new ViewResult('admin/presave/campaigns/edit', [
'campaign' => $campaign
]);
}
/**
* Update campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}', method: Method::PUT)]
public function update(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$data = $request->parsedBody->toArray();
$this->validateCampaignData($data);
// Parse track URLs
$trackUrls = [];
if (!empty($data['spotify_url'])) {
$trackUrls['spotify'] = $data['spotify_url'];
}
if (!empty($data['apple_music_url'])) {
$trackUrls['apple_music'] = $data['apple_music_url'];
}
if (!empty($data['tidal_url'])) {
$trackUrls['tidal'] = $data['tidal_url'];
}
// Update campaign
$updatedCampaign = $campaign->updateDetails(
title: $data['title'],
artistName: $data['artist_name'],
coverImageUrl: $data['cover_image_url'],
description: $data['description'] ?? null,
releaseDate: strtotime($data['release_date']),
trackUrls: $trackUrls
);
$this->campaignRepository->save($updatedCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign updated successfully!');
}
/**
* Delete campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}', method: Method::DELETE)]
public function destroy(HttpRequest $request): JsonResult
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
return new JsonResult([
'success' => false,
'message' => 'Campaign not found'
], 404);
}
$this->campaignRepository->delete($campaignId);
return new JsonResult([
'success' => true,
'message' => 'Campaign deleted successfully'
]);
}
/**
* Activate campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}/activate', method: Method::POST)]
public function activate(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$activeCampaign = $campaign->updateStatus(CampaignStatus::ACTIVE);
$this->campaignRepository->save($activeCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign activated successfully!');
}
/**
* Pause campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}/pause', method: Method::POST)]
public function pause(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$pausedCampaign = $campaign->updateStatus(CampaignStatus::PAUSED);
$this->campaignRepository->save($pausedCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign paused successfully!');
}
/**
* Complete campaign
*/
#[Route(path: '/admin/presave/campaigns/{id}/complete', method: Method::POST)]
public function complete(HttpRequest $request): Redirect
{
$campaignId = $request->routeParams->get('id');
$campaign = $this->campaignRepository->findById($campaignId);
if ($campaign === null) {
throw FrameworkException::create(
ErrorCode::ENTITY_NOT_FOUND,
"Campaign with ID {$campaignId} not found"
);
}
$completedCampaign = $campaign->updateStatus(CampaignStatus::COMPLETED);
$this->campaignRepository->save($completedCampaign);
return Redirect::to("/admin/presave/campaigns/{$campaignId}")
->withFlashMessage('success', 'Campaign marked as completed!');
}
/**
* Validate campaign data
*/
private function validateCampaignData(array $data): void
{
$required = ['title', 'artist_name', 'cover_image_url', 'release_date'];
foreach ($required as $field) {
if (empty($data[$field])) {
throw FrameworkException::create(
ErrorCode::VAL_REQUIRED_FIELD_MISSING,
"Field '{$field}' is required"
)->withData(['field' => $field]);
}
}
// Validate at least one track URL
if (empty($data['spotify_url']) && empty($data['apple_music_url']) && empty($data['tidal_url'])) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'At least one track URL (Spotify, Apple Music, or Tidal) is required'
);
}
}
/**
* Get global campaign statistics
*/
private function getGlobalStats(): array
{
$allCampaigns = $this->campaignRepository->findAll();
$stats = [
'total' => count($allCampaigns),
'draft' => 0,
'active' => 0,
'paused' => 0,
'completed' => 0,
'total_registrations' => 0
];
foreach ($allCampaigns as $campaign) {
$stats[strtolower($campaign->status->value)]++;
$registrations = $this->registrationRepository->findByCampaign($campaign->id);
$stats['total_registrations'] += count($registrations);
}
return $stats;
}
/**
* Get campaign-specific statistics
*/
private function getCampaignStats(PreSaveCampaign $campaign, array $registrations): array
{
$stats = [
'total_registrations' => count($registrations),
'pending' => 0,
'processing' => 0,
'completed' => 0,
'failed' => 0,
'by_platform' => [
'spotify' => 0,
'apple_music' => 0,
'tidal' => 0
]
];
foreach ($registrations as $registration) {
$stats[strtolower($registration->status->value)]++;
$stats['by_platform'][strtolower($registration->platform->value)]++;
}
return $stats;
}
}