dc1457a008
P0 этап 5 — frontend для reminders (после backend-этапа 4). Пользователь может создавать/просматривать/завершать/удалять напоминания из UI с inline-create в DealDetailDrawer. Frontend: - api/reminders.ts: типизированные helpers для 5 endpoints + ensureCsrfCookie для mutating. Types ReminderFilter/ApiReminder/ReminderCounts. - stores/reminders.ts: Pinia с items/counts/currentFilter + load/refreshCounts/create/update/complete/remove. Optimistic для complete/remove с revert на reject. - components/reminders/ReminderDialog.vue: dual-mode (create/edit) modal с native datetime-local input. Props dealId?/reminder? (edit), ISO-конверсия при submit. - views/RemindersView.vue: page-head с stats (active/overdue) + reload-btn; 4 tabs (today/upcoming/overdue/completed) с counts на бейджах (overdue=error color); v-list с complete-btn / dropdown Изменить/Удалить с confirm-dialog; empty-state. - router: /reminders маршрут (lazy). - AppLayout: nav-badge «Напоминания» биндит count из store (replace static «12»); скрыт при count=0; polling 60 сек для refreshCounts. - DealDetailDrawer: секция «Напоминания» (только при tenantId+deal): inline + create-btn / список / complete / встроенный ReminderDialog. Vitest +20 (369/369 за 21.20 сек): - reminders-store 11: initial / load+reject / refreshCounts / create+reject / complete optimistic+revert / remove+reject / reset. - RemindersView 7: mount / 4 tabs / counts / empty-state / список / reload-btn / filter=today default. - AppLayout +2: бейдж скрыт при count=0 / показывает count при >0. Pest 347/347 (без изменений — backend нетронут). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
4.9 KiB
TypeScript
151 lines
4.9 KiB
TypeScript
import { ref } from 'vue';
|
||
import { defineStore } from 'pinia';
|
||
import {
|
||
type ApiReminder,
|
||
type CreateReminderPayload,
|
||
type ListRemindersParams,
|
||
type ReminderCounts,
|
||
type ReminderFilter,
|
||
type UpdateReminderPayload,
|
||
completeReminder,
|
||
createReminder,
|
||
deleteReminder,
|
||
listReminders,
|
||
updateReminder,
|
||
} from '../api/reminders';
|
||
|
||
/**
|
||
* Pinia store для напоминаний (RemindersView + nav-badge в AppLayout
|
||
* + DealDetailDrawer-секция).
|
||
*
|
||
* Стратегия: counts всегда обновляются на load() — даже если items
|
||
* фильтруются. nav-badge в AppLayout биндит counts.active.
|
||
*
|
||
* Optimistic-updates для complete/delete с revert на reject.
|
||
*/
|
||
export const useRemindersStore = defineStore('reminders', () => {
|
||
const items = ref<ApiReminder[]>([]);
|
||
const counts = ref<ReminderCounts>({ active: 0, today: 0, upcoming: 0, overdue: 0 });
|
||
const loading = ref(false);
|
||
const fetchError = ref(false);
|
||
const currentFilter = ref<ReminderFilter>('active');
|
||
|
||
async function load(params: ListRemindersParams = {}): Promise<void> {
|
||
loading.value = true;
|
||
fetchError.value = false;
|
||
try {
|
||
const response = await listReminders(params);
|
||
items.value = response.items;
|
||
counts.value = response.counts;
|
||
currentFilter.value = params.filter ?? 'active';
|
||
} catch {
|
||
fetchError.value = true;
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
async function refreshCounts(): Promise<void> {
|
||
// Лёгкий запрос только для bell-badge: limit=1, нам важны counts.
|
||
try {
|
||
const response = await listReminders({ filter: 'active', limit: 1 });
|
||
counts.value = response.counts;
|
||
} catch {
|
||
/* silent */
|
||
}
|
||
}
|
||
|
||
async function create(payload: CreateReminderPayload): Promise<ApiReminder | null> {
|
||
try {
|
||
const reminder = await createReminder(payload);
|
||
// Если новый reminder подходит под текущий filter и deal — добавляем в начало.
|
||
// (load() в любом случае пересчитает counts, но optimistic покажет сразу.)
|
||
items.value = [reminder, ...items.value];
|
||
counts.value.active++;
|
||
return reminder;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function update(id: number, payload: UpdateReminderPayload): Promise<ApiReminder | null> {
|
||
try {
|
||
const updated = await updateReminder(id, payload);
|
||
const idx = items.value.findIndex((r) => r.id === id);
|
||
if (idx >= 0) items.value.splice(idx, 1, updated);
|
||
return updated;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function complete(id: number): Promise<void> {
|
||
const item = items.value.find((r) => r.id === id);
|
||
if (!item || item.completed_at !== null) return;
|
||
|
||
// Optimistic.
|
||
const previous = item.completed_at;
|
||
item.completed_at = new Date().toISOString();
|
||
counts.value.active = Math.max(0, counts.value.active - 1);
|
||
|
||
try {
|
||
const updated = await completeReminder(id);
|
||
item.completed_at = updated.completed_at;
|
||
// Если фильтр active — убираем из items.
|
||
if (
|
||
currentFilter.value === 'active' ||
|
||
currentFilter.value === 'today' ||
|
||
currentFilter.value === 'upcoming' ||
|
||
currentFilter.value === 'overdue'
|
||
) {
|
||
items.value = items.value.filter((r) => r.id !== id);
|
||
}
|
||
} catch {
|
||
// Revert.
|
||
item.completed_at = previous;
|
||
counts.value.active++;
|
||
}
|
||
}
|
||
|
||
async function remove(id: number): Promise<void> {
|
||
const item = items.value.find((r) => r.id === id);
|
||
if (!item) return;
|
||
const wasActive = item.completed_at === null;
|
||
const previousItems = [...items.value];
|
||
const previousCounts = { ...counts.value };
|
||
|
||
// Optimistic.
|
||
items.value = items.value.filter((r) => r.id !== id);
|
||
if (wasActive) counts.value.active = Math.max(0, counts.value.active - 1);
|
||
|
||
try {
|
||
await deleteReminder(id);
|
||
} catch {
|
||
// Revert.
|
||
items.value = previousItems;
|
||
counts.value = previousCounts;
|
||
}
|
||
}
|
||
|
||
function reset(): void {
|
||
items.value = [];
|
||
counts.value = { active: 0, today: 0, upcoming: 0, overdue: 0 };
|
||
fetchError.value = false;
|
||
}
|
||
|
||
return {
|
||
items,
|
||
counts,
|
||
loading,
|
||
fetchError,
|
||
currentFilter,
|
||
load,
|
||
refreshCounts,
|
||
create,
|
||
update,
|
||
complete,
|
||
remove,
|
||
reset,
|
||
};
|
||
});
|