feat(crm): add deal create/update controls with status and payment
This commit is contained in:
@@ -229,6 +229,11 @@ const {
|
|||||||
getDealCurrentStepLabel,
|
getDealCurrentStepLabel,
|
||||||
isDealStepDone,
|
isDealStepDone,
|
||||||
formatDealStepMeta,
|
formatDealStepMeta,
|
||||||
|
dealStageOptions,
|
||||||
|
createDealForContact,
|
||||||
|
dealCreateLoading,
|
||||||
|
updateDealDetails,
|
||||||
|
dealUpdateLoading,
|
||||||
refetchDeals,
|
refetchDeals,
|
||||||
} = useDeals({ apolloAuthReady, contacts, calendarEvents });
|
} = useDeals({ apolloAuthReady, contacts, calendarEvents });
|
||||||
|
|
||||||
@@ -896,31 +901,6 @@ function closeCommQuickMenu() {
|
|||||||
commQuickMenuOpen.value = false;
|
commQuickMenuOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commEventDateOptions = computed(() => {
|
|
||||||
const out: string[] = [];
|
|
||||||
const today = new Date();
|
|
||||||
for (let i = -7; i <= 30; i += 1) {
|
|
||||||
const d = new Date(today);
|
|
||||||
d.setDate(today.getDate() + i);
|
|
||||||
out.push(dayKey(d));
|
|
||||||
}
|
|
||||||
const selected = String(commEventForm.value.startDate ?? "").trim();
|
|
||||||
if (selected && !out.includes(selected)) out.unshift(selected);
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
|
|
||||||
const commEventTimeOptions = computed(() => {
|
|
||||||
const out: string[] = [];
|
|
||||||
for (let hour = 0; hour < 24; hour += 1) {
|
|
||||||
for (let minute = 0; minute < 60; minute += 15) {
|
|
||||||
out.push(`${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const selected = String(commEventForm.value.startTime ?? "").trim();
|
|
||||||
if (selected && !out.includes(selected)) out.unshift(selected);
|
|
||||||
return out;
|
|
||||||
});
|
|
||||||
|
|
||||||
function commComposerPlaceholder() {
|
function commComposerPlaceholder() {
|
||||||
if (commComposerMode.value === "planned") return "Опиши, что нужно запланировать...";
|
if (commComposerMode.value === "planned") return "Опиши, что нужно запланировать...";
|
||||||
if (commComposerMode.value === "logged") return "Опиши итог/отчёт по прошедшему событию...";
|
if (commComposerMode.value === "logged") return "Опиши итог/отчёт по прошедшему событию...";
|
||||||
@@ -2135,32 +2115,18 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="commComposerMode === 'planned' || commComposerMode === 'logged'" class="comm-event-controls">
|
<div v-if="commComposerMode === 'planned' || commComposerMode === 'logged'" class="comm-event-controls">
|
||||||
<select
|
<input
|
||||||
v-model="commEventForm.startDate"
|
v-model="commEventForm.startDate"
|
||||||
class="select select-bordered select-xs h-7 min-h-7"
|
type="date"
|
||||||
|
class="input input-bordered input-xs h-7 min-h-7"
|
||||||
:disabled="commEventSaving"
|
:disabled="commEventSaving"
|
||||||
>
|
>
|
||||||
<option
|
<input
|
||||||
v-for="dateValue in commEventDateOptions"
|
|
||||||
:key="`comm-event-date-${dateValue}`"
|
|
||||||
:value="dateValue"
|
|
||||||
>
|
|
||||||
{{ formatDay(`${dateValue}T00:00:00`) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
v-model="commEventForm.startTime"
|
v-model="commEventForm.startTime"
|
||||||
class="select select-bordered select-xs h-7 min-h-7"
|
type="time"
|
||||||
|
class="input input-bordered input-xs h-7 min-h-7"
|
||||||
:disabled="commEventSaving"
|
:disabled="commEventSaving"
|
||||||
>
|
>
|
||||||
<option
|
|
||||||
v-for="timeValue in commEventTimeOptions"
|
|
||||||
:key="`comm-event-time-${timeValue}`"
|
|
||||||
:value="timeValue"
|
|
||||||
>
|
|
||||||
{{ timeValue }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<select
|
<select
|
||||||
v-model.number="commEventForm.durationMinutes"
|
v-model.number="commEventForm.durationMinutes"
|
||||||
class="select select-bordered select-xs h-7 min-h-7"
|
class="select select-bordered select-xs h-7 min-h-7"
|
||||||
@@ -2290,6 +2256,11 @@ onBeforeUnmount(() => {
|
|||||||
:on-selected-deal-steps-expanded-change="(value) => { selectedDealStepsExpanded = value; }"
|
:on-selected-deal-steps-expanded-change="(value) => { selectedDealStepsExpanded = value; }"
|
||||||
:is-deal-step-done="isDealStepDone"
|
:is-deal-step-done="isDealStepDone"
|
||||||
:format-deal-step-meta="formatDealStepMeta"
|
:format-deal-step-meta="formatDealStepMeta"
|
||||||
|
:deal-stage-options="dealStageOptions"
|
||||||
|
:create-deal-for-contact="createDealForContact"
|
||||||
|
:deal-create-loading="dealCreateLoading"
|
||||||
|
:update-deal-details="updateDealDetails"
|
||||||
|
:deal-update-loading="dealUpdateLoading"
|
||||||
:active-review-contact-diff="activeReviewContactDiff"
|
:active-review-contact-diff="activeReviewContactDiff"
|
||||||
:selected-workspace-contact="selectedWorkspaceContact"
|
:selected-workspace-contact="selectedWorkspaceContact"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
|
||||||
type ContactRightPanelMode = "summary" | "documents";
|
type ContactRightPanelMode = "summary" | "documents";
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
selectedWorkspaceContactDocuments: any[];
|
selectedWorkspaceContactDocuments: any[];
|
||||||
contactRightPanelMode: ContactRightPanelMode;
|
contactRightPanelMode: ContactRightPanelMode;
|
||||||
onContactRightPanelModeChange: (mode: ContactRightPanelMode) => void;
|
onContactRightPanelModeChange: (mode: ContactRightPanelMode) => void;
|
||||||
@@ -24,6 +26,17 @@ defineProps<{
|
|||||||
onSelectedDealStepsExpandedChange: (value: boolean) => void;
|
onSelectedDealStepsExpandedChange: (value: boolean) => void;
|
||||||
isDealStepDone: (step: any) => boolean;
|
isDealStepDone: (step: any) => boolean;
|
||||||
formatDealStepMeta: (step: any) => string;
|
formatDealStepMeta: (step: any) => string;
|
||||||
|
dealStageOptions: string[];
|
||||||
|
createDealForContact: (input: {
|
||||||
|
contactId: string;
|
||||||
|
title: string;
|
||||||
|
stage: string;
|
||||||
|
amount: string;
|
||||||
|
paidAmount: string;
|
||||||
|
}) => Promise<any>;
|
||||||
|
dealCreateLoading: boolean;
|
||||||
|
updateDealDetails: (input: { dealId: string; stage: string; amount: string; paidAmount: string }) => Promise<boolean>;
|
||||||
|
dealUpdateLoading: boolean;
|
||||||
activeReviewContactDiff: {
|
activeReviewContactDiff: {
|
||||||
contactId?: string;
|
contactId?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
@@ -31,13 +44,132 @@ defineProps<{
|
|||||||
} | null;
|
} | null;
|
||||||
selectedWorkspaceContact: {
|
selectedWorkspaceContact: {
|
||||||
id: string;
|
id: string;
|
||||||
|
name?: string;
|
||||||
description: string;
|
description: string;
|
||||||
} | null;
|
} | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function onDocumentsSearchInput(event: Event) {
|
function onDocumentsSearchInput(event: Event) {
|
||||||
const target = event.target as HTMLInputElement | null;
|
const target = event.target as HTMLInputElement | null;
|
||||||
onContactDocumentsSearchInput(target?.value ?? "");
|
props.onContactDocumentsSearchInput(target?.value ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dealStageDraft = ref("");
|
||||||
|
const dealAmountDraft = ref("");
|
||||||
|
const dealPaidAmountDraft = ref("");
|
||||||
|
const dealNewStageDraft = ref("");
|
||||||
|
const dealSaveError = ref("");
|
||||||
|
const dealSaveSuccess = ref("");
|
||||||
|
const dealCreateTitleDraft = ref("");
|
||||||
|
const dealCreateStageDraft = ref("");
|
||||||
|
const dealCreateAmountDraft = ref("");
|
||||||
|
const dealCreatePaidAmountDraft = ref("");
|
||||||
|
const dealCreateError = ref("");
|
||||||
|
const dealCreateSuccess = ref("");
|
||||||
|
const visibleDealStageOptions = computed(() => {
|
||||||
|
const unique = new Set<string>(props.dealStageOptions);
|
||||||
|
const current = dealStageDraft.value.trim();
|
||||||
|
if (current) unique.add(current);
|
||||||
|
return [...unique];
|
||||||
|
});
|
||||||
|
const visibleDealCreateStageOptions = computed(() => {
|
||||||
|
const unique = new Set<string>(props.dealStageOptions);
|
||||||
|
const current = dealCreateStageDraft.value.trim();
|
||||||
|
if (current) unique.add(current);
|
||||||
|
return [...unique];
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedWorkspaceDeal?.id ?? "",
|
||||||
|
() => {
|
||||||
|
dealStageDraft.value = String(props.selectedWorkspaceDeal?.stage ?? "").trim();
|
||||||
|
dealAmountDraft.value = String(props.selectedWorkspaceDeal?.amount ?? "").trim();
|
||||||
|
dealPaidAmountDraft.value = String(props.selectedWorkspaceDeal?.paidAmount ?? "").trim();
|
||||||
|
dealNewStageDraft.value = "";
|
||||||
|
dealSaveError.value = "";
|
||||||
|
dealSaveSuccess.value = "";
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedWorkspaceContact?.id ?? "",
|
||||||
|
() => {
|
||||||
|
dealCreateTitleDraft.value = "";
|
||||||
|
dealCreateAmountDraft.value = "";
|
||||||
|
dealCreatePaidAmountDraft.value = "";
|
||||||
|
dealCreateStageDraft.value = props.dealStageOptions[0] ?? "Новый";
|
||||||
|
dealCreateError.value = "";
|
||||||
|
dealCreateSuccess.value = "";
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.dealStageOptions.join("|"),
|
||||||
|
() => {
|
||||||
|
if (!dealCreateStageDraft.value.trim()) {
|
||||||
|
dealCreateStageDraft.value = props.dealStageOptions[0] ?? "Новый";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function applyNewDealStage() {
|
||||||
|
const value = dealNewStageDraft.value.trim();
|
||||||
|
if (!value) {
|
||||||
|
dealSaveError.value = "Введите название статуса";
|
||||||
|
dealSaveSuccess.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dealStageDraft.value = value;
|
||||||
|
dealNewStageDraft.value = "";
|
||||||
|
dealSaveError.value = "";
|
||||||
|
dealSaveSuccess.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDealDetails() {
|
||||||
|
if (!props.selectedWorkspaceDeal) return;
|
||||||
|
dealSaveError.value = "";
|
||||||
|
dealSaveSuccess.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const changed = await props.updateDealDetails({
|
||||||
|
dealId: props.selectedWorkspaceDeal.id,
|
||||||
|
stage: dealStageDraft.value,
|
||||||
|
amount: dealAmountDraft.value,
|
||||||
|
paidAmount: dealPaidAmountDraft.value,
|
||||||
|
});
|
||||||
|
dealSaveSuccess.value = changed ? "Сделка обновлена" : "Изменений нет";
|
||||||
|
} catch (error) {
|
||||||
|
dealSaveError.value = error instanceof Error ? error.message : "Не удалось обновить сделку";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDeal() {
|
||||||
|
if (!props.selectedWorkspaceContact) return;
|
||||||
|
dealCreateError.value = "";
|
||||||
|
dealCreateSuccess.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await props.createDealForContact({
|
||||||
|
contactId: props.selectedWorkspaceContact.id,
|
||||||
|
title: dealCreateTitleDraft.value,
|
||||||
|
stage: dealCreateStageDraft.value,
|
||||||
|
amount: dealCreateAmountDraft.value,
|
||||||
|
paidAmount: dealCreatePaidAmountDraft.value,
|
||||||
|
});
|
||||||
|
if (!created) {
|
||||||
|
dealCreateError.value = "Не удалось создать сделку";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dealCreateSuccess.value = "Сделка создана";
|
||||||
|
dealCreateTitleDraft.value = "";
|
||||||
|
dealCreateAmountDraft.value = "";
|
||||||
|
dealCreatePaidAmountDraft.value = "";
|
||||||
|
} catch (error) {
|
||||||
|
dealCreateError.value = error instanceof Error ? error.message : "Не удалось создать сделку";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -45,35 +177,35 @@ function onDocumentsSearchInput(event: Event) {
|
|||||||
<aside class="h-full min-h-0">
|
<aside class="h-full min-h-0">
|
||||||
<div class="flex h-full min-h-0 flex-col p-3">
|
<div class="flex h-full min-h-0 flex-col p-3">
|
||||||
<div
|
<div
|
||||||
v-if="selectedWorkspaceContactDocuments.length"
|
v-if="props.selectedWorkspaceContactDocuments.length"
|
||||||
class="mb-2 flex flex-wrap items-center gap-1.5 rounded-xl border border-base-300 bg-base-200/35 px-2 py-1.5"
|
class="mb-2 flex flex-wrap items-center gap-1.5 rounded-xl border border-base-300 bg-base-200/35 px-2 py-1.5"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="badge badge-sm badge-outline"
|
class="badge badge-sm badge-outline"
|
||||||
@click="onContactRightPanelModeChange('documents')"
|
@click="props.onContactRightPanelModeChange('documents')"
|
||||||
>
|
>
|
||||||
{{ selectedWorkspaceContactDocuments.length }} documents
|
{{ props.selectedWorkspaceContactDocuments.length }} documents
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-for="doc in selectedWorkspaceContactDocuments.slice(0, 15)"
|
v-for="doc in props.selectedWorkspaceContactDocuments.slice(0, 15)"
|
||||||
:key="`contact-doc-chip-${doc.id}`"
|
:key="`contact-doc-chip-${doc.id}`"
|
||||||
class="rounded-full border border-base-300 bg-base-100 px-2 py-0.5 text-[10px] text-base-content/80 hover:bg-base-200/70"
|
class="rounded-full border border-base-300 bg-base-100 px-2 py-0.5 text-[10px] text-base-content/80 hover:bg-base-200/70"
|
||||||
@click="onContactRightPanelModeChange('documents'); onSelectedDocumentIdChange(doc.id)"
|
@click="props.onContactRightPanelModeChange('documents'); props.onSelectedDocumentIdChange(doc.id)"
|
||||||
>
|
>
|
||||||
{{ doc.title }}
|
{{ doc.title }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="contactRightPanelMode === 'documents'" class="min-h-0 flex-1 overflow-y-auto pr-1">
|
<div v-if="props.contactRightPanelMode === 'documents'" class="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||||
<div class="sticky top-0 z-10 border-b border-base-300 bg-base-100 pb-2">
|
<div class="sticky top-0 z-10 border-b border-base-300 bg-base-100 pb-2">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
|
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">
|
||||||
Contact documents
|
Contact documents
|
||||||
</p>
|
</p>
|
||||||
<button class="btn btn-ghost btn-xs" @click="onContactRightPanelModeChange('summary')">Summary</button>
|
<button class="btn btn-ghost btn-xs" @click="props.onContactRightPanelModeChange('summary')">Summary</button>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:value="contactDocumentsSearch"
|
:value="props.contactDocumentsSearch"
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered input-xs mt-2 w-full"
|
class="input input-bordered input-xs mt-2 w-full"
|
||||||
placeholder="Search documents..."
|
placeholder="Search documents..."
|
||||||
@@ -82,22 +214,22 @@ function onDocumentsSearchInput(event: Event) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-2 space-y-1.5">
|
<div class="mt-2 space-y-1.5">
|
||||||
<article
|
<article
|
||||||
v-for="doc in filteredSelectedWorkspaceContactDocuments"
|
v-for="doc in props.filteredSelectedWorkspaceContactDocuments"
|
||||||
:key="`contact-doc-right-${doc.id}`"
|
:key="`contact-doc-right-${doc.id}`"
|
||||||
class="w-full rounded-xl border border-base-300 px-2.5 py-2 text-left transition hover:bg-base-200/50"
|
class="w-full rounded-xl border border-base-300 px-2.5 py-2 text-left transition hover:bg-base-200/50"
|
||||||
:class="selectedDocumentId === doc.id ? 'border-primary bg-primary/10' : ''"
|
:class="props.selectedDocumentId === doc.id ? 'border-primary bg-primary/10' : ''"
|
||||||
@click="onSelectedDocumentIdChange(doc.id)"
|
@click="props.onSelectedDocumentIdChange(doc.id)"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
|
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
|
||||||
<div class="mt-1 flex items-center justify-between gap-2">
|
<div class="mt-1 flex items-center justify-between gap-2">
|
||||||
<p class="text-[10px] text-base-content/55">Updated {{ formatStamp(doc.updatedAt) }}</p>
|
<p class="text-[10px] text-base-content/55">Updated {{ props.formatStamp(doc.updatedAt) }}</p>
|
||||||
<button class="btn btn-ghost btn-xs px-1" @click.stop="onSelectedDocumentIdChange(doc.id); openDocumentsTab(true)">Open</button>
|
<button class="btn btn-ghost btn-xs px-1" @click.stop="props.onSelectedDocumentIdChange(doc.id); props.openDocumentsTab(true)">Open</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<p v-if="filteredSelectedWorkspaceContactDocuments.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
<p v-if="props.filteredSelectedWorkspaceContactDocuments.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
||||||
No linked documents.
|
No linked documents.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,46 +237,185 @@ function onDocumentsSearchInput(event: Event) {
|
|||||||
|
|
||||||
<div v-else class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
<div v-else class="min-h-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||||
<div
|
<div
|
||||||
v-if="selectedWorkspaceDeal"
|
v-if="props.selectedWorkspaceContact"
|
||||||
|
class="rounded-xl border border-base-300 bg-base-200/25 p-2.5"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-base-content/60">Новая сделка</p>
|
||||||
|
<input
|
||||||
|
v-model="dealCreateTitleDraft"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-sm mt-2 w-full"
|
||||||
|
:disabled="props.dealCreateLoading"
|
||||||
|
placeholder="Название сделки"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="mt-2 grid grid-cols-2 gap-1.5">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-base-content/60">Статус</p>
|
||||||
|
<select
|
||||||
|
v-model="dealCreateStageDraft"
|
||||||
|
class="select select-bordered select-xs w-full"
|
||||||
|
:disabled="props.dealCreateLoading"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="stageOption in visibleDealCreateStageOptions"
|
||||||
|
:key="`create-deal-stage-${stageOption}`"
|
||||||
|
:value="stageOption"
|
||||||
|
>
|
||||||
|
{{ stageOption }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-base-content/60">Сумма</p>
|
||||||
|
<input
|
||||||
|
v-model="dealCreateAmountDraft"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
class="input input-bordered input-xs h-7 min-h-7 w-full"
|
||||||
|
:disabled="props.dealCreateLoading"
|
||||||
|
placeholder="0"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1.5 space-y-1">
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-base-content/60">Оплачено</p>
|
||||||
|
<input
|
||||||
|
v-model="dealCreatePaidAmountDraft"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
class="input input-bordered input-xs h-7 min-h-7 w-full"
|
||||||
|
:disabled="props.dealCreateLoading"
|
||||||
|
placeholder="0"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="dealCreateError" class="mt-2 text-[10px] text-error">{{ dealCreateError }}</p>
|
||||||
|
<p v-if="dealCreateSuccess" class="mt-2 text-[10px] text-success">{{ dealCreateSuccess }}</p>
|
||||||
|
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-xs h-7 min-h-7 px-2.5"
|
||||||
|
:disabled="props.dealCreateLoading"
|
||||||
|
@click="createDeal"
|
||||||
|
>
|
||||||
|
Создать сделку
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.selectedWorkspaceDeal"
|
||||||
class="rounded-xl border border-base-300 bg-base-200/30 p-2.5"
|
class="rounded-xl border border-base-300 bg-base-200/30 p-2.5"
|
||||||
:class="[
|
:class="[
|
||||||
isReviewHighlightedDeal(selectedWorkspaceDeal.id) ? 'border-primary/60 bg-primary/10' : '',
|
props.isReviewHighlightedDeal(props.selectedWorkspaceDeal.id) ? 'border-primary/60 bg-primary/10' : '',
|
||||||
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
|
props.contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
|
||||||
hasContextScope('deal') ? 'context-scope-block-selected' : '',
|
props.hasContextScope('deal') ? 'context-scope-block-selected' : '',
|
||||||
]"
|
]"
|
||||||
@click="toggleContextScope('deal')"
|
@click="props.toggleContextScope('deal')"
|
||||||
>
|
>
|
||||||
<span v-if="contextPickerEnabled" class="context-scope-label">Сделка</span>
|
<span v-if="props.contextPickerEnabled" class="context-scope-label">Сделка</span>
|
||||||
<p class="text-sm font-medium">
|
<p class="text-sm font-medium">
|
||||||
{{ formatDealHeadline(selectedWorkspaceDeal) }}
|
{{ props.formatDealHeadline(props.selectedWorkspaceDeal) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 text-[11px] text-base-content/75">
|
<p class="mt-1 text-[11px] text-base-content/75">
|
||||||
{{ selectedWorkspaceDealSubtitle }}
|
{{ props.selectedWorkspaceDealSubtitle }}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="mt-2 space-y-2 rounded-lg border border-base-300/70 bg-base-100/75 p-2" @click.stop>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-base-content/60">Статус сделки</p>
|
||||||
|
<select
|
||||||
|
v-model="dealStageDraft"
|
||||||
|
class="select select-bordered select-xs w-full"
|
||||||
|
:disabled="props.dealUpdateLoading"
|
||||||
|
>
|
||||||
|
<option v-for="stageOption in visibleDealStageOptions" :key="`deal-stage-${stageOption}`" :value="stageOption">
|
||||||
|
{{ stageOption }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<input
|
||||||
|
v-model="dealNewStageDraft"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered input-xs h-7 min-h-7 flex-1"
|
||||||
|
:disabled="props.dealUpdateLoading"
|
||||||
|
placeholder="Добавить статус"
|
||||||
|
@keydown.enter.prevent="applyNewDealStage"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs h-7 min-h-7 px-2"
|
||||||
|
:disabled="props.dealUpdateLoading"
|
||||||
|
@click="applyNewDealStage"
|
||||||
|
>
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-1.5">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-base-content/60">Сумма</p>
|
||||||
|
<input
|
||||||
|
v-model="dealAmountDraft"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
class="input input-bordered input-xs h-7 min-h-7 w-full"
|
||||||
|
:disabled="props.dealUpdateLoading"
|
||||||
|
placeholder="0"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-[10px] uppercase tracking-wide text-base-content/60">Оплачено</p>
|
||||||
|
<input
|
||||||
|
v-model="dealPaidAmountDraft"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
class="input input-bordered input-xs h-7 min-h-7 w-full"
|
||||||
|
:disabled="props.dealUpdateLoading"
|
||||||
|
placeholder="0"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="dealSaveError" class="text-[10px] text-error">{{ dealSaveError }}</p>
|
||||||
|
<p v-if="dealSaveSuccess" class="text-[10px] text-success">{{ dealSaveSuccess }}</p>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-xs h-7 min-h-7 px-2.5"
|
||||||
|
:disabled="props.dealUpdateLoading"
|
||||||
|
@click="saveDealDetails"
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="selectedWorkspaceDealSteps.length"
|
v-if="props.selectedWorkspaceDealSteps.length"
|
||||||
class="mt-2 text-[11px] font-medium text-primary hover:underline"
|
class="mt-2 text-[11px] font-medium text-primary hover:underline"
|
||||||
@click="onSelectedDealStepsExpandedChange(!selectedDealStepsExpanded)"
|
@click="props.onSelectedDealStepsExpandedChange(!props.selectedDealStepsExpanded)"
|
||||||
>
|
>
|
||||||
{{ selectedDealStepsExpanded ? "Скрыть шаги" : `Показать шаги (${selectedWorkspaceDealSteps.length})` }}
|
{{ props.selectedDealStepsExpanded ? "Скрыть шаги" : `Показать шаги (${props.selectedWorkspaceDealSteps.length})` }}
|
||||||
</button>
|
</button>
|
||||||
<div v-if="selectedDealStepsExpanded && selectedWorkspaceDealSteps.length" class="mt-2 space-y-1.5">
|
<div v-if="props.selectedDealStepsExpanded && props.selectedWorkspaceDealSteps.length" class="mt-2 space-y-1.5">
|
||||||
<div
|
<div
|
||||||
v-for="step in selectedWorkspaceDealSteps"
|
v-for="step in props.selectedWorkspaceDealSteps"
|
||||||
:key="step.id"
|
:key="step.id"
|
||||||
class="flex items-start gap-2 rounded-lg border border-base-300/70 bg-base-100/80 px-2 py-1.5"
|
class="flex items-start gap-2 rounded-lg border border-base-300/70 bg-base-100/80 px-2 py-1.5"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-xs mt-0.5"
|
class="checkbox checkbox-xs mt-0.5"
|
||||||
:checked="isDealStepDone(step)"
|
:checked="props.isDealStepDone(step)"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-[11px] font-medium" :class="isDealStepDone(step) ? 'line-through text-base-content/60' : 'text-base-content/90'">
|
<p class="truncate text-[11px] font-medium" :class="props.isDealStepDone(step) ? 'line-through text-base-content/60' : 'text-base-content/90'">
|
||||||
{{ step.title }}
|
{{ step.title }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-0.5 text-[10px] text-base-content/55">{{ formatDealStepMeta(step) }}</p>
|
<p class="mt-0.5 text-[10px] text-base-content/55">{{ props.formatDealStepMeta(step) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,28 +424,28 @@ function onDocumentsSearchInput(event: Event) {
|
|||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
:class="[
|
:class="[
|
||||||
contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
|
props.contextPickerEnabled ? 'context-scope-block context-scope-block-active' : '',
|
||||||
hasContextScope('summary') ? 'context-scope-block-selected' : '',
|
props.hasContextScope('summary') ? 'context-scope-block-selected' : '',
|
||||||
]"
|
]"
|
||||||
@click="toggleContextScope('summary')"
|
@click="props.toggleContextScope('summary')"
|
||||||
>
|
>
|
||||||
<span v-if="contextPickerEnabled" class="context-scope-label">Summary</span>
|
<span v-if="props.contextPickerEnabled" class="context-scope-label">Summary</span>
|
||||||
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-base-content/60">Summary</p>
|
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-base-content/60">Summary</p>
|
||||||
<div
|
<div
|
||||||
v-if="activeReviewContactDiff && selectedWorkspaceContact && activeReviewContactDiff.contactId === selectedWorkspaceContact.id"
|
v-if="props.activeReviewContactDiff && props.selectedWorkspaceContact && props.activeReviewContactDiff.contactId === props.selectedWorkspaceContact.id"
|
||||||
class="mb-2 rounded-xl border border-primary/35 bg-primary/5 p-2"
|
class="mb-2 rounded-xl border border-primary/35 bg-primary/5 p-2"
|
||||||
>
|
>
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-wide text-primary/80">Review diff</p>
|
<p class="text-[11px] font-semibold uppercase tracking-wide text-primary/80">Review diff</p>
|
||||||
<p class="mt-1 text-[11px] text-base-content/65">Before</p>
|
<p class="mt-1 text-[11px] text-base-content/65">Before</p>
|
||||||
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-base-300/70 bg-base-100 px-2 py-1.5 text-[11px] leading-relaxed text-base-content/65 line-through">{{ activeReviewContactDiff.before || "Empty" }}</pre>
|
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-base-300/70 bg-base-100 px-2 py-1.5 text-[11px] leading-relaxed text-base-content/65 line-through">{{ props.activeReviewContactDiff.before || "Empty" }}</pre>
|
||||||
<p class="mt-2 text-[11px] text-base-content/65">After</p>
|
<p class="mt-2 text-[11px] text-base-content/65">After</p>
|
||||||
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-success/40 bg-success/10 px-2 py-1.5 text-[11px] leading-relaxed text-base-content">{{ activeReviewContactDiff.after || "Empty" }}</pre>
|
<pre class="mt-1 whitespace-pre-wrap rounded-lg border border-success/40 bg-success/10 px-2 py-1.5 text-[11px] leading-relaxed text-base-content">{{ props.activeReviewContactDiff.after || "Empty" }}</pre>
|
||||||
</div>
|
</div>
|
||||||
<ContactCollaborativeEditor
|
<ContactCollaborativeEditor
|
||||||
v-if="selectedWorkspaceContact"
|
v-if="props.selectedWorkspaceContact"
|
||||||
:key="`contact-summary-${selectedWorkspaceContact.id}`"
|
:key="`contact-summary-${props.selectedWorkspaceContact.id}`"
|
||||||
v-model="selectedWorkspaceContact.description"
|
v-model="props.selectedWorkspaceContact.description"
|
||||||
:room="`crm-contact-${selectedWorkspaceContact.id}`"
|
:room="`crm-contact-${props.selectedWorkspaceContact.id}`"
|
||||||
placeholder="Contact summary..."
|
placeholder="Contact summary..."
|
||||||
:plain="true"
|
:plain="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref, computed, watch, type ComputedRef, type Ref } from "vue";
|
import { ref, computed, watch, type ComputedRef, type Ref } from "vue";
|
||||||
import { useQuery } from "@vue/apollo-composable";
|
import { useQuery, useMutation } from "@vue/apollo-composable";
|
||||||
import { DealsQueryDocument } from "~~/graphql/generated";
|
import { CreateDealMutationDocument, DealsQueryDocument, UpdateDealMutationDocument } from "~~/graphql/generated";
|
||||||
|
|
||||||
import type { Contact } from "~/composables/useContacts";
|
import type { Contact } from "~/composables/useContacts";
|
||||||
import type { CalendarEvent } from "~/composables/useCalendar";
|
import type { CalendarEvent } from "~/composables/useCalendar";
|
||||||
@@ -22,6 +22,7 @@ export type Deal = {
|
|||||||
title: string;
|
title: string;
|
||||||
stage: string;
|
stage: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
|
paidAmount: string;
|
||||||
nextStep: string;
|
nextStep: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
currentStepId: string;
|
currentStepId: string;
|
||||||
@@ -29,6 +30,26 @@ export type Deal = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
|
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
|
||||||
|
const DEFAULT_DEAL_STAGES = ["Новый", "Квалификация", "Переговоры", "Согласование", "Выиграно", "Проиграно"];
|
||||||
|
|
||||||
|
function parseMoneyInput(value: unknown, fieldLabel: "Сумма" | "Оплачено") {
|
||||||
|
const normalized = safeTrim(value).replace(/\s+/g, "").replace(",", ".");
|
||||||
|
if (!normalized) return null;
|
||||||
|
if (!/^\d+(\.\d+)?$/.test(normalized)) {
|
||||||
|
throw new Error(`${fieldLabel} должно быть числом`);
|
||||||
|
}
|
||||||
|
const num = Number(normalized);
|
||||||
|
if (!Number.isFinite(num)) {
|
||||||
|
throw new Error(`${fieldLabel} заполнено некорректно`);
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(num)) {
|
||||||
|
throw new Error(`${fieldLabel} должно быть целым числом`);
|
||||||
|
}
|
||||||
|
if (num < 0) {
|
||||||
|
throw new Error(`${fieldLabel} не может быть отрицательным`);
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
export function useDeals(opts: {
|
export function useDeals(opts: {
|
||||||
apolloAuthReady: ComputedRef<boolean>;
|
apolloAuthReady: ComputedRef<boolean>;
|
||||||
@@ -40,6 +61,12 @@ export function useDeals(opts: {
|
|||||||
null,
|
null,
|
||||||
{ enabled: opts.apolloAuthReady },
|
{ enabled: opts.apolloAuthReady },
|
||||||
);
|
);
|
||||||
|
const { mutate: doUpdateDeal, loading: dealUpdateLoading } = useMutation(UpdateDealMutationDocument, {
|
||||||
|
refetchQueries: [{ query: DealsQueryDocument }],
|
||||||
|
});
|
||||||
|
const { mutate: doCreateDeal, loading: dealCreateLoading } = useMutation(CreateDealMutationDocument, {
|
||||||
|
refetchQueries: [{ query: DealsQueryDocument }],
|
||||||
|
});
|
||||||
|
|
||||||
const deals = ref<Deal[]>([]);
|
const deals = ref<Deal[]>([]);
|
||||||
const selectedDealId = ref(deals.value[0]?.id ?? "");
|
const selectedDealId = ref(deals.value[0]?.id ?? "");
|
||||||
@@ -206,6 +233,136 @@ export function useDeals(opts: {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dealStageOptions = computed(() => {
|
||||||
|
const unique = new Set<string>();
|
||||||
|
for (const stage of DEFAULT_DEAL_STAGES) unique.add(stage);
|
||||||
|
for (const deal of deals.value) {
|
||||||
|
const stage = safeTrim(deal.stage);
|
||||||
|
if (stage) unique.add(stage);
|
||||||
|
}
|
||||||
|
return [...unique];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateDealDetails(input: {
|
||||||
|
dealId: string;
|
||||||
|
stage: string;
|
||||||
|
amount: string;
|
||||||
|
paidAmount: string;
|
||||||
|
}) {
|
||||||
|
const dealId = safeTrim(input.dealId);
|
||||||
|
if (!dealId) throw new Error("Не выбрана сделка");
|
||||||
|
const current = deals.value.find((deal) => deal.id === dealId);
|
||||||
|
if (!current) throw new Error("Сделка не найдена");
|
||||||
|
|
||||||
|
const stage = safeTrim(input.stage);
|
||||||
|
if (!stage) throw new Error("Статус обязателен");
|
||||||
|
|
||||||
|
const amount = parseMoneyInput(input.amount, "Сумма");
|
||||||
|
const paidAmount = parseMoneyInput(input.paidAmount, "Оплачено");
|
||||||
|
if (amount === null && paidAmount !== null) {
|
||||||
|
throw new Error("Нельзя указать 'Оплачено' без поля 'Сумма'");
|
||||||
|
}
|
||||||
|
if (amount !== null && paidAmount !== null && paidAmount > amount) {
|
||||||
|
throw new Error("'Оплачено' не может быть больше 'Сумма'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: {
|
||||||
|
id: string;
|
||||||
|
stage?: string;
|
||||||
|
amount?: number | null;
|
||||||
|
paidAmount?: number | null;
|
||||||
|
} = { id: dealId };
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
if (stage !== current.stage) {
|
||||||
|
payload.stage = stage;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (amount !== parseMoneyInput(current.amount, "Сумма")) {
|
||||||
|
payload.amount = amount;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (paidAmount !== parseMoneyInput(current.paidAmount, "Оплачено")) {
|
||||||
|
payload.paidAmount = paidAmount;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) return false;
|
||||||
|
|
||||||
|
const res = await doUpdateDeal({ input: payload });
|
||||||
|
const updated = res?.data?.updateDeal as Deal | undefined;
|
||||||
|
if (updated) {
|
||||||
|
deals.value = deals.value.map((deal) => (deal.id === updated.id ? updated : deal));
|
||||||
|
} else {
|
||||||
|
await refetchDeals();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDealForContact(input: {
|
||||||
|
contactId: string;
|
||||||
|
title: string;
|
||||||
|
stage: string;
|
||||||
|
amount: string;
|
||||||
|
paidAmount: string;
|
||||||
|
nextStep?: string;
|
||||||
|
summary?: string;
|
||||||
|
}) {
|
||||||
|
const contactId = safeTrim(input.contactId);
|
||||||
|
if (!contactId) throw new Error("Не выбран контакт");
|
||||||
|
const title = safeTrim(input.title);
|
||||||
|
if (!title) throw new Error("Название сделки обязательно");
|
||||||
|
const stage = safeTrim(input.stage) || DEFAULT_DEAL_STAGES[0]!;
|
||||||
|
if (!stage) throw new Error("Статус обязателен");
|
||||||
|
|
||||||
|
const amount = parseMoneyInput(input.amount, "Сумма");
|
||||||
|
const paidAmount = parseMoneyInput(input.paidAmount, "Оплачено");
|
||||||
|
if (amount === null && paidAmount !== null) {
|
||||||
|
throw new Error("Нельзя указать 'Оплачено' без поля 'Сумма'");
|
||||||
|
}
|
||||||
|
if (amount !== null && paidAmount !== null && paidAmount > amount) {
|
||||||
|
throw new Error("'Оплачено' не может быть больше 'Сумма'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: {
|
||||||
|
contactId: string;
|
||||||
|
title: string;
|
||||||
|
stage: string;
|
||||||
|
amount?: number | null;
|
||||||
|
paidAmount?: number | null;
|
||||||
|
nextStep?: string;
|
||||||
|
summary?: string;
|
||||||
|
} = {
|
||||||
|
contactId,
|
||||||
|
title,
|
||||||
|
stage,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.amount.trim()) payload.amount = amount;
|
||||||
|
if (input.paidAmount.trim()) payload.paidAmount = paidAmount;
|
||||||
|
const nextStep = safeTrim(input.nextStep);
|
||||||
|
if (nextStep) payload.nextStep = nextStep;
|
||||||
|
const summary = safeTrim(input.summary);
|
||||||
|
if (summary) payload.summary = summary;
|
||||||
|
|
||||||
|
const res = await doCreateDeal({ input: payload });
|
||||||
|
const created = res?.data?.createDeal as Deal | undefined;
|
||||||
|
if (created) {
|
||||||
|
deals.value = [created, ...deals.value.filter((deal) => deal.id !== created.id)];
|
||||||
|
selectedDealId.value = created.id;
|
||||||
|
selectedDealStepsExpanded.value = false;
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refetchDeals();
|
||||||
|
const refreshed = deals.value.find((deal) => deal.title === title && deal.stage === stage && deal.contact);
|
||||||
|
if (refreshed) {
|
||||||
|
selectedDealId.value = refreshed.id;
|
||||||
|
selectedDealStepsExpanded.value = false;
|
||||||
|
}
|
||||||
|
return refreshed ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deals,
|
deals,
|
||||||
selectedDealId,
|
selectedDealId,
|
||||||
@@ -221,6 +378,11 @@ export function useDeals(opts: {
|
|||||||
formatDealDeadline,
|
formatDealDeadline,
|
||||||
isDealStepDone,
|
isDealStepDone,
|
||||||
formatDealStepMeta,
|
formatDealStepMeta,
|
||||||
|
dealStageOptions,
|
||||||
|
createDealForContact,
|
||||||
|
dealCreateLoading,
|
||||||
|
updateDealDetails,
|
||||||
|
dealUpdateLoading,
|
||||||
refetchDeals,
|
refetchDeals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,16 @@ export type CreateCommunicationInput = {
|
|||||||
transcript?: InputMaybe<Array<Scalars['String']['input']>>;
|
transcript?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateDealInput = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
contactId: Scalars['ID']['input'];
|
||||||
|
nextStep?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
paidAmount?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
stage?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
summary?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
title: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateWorkspaceDocumentInput = {
|
export type CreateWorkspaceDocumentInput = {
|
||||||
body?: InputMaybe<Scalars['String']['input']>;
|
body?: InputMaybe<Scalars['String']['input']>;
|
||||||
owner?: InputMaybe<Scalars['String']['input']>;
|
owner?: InputMaybe<Scalars['String']['input']>;
|
||||||
@@ -155,6 +165,7 @@ export type Deal = {
|
|||||||
currentStepId: Scalars['String']['output'];
|
currentStepId: Scalars['String']['output'];
|
||||||
id: Scalars['ID']['output'];
|
id: Scalars['ID']['output'];
|
||||||
nextStep: Scalars['String']['output'];
|
nextStep: Scalars['String']['output'];
|
||||||
|
paidAmount: Scalars['String']['output'];
|
||||||
stage: Scalars['String']['output'];
|
stage: Scalars['String']['output'];
|
||||||
steps: Array<DealStep>;
|
steps: Array<DealStep>;
|
||||||
summary: Scalars['String']['output'];
|
summary: Scalars['String']['output'];
|
||||||
@@ -218,6 +229,7 @@ export type Mutation = {
|
|||||||
createCalendarEvent: CalendarEvent;
|
createCalendarEvent: CalendarEvent;
|
||||||
createChatConversation: Conversation;
|
createChatConversation: Conversation;
|
||||||
createCommunication: MutationWithIdResult;
|
createCommunication: MutationWithIdResult;
|
||||||
|
createDeal: Deal;
|
||||||
createWorkspaceDocument: WorkspaceDocument;
|
createWorkspaceDocument: WorkspaceDocument;
|
||||||
deleteWorkspaceDocument: MutationWithIdResult;
|
deleteWorkspaceDocument: MutationWithIdResult;
|
||||||
logPilotNote: MutationResult;
|
logPilotNote: MutationResult;
|
||||||
@@ -231,6 +243,7 @@ export type Mutation = {
|
|||||||
setContactInboxHidden: MutationResult;
|
setContactInboxHidden: MutationResult;
|
||||||
toggleContactPin: PinToggleResult;
|
toggleContactPin: PinToggleResult;
|
||||||
updateCommunicationTranscript: MutationWithIdResult;
|
updateCommunicationTranscript: MutationWithIdResult;
|
||||||
|
updateDeal: Deal;
|
||||||
updateFeedDecision: MutationWithIdResult;
|
updateFeedDecision: MutationWithIdResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,6 +273,11 @@ export type MutationcreateCommunicationArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationcreateDealArgs = {
|
||||||
|
input: CreateDealInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationcreateWorkspaceDocumentArgs = {
|
export type MutationcreateWorkspaceDocumentArgs = {
|
||||||
input: CreateWorkspaceDocumentInput;
|
input: CreateWorkspaceDocumentInput;
|
||||||
};
|
};
|
||||||
@@ -320,6 +338,11 @@ export type MutationupdateCommunicationTranscriptArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationupdateDealArgs = {
|
||||||
|
input: UpdateDealInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationupdateFeedDecisionArgs = {
|
export type MutationupdateFeedDecisionArgs = {
|
||||||
decision: Scalars['String']['input'];
|
decision: Scalars['String']['input'];
|
||||||
decisionNote?: InputMaybe<Scalars['String']['input']>;
|
decisionNote?: InputMaybe<Scalars['String']['input']>;
|
||||||
@@ -411,6 +434,13 @@ export type QuerygetClientTimelineArgs = {
|
|||||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateDealInput = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
id: Scalars['ID']['input'];
|
||||||
|
paidAmount?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
stage?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkspaceDocument = {
|
export type WorkspaceDocument = {
|
||||||
__typename?: 'WorkspaceDocument';
|
__typename?: 'WorkspaceDocument';
|
||||||
body: Scalars['String']['output'];
|
body: Scalars['String']['output'];
|
||||||
@@ -496,6 +526,13 @@ export type CreateCommunicationMutationMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type CreateCommunicationMutationMutation = { __typename?: 'Mutation', createCommunication: { __typename?: 'MutationWithIdResult', ok: boolean, id: string } };
|
export type CreateCommunicationMutationMutation = { __typename?: 'Mutation', createCommunication: { __typename?: 'MutationWithIdResult', ok: boolean, id: string } };
|
||||||
|
|
||||||
|
export type CreateDealMutationMutationVariables = Exact<{
|
||||||
|
input: CreateDealInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateDealMutationMutation = { __typename?: 'Mutation', createDeal: { __typename?: 'Deal', id: string, contact: string, title: string, stage: string, amount: string, paidAmount: string, nextStep: string, summary: string, currentStepId: string, steps: Array<{ __typename?: 'DealStep', id: string, title: string, description: string, status: string, dueAt: string, order: number, completedAt: string }> } };
|
||||||
|
|
||||||
export type CreateWorkspaceDocumentMutationVariables = Exact<{
|
export type CreateWorkspaceDocumentMutationVariables = Exact<{
|
||||||
input: CreateWorkspaceDocumentInput;
|
input: CreateWorkspaceDocumentInput;
|
||||||
}>;
|
}>;
|
||||||
@@ -506,7 +543,7 @@ export type CreateWorkspaceDocumentMutation = { __typename?: 'Mutation', createW
|
|||||||
export type DealsQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
export type DealsQueryQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type DealsQueryQuery = { __typename?: 'Query', deals: Array<{ __typename?: 'Deal', id: string, contact: string, title: string, stage: string, amount: string, nextStep: string, summary: string, currentStepId: string, steps: Array<{ __typename?: 'DealStep', id: string, title: string, description: string, status: string, dueAt: string, order: number, completedAt: string }> }> };
|
export type DealsQueryQuery = { __typename?: 'Query', deals: Array<{ __typename?: 'Deal', id: string, contact: string, title: string, stage: string, amount: string, paidAmount: string, nextStep: string, summary: string, currentStepId: string, steps: Array<{ __typename?: 'DealStep', id: string, title: string, description: string, status: string, dueAt: string, order: number, completedAt: string }> }> };
|
||||||
|
|
||||||
export type DeleteWorkspaceDocumentMutationVariables = Exact<{
|
export type DeleteWorkspaceDocumentMutationVariables = Exact<{
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
@@ -621,6 +658,13 @@ export type UpdateCommunicationTranscriptMutationMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type UpdateCommunicationTranscriptMutationMutation = { __typename?: 'Mutation', updateCommunicationTranscript: { __typename?: 'MutationWithIdResult', ok: boolean, id: string } };
|
export type UpdateCommunicationTranscriptMutationMutation = { __typename?: 'Mutation', updateCommunicationTranscript: { __typename?: 'MutationWithIdResult', ok: boolean, id: string } };
|
||||||
|
|
||||||
|
export type UpdateDealMutationMutationVariables = Exact<{
|
||||||
|
input: UpdateDealInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UpdateDealMutationMutation = { __typename?: 'Mutation', updateDeal: { __typename?: 'Deal', id: string, contact: string, title: string, stage: string, amount: string, paidAmount: string, nextStep: string, summary: string, currentStepId: string, steps: Array<{ __typename?: 'DealStep', id: string, title: string, description: string, status: string, dueAt: string, order: number, completedAt: string }> } };
|
||||||
|
|
||||||
export type UpdateFeedDecisionMutationMutationVariables = Exact<{
|
export type UpdateFeedDecisionMutationMutationVariables = Exact<{
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
decision: Scalars['String']['input'];
|
decision: Scalars['String']['input'];
|
||||||
@@ -1066,6 +1110,52 @@ export function useCreateCommunicationMutationMutation(options: VueApolloComposa
|
|||||||
return VueApolloComposable.useMutation<CreateCommunicationMutationMutation, CreateCommunicationMutationMutationVariables>(CreateCommunicationMutationDocument, options);
|
return VueApolloComposable.useMutation<CreateCommunicationMutationMutation, CreateCommunicationMutationMutationVariables>(CreateCommunicationMutationDocument, options);
|
||||||
}
|
}
|
||||||
export type CreateCommunicationMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<CreateCommunicationMutationMutation, CreateCommunicationMutationMutationVariables>;
|
export type CreateCommunicationMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<CreateCommunicationMutationMutation, CreateCommunicationMutationMutationVariables>;
|
||||||
|
export const CreateDealMutationDocument = gql`
|
||||||
|
mutation CreateDealMutation($input: CreateDealInput!) {
|
||||||
|
createDeal(input: $input) {
|
||||||
|
id
|
||||||
|
contact
|
||||||
|
title
|
||||||
|
stage
|
||||||
|
amount
|
||||||
|
paidAmount
|
||||||
|
nextStep
|
||||||
|
summary
|
||||||
|
currentStepId
|
||||||
|
steps {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
dueAt
|
||||||
|
order
|
||||||
|
completedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useCreateDealMutationMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useCreateDealMutationMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useCreateDealMutationMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useCreateDealMutationMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useCreateDealMutationMutation(options: VueApolloComposable.UseMutationOptions<CreateDealMutationMutation, CreateDealMutationMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<CreateDealMutationMutation, CreateDealMutationMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<CreateDealMutationMutation, CreateDealMutationMutationVariables>(CreateDealMutationDocument, options);
|
||||||
|
}
|
||||||
|
export type CreateDealMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<CreateDealMutationMutation, CreateDealMutationMutationVariables>;
|
||||||
export const CreateWorkspaceDocumentDocument = gql`
|
export const CreateWorkspaceDocumentDocument = gql`
|
||||||
mutation CreateWorkspaceDocument($input: CreateWorkspaceDocumentInput!) {
|
mutation CreateWorkspaceDocument($input: CreateWorkspaceDocumentInput!) {
|
||||||
createWorkspaceDocument(input: $input) {
|
createWorkspaceDocument(input: $input) {
|
||||||
@@ -1110,6 +1200,7 @@ export const DealsQueryDocument = gql`
|
|||||||
title
|
title
|
||||||
stage
|
stage
|
||||||
amount
|
amount
|
||||||
|
paidAmount
|
||||||
nextStep
|
nextStep
|
||||||
summary
|
summary
|
||||||
currentStepId
|
currentStepId
|
||||||
@@ -1720,6 +1811,52 @@ export function useUpdateCommunicationTranscriptMutationMutation(options: VueApo
|
|||||||
return VueApolloComposable.useMutation<UpdateCommunicationTranscriptMutationMutation, UpdateCommunicationTranscriptMutationMutationVariables>(UpdateCommunicationTranscriptMutationDocument, options);
|
return VueApolloComposable.useMutation<UpdateCommunicationTranscriptMutationMutation, UpdateCommunicationTranscriptMutationMutationVariables>(UpdateCommunicationTranscriptMutationDocument, options);
|
||||||
}
|
}
|
||||||
export type UpdateCommunicationTranscriptMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<UpdateCommunicationTranscriptMutationMutation, UpdateCommunicationTranscriptMutationMutationVariables>;
|
export type UpdateCommunicationTranscriptMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<UpdateCommunicationTranscriptMutationMutation, UpdateCommunicationTranscriptMutationMutationVariables>;
|
||||||
|
export const UpdateDealMutationDocument = gql`
|
||||||
|
mutation UpdateDealMutation($input: UpdateDealInput!) {
|
||||||
|
updateDeal(input: $input) {
|
||||||
|
id
|
||||||
|
contact
|
||||||
|
title
|
||||||
|
stage
|
||||||
|
amount
|
||||||
|
paidAmount
|
||||||
|
nextStep
|
||||||
|
summary
|
||||||
|
currentStepId
|
||||||
|
steps {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
dueAt
|
||||||
|
order
|
||||||
|
completedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useUpdateDealMutationMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useUpdateDealMutationMutation` within a Vue component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useUpdateDealMutationMutation` returns an object that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - Several other properties: https://v4.apollo.vuejs.org/api/use-mutation.html#return
|
||||||
|
*
|
||||||
|
* @param options that will be passed into the mutation, supported options are listed on: https://v4.apollo.vuejs.org/guide-composable/mutation.html#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { mutate, loading, error, onDone } = useUpdateDealMutationMutation({
|
||||||
|
* variables: {
|
||||||
|
* input: // value for 'input'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useUpdateDealMutationMutation(options: VueApolloComposable.UseMutationOptions<UpdateDealMutationMutation, UpdateDealMutationMutationVariables> | ReactiveFunction<VueApolloComposable.UseMutationOptions<UpdateDealMutationMutation, UpdateDealMutationMutationVariables>> = {}) {
|
||||||
|
return VueApolloComposable.useMutation<UpdateDealMutationMutation, UpdateDealMutationMutationVariables>(UpdateDealMutationDocument, options);
|
||||||
|
}
|
||||||
|
export type UpdateDealMutationMutationCompositionFunctionResult = VueApolloComposable.UseMutationReturn<UpdateDealMutationMutation, UpdateDealMutationMutationVariables>;
|
||||||
export const UpdateFeedDecisionMutationDocument = gql`
|
export const UpdateFeedDecisionMutationDocument = gql`
|
||||||
mutation UpdateFeedDecisionMutation($id: ID!, $decision: String!, $decisionNote: String) {
|
mutation UpdateFeedDecisionMutation($id: ID!, $decision: String!, $decisionNote: String) {
|
||||||
updateFeedDecision(id: $id, decision: $decision, decisionNote: $decisionNote) {
|
updateFeedDecision(id: $id, decision: $decision, decisionNote: $decisionNote) {
|
||||||
|
|||||||
22
frontend/graphql/operations/create-deal.graphql
Normal file
22
frontend/graphql/operations/create-deal.graphql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
mutation CreateDealMutation($input: CreateDealInput!) {
|
||||||
|
createDeal(input: $input) {
|
||||||
|
id
|
||||||
|
contact
|
||||||
|
title
|
||||||
|
stage
|
||||||
|
amount
|
||||||
|
paidAmount
|
||||||
|
nextStep
|
||||||
|
summary
|
||||||
|
currentStepId
|
||||||
|
steps {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
dueAt
|
||||||
|
order
|
||||||
|
completedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ query DealsQuery {
|
|||||||
title
|
title
|
||||||
stage
|
stage
|
||||||
amount
|
amount
|
||||||
|
paidAmount
|
||||||
nextStep
|
nextStep
|
||||||
summary
|
summary
|
||||||
currentStepId
|
currentStepId
|
||||||
|
|||||||
22
frontend/graphql/operations/update-deal.graphql
Normal file
22
frontend/graphql/operations/update-deal.graphql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
mutation UpdateDealMutation($input: UpdateDealInput!) {
|
||||||
|
updateDeal(input: $input) {
|
||||||
|
id
|
||||||
|
contact
|
||||||
|
title
|
||||||
|
stage
|
||||||
|
amount
|
||||||
|
paidAmount
|
||||||
|
nextStep
|
||||||
|
summary
|
||||||
|
currentStepId
|
||||||
|
steps {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
dueAt
|
||||||
|
order
|
||||||
|
completedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ type Mutation {
|
|||||||
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
|
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
|
||||||
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
||||||
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
||||||
|
createDeal(input: CreateDealInput!): Deal!
|
||||||
|
updateDeal(input: UpdateDealInput!): Deal!
|
||||||
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
||||||
deleteWorkspaceDocument(id: ID!): MutationWithIdResult!
|
deleteWorkspaceDocument(id: ID!): MutationWithIdResult!
|
||||||
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
||||||
@@ -90,6 +92,23 @@ input CreateWorkspaceDocumentInput {
|
|||||||
body: String
|
body: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CreateDealInput {
|
||||||
|
contactId: ID!
|
||||||
|
title: String!
|
||||||
|
stage: String
|
||||||
|
amount: Int
|
||||||
|
paidAmount: Int
|
||||||
|
nextStep: String
|
||||||
|
summary: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateDealInput {
|
||||||
|
id: ID!
|
||||||
|
stage: String
|
||||||
|
amount: Int
|
||||||
|
paidAmount: Int
|
||||||
|
}
|
||||||
|
|
||||||
type MePayload {
|
type MePayload {
|
||||||
user: MeUser!
|
user: MeUser!
|
||||||
team: MeTeam!
|
team: MeTeam!
|
||||||
@@ -228,6 +247,7 @@ type Deal {
|
|||||||
title: String!
|
title: String!
|
||||||
stage: String!
|
stage: String!
|
||||||
amount: String!
|
amount: String!
|
||||||
|
paidAmount: String!
|
||||||
nextStep: String!
|
nextStep: String!
|
||||||
summary: String!
|
summary: String!
|
||||||
currentStepId: String!
|
currentStepId: String!
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Deal" ADD COLUMN "paidAmount" INTEGER;
|
||||||
|
|
||||||
@@ -342,6 +342,7 @@ model Deal {
|
|||||||
title String
|
title String
|
||||||
stage String
|
stage String
|
||||||
amount Int?
|
amount Int?
|
||||||
|
paidAmount Int?
|
||||||
nextStep String?
|
nextStep String?
|
||||||
summary String?
|
summary String?
|
||||||
currentStepId String?
|
currentStepId String?
|
||||||
|
|||||||
@@ -726,12 +726,36 @@ async function getDeals(auth: AuthContext | null) {
|
|||||||
take: 500,
|
take: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
return dealsRaw.map((d) => ({
|
return dealsRaw.map((d) => mapDealRecord(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDealRecord(d: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
stage: string;
|
||||||
|
amount: number | null;
|
||||||
|
paidAmount: number | null;
|
||||||
|
nextStep: string | null;
|
||||||
|
summary: string | null;
|
||||||
|
currentStepId: string | null;
|
||||||
|
contact: { name: string };
|
||||||
|
steps: Array<{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
status: string;
|
||||||
|
dueAt: Date | null;
|
||||||
|
order: number;
|
||||||
|
completedAt: Date | null;
|
||||||
|
}>;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
id: d.id,
|
id: d.id,
|
||||||
contact: d.contact.name,
|
contact: d.contact.name,
|
||||||
title: d.title,
|
title: d.title,
|
||||||
stage: d.stage,
|
stage: d.stage,
|
||||||
amount: d.amount ? String(d.amount) : "",
|
amount: d.amount !== null ? String(d.amount) : "",
|
||||||
|
paidAmount: d.paidAmount !== null ? String(d.paidAmount) : "",
|
||||||
nextStep: d.nextStep ?? "",
|
nextStep: d.nextStep ?? "",
|
||||||
summary: d.summary ?? "",
|
summary: d.summary ?? "",
|
||||||
currentStepId: d.currentStepId ?? "",
|
currentStepId: d.currentStepId ?? "",
|
||||||
@@ -744,7 +768,7 @@ async function getDeals(auth: AuthContext | null) {
|
|||||||
order: step.order,
|
order: step.order,
|
||||||
completedAt: step.completedAt?.toISOString() ?? "",
|
completedAt: step.completedAt?.toISOString() ?? "",
|
||||||
})),
|
})),
|
||||||
}));
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFeed(auth: AuthContext | null) {
|
async function getFeed(auth: AuthContext | null) {
|
||||||
@@ -1216,6 +1240,143 @@ async function archiveCalendarEvent(auth: AuthContext | null, input: { id: strin
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseOptionalDealMoney(value: unknown, field: "amount" | "paidAmount") {
|
||||||
|
if (value === null) return null;
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isFinite(num)) throw new Error(`${field} is invalid`);
|
||||||
|
if (!Number.isInteger(num)) throw new Error(`${field} must be an integer`);
|
||||||
|
if (num < 0) throw new Error(`${field} must be greater than or equal to 0`);
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDeal(auth: AuthContext | null, input: {
|
||||||
|
id: string;
|
||||||
|
stage?: string | null;
|
||||||
|
amount?: number | null;
|
||||||
|
paidAmount?: number | null;
|
||||||
|
}) {
|
||||||
|
const ctx = requireAuth(auth);
|
||||||
|
const id = String(input?.id ?? "").trim();
|
||||||
|
if (!id) throw new Error("id is required");
|
||||||
|
|
||||||
|
const existing = await prisma.deal.findFirst({
|
||||||
|
where: { id, teamId: ctx.teamId },
|
||||||
|
select: { id: true, amount: true, paidAmount: true },
|
||||||
|
});
|
||||||
|
if (!existing) throw new Error("deal not found");
|
||||||
|
|
||||||
|
const data: {
|
||||||
|
stage?: string;
|
||||||
|
amount?: number | null;
|
||||||
|
paidAmount?: number | null;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
let nextAmount = existing.amount;
|
||||||
|
let nextPaidAmount = existing.paidAmount;
|
||||||
|
|
||||||
|
if ("stage" in input) {
|
||||||
|
const stage = String(input.stage ?? "").trim();
|
||||||
|
if (!stage) throw new Error("stage is required");
|
||||||
|
data.stage = stage;
|
||||||
|
}
|
||||||
|
if ("amount" in input) {
|
||||||
|
const amount = parseOptionalDealMoney(input.amount, "amount");
|
||||||
|
data.amount = amount;
|
||||||
|
nextAmount = amount;
|
||||||
|
}
|
||||||
|
if ("paidAmount" in input) {
|
||||||
|
const paidAmount = parseOptionalDealMoney(input.paidAmount, "paidAmount");
|
||||||
|
data.paidAmount = paidAmount;
|
||||||
|
nextPaidAmount = paidAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextAmount === null && nextPaidAmount !== null) {
|
||||||
|
throw new Error("paidAmount requires amount");
|
||||||
|
}
|
||||||
|
if (nextAmount !== null && nextPaidAmount !== null && nextPaidAmount > nextAmount) {
|
||||||
|
throw new Error("paidAmount cannot exceed amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(data).length) {
|
||||||
|
const current = await prisma.deal.findFirst({
|
||||||
|
where: { id: existing.id, teamId: ctx.teamId },
|
||||||
|
include: {
|
||||||
|
contact: { select: { name: true } },
|
||||||
|
steps: { orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!current) throw new Error("deal not found");
|
||||||
|
return mapDealRecord(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.deal.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data,
|
||||||
|
include: {
|
||||||
|
contact: { select: { name: true } },
|
||||||
|
steps: { orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapDealRecord(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDeal(auth: AuthContext | null, input: {
|
||||||
|
contactId: string;
|
||||||
|
title: string;
|
||||||
|
stage?: string | null;
|
||||||
|
amount?: number | null;
|
||||||
|
paidAmount?: number | null;
|
||||||
|
nextStep?: string | null;
|
||||||
|
summary?: string | null;
|
||||||
|
}) {
|
||||||
|
const ctx = requireAuth(auth);
|
||||||
|
const contactId = String(input?.contactId ?? "").trim();
|
||||||
|
const title = String(input?.title ?? "").trim();
|
||||||
|
const stage = String(input?.stage ?? "").trim() || "Новый";
|
||||||
|
const nextStep = String(input?.nextStep ?? "").trim() || null;
|
||||||
|
const summary = String(input?.summary ?? "").trim() || null;
|
||||||
|
|
||||||
|
if (!contactId) throw new Error("contactId is required");
|
||||||
|
if (!title) throw new Error("title is required");
|
||||||
|
if (!stage) throw new Error("stage is required");
|
||||||
|
|
||||||
|
const contact = await prisma.contact.findFirst({
|
||||||
|
where: { id: contactId, teamId: ctx.teamId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!contact) throw new Error("contact not found");
|
||||||
|
|
||||||
|
const amount = "amount" in input ? parseOptionalDealMoney(input.amount ?? null, "amount") : null;
|
||||||
|
const paidAmount = "paidAmount" in input ? parseOptionalDealMoney(input.paidAmount ?? null, "paidAmount") : null;
|
||||||
|
|
||||||
|
if (amount === null && paidAmount !== null) {
|
||||||
|
throw new Error("paidAmount requires amount");
|
||||||
|
}
|
||||||
|
if (amount !== null && paidAmount !== null && paidAmount > amount) {
|
||||||
|
throw new Error("paidAmount cannot exceed amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await prisma.deal.create({
|
||||||
|
data: {
|
||||||
|
teamId: ctx.teamId,
|
||||||
|
contactId: contact.id,
|
||||||
|
title,
|
||||||
|
stage,
|
||||||
|
amount,
|
||||||
|
paidAmount,
|
||||||
|
nextStep,
|
||||||
|
summary,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
contact: { select: { name: true } },
|
||||||
|
steps: { orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapDealRecord(created);
|
||||||
|
}
|
||||||
|
|
||||||
async function createCommunication(auth: AuthContext | null, input: {
|
async function createCommunication(auth: AuthContext | null, input: {
|
||||||
contact: string;
|
contact: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
@@ -1889,6 +2050,8 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
|
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
|
||||||
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
archiveCalendarEvent(input: ArchiveCalendarEventInput!): CalendarEvent!
|
||||||
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
||||||
|
createDeal(input: CreateDealInput!): Deal!
|
||||||
|
updateDeal(input: UpdateDealInput!): Deal!
|
||||||
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
createWorkspaceDocument(input: CreateWorkspaceDocumentInput!): WorkspaceDocument!
|
||||||
deleteWorkspaceDocument(id: ID!): MutationWithIdResult!
|
deleteWorkspaceDocument(id: ID!): MutationWithIdResult!
|
||||||
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
updateCommunicationTranscript(id: ID!, transcript: [String!]!): MutationWithIdResult!
|
||||||
@@ -1951,6 +2114,23 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
body: String
|
body: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CreateDealInput {
|
||||||
|
contactId: ID!
|
||||||
|
title: String!
|
||||||
|
stage: String
|
||||||
|
amount: Int
|
||||||
|
paidAmount: Int
|
||||||
|
nextStep: String
|
||||||
|
summary: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateDealInput {
|
||||||
|
id: ID!
|
||||||
|
stage: String
|
||||||
|
amount: Int
|
||||||
|
paidAmount: Int
|
||||||
|
}
|
||||||
|
|
||||||
type MePayload {
|
type MePayload {
|
||||||
user: MeUser!
|
user: MeUser!
|
||||||
team: MeTeam!
|
team: MeTeam!
|
||||||
@@ -2089,6 +2269,7 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
title: String!
|
title: String!
|
||||||
stage: String!
|
stage: String!
|
||||||
amount: String!
|
amount: String!
|
||||||
|
paidAmount: String!
|
||||||
nextStep: String!
|
nextStep: String!
|
||||||
summary: String!
|
summary: String!
|
||||||
currentStepId: String!
|
currentStepId: String!
|
||||||
@@ -2216,6 +2397,26 @@ export const crmGraphqlRoot = {
|
|||||||
context: GraphQLContext,
|
context: GraphQLContext,
|
||||||
) => createCommunication(context.auth, args.input),
|
) => createCommunication(context.auth, args.input),
|
||||||
|
|
||||||
|
createDeal: async (
|
||||||
|
args: {
|
||||||
|
input: {
|
||||||
|
contactId: string;
|
||||||
|
title: string;
|
||||||
|
stage?: string;
|
||||||
|
amount?: number | null;
|
||||||
|
paidAmount?: number | null;
|
||||||
|
nextStep?: string;
|
||||||
|
summary?: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
context: GraphQLContext,
|
||||||
|
) => createDeal(context.auth, args.input),
|
||||||
|
|
||||||
|
updateDeal: async (
|
||||||
|
args: { input: { id: string; stage?: string; amount?: number | null; paidAmount?: number | null } },
|
||||||
|
context: GraphQLContext,
|
||||||
|
) => updateDeal(context.auth, args.input),
|
||||||
|
|
||||||
createWorkspaceDocument: async (
|
createWorkspaceDocument: async (
|
||||||
args: {
|
args: {
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ model Deal {
|
|||||||
title String
|
title String
|
||||||
stage String
|
stage String
|
||||||
amount Int?
|
amount Int?
|
||||||
|
paidAmount Int?
|
||||||
nextStep String?
|
nextStep String?
|
||||||
summary String?
|
summary String?
|
||||||
currentStepId String?
|
currentStepId String?
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ model Deal {
|
|||||||
title String
|
title String
|
||||||
stage String
|
stage String
|
||||||
amount Int?
|
amount Int?
|
||||||
|
paidAmount Int?
|
||||||
nextStep String?
|
nextStep String?
|
||||||
summary String?
|
summary String?
|
||||||
currentStepId String?
|
currentStepId String?
|
||||||
|
|||||||
Reference in New Issue
Block a user