galaxy-opc-plugin 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -10
- package/index.ts +106 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/api/companies.ts +4 -0
- package/src/api/dashboard.ts +368 -16
- package/src/api/routes.ts +2 -2
- package/src/db/migrations.ts +146 -0
- package/src/db/schema.ts +104 -3
- package/src/db/sqlite-adapter.ts +39 -2
- package/src/opc/accounting-parser.ts +178 -0
- package/src/opc/daily-brief.ts +529 -0
- package/src/opc/onboarding-flow.ts +332 -0
- package/src/opc/proactive-service.ts +290 -3
- package/src/tools/finance-tool.test-payment.ts +326 -0
- package/src/tools/finance-tool.ts +653 -1
- package/src/tools/onboarding-tool.ts +233 -0
- package/src/tools/order-tool.ts +481 -0
- package/src/tools/smart-accounting-tool.ts +144 -0
- package/src/web/DASHBOARD_INTEGRATION_GUIDE.md +478 -0
- package/src/web/config-ui-patches.ts +389 -0
- package/src/web/config-ui.ts +4162 -3809
- package/src/web/dashboard-ui.ts +582 -0
- package/src/web/landing-page.ts +56 -6
package/src/api/dashboard.ts
CHANGED
|
@@ -1,32 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 星环OPC中心 — Dashboard
|
|
2
|
+
* 星环OPC中心 — Dashboard 监控中心 API
|
|
3
3
|
*
|
|
4
4
|
* 路由:
|
|
5
|
-
* GET /opc/api/dashboard
|
|
5
|
+
* GET /opc/admin/api/dashboard — 获取监控中心数据
|
|
6
|
+
* POST /opc/admin/api/alerts/:id/dismiss — 忽略预警
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
9
10
|
import type { OpcDatabase } from "../db/index.js";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
generateDashboardHtml,
|
|
13
|
+
type DashboardData,
|
|
14
|
+
type DashboardMetrics,
|
|
15
|
+
type DashboardAlert,
|
|
16
|
+
type DashboardTodo,
|
|
17
|
+
type DashboardSuggestion,
|
|
18
|
+
} from "../web/dashboard-ui.js";
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
interface DashboardRow {
|
|
21
|
+
total_income: number;
|
|
22
|
+
total_expense: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PaymentRow {
|
|
26
|
+
id: string;
|
|
27
|
+
company_id: string;
|
|
28
|
+
direction: string;
|
|
29
|
+
counterparty: string;
|
|
30
|
+
amount: number;
|
|
31
|
+
paid_amount: number;
|
|
32
|
+
status: string;
|
|
33
|
+
due_date: string;
|
|
34
|
+
category: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface AlertRow {
|
|
38
|
+
id: string;
|
|
39
|
+
company_id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
severity: string;
|
|
42
|
+
category: string;
|
|
43
|
+
status: string;
|
|
44
|
+
message: string;
|
|
45
|
+
created_at: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TodoRow {
|
|
49
|
+
id: string;
|
|
50
|
+
company_id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
priority: string;
|
|
53
|
+
category: string;
|
|
54
|
+
status: string;
|
|
55
|
+
due_date: string;
|
|
56
|
+
description: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 注册 Dashboard API 路由
|
|
61
|
+
*/
|
|
62
|
+
export function registerDashboardApiRoutes(api: OpenClawPluginApi, db: OpcDatabase): void {
|
|
63
|
+
// GET /opc/admin/api/dashboard
|
|
13
64
|
api.registerHttpRoute({
|
|
14
|
-
path: "/opc/api/dashboard
|
|
65
|
+
path: "/opc/admin/api/dashboard",
|
|
15
66
|
handler: (req, res) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
67
|
+
try {
|
|
68
|
+
const data = getDashboardData(db);
|
|
69
|
+
const html = generateDashboardHtml(data);
|
|
70
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
71
|
+
res.end(JSON.stringify({ ...data, html }));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
74
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
24
75
|
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
25
78
|
|
|
79
|
+
// POST /opc/admin/api/alerts/:id/dismiss
|
|
80
|
+
api.registerHttpRoute({
|
|
81
|
+
path: "/opc/admin/api/alerts/:id/dismiss",
|
|
82
|
+
handler: async (req, res) => {
|
|
26
83
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
84
|
+
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
85
|
+
const pathParts = url.pathname.split("/");
|
|
86
|
+
const alertId = pathParts[pathParts.length - 2]; // .../alerts/{id}/dismiss
|
|
87
|
+
|
|
88
|
+
if (!alertId) {
|
|
89
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
90
|
+
res.end(JSON.stringify({ error: "Missing alert ID" }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
const result = db.execute(
|
|
96
|
+
"UPDATE opc_alerts SET status = 'resolved', resolved_at = ? WHERE id = ? AND status = 'active'",
|
|
97
|
+
now,
|
|
98
|
+
alertId,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (result.changes > 0) {
|
|
102
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
103
|
+
res.end(JSON.stringify({ ok: true }));
|
|
104
|
+
} else {
|
|
105
|
+
res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
|
|
106
|
+
res.end(JSON.stringify({ error: "Alert not found or already resolved" }));
|
|
107
|
+
}
|
|
30
108
|
} catch (err) {
|
|
31
109
|
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
32
110
|
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
@@ -34,3 +112,277 @@ export function registerDashboardRoutes(api: OpenClawPluginApi, db: OpcDatabase,
|
|
|
34
112
|
},
|
|
35
113
|
});
|
|
36
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 获取 Dashboard 数据
|
|
118
|
+
*/
|
|
119
|
+
function getDashboardData(db: OpcDatabase): DashboardData {
|
|
120
|
+
const metrics = calculateMetrics(db);
|
|
121
|
+
const alerts = getActiveAlerts(db);
|
|
122
|
+
const todos = getTodayTodos(db);
|
|
123
|
+
const suggestions = generateSuggestions(db, metrics, alerts, todos);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
metrics,
|
|
127
|
+
alerts,
|
|
128
|
+
todos,
|
|
129
|
+
suggestions,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 计算关键指标
|
|
135
|
+
*/
|
|
136
|
+
function calculateMetrics(db: OpcDatabase): DashboardMetrics {
|
|
137
|
+
// 本月收入和支出
|
|
138
|
+
const now = new Date();
|
|
139
|
+
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
|
|
140
|
+
const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10);
|
|
141
|
+
|
|
142
|
+
const currentMonth = db.queryOne(
|
|
143
|
+
`SELECT
|
|
144
|
+
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as total_income,
|
|
145
|
+
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as total_expense
|
|
146
|
+
FROM opc_transactions
|
|
147
|
+
WHERE transaction_date >= ? AND transaction_date <= ?`,
|
|
148
|
+
currentMonthStart,
|
|
149
|
+
currentMonthEnd,
|
|
150
|
+
) as DashboardRow;
|
|
151
|
+
|
|
152
|
+
// 上月收入和支出(用于计算同比)
|
|
153
|
+
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().slice(0, 10);
|
|
154
|
+
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0).toISOString().slice(0, 10);
|
|
155
|
+
|
|
156
|
+
const lastMonth = db.queryOne(
|
|
157
|
+
`SELECT
|
|
158
|
+
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) as total_income,
|
|
159
|
+
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as total_expense
|
|
160
|
+
FROM opc_transactions
|
|
161
|
+
WHERE transaction_date >= ? AND transaction_date <= ?`,
|
|
162
|
+
lastMonthStart,
|
|
163
|
+
lastMonthEnd,
|
|
164
|
+
) as DashboardRow;
|
|
165
|
+
|
|
166
|
+
const monthlyIncome = currentMonth.total_income;
|
|
167
|
+
const monthlyExpense = currentMonth.total_expense;
|
|
168
|
+
const monthlyProfit = monthlyIncome - monthlyExpense;
|
|
169
|
+
|
|
170
|
+
// 计算同比变化(避免除零)
|
|
171
|
+
const monthlyIncomeChange = lastMonth.total_income > 0
|
|
172
|
+
? ((monthlyIncome - lastMonth.total_income) / lastMonth.total_income) * 100
|
|
173
|
+
: 0;
|
|
174
|
+
|
|
175
|
+
const lastMonthProfit = lastMonth.total_income - lastMonth.total_expense;
|
|
176
|
+
const monthlyProfitChange = lastMonthProfit !== 0
|
|
177
|
+
? ((monthlyProfit - lastMonthProfit) / Math.abs(lastMonthProfit)) * 100
|
|
178
|
+
: 0;
|
|
179
|
+
|
|
180
|
+
// 现金余额(所有收入 - 所有支出)
|
|
181
|
+
const cashBalance = db.queryOne(
|
|
182
|
+
`SELECT
|
|
183
|
+
COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END), 0) -
|
|
184
|
+
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END), 0) as balance
|
|
185
|
+
FROM opc_transactions`,
|
|
186
|
+
) as { balance: number };
|
|
187
|
+
|
|
188
|
+
// 可撑月数(基于最近3个月平均支出)
|
|
189
|
+
const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 3, 1).toISOString().slice(0, 10);
|
|
190
|
+
const avgExpense = db.queryOne(
|
|
191
|
+
`SELECT COALESCE(AVG(monthly_expense), 0) as avg_expense FROM (
|
|
192
|
+
SELECT SUM(amount) as monthly_expense
|
|
193
|
+
FROM opc_transactions
|
|
194
|
+
WHERE type='expense' AND transaction_date >= ?
|
|
195
|
+
GROUP BY strftime('%Y-%m', transaction_date)
|
|
196
|
+
)`,
|
|
197
|
+
threeMonthsAgo,
|
|
198
|
+
) as { avg_expense: number };
|
|
199
|
+
|
|
200
|
+
const monthsOfRunway = avgExpense.avg_expense > 0
|
|
201
|
+
? cashBalance.balance / avgExpense.avg_expense
|
|
202
|
+
: 999;
|
|
203
|
+
|
|
204
|
+
// 应收账款(未收和部分已收的应收款)
|
|
205
|
+
const receivables = db.queryOne(
|
|
206
|
+
`SELECT COALESCE(SUM(amount - paid_amount), 0) as total
|
|
207
|
+
FROM opc_payments
|
|
208
|
+
WHERE direction = 'receivable' AND status IN ('pending', 'partial')`,
|
|
209
|
+
) as { total: number };
|
|
210
|
+
|
|
211
|
+
// 逾期应收账款
|
|
212
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
213
|
+
const overdueReceivables = db.queryOne(
|
|
214
|
+
`SELECT COALESCE(SUM(amount - paid_amount), 0) as total
|
|
215
|
+
FROM opc_payments
|
|
216
|
+
WHERE direction = 'receivable'
|
|
217
|
+
AND status IN ('pending', 'partial', 'overdue')
|
|
218
|
+
AND due_date < ?`,
|
|
219
|
+
today,
|
|
220
|
+
) as { total: number };
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
monthlyIncome,
|
|
224
|
+
monthlyIncomeChange,
|
|
225
|
+
monthlyProfit,
|
|
226
|
+
monthlyProfitChange,
|
|
227
|
+
cashBalance: cashBalance.balance,
|
|
228
|
+
monthsOfRunway,
|
|
229
|
+
receivables: receivables.total,
|
|
230
|
+
overdueReceivables: overdueReceivables.total,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 获取活跃预警
|
|
236
|
+
*/
|
|
237
|
+
function getActiveAlerts(db: OpcDatabase): DashboardAlert[] {
|
|
238
|
+
const alerts = db.query(
|
|
239
|
+
`SELECT id, company_id, title, severity, category, message, created_at
|
|
240
|
+
FROM opc_alerts
|
|
241
|
+
WHERE status = 'active'
|
|
242
|
+
ORDER BY
|
|
243
|
+
CASE severity
|
|
244
|
+
WHEN 'critical' THEN 0
|
|
245
|
+
WHEN 'warning' THEN 1
|
|
246
|
+
ELSE 2
|
|
247
|
+
END,
|
|
248
|
+
created_at DESC
|
|
249
|
+
LIMIT 10`,
|
|
250
|
+
) as AlertRow[];
|
|
251
|
+
|
|
252
|
+
return alerts.map(alert => ({
|
|
253
|
+
id: alert.id,
|
|
254
|
+
title: alert.title,
|
|
255
|
+
severity: alert.severity as 'critical' | 'warning' | 'info',
|
|
256
|
+
category: alert.category,
|
|
257
|
+
message: alert.message,
|
|
258
|
+
created_at: alert.created_at,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 获取今日待办
|
|
264
|
+
*/
|
|
265
|
+
function getTodayTodos(db: OpcDatabase): DashboardTodo[] {
|
|
266
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
267
|
+
|
|
268
|
+
const todos = db.query(
|
|
269
|
+
`SELECT id, company_id, title, priority, related_type as category, due_date, description
|
|
270
|
+
FROM opc_todos
|
|
271
|
+
WHERE status = 'pending'
|
|
272
|
+
AND (due_date = ? OR due_date < ?)
|
|
273
|
+
ORDER BY
|
|
274
|
+
CASE priority
|
|
275
|
+
WHEN 'urgent' THEN 0
|
|
276
|
+
WHEN 'high' THEN 1
|
|
277
|
+
ELSE 2
|
|
278
|
+
END,
|
|
279
|
+
due_date ASC
|
|
280
|
+
LIMIT 20`,
|
|
281
|
+
today,
|
|
282
|
+
today,
|
|
283
|
+
) as TodoRow[];
|
|
284
|
+
|
|
285
|
+
return todos.map(todo => ({
|
|
286
|
+
id: todo.id,
|
|
287
|
+
title: todo.title,
|
|
288
|
+
priority: todo.priority as 'urgent' | 'high' | 'normal',
|
|
289
|
+
category: todo.category || '',
|
|
290
|
+
due_date: todo.due_date || undefined,
|
|
291
|
+
description: todo.description || undefined,
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 生成 AI 建议
|
|
297
|
+
*/
|
|
298
|
+
function generateSuggestions(
|
|
299
|
+
db: OpcDatabase,
|
|
300
|
+
metrics: DashboardMetrics,
|
|
301
|
+
alerts: DashboardAlert[],
|
|
302
|
+
todos: DashboardTodo[],
|
|
303
|
+
): DashboardSuggestion[] {
|
|
304
|
+
const suggestions: DashboardSuggestion[] = [];
|
|
305
|
+
|
|
306
|
+
// 建议1: 现金流预警
|
|
307
|
+
if (metrics.monthsOfRunway < 2) {
|
|
308
|
+
suggestions.push({
|
|
309
|
+
id: "cash-runway-low",
|
|
310
|
+
title: "现金流预警:可撑月数不足",
|
|
311
|
+
description: `当前现金余额 ¥${metrics.cashBalance.toFixed(0)} 仅能维持 ${metrics.monthsOfRunway.toFixed(1)} 个月运营。建议尽快催收应收账款或拓展收入来源。`,
|
|
312
|
+
action: {
|
|
313
|
+
label: "查看应收账款",
|
|
314
|
+
onclick: "loadView('finance')",
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 建议2: 逾期催收
|
|
320
|
+
if (metrics.overdueReceivables > 0) {
|
|
321
|
+
suggestions.push({
|
|
322
|
+
id: "overdue-receivables",
|
|
323
|
+
title: "逾期账款催收提醒",
|
|
324
|
+
description: `有 ¥${metrics.overdueReceivables.toFixed(0)} 的应收账款已逾期,建议立即联系客户催收。`,
|
|
325
|
+
action: {
|
|
326
|
+
label: "查看逾期清单",
|
|
327
|
+
onclick: "loadView('finance')",
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 建议3: 利润优化
|
|
333
|
+
if (metrics.monthlyProfit < 0) {
|
|
334
|
+
suggestions.push({
|
|
335
|
+
id: "negative-profit",
|
|
336
|
+
title: "本月利润为负,需优化成本",
|
|
337
|
+
description: `本月支出超过收入 ¥${Math.abs(metrics.monthlyProfit).toFixed(0)},建议检查支出明细,削减非必要成本。`,
|
|
338
|
+
action: {
|
|
339
|
+
label: "查看支出分析",
|
|
340
|
+
onclick: "loadView('finance')",
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 建议4: 收入增长策略
|
|
346
|
+
if (metrics.monthlyIncomeChange < -10) {
|
|
347
|
+
suggestions.push({
|
|
348
|
+
id: "income-decline",
|
|
349
|
+
title: "收入下滑,需拓展业务",
|
|
350
|
+
description: `本月收入较上月下降 ${Math.abs(metrics.monthlyIncomeChange).toFixed(1)}%,建议加强客户开发和老客户维护。`,
|
|
351
|
+
action: {
|
|
352
|
+
label: "查看客户管理",
|
|
353
|
+
onclick: "loadView('companies')",
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 建议5: 合同到期提醒
|
|
359
|
+
const expiringContracts = db.query(
|
|
360
|
+
`SELECT COUNT(*) as cnt FROM opc_contracts
|
|
361
|
+
WHERE status = 'active'
|
|
362
|
+
AND end_date <= date('now', '+30 days')
|
|
363
|
+
AND end_date >= date('now')`,
|
|
364
|
+
) as { cnt: number }[];
|
|
365
|
+
|
|
366
|
+
if (expiringContracts.length > 0 && expiringContracts[0].cnt > 0) {
|
|
367
|
+
suggestions.push({
|
|
368
|
+
id: "contracts-expiring",
|
|
369
|
+
title: "合同即将到期",
|
|
370
|
+
description: `有 ${expiringContracts[0].cnt} 份合同将在 30 天内到期,建议提前联系续约。`,
|
|
371
|
+
action: {
|
|
372
|
+
label: "查看合同列表",
|
|
373
|
+
onclick: "loadView('companies')",
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 默认建议:一切正常
|
|
379
|
+
if (suggestions.length === 0) {
|
|
380
|
+
suggestions.push({
|
|
381
|
+
id: "all-good",
|
|
382
|
+
title: "经营状况良好",
|
|
383
|
+
description: "当前各项指标正常,继续保持!建议定期回顾财务数据,优化运营效率。",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return suggestions.slice(0, 5); // 最多返回5条建议
|
|
388
|
+
}
|
package/src/api/routes.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
6
6
|
import type { OpcDatabase } from "../db/index.js";
|
|
7
7
|
import { registerCompanyRoutes } from "./companies.js";
|
|
8
|
-
import {
|
|
8
|
+
import { registerDashboardApiRoutes } from "./dashboard.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 从 OpenClaw 配置中读取 gateway auth token。
|
|
@@ -26,6 +26,6 @@ export function registerHttpRoutes(api: OpenClawPluginApi, db: OpcDatabase): voi
|
|
|
26
26
|
const gatewayToken = getGatewayToken(api);
|
|
27
27
|
|
|
28
28
|
registerCompanyRoutes(api, db, gatewayToken);
|
|
29
|
-
|
|
29
|
+
registerDashboardApiRoutes(api, db);
|
|
30
30
|
api.logger.info("opc: 已注册 HTTP API 路由");
|
|
31
31
|
}
|
package/src/db/migrations.ts
CHANGED
|
@@ -166,6 +166,152 @@ export const migrations: Migration[] = [
|
|
|
166
166
|
// Indexes created via OPC_INDEXES
|
|
167
167
|
},
|
|
168
168
|
},
|
|
169
|
+
{
|
|
170
|
+
version: 15,
|
|
171
|
+
description: "Payment management enhancement — add direction, counterparty, paid_amount, category for receivables/payables",
|
|
172
|
+
up(db) {
|
|
173
|
+
const cols = db.pragma("table_info(opc_payments)") as { name: string }[];
|
|
174
|
+
if (!cols.some(c => c.name === "direction")) {
|
|
175
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN direction TEXT NOT NULL DEFAULT 'receivable'");
|
|
176
|
+
}
|
|
177
|
+
if (!cols.some(c => c.name === "counterparty")) {
|
|
178
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN counterparty TEXT NOT NULL DEFAULT ''");
|
|
179
|
+
}
|
|
180
|
+
if (!cols.some(c => c.name === "paid_amount")) {
|
|
181
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN paid_amount REAL NOT NULL DEFAULT 0");
|
|
182
|
+
}
|
|
183
|
+
if (!cols.some(c => c.name === "category")) {
|
|
184
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN category TEXT NOT NULL DEFAULT ''");
|
|
185
|
+
}
|
|
186
|
+
if (!cols.some(c => c.name === "payment_method")) {
|
|
187
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN payment_method TEXT NOT NULL DEFAULT ''");
|
|
188
|
+
}
|
|
189
|
+
if (!cols.some(c => c.name === "contract_id")) {
|
|
190
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN contract_id TEXT NOT NULL DEFAULT ''");
|
|
191
|
+
}
|
|
192
|
+
// 创建新索引
|
|
193
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_payments_direction ON opc_payments(direction)");
|
|
194
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_payments_counterparty ON opc_payments(counterparty)");
|
|
195
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_payments_company_direction ON opc_payments(company_id, direction)");
|
|
196
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_payments_company_status ON opc_payments(company_id, status)");
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
version: 16,
|
|
201
|
+
description: "Order workflow — quotations, quotation_items, contract_milestones tables",
|
|
202
|
+
up(db) {
|
|
203
|
+
// 报价单表
|
|
204
|
+
db.exec(`
|
|
205
|
+
CREATE TABLE IF NOT EXISTS opc_quotations (
|
|
206
|
+
id TEXT PRIMARY KEY,
|
|
207
|
+
company_id TEXT NOT NULL,
|
|
208
|
+
contact_id TEXT NOT NULL DEFAULT '',
|
|
209
|
+
quotation_number TEXT NOT NULL,
|
|
210
|
+
title TEXT NOT NULL,
|
|
211
|
+
total_amount REAL NOT NULL DEFAULT 0,
|
|
212
|
+
valid_until TEXT NOT NULL DEFAULT '',
|
|
213
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
214
|
+
notes TEXT NOT NULL DEFAULT '',
|
|
215
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
216
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
217
|
+
FOREIGN KEY (company_id) REFERENCES opc_companies(id),
|
|
218
|
+
FOREIGN KEY (contact_id) REFERENCES opc_contacts(id)
|
|
219
|
+
)
|
|
220
|
+
`);
|
|
221
|
+
|
|
222
|
+
// 报价明细表
|
|
223
|
+
db.exec(`
|
|
224
|
+
CREATE TABLE IF NOT EXISTS opc_quotation_items (
|
|
225
|
+
id TEXT PRIMARY KEY,
|
|
226
|
+
quotation_id TEXT NOT NULL,
|
|
227
|
+
description TEXT NOT NULL,
|
|
228
|
+
quantity REAL NOT NULL DEFAULT 1,
|
|
229
|
+
unit_price REAL NOT NULL DEFAULT 0,
|
|
230
|
+
total_price REAL NOT NULL DEFAULT 0,
|
|
231
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
232
|
+
FOREIGN KEY (quotation_id) REFERENCES opc_quotations(id)
|
|
233
|
+
)
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
// 合同里程碑表
|
|
237
|
+
db.exec(`
|
|
238
|
+
CREATE TABLE IF NOT EXISTS opc_contract_milestones (
|
|
239
|
+
id TEXT PRIMARY KEY,
|
|
240
|
+
contract_id TEXT NOT NULL,
|
|
241
|
+
company_id TEXT NOT NULL,
|
|
242
|
+
title TEXT NOT NULL,
|
|
243
|
+
description TEXT NOT NULL DEFAULT '',
|
|
244
|
+
due_date TEXT NOT NULL DEFAULT '',
|
|
245
|
+
amount REAL NOT NULL DEFAULT 0,
|
|
246
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
247
|
+
completed_date TEXT NOT NULL DEFAULT '',
|
|
248
|
+
notes TEXT NOT NULL DEFAULT '',
|
|
249
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
250
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
251
|
+
FOREIGN KEY (contract_id) REFERENCES opc_contracts(id),
|
|
252
|
+
FOREIGN KEY (company_id) REFERENCES opc_companies(id)
|
|
253
|
+
)
|
|
254
|
+
`);
|
|
255
|
+
|
|
256
|
+
// 创建索引
|
|
257
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quotations_company ON opc_quotations(company_id)");
|
|
258
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quotations_status ON opc_quotations(status)");
|
|
259
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quotations_contact ON opc_quotations(contact_id)");
|
|
260
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quotation_items_quotation ON opc_quotation_items(quotation_id)");
|
|
261
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_contract_milestones_contract ON opc_contract_milestones(contract_id)");
|
|
262
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_contract_milestones_status ON opc_contract_milestones(status)");
|
|
263
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_contract_milestones_due_date ON opc_contract_milestones(due_date)");
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
version: 17,
|
|
268
|
+
description: "Contract enhancement — add quotation_id, signed_date, payment_terms",
|
|
269
|
+
up(db) {
|
|
270
|
+
const cols = db.pragma("table_info(opc_contracts)") as { name: string }[];
|
|
271
|
+
|
|
272
|
+
if (!cols.some(c => c.name === "quotation_id")) {
|
|
273
|
+
db.exec("ALTER TABLE opc_contracts ADD COLUMN quotation_id TEXT NOT NULL DEFAULT ''");
|
|
274
|
+
}
|
|
275
|
+
if (!cols.some(c => c.name === "signed_date")) {
|
|
276
|
+
db.exec("ALTER TABLE opc_contracts ADD COLUMN signed_date TEXT NOT NULL DEFAULT ''");
|
|
277
|
+
}
|
|
278
|
+
if (!cols.some(c => c.name === "payment_terms")) {
|
|
279
|
+
db.exec("ALTER TABLE opc_contracts ADD COLUMN payment_terms TEXT NOT NULL DEFAULT ''");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 创建索引
|
|
283
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_contracts_quotation ON opc_contracts(quotation_id)");
|
|
284
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_contracts_signed_date ON opc_contracts(signed_date)");
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
version: 18,
|
|
289
|
+
description: "Payment enhancement — add milestone_id, overdue_days, risk_level for collection management",
|
|
290
|
+
up(db) {
|
|
291
|
+
const cols = db.pragma("table_info(opc_payments)") as { name: string }[];
|
|
292
|
+
|
|
293
|
+
if (!cols.some(c => c.name === "milestone_id")) {
|
|
294
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN milestone_id TEXT NOT NULL DEFAULT ''");
|
|
295
|
+
}
|
|
296
|
+
if (!cols.some(c => c.name === "overdue_days")) {
|
|
297
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN overdue_days INTEGER NOT NULL DEFAULT 0");
|
|
298
|
+
}
|
|
299
|
+
if (!cols.some(c => c.name === "risk_level")) {
|
|
300
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN risk_level TEXT NOT NULL DEFAULT 'normal'");
|
|
301
|
+
}
|
|
302
|
+
if (!cols.some(c => c.name === "last_remind_date")) {
|
|
303
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN last_remind_date TEXT NOT NULL DEFAULT ''");
|
|
304
|
+
}
|
|
305
|
+
if (!cols.some(c => c.name === "remind_count")) {
|
|
306
|
+
db.exec("ALTER TABLE opc_payments ADD COLUMN remind_count INTEGER NOT NULL DEFAULT 0");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 创建索引
|
|
310
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_payments_milestone ON opc_payments(milestone_id)");
|
|
311
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_payments_risk_level ON opc_payments(risk_level)");
|
|
312
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_payments_overdue ON opc_payments(overdue_days)");
|
|
313
|
+
},
|
|
314
|
+
},
|
|
169
315
|
];
|
|
170
316
|
|
|
171
317
|
/**
|