2026-05-09 12:41:41 +03:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
/**
|
|
|
|
|
* Dialog создания/редактирования напоминания.
|
|
|
|
|
*
|
|
|
|
|
* Поддерживает 2 режима:
|
|
|
|
|
* - Create: props.dealId передаётся, props.reminder=null;
|
|
|
|
|
* submit → create + emit('saved', newReminder).
|
|
|
|
|
* - Edit: props.reminder=ApiReminder, props.dealId опционален;
|
|
|
|
|
* submit → update + emit('saved', updatedReminder).
|
|
|
|
|
*
|
|
|
|
|
* Datetime-input через native HTML5 type="datetime-local" — без
|
|
|
|
|
* лишней библиотеки picker'а. Конвертация в ISO происходит при submit.
|
|
|
|
|
*/
|
|
|
|
|
import { computed, ref, watch } from 'vue';
|
|
|
|
|
import type { ApiReminder } from '../../api/reminders';
|
|
|
|
|
import { useRemindersStore } from '../../stores/reminders';
|
|
|
|
|
|
2026-05-09 19:36:02 +03:00
|
|
|
// Vue 3.5 defineModel() — Sprint 2 Phase B / O-stack-04.
|
|
|
|
|
const dialogOpen = defineModel<boolean>({ required: true });
|
|
|
|
|
|
2026-05-09 12:41:41 +03:00
|
|
|
const props = defineProps<{
|
|
|
|
|
dealId?: number | null;
|
|
|
|
|
reminder?: ApiReminder | null;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
(e: 'saved', reminder: ApiReminder): void;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const store = useRemindersStore();
|
|
|
|
|
|
|
|
|
|
const isEditing = computed(() => props.reminder !== null && props.reminder !== undefined);
|
|
|
|
|
const dialogTitle = computed(() => (isEditing.value ? 'Редактировать напоминание' : 'Новое напоминание'));
|
|
|
|
|
|
|
|
|
|
const text = ref('');
|
|
|
|
|
const remindAt = ref(''); // ISO local
|
|
|
|
|
const error = ref<string | null>(null);
|
|
|
|
|
const submitting = ref(false);
|
|
|
|
|
|
|
|
|
|
function defaultRemindAt(): string {
|
|
|
|
|
// Default: через 1 час от текущего времени, в формате datetime-local.
|
|
|
|
|
const d = new Date(Date.now() + 60 * 60 * 1000);
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isoToLocalInput(iso: string | null): string {
|
|
|
|
|
if (!iso) return defaultRemindAt();
|
|
|
|
|
const d = new Date(iso);
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(
|
2026-05-09 19:36:02 +03:00
|
|
|
dialogOpen,
|
2026-05-09 12:41:41 +03:00
|
|
|
(open) => {
|
|
|
|
|
if (open) {
|
|
|
|
|
if (isEditing.value && props.reminder) {
|
|
|
|
|
text.value = props.reminder.text ?? '';
|
|
|
|
|
remindAt.value = isoToLocalInput(props.reminder.remind_at);
|
|
|
|
|
} else {
|
|
|
|
|
text.value = '';
|
|
|
|
|
remindAt.value = defaultRemindAt();
|
|
|
|
|
}
|
|
|
|
|
error.value = null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
async function submit(): Promise<void> {
|
|
|
|
|
error.value = null;
|
|
|
|
|
|
|
|
|
|
if (!remindAt.value) {
|
|
|
|
|
error.value = 'Укажите дату и время напоминания.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const remindAtIso = new Date(remindAt.value).toISOString();
|
|
|
|
|
|
|
|
|
|
submitting.value = true;
|
|
|
|
|
try {
|
|
|
|
|
let saved: ApiReminder | null = null;
|
|
|
|
|
|
|
|
|
|
if (isEditing.value && props.reminder) {
|
|
|
|
|
saved = await store.update(props.reminder.id, {
|
|
|
|
|
text: text.value.trim() || null,
|
|
|
|
|
remind_at: remindAtIso,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
if (!props.dealId) {
|
|
|
|
|
error.value = 'Не указан deal_id для создания.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
saved = await store.create({
|
|
|
|
|
deal_id: props.dealId,
|
|
|
|
|
text: text.value.trim() || null,
|
|
|
|
|
remind_at: remindAtIso,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (saved === null) {
|
|
|
|
|
error.value = 'Не удалось сохранить. Попробуйте позже.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit('saved', saved);
|
2026-05-09 19:36:02 +03:00
|
|
|
dialogOpen.value = false;
|
2026-05-09 12:41:41 +03:00
|
|
|
} finally {
|
|
|
|
|
submitting.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancel(): void {
|
2026-05-09 19:36:02 +03:00
|
|
|
dialogOpen.value = false;
|
2026-05-09 12:41:41 +03:00
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-05-09 19:36:02 +03:00
|
|
|
<v-dialog v-model="dialogOpen" max-width="480" persistent>
|
2026-05-09 12:41:41 +03:00
|
|
|
<v-card>
|
|
|
|
|
<v-card-title>
|
|
|
|
|
{{ dialogTitle }}
|
|
|
|
|
</v-card-title>
|
|
|
|
|
<v-card-text>
|
|
|
|
|
<v-form @submit.prevent="submit">
|
|
|
|
|
<v-textarea
|
|
|
|
|
v-model="text"
|
|
|
|
|
label="Описание"
|
|
|
|
|
placeholder="Перезвонить клиенту, обсудить условия..."
|
|
|
|
|
rows="3"
|
|
|
|
|
counter
|
|
|
|
|
maxlength="255"
|
|
|
|
|
data-testid="reminder-text"
|
|
|
|
|
/>
|
|
|
|
|
<div class="datetime-row">
|
|
|
|
|
<label class="datetime-label" for="reminder-at-input">Когда напомнить</label>
|
|
|
|
|
<input
|
|
|
|
|
id="reminder-at-input"
|
|
|
|
|
v-model="remindAt"
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
class="datetime-input"
|
|
|
|
|
data-testid="reminder-at"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<v-alert
|
|
|
|
|
v-if="error"
|
|
|
|
|
type="warning"
|
|
|
|
|
variant="tonal"
|
|
|
|
|
density="compact"
|
|
|
|
|
class="mt-3"
|
|
|
|
|
data-testid="reminder-error"
|
|
|
|
|
>
|
|
|
|
|
{{ error }}
|
|
|
|
|
</v-alert>
|
|
|
|
|
</v-form>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
<v-card-actions>
|
|
|
|
|
<v-spacer />
|
|
|
|
|
<v-btn variant="text" @click="cancel">Отмена</v-btn>
|
|
|
|
|
<v-btn color="primary" :loading="submitting" data-testid="reminder-submit" @click="submit">
|
|
|
|
|
{{ isEditing ? 'Сохранить' : 'Создать' }}
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-card-actions>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.datetime-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.datetime-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #66635c;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.datetime-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border: 1px solid #d9d5cd;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.datetime-input:focus {
|
|
|
|
|
outline: 2px solid #0f6e56;
|
|
|
|
|
outline-offset: -1px;
|
|
|
|
|
border-color: #0f6e56;
|
|
|
|
|
}
|
|
|
|
|
</style>
|