39b6127bce
Закрыт пункт «Reminders ⏸ no-view» из AppLayout nav-tree. Schema-таблица
reminders уже была в v8.10 §17.5 — теперь работает целиком backend-side.
Backend:
- App\Models\Reminder — Eloquent с casts/relations + isCompleted/isOverdue.
- ReminderFactory с states overdue/completed/sent.
- App\Http\Controllers\Api\ReminderController под auth:sanctum:
GET ?filter=&deal_id=&limit= (active/today/upcoming/overdue/completed,
окно ±1 день, counts для UI badges);
POST {deal_id, text?, remind_at, assignee_id?} (FK guard на assignee);
PATCH {id} (при смене remind_at сбрасывает is_sent+sent_at для retrigger);
POST {id}/complete (idempotent);
DELETE {id}.
RLS-обёртка + defense-in-depth where('tenant_id').
- App\Mail\ReminderDueNotification + emails/reminder.blade.php (Forest,
TZ из recipient.timezone).
- NotificationService::notifyReminder(Reminder) — recipient = assignee_id
?? created_by (если active+!deleted). Каналы email+inapp по prefs.
payload {reminder_id, deal_id} для UI deep-link.
- App\Console\Commands\RemindersDispatchDue — cron reminders:dispatch-due
{--dry-run} {--limit=500}. По одному reminder в DB::transaction (SET
LOCAL app.current_tenant_id нельзя переключать). После notifyReminder
ставит is_sent=true даже если recipient deactivated (защита от retry-spam).
Pest +32 (347/347 за 41.21 сек, 1203 assertions):
- ReminderControllerTest 21: 401 / RLS / 5 filter'ов / counts / deal_id /
store + FK guard / update text+remind_at сбрасывает is_sent / complete
idempotent / delete + 404 чужой.
- RemindersDispatchDueTest 11: due → email+inapp / future skip / completed
skip / уже sent / assignee вместо created_by / deactivated user (is_sent
всё равно) / только inapp при email=false / --dry-run / --limit / RLS.
PHPStan baseline регенерирован. IDE-helper для всех моделей.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
40 lines
1.9 KiB
PHP
40 lines
1.9 KiB
PHP
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Напоминание</title>
|
||
</head>
|
||
<body style="font-family: Inter, -apple-system, sans-serif; max-width: 600px; margin: 0 auto; padding: 24px; color: #081319;">
|
||
<h1 style="color: #0F6E56; font-size: 20px;">Лидерра. Напоминание</h1>
|
||
|
||
<p>Здравствуйте, {{ $recipient->first_name ?? $recipient->email }}.</p>
|
||
|
||
<p>Наступил срок напоминания по сделке <strong>#{{ $reminder->deal_id }}</strong>.</p>
|
||
|
||
@if ($reminder->text)
|
||
<blockquote style="border-left: 3px solid #0F6E56; padding: 8px 14px; background: #F0F8F5; margin: 16px 0;">
|
||
{{ $reminder->text }}
|
||
</blockquote>
|
||
@endif
|
||
|
||
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
|
||
<tr>
|
||
<td style="padding: 8px 12px; background: #F6F3EC; width: 140px; vertical-align: top;">Запланировано</td>
|
||
<td style="padding: 8px 12px; background: #FFFFFF; border-left: 1px solid #E1EEEA;">
|
||
{{ $reminder->remind_at?->setTimezone($recipient->timezone ?? 'Europe/Moscow')->format('d.m.Y H:i') }}
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 12px; background: #F6F3EC; vertical-align: top;">Сделка</td>
|
||
<td style="padding: 8px 12px; background: #FFFFFF; border-left: 1px solid #E1EEEA;">#{{ $reminder->deal_id }}</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<p style="margin-top: 24px;">Откройте CRM, чтобы пометить напоминание выполненным.</p>
|
||
|
||
<p style="color: #66635C; font-size: 12px; margin-top: 32px;">
|
||
Это автоматическое уведомление о событии «Напоминание». Чтобы изменить настройки уведомлений — перейдите в Настройки → Уведомления.
|
||
</p>
|
||
</body>
|
||
</html>
|