Rename compose file to docker-compose.yml
This commit is contained in:
@@ -1487,6 +1487,7 @@ const peopleSortOptions: Array<{ value: PeopleSortMode; label: string }> = [
|
|||||||
{ value: "country", label: "Country" },
|
{ value: "country", label: "Country" },
|
||||||
];
|
];
|
||||||
const selectedDealId = ref(deals.value[0]?.id ?? "");
|
const selectedDealId = ref(deals.value[0]?.id ?? "");
|
||||||
|
const selectedDealStepsExpanded = ref(false);
|
||||||
|
|
||||||
const commThreads = computed(() => {
|
const commThreads = computed(() => {
|
||||||
const sorted = [...commItems.value].sort((a, b) => a.at.localeCompare(b.at));
|
const sorted = [...commItems.value].sort((a, b) => a.at.localeCompare(b.at));
|
||||||
@@ -1803,6 +1804,22 @@ function formatDealHeadline(deal: Deal) {
|
|||||||
return `${title} за ${amountRaw}`;
|
return `${title} за ${amountRaw}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDealCurrentStep(deal: Deal) {
|
||||||
|
if (!deal.steps?.length) return null;
|
||||||
|
if (deal.currentStepId) {
|
||||||
|
const explicit = deal.steps.find((step) => step.id === deal.currentStepId);
|
||||||
|
if (explicit) return explicit;
|
||||||
|
}
|
||||||
|
const inProgress = deal.steps.find((step) => step.status === "in_progress");
|
||||||
|
if (inProgress) return inProgress;
|
||||||
|
const nextTodo = deal.steps.find((step) => step.status !== "done");
|
||||||
|
return nextTodo ?? deal.steps[deal.steps.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDealCurrentStepLabel(deal: Deal) {
|
||||||
|
return getDealCurrentStep(deal)?.title?.trim() || deal.nextStep.trim() || deal.stage.trim() || "Без шага";
|
||||||
|
}
|
||||||
|
|
||||||
function parseDateFromText(input: string) {
|
function parseDateFromText(input: string) {
|
||||||
const text = input.trim();
|
const text = input.trim();
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
@@ -1847,11 +1864,33 @@ function formatDealDeadline(dueDate: Date) {
|
|||||||
return `через ${dayDiff} ${pluralizeRuDays(dayDiff)}`;
|
return `через ${dayDiff} ${pluralizeRuDays(dayDiff)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDealStepDone(step: DealStep) {
|
||||||
|
return step.status === "done";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDealStepMeta(step: DealStep) {
|
||||||
|
if (step.status === "done") return "выполнено";
|
||||||
|
if (step.status === "blocked") return "заблокировано";
|
||||||
|
if (!step.dueAt) {
|
||||||
|
if (step.status === "in_progress") return "в работе";
|
||||||
|
return "без дедлайна";
|
||||||
|
}
|
||||||
|
const parsed = new Date(step.dueAt);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return "без дедлайна";
|
||||||
|
return formatDealDeadline(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
const selectedWorkspaceDealDueDate = computed(() => {
|
const selectedWorkspaceDealDueDate = computed(() => {
|
||||||
const deal = selectedWorkspaceDeal.value;
|
const deal = selectedWorkspaceDeal.value;
|
||||||
if (!deal) return null;
|
if (!deal) return null;
|
||||||
|
|
||||||
const fromNextStep = parseDateFromText(deal.nextStep);
|
const currentStep = getDealCurrentStep(deal);
|
||||||
|
if (currentStep?.dueAt) {
|
||||||
|
const parsed = new Date(currentStep.dueAt);
|
||||||
|
if (!Number.isNaN(parsed.getTime())) return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromNextStep = parseDateFromText(currentStep?.title || deal.nextStep);
|
||||||
if (fromNextStep) return fromNextStep;
|
if (fromNextStep) return fromNextStep;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -1870,12 +1909,25 @@ const selectedWorkspaceDealDueDate = computed(() => {
|
|||||||
const selectedWorkspaceDealSubtitle = computed(() => {
|
const selectedWorkspaceDealSubtitle = computed(() => {
|
||||||
const deal = selectedWorkspaceDeal.value;
|
const deal = selectedWorkspaceDeal.value;
|
||||||
if (!deal) return "";
|
if (!deal) return "";
|
||||||
const stepLabel = deal.nextStep.trim() || deal.stage.trim() || "Без шага";
|
const stepLabel = getDealCurrentStepLabel(deal);
|
||||||
const dueDate = selectedWorkspaceDealDueDate.value;
|
const dueDate = selectedWorkspaceDealDueDate.value;
|
||||||
if (!dueDate) return `${stepLabel} · без дедлайна`;
|
if (!dueDate) return `${stepLabel} · без дедлайна`;
|
||||||
return `${stepLabel} · ${formatDealDeadline(dueDate)}`;
|
return `${stepLabel} · ${formatDealDeadline(dueDate)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedWorkspaceDealSteps = computed(() => {
|
||||||
|
const deal = selectedWorkspaceDeal.value;
|
||||||
|
if (!deal?.steps?.length) return [];
|
||||||
|
return [...deal.steps].sort((a, b) => a.order - b.order);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedWorkspaceDeal.value?.id ?? "",
|
||||||
|
() => {
|
||||||
|
selectedDealStepsExpanded.value = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
async function transcribeCallItem(item: CommItem) {
|
async function transcribeCallItem(item: CommItem) {
|
||||||
const itemId = item.id;
|
const itemId = item.id;
|
||||||
if (callTranscriptLoading.value[itemId]) return;
|
if (callTranscriptLoading.value[itemId]) return;
|
||||||
@@ -1964,6 +2016,7 @@ function pushPilotNote(text: string) {
|
|||||||
function openCommunicationThread(contact: string) {
|
function openCommunicationThread(contact: string) {
|
||||||
selectedTab.value = "communications";
|
selectedTab.value = "communications";
|
||||||
peopleLeftMode.value = "contacts";
|
peopleLeftMode.value = "contacts";
|
||||||
|
selectedDealStepsExpanded.value = false;
|
||||||
const linkedContact = contacts.value.find((item) => item.name === contact);
|
const linkedContact = contacts.value.find((item) => item.name === contact);
|
||||||
if (linkedContact) {
|
if (linkedContact) {
|
||||||
selectedContactId.value = linkedContact.id;
|
selectedContactId.value = linkedContact.id;
|
||||||
@@ -1980,6 +2033,7 @@ function openCommunicationThread(contact: string) {
|
|||||||
|
|
||||||
function openDealThread(deal: Deal) {
|
function openDealThread(deal: Deal) {
|
||||||
selectedDealId.value = deal.id;
|
selectedDealId.value = deal.id;
|
||||||
|
selectedDealStepsExpanded.value = false;
|
||||||
openCommunicationThread(deal.contact);
|
openCommunicationThread(deal.contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2955,7 +3009,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
<span class="shrink-0 text-[10px] text-base-content/55">{{ deal.amount }}</span>
|
<span class="shrink-0 text-[10px] text-base-content/55">{{ deal.amount }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-0.5 truncate text-[11px] text-base-content/75">{{ deal.company }} · {{ deal.stage }}</p>
|
<p class="mt-0.5 truncate text-[11px] text-base-content/75">{{ deal.company }} · {{ deal.stage }}</p>
|
||||||
<p class="mt-0.5 truncate text-[11px] text-base-content/60">{{ deal.nextStep }}</p>
|
<p class="mt-0.5 truncate text-[11px] text-base-content/60">{{ getDealCurrentStepLabel(deal) }}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p v-if="peopleListMode === 'contacts' && peopleContactList.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
<p v-if="peopleListMode === 'contacts' && peopleContactList.length === 0" class="px-1 py-2 text-xs text-base-content/55">
|
||||||
@@ -3421,6 +3475,33 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
<p class="mt-1 text-[11px] text-base-content/75">
|
<p class="mt-1 text-[11px] text-base-content/75">
|
||||||
{{ selectedWorkspaceDealSubtitle }}
|
{{ selectedWorkspaceDealSubtitle }}
|
||||||
</p>
|
</p>
|
||||||
|
<button
|
||||||
|
v-if="selectedWorkspaceDealSteps.length"
|
||||||
|
class="mt-2 text-[11px] font-medium text-primary hover:underline"
|
||||||
|
@click="selectedDealStepsExpanded = !selectedDealStepsExpanded"
|
||||||
|
>
|
||||||
|
{{ selectedDealStepsExpanded ? "Скрыть шаги" : `Показать шаги (${selectedWorkspaceDealSteps.length})` }}
|
||||||
|
</button>
|
||||||
|
<div v-if="selectedDealStepsExpanded && selectedWorkspaceDealSteps.length" class="mt-2 space-y-1.5">
|
||||||
|
<div
|
||||||
|
v-for="step in selectedWorkspaceDealSteps"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox checkbox-xs mt-0.5"
|
||||||
|
:checked="isDealStepDone(step)"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<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'">
|
||||||
|
{{ step.title }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-0.5 text-[10px] text-base-content/55">{{ formatDealStepMeta(step) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -249,23 +249,68 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stages = ["Лид", "Уточнение", "Подбор решения", "Коммерческое предложение", "Переговоры", "Пилот", "Проверка договора"];
|
const stages = ["Лид", "Уточнение", "Подбор решения", "Коммерческое предложение", "Переговоры", "Пилот", "Проверка договора"];
|
||||||
await prisma.deal.createMany({
|
for (const [idx, c] of contacts.entries()) {
|
||||||
data: contacts.map((c, idx) => ({
|
const nextStepText =
|
||||||
|
idx % 4 === 0
|
||||||
|
? "Отправить предложение по пилоту и зафиксировать список задач интеграции."
|
||||||
|
: "Провести воркшоп по решению и согласовать сроки с коммерческим владельцем.";
|
||||||
|
|
||||||
|
const deal = await prisma.deal.create({
|
||||||
|
data: {
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
contactId: c.id,
|
contactId: c.id,
|
||||||
title: `${c.company ?? "Клиент"}: интеграция Odoo + AI`,
|
title: `${c.company ?? "Клиент"}: интеграция Odoo + AI`,
|
||||||
stage: stages[idx % stages.length],
|
stage: stages[idx % stages.length],
|
||||||
amount: 18000 + (idx % 8) * 7000,
|
amount: 18000 + (idx % 8) * 7000,
|
||||||
nextStep:
|
nextStep: nextStepText,
|
||||||
idx % 4 === 0
|
|
||||||
? "Отправить предложение по пилоту и зафиксировать список задач интеграции."
|
|
||||||
: "Провести воркшоп по решению и согласовать сроки с коммерческим владельцем.",
|
|
||||||
summary:
|
summary:
|
||||||
"Потенциальная сделка на поэтапное внедрение Odoo с AI-копилотами для операций, продаж и планирования. " +
|
"Потенциальная сделка на поэтапное внедрение Odoo с AI-копилотами для операций, продаж и планирования. " +
|
||||||
"Коммерческая модель: уточнение + пилот + тиражирование.",
|
"Коммерческая модель: уточнение + пилот + тиражирование.",
|
||||||
})),
|
},
|
||||||
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dueBase = atOffset((idx % 5) + 1, 11 + (idx % 4), 0);
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
dealId: deal.id,
|
||||||
|
title: "Собрать уточняющие требования",
|
||||||
|
description: "Подтвердить модули Odoo, владельцев данных и критерии успеха.",
|
||||||
|
status: "done",
|
||||||
|
order: 1,
|
||||||
|
completedAt: atOffset(-2 - (idx % 3), 16, 0),
|
||||||
|
dueAt: atOffset(-1, 12, 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dealId: deal.id,
|
||||||
|
title: "Провести воркшоп по решению",
|
||||||
|
description: "Согласовать границы интеграции и план пилота.",
|
||||||
|
status: idx % 3 === 0 ? "in_progress" : "todo",
|
||||||
|
order: 2,
|
||||||
|
dueAt: dueBase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dealId: deal.id,
|
||||||
|
title: "Согласовать и отправить договор",
|
||||||
|
description: "Выслать договор и зафиксировать дату подписи.",
|
||||||
|
status: "todo",
|
||||||
|
order: 3,
|
||||||
|
dueAt: atOffset((idx % 5) + 6, 15, 0),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await prisma.dealStep.createMany({ data: steps });
|
||||||
|
const current = await prisma.dealStep.findFirst({
|
||||||
|
where: { dealId: deal.id, status: { not: "done" } },
|
||||||
|
orderBy: [{ order: "asc" }, { createdAt: "asc" }],
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
await prisma.deal.update({
|
||||||
|
where: { id: deal.id },
|
||||||
|
data: { currentStepId: current?.id ?? null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.contactPin.createMany({
|
await prisma.contactPin.createMany({
|
||||||
data: contacts.map((c, idx) => ({
|
data: contacts.map((c, idx) => ({
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
|
|||||||
@@ -200,7 +200,20 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
|||||||
take: 4,
|
take: 4,
|
||||||
},
|
},
|
||||||
deals: {
|
deals: {
|
||||||
select: { id: true, title: true, stage: true, amount: true, updatedAt: true, nextStep: true, summary: true },
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
stage: true,
|
||||||
|
amount: true,
|
||||||
|
updatedAt: true,
|
||||||
|
nextStep: true,
|
||||||
|
summary: true,
|
||||||
|
currentStepId: true,
|
||||||
|
steps: {
|
||||||
|
select: { id: true, title: true, status: true, dueAt: true, order: true, completedAt: true },
|
||||||
|
orderBy: [{ order: "asc" }, { createdAt: "asc" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
take: 3,
|
take: 3,
|
||||||
},
|
},
|
||||||
@@ -221,7 +234,10 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
|||||||
where: { teamId: input.teamId },
|
where: { teamId: input.teamId },
|
||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
take: 20,
|
take: 20,
|
||||||
include: { contact: { select: { name: true, company: true } } },
|
include: {
|
||||||
|
contact: { select: { name: true, company: true } },
|
||||||
|
steps: { select: { id: true, title: true, status: true, dueAt: true, order: true, completedAt: true }, orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
prisma.workspaceDocument.findMany({
|
prisma.workspaceDocument.findMany({
|
||||||
where: { teamId: input.teamId },
|
where: { teamId: input.teamId },
|
||||||
@@ -270,6 +286,15 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
|||||||
amount: d.amount,
|
amount: d.amount,
|
||||||
nextStep: d.nextStep,
|
nextStep: d.nextStep,
|
||||||
summary: d.summary,
|
summary: d.summary,
|
||||||
|
currentStepId: d.currentStepId,
|
||||||
|
steps: d.steps.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
status: s.status,
|
||||||
|
dueAt: s.dueAt ? iso(s.dueAt) : null,
|
||||||
|
order: s.order,
|
||||||
|
completedAt: s.completedAt ? iso(s.completedAt) : null,
|
||||||
|
})),
|
||||||
updatedAt: iso(d.updatedAt),
|
updatedAt: iso(d.updatedAt),
|
||||||
contact: {
|
contact: {
|
||||||
name: d.contact.name,
|
name: d.contact.name,
|
||||||
@@ -310,6 +335,15 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
|||||||
amount: d.amount,
|
amount: d.amount,
|
||||||
nextStep: d.nextStep,
|
nextStep: d.nextStep,
|
||||||
summary: d.summary,
|
summary: d.summary,
|
||||||
|
currentStepId: d.currentStepId,
|
||||||
|
steps: d.steps.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
status: s.status,
|
||||||
|
dueAt: s.dueAt ? iso(s.dueAt) : null,
|
||||||
|
order: s.order,
|
||||||
|
completedAt: s.completedAt ? iso(s.completedAt) : null,
|
||||||
|
})),
|
||||||
updatedAt: iso(d.updatedAt),
|
updatedAt: iso(d.updatedAt),
|
||||||
})),
|
})),
|
||||||
pins: c.pins.map((p) => ({
|
pins: c.pins.map((p) => ({
|
||||||
@@ -621,7 +655,10 @@ export async function runLangGraphCrmAgentFor(input: {
|
|||||||
where: { teamId: input.teamId, ...(raw.stage ? { stage: raw.stage } : {}) },
|
where: { teamId: input.teamId, ...(raw.stage ? { stage: raw.stage } : {}) },
|
||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
take: Math.max(1, Math.min(raw.limit ?? 20, 100)),
|
take: Math.max(1, Math.min(raw.limit ?? 20, 100)),
|
||||||
include: { contact: { select: { name: true, company: true } } },
|
include: {
|
||||||
|
contact: { select: { name: true, company: true } },
|
||||||
|
steps: { select: { id: true, title: true, status: true, dueAt: true, order: true, completedAt: true }, orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
items.map((d) => ({
|
items.map((d) => ({
|
||||||
@@ -631,6 +668,15 @@ export async function runLangGraphCrmAgentFor(input: {
|
|||||||
amount: d.amount,
|
amount: d.amount,
|
||||||
nextStep: d.nextStep,
|
nextStep: d.nextStep,
|
||||||
summary: d.summary,
|
summary: d.summary,
|
||||||
|
currentStepId: d.currentStepId,
|
||||||
|
steps: d.steps.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
status: s.status,
|
||||||
|
dueAt: s.dueAt ? s.dueAt.toISOString() : null,
|
||||||
|
order: s.order,
|
||||||
|
completedAt: s.completedAt ? s.completedAt.toISOString() : null,
|
||||||
|
})),
|
||||||
contact: d.contact.name,
|
contact: d.contact.name,
|
||||||
company: d.contact.company,
|
company: d.contact.company,
|
||||||
})),
|
})),
|
||||||
|
|||||||
Reference in New Issue
Block a user