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" },
|
||||
];
|
||||
const selectedDealId = ref(deals.value[0]?.id ?? "");
|
||||
const selectedDealStepsExpanded = ref(false);
|
||||
|
||||
const commThreads = computed(() => {
|
||||
const sorted = [...commItems.value].sort((a, b) => a.at.localeCompare(b.at));
|
||||
@@ -1803,6 +1804,22 @@ function formatDealHeadline(deal: Deal) {
|
||||
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) {
|
||||
const text = input.trim();
|
||||
if (!text) return null;
|
||||
@@ -1847,11 +1864,33 @@ function formatDealDeadline(dueDate: Date) {
|
||||
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 deal = selectedWorkspaceDeal.value;
|
||||
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;
|
||||
|
||||
const now = Date.now();
|
||||
@@ -1870,12 +1909,25 @@ const selectedWorkspaceDealDueDate = computed(() => {
|
||||
const selectedWorkspaceDealSubtitle = computed(() => {
|
||||
const deal = selectedWorkspaceDeal.value;
|
||||
if (!deal) return "";
|
||||
const stepLabel = deal.nextStep.trim() || deal.stage.trim() || "Без шага";
|
||||
const stepLabel = getDealCurrentStepLabel(deal);
|
||||
const dueDate = selectedWorkspaceDealDueDate.value;
|
||||
if (!dueDate) return `${stepLabel} · без дедлайна`;
|
||||
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) {
|
||||
const itemId = item.id;
|
||||
if (callTranscriptLoading.value[itemId]) return;
|
||||
@@ -1964,6 +2016,7 @@ function pushPilotNote(text: string) {
|
||||
function openCommunicationThread(contact: string) {
|
||||
selectedTab.value = "communications";
|
||||
peopleLeftMode.value = "contacts";
|
||||
selectedDealStepsExpanded.value = false;
|
||||
const linkedContact = contacts.value.find((item) => item.name === contact);
|
||||
if (linkedContact) {
|
||||
selectedContactId.value = linkedContact.id;
|
||||
@@ -1980,6 +2033,7 @@ function openCommunicationThread(contact: string) {
|
||||
|
||||
function openDealThread(deal: Deal) {
|
||||
selectedDealId.value = deal.id;
|
||||
selectedDealStepsExpanded.value = false;
|
||||
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>
|
||||
</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/60">{{ deal.nextStep }}</p>
|
||||
<p class="mt-0.5 truncate text-[11px] text-base-content/60">{{ getDealCurrentStepLabel(deal) }}</p>
|
||||
</button>
|
||||
|
||||
<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">
|
||||
{{ selectedWorkspaceDealSubtitle }}
|
||||
</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>
|
||||
|
||||
@@ -249,23 +249,68 @@ async function main() {
|
||||
});
|
||||
|
||||
const stages = ["Лид", "Уточнение", "Подбор решения", "Коммерческое предложение", "Переговоры", "Пилот", "Проверка договора"];
|
||||
await prisma.deal.createMany({
|
||||
data: contacts.map((c, idx) => ({
|
||||
for (const [idx, c] of contacts.entries()) {
|
||||
const nextStepText =
|
||||
idx % 4 === 0
|
||||
? "Отправить предложение по пилоту и зафиксировать список задач интеграции."
|
||||
: "Провести воркшоп по решению и согласовать сроки с коммерческим владельцем.";
|
||||
|
||||
const deal = await prisma.deal.create({
|
||||
data: {
|
||||
teamId: team.id,
|
||||
contactId: c.id,
|
||||
title: `${c.company ?? "Клиент"}: интеграция Odoo + AI`,
|
||||
stage: stages[idx % stages.length],
|
||||
amount: 18000 + (idx % 8) * 7000,
|
||||
nextStep:
|
||||
idx % 4 === 0
|
||||
? "Отправить предложение по пилоту и зафиксировать список задач интеграции."
|
||||
: "Провести воркшоп по решению и согласовать сроки с коммерческим владельцем.",
|
||||
nextStep: nextStepText,
|
||||
summary:
|
||||
"Потенциальная сделка на поэтапное внедрение 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({
|
||||
data: contacts.map((c, idx) => ({
|
||||
teamId: team.id,
|
||||
|
||||
@@ -200,7 +200,20 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
||||
take: 4,
|
||||
},
|
||||
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" },
|
||||
take: 3,
|
||||
},
|
||||
@@ -221,7 +234,10 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
||||
where: { teamId: input.teamId },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
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({
|
||||
where: { teamId: input.teamId },
|
||||
@@ -270,6 +286,15 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
||||
amount: d.amount,
|
||||
nextStep: d.nextStep,
|
||||
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),
|
||||
contact: {
|
||||
name: d.contact.name,
|
||||
@@ -310,6 +335,15 @@ async function buildCrmSnapshot(input: SnapshotOptions) {
|
||||
amount: d.amount,
|
||||
nextStep: d.nextStep,
|
||||
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),
|
||||
})),
|
||||
pins: c.pins.map((p) => ({
|
||||
@@ -621,7 +655,10 @@ export async function runLangGraphCrmAgentFor(input: {
|
||||
where: { teamId: input.teamId, ...(raw.stage ? { stage: raw.stage } : {}) },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
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(
|
||||
items.map((d) => ({
|
||||
@@ -631,6 +668,15 @@ export async function runLangGraphCrmAgentFor(input: {
|
||||
amount: d.amount,
|
||||
nextStep: d.nextStep,
|
||||
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,
|
||||
company: d.contact.company,
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user