fix: align reflection frontend API calls with backend

- trReflSaveEdit now uses POST /reflections for new and PUT /reflections/{id} for updates
- Tags are synced separately via /trade-pairs/{id}/tags endpoints
- trReflSaveDaily uses correct field names: overall_rating, market_judgment, improvements
- trReflOpenAIModal now calls /reanalyze/{id}/latest and falls back to POST /reanalyze
- trReflSaveExperience uses /ai-analysis/{id}/save-experience with analysis_id
- Added save-and-analyze flow and tag sync helpers
- Verified JS syntax with node --check
refactor3.0
Lxy 2 weeks ago
parent eaa051d0ee
commit 3d7ddd5892

@ -635,6 +635,7 @@ let trReflTags = [];
let trReflExperiences = []; let trReflExperiences = [];
let trReflTagState = new Set(); let trReflTagState = new Set();
let trReflCurrentPairId = null; let trReflCurrentPairId = null;
let trReflCurrentReflectionId = null;
let trReflCurrentAIResult = null; let trReflCurrentAIResult = null;
function trSwitchSubtab(tab) { function trSwitchSubtab(tab) {
@ -673,6 +674,14 @@ function trReflInit() {
trReflSaveDaily(); trReflSaveDaily();
}); });
const saveAndAiBtn = document.getElementById('tr-refl-save-and-ai');
if (saveAndAiBtn) {
saveAndAiBtn.addEventListener('click', e => {
e.preventDefault();
trReflSaveAndAnalyze();
});
}
document.querySelectorAll('#tr-refl-discipline-score .tr-form-star, #tr-refl-daily-discipline-score .tr-form-star').forEach(star => { document.querySelectorAll('#tr-refl-discipline-score .tr-form-star, #tr-refl-daily-discipline-score .tr-form-star').forEach(star => {
star.addEventListener('click', () => { star.addEventListener('click', () => {
const container = star.parentElement; const container = star.parentElement;
@ -751,10 +760,10 @@ function trReflRender() {
document.getElementById('tr-refl-emotion').className = 'tr-refl-daily-value ' + (daily.emotion_state ? '' : 'empty'); document.getElementById('tr-refl-emotion').className = 'tr-refl-daily-value ' + (daily.emotion_state ? '' : 'empty');
document.getElementById('tr-refl-discipline').textContent = daily.discipline_score ? daily.discipline_score + '/5' : '未填写'; document.getElementById('tr-refl-discipline').textContent = daily.discipline_score ? daily.discipline_score + '/5' : '未填写';
document.getElementById('tr-refl-discipline').className = 'tr-refl-daily-value ' + (daily.discipline_score ? '' : 'empty'); document.getElementById('tr-refl-discipline').className = 'tr-refl-daily-value ' + (daily.discipline_score ? '' : 'empty');
document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_evaluation] || daily.overall_evaluation || '未填写'; document.getElementById('tr-refl-overall').textContent = trReflTextMap.overall[daily.overall_rating] || daily.overall_rating || '未填写';
document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_evaluation ? '' : 'empty'); document.getElementById('tr-refl-overall').className = 'tr-refl-daily-value ' + (daily.overall_rating ? '' : 'empty');
document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgement] || daily.market_judgement || '未填写'; document.getElementById('tr-refl-market').textContent = trReflTextMap.market[daily.market_judgment] || daily.market_judgment || '未填写';
document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgement ? '' : 'empty'); document.getElementById('tr-refl-market').className = 'tr-refl-daily-value ' + (daily.market_judgment ? '' : 'empty');
const pairsContainer = document.getElementById('tr-refl-pairs'); const pairsContainer = document.getElementById('tr-refl-pairs');
const pairs = data.pairs || []; const pairs = data.pairs || [];
@ -792,8 +801,8 @@ function trReflRenderPairCard(p) {
const pnlCls = pnl >= 0 ? 'profit' : 'loss'; const pnlCls = pnl >= 0 ? 'profit' : 'loss';
const dirCls = p.direction === '多' ? 'long' : 'short'; const dirCls = p.direction === '多' ? 'long' : 'short';
const dirText = p.direction === '多' ? '做多' : '做空'; const dirText = p.direction === '多' ? '做多' : '做空';
const tags = (p.tags || []).map(t => `<span class="tr-refl-tag">${t}</span>`).join(''); const tags = (p.tags || []).map(t => `<span class="tr-refl-tag">${typeof t === 'string' ? t : t.name}</span>`).join('');
const hasReflection = p.reflection && (p.reflection.entry_reason || p.reflection.free_reflection); const hasReflection = p.reflection_status === 'done';
return `<div class="tr-refl-card ${pnlCls}"> return `<div class="tr-refl-card ${pnlCls}">
<div class="tr-refl-card-header"> <div class="tr-refl-card-header">
<div> <div>
@ -835,9 +844,30 @@ async function trReflOpenEditModal(pairId) {
const pair = data.pairs.find(p => p.id === pairId); const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return; if (!pair) return;
trReflCurrentPairId = pairId; trReflCurrentPairId = pairId;
const reflection = pair.reflection || {}; trReflCurrentReflectionId = null;
document.getElementById('tr-refl-edit-pair-id').value = pairId; document.getElementById('tr-refl-edit-pair-id').value = pairId;
let reflection = {};
let selectedTagNames = new Set();
try {
const [reflRes, tagsRes] = await Promise.all([
fetch(`${TR_API_BASE}/reflections?trade_pair_id=${pairId}`),
fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`),
]);
const reflJson = await reflRes.json();
const tagsJson = await tagsRes.json();
if (reflJson.success && reflJson.data && reflJson.data.length) {
reflection = reflJson.data[0];
trReflCurrentReflectionId = reflection.id || null;
}
if (tagsJson.success && tagsJson.data) {
selectedTagNames = new Set(tagsJson.data.map(t => typeof t === 'string' ? t : t.name));
}
} catch (e) {
console.error('加载反思/标签失败', e);
}
document.getElementById('tr-refl-entry-reason').value = reflection.entry_reason || ''; document.getElementById('tr-refl-entry-reason').value = reflection.entry_reason || '';
document.getElementById('tr-refl-exit-reason').value = reflection.exit_reason || ''; document.getElementById('tr-refl-exit-reason').value = reflection.exit_reason || '';
document.getElementById('tr-refl-entry-timing').value = reflection.entry_timing || ''; document.getElementById('tr-refl-entry-timing').value = reflection.entry_timing || '';
@ -852,10 +882,9 @@ async function trReflOpenEditModal(pairId) {
// 渲染标签选择 // 渲染标签选择
const tagPool = document.getElementById('tr-refl-edit-tags'); const tagPool = document.getElementById('tr-refl-edit-tags');
const selected = new Set(reflection.tags || pair.tags || []);
tagPool.innerHTML = trReflTags.map(t => { tagPool.innerHTML = trReflTags.map(t => {
const name = typeof t === 'string' ? t : t.name; const name = typeof t === 'string' ? t : t.name;
const active = selected.has(name) ? 'selected' : ''; const active = selectedTagNames.has(name) ? 'selected' : '';
return `<span class="tr-tag-chip ${active}" onclick="this.classList.toggle('selected')">${name}</span>`; return `<span class="tr-tag-chip ${active}" onclick="this.classList.toggle('selected')">${name}</span>`;
}).join(''); }).join('');
@ -865,15 +894,16 @@ async function trReflOpenEditModal(pairId) {
function trReflCloseEditModal() { function trReflCloseEditModal() {
document.getElementById('tr-refl-edit-modal').classList.remove('show'); document.getElementById('tr-refl-edit-modal').classList.remove('show');
trReflCurrentPairId = null; trReflCurrentPairId = null;
trReflCurrentReflectionId = null;
} }
async function trReflSaveEdit() { async function trReflSaveEdit(andAnalyze = false) {
const pairId = parseInt(document.getElementById('tr-refl-edit-pair-id').value); const pairId = parseInt(document.getElementById('tr-refl-edit-pair-id').value);
if (!pairId) return; if (!pairId) return;
const selectedTags = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent);
const body = { const body = {
trade_pair_id: pairId, trade_pair_id: pairId,
trade_date: trReflCurrentDate,
entry_reason: document.getElementById('tr-refl-entry-reason').value, entry_reason: document.getElementById('tr-refl-entry-reason').value,
exit_reason: document.getElementById('tr-refl-exit-reason').value, exit_reason: document.getElementById('tr-refl-exit-reason').value,
entry_timing: document.getElementById('tr-refl-entry-timing').value, entry_timing: document.getElementById('tr-refl-entry-timing').value,
@ -881,36 +911,108 @@ async function trReflSaveEdit() {
position_management: document.getElementById('tr-refl-position').value, position_management: document.getElementById('tr-refl-position').value,
discipline_score: parseInt(document.getElementById('tr-refl-discipline-score').dataset.value || '0'), discipline_score: parseInt(document.getElementById('tr-refl-discipline-score').dataset.value || '0'),
free_reflection: document.getElementById('tr-refl-free').value, free_reflection: document.getElementById('tr-refl-free').value,
tags: selectedTags,
}; };
try { try {
const res = await fetch(`${TR_API_BASE}/reflections`, { const url = trReflCurrentReflectionId
method: 'POST', ? `${TR_API_BASE}/reflections/${trReflCurrentReflectionId}`
: `${TR_API_BASE}/reflections`;
const method = trReflCurrentReflectionId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
const json = await res.json(); const json = await res.json();
if (json.success) { if (!json.success) {
showToast('error', '保存失败', json.message || '请重试');
return;
}
// 同步标签(编辑 Modal 中的标签选择)
const selectedTagNames = Array.from(document.querySelectorAll('#tr-refl-edit-tags .tr-tag-chip.selected')).map(el => el.textContent);
try {
await trReflSyncPairTags(pairId, selectedTagNames);
} catch (e) {
console.error('标签同步失败', e);
}
if (andAnalyze) {
try {
const reRes = await fetch(`${TR_API_BASE}/reanalyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ trade_pair_id: pairId }),
});
const reJson = await reRes.json();
if (reJson.success && reJson.data) {
// 重新分析接口不返回 id需要再取一次 latest 获取完整记录
const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`);
const latestJson = await latestRes.json();
if (latestJson.success && latestJson.data) {
trReflCurrentAIResult = latestJson.data;
} else {
trReflCurrentAIResult = reJson.data;
}
trReflCloseEditModal();
trReflOpenAIModal(pairId, false);
return;
}
showToast('error', 'AI分析失败', reJson.message || '请重试');
return;
} catch (e) {
showToast('error', 'AI分析失败', e.message);
return;
}
}
showToast('success', '保存成功', '反思已保存'); showToast('success', '保存成功', '反思已保存');
trReflCloseEditModal(); trReflCloseEditModal();
trReflLoadData(); trReflLoadData();
} else {
showToast('error', '保存失败', json.message || '请重试');
}
} catch (e) { } catch (e) {
showToast('error', '保存失败', e.message); showToast('error', '保存失败', e.message);
} }
} }
async function trReflSaveAndAnalyze() {
await trReflSaveEdit(true);
}
async function trReflSyncPairTags(pairId, selectedNames) {
const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`);
const json = await res.json();
const currentTags = (json.success ? json.data : []) || [];
const currentNames = new Set(currentTags.map(t => typeof t === 'string' ? t : t.name));
// 新增标签
for (const name of selectedNames) {
if (currentNames.has(name)) continue;
const tag = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name);
if (!tag || typeof tag === 'string') continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tag.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
}
// 移除标签
for (const tag of currentTags) {
const name = typeof tag === 'string' ? tag : tag.name;
if (selectedNames.includes(name)) continue;
const tagId = typeof tag === 'string' ? null : tag.id;
if (!tagId) continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' });
}
}
// ===== 当日反思 Modal ===== // ===== 当日反思 Modal =====
function trReflOpenDailyModal() { function trReflOpenDailyModal() {
const daily = (trReflData && trReflData.daily_reflection) || {}; const daily = (trReflData && trReflData.daily_reflection) || {};
document.getElementById('tr-refl-daily-emotion').value = daily.emotion_state || ''; document.getElementById('tr-refl-daily-emotion').value = daily.emotion_state || '';
document.getElementById('tr-refl-daily-overall').value = daily.overall_evaluation || ''; document.getElementById('tr-refl-daily-overall').value = daily.overall_rating || '';
document.getElementById('tr-refl-daily-market').value = daily.market_judgement || ''; document.getElementById('tr-refl-daily-market').value = daily.market_judgment || '';
document.getElementById('tr-refl-daily-summary').value = daily.summary || ''; document.getElementById('tr-refl-daily-summary').value = daily.summary || '';
document.getElementById('tr-refl-daily-improvement').value = daily.improvement || ''; document.getElementById('tr-refl-daily-improvement').value = daily.improvements || '';
const score = daily.discipline_score || 0; const score = daily.discipline_score || 0;
const container = document.getElementById('tr-refl-daily-discipline-score'); const container = document.getElementById('tr-refl-daily-discipline-score');
@ -926,13 +1028,13 @@ function trReflCloseDailyModal() {
async function trReflSaveDaily() { async function trReflSaveDaily() {
const body = { const body = {
trade_date: trReflCurrentDate, reflection_date: trReflCurrentDate,
emotion_state: document.getElementById('tr-refl-daily-emotion').value, emotion_state: document.getElementById('tr-refl-daily-emotion').value,
overall_evaluation: document.getElementById('tr-refl-daily-overall').value, overall_rating: document.getElementById('tr-refl-daily-overall').value,
market_judgement: document.getElementById('tr-refl-daily-market').value, market_judgment: document.getElementById('tr-refl-daily-market').value,
discipline_score: parseInt(document.getElementById('tr-refl-daily-discipline-score').dataset.value || '0'), discipline_score: parseInt(document.getElementById('tr-refl-daily-discipline-score').dataset.value || '0'),
summary: document.getElementById('tr-refl-daily-summary').value, summary: document.getElementById('tr-refl-daily-summary').value,
improvement: document.getElementById('tr-refl-daily-improvement').value, improvements: document.getElementById('tr-refl-daily-improvement').value,
}; };
try { try {
@ -960,7 +1062,15 @@ async function trReflOpenTagsModal(pairId) {
const pair = data.pairs.find(p => p.id === pairId); const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return; if (!pair) return;
trReflCurrentPairId = pairId; trReflCurrentPairId = pairId;
trReflTagState = new Set((pair.tags || []).map(t => typeof t === 'string' ? t : t.name)); trReflTagState = new Set();
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`);
const json = await res.json();
if (json.success && json.data) {
trReflTagState = new Set(json.data.map(t => typeof t === 'string' ? t : t.name));
}
} catch (e) { console.error('加载交易标签失败', e); }
document.getElementById('tr-refl-tags-pair-id').value = pairId; document.getElementById('tr-refl-tags-pair-id').value = pairId;
trReflRenderTagPreset(); trReflRenderTagPreset();
@ -1009,36 +1119,58 @@ function trReflAddCustomTag() {
trReflRenderTagPreset(); trReflRenderTagPreset();
} }
async function trReflSaveTags() { async function trReflResolveTagId(name) {
const pairId = parseInt(document.getElementById('tr-refl-tags-pair-id').value); const existing = trReflTags.find(t => (typeof t === 'string' ? t : t.name) === name);
if (!pairId) return; if (existing && typeof existing === 'object' && existing.id) return existing.id;
const currentTags = Array.from(trReflTagState);
const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId);
const oldTags = (pair && pair.tags) || [];
const oldNames = new Set(oldTags.map(t => typeof t === 'string' ? t : t.name));
// 对自定义标签创建后再使用
try { try {
for (const tag of currentTags) { const res = await fetch(`${TR_API_BASE}/tags`, {
if (!oldNames.has(tag)) {
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag }), body: JSON.stringify({ name, category: 'custom' }),
}); });
const json = await res.json();
if (json.success && json.data && json.data.id) {
trReflTags.push({ id: json.data.id, name, category: 'custom', is_preset: false });
return json.data.id;
} }
} catch (e) { console.error('创建标签失败', e); }
return null;
} }
for (const tag of oldTags) {
const name = typeof tag === 'string' ? tag : tag.name; async function trReflSaveTags() {
if (!trReflTagState.has(name)) { const pairId = parseInt(document.getElementById('tr-refl-tags-pair-id').value);
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`, { if (!pairId) return;
method: 'DELETE',
const selectedNames = Array.from(trReflTagState);
try {
const res = await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags`);
const json = await res.json();
const currentTags = (json.success ? json.data : []) || [];
const currentMap = new Map(currentTags.map(t => [
typeof t === 'string' ? t : t.name,
typeof t === 'string' ? null : t.id,
]));
// 新增标签
for (const name of selectedNames) {
if (currentMap.has(name)) continue;
const tagId = await trReflResolveTagId(name);
if (!tagId) continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags?tag_id=${tagId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag: name }),
}); });
} }
// 移除标签
for (const [name, tagId] of currentMap) {
if (selectedNames.includes(name) || !tagId) continue;
await fetch(`${TR_API_BASE}/trade-pairs/${pairId}/tags/${tagId}`, { method: 'DELETE' });
} }
showToast('success', '保存成功', '标签已更新'); showToast('success', '保存成功', '标签已更新');
trReflCloseTagsModal(); trReflCloseTagsModal();
trReflLoadData(); trReflLoadData();
@ -1053,43 +1185,50 @@ function trReflCloseTagsModal() {
} }
// ===== AI 分析结果 Modal ===== // ===== AI 分析结果 Modal =====
async function trReflOpenAIModal(pairId) { async function trReflOpenAIModal(pairId, fetchLatest = true) {
const data = trReflData || { pairs: [] }; const data = trReflData || { pairs: [] };
const pair = data.pairs.find(p => p.id === pairId); const pair = data.pairs.find(p => p.id === pairId);
if (!pair) return; if (!pair) return;
trReflCurrentPairId = pairId; trReflCurrentPairId = pairId;
trReflCurrentAIResult = pair.ai_analysis || null;
if (fetchLatest) {
trReflCurrentAIResult = null;
try {
const res = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`);
const json = await res.json();
if (json.success && json.data) {
trReflCurrentAIResult = json.data;
}
} catch (e) { console.error('获取最新AI分析失败', e); }
// 没有最新结果时,自动基于反思重新分析
if (!trReflCurrentAIResult) { if (!trReflCurrentAIResult) {
// 如果没有缓存的 AI 结果,尝试请求分析接口
try { try {
const res = await fetch(`${TR_API_BASE}/analyze-trade`, { const reRes = await fetch(`${TR_API_BASE}/reanalyze`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({ trade_pair_id: pairId }),
symbol: pair.symbol,
open_date: pair.open_date,
open_time: pair.open_time,
close_date: pair.close_date,
close_time: pair.close_time,
direction: pair.direction,
open_price: pair.open_price,
close_price: pair.close_price,
}),
}); });
const json = await res.json(); const reJson = await reRes.json();
if (json.success && json.data) { if (reJson.success && reJson.data) {
trReflCurrentAIResult = json.data; const latestRes = await fetch(`${TR_API_BASE}/reanalyze/${pairId}/latest`);
const latestJson = await latestRes.json();
if (latestJson.success && latestJson.data) {
trReflCurrentAIResult = latestJson.data;
} else {
trReflCurrentAIResult = reJson.data;
}
}
} catch (e) { console.error('AI重新分析失败', e); }
} }
} catch (e) { console.error('AI分析请求失败', e); }
} }
const result = trReflCurrentAIResult || {}; const result = trReflCurrentAIResult || {};
document.getElementById('tr-refl-ai-pair-id').value = pairId; document.getElementById('tr-refl-ai-pair-id').value = pairId;
document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果'; document.getElementById('tr-refl-ai-overall').textContent = result.overall_evaluation || result.analysis || '暂无 AI 分析结果(请先保存反思)';
document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>'; document.getElementById('tr-refl-ai-strengths').innerHTML = (result.strengths || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>'; document.getElementById('tr-refl-ai-weaknesses').innerHTML = (result.weaknesses || []).map(s => `<li>${s}</li>`).join('') || '<li>-</li>';
document.getElementById('tr-refl-ai-suggestion').textContent = result.suggestion || result.experience_suggestion || '暂无建议'; document.getElementById('tr-refl-ai-suggestion').textContent = result.experience_suggestion || result.suggestion || '暂无建议';
document.getElementById('tr-refl-ai-modal').classList.add('show'); document.getElementById('tr-refl-ai-modal').classList.add('show');
} }
@ -1101,19 +1240,20 @@ async function trReflSaveExperience() {
if (!pair || !trReflCurrentAIResult) return; if (!pair || !trReflCurrentAIResult) return;
const result = trReflCurrentAIResult; const result = trReflCurrentAIResult;
const analysisId = result.id;
if (!analysisId) {
showToast('error', '保存失败', '缺少分析记录ID');
return;
}
const body = { const body = {
title: `${pair.symbol_name || pair.variety || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 经验`, title: `${pair.symbol_name || pair.variety || pair.symbol} ${pair.direction === '多' ? '做多' : '做空'} 经验`,
content: result.overall_evaluation || result.analysis || '', content: result.overall_evaluation || result.analysis || result.experience_suggestion || '',
type: pair.net_pnl >= 0 ? 'insight' : 'lesson', tags: ['AI分析'],
tags: ['AI分析', ...(pair.tags || [])].slice(0, 5),
source_trade_pair_id: pairId,
strengths: result.strengths || [],
weaknesses: result.weaknesses || [],
suggestion: result.suggestion || result.experience_suggestion || '',
}; };
try { try {
const res = await fetch(`${TR_API_BASE}/experiences`, { const res = await fetch(`${TR_API_BASE}/ai-analysis/${analysisId}/save-experience`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), body: JSON.stringify(body),

Loading…
Cancel
Save