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:
@@ -190,6 +190,15 @@ return [
|
||||
'address_invoice' => 'Dane do faktury',
|
||||
'address_delivery' => 'Dane wysylki',
|
||||
'notes_title' => 'Wiadomosci i zalaczniki',
|
||||
'notes_user_title' => 'Notatki',
|
||||
'notes_user_empty' => 'Brak notatek.',
|
||||
'notes_user_add_placeholder' => 'Wpisz notatke...',
|
||||
'notes_user_save' => 'Zapisz',
|
||||
'notes_user_edit' => 'Edytuj',
|
||||
'notes_user_delete' => 'Usun',
|
||||
'notes_user_cancel' => 'Anuluj',
|
||||
'notes_user_confirm_delete' => 'Usunac notatke?',
|
||||
'notes_imported_title' => 'Wiadomosci ze zrodla',
|
||||
'history_title' => 'Historia statusow',
|
||||
'fields' => [
|
||||
'status' => 'Status',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@use "modules/project-mappings";
|
||||
@use "modules/customer-risk-alert";
|
||||
@use "modules/sms-templates";
|
||||
@use "modules/order-notes";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
||||
112
resources/scss/modules/_order-notes.scss
Normal file
112
resources/scss/modules/_order-notes.scss
Normal file
@@ -0,0 +1,112 @@
|
||||
// Phase 129-01: notatki autorskie operatora w szczegolach zamowienia + badge na liscie.
|
||||
|
||||
.order-notes-badge {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
background: #eef2ff;
|
||||
color: #4338ca;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e0e7ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.order-notes-subtitle {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-muted, #64748b);
|
||||
}
|
||||
|
||||
.order-user-notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.order-event--user {
|
||||
border-left: 3px solid #6366f1;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.order-event__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.order-event__meta {
|
||||
font-size: 11px;
|
||||
color: var(--c-muted, #64748b);
|
||||
}
|
||||
|
||||
.order-event__actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.order-imported-notes {
|
||||
.order-event--imported {
|
||||
opacity: 0.75;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: var(--c-primary, #2563eb);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.order-note-form,
|
||||
.order-note-edit-form {
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.order-note-form__actions,
|
||||
.order-note-edit-form__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.order-note-edit-form {
|
||||
margin-top: 6px;
|
||||
|
||||
.order-note-edit-form__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
@@ -218,6 +218,7 @@
|
||||
<script src="/assets/js/modules/alert-dismiss.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/alert-dismiss.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/notifications.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/notifications.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/sms-template-picker.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/sms-template-picker.js') ?: 0 ?>"></script>
|
||||
<script src="/assets/js/modules/order-notes.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/order-notes.js') ?: 0 ?>"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||
<script src="/assets/js/modules/statistics-summary-charts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/statistics-summary-charts.js') ?: 0 ?>"></script>
|
||||
<script>
|
||||
|
||||
@@ -7,6 +7,9 @@ $shipmentsList = is_array($shipments ?? null) ? $shipments : [];
|
||||
$packagesList = is_array($packages ?? null) ? $packages : [];
|
||||
$documentsList = is_array($documents ?? null) ? $documents : [];
|
||||
$notesList = is_array($notes ?? null) ? $notes : [];
|
||||
$userNotesList = is_array($userNotes ?? null) ? $userNotes : [];
|
||||
$currentUserIdValue = (int) ($currentUserId ?? 0);
|
||||
$csrfTokenValue = (string) ($csrfToken ?? '');
|
||||
$receiptsList = is_array($receipts ?? null) ? $receipts : [];
|
||||
$receiptConfigsList = is_array($receiptConfigs ?? null) ? $receiptConfigs : [];
|
||||
$invoicesList = is_array($invoices ?? null) ? $invoices : [];
|
||||
@@ -447,19 +450,70 @@ foreach ($addressesList as $address) {
|
||||
</section>
|
||||
|
||||
<section class="mt-16 order-grid-2">
|
||||
<article class="card">
|
||||
<article class="card" id="notes">
|
||||
<h3 class="section-title"><?= $e($t('orders.details.notes_title')) ?></h3>
|
||||
<div class="order-events mt-12">
|
||||
<?php if ($notesList === []): ?>
|
||||
<div class="muted">-</div>
|
||||
|
||||
<div class="order-user-notes mt-12" data-order-id="<?= (int) ($orderId ?? 0) ?>">
|
||||
<h4 class="order-notes-subtitle"><?= $e($t('orders.details.notes_user_title')) ?></h4>
|
||||
<?php if ($userNotesList === []): ?>
|
||||
<div class="muted"><?= $e($t('orders.details.notes_user_empty')) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($userNotesList as $userNote): ?>
|
||||
<?php
|
||||
$noteId = (int) ($userNote['id'] ?? 0);
|
||||
$noteAuthorId = (int) ($userNote['user_id'] ?? 0);
|
||||
$noteAuthorName = trim((string) ($userNote['author_name'] ?? ''));
|
||||
$noteCreatedAt = (string) ($userNote['created_at'] ?? '');
|
||||
$noteBody = (string) ($userNote['body'] ?? '');
|
||||
$canEdit = $currentUserIdValue > 0 && $noteAuthorId === $currentUserIdValue;
|
||||
?>
|
||||
<div class="order-event order-event--user" data-note-id="<?= $noteId ?>">
|
||||
<div class="order-event__head">
|
||||
<span class="order-event__meta"><?= $e($noteCreatedAt) ?><?php if ($noteAuthorName !== ''): ?> · <?= $e($noteAuthorName) ?><?php endif; ?></span>
|
||||
<?php if ($canEdit): ?>
|
||||
<span class="order-event__actions">
|
||||
<button type="button" class="btn-link js-order-note-edit" data-note-id="<?= $noteId ?>"><?= $e($t('orders.details.notes_user_edit')) ?></button>
|
||||
<form method="post" action="/orders/<?= (int) ($orderId ?? 0) ?>/notes/<?= $noteId ?>/delete" class="js-order-note-delete" style="display:inline">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfTokenValue) ?>">
|
||||
<button type="submit" class="btn-link btn-link--danger"><?= $e($t('orders.details.notes_user_delete')) ?></button>
|
||||
</form>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="order-event__body js-order-note-body"><?= nl2br($e($noteBody)) ?></div>
|
||||
<?php if ($canEdit): ?>
|
||||
<form method="post" action="/orders/<?= (int) ($orderId ?? 0) ?>/notes/<?= $noteId ?>/update" class="order-note-edit-form js-order-note-edit-form" style="display:none">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfTokenValue) ?>">
|
||||
<textarea name="body" class="form-control" rows="3" maxlength="2000" required><?= $e($noteBody) ?></textarea>
|
||||
<div class="order-note-edit-form__actions">
|
||||
<button type="submit" class="btn btn-primary btn-sm"><?= $e($t('orders.details.notes_user_save')) ?></button>
|
||||
<button type="button" class="btn btn-default btn-sm js-order-note-edit-cancel"><?= $e($t('orders.details.notes_user_cancel')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<form method="post" action="/orders/<?= (int) ($orderId ?? 0) ?>/notes" class="order-note-form mt-12">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfTokenValue) ?>">
|
||||
<textarea name="body" class="form-control" rows="3" maxlength="2000" placeholder="<?= $e($t('orders.details.notes_user_add_placeholder')) ?>" required></textarea>
|
||||
<div class="order-note-form__actions">
|
||||
<button type="submit" class="btn btn-primary btn-sm"><?= $e($t('orders.details.notes_user_save')) ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($notesList !== []): ?>
|
||||
<div class="order-imported-notes mt-16">
|
||||
<h4 class="order-notes-subtitle"><?= $e($t('orders.details.notes_imported_title')) ?></h4>
|
||||
<?php foreach ($notesList as $note): ?>
|
||||
<div class="order-event">
|
||||
<div class="order-event order-event--imported">
|
||||
<div class="order-event__head"><?= $e((string) ($note['note_type'] ?? '')) ?> | <?= $e((string) ($note['created_at_external'] ?? '')) ?></div>
|
||||
<div class="order-event__body"><?= $e((string) ($note['comment'] ?? '')) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
|
||||
Reference in New Issue
Block a user