feat: 添加客户截图上传与展示功能,包括后端文件处理和前端界面集成。
This commit is contained in:
parent
3de6cfd48d
commit
9250803979
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# IDE specific files
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Build
|
||||
bin/
|
||||
dist/
|
||||
|
||||
# Environment variables
|
||||
/apps/**/.env
|
||||
@ -183,6 +183,14 @@ func main() {
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
customerHandler.UploadScreenshots(w, r)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/customers/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
@ -340,6 +348,9 @@ func main() {
|
||||
// Serve static files
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./frontend"))))
|
||||
|
||||
// Serve uploaded files
|
||||
http.Handle("/static/uploads/", http.StripPrefix("/static/uploads/", http.FileServer(http.Dir("./frontend/uploads"))))
|
||||
|
||||
// Serve index page
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, filepath.Join("./frontend", "index.html"))
|
||||
|
||||
@ -603,6 +603,24 @@ body {
|
||||
box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.1);
|
||||
}
|
||||
|
||||
/* Select placeholder styling - Simple and direct approach */
|
||||
select {
|
||||
color: var(--dark-gray);
|
||||
}
|
||||
|
||||
select option {
|
||||
color: var(--dark-gray);
|
||||
}
|
||||
|
||||
select option[disabled] {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* This will be controlled by JavaScript for placeholder state */
|
||||
select.placeholder-active {
|
||||
color: #999 !important;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
@ -716,6 +734,7 @@ th {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td {
|
||||
@ -728,6 +747,12 @@ td {
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
height: inherit; /* Ensure td takes full height of tr */
|
||||
}
|
||||
|
||||
#customerTable td {
|
||||
display: table-cell; /* Force correct display mode */
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
@ -2258,35 +2283,20 @@ tr:hover .action-cell {
|
||||
/* Font Awesome check icon */
|
||||
}
|
||||
|
||||
/* Table refresh animation */
|
||||
@keyframes tableRefresh {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table refresh animation simplified */
|
||||
.table-refreshing {
|
||||
animation: tableRefresh 0.5s ease-out;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Row highlight on refresh */
|
||||
/* Row highlight on refresh - removed transform */
|
||||
@keyframes rowHighlight {
|
||||
0% {
|
||||
background-color: rgba(255, 107, 53, 0.2);
|
||||
transform: translateX(-5px);
|
||||
background-color: rgba(255, 107, 53, 0.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: transparent;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2699,4 +2709,174 @@ tr:hover .action-cell {
|
||||
gap: 8px;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Screenshot Preview & List Styles */
|
||||
.screenshot-wrapper {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 4px 0;
|
||||
scrollbar-width: thin;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.screenshot-column-cell {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.screenshot-wrapper::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.screenshot-wrapper::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 107, 53, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.screenshot-thumbnail {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.screenshot-thumbnail:hover {
|
||||
transform: scale(1.1);
|
||||
border-color: var(--primary-orange);
|
||||
}
|
||||
|
||||
.no-screenshots {
|
||||
color: #ccc;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Upload Container */
|
||||
.screenshot-upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.screenshot-preview-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.preview-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.remove-preview {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.screenshot-upload-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background-color: var(--white);
|
||||
border: 1px dashed var(--primary-orange);
|
||||
border-radius: 6px;
|
||||
color: var(--primary-orange);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.screenshot-upload-btn:hover {
|
||||
background-color: rgba(255, 107, 53, 0.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Lightbox / Image Viewer */
|
||||
.image-viewer-modal {
|
||||
position: fixed;
|
||||
z-index: 5000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.image-viewer-content {
|
||||
max-width: 90%;
|
||||
max-height: 80%;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
animation: zoomIn 0.3s ease;
|
||||
}
|
||||
|
||||
.image-viewer-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 35px;
|
||||
color: #f1f1f1;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.image-viewer-close:hover {
|
||||
color: var(--primary-orange);
|
||||
}
|
||||
|
||||
#imageViewerCaption {
|
||||
margin: 15px;
|
||||
color: #ccc;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from { transform: scale(0.9); }
|
||||
to { transform: scale(1); }
|
||||
}
|
||||
@ -222,6 +222,7 @@
|
||||
<th>类型</th>
|
||||
<th>模块</th>
|
||||
<th>状态与进度</th>
|
||||
<th>截图</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -529,13 +530,13 @@
|
||||
<div class="form-group">
|
||||
<label for="followupCustomerName">客户名称 <span style="color: red">*</span></label>
|
||||
<select id="followupCustomerName" name="customerName" required>
|
||||
<option value="">请选择客户</option>
|
||||
<option value="" disabled selected hidden>请选择客户</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="followupDealStatus">成交状态 <span style="color: red">*</span></label>
|
||||
<select id="followupDealStatus" name="dealStatus" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="" disabled selected hidden>请选择</option>
|
||||
<option value="未成交">未成交</option>
|
||||
<option value="已成交">已成交</option>
|
||||
</select>
|
||||
@ -546,7 +547,7 @@
|
||||
<label for="followupCustomerLevel">客户级别 <span
|
||||
style="color: red">*</span></label>
|
||||
<select id="followupCustomerLevel" name="customerLevel" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="" disabled selected hidden>请选择</option>
|
||||
<option value="A (重点关注)">A (重点关注)</option>
|
||||
<option value="B (很有希望)">B (很有希望)</option>
|
||||
<option value="C (常规意向)">C (常规意向)</option>
|
||||
@ -640,7 +641,7 @@
|
||||
<div class="form-group">
|
||||
<label for="createCustomerName">客户名称 <span style="color: red">*</span></label>
|
||||
<select id="createCustomerName" name="customerName" required>
|
||||
<option value="">请选择客户</option>
|
||||
<option value="" disabled selected hidden>请选择客户</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -656,10 +657,11 @@
|
||||
<div class="form-group">
|
||||
<label for="createType">类型 <span style="color: red">*</span></label>
|
||||
<select id="createType" name="type" required>
|
||||
<option value="">请选择类型</option>
|
||||
<option value="私有化">私有化</option>
|
||||
<option value="公有云">公有云</option>
|
||||
<option value="其他">其他</option>
|
||||
<option value="" disabled selected hidden>请选择类型</option>
|
||||
<option value="反馈">反馈</option>
|
||||
<option value="需求">需求</option>
|
||||
<option value="咨询">咨询</option>
|
||||
<option value="功能问题">功能问题</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -667,7 +669,7 @@
|
||||
<div class="form-group">
|
||||
<label for="createModule">模块</label>
|
||||
<select id="createModule" name="module">
|
||||
<option value="">请选择模块</option>
|
||||
<option value="" disabled selected hidden>请选择模块</option>
|
||||
<option value="数据生成">数据生成</option>
|
||||
<option value="数据集">数据集</option>
|
||||
<option value="数据空间">数据空间</option>
|
||||
@ -682,7 +684,7 @@
|
||||
<div class="form-group">
|
||||
<label for="createStatusProgress">状态与进度 <span style="color: red">*</span></label>
|
||||
<select id="createStatusProgress" name="statusProgress" required>
|
||||
<option value="">请选择状态</option>
|
||||
<option value="" disabled selected hidden>请选择状态</option>
|
||||
<option value="已完成">已完成</option>
|
||||
<option value="进行中">进行中</option>
|
||||
<option value="待排期">待排期</option>
|
||||
@ -699,6 +701,17 @@
|
||||
<label for="createSolution">解决方案</label>
|
||||
<textarea id="createSolution" name="solution" rows="3" placeholder="请输入拟采取的解决方案..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>截图 (支持多张)</label>
|
||||
<div class="screenshot-upload-container">
|
||||
<div id="createScreenshotPreview" class="screenshot-preview-list"></div>
|
||||
<label class="screenshot-upload-btn">
|
||||
<i class="fas fa-camera"></i>
|
||||
<span>上传截图</span>
|
||||
<input type="file" id="createScreenshots" accept="image/*" multiple hidden />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
@ -742,9 +755,11 @@
|
||||
<div class="form-group">
|
||||
<label for="editType">类型 <span style="color: red">*</span></label>
|
||||
<select id="editType" name="type" required>
|
||||
<option value="">请选择类型</option>
|
||||
<option value="私有化">私有化</option>
|
||||
<option value="公有云">公有云</option>
|
||||
<option value="" disabled selected hidden>请选择类型</option>
|
||||
<option value="反馈">反馈</option>
|
||||
<option value="需求">需求</option>
|
||||
<option value="咨询">咨询</option>
|
||||
<option value="功能问题">功能问题</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -753,7 +768,7 @@
|
||||
<div class="form-group">
|
||||
<label for="editModule">模块</label>
|
||||
<select id="editModule" name="module">
|
||||
<option value="">请选择模块</option>
|
||||
<option value="" disabled selected hidden>请选择模块</option>
|
||||
<option value="数据生成">数据生成</option>
|
||||
<option value="数据集">数据集</option>
|
||||
<option value="数据空间">数据空间</option>
|
||||
@ -768,7 +783,7 @@
|
||||
<div class="form-group">
|
||||
<label for="editStatusProgress">状态与进度 <span style="color: red">*</span></label>
|
||||
<select id="editStatusProgress" name="statusProgress" required>
|
||||
<option value="">请选择状态</option>
|
||||
<option value="" disabled selected hidden>请选择状态</option>
|
||||
<option value="已完成">已完成</option>
|
||||
<option value="进行中">进行中</option>
|
||||
<option value="待排期">待排期</option>
|
||||
@ -785,6 +800,17 @@
|
||||
<label for="editSolution">解决方案</label>
|
||||
<textarea id="editSolution" name="solution" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>截图 (支持多张)</label>
|
||||
<div class="screenshot-upload-container">
|
||||
<div id="editScreenshotPreview" class="screenshot-preview-list"></div>
|
||||
<label class="screenshot-upload-btn">
|
||||
<i class="fas fa-camera"></i>
|
||||
<span>上传截图</span>
|
||||
<input type="file" id="editScreenshots" accept="image/*" multiple hidden />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trial period details (Read-only reference) -->
|
||||
<div class="trial-details-section" style="margin-top: 15px; padding-top: 15px; border-top: 1px dashed #eee;">
|
||||
@ -829,13 +855,13 @@
|
||||
<div class="form-group">
|
||||
<label for="addFollowupCustomerName">客户名称 <span style="color: red">*</span></label>
|
||||
<select id="addFollowupCustomerName" name="customerName" required>
|
||||
<option value="">请选择客户</option>
|
||||
<option value="" disabled selected hidden>请选择客户</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="addFollowupDealStatus">成交状态 <span style="color: red">*</span></label>
|
||||
<select id="addFollowupDealStatus" name="dealStatus" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="" disabled selected hidden>请选择</option>
|
||||
<option value="已成交">已成交</option>
|
||||
<option value="未成交">未成交</option>
|
||||
</select>
|
||||
@ -845,7 +871,7 @@
|
||||
<div class="form-group">
|
||||
<label for="addFollowupCustomerLevel">客户级别 <span style="color: red">*</span></label>
|
||||
<select id="addFollowupCustomerLevel" name="customerLevel" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="" disabled selected hidden>请选择</option>
|
||||
<option value="A">A级 (重点客户)</option>
|
||||
<option value="B">B级 (潜在客户)</option>
|
||||
<option value="C">C级 (一般客户)</option>
|
||||
@ -888,13 +914,13 @@
|
||||
<div class="form-group">
|
||||
<label for="editFollowupCustomerName">客户名称 <span style="color: red">*</span></label>
|
||||
<select id="editFollowupCustomerName" name="customerName" required>
|
||||
<option value="">请选择客户</option>
|
||||
<option value="" disabled selected hidden>请选择客户</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="editFollowupDealStatus">成交状态 <span style="color: red">*</span></label>
|
||||
<select id="editFollowupDealStatus" name="dealStatus" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="" disabled selected hidden>请选择</option>
|
||||
<option value="已成交">已成交</option>
|
||||
<option value="未成交">未成交</option>
|
||||
</select>
|
||||
@ -904,7 +930,7 @@
|
||||
<div class="form-group">
|
||||
<label for="editFollowupCustomerLevel">客户级别 <span style="color: red">*</span></label>
|
||||
<select id="editFollowupCustomerLevel" name="customerLevel" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="" disabled selected hidden>请选择</option>
|
||||
<option value="A">A级 (重点客户)</option>
|
||||
<option value="B">B级 (潜在客户)</option>
|
||||
<option value="C">C级 (一般客户)</option>
|
||||
@ -1154,6 +1180,12 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Image Viewer Modal (Lightbox) -->
|
||||
<div id="imageViewerModal" class="image-viewer-modal" style="display: none">
|
||||
<span class="image-viewer-close">×</span>
|
||||
<img class="image-viewer-content" id="fullImage" />
|
||||
<div id="imageViewerCaption"></div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/static/js/main.js?v=2.6"></script>
|
||||
|
||||
@ -116,6 +116,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
let customerStartDate = "";
|
||||
let customerEndDate = "";
|
||||
let customerSearchQuery = "";
|
||||
let createUploadedScreenshots = [];
|
||||
let editUploadedScreenshots = [];
|
||||
|
||||
let cellTooltipEl = null;
|
||||
let tooltipAnchorCell = null;
|
||||
@ -470,7 +472,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const data = await response.json();
|
||||
|
||||
const customerSelect = document.getElementById("createCustomerName");
|
||||
customerSelect.innerHTML = '<option value="">请选择客户</option>';
|
||||
customerSelect.innerHTML = '<option value="" disabled selected hidden>请选择客户</option>';
|
||||
|
||||
if (data.customerNames && data.customerNames.length > 0) {
|
||||
data.customerNames.forEach((customerName) => {
|
||||
@ -630,6 +632,21 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.innerWidth <= 768) {
|
||||
sidebar.classList.remove("open");
|
||||
}
|
||||
|
||||
// Re-trigger animate-up animation for visible cards
|
||||
setTimeout(() => {
|
||||
const activeSection = document.querySelector('.content-section.active');
|
||||
if (activeSection) {
|
||||
const animatedCards = activeSection.querySelectorAll('.animate-up');
|
||||
animatedCards.forEach(card => {
|
||||
// Remove and re-add animation class to trigger animation
|
||||
card.style.animation = 'none';
|
||||
// Force reflow
|
||||
card.offsetHeight;
|
||||
card.style.animation = '';
|
||||
});
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Load customers from API
|
||||
@ -962,6 +979,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
{ value: customer.type || "", name: "type" },
|
||||
{ value: customer.module || "", name: "module" },
|
||||
{ value: customer.statusProgress || "", name: "statusProgress" },
|
||||
{ value: customer.screenshots || [], name: "screenshots" },
|
||||
];
|
||||
|
||||
fields.forEach((field) => {
|
||||
@ -973,6 +991,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
td.innerHTML = getStatusBadge(textValue);
|
||||
} else if (field.name === "customerName") {
|
||||
td.innerHTML = `<strong>${textValue}</strong>`;
|
||||
} else if (field.name === "screenshots") {
|
||||
td.classList.add("screenshot-column-cell");
|
||||
renderScreenshotsInCell(td, field.value);
|
||||
} else {
|
||||
td.textContent = textValue;
|
||||
}
|
||||
@ -1010,6 +1031,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
|
||||
checkTextOverflow();
|
||||
initImageViewer();
|
||||
|
||||
document.querySelectorAll(".edit-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", function () {
|
||||
@ -1095,6 +1117,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
module: getModuleValue("createModule", "createModuleOther"),
|
||||
statusProgress: document.getElementById("createStatusProgress").value,
|
||||
reporter: "", // Trial periods managed separately
|
||||
screenshots: createUploadedScreenshots,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -1108,6 +1131,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
if (response.ok) {
|
||||
createCustomerForm.reset();
|
||||
createUploadedScreenshots = [];
|
||||
renderScreenshotPreviews("create", []);
|
||||
// Hide module "other" input
|
||||
const createModuleOther = document.getElementById("createModuleOther");
|
||||
if (createModuleOther) createModuleOther.style.display = "none";
|
||||
@ -1180,6 +1205,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
document.getElementById("editStatusProgress").value =
|
||||
customer.statusProgress || "";
|
||||
|
||||
// Screenshots
|
||||
editUploadedScreenshots = customer.screenshots || [];
|
||||
renderScreenshotPreviews("edit", editUploadedScreenshots);
|
||||
|
||||
// Parse trial period and fill datetime inputs
|
||||
const reporter = customer.reporter || "";
|
||||
if (reporter) {
|
||||
@ -1247,6 +1276,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
module: getModuleValue("editModule", "editModuleOther"),
|
||||
statusProgress: document.getElementById("editStatusProgress").value,
|
||||
reporter: "", // Trial periods managed separately
|
||||
screenshots: editUploadedScreenshots,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -1976,7 +2006,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const selectElement = document.getElementById(selectId);
|
||||
if (!selectElement) return;
|
||||
|
||||
selectElement.innerHTML = '<option value="">请选择客户</option>';
|
||||
selectElement.innerHTML = '<option value="" disabled selected hidden>请选择客户</option>';
|
||||
if (data.customerNames && data.customerNames.length > 0) {
|
||||
data.customerNames.forEach((customerName) => {
|
||||
const option = document.createElement("option");
|
||||
@ -3144,4 +3174,179 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// ========== Screenshot & Image Viewer Logic ==========
|
||||
|
||||
function renderScreenshotsInCell(td, screenshots) {
|
||||
td.innerHTML = "";
|
||||
|
||||
if (!screenshots || screenshots.length === 0) {
|
||||
td.innerHTML = '<span class="no-screenshots">无</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "screenshot-wrapper";
|
||||
|
||||
screenshots.forEach((path) => {
|
||||
const img = document.createElement("img");
|
||||
img.src = path;
|
||||
img.className = "screenshot-thumbnail";
|
||||
img.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const viewer = document.getElementById("imageViewerModal");
|
||||
const fullImg = document.getElementById("fullImage");
|
||||
if (viewer && fullImg) {
|
||||
fullImg.src = path;
|
||||
viewer.style.display = "flex";
|
||||
}
|
||||
};
|
||||
wrapper.appendChild(img);
|
||||
});
|
||||
|
||||
td.appendChild(wrapper);
|
||||
}
|
||||
|
||||
function initImageViewer() {
|
||||
const viewer = document.getElementById("imageViewerModal");
|
||||
const closeBtn = document.querySelector(".image-viewer-close");
|
||||
|
||||
if (!viewer || !closeBtn) return;
|
||||
|
||||
closeBtn.onclick = () => {
|
||||
viewer.style.display = "none";
|
||||
};
|
||||
|
||||
viewer.onclick = (e) => {
|
||||
if (e.target === viewer) {
|
||||
viewer.style.display = "none";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// File upload handling for modals
|
||||
function handleScreenshotUpload(type) {
|
||||
const input = document.getElementById(
|
||||
type === "create" ? "createScreenshots" : "editScreenshots",
|
||||
);
|
||||
const previewList = document.getElementById(
|
||||
type === "create" ? "createScreenshotPreview" : "editScreenshotPreview",
|
||||
);
|
||||
|
||||
if (!input || !previewList) return;
|
||||
|
||||
input.addEventListener("change", async function () {
|
||||
if (!this.files.length) return;
|
||||
|
||||
const formData = new FormData();
|
||||
for (const file of this.files) {
|
||||
formData.append("screenshots", file);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch("/api/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const filePaths = data.filePaths || [];
|
||||
|
||||
if (type === "create") {
|
||||
createUploadedScreenshots = [
|
||||
...createUploadedScreenshots,
|
||||
...filePaths,
|
||||
];
|
||||
renderScreenshotPreviews("create", createUploadedScreenshots);
|
||||
} else {
|
||||
editUploadedScreenshots = [...editUploadedScreenshots, ...filePaths];
|
||||
renderScreenshotPreviews("edit", editUploadedScreenshots);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error uploading screenshots:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderScreenshotPreviews(type, screenshots) {
|
||||
const previewList = document.getElementById(
|
||||
type === "create" ? "createScreenshotPreview" : "editScreenshotPreview",
|
||||
);
|
||||
if (!previewList) return;
|
||||
|
||||
previewList.innerHTML = "";
|
||||
|
||||
screenshots.forEach((path, index) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "preview-item";
|
||||
item.innerHTML = `
|
||||
<img src="${path}" alt="Screenshot">
|
||||
<span class="remove-preview" data-index="${index}">×</span>
|
||||
`;
|
||||
|
||||
item.querySelector(".remove-preview").onclick = () => {
|
||||
if (type === "create") {
|
||||
createUploadedScreenshots.splice(index, 1);
|
||||
renderScreenshotPreviews("create", createUploadedScreenshots);
|
||||
} else {
|
||||
editUploadedScreenshots.splice(index, 1);
|
||||
renderScreenshotPreviews("edit", editUploadedScreenshots);
|
||||
}
|
||||
};
|
||||
|
||||
previewList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize upload handlers
|
||||
handleScreenshotUpload("create");
|
||||
handleScreenshotUpload("edit");
|
||||
|
||||
// ========== Select Placeholder Color Control ==========
|
||||
// Function to update select color based on value
|
||||
function updateSelectColor(select) {
|
||||
if (!select.value || select.value === "") {
|
||||
select.classList.add("placeholder-active");
|
||||
} else {
|
||||
select.classList.remove("placeholder-active");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to all select elements
|
||||
const allSelects = document.querySelectorAll("select");
|
||||
allSelects.forEach((select) => {
|
||||
// Set initial color
|
||||
updateSelectColor(select);
|
||||
|
||||
// Update color on change
|
||||
select.addEventListener("change", function () {
|
||||
updateSelectColor(this);
|
||||
});
|
||||
|
||||
// Also update on input (for some browsers)
|
||||
select.addEventListener("input", function () {
|
||||
updateSelectColor(this);
|
||||
});
|
||||
});
|
||||
|
||||
// Re-apply when modals open (for dynamically populated selects)
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if (mutation.type === "attributes" && mutation.attributeName === "style") {
|
||||
const target = mutation.target;
|
||||
if (target.classList.contains("modal") && target.style.display === "block") {
|
||||
setTimeout(() => {
|
||||
target.querySelectorAll("select").forEach(updateSelectColor);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Observe all modals
|
||||
document.querySelectorAll(".modal").forEach((modal) => {
|
||||
observer.observe(modal, { attributes: true });
|
||||
});
|
||||
});
|
||||
|
||||
@ -143,7 +143,7 @@ function populateCustomerSelect() {
|
||||
const select = document.getElementById('trialCustomerSelect');
|
||||
if (!select) return;
|
||||
|
||||
select.innerHTML = '<option value="">-- 选择已有客户或输入新客户 --</option>';
|
||||
select.innerHTML = '<option value="" disabled selected hidden>-- 选择已有客户或输入新客户 --</option>';
|
||||
|
||||
// Get unique names from customersMap
|
||||
const names = [...new Set(Object.values(customersMap))].sort();
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ toolchain go1.24.3
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/xuri/excelize/v2 v2.8.0
|
||||
)
|
||||
|
||||
|
||||
2
go.sum
2
go.sum
@ -7,6 +7,8 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@ -8,6 +8,8 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -15,6 +17,7 @@ import (
|
||||
"crm-go/internal/storage"
|
||||
"crm-go/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
@ -128,6 +131,7 @@ func (h *CustomerHandler) CreateCustomer(w http.ResponseWriter, r *http.Request)
|
||||
Module: req.Module,
|
||||
StatusProgress: req.StatusProgress,
|
||||
Reporter: req.Reporter,
|
||||
Screenshots: req.Screenshots,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
@ -185,6 +189,63 @@ func (h *CustomerHandler) UpdateCustomer(w http.ResponseWriter, r *http.Request)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *CustomerHandler) UploadScreenshots(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse multipart form
|
||||
err := r.ParseMultipartForm(32 << 20) // 32MB max
|
||||
if err != nil {
|
||||
http.Error(w, "Unable to parse form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
files := r.MultipartForm.File["screenshots"]
|
||||
if len(files) == 0 {
|
||||
http.Error(w, "No files uploaded", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure upload directory exists
|
||||
uploadDir := "./frontend/uploads/screenshots"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var filePaths []string
|
||||
for _, fileHeader := range files {
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Generate unique filename
|
||||
ext := filepath.Ext(fileHeader.Filename)
|
||||
newFilename := uuid.New().String() + ext
|
||||
filePath := filepath.Join(uploadDir, newFilename)
|
||||
|
||||
// Create file on disk
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Copy file content
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Save relative path for frontend
|
||||
relativePath := "/static/uploads/screenshots/" + newFilename
|
||||
filePaths = append(filePaths, relativePath)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"filePaths": filePaths,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CustomerHandler) DeleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract customer ID from URL path
|
||||
urlPath := r.URL.Path
|
||||
|
||||
@ -147,6 +147,9 @@ func (cs *customerStorage) UpdateCustomer(id string, updates models.UpdateCustom
|
||||
if updates.Reporter != nil {
|
||||
customers[i].Reporter = *updates.Reporter
|
||||
}
|
||||
if updates.Screenshots != nil {
|
||||
customers[i].Screenshots = *updates.Screenshots
|
||||
}
|
||||
|
||||
return cs.SaveCustomers(customers)
|
||||
}
|
||||
|
||||
@ -43,6 +43,105 @@ func InitDB(config DBConfig) error {
|
||||
}
|
||||
|
||||
log.Println("✅ Database connection established")
|
||||
|
||||
// 自动迁移表结构
|
||||
if err := autoMigrate(); err != nil {
|
||||
return fmt.Errorf("failed to auto migrate: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoMigrate 自动创建或更新表结构
|
||||
func autoMigrate() error {
|
||||
// 创建 customers 表
|
||||
createCustomersTable := `
|
||||
CREATE TABLE IF NOT EXISTS customers (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
customer_name VARCHAR(255) NOT NULL,
|
||||
intended_product VARCHAR(255),
|
||||
version VARCHAR(100),
|
||||
description TEXT,
|
||||
solution TEXT,
|
||||
type VARCHAR(100),
|
||||
module VARCHAR(100),
|
||||
status_progress VARCHAR(100),
|
||||
reporter VARCHAR(255),
|
||||
screenshots TEXT,
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`
|
||||
|
||||
if _, err := db.Exec(createCustomersTable); err != nil {
|
||||
return fmt.Errorf("failed to create customers table: %v", err)
|
||||
}
|
||||
|
||||
// 创建 followups 表
|
||||
createFollowupsTable := `
|
||||
CREATE TABLE IF NOT EXISTS followups (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
customer_name VARCHAR(255) NOT NULL,
|
||||
deal_status VARCHAR(50),
|
||||
customer_level VARCHAR(50),
|
||||
industry VARCHAR(100),
|
||||
follow_up_time DATETIME,
|
||||
notification_sent BOOLEAN DEFAULT FALSE,
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_follow_up_time (follow_up_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`
|
||||
|
||||
if _, err := db.Exec(createFollowupsTable); err != nil {
|
||||
return fmt.Errorf("failed to create followups table: %v", err)
|
||||
}
|
||||
|
||||
// 创建 trial_periods 表
|
||||
createTrialPeriodsTable := `
|
||||
CREATE TABLE IF NOT EXISTS trial_periods (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
customer_name VARCHAR(255) NOT NULL,
|
||||
source VARCHAR(100),
|
||||
intended_product VARCHAR(255),
|
||||
start_time DATETIME,
|
||||
end_time DATETIME,
|
||||
is_trial BOOLEAN DEFAULT TRUE,
|
||||
created_at DATETIME NOT NULL,
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_end_time (end_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`
|
||||
|
||||
if _, err := db.Exec(createTrialPeriodsTable); err != nil {
|
||||
return fmt.Errorf("failed to create trial_periods table: %v", err)
|
||||
}
|
||||
|
||||
// 检查并添加 screenshots 列(如果不存在)
|
||||
// MySQL 不支持 IF NOT EXISTS,所以我们需要先检查列是否存在
|
||||
var columnExists int
|
||||
err := db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'customers'
|
||||
AND COLUMN_NAME = 'screenshots'
|
||||
`).Scan(&columnExists)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check screenshots column: %v", err)
|
||||
}
|
||||
|
||||
if columnExists == 0 {
|
||||
_, err = db.Exec("ALTER TABLE customers ADD COLUMN screenshots TEXT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add screenshots column: %v", err)
|
||||
}
|
||||
log.Println("✅ Added screenshots column to customers table")
|
||||
}
|
||||
|
||||
log.Println("✅ Database tables migrated successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ func NewMySQLCustomerStorage() CustomerStorage {
|
||||
func (cs *mysqlCustomerStorage) GetAllCustomers() ([]models.Customer, error) {
|
||||
query := `
|
||||
SELECT id, created_at, customer_name, intended_product, version,
|
||||
description, solution, type, module, status_progress, reporter
|
||||
description, solution, type, module, status_progress, reporter, screenshots
|
||||
FROM customers
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
@ -38,12 +38,12 @@ func (cs *mysqlCustomerStorage) GetAllCustomers() ([]models.Customer, error) {
|
||||
var customers []models.Customer
|
||||
for rows.Next() {
|
||||
var c models.Customer
|
||||
var intendedProduct, version, description, solution, typ, module, statusProgress, reporter sql.NullString
|
||||
var intendedProduct, version, description, solution, typ, module, statusProgress, reporter, screenshots sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
&c.ID, &c.CreatedAt, &c.CustomerName,
|
||||
&intendedProduct, &version, &description,
|
||||
&solution, &typ, &module, &statusProgress, &reporter,
|
||||
&solution, &typ, &module, &statusProgress, &reporter, &screenshots,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -57,6 +57,11 @@ func (cs *mysqlCustomerStorage) GetAllCustomers() ([]models.Customer, error) {
|
||||
c.Module = module.String
|
||||
c.StatusProgress = statusProgress.String
|
||||
c.Reporter = reporter.String
|
||||
if screenshots.Valid && screenshots.String != "" {
|
||||
c.Screenshots = strings.Split(screenshots.String, ",")
|
||||
} else {
|
||||
c.Screenshots = []string{}
|
||||
}
|
||||
|
||||
customers = append(customers, c)
|
||||
}
|
||||
@ -67,18 +72,18 @@ func (cs *mysqlCustomerStorage) GetAllCustomers() ([]models.Customer, error) {
|
||||
func (cs *mysqlCustomerStorage) GetCustomerByID(id string) (*models.Customer, error) {
|
||||
query := `
|
||||
SELECT id, created_at, customer_name, intended_product, version,
|
||||
description, solution, type, module, status_progress, reporter
|
||||
description, solution, type, module, status_progress, reporter, screenshots
|
||||
FROM customers
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
var c models.Customer
|
||||
var intendedProduct, version, description, solution, typ, module, statusProgress, reporter sql.NullString
|
||||
var intendedProduct, version, description, solution, typ, module, statusProgress, reporter, screenshots sql.NullString
|
||||
|
||||
err := cs.db.QueryRow(query, id).Scan(
|
||||
&c.ID, &c.CreatedAt, &c.CustomerName,
|
||||
&intendedProduct, &version, &description,
|
||||
&solution, &typ, &module, &statusProgress, &reporter,
|
||||
&solution, &typ, &module, &statusProgress, &reporter, &screenshots,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@ -96,6 +101,11 @@ func (cs *mysqlCustomerStorage) GetCustomerByID(id string) (*models.Customer, er
|
||||
c.Module = module.String
|
||||
c.StatusProgress = statusProgress.String
|
||||
c.Reporter = reporter.String
|
||||
if screenshots.Valid && screenshots.String != "" {
|
||||
c.Screenshots = strings.Split(screenshots.String, ",")
|
||||
} else {
|
||||
c.Screenshots = []string{}
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
@ -110,15 +120,17 @@ func (cs *mysqlCustomerStorage) CreateCustomer(customer models.Customer) error {
|
||||
|
||||
query := `
|
||||
INSERT INTO customers (id, created_at, customer_name, intended_product, version,
|
||||
description, solution, type, module, status_progress, reporter)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
description, solution, type, module, status_progress, reporter, screenshots)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
screenshots := strings.Join(customer.Screenshots, ",")
|
||||
|
||||
_, err := cs.db.Exec(query,
|
||||
customer.ID, customer.CreatedAt, customer.CustomerName,
|
||||
customer.IntendedProduct, customer.Version, customer.Description,
|
||||
customer.Solution, customer.Type, customer.Module,
|
||||
customer.StatusProgress, customer.Reporter,
|
||||
customer.StatusProgress, customer.Reporter, screenshots,
|
||||
)
|
||||
|
||||
return err
|
||||
@ -159,19 +171,24 @@ func (cs *mysqlCustomerStorage) UpdateCustomer(id string, updates models.UpdateC
|
||||
if updates.Reporter != nil {
|
||||
existing.Reporter = *updates.Reporter
|
||||
}
|
||||
if updates.Screenshots != nil {
|
||||
existing.Screenshots = *updates.Screenshots
|
||||
}
|
||||
|
||||
query := `
|
||||
UPDATE customers
|
||||
SET customer_name = ?, intended_product = ?, version = ?,
|
||||
description = ?, solution = ?, type = ?,
|
||||
module = ?, status_progress = ?, reporter = ?
|
||||
module = ?, status_progress = ?, reporter = ?, screenshots = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
screenshots := strings.Join(existing.Screenshots, ",")
|
||||
|
||||
_, err = cs.db.Exec(query,
|
||||
existing.CustomerName, existing.IntendedProduct, existing.Version,
|
||||
existing.Description, existing.Solution, existing.Type,
|
||||
existing.Module, existing.StatusProgress, existing.Reporter,
|
||||
existing.Module, existing.StatusProgress, existing.Reporter, screenshots,
|
||||
id,
|
||||
)
|
||||
|
||||
|
||||
@ -14,28 +14,31 @@ type Customer struct {
|
||||
Module string `json:"module" csv:"module"`
|
||||
StatusProgress string `json:"statusProgress" csv:"statusProgress"`
|
||||
Reporter string `json:"reporter" csv:"reporter"`
|
||||
Screenshots []string `json:"screenshots" csv:"screenshots"`
|
||||
}
|
||||
|
||||
type CreateCustomerRequest struct {
|
||||
CustomerName string `json:"customerName"`
|
||||
IntendedProduct string `json:"intendedProduct"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Solution string `json:"solution"`
|
||||
Type string `json:"type"`
|
||||
Module string `json:"module"`
|
||||
StatusProgress string `json:"statusProgress"`
|
||||
Reporter string `json:"reporter"`
|
||||
CustomerName string `json:"customerName"`
|
||||
IntendedProduct string `json:"intendedProduct"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Solution string `json:"solution"`
|
||||
Type string `json:"type"`
|
||||
Module string `json:"module"`
|
||||
StatusProgress string `json:"statusProgress"`
|
||||
Reporter string `json:"reporter"`
|
||||
Screenshots []string `json:"screenshots"`
|
||||
}
|
||||
|
||||
type UpdateCustomerRequest struct {
|
||||
CustomerName *string `json:"customerName"`
|
||||
IntendedProduct *string `json:"intendedProduct"`
|
||||
Version *string `json:"version"`
|
||||
Description *string `json:"description"`
|
||||
Solution *string `json:"solution"`
|
||||
Type *string `json:"type"`
|
||||
Module *string `json:"module"`
|
||||
StatusProgress *string `json:"statusProgress"`
|
||||
Reporter *string `json:"reporter"`
|
||||
CustomerName *string `json:"customerName"`
|
||||
IntendedProduct *string `json:"intendedProduct"`
|
||||
Version *string `json:"version"`
|
||||
Description *string `json:"description"`
|
||||
Solution *string `json:"solution"`
|
||||
Type *string `json:"type"`
|
||||
Module *string `json:"module"`
|
||||
StatusProgress *string `json:"statusProgress"`
|
||||
Reporter *string `json:"reporter"`
|
||||
Screenshots *[]string `json:"screenshots"`
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user