# Analiza: Odpowiedzialni za kontrolę wykonania zadania

**Data:** 2026-04-09  
**Projekty:** `api`, `admin`  
**Status:** Faza analizy — gotowe do implementacji po akceptacji

---

## 1. Kontekst i cel

Funkcjonalność pozwala przypisać do **definicji zadania** (`todo_task`) jeden lub wiele podmiotów odpowiedzialnych za weryfikację jego wykonania.

Dostępne typy kontrolerów:
| Typ | Wartość ENUM | FK |
|---|---|---|
| Samokontrola (wykonujący sam sobie potwierdza) | `self` | brak |
| Konsultant franczyzowy | `franchise_consultant` | brak |
| Pracownik sklepu z wybranym stanowiskiem | `shop_employee_role` | `employee_role.id` |

---

## 2. Stan obecny — tabele bazy danych

### 2.1. `todo_task` — definicja zadania (tabela docelowa)

```sql
CREATE TABLE `todo_task` (
  `id` int(11) NOT NULL,
  `subcategory_id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` text NOT NULL,
  `day_of_week` varchar(255) DEFAULT NULL,
  `execution_time` time DEFAULT NULL,
  `frequency` varchar(255) DEFAULT NULL,
  `is_mandatory` int(11) NOT NULL,
  `priority` int(11) NOT NULL DEFAULT 5,
  `requires_photo` tinyint(4) NOT NULL DEFAULT 0
)
```

**Uwaga:** Tabela nie ma kolumny `id_brand` — jest globalna (ponad-markowa).

### 2.2. `todo_task_completion` — instancja wykonania zadania

```sql
CREATE TABLE `todo_task_completion` (
  `id` int(11) NOT NULL,
  `task_id` int(11) NOT NULL,
  `shop_id` int(11) NOT NULL,
  `scheduled_for_date` date NOT NULL,
  `scheduled_time` time NOT NULL DEFAULT '00:00:00',
  `status` enum('DONE','SKIPPED','FAILED') NOT NULL DEFAULT 'DONE',
  `completed_at` datetime NOT NULL,
  `completed_by_employee_id` int(11) DEFAULT NULL,
  `note` text DEFAULT NULL,
  `overridden_completed_at` datetime DEFAULT NULL,
  `overridden_by_employee_id` int(11) DEFAULT NULL,
  `override_reason` varchar(255) DEFAULT NULL,
  `created_at` datetime NOT NULL DEFAULT current_timestamp()
)
```

### 2.3. `employee_role` — stanowiska pracowników (tabela globalna)

```sql
CREATE TABLE `employee_role` (
  `id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL
)
```

**Kluczowa decyzja:** To jest właściwa tabela stanowisk do użycia dla nowej funkcji.  
Uzasadnienie: Training module (identyczny wzorzec) używa `employee_role` (globalna, bez `id_brand`),  
co jest spójne z faktem, że `todo_task` też jest globalna.

### 2.4. `franchisee_employee_role` — stanowiska specyficzne dla marki

```sql
CREATE TABLE `franchisee_employee_role` (
  `id_brand` int(11) NOT NULL,
  `id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `description` text NOT NULL
)
```

**Nie używamy tej tabeli** — jest per-brand, a zadania są globalne.

---

## 3. Wzorzec istniejący — moduł Training

Moduł Training jest bezpośrednim wzorcem dla naszej funkcjonalności.

### Tabele:
- `training_admin_role_connection` (id, training_id, admin_role_id → admin_panel_user_role)
- `training_employee_role_connection` (id, training_id, employee_role_id → employee_role)

### Pliki API:
- `api/app/Repositories/OperationalFramework/Training/TrainingAdminRoleConnection.php`
- `api/app/Repositories/OperationalFramework/Training/TrainingEmployeeRoleConnection.php`
- `api/app/Services/OperationalFramework/Training/TrainingService.php`
- `api/app/Controllers/OperationalFramework/Training/TrainingController.php`

### Pliki Admin:
- `admin/controllers/OperationalFramework/Training/TrainingController.php`
- `admin/services/OperationalFramework/Training/TrainingService.php`
- `admin/views/operational_framework/training/modals/trainers.twig` ← **wzorzec UI**
- `admin/views/operational_framework/training/overview.twig` ← **wzorzec JavaScript**

---

## 4. Decyzja architektoniczna: poziom konfiguracji

| Poziom | Opis | Decyzja |
|---|---|---|
| Definicja zadania (`todo_task`) | Globalna reguła "kto zawsze sprawdza ten typ zadania" | ✅ **WYBRANO** |
| Instancja (`todo_task_completion`) | Per-wykonanie, per-sklep | ❌ Zbyt szczegółowe |
| Wykonanie per-sklep | Konfiguracja dla konkretnego sklepu | ❌ Poza zakresem |

**Uzasadnienie:** Pytanie "kto odpowiada za kontrolę?" jest atrybutem samego zadania (jak `is_mandatory`), a nie konkretnego wykonania. Analogicznie Training przypisuje prowadzących do szkolenia, nie do konkretnej sesji.

---

## 5. Model danych — DDL

### Nowa tabela `todo_task_controller`

```sql
CREATE TABLE `todo_task_controller` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `task_id` int(11) NOT NULL,
  `controller_type` enum('self','franchise_consultant','shop_employee_role') NOT NULL,
  `employee_role_id` int(11) DEFAULT NULL COMMENT 'FK do employee_role.id; NULL dla typów self i franchise_consultant',
  PRIMARY KEY (`id`),
  KEY `idx_task_id` (`task_id`),
  CONSTRAINT `fk_ttc_task`
    FOREIGN KEY (`task_id`) REFERENCES `todo_task` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_ttc_role`
    FOREIGN KEY (`employee_role_id`) REFERENCES `employee_role` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
```

### Uwagi do modelu:

1. **Brak UNIQUE na poziomie DDL** — W MySQL/MariaDB wartości NULL w kluczu unikalnym są traktowane jako różne,  
   co oznacza, że `UNIQUE(task_id, controller_type, employee_role_id)` nie chroni przed duplikatami  
   dla typów `self` i `franchise_consultant` (obie mają `employee_role_id = NULL`).  
   **Rozwiązanie:** Walidacja duplikatów w warstwie serwisowej (`TaskControllerService`).

2. **`employee_role_id`** jest wymagane tylko dla typu `shop_employee_role`.  
   Serwis powinien walidować: `shop_employee_role` → `employee_role_id` NOT NULL; pozostałe → NULL.

---

## 6. Plan implementacji — `api`

### Krok 1: Repozytorium

**Plik:** `api/app/Repositories/OperationalFramework/OperationalActivities/TaskControllerRepository.php`

```php
namespace App\Api\Repositories\OperationalFramework\OperationalActivities;
use App\Api\Repositories\BaseRepository;

class TaskControllerRepository extends BaseRepository
{
    protected string $table = 'todo_task_controller';
    protected $allowedColumns = ['task_id', 'controller_type', 'employee_role_id'];
    protected $fetchAllowedColumns = ['id', 'task_id', 'controller_type', 'employee_role_id'];

    public function getByTaskId(int $taskId): array
    {
        $sql = "SELECT
                    ttc.id,
                    ttc.task_id,
                    ttc.controller_type,
                    ttc.employee_role_id,
                    er.name as role_name
                FROM todo_task_controller ttc
                LEFT JOIN employee_role er ON er.id = ttc.employee_role_id
                WHERE ttc.task_id = :task_id";
        return $this->query($sql, ['task_id' => $taskId]);
    }

    public function existsForType(int $taskId, string $type): bool
    {
        $sql = "SELECT id FROM todo_task_controller
                WHERE task_id = :task_id AND controller_type = :type AND employee_role_id IS NULL
                LIMIT 1";
        $result = $this->query($sql, ['task_id' => $taskId, 'type' => $type]);
        return !empty($result);
    }

    public function existsForRole(int $taskId, int $roleId): bool
    {
        $sql = "SELECT id FROM todo_task_controller
                WHERE task_id = :task_id AND employee_role_id = :role_id
                LIMIT 1";
        $result = $this->query($sql, ['task_id' => $taskId, 'role_id' => $roleId]);
        return !empty($result);
    }

    public function deleteById(int $id): int
    {
        $sql = "DELETE FROM todo_task_controller WHERE id = :id";
        return $this->query($sql, ['id' => $id]);
    }
}
```

### Krok 2: Serwis

**Plik:** `api/app/Services/OperationalFramework/OperationalActivities/TaskControllerService.php`

```php
namespace App\Api\Services\OperationalFramework\OperationalActivities;
use App\Api\Exceptions\CustomException;
use App\Api\Repositories\OperationalFramework\OperationalActivities\TaskControllerRepository;

class TaskControllerService
{
    private const ALLOWED_TYPES = ['self', 'franchise_consultant', 'shop_employee_role'];

    public function __construct(
        private TaskControllerRepository $repository
    ) {}

    public function getByTaskId(int $taskId): array
    {
        return $this->repository->getByTaskId($taskId);
    }

    public function assign(int $taskId, array $data): int|bool
    {
        $type = $data['controller_type'] ?? null;
        if (!in_array($type, self::ALLOWED_TYPES, true)) {
            throw new CustomException('Invalid controller_type', 'task_controller', 400);
        }

        $roleId = !empty($data['employee_role_id']) ? (int)$data['employee_role_id'] : null;

        if ($type === 'shop_employee_role' && !$roleId) {
            throw new CustomException('employee_role_id is required for shop_employee_role type', 'task_controller', 400);
        }
        if ($type !== 'shop_employee_role' && $roleId) {
            throw new CustomException('employee_role_id must be empty for this type', 'task_controller', 400);
        }

        // Sprawdź duplikaty
        if ($type !== 'shop_employee_role' && $this->repository->existsForType($taskId, $type)) {
            throw new CustomException('Controller of this type already assigned', 'task_controller', 409);
        }
        if ($type === 'shop_employee_role' && $roleId && $this->repository->existsForRole($taskId, $roleId)) {
            throw new CustomException('This employee role is already assigned as controller', 'task_controller', 409);
        }

        return $this->repository->insert([
            'task_id'          => $taskId,
            'controller_type'  => $type,
            'employee_role_id' => $roleId
        ]);
    }

    public function unassign(int $taskId, int $entryId): int
    {
        return $this->repository->deleteById($entryId);
    }
}
```

### Krok 3: Kontroler API

**Plik:** `api/app/Controllers/OperationalFramework/OperationalActivities/TaskController.php`  
**Zmiana:** Dodać metodę `getControllers`, `assignController`, `unassignController`

```php
// Dodać do konstruktora:
private TaskControllerService $taskControllerService,

public function getControllers(int $id): void
{
    $result = $this->taskControllerService->getByTaskId($id);
    ResponseHandler::send($result);
}

public function assignController(int $id): void
{
    $data = $this->getValueFromPostRequest();
    $result = $this->taskControllerService->assign($id, $data);
    ResponseHandler::sendCreated($result);
}

public function unassignController(int $id, int $entryId): void
{
    $result = $this->taskControllerService->unassign($id, $entryId);
    ResponseHandler::sendUpdated($result);
}
```

### Krok 4: Routing API

**Plik:** `api/v1/Routes/OperationalFramework/OperationalActivities/TaskRoutes.php`  
**Dodać:**

```php
$router->get('/tasks/(\d+)/controllers', function ($id) use ($container) {
    AuthMiddleware::checkAuth();
    $controller = $container->get(TaskController::class);
    $controller->getControllers($id);
});

$router->post('/tasks/(\d+)/controllers', function ($id) use ($container) {
    AuthMiddleware::checkAuth();
    $controller = $container->get(TaskController::class);
    $controller->assignController($id);
});

$router->delete('/tasks/(\d+)/controllers/(\d+)', function ($id, $entryId) use ($container) {
    AuthMiddleware::checkAuth();
    $controller = $container->get(TaskController::class);
    $controller->unassignController($id, $entryId);
});
```

---

## 7. Plan implementacji — `admin`

### Krok 5: Repozytorium Admin

**Plik:** `admin/repositories/OperationalFramework/OperationalActivities/TaskRepository.php`  
**Dodać metody proxy:**

```php
public function getControllers(int $id): array
{
    $response = $this->apiClient->get("/tasks/{$id}/controllers");
    return $response['data'] ?? [];
}

public function assignController(int $id, array $data): array
{
    return $this->apiClient->post("/tasks/{$id}/controllers", $data);
}

public function unassignController(int $id, int $entryId): array
{
    return $this->apiClient->delete("/tasks/{$id}/controllers/{$entryId}");
}
```

### Krok 6: Serwis Admin

**Plik:** `admin/services/OperationalFramework/OperationalActivities/TaskService.php`  
**Dodać:**

```php
public function getControllers(int $id): array
{
    return $this->taskRepository->getControllers($id);
}

public function assignController(int $id, array $data): array
{
    return $this->taskRepository->assignController($id, $data);
}

public function unassignController(int $id, int $entryId): array
{
    return $this->taskRepository->unassignController($id, $entryId);
}
```

### Krok 7: Kontroler Admin

**Plik:** `admin/controllers/OperationalFramework/OperationalActivities/TaskController.php`

**Zmiana konstruktora** — dodać `RoleService`:

```php
use App\services\Employee\RoleService;

public function __construct(
    private TaskService $taskService,
    private SectionService $sectionService,
    private CategoryService $categoryService,
    private RoleService $employeeRoleService,  // ← dodać
) {}
```

**Zmiana `overview()`** — przekazać role do widoku:
```php
$data['employee_roles'] = $this->employeeRoleService->getAll();
```

**Nowe ajax routes:**
```php
#[Route('GET', '/ajax/tasks/{id:\d+}/controllers')]
public function ajaxGetControllers(int $id) { ... }

#[Route('POST', '/ajax/tasks/{id:\d+}/controllers')]
public function ajaxAssignController(int $id) { ... }

#[Route('DELETE', '/ajax/tasks/{id:\d+}/controllers/{entryId:\d+}')]
public function ajaxUnassignController(int $id, int $entryId) { ... }
```

### Krok 8: Modal UI

**Plik:** `admin/views/operational_framework/operational_activities/task/modals/controllers.twig`

Wzorowany na `trainers.twig`, 3 sekcje:

```
┌──────────────────────────────────────────────────────────────┐
│  Odpowiedzialni za kontrolę — [nazwa zadania]                │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Samokontrola                                                │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  [przycisk: Dodaj samokontrole / Usuń samokontrole]   │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                              │
│  Konsultant franczyzowy                                      │
│  ┌───────────────────────────────────────────────────────┐  │
│  │  [przycisk: Dodaj / Usuń]                             │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                              │
│  Stanowisko pracownika sklepu                                │
│  ┌─────────────────────────────────────────────┬────────┐   │
│  │ [select: wybierz stanowisko]                │ Dodaj  │   │
│  └─────────────────────────────────────────────┴────────┘   │
│  ┌─────────────────────────────────────────────┬────────┐   │
│  │ Kierownik zmiany                            │ [🗑️]   │   │
│  │ Pracownik kasy                              │ [🗑️]   │   │
│  └─────────────────────────────────────────────┴────────┘   │
│                                                              │
│                                               [Zamknij]      │
└──────────────────────────────────────────────────────────────┘
```

**Przycisk w tabeli zadań** (overview.twig):
```html
<button class="btn btn-info btn-sm controllers_btn"
        data-id="{{ task.getId() }}"
        data-name="{{ task.getName() }}">
    <i class="bi bi-shield-check"></i>
</button>
```

---

## 8. Pełna lista plików do zmiany/stworzenia

### API (nowe):
- `api/app/Repositories/OperationalFramework/OperationalActivities/TaskControllerRepository.php`
- `api/app/Services/OperationalFramework/OperationalActivities/TaskControllerService.php`

### API (modyfikacje):
- `api/app/Controllers/OperationalFramework/OperationalActivities/TaskController.php`
- `api/v1/Routes/OperationalFramework/OperationalActivities/TaskRoutes.php`

### Admin (nowe):
- `admin/views/operational_framework/operational_activities/task/modals/controllers.twig`

### Admin (modyfikacje):
- `admin/controllers/OperationalFramework/OperationalActivities/TaskController.php`
- `admin/services/OperationalFramework/OperationalActivities/TaskService.php`
- `admin/repositories/OperationalFramework/OperationalActivities/TaskRepository.php`
- `admin/views/operational_framework/operational_activities/task/overview.twig`

**Łącznie:** 2 nowe pliki + 6 modyfikacji

---

## 9. Otwarte pytania / brakujące decyzje

### ~~Pytanie A — Kontener DI (api)~~ ✅ Rozwiązane
`api` używa **PHP-DI z `autowire()`** (`api/core/bootstrap.php`).  
Klasy z `OperationalFramework` (Training, PositionLevelTask) **nie są jawnie rejestrowane** w bootstrap.php — PHP-DI rozwiązuje je automatycznie.  
→ Nowe klasy `TaskControllerService` i `TaskControllerRepository` **nie wymagają dodatkowej rejestracji** — zostaną wstrzyknięte automatycznie.

### Pytanie B — Tłumaczenia (admin)
Nowe klucze tłumaczeń dla modalu kontrolerów (np. `task_controllers`, `self_control`, `franchise_consultant`, `shop_employee_role`).  
Dotyczy plików: `admin/public/resources/lang/page/pl/todo.json` i analogicznych dla `en`, `fr`, `it`, `nl`.  
→ Czy mam wygenerować wersje dla wszystkich dostępnych języków?

### Pytanie C — Walidacja `employee_role_id` w API
Czy serwis powinien weryfikować, że podane `employee_role_id` faktycznie istnieje w `employee_role` (dodatkowy SELECT)?  
→ Zalecane tak — dla spójności z podejściem w innych serwisach.

### Pytanie D — Kontener DI admin
Sprawdzić, czy `RoleService` jest już rejestrowany w kontenerze admin — jeśli tak, wystarczy wstrzyknąć; jeśli nie, trzeba dodać.

---

## 10. Podsumowanie wzorca

```
Training module (wzorzec)      │  Nowa funkcjonalność
──────────────────────────────────────────────────────
training                       │  todo_task
training_admin_role_connection │  todo_task_controller (self + franchise_consultant)
training_employee_role_connection │  todo_task_controller (shop_employee_role)
employee_role                  │  employee_role (ta sama tabela)
TrainingController (api)       │  TaskController (api) — rozszerzenie
TrainingService (api)          │  TaskControllerService (api)
TrainingAdminRoleConnection    │  TaskControllerRepository
trainers.twig (modal)          │  controllers.twig (modal)
verifiers_btn (overview)       │  controllers_btn (overview)
```


