初始化:连锁餐饮数字化运营管理平台
This commit is contained in:
@@ -0,0 +1,571 @@
|
||||
import { Router } from 'express'
|
||||
import { query, withTransaction } from '../config/database.js'
|
||||
import { sendSuccess, sendError, parseMonth, parsePagination } from '../middleware/error.js'
|
||||
import type { AuthRequest } from '../middleware/auth.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// ============================================================
|
||||
// 固定路径路由(必须在 /:id 之前定义)
|
||||
// ============================================================
|
||||
|
||||
router.get('/', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const { page, pageSize, offset } = parsePagination(req)
|
||||
const month = parseMonth(req)
|
||||
const priority = req.query.priority as string
|
||||
const status = req.query.status as string
|
||||
const storeCode = req.query.store_code as string
|
||||
|
||||
const conditions: string[] = [`plan_month = $1`]
|
||||
const params: any[] = [month]
|
||||
let paramIdx = 2
|
||||
|
||||
if (priority) {
|
||||
conditions.push(`priority = $${paramIdx++}`)
|
||||
params.push(priority)
|
||||
}
|
||||
if (status) {
|
||||
conditions.push(`status = $${paramIdx++}`)
|
||||
params.push(status)
|
||||
}
|
||||
if (storeCode) {
|
||||
conditions.push(`store_code = $${paramIdx++}`)
|
||||
params.push(storeCode)
|
||||
}
|
||||
|
||||
const where = conditions.join(' AND ')
|
||||
const countResult = await query(`SELECT count(*) AS total FROM analytics.store_task WHERE ${where}`, params)
|
||||
const result = await query(
|
||||
`SELECT * FROM analytics.store_task WHERE ${where} ORDER BY
|
||||
CASE priority WHEN 'P0-修复数据口径' THEN 1 WHEN 'P0-综合专项整改' THEN 2 WHEN 'P1' THEN 3 WHEN 'P2' THEN 4 ELSE 5 END,
|
||||
deadline ASC
|
||||
LIMIT $${paramIdx++} OFFSET $${paramIdx++}`,
|
||||
[...params, pageSize, offset]
|
||||
)
|
||||
sendSuccess(res, result.rows, { total: parseInt(countResult.rows[0].total), page, page_size: pageSize })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const b = req.body
|
||||
const result = await withTransaction(async (client) => {
|
||||
const taskResult = await client.query(`
|
||||
INSERT INTO analytics.store_task
|
||||
(plan_month, store_code, store_name, priority, problem_indicator,
|
||||
current_value, benchmark_value, target_value, problem_description,
|
||||
action_required, owner, collaborators, deadline,
|
||||
verification_indicator)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
RETURNING task_id
|
||||
`, [b.plan_month, b.store_code, b.store_name, b.priority, b.problem_indicator,
|
||||
b.current_value, b.benchmark_value, b.target_value, b.problem_description,
|
||||
b.action_required, b.owner, b.collaborators, b.deadline, b.verification_indicator])
|
||||
|
||||
const taskId = taskResult.rows[0].task_id
|
||||
await client.query(`
|
||||
INSERT INTO analytics.store_task_log (task_id, action, new_status, operator, comment)
|
||||
VALUES ($1, '创建', '待启动', $2, $3)
|
||||
`, [taskId, req.user?.name || 'system', b.problem_description])
|
||||
|
||||
return taskId
|
||||
})
|
||||
sendSuccess(res, { task_id: result })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/auto-generate', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = req.body.month ? (req.body.month.length === 7 ? `${req.body.month}-01` : req.body.month) : '2026-05-01'
|
||||
const result = await query(`SELECT * FROM analytics.f_generate_store_tasks($1)`, [month])
|
||||
sendSuccess(res, result.rows[0] || { generated: 0, message: 'No tasks generated' })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/weekly-check', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.v_task_weekly_check WHERE plan_month = $1 ORDER BY consecutive_no_improve_weeks DESC, store_code`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/monthly-review', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT * FROM analytics.v_task_monthly_review WHERE plan_month = $1 ORDER BY store_code`, [month])
|
||||
const summary = {
|
||||
total: result.rows.length,
|
||||
passed: result.rows.filter((r: any) => r.review_result === '达标').length,
|
||||
improving: result.rows.filter((r: any) => r.review_result === '改善中').length,
|
||||
failed: result.rows.filter((r: any) => r.review_result === '未改善').length,
|
||||
}
|
||||
sendSuccess(res, { summary, details: result.rows })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/followup', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_store_monthly_followup ORDER BY priority, store_code`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/grade-change', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.store_grade_change ORDER BY change_month DESC, store_code`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/practices', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const mod = req.query.module as string
|
||||
const status = req.query.status as string
|
||||
let sql = `SELECT * FROM analytics.standardized_practice`
|
||||
const params: any[] = []
|
||||
const conditions: string[] = []
|
||||
if (mod) {
|
||||
conditions.push(`practice_module = $${params.length + 1}`)
|
||||
params.push(mod)
|
||||
}
|
||||
if (status) {
|
||||
conditions.push(`status = $${params.length + 1}`)
|
||||
params.push(status)
|
||||
}
|
||||
if (conditions.length > 0) {
|
||||
sql += ` WHERE ` + conditions.join(' AND ')
|
||||
}
|
||||
sql += ` ORDER BY created_at DESC`
|
||||
const result = await query(sql, params)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/practices', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const b = req.body
|
||||
const result = await query(`
|
||||
INSERT INTO analytics.standardized_practice
|
||||
(practice_module, benchmark_store_code, benchmark_store_name, key_actions, verification_indicators)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING id
|
||||
`, [b.practice_module, b.benchmark_store_code, b.benchmark_store_name, b.key_actions, b.verification_indicators])
|
||||
sendSuccess(res, { id: result.rows[0].id })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/practices/:id/replication-result', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const practiceId = parseInt(req.params.id)
|
||||
const result = await query(`SELECT * FROM analytics.practice_replication WHERE practice_id = $1 ORDER BY id`, [practiceId])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/practices/:id/replicate', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const practiceId = parseInt(req.params.id)
|
||||
const b = req.body
|
||||
const result = await query(`
|
||||
INSERT INTO analytics.practice_replication
|
||||
(practice_id, trial_store_code, trial_store_name, observation_weeks, before_value, status)
|
||||
VALUES ($1, $2, $3, $4, $5, '观察中') RETURNING id
|
||||
`, [practiceId, b.trial_store_code, b.trial_store_name, b.observation_weeks || 4, b.before_value])
|
||||
sendSuccess(res, { id: result.rows[0].id })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/practices/:id/promote', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const practiceId = parseInt(req.params.id)
|
||||
await query(`UPDATE analytics.standardized_practice SET status = '已推广' WHERE id = $1`, [practiceId])
|
||||
sendSuccess(res, { id: practiceId, status: '已推广' })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/indicators', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.indicator_dictionary ORDER BY id`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/indicators', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const b = req.body
|
||||
const result = await query(`
|
||||
INSERT INTO analytics.indicator_dictionary
|
||||
(indicator_name, business_definition, formula, data_source, update_frequency,
|
||||
owner, scope, yellow_threshold, red_threshold, version_date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (indicator_name) DO UPDATE SET
|
||||
business_definition = EXCLUDED.business_definition,
|
||||
formula = EXCLUDED.formula,
|
||||
data_source = EXCLUDED.data_source,
|
||||
update_frequency = EXCLUDED.update_frequency,
|
||||
owner = EXCLUDED.owner,
|
||||
scope = EXCLUDED.scope,
|
||||
yellow_threshold = EXCLUDED.yellow_threshold,
|
||||
red_threshold = EXCLUDED.red_threshold,
|
||||
version_date = EXCLUDED.version_date
|
||||
RETURNING id
|
||||
`, [b.indicator_name, b.business_definition, b.formula, b.data_source,
|
||||
b.update_frequency, b.owner, b.scope, b.yellow_threshold, b.red_threshold,
|
||||
b.version_date || new Date().toISOString().substring(0, 10)])
|
||||
sendSuccess(res, { id: result.rows[0].id })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/loop-health', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.v_loop_health`)
|
||||
sendSuccess(res, result.rows[0] || {
|
||||
task_generation_rate: 0,
|
||||
store_execution_rate: 0,
|
||||
weekly_check_rate: 0,
|
||||
monthly_review_rate: 0,
|
||||
practice_promotion_rate: 0,
|
||||
})
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/notifications/dispatch', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`SELECT * FROM analytics.f_dispatch_daily_notifications()`)
|
||||
sendSuccess(res, result.rows[0] || { dispatched: 0 })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/stores/:code/daily-card', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const code = req.params.code
|
||||
const result = await query(`SELECT * FROM analytics.v_store_daily_card WHERE store_code = $1 ORDER BY module`, [code])
|
||||
const tasks = await query(`
|
||||
SELECT t.* FROM analytics.store_task t
|
||||
WHERE t.store_code = $1 AND t.status IN ('待启动', '进行中')
|
||||
ORDER BY t.priority LIMIT 3
|
||||
`, [code])
|
||||
sendSuccess(res, { anomalies: result.rows, todos: tasks.rows })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 动态路径路由 /:id(必须在所有固定路径之后)
|
||||
// ============================================================
|
||||
|
||||
router.get('/:id', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id)
|
||||
if (isNaN(id)) {
|
||||
return sendError(res, 'Invalid task id', 400)
|
||||
}
|
||||
const result = await query(`SELECT * FROM analytics.store_task WHERE task_id = $1`, [id])
|
||||
if (result.rows.length === 0) {
|
||||
return sendError(res, 'Task not found', 404)
|
||||
}
|
||||
const logs = await query(`SELECT * FROM analytics.store_task_log WHERE task_id = $1 ORDER BY created_at`, [id])
|
||||
sendSuccess(res, { task: result.rows[0], logs: logs.rows })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id)
|
||||
const b = req.body
|
||||
const fields = ['status', 'process_evidence', 'verification_result', 'incomplete_reason', 'next_step', 'owner', 'collaborators', 'deadline', 'action_required']
|
||||
const updates: string[] = []
|
||||
const params: any[] = []
|
||||
let idx = 1
|
||||
for (const f of fields) {
|
||||
if (b[f] !== undefined) {
|
||||
updates.push(`${f} = $${idx++}`)
|
||||
params.push(b[f])
|
||||
}
|
||||
}
|
||||
if (updates.length === 0) {
|
||||
return sendError(res, 'No fields to update')
|
||||
}
|
||||
updates.push(`updated_at = NOW()`)
|
||||
params.push(id)
|
||||
await query(`UPDATE analytics.store_task SET ${updates.join(', ')} WHERE task_id = $${idx}`, params)
|
||||
await query(`
|
||||
INSERT INTO analytics.store_task_log (task_id, action, new_status, operator, comment)
|
||||
VALUES ($1, '更新', $2, $3, $4)
|
||||
`, [id, b.status || null, req.user?.name || 'system', b.next_step || null])
|
||||
sendSuccess(res, { task_id: id })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id/execute', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id)
|
||||
const b = req.body
|
||||
await withTransaction(async (client) => {
|
||||
await client.query(`
|
||||
UPDATE analytics.store_task
|
||||
SET status = '进行中', process_evidence = $1, action_required = $2,
|
||||
incomplete_reason = $3, next_step = $4, updated_at = NOW()
|
||||
WHERE task_id = $5
|
||||
`, [b.process_evidence, b.action_required, b.incomplete_reason, b.next_step, id])
|
||||
await client.query(`
|
||||
INSERT INTO analytics.store_task_log (task_id, action, old_status, new_status, operator, comment)
|
||||
VALUES ($1, '执行反馈', '待启动', '进行中', $2, $3)
|
||||
`, [id, req.user?.name || 'store', b.process_evidence])
|
||||
})
|
||||
sendSuccess(res, { task_id: id })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id/weekly-check', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id)
|
||||
const b = req.body
|
||||
await withTransaction(async (client) => {
|
||||
await client.query(`
|
||||
UPDATE analytics.store_task
|
||||
SET process_evidence = COALESCE($1, process_evidence),
|
||||
next_step = $2, updated_at = NOW()
|
||||
WHERE task_id = $3
|
||||
`, [b.check_evidence, b.next_step, id])
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO analytics.store_task_log (task_id, action, new_status, operator, comment)
|
||||
VALUES ($1, '周度检查', NULL, $2, $3)
|
||||
`, [id, req.user?.name || 'regional', b.check_comment])
|
||||
|
||||
const task = await client.query(`SELECT store_code, store_name, problem_indicator, plan_month FROM analytics.store_task WHERE task_id = $1`, [id])
|
||||
if (task.rows.length > 0) {
|
||||
const t = task.rows[0]
|
||||
await client.query(`
|
||||
INSERT INTO analytics.task_weekly_check
|
||||
(task_id, store_code, store_name, problem_indicator, iso_week,
|
||||
this_week_value, last_week_value, change_direction,
|
||||
consecutive_no_improve_weeks, check_comment, checked_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`, [id, t.store_code, t.store_name, t.problem_indicator, b.iso_week,
|
||||
b.this_week_value, b.last_week_value, b.change_direction,
|
||||
b.consecutive_no_improve_weeks || 0, b.check_comment, req.user?.name])
|
||||
}
|
||||
})
|
||||
sendSuccess(res, { task_id: id })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id/verify', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id)
|
||||
const b = req.body
|
||||
const reviewResult = b.review_result || '改善中'
|
||||
await withTransaction(async (client) => {
|
||||
await client.query(`
|
||||
UPDATE analytics.store_task
|
||||
SET status = CASE WHEN $1 = '达标' THEN '已验收'
|
||||
WHEN $1 = '未改善' THEN '已验收'
|
||||
ELSE '进行中' END,
|
||||
verification_result = $1,
|
||||
verification_indicator = $2,
|
||||
incomplete_reason = $3,
|
||||
next_step = $4,
|
||||
updated_at = NOW()
|
||||
WHERE task_id = $5
|
||||
`, [reviewResult, b.verification_indicator, b.incomplete_reason, b.next_step, id])
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO analytics.store_task_log (task_id, action, new_status, operator, comment)
|
||||
VALUES ($1, '月度验收', $2, $3, $4)
|
||||
`, [id, reviewResult, req.user?.name || 'hq', JSON.stringify({
|
||||
revenue_stable: b.revenue_stable,
|
||||
margin_improved: b.margin_improved,
|
||||
customer_stable: b.customer_stable,
|
||||
anomaly_decreased: b.anomaly_decreased,
|
||||
})])
|
||||
|
||||
const task = await client.query(`SELECT store_code, store_name, problem_indicator, plan_month, baseline_value, target_value FROM analytics.store_task WHERE task_id = $1`, [id])
|
||||
if (task.rows.length > 0) {
|
||||
const t = task.rows[0]
|
||||
await client.query(`
|
||||
INSERT INTO analytics.task_monthly_review
|
||||
(task_id, store_code, store_name, plan_month, problem_indicator,
|
||||
baseline_value, target_value, actual_value, review_result,
|
||||
revenue_stable, margin_improved, customer_stable, anomaly_decreased)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
`, [id, t.store_code, t.store_name, t.plan_month, t.problem_indicator,
|
||||
t.baseline_value, t.target_value, b.actual_value, reviewResult,
|
||||
b.revenue_stable || false, b.margin_improved || false,
|
||||
b.customer_stable || false, b.anomaly_decreased || false])
|
||||
}
|
||||
})
|
||||
sendSuccess(res, { task_id: id, review_result: reviewResult })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:id/rollback', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id)
|
||||
const reason = req.body.reason
|
||||
await withTransaction(async (client) => {
|
||||
await client.query(`
|
||||
UPDATE analytics.store_task SET status = '已回滚', incomplete_reason = $1, updated_at = NOW()
|
||||
WHERE task_id = $2
|
||||
`, [reason, id])
|
||||
await client.query(`
|
||||
INSERT INTO analytics.store_task_log (task_id, action, new_status, operator, comment)
|
||||
VALUES ($1, '回滚', '已回滚', $2, $3)
|
||||
`, [id, req.user?.name || 'hq', reason])
|
||||
})
|
||||
sendSuccess(res, { task_id: id })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 本体标准 (Ontology Standard)
|
||||
// ============================================================
|
||||
|
||||
router.get('/ontology/overview', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const dims = await query(`
|
||||
SELECT table_name,
|
||||
(SELECT count(*) FROM information_schema.columns WHERE table_schema='analytics' AND table_name=t.table_name) AS column_count,
|
||||
(SELECT reltuples::bigint FROM pg_class WHERE relname=t.table_name) AS row_count
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema='analytics' AND t.table_name LIKE 'dim\_%'
|
||||
ORDER BY t.table_name
|
||||
`)
|
||||
const facts = await query(`
|
||||
SELECT table_name,
|
||||
(SELECT count(*) FROM information_schema.columns WHERE table_schema='analytics' AND table_name=t.table_name) AS column_count,
|
||||
(SELECT reltuples::bigint FROM pg_class WHERE relname=t.table_name) AS row_count
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema='analytics' AND t.table_name LIKE 'fact\_%'
|
||||
ORDER BY t.table_name
|
||||
`)
|
||||
const enums = await query(`
|
||||
SELECT table_name,
|
||||
(SELECT count(*) FROM information_schema.columns WHERE table_schema='analytics' AND table_name=t.table_name) AS column_count,
|
||||
(SELECT reltuples::bigint FROM pg_class WHERE relname=t.table_name) AS row_count
|
||||
FROM information_schema.tables t
|
||||
WHERE t.table_schema='analytics' AND t.table_name LIKE 'enum\_%'
|
||||
ORDER BY t.table_name
|
||||
`)
|
||||
const metrics = await query(`
|
||||
SELECT count(*) AS total,
|
||||
count(*) FILTER (WHERE metric_category IS NOT NULL) AS standardized,
|
||||
count(*) FILTER (WHERE is_active = true) AS active
|
||||
FROM analytics.indicator_dictionary
|
||||
`)
|
||||
sendSuccess(res, {
|
||||
dimensions: dims.rows,
|
||||
facts: facts.rows,
|
||||
enums: enums.rows,
|
||||
metrics: metrics.rows[0],
|
||||
})
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/ontology/dim/:table', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const tableName = req.params.table
|
||||
if (!/^dim_\w+$/.test(tableName)) { sendError(res, 'Invalid table name'); return }
|
||||
const cols = await query(`
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='analytics' AND table_name=$1
|
||||
ORDER BY ordinal_position
|
||||
`, [tableName])
|
||||
const rows = await query(`SELECT * FROM analytics.${tableName} LIMIT 100`)
|
||||
sendSuccess(res, { columns: cols.rows, rows: rows.rows })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/ontology/fact/:table', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const tableName = req.params.table
|
||||
if (!/^fact_\w+$/.test(tableName)) { sendError(res, 'Invalid table name'); return }
|
||||
const cols = await query(`
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='analytics' AND table_name=$1
|
||||
ORDER BY ordinal_position
|
||||
`, [tableName])
|
||||
const count = await query(`SELECT count(*) AS cnt FROM analytics.${tableName}`)
|
||||
sendSuccess(res, { columns: cols.rows, total: count.rows[0].cnt })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/ontology/enum/:table', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const tableName = req.params.table
|
||||
if (!/^enum_\w+$/.test(tableName)) { sendError(res, 'Invalid table name'); return }
|
||||
const rows = await query(`SELECT * FROM analytics.${tableName} ORDER BY sort_order`)
|
||||
sendSuccess(res, rows.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/ontology/metrics', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT * FROM analytics.indicator_dictionary
|
||||
ORDER BY metric_category NULLS LAST, indicator_name
|
||||
`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user