cb05657f30
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>
179 lines
5.6 KiB
Vue
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>
|