Files
portal/app/resources/js/views/RemindersView.vue
T
Дмитрий cb05657f30 chore(format): prettier --write across 37 .vue/.ts files
Phase 1B audit found 48 files failing `prettier --check`. Auto-apply
via `npx prettier --write resources/js/**/*.{ts,vue,css}` produced
style-only changes:
- consistent quote style
- trailing comma normalization
- spaces around : in v-card style="position: relative" attrs
- explicit ; insertion

No semantic changes. No code-behavior changes. Production-code only;
test files batched separately into `test(frontend):` commit.

Verification:
- npx vitest run → 79/79 files, 614/614 + 3 skipped (no regression).
- npx vue-tsc --noEmit → 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:24:33 +03:00

179 lines
5.6 KiB
Vue

<script setup lang="ts">
/**
* RemindersView — экран «Напоминания» из nav-tree.
*
* Тонкая shell: page-head + RemindersFilters + RemindersList + ReminderDialog (edit) +
* confirm-dialog (delete).
*
* Sprint 4 Phase B/2 — split на shell + 2 sub-components (audit O-refactor-04 хвост).
* ReminderDialog (форма создания/редактирования) уже отдельный компонент с прошлой фазы —
* используется как ReminderForm для edit-flow.
*
* State (`activeTab`, `editingReminder`, `deletingReminderId`) остаётся в parent
* ради единого reload-flow через watch(activeTab) + единых confirm-dialog'ов.
*/
import { defineAsyncComponent, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useRemindersStore } from '../stores/reminders';
import { type ApiReminder, type ReminderFilter } from '../api/reminders';
import RemindersFilters from '../components/reminders/RemindersFilters.vue';
import RemindersList from '../components/reminders/RemindersList.vue';
// Sprint 2 Phase B / O-perf-06: ReminderDialog открывается по action — chunk-split.
const ReminderDialog = defineAsyncComponent(() => import('../components/reminders/ReminderDialog.vue'));
const router = useRouter();
const store = useRemindersStore();
const activeTab = ref<ReminderFilter>('today');
const editDialogOpen = ref(false);
const editingReminder = ref<ApiReminder | null>(null);
const deleteConfirmOpen = ref(false);
const deletingReminderId = ref<number | null>(null);
async function reload(): Promise<void> {
await store.load({ filter: activeTab.value, limit: 200 });
}
watch(activeTab, () => reload());
onMounted(reload);
function openEdit(reminder: ApiReminder): void {
editingReminder.value = reminder;
editDialogOpen.value = true;
}
async function onSaved(): Promise<void> {
await reload();
}
function confirmDelete(id: number): void {
deletingReminderId.value = id;
deleteConfirmOpen.value = true;
}
async function executeDelete(): Promise<void> {
if (deletingReminderId.value === null) return;
await store.remove(deletingReminderId.value);
deletingReminderId.value = null;
deleteConfirmOpen.value = false;
// counts могут не пересчитаться оптимистично корректно — рефрешим.
await store.refreshCounts();
}
async function executeComplete(id: number): Promise<void> {
await store.complete(id);
await store.refreshCounts();
}
async function openDeal(dealId: number): Promise<void> {
void dealId; // на MVP — без deep-link на конкретный drawer.
await router.push('/deals');
}
</script>
<template>
<v-container class="reminders-view" max-width="900">
<div class="page-head">
<h1 class="page-title">Напоминания</h1>
<div class="page-meta">
<span class="page-stat">
<strong>{{ store.counts.active }}</strong>
<span>активные</span>
</span>
<span v-if="store.counts.overdue > 0" class="page-stat overdue">
<strong>{{ store.counts.overdue }}</strong>
<span>просрочено</span>
</span>
</div>
<v-btn
color="primary"
prepend-icon="mdi-refresh"
variant="outlined"
size="small"
:loading="store.loading"
data-testid="reload-btn"
@click="reload"
>
Обновить
</v-btn>
</div>
<v-alert
v-if="store.fetchError"
type="warning"
variant="tonal"
density="compact"
class="mb-3"
data-testid="fetch-error-alert"
>
Не удалось загрузить напоминания. Попробуйте «Обновить».
</v-alert>
<RemindersFilters v-model="activeTab" />
<RemindersList @edit="openEdit" @delete="confirmDelete" @complete="executeComplete" @open-deal="openDeal" />
<ReminderDialog v-model="editDialogOpen" :reminder="editingReminder" @saved="onSaved" />
<v-dialog v-model="deleteConfirmOpen" max-width="400">
<v-card>
<v-card-title>Удалить напоминание?</v-card-title>
<v-card-text>Действие необратимо.</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="deleteConfirmOpen = false">Отмена</v-btn>
<v-btn color="error" data-testid="confirm-delete" @click="executeDelete">Удалить</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<style scoped>
.reminders-view {
padding-top: 24px;
padding-bottom: 32px;
}
.page-head {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin: 0;
flex-shrink: 0;
}
.page-meta {
display: flex;
gap: 16px;
flex: 1;
}
.page-stat {
display: flex;
align-items: baseline;
gap: 4px;
font-size: 13px;
color: #66635c;
}
.page-stat strong {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-feature-settings: 'tnum';
font-size: 16px;
color: #081319;
}
.page-stat.overdue strong {
color: #b94837;
}
</style>