feat:新增客户信息跟进状态字段&优化图形样式
This commit is contained in:
parent
0ab17d54eb
commit
af125ea184
@ -50,7 +50,7 @@ func main() {
|
|||||||
|
|
||||||
// Initialize handlers
|
// Initialize handlers
|
||||||
customerHandler := handlers.NewCustomerHandler(customerStorage, feishuWebhook)
|
customerHandler := handlers.NewCustomerHandler(customerStorage, feishuWebhook)
|
||||||
followUpHandler := handlers.NewFollowUpHandler(followUpStorage, customerStorage, feishuWebhook)
|
followUpHandler := handlers.NewFollowUpHandler(followUpStorage, customerStorage, trialPeriodStorage, feishuWebhook)
|
||||||
trialPeriodHandler := handlers.NewTrialPeriodHandler(trialPeriodStorage, customerStorage, feishuWebhook)
|
trialPeriodHandler := handlers.NewTrialPeriodHandler(trialPeriodStorage, customerStorage, feishuWebhook)
|
||||||
authHandler := handlers.NewAuthHandler()
|
authHandler := handlers.NewAuthHandler()
|
||||||
|
|
||||||
|
|||||||
@ -1580,6 +1580,52 @@ td.overflow-cell {
|
|||||||
border-color: #ffd591;
|
border-color: #ffd591;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Deal Status Badges */
|
||||||
|
.deal-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deal-badge i {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deal-prospect {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #595959;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deal-trial {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deal-negotiation {
|
||||||
|
background-color: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deal-closed-won {
|
||||||
|
background-color: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
border: 1px solid #b7eb8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deal-lost {
|
||||||
|
background-color: #fff2f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
border: 1px solid #ffccc7;
|
||||||
|
}
|
||||||
|
|
||||||
.status-active {
|
.status-active {
|
||||||
background-color: #d4edda;
|
background-color: #d4edda;
|
||||||
color: #155724;
|
color: #155724;
|
||||||
|
|||||||
@ -341,7 +341,8 @@
|
|||||||
<th>客户名称</th>
|
<th>客户名称</th>
|
||||||
<th>来源</th>
|
<th>来源</th>
|
||||||
<th>意向产品</th>
|
<th>意向产品</th>
|
||||||
<th>状态</th>
|
<th>跟进状态</th>
|
||||||
|
<th>试用状态</th>
|
||||||
<th>开始时间</th>
|
<th>开始时间</th>
|
||||||
<th>结束时间</th>
|
<th>结束时间</th>
|
||||||
<th>创建时间</th>
|
<th>创建时间</th>
|
||||||
@ -409,7 +410,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-icon">
|
<div class="stat-icon">
|
||||||
<i class="fas fa-check-circle"></i>
|
<i class="fas fa-handshake"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<h3 id="completedTasks">0</h3>
|
<h3 id="completedTasks">0</h3>
|
||||||
@ -473,6 +474,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid" style="margin-top: 20px">
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-boxes"></i> 意向产品分布</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="productChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card chart-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3><i class="fas fa-tasks"></i> 跟进状态分布</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="followupStatusChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Trend Line Chart -->
|
<!-- Trend Line Chart -->
|
||||||
<div class="dashboard-grid" style="margin-top: 20px">
|
<div class="dashboard-grid" style="margin-top: 20px">
|
||||||
<div class="card chart-card" style="grid-column: 1 / -1">
|
<div class="card chart-card" style="grid-column: 1 / -1">
|
||||||
@ -1032,6 +1052,16 @@
|
|||||||
<input type="text" id="trialIntendedProductOther" name="intendedProductOther"
|
<input type="text" id="trialIntendedProductOther" name="intendedProductOther"
|
||||||
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px" />
|
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="trialDealStatus">跟进状态</label>
|
||||||
|
<select id="trialDealStatus" name="dealStatus">
|
||||||
|
<option value="初步接触">初步接触</option>
|
||||||
|
<option value="需求确认">需求确认</option>
|
||||||
|
<option value="商务洽谈">商务洽谈</option>
|
||||||
|
<option value="已成交">已成交</option>
|
||||||
|
<option value="已流失">已流失</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group" id="trialIsTrialGroup">
|
<div class="form-group" id="trialIsTrialGroup">
|
||||||
<label>是否试用</label>
|
<label>是否试用</label>
|
||||||
<div style="display: flex; gap: 20px; align-items: center">
|
<div style="display: flex; gap: 20px; align-items: center">
|
||||||
@ -1113,6 +1143,16 @@
|
|||||||
<input type="text" id="editTrialIntendedProductOther" name="editIntendedProductOther"
|
<input type="text" id="editTrialIntendedProductOther" name="editIntendedProductOther"
|
||||||
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px" />
|
placeholder="请输入其他意向产品" style="display: none; margin-top: 8px" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="editTrialDealStatus">跟进状态</label>
|
||||||
|
<select id="editTrialDealStatus" name="dealStatus">
|
||||||
|
<option value="初步接触">初步接触</option>
|
||||||
|
<option value="需求确认">需求确认</option>
|
||||||
|
<option value="商务洽谈">商务洽谈</option>
|
||||||
|
<option value="已成交">已成交</option>
|
||||||
|
<option value="已流失">已流失</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group" id="editTrialStartTimeGroup">
|
<div class="form-group" id="editTrialStartTimeGroup">
|
||||||
<label for="editTrialStartTime">开始时间</label>
|
<label for="editTrialStartTime">开始时间</label>
|
||||||
<input type="datetime-local" id="editTrialStartTime" name="startTime" />
|
<input type="datetime-local" id="editTrialStartTime" name="startTime" />
|
||||||
@ -1188,9 +1228,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/static/js/main.js?v=5.5"></script>
|
<script src="/static/js/main.js?v=20260205005"></script>
|
||||||
<script src="/static/js/trial-periods.js?v=1.3"></script>
|
<script src="/static/js/trial-periods.js?v=20260205001"></script>
|
||||||
<script src="/static/js/trial-periods-page.js?v=1.9"></script>
|
<script src="/static/js/trial-periods-page.js?v=20260205001"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -101,6 +101,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
let statusChartInstance = null;
|
let statusChartInstance = null;
|
||||||
let typeChartInstance = null;
|
let typeChartInstance = null;
|
||||||
let trendChartInstance = null;
|
let trendChartInstance = null;
|
||||||
|
let productChartInstance = null;
|
||||||
|
let followupStatusChartInstance = null;
|
||||||
|
|
||||||
// Current section tracking
|
// Current section tracking
|
||||||
let currentSection = "dashboard";
|
let currentSection = "dashboard";
|
||||||
@ -1360,6 +1362,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
// Update total customers display (数据仪表盘的总客户数从每周进度获取)
|
// Update total customers display (数据仪表盘的总客户数从每周进度获取)
|
||||||
// This function will be called after loading dashboard customers
|
// This function will be called after loading dashboard customers
|
||||||
updateTotalCustomersWithTrialData(trialCustomerNames);
|
updateTotalCustomersWithTrialData(trialCustomerNames);
|
||||||
|
|
||||||
|
// Render trial period analysis charts
|
||||||
|
renderTrialAnalysisCharts(trialPeriods);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading trial customers for dashboard:", error);
|
console.error("Error loading trial customers for dashboard:", error);
|
||||||
}
|
}
|
||||||
@ -1416,23 +1421,41 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (data.followUps) {
|
if (data.followUps) {
|
||||||
const followUpCount = data.followUps.length;
|
const followUpCount = data.followUps.length;
|
||||||
document.getElementById("followUpCount").textContent = followUpCount;
|
document.getElementById("followUpCount").textContent = followUpCount;
|
||||||
|
} else {
|
||||||
|
document.getElementById("followUpCount").textContent = "0";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading follow-up count:", error);
|
||||||
|
document.getElementById("followUpCount").textContent = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已成交数量从客户信息表(trial_periods)统计
|
||||||
|
await loadCompletedDealsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load completed deals count from trial periods
|
||||||
|
async function loadCompletedDealsCount() {
|
||||||
|
try {
|
||||||
|
const response = await authenticatedFetch(
|
||||||
|
"/api/trial-periods/all",
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.trialPeriods) {
|
||||||
// 计算已成交数量:统计 dealStatus 为"已成交"的唯一客户数
|
// 计算已成交数量:统计 dealStatus 为"已成交"的唯一客户数
|
||||||
const completedCustomers = new Set(
|
const completedCustomers = new Set(
|
||||||
data.followUps
|
data.trialPeriods
|
||||||
.filter((f) => f.dealStatus === "已成交")
|
.filter((tp) => tp.dealStatus === "已成交")
|
||||||
.map((f) => f.customerName)
|
.map((tp) => tp.customerName)
|
||||||
.filter((name) => name),
|
.filter((name) => name),
|
||||||
).size;
|
).size;
|
||||||
document.getElementById("completedTasks").textContent =
|
document.getElementById("completedTasks").textContent =
|
||||||
completedCustomers;
|
completedCustomers;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("followUpCount").textContent = "0";
|
|
||||||
document.getElementById("completedTasks").textContent = "0";
|
document.getElementById("completedTasks").textContent = "0";
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading follow-up count:", error);
|
console.error("Error loading completed deals count:", error);
|
||||||
document.getElementById("followUpCount").textContent = "0";
|
|
||||||
document.getElementById("completedTasks").textContent = "0";
|
document.getElementById("completedTasks").textContent = "0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1643,6 +1666,155 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render trial analysis charts (Intended Product & Follow-up Status)
|
||||||
|
function renderTrialAnalysisCharts(trialPeriods) {
|
||||||
|
renderProductChart(trialPeriods);
|
||||||
|
renderFollowupStatusChart(trialPeriods);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProductChart(trialPeriods) {
|
||||||
|
const canvas = document.getElementById("productChart");
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const productCount = {};
|
||||||
|
trialPeriods.forEach((tp) => {
|
||||||
|
if (!tp.intendedProduct) return;
|
||||||
|
// Split by comma if multiple products selected
|
||||||
|
const products = tp.intendedProduct.split(",").map((p) => p.trim());
|
||||||
|
products.forEach((p) => {
|
||||||
|
if (p) {
|
||||||
|
productCount[p] = (productCount[p] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = Object.keys(productCount);
|
||||||
|
const data = Object.values(productCount);
|
||||||
|
|
||||||
|
if (productChartInstance) {
|
||||||
|
productChartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
productChartInstance = new Chart(ctx, {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "产品数量",
|
||||||
|
data: data,
|
||||||
|
backgroundColor: "#3b82f6",
|
||||||
|
borderRadius: 6,
|
||||||
|
maxBarThickness: 40,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => `数量: ${context.parsed.y}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datalabels: {
|
||||||
|
anchor: "end",
|
||||||
|
align: "top",
|
||||||
|
color: "#666",
|
||||||
|
font: { weight: "bold" },
|
||||||
|
formatter: (value) => value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: { stepSize: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [ChartDataLabels],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFollowupStatusChart(trialPeriods) {
|
||||||
|
const canvas = document.getElementById("followupStatusChart");
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
const statusCount = {};
|
||||||
|
trialPeriods.forEach((tp) => {
|
||||||
|
let status = tp.dealStatus || "初步接触";
|
||||||
|
|
||||||
|
// 数据清洗:统一新旧名称
|
||||||
|
if (status === "潜在客户") status = "初步接触";
|
||||||
|
if (status === "试用中") status = "需求确认";
|
||||||
|
if (status === "") status = "初步接触";
|
||||||
|
|
||||||
|
statusCount[status] = (statusCount[status] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const labels = Object.keys(statusCount);
|
||||||
|
const data = Object.values(statusCount);
|
||||||
|
|
||||||
|
if (followupStatusChartInstance) {
|
||||||
|
followupStatusChartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
"已成交": "#10b981", // Emerald 500
|
||||||
|
"商务洽谈": "#f59e0b", // Amber 500
|
||||||
|
"需求确认": "#6366f1", // Indigo 500
|
||||||
|
"初步接触": "#94a3b8", // Slate 400
|
||||||
|
"已流失": "#ef4444", // Red 500
|
||||||
|
"未设置": "#cbd5e1", // Slate 300
|
||||||
|
};
|
||||||
|
|
||||||
|
const backgroundColors = labels.map((l) => colors[l] || "#9E9E9E");
|
||||||
|
|
||||||
|
followupStatusChartInstance = new Chart(ctx, {
|
||||||
|
type: "doughnut",
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: data,
|
||||||
|
backgroundColor: backgroundColors,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: "right",
|
||||||
|
labels: {
|
||||||
|
generateLabels: (chart) => {
|
||||||
|
const data = chart.data;
|
||||||
|
return data.labels.map((label, i) => ({
|
||||||
|
text: `${label}: ${data.datasets[0].data[i]}`,
|
||||||
|
fillStyle: data.datasets[0].backgroundColor[i],
|
||||||
|
index: i,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
datalabels: {
|
||||||
|
formatter: (value, ctx) => {
|
||||||
|
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
||||||
|
const percentage = ((value / total) * 100).toFixed(1);
|
||||||
|
return `${value}\n(${percentage}%)`;
|
||||||
|
},
|
||||||
|
color: "#fff",
|
||||||
|
font: { weight: "bold", size: 10 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [ChartDataLabels],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Render trend line chart
|
// Render trend line chart
|
||||||
function renderTrendChart(customers) {
|
function renderTrendChart(customers) {
|
||||||
const canvas = document.getElementById("trendChart");
|
const canvas = document.getElementById("trendChart");
|
||||||
|
|||||||
@ -553,10 +553,26 @@ function renderTrialPeriodsTable() {
|
|||||||
return `<span class="product-badge ${colorClass}">${p}</span>`;
|
return `<span class="product-badge ${colorClass}">${p}</span>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// 生成跟进状态标签
|
||||||
|
const dealStatus = period.dealStatus || '初步接触';
|
||||||
|
let dealStatusBadge = '';
|
||||||
|
if (dealStatus === '已成交') {
|
||||||
|
dealStatusBadge = '<span class="deal-badge deal-closed-won"><i class="fas fa-trophy"></i> 已成交</span>';
|
||||||
|
} else if (dealStatus === '需求确认') {
|
||||||
|
dealStatusBadge = '<span class="deal-badge deal-trial"><i class="fas fa-clipboard-check"></i> 需求确认</span>';
|
||||||
|
} else if (dealStatus === '商务洽谈') {
|
||||||
|
dealStatusBadge = '<span class="deal-badge deal-negotiation"><i class="fas fa-handshake"></i> 商务洽谈</span>';
|
||||||
|
} else if (dealStatus === '已流失') {
|
||||||
|
dealStatusBadge = '<span class="deal-badge deal-lost"><i class="fas fa-times-circle"></i> 已流失</span>';
|
||||||
|
} else {
|
||||||
|
dealStatusBadge = '<span class="deal-badge deal-prospect"><i class="fas fa-user-clock"></i> 初步接触</span>';
|
||||||
|
}
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td><strong>${customerName}</strong></td>
|
<td><strong>${customerName}</strong></td>
|
||||||
<td>${source}</td>
|
<td>${source}</td>
|
||||||
<td>${productBadges}</td>
|
<td>${productBadges}</td>
|
||||||
|
<td>${dealStatusBadge}</td>
|
||||||
<td>${statusCell}</td>
|
<td>${statusCell}</td>
|
||||||
<td>${startTimeCell}</td>
|
<td>${startTimeCell}</td>
|
||||||
<td>${endTimeCell}</td>
|
<td>${endTimeCell}</td>
|
||||||
@ -641,6 +657,10 @@ function openAddTrialModal() {
|
|||||||
// Reset intended product
|
// Reset intended product
|
||||||
setIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther', '');
|
setIntendedProductValue('trialIntendedProduct', 'trialIntendedProductOther', '');
|
||||||
|
|
||||||
|
// Reset deal status
|
||||||
|
const dealStatusEl = document.getElementById('trialDealStatus');
|
||||||
|
if (dealStatusEl) dealStatusEl.value = '初步接触';
|
||||||
|
|
||||||
document.querySelector('input[name="isTrial"][value="true"]').checked = true;
|
document.querySelector('input[name="isTrial"][value="true"]').checked = true;
|
||||||
document.getElementById('trialStartTime').value = '';
|
document.getElementById('trialStartTime').value = '';
|
||||||
document.getElementById('trialEndTime').value = '';
|
document.getElementById('trialEndTime').value = '';
|
||||||
@ -661,6 +681,10 @@ function openEditTrialModal(periodId) {
|
|||||||
// Set intended product
|
// Set intended product
|
||||||
setIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther', period.intendedProduct || '');
|
setIntendedProductValue('editTrialIntendedProduct', 'editTrialIntendedProductOther', period.intendedProduct || '');
|
||||||
|
|
||||||
|
// Set deal status
|
||||||
|
const dealStatusEl = document.getElementById('editTrialDealStatus');
|
||||||
|
if (dealStatusEl) dealStatusEl.value = period.dealStatus || '初步接触';
|
||||||
|
|
||||||
const startDate = new Date(period.startTime);
|
const startDate = new Date(period.startTime);
|
||||||
const endDate = new Date(period.endTime);
|
const endDate = new Date(period.endTime);
|
||||||
|
|
||||||
@ -740,6 +764,7 @@ async function createTrialPeriodFromPage() {
|
|||||||
const isTrial = isTrialValue === 'true';
|
const isTrial = isTrialValue === 'true';
|
||||||
const startTime = document.getElementById('trialStartTime').value;
|
const startTime = document.getElementById('trialStartTime').value;
|
||||||
const endTime = document.getElementById('trialEndTime').value;
|
const endTime = document.getElementById('trialEndTime').value;
|
||||||
|
const dealStatus = document.getElementById('trialDealStatus').value;
|
||||||
|
|
||||||
if (!customerName) {
|
if (!customerName) {
|
||||||
alert('请选择或输入客户名称');
|
alert('请选择或输入客户名称');
|
||||||
@ -758,6 +783,7 @@ async function createTrialPeriodFromPage() {
|
|||||||
customerName: customerName,
|
customerName: customerName,
|
||||||
source: source,
|
source: source,
|
||||||
intendedProduct: intendedProduct,
|
intendedProduct: intendedProduct,
|
||||||
|
dealStatus: dealStatus,
|
||||||
startTime: startTime ? new Date(startTime).toISOString() : '',
|
startTime: startTime ? new Date(startTime).toISOString() : '',
|
||||||
endTime: endTime ? new Date(endTime).toISOString() : '',
|
endTime: endTime ? new Date(endTime).toISOString() : '',
|
||||||
isTrial: isTrial
|
isTrial: isTrial
|
||||||
@ -777,11 +803,12 @@ async function createTrialPeriodFromPage() {
|
|||||||
document.getElementById('addTrialPeriodForm').reset();
|
document.getElementById('addTrialPeriodForm').reset();
|
||||||
await loadAllTrialPeriods();
|
await loadAllTrialPeriods();
|
||||||
} else {
|
} else {
|
||||||
alert('添加试用时间时出错');
|
const errorText = await response.text();
|
||||||
|
alert(errorText || '添加试用时间时出错');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating trial period:', error);
|
console.error('Error creating trial period:', error);
|
||||||
alert('添加试用时间时出错');
|
alert('网络错误,请稍后再试');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -187,13 +187,14 @@ async function createTrialPeriod() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
document.getElementById('addTrialPeriodModal').style.display = 'none';
|
document.getElementById('addTrialPeriodModal').style.display = 'none';
|
||||||
document.getElementById('addTrialPeriodForm').reset();
|
document.getElementById('addTrialPeriodForm').reset();
|
||||||
await loadTrialPeriods(customerId);
|
loadTrialPeriods(currentCustomerId); // Reload trial periods for the current customer
|
||||||
} else {
|
} else {
|
||||||
alert('添加试用时间时出错');
|
const errorText = await response.text();
|
||||||
|
alert(errorText || '创建试用期时出错');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating trial period:', error);
|
console.error('Error creating trial period:', error);
|
||||||
alert('添加试用时间时出错');
|
alert('网络错误,请稍后再试');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +261,13 @@ async function updateTrialPeriod() {
|
|||||||
const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked');
|
const isTrialRadio = document.querySelector('input[name="editIsTrial"]:checked');
|
||||||
const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true;
|
const isTrial = isTrialRadio ? isTrialRadio.value === 'true' : true;
|
||||||
|
|
||||||
if (!startTime || !endTime) {
|
// Get deal status value
|
||||||
|
const dealStatusEl = document.getElementById('editTrialDealStatus');
|
||||||
|
const dealStatus = dealStatusEl ? dealStatusEl.value : '初步接触';
|
||||||
|
|
||||||
|
// Only require start/end time if "数据闭环" is selected
|
||||||
|
const requiresTimeFields = intendedProduct.includes('数据闭环');
|
||||||
|
if (requiresTimeFields && (!startTime || !endTime)) {
|
||||||
alert('请填写开始时间和结束时间');
|
alert('请填写开始时间和结束时间');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -268,11 +275,18 @@ async function updateTrialPeriod() {
|
|||||||
const formData = {
|
const formData = {
|
||||||
source: source,
|
source: source,
|
||||||
intendedProduct: intendedProduct,
|
intendedProduct: intendedProduct,
|
||||||
startTime: new Date(startTime).toISOString(),
|
dealStatus: dealStatus,
|
||||||
endTime: new Date(endTime).toISOString(),
|
|
||||||
isTrial: isTrial
|
isTrial: isTrial
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only include time fields if they have values
|
||||||
|
if (startTime) {
|
||||||
|
formData.startTime = new Date(startTime).toISOString();
|
||||||
|
}
|
||||||
|
if (endTime) {
|
||||||
|
formData.endTime = new Date(endTime).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch(`/api/trial-periods/${periodId}`, {
|
const response = await authenticatedFetch(`/api/trial-periods/${periodId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@ -14,16 +14,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type FollowUpHandler struct {
|
type FollowUpHandler struct {
|
||||||
storage storage.FollowUpStorage
|
storage storage.FollowUpStorage
|
||||||
customerStorage storage.CustomerStorage
|
customerStorage storage.CustomerStorage
|
||||||
feishuWebhook string
|
trialPeriodStorage storage.TrialPeriodStorage
|
||||||
|
feishuWebhook string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFollowUpHandler(storage storage.FollowUpStorage, customerStorage storage.CustomerStorage, feishuWebhook string) *FollowUpHandler {
|
func NewFollowUpHandler(storage storage.FollowUpStorage, customerStorage storage.CustomerStorage, trialPeriodStorage storage.TrialPeriodStorage, feishuWebhook string) *FollowUpHandler {
|
||||||
return &FollowUpHandler{
|
return &FollowUpHandler{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
customerStorage: customerStorage,
|
customerStorage: customerStorage,
|
||||||
feishuWebhook: feishuWebhook,
|
trialPeriodStorage: trialPeriodStorage,
|
||||||
|
feishuWebhook: feishuWebhook,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +136,11 @@ func (h *FollowUpHandler) CreateFollowUp(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 联动更新: 当跟进记录状态为"已成交"时,自动更新客户信息表的成交状态
|
||||||
|
if req.DealStatus == "已成交" {
|
||||||
|
h.updateCustomerDealStatus(req.CustomerName, "已成交")
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
json.NewEncoder(w).Encode(followUp)
|
json.NewEncoder(w).Encode(followUp)
|
||||||
@ -160,11 +167,32 @@ func (h *FollowUpHandler) UpdateFollowUp(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先获取原始跟进记录,用于获取客户名称
|
||||||
|
existingFollowUp, err := h.storage.GetFollowUpByID(id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if existingFollowUp == nil {
|
||||||
|
http.Error(w, "Follow-up not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := h.storage.UpdateFollowUp(id, req); err != nil {
|
if err := h.storage.UpdateFollowUp(id, req); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 联动更新: 当跟进记录状态更新为"已成交"时,自动更新客户信息表的成交状态
|
||||||
|
if req.DealStatus != nil && *req.DealStatus == "已成交" {
|
||||||
|
// 优先使用请求中的客户名称,如果没有则使用原记录的客户名称
|
||||||
|
customerName := existingFollowUp.CustomerName
|
||||||
|
if req.CustomerName != nil {
|
||||||
|
customerName = *req.CustomerName
|
||||||
|
}
|
||||||
|
h.updateCustomerDealStatus(customerName, "已成交")
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,3 +331,26 @@ func (h *FollowUpHandler) GetCustomerList(w http.ResponseWriter, r *http.Request
|
|||||||
"customerMap": customerMap, // Complete ID->Name mapping for display
|
"customerMap": customerMap, // Complete ID->Name mapping for display
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateCustomerDealStatus 联动更新客户信息表的成交状态
|
||||||
|
func (h *FollowUpHandler) updateCustomerDealStatus(customerName string, dealStatus string) {
|
||||||
|
if h.trialPeriodStorage == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有试用记录,找到匹配客户名称的记录
|
||||||
|
trialPeriods, err := h.trialPeriodStorage.GetAllTrialPeriods()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新所有匹配客户名称的记录的成交状态
|
||||||
|
for _, period := range trialPeriods {
|
||||||
|
if period.CustomerName == customerName {
|
||||||
|
updateReq := models.UpdateTrialPeriodRequest{
|
||||||
|
DealStatus: &dealStatus,
|
||||||
|
}
|
||||||
|
h.trialPeriodStorage.UpdateTrialPeriod(period.ID, updateReq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -93,12 +93,20 @@ func (h *TrialPeriodHandler) CreateTrialPeriod(w http.ResponseWriter, r *http.Re
|
|||||||
CustomerName: req.CustomerName,
|
CustomerName: req.CustomerName,
|
||||||
Source: req.Source,
|
Source: req.Source,
|
||||||
IntendedProduct: req.IntendedProduct,
|
IntendedProduct: req.IntendedProduct,
|
||||||
|
DealStatus: req.DealStatus,
|
||||||
StartTime: startTime,
|
StartTime: startTime,
|
||||||
EndTime: endTime,
|
EndTime: endTime,
|
||||||
IsTrial: req.IsTrial,
|
IsTrial: req.IsTrial,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if customer name already exists
|
||||||
|
existing, err := h.storage.GetTrialPeriodsByCustomerID(req.CustomerName)
|
||||||
|
if err == nil && len(existing) > 0 {
|
||||||
|
http.Error(w, fmt.Sprintf("客户名称 '%s' 已存在,请勿重复添加", req.CustomerName), http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
createdPeriod, err := h.storage.CreateTrialPeriod(trialPeriod)
|
createdPeriod, err := h.storage.CreateTrialPeriod(trialPeriod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|||||||
@ -147,6 +147,31 @@ func autoMigrate() error {
|
|||||||
log.Printf("✅ Modified screenshots column type from %s to LONGTEXT\n", columnType.String)
|
log.Printf("✅ Modified screenshots column type from %s to LONGTEXT\n", columnType.String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查并添加 deal_status 列到 trial_periods 表
|
||||||
|
var dealStatusColumn sql.NullString
|
||||||
|
err = db.QueryRow(`
|
||||||
|
SELECT COLUMN_NAME
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'trial_periods'
|
||||||
|
AND COLUMN_NAME = 'deal_status'
|
||||||
|
`).Scan(&dealStatusColumn)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
// deal_status 列不存在,添加新列
|
||||||
|
_, err = db.Exec("ALTER TABLE trial_periods ADD COLUMN deal_status VARCHAR(50) DEFAULT '初步接触'")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add deal_status column: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("✅ Added deal_status column to trial_periods table")
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to check deal_status column: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据清理:将旧的状态名称统一更新为新名称
|
||||||
|
_, _ = db.Exec("UPDATE trial_periods SET deal_status = '初步接触' WHERE deal_status = '潜在客户' OR deal_status = '' OR deal_status IS NULL")
|
||||||
|
_, _ = db.Exec("UPDATE trial_periods SET deal_status = '需求确认' WHERE deal_status = '试用中'")
|
||||||
|
|
||||||
log.Println("✅ Database tables migrated successfully")
|
log.Println("✅ Database tables migrated successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,8 @@ func NewMySQLTrialPeriodStorage() TrialPeriodStorage {
|
|||||||
|
|
||||||
func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) {
|
func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
|
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product,
|
||||||
|
COALESCE(deal_status, '') as deal_status, start_time, end_time, is_trial, created_at
|
||||||
FROM trial_periods
|
FROM trial_periods
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -40,8 +41,8 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e
|
|||||||
var isTrial int
|
var isTrial int
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
|
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.DealStatus,
|
||||||
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
&tp.StartTime, &tp.EndTime, &isTrial, &tp.CreatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -56,7 +57,8 @@ func (ts *mysqlTrialPeriodStorage) GetAllTrialPeriods() ([]models.TrialPeriod, e
|
|||||||
|
|
||||||
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) {
|
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string) ([]models.TrialPeriod, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
|
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product,
|
||||||
|
COALESCE(deal_status, '') as deal_status, start_time, end_time, is_trial, created_at
|
||||||
FROM trial_periods
|
FROM trial_periods
|
||||||
WHERE customer_name = ?
|
WHERE customer_name = ?
|
||||||
ORDER BY end_time DESC
|
ORDER BY end_time DESC
|
||||||
@ -74,8 +76,8 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string
|
|||||||
var isTrial int
|
var isTrial int
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
|
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.DealStatus,
|
||||||
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
&tp.StartTime, &tp.EndTime, &isTrial, &tp.CreatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -90,7 +92,8 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodsByCustomerID(customerID string
|
|||||||
|
|
||||||
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) {
|
func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialPeriod, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product, start_time, end_time, is_trial, created_at
|
SELECT id, customer_name, COALESCE(source, '') as source, COALESCE(intended_product, '') as intended_product,
|
||||||
|
COALESCE(deal_status, '') as deal_status, start_time, end_time, is_trial, created_at
|
||||||
FROM trial_periods
|
FROM trial_periods
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`
|
`
|
||||||
@ -99,8 +102,8 @@ func (ts *mysqlTrialPeriodStorage) GetTrialPeriodByID(id string) (*models.TrialP
|
|||||||
var isTrial int
|
var isTrial int
|
||||||
|
|
||||||
err := ts.db.QueryRow(query, id).Scan(
|
err := ts.db.QueryRow(query, id).Scan(
|
||||||
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.StartTime,
|
&tp.ID, &tp.CustomerName, &tp.Source, &tp.IntendedProduct, &tp.DealStatus,
|
||||||
&tp.EndTime, &isTrial, &tp.CreatedAt,
|
&tp.StartTime, &tp.EndTime, &isTrial, &tp.CreatedAt,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
@ -123,8 +126,8 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO trial_periods (id, customer_name, source, intended_product, start_time, end_time, is_trial, created_at)
|
INSERT INTO trial_periods (id, customer_name, source, intended_product, deal_status, start_time, end_time, is_trial, created_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
|
|
||||||
isTrial := 0
|
isTrial := 0
|
||||||
@ -134,7 +137,7 @@ func (ts *mysqlTrialPeriodStorage) CreateTrialPeriod(trialPeriod models.TrialPer
|
|||||||
|
|
||||||
_, err := ts.db.Exec(query,
|
_, err := ts.db.Exec(query,
|
||||||
trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.Source, trialPeriod.IntendedProduct,
|
trialPeriod.ID, trialPeriod.CustomerName, trialPeriod.Source, trialPeriod.IntendedProduct,
|
||||||
trialPeriod.StartTime, trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt,
|
trialPeriod.DealStatus, trialPeriod.StartTime, trialPeriod.EndTime, isTrial, trialPeriod.CreatedAt,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -161,6 +164,9 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
|
|||||||
if updates.IntendedProduct != nil {
|
if updates.IntendedProduct != nil {
|
||||||
existing.IntendedProduct = *updates.IntendedProduct
|
existing.IntendedProduct = *updates.IntendedProduct
|
||||||
}
|
}
|
||||||
|
if updates.DealStatus != nil {
|
||||||
|
existing.DealStatus = *updates.DealStatus
|
||||||
|
}
|
||||||
if updates.StartTime != nil {
|
if updates.StartTime != nil {
|
||||||
startTime, err := time.Parse(time.RFC3339, *updates.StartTime)
|
startTime, err := time.Parse(time.RFC3339, *updates.StartTime)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -179,7 +185,7 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
|
|||||||
|
|
||||||
query := `
|
query := `
|
||||||
UPDATE trial_periods
|
UPDATE trial_periods
|
||||||
SET customer_name = ?, source = ?, intended_product = ?, start_time = ?, end_time = ?, is_trial = ?
|
SET customer_name = ?, source = ?, intended_product = ?, deal_status = ?, start_time = ?, end_time = ?, is_trial = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -189,7 +195,7 @@ func (ts *mysqlTrialPeriodStorage) UpdateTrialPeriod(id string, updates models.U
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = ts.db.Exec(query,
|
_, err = ts.db.Exec(query,
|
||||||
existing.CustomerName, existing.Source, existing.IntendedProduct,
|
existing.CustomerName, existing.Source, existing.IntendedProduct, existing.DealStatus,
|
||||||
existing.StartTime, existing.EndTime, isTrial, id,
|
existing.StartTime, existing.EndTime, isTrial, id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -139,6 +139,12 @@ func (ts *trialPeriodStorage) UpdateTrialPeriod(id string, updates models.Update
|
|||||||
if updates.Source != nil {
|
if updates.Source != nil {
|
||||||
trialPeriods[i].Source = *updates.Source
|
trialPeriods[i].Source = *updates.Source
|
||||||
}
|
}
|
||||||
|
if updates.IntendedProduct != nil {
|
||||||
|
trialPeriods[i].IntendedProduct = *updates.IntendedProduct
|
||||||
|
}
|
||||||
|
if updates.DealStatus != nil {
|
||||||
|
trialPeriods[i].DealStatus = *updates.DealStatus
|
||||||
|
}
|
||||||
if updates.StartTime != nil {
|
if updates.StartTime != nil {
|
||||||
startTime, err := time.Parse(time.RFC3339, *updates.StartTime)
|
startTime, err := time.Parse(time.RFC3339, *updates.StartTime)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ type TrialPeriod struct {
|
|||||||
CustomerName string `json:"customerName"` // 直接存储客户名称
|
CustomerName string `json:"customerName"` // 直接存储客户名称
|
||||||
Source string `json:"source"` // 客户来源
|
Source string `json:"source"` // 客户来源
|
||||||
IntendedProduct string `json:"intendedProduct"` // 意向产品
|
IntendedProduct string `json:"intendedProduct"` // 意向产品
|
||||||
|
DealStatus string `json:"dealStatus"` // 成交状态
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
IsTrial bool `json:"isTrial"`
|
IsTrial bool `json:"isTrial"`
|
||||||
@ -19,6 +20,7 @@ type CreateTrialPeriodRequest struct {
|
|||||||
CustomerName string `json:"customerName"` // 直接使用客户名称
|
CustomerName string `json:"customerName"` // 直接使用客户名称
|
||||||
Source string `json:"source"` // 客户来源
|
Source string `json:"source"` // 客户来源
|
||||||
IntendedProduct string `json:"intendedProduct"` // 意向产品
|
IntendedProduct string `json:"intendedProduct"` // 意向产品
|
||||||
|
DealStatus string `json:"dealStatus"` // 成交状态
|
||||||
StartTime string `json:"startTime"`
|
StartTime string `json:"startTime"`
|
||||||
EndTime string `json:"endTime"`
|
EndTime string `json:"endTime"`
|
||||||
IsTrial bool `json:"isTrial"`
|
IsTrial bool `json:"isTrial"`
|
||||||
@ -29,6 +31,7 @@ type UpdateTrialPeriodRequest struct {
|
|||||||
CustomerName *string `json:"customerName,omitempty"`
|
CustomerName *string `json:"customerName,omitempty"`
|
||||||
Source *string `json:"source,omitempty"` // 客户来源
|
Source *string `json:"source,omitempty"` // 客户来源
|
||||||
IntendedProduct *string `json:"intendedProduct,omitempty"` // 意向产品
|
IntendedProduct *string `json:"intendedProduct,omitempty"` // 意向产品
|
||||||
|
DealStatus *string `json:"dealStatus,omitempty"` // 成交状态
|
||||||
StartTime *string `json:"startTime,omitempty"`
|
StartTime *string `json:"startTime,omitempty"`
|
||||||
EndTime *string `json:"endTime,omitempty"`
|
EndTime *string `json:"endTime,omitempty"`
|
||||||
IsTrial *bool `json:"isTrial,omitempty"`
|
IsTrial *bool `json:"isTrial,omitempty"`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user