@@ -170,56 +170,121 @@ window.createSales = (event) => createResource(event, "sales");
window . createProposal = ( event ) => createResource ( event , "proposals" ) ;
window . createProposal = ( event ) => createResource ( event , "proposals" ) ;
window . createOperation = ( event ) => createResource ( event , "operations" ) ;
window . createOperation = ( event ) => createResource ( event , "operations" ) ;
window . createProduct = ( event ) => createResource ( event , "products" ) ;
window . createProduct = ( event ) => createResource ( event , "products" ) ;
window . openTaskForm = ( projectId , taskId ) => {
const form = document . querySelector ( ` #task-form- ${ projectId } ` ) ;
form . classList . remove ( "hidden" ) ;
if ( taskId === null ) {
// new task
document . querySelector ( ` #task-id- ${ projectId } ` ) . value = "" ;
document . querySelector ( ` #task-name- ${ projectId } ` ) . value = "" ;
document . querySelector ( ` #task-phase- ${ projectId } ` ) . value = "项目准备" ;
document . querySelector ( ` #task-owner- ${ projectId } ` ) . value = "" ;
document . querySelector ( ` #task-due- ${ projectId } ` ) . value = "" ;
document . querySelector ( ` #task-notes- ${ projectId } ` ) . value = "" ;
document . querySelector ( ` #task-submit-btn- ${ projectId } ` ) . textContent = "确认新增" ;
} else {
// edit task
const task = ( state . data . tasks || [ ] ) . find ( ( t ) => t . id === taskId ) ;
if ( ! task ) return ;
document . querySelector ( ` #task-id- ${ projectId } ` ) . value = task . id ;
document . querySelector ( ` #task-name- ${ projectId } ` ) . value = task . task || "" ;
document . querySelector ( ` #task-phase- ${ projectId } ` ) . value = task . phase || "项目准备" ;
document . querySelector ( ` #task-owner- ${ projectId } ` ) . value = task . owner || "" ;
document . querySelector ( ` #task-due- ${ projectId } ` ) . value = task . due _date || "" ;
document . querySelector ( ` #task-notes- ${ projectId } ` ) . value = task . notes || "" ;
document . querySelector ( ` #task-submit-btn- ${ projectId } ` ) . textContent = "保存修改" ;
}
form . scrollIntoView ( { behavior : "smooth" } ) ;
} ;
window . submitTaskForm = async ( event , projectId ) => {
event . preventDefault ( ) ;
const form = event . currentTarget ;
const data = Object . fromEntries ( new FormData ( form ) . entries ( ) ) ;
data . project _id = projectId ;
const taskId = data . task _id ;
delete data . task _id ;
try {
if ( taskId ) {
await api ( ` /api/tasks/ ${ taskId } ` , { method : "PUT" , body : JSON . stringify ( { data } ) } ) ;
} else {
await api ( "/api/tasks" , { method : "POST" , body : JSON . stringify ( { data } ) } ) ;
}
form . reset ( ) ;
form . classList . add ( "hidden" ) ;
await load ( ) ;
showTaskModal ( projectId ) ;
} catch ( error ) {
alert ( "保存失败:" + error . message ) ;
}
} ;
window . createFinance = ( event ) => createResource ( event , "finance" ) ;
window . createFinance = ( event ) => createResource ( event , "finance" ) ;
window . switchTab = switchTab ;
window . switchTab = switchTab ;
function renderProjects ( ) {
function renderProjects ( ) {
// Merge sales_leads and operation_projects into one table
const items = state . data . operations ;
const salesItems = state . data . sale s. map ( ( x ) => ( {
const rows = item s. map ( ( x ) => [
name : x . target _customer ,
` <strong> ${ x . project _name } </strong> ` ,
version : "" ,
text ( x . customer _need || x . notes ) ,
type : "opportunity" ,
badge ( x . current _stage || x . project _status ) ,
status : x . status ,
x . expected _contract _amount ? money ( x . expected _contract _amount ) : "—" ,
amount : 0 ,
text ( x . owner || "—" ) ,
stage : "" ,
` <button class="btn btn-ghost btn-sm text-blue-600" onclick="event.stopPropagation(); showTaskModal( ${ x . id } )"><i data-lucide="eye"></i>查看</button> `
files : 0 ,
] ) ;
followup : x . latest _follow _up _record ,
resource : "sales" ,
id : x . id ,
} ) ) ;
const opItems = state . data . operations . map ( ( x ) => ( {
name : x . project _name ,
version : x . project _version ,
type : x . project _type ,
status : x . project _status ,
amount : x . expected _contract _amount || 0 ,
stage : x . current _stage || x . sop _stage ,
files : x . files . length ,
followup : x . latest _follow _up _record ,
resource : "operations" ,
id : x . id ,
} ) ) ;
const allItems = [ ... salesItems , ... opItems ] ;
const items = state . opFilter === "all" ? allItems : allItems . filter ( ( x ) => x . type === state . opFilter || ( state . opFilter === "opportunity" && x . type === "opportunity" ) ) ;
const rows = items . map ( ( x ) => [ ` <strong> ${ x . name } </strong> ${ x . version ? ` <p class="text-xs text-slate-500"> ${ x . version } </p> ` : "" } ` , badge ( x . type ) , badge ( x . status ) , x . amount ? money ( x . amount ) : "—" , text ( x . stage ) , text ( x . followup ) ] ) ;
const clicks = items . map ( ( x ) => ( { resource : x . resource , id : x . id } ) ) ;
document . querySelector ( "#projects" ) . innerHTML = ` <div class="grid gap-4">
document . querySelector ( "#projects" ) . innerHTML = ` <div class="grid gap-4">
${ card ( formHtml ( [
<div class="flex items-center justify-between">
{ label : "业务机会" , input : ` <input name="target_customer" required placeholder="客户名称"> ` } ,
<div class="flex gap-2">
{ label: "优先级", input: ` < select name = "priority" > < option > P0 < / o p t i o n > < o p t i o n s e l e c t e d > P 1 < / o p t i o n > < o p t i o n > P 2 < / o p t i o n > < o p t i o n > P 3 < / o p t i o n > < / s e l e c t > ` } ,
${ [ [ "all" , "全部" ] , [ "项目准备" , "准备" ] , [ "项目执行" , "执行" ] , [ "项目验收" , "验收" ] , [ "验收完毕" , "完毕" ] ] . map ( ( [ k , v ] ) => ` <button class="btn ${ state . opFilter === k ? "btn-primary" : "btn-ghost" } btn-sm" onclick="state.opFilter=' ${ k } '; renderProjects()"> ${ v } </button> ` ) . join ( "" ) }
{ label : "状态" , input : ` <select name="status"><option>待跟进</option><option>跟进中</option><option>方案中</option><option>商务谈判</option><option>已签约</option><option>暂缓</option><option>已丢单</option></select> ` } ,
</div>
] , { handler : "createSales" , text : ` <i data-lucide="plus"></i>新增业务机会 ` } ) , "p-4" ) }
<button class="btn btn-primary" onclick="document.querySelector('#project-form').classList.toggle('hidden')">
<i data-lucide="plus"></i>新增项目
</button>
</div>
<div id="project-form" class="hidden">
${ card ( formHtml ( [
${ card ( formHtml ( [
{ label : "项目名称" , input : ` <input name="project_name" required> ` } ,
{ label : "项目名称" , input : ` <input name="project_name" required> ` } ,
{ label : "项目版本" , input : ` <input name="project_version" value="v1.0" > ` } ,
{ label: "当前阶段", input: ` < select name = "current_stage" > < option > 项目准备 < / o p t i o n > < o p t i o n > 项 目 执 行 < / o p t i o n > < o p t i o n > 项 目 验 收 < / o p t i o n > < o p t i o n > 验 收 完 毕 < / o p t i o n > < / s e l e c t > ` } ,
{ label : "项目类型 " , input : ` <selec t name="project_type"><option value="opportunity">业务机会项目</option><option value="execution">已签约执行项目</option></select > ` } ,
{ label : "项目金额 " , input : ` <inpu t name="expected_contract_amount" type="number" step="0.01" placeholder="万元" > ` } ,
{ label : "状态 " , input : ` <input name="project_status" value="线索发现 "> ` } ,
{ label : "负责人 " , input : ` <input name="owner "> ` } ,
] , { handler : "createOperation" , text : "新增项目 " } ) , "p-4" ) }
] , { handler : "createOperation" , text : "确认 新增" } ) , "p-4" ) }
< div class = "flex gap-2" > $ { [ [ "all" , "全部" ] , [ "opportunity" , "业务机会" ] , [ "execution" , "已签约执行" ] ] . map ( ( [ k , v ] ) => ` <button class="btn ${ state . opFilter === k ? "btn-primary" : "btn-ghost" } " onclick="state.opFilter=' ${ k } '; renderProjects()"> ${ v } </button> ` ) . join ( "" ) } < / d i v >
< / d i v >
$ { renderTable ( [ "项目/客户 " , "类型 " , "状态 " , "金额" , "当前阶段 " , "最新跟 进" ] , rows , clicks ) }
$ { renderTable ( [ "项目" , "项目说明 " , "当前阶段 " , "项目 金额" , "负责人 " , "进展 " ] , rows , items . map ( ( x ) => ( { resource : "operations" , id : x . id } ) ) ) }
< / d i v > ` ;
< / d i v > ` ;
}
}
function showTaskModal ( projectId ) {
const project = state . data . operations . find ( ( x ) => x . id === projectId ) ;
const tasks = ( state . data . tasks || [ ] ) . filter ( ( t ) => t . project _id === projectId ) ;
const phases = [ "项目准备" , "项目执行" , "项目验收" , "验收完毕" ] ;
document . querySelector ( "#taskModal" ) . innerHTML = ` <div class="task-overlay" onclick="closeTaskModal()"><div class="task-panel" onclick="event.stopPropagation()"><div class="task-header"><h2 class="task-title"> ${ project . project _name } · 任务清单</h2><div class="flex items-center gap-3"><button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openTaskForm( ${ projectId } , null)"><i data-lucide="plus"></i>新增任务</button><button class="task-close" onclick="closeTaskModal()"><i data-lucide="x"></i></button></div></div><div class="task-body">
<form id="task-form- ${ projectId } " class="hidden task-form" onsubmit="submitTaskForm(event, ${ projectId } )">
<input type="hidden" name="task_id" id="task-id- ${ projectId } " value="">
<div class="grid grid-cols-2 gap-3">
<label class="task-field"><span>任务名称</span><input name="task" required id="task-name- ${ projectId } "></label>
<label class="task-field"><span>任务阶段</span><select name="phase" id="task-phase- ${ projectId } "> ${ phases . map ( ( p ) => ` <option> ${ p } </option> ` ) . join ( "" ) } </select></label>
<label class="task-field"><span>负责人</span><input name="owner" id="task-owner- ${ projectId } "></label>
<label class="task-field"><span>截止时间</span><input name="due_date" type="date" id="task-due- ${ projectId } "></label>
<label class="task-field col-span-2"><span>任务说明</span><textarea name="notes" rows="2" id="task-notes- ${ projectId } "></textarea></label>
</div>
<div class="flex justify-end gap-2 mt-3">
<button type="button" class="btn btn-ghost btn-sm" onclick="document.querySelector('#task-form- ${ projectId } ').classList.add('hidden')">取消</button>
<button type="submit" class="btn btn-primary btn-sm" id="task-submit-btn- ${ projectId } ">确认新增</button>
</div>
</form>
${ phases . map ( ( phase ) => {
const pt = tasks . filter ( ( t ) => t . phase === phase ) ;
if ( ! pt . length ) return "" ;
return ` <div class="task-group"><div class="task-group-hd"><span class="task-group-icon"><i data-lucide="layers"></i></span><span class="task-group-label"> ${ phase } </span><span class="task-group-n"> ${ pt . length } </span></div><div class="task-group-list"> ${ pt . map ( ( t ) => ` <div class="task-row" data-id=" ${ t . id } " onclick="event.stopPropagation(); openTaskForm( ${ projectId } , ${ t . id } )"><span class="task-dot"><i data-lucide=" ${ t . status === 'done' ? 'check-circle' : 'circle' } "></i></span><div class="task-main"><span class="task-name"> ${ t . task } </span> ${ t . notes ? ` <span class="task-desc"> ${ t . notes } </span> ` : "" } </div><span class="task-col"> ${ t . owner || "" } </span><span class="task-col-badge"> ${ t . due _date || "" } </span></div> ` ) . join ( "" ) } </div></div> ` ;
} ).join("")}</div></div></div> ` ;
document . querySelector ( "#taskModal" ) . classList . add ( "active" ) ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
}
window . closeTaskModal = ( ) => {
document . querySelector ( "#taskModal" ) . classList . remove ( "active" ) ;
document . querySelector ( "#taskModal" ) . innerHTML = "" ;
} ;
function renderProposals ( ) {
function renderProposals ( ) {
const proposalRows = state . data . proposals . map ( ( p ) => [ p . customer _or _project _name , p . version , badge ( p . status ) , p . files . length + " 个" ] ) ;
const proposalRows = state . data . proposals . map ( ( p ) => [ p . customer _or _project _name , p . version , badge ( p . status ) , p . files . length + " 个" ] ) ;
const proposalClicks = state . data . proposals . map ( ( p ) => ( { resource : "proposals" , id : p . id } ) ) ;
const proposalClicks = state . data . proposals . map ( ( p ) => ( { resource : "proposals" , id : p . id } ) ) ;
@@ -354,20 +419,6 @@ function openDrawer(resource, id) {
<form id="drawerForm" class="drawer-fields"> ${ fields . map ( ( [ key , label ] ) => drawerField ( fieldIcons [ key ] || "circle" , label , key , item [ key ] , multilineFields . includes ( key ) ) ) . join ( "" ) } </form>
<form id="drawerForm" class="drawer-fields"> ${ fields . map ( ( [ key , label ] ) => drawerField ( fieldIcons [ key ] || "circle" , label , key , item [ key ] , multilineFields . includes ( key ) ) ) . join ( "" ) } </form>
</section>
</section>
${ resource === "proposals" ? ` <section><h3 class="drawer-section-title">方案文件</h3><div class="grid gap-2"> ${ [ "方案" , "成本" , "SOP" , "财务流程" ] . map ( ( cat ) => fileGroup ( "proposal" , item . id , item . version , cat , item . files . filter ( ( f ) => f . file _category === cat ) ) ) . join ( "" ) } </div></section> ` : "" }
${ resource === "proposals" ? ` <section><h3 class="drawer-section-title">方案文件</h3><div class="grid gap-2"> ${ [ "方案" , "成本" , "SOP" , "财务流程" ] . map ( ( cat ) => fileGroup ( "proposal" , item . id , item . version , cat , item . files . filter ( ( f ) => f . file _category === cat ) ) ) . join ( "" ) } </div></section> ` : "" }
${ resource === "operations" ? ( ( ) => {
const tasks = ( state . data . tasks || [ ] ) . filter ( ( t ) => t . project _id === id ) ;
if ( ! tasks . length ) return "" ;
const phases = [ ... new Set ( tasks . map ( ( t ) => t . phase ) ) ] ;
return ` <section><h3 class="drawer-section-title">项目任务</h3><div class="grid gap-3"> ${ phases . map ( ( phase ) => {
const pt = tasks . filter ( ( t ) => t . phase === phase ) ;
return ` <div class="rounded-md border border-slate-200 p-3"><p class="text-[13px] font-semibold text-slate-700 mb-2"><i data-lucide="layers" style="width:14px;height:14px;display:inline;vertical-align:-2px;margin-right:4px"></i> ${ phase } </p><div class="grid gap-1.5"> ${ pt . map ( ( t ) => {
const due = t . due _date ? ` <span class="text-[11px] text-slate-400 ml-1">📅 ${ t . due _date } </span> ` : "" ;
const owner = t . owner ? ` <span class="text-[11px] text-slate-500 ml-1">👤 ${ t . owner } </span> ` : "" ;
const blocker = t . blockers ? ` <p class="text-[11px] text-red-600 mt-0.5">⚠ ${ t . blockers } </p> ` : "" ;
return ` <div class="rounded bg-slate-50 px-2.5 py-1.5"><p class="text-[12px] text-slate-800"><strong> ${ t . milestone ? t . milestone + ": " : "" } </strong> ${ t . task } ${ due } ${ owner } </p> ${ blocker } </div> ` ;
} ).join("")}</div></div> ` ;
} ).join("")}</div></section> ` ;
} )() : ""}
${ followupTarget ? ` <section>
${ followupTarget ? ` <section>
<h3 class="drawer-section-title">活动 / 跟进</h3>
<h3 class="drawer-section-title">活动 / 跟进</h3>
<div class="grid gap-2"> ${ ( item . followups || [ ] ) . map ( ( f ) => ` <div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span> ${ f . follower } · ${ f . follow _up _method } </span><span> ${ f . followed _at } </span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html=" ${ encodeURIComponent ( f . content || '' ) } "></div> ${ f . next _action ? ` <p class="mt-1 leading-5 text-blue-700">下一步: ${ text ( f . next _action ) } </p> ` : "" } </div><button class="activity-delete" onclick="deleteFollowup(event, ${ f . id } , ' ${ resource } ', ${ item . id } )" title="删除评论"><i data-lucide="trash-2"></i></button></div> ` ) . join ( "" ) } </div>
<div class="grid gap-2"> ${ ( item . followups || [ ] ) . map ( ( f ) => ` <div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span> ${ f . follower } · ${ f . follow _up _method } </span><span> ${ f . followed _at } </span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html=" ${ encodeURIComponent ( f . content || '' ) } "></div> ${ f . next _action ? ` <p class="mt-1 leading-5 text-blue-700">下一步: ${ text ( f . next _action ) } </p> ` : "" } </div><button class="activity-delete" onclick="deleteFollowup(event, ${ f . id } , ' ${ resource } ', ${ item . id } )" title="删除评论"><i data-lucide="trash-2"></i></button></div> ` ) . join ( "" ) } </div>