feat(129): order user notes module
CRUD notatek autorskich operatora per zamowienie z badge [N] na liscie
zamowien. Reuse istniejacej tabeli `order_notes` przez nowy
`note_type='user'` z `user_id` (FK->users SET NULL) i `author_name`
(snapshot). Sekcja `#notes` w "Wiadomosci i zalaczniki" w
`/orders/{id}` z inline edit form + delete przez
`OrderProAlerts.confirm`. Autoryzacja DB-level
(`WHERE user_id = :user_id`, rowCount=0 ⇒ 403) — bez admin override
(brak systemu rol w aplikacji).
- Migracja `20260514_000116_*.sql` (ADD COLUMN user_id + author_name +
FK + indeks `idx_order_notes_type_order`); idempotentne z DDL
no-op fallback.
- `OrderNotesService` (CRUD + walidacja body ≤ 2000 znakow); subquery
`user_notes_count` w paginate; badge HTML w `toTableRow()`.
- 3 routy POST /orders/{id}/notes(/update|/delete).
- SCSS module `_order-notes.scss` + vanilla JS `order-notes.js`
(inline edit toggle + delete confirm; idempotent guard).
- 9 kluczy i18n PL; PROJECT.md + ROADMAP.md + tech_changelog.md +
db_schema.md zaktualizowane.
Follow-up: `php bin/migrate.php` + manualny smoke test (autor vs inny
user + badge na /orders/list).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ use App\Modules\Sms\SmsConversationService;
|
||||
use App\Modules\Sms\SmsMessageRepository;
|
||||
use App\Modules\Sms\SmsTemplateRepository;
|
||||
use App\Modules\Sms\SmsVariableResolver;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class OrdersController
|
||||
@@ -52,7 +53,8 @@ final class OrdersController
|
||||
private readonly ?SmsConversationService $smsConversation = null,
|
||||
private readonly ?SmsTemplateRepository $smsTemplates = null,
|
||||
private readonly ?SmsVariableResolver $smsVariableResolver = null,
|
||||
private readonly ?CompanySettingsRepository $companySettingsRepo = null
|
||||
private readonly ?CompanySettingsRepository $companySettingsRepo = null,
|
||||
private readonly ?OrderNotesService $orderNotes = null
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -203,6 +205,9 @@ final class OrdersController
|
||||
$shipments = is_array($details['shipments'] ?? null) ? $details['shipments'] : [];
|
||||
$documents = is_array($details['documents'] ?? null) ? $details['documents'] : [];
|
||||
$notes = is_array($details['notes'] ?? null) ? $details['notes'] : [];
|
||||
$userNotes = $this->orderNotes !== null ? $this->orderNotes->listUserNotes($orderId) : [];
|
||||
$currentUser = $this->auth->user();
|
||||
$currentUserId = is_array($currentUser) ? (int) ($currentUser['id'] ?? 0) : 0;
|
||||
$history = is_array($details['status_history'] ?? null) ? $details['status_history'] : [];
|
||||
$activityLog = is_array($details['activity_log'] ?? null) ? $details['activity_log'] : [];
|
||||
$statusCode = (string) (($order['effective_status_id'] ?? '') !== '' ? $order['effective_status_id'] : ($order['status_code'] ?? ''));
|
||||
@@ -279,6 +284,8 @@ final class OrdersController
|
||||
'pendingPrintPackageIds' => $this->printJobRepo !== null ? $this->printJobRepo->pendingPackageIds() : [],
|
||||
'documents' => $documents,
|
||||
'notes' => $notes,
|
||||
'userNotes' => $userNotes,
|
||||
'currentUserId' => $currentUserId,
|
||||
'history' => $resolvedHistory,
|
||||
'activityLog' => $activityLog,
|
||||
'statusLabel' => $this->statusLabel($statusCode, $statusLabelMap),
|
||||
@@ -624,6 +631,136 @@ final class OrdersController
|
||||
return Response::json(['success' => true, 'invoice_requested' => $value ? 1 : 0]);
|
||||
}
|
||||
|
||||
public function storeNote(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
$redirectTo = '/orders/' . $orderId . '#notes';
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
if ($orderId <= 0 || $this->orderNotes === null) {
|
||||
Flash::set('order.error', 'Modul notatek nie jest dostepny.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
$user = $this->auth->user();
|
||||
$userId = is_array($user) ? (int) ($user['id'] ?? 0) : 0;
|
||||
$authorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : '';
|
||||
if ($userId <= 0) {
|
||||
Flash::set('order.error', 'Wymagane zalogowanie.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->orderNotes->create($orderId, $userId, $authorName, (string) $request->input('body', ''));
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
'note',
|
||||
'Dodano notatke',
|
||||
null,
|
||||
'user',
|
||||
$authorName !== '' ? $authorName : null
|
||||
);
|
||||
Flash::set('order.success', 'Notatka dodana.');
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
Flash::set('order.error', $exception->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('order.error', 'Nie udalo sie dodac notatki: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
public function updateNote(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
$noteId = max(0, (int) $request->input('noteId', 0));
|
||||
$redirectTo = '/orders/' . $orderId . '#notes';
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) {
|
||||
Flash::set('order.error', 'Nieprawidlowe parametry.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
$user = $this->auth->user();
|
||||
$userId = is_array($user) ? (int) ($user['id'] ?? 0) : 0;
|
||||
$authorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : '';
|
||||
if ($userId <= 0) {
|
||||
Flash::set('order.error', 'Wymagane zalogowanie.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->orderNotes->update($noteId, $userId, (string) $request->input('body', ''));
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
'note',
|
||||
'Zaktualizowano notatke',
|
||||
['note_id' => $noteId],
|
||||
'user',
|
||||
$authorName !== '' ? $authorName : null
|
||||
);
|
||||
Flash::set('order.success', 'Notatka zaktualizowana.');
|
||||
} catch (RuntimeException $exception) {
|
||||
Flash::set('order.error', $exception->getMessage());
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
Flash::set('order.error', $exception->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('order.error', 'Nie udalo sie zaktualizowac notatki: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
public function deleteNote(Request $request): Response
|
||||
{
|
||||
$orderId = max(0, (int) $request->input('id', 0));
|
||||
$noteId = max(0, (int) $request->input('noteId', 0));
|
||||
$redirectTo = '/orders/' . $orderId . '#notes';
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('order.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
if ($orderId <= 0 || $noteId <= 0 || $this->orderNotes === null) {
|
||||
Flash::set('order.error', 'Nieprawidlowe parametry.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
$user = $this->auth->user();
|
||||
$userId = is_array($user) ? (int) ($user['id'] ?? 0) : 0;
|
||||
$authorName = is_array($user) ? trim((string) ($user['name'] ?? $user['email'] ?? '')) : '';
|
||||
if ($userId <= 0) {
|
||||
Flash::set('order.error', 'Wymagane zalogowanie.');
|
||||
return Response::redirect('/orders/' . $orderId);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->orderNotes->delete($noteId, $userId);
|
||||
$this->orders->recordActivity(
|
||||
$orderId,
|
||||
'note',
|
||||
'Usunieto notatke',
|
||||
['note_id' => $noteId],
|
||||
'user',
|
||||
$authorName !== '' ? $authorName : null
|
||||
);
|
||||
Flash::set('order.success', 'Notatka usunieta.');
|
||||
} catch (RuntimeException $exception) {
|
||||
Flash::set('order.error', $exception->getMessage());
|
||||
} catch (Throwable $exception) {
|
||||
Flash::set('order.error', 'Nie udalo sie usunac notatki: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<string, mixed>
|
||||
@@ -658,6 +795,13 @@ final class OrdersController
|
||||
? ' <span class="risk-return-badge" title="Klient nie odebral ' . $returnedCount . ' przesylek w historii">zwroty: ' . $returnedCount . '</span>'
|
||||
: '';
|
||||
|
||||
$userNotesCount = max(0, (int) ($row['user_notes_count'] ?? 0));
|
||||
$notesBadge = $userNotesCount >= 1
|
||||
? ' <a href="/orders/' . (int) ($row['id'] ?? 0) . '#notes" class="order-notes-badge" title="' . $userNotesCount . ' '
|
||||
. ($userNotesCount === 1 ? 'notatka' : ($userNotesCount < 5 ? 'notatki' : 'notatek')) . '">['
|
||||
. $userNotesCount . ']</a>'
|
||||
: '';
|
||||
|
||||
$previewBtn = '<button type="button" class="btn-icon js-order-preview-btn" data-order-id="' . (int) ($row['id'] ?? 0) . '" title="Podglad">'
|
||||
. '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>'
|
||||
. '</button>';
|
||||
@@ -667,7 +811,7 @@ final class OrdersController
|
||||
'order_ref' => '<div class="orders-ref">'
|
||||
. '<div class="orders-ref__main">' . $previewBtn . '<a href="/orders/' . (int) ($row['id'] ?? 0) . '">'
|
||||
. htmlspecialchars($internalOrderNumber !== '' ? $internalOrderNumber : ('#' . (string) ($row['id'] ?? 0)), ENT_QUOTES, 'UTF-8')
|
||||
. '</a></div>'
|
||||
. '</a>' . $notesBadge . '</div>'
|
||||
. '<div class="orders-ref__meta">'
|
||||
. '<span>' . htmlspecialchars($integrationName !== '' ? $integrationName : $this->sourceLabel($source), ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
. '<span>ID: ' . htmlspecialchars($sourceOrderId !== '' ? $sourceOrderId : $externalOrderId, ENT_QUOTES, 'UTF-8') . '</span>'
|
||||
|
||||
Reference in New Issue
Block a user