- departments.name → department_name (3곳) - users → sso_users 테이블 참조 수정 (7곳) - tbm_sessions.start_time → created_at (존재하지 않는 컬럼) - tbm_team_assignments JOIN: ta.user_id → ta.worker_id - workers leader JOIN: leader.worker_id → leader.user_id - tbm_weather_conditions → weather_conditions 테이블명 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
590 lines
20 KiB
JavaScript
590 lines
20 KiB
JavaScript
const { getPool } = require('../middleware/auth');
|
|
|
|
// ==================== DB 마이그레이션 ====================
|
|
|
|
const runMigration = async () => {
|
|
const db = getPool();
|
|
try {
|
|
// 컬럼 존재 여부 체크
|
|
const [cols] = await db.query(
|
|
`SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'workplace_visit_requests'
|
|
AND COLUMN_NAME IN ('request_type','visitor_name','department_id','check_in_time','check_out_time')`
|
|
);
|
|
const existing = cols.map(c => c.COLUMN_NAME);
|
|
|
|
if (!existing.includes('request_type')) {
|
|
await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN request_type ENUM('external','internal') NOT NULL DEFAULT 'external' AFTER request_id`);
|
|
console.log('[migration] Added request_type column');
|
|
}
|
|
if (!existing.includes('visitor_name')) {
|
|
await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN visitor_name VARCHAR(100) NULL AFTER visitor_company`);
|
|
console.log('[migration] Added visitor_name column');
|
|
}
|
|
if (!existing.includes('department_id')) {
|
|
await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN department_id INT NULL AFTER visitor_name`);
|
|
console.log('[migration] Added department_id column');
|
|
}
|
|
if (!existing.includes('check_in_time')) {
|
|
await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN check_in_time DATETIME NULL AFTER status`);
|
|
console.log('[migration] Added check_in_time column');
|
|
}
|
|
if (!existing.includes('check_out_time')) {
|
|
await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN check_out_time DATETIME NULL AFTER check_in_time`);
|
|
console.log('[migration] Added check_out_time column');
|
|
}
|
|
|
|
// status ENUM 확장 (checked_in, checked_out 추가)
|
|
await db.query(`ALTER TABLE workplace_visit_requests MODIFY COLUMN status ENUM('pending','approved','rejected','training_completed','checked_in','checked_out') NOT NULL DEFAULT 'pending'`);
|
|
|
|
// 인덱스 추가 (이미 있으면 무시)
|
|
try { await db.query(`CREATE INDEX idx_visit_date_type ON workplace_visit_requests (visit_date, request_type)`); } catch (e) { /* already exists */ }
|
|
try { await db.query(`CREATE INDEX idx_checkin_time ON workplace_visit_requests (check_in_time)`); } catch (e) { /* already exists */ }
|
|
|
|
console.log('[migration] workplace_visit_requests migration complete');
|
|
} catch (err) {
|
|
console.error('[migration] Error:', err.message);
|
|
}
|
|
};
|
|
|
|
// ==================== 출입 신청 관리 ====================
|
|
|
|
const createVisitRequest = async (requestData) => {
|
|
const db = getPool();
|
|
const {
|
|
requester_id,
|
|
request_type = 'external',
|
|
visitor_company,
|
|
visitor_name = null,
|
|
visitor_count = 1,
|
|
department_id = null,
|
|
category_id,
|
|
workplace_id,
|
|
visit_date,
|
|
visit_time,
|
|
purpose_id,
|
|
notes = null
|
|
} = requestData;
|
|
|
|
// 내부 출입은 바로 approved
|
|
const status = request_type === 'internal' ? 'approved' : 'pending';
|
|
|
|
const [result] = await db.query(
|
|
`INSERT INTO workplace_visit_requests
|
|
(requester_id, request_type, visitor_company, visitor_name, visitor_count,
|
|
department_id, category_id, workplace_id, visit_date, visit_time, purpose_id, notes, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[requester_id, request_type, visitor_company, visitor_name, visitor_count,
|
|
department_id, category_id, workplace_id, visit_date, visit_time, purpose_id, notes, status]
|
|
);
|
|
|
|
return result.insertId;
|
|
};
|
|
|
|
const getAllVisitRequests = async (filters = {}) => {
|
|
const db = getPool();
|
|
let query = `
|
|
SELECT
|
|
vr.request_id, vr.requester_id, vr.request_type,
|
|
vr.visitor_company, vr.visitor_name, vr.visitor_count,
|
|
vr.department_id, vr.category_id, vr.workplace_id,
|
|
vr.visit_date, vr.visit_time,
|
|
vr.purpose_id, vr.notes, vr.status,
|
|
vr.check_in_time, vr.check_out_time,
|
|
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
|
vr.created_at, vr.updated_at,
|
|
u.username as requester_name, u.name as requester_full_name,
|
|
wc.category_name, w.workplace_name,
|
|
vpt.purpose_name,
|
|
approver.username as approver_name,
|
|
d.department_name
|
|
FROM workplace_visit_requests vr
|
|
INNER JOIN sso_users u ON vr.requester_id = u.user_id
|
|
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
|
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
|
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
|
LEFT JOIN sso_users approver ON vr.approved_by = approver.user_id
|
|
LEFT JOIN departments d ON vr.department_id = d.department_id
|
|
WHERE 1=1
|
|
`;
|
|
|
|
const params = [];
|
|
|
|
if (filters.status) {
|
|
query += ` AND vr.status = ?`;
|
|
params.push(filters.status);
|
|
}
|
|
if (filters.visit_date) {
|
|
query += ` AND vr.visit_date = ?`;
|
|
params.push(filters.visit_date);
|
|
}
|
|
if (filters.start_date && filters.end_date) {
|
|
query += ` AND vr.visit_date BETWEEN ? AND ?`;
|
|
params.push(filters.start_date, filters.end_date);
|
|
}
|
|
if (filters.requester_id) {
|
|
query += ` AND vr.requester_id = ?`;
|
|
params.push(filters.requester_id);
|
|
}
|
|
if (filters.category_id) {
|
|
query += ` AND vr.category_id = ?`;
|
|
params.push(filters.category_id);
|
|
}
|
|
if (filters.request_type) {
|
|
query += ` AND vr.request_type = ?`;
|
|
params.push(filters.request_type);
|
|
}
|
|
|
|
query += ` ORDER BY vr.visit_date DESC, vr.visit_time DESC, vr.created_at DESC`;
|
|
|
|
const [rows] = await db.query(query, params);
|
|
return rows;
|
|
};
|
|
|
|
const getVisitRequestById = async (requestId) => {
|
|
const db = getPool();
|
|
const [rows] = await db.query(
|
|
`SELECT
|
|
vr.request_id, vr.requester_id, vr.request_type,
|
|
vr.visitor_company, vr.visitor_name, vr.visitor_count,
|
|
vr.department_id, vr.category_id, vr.workplace_id,
|
|
vr.visit_date, vr.visit_time,
|
|
vr.purpose_id, vr.notes, vr.status,
|
|
vr.check_in_time, vr.check_out_time,
|
|
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
|
vr.created_at, vr.updated_at,
|
|
u.username as requester_name, u.name as requester_full_name,
|
|
wc.category_name, w.workplace_name,
|
|
vpt.purpose_name,
|
|
approver.username as approver_name,
|
|
d.department_name
|
|
FROM workplace_visit_requests vr
|
|
INNER JOIN sso_users u ON vr.requester_id = u.user_id
|
|
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
|
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
|
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
|
LEFT JOIN sso_users approver ON vr.approved_by = approver.user_id
|
|
LEFT JOIN departments d ON vr.department_id = d.department_id
|
|
WHERE vr.request_id = ?`,
|
|
[requestId]
|
|
);
|
|
return rows[0];
|
|
};
|
|
|
|
const updateVisitRequest = async (requestId, requestData) => {
|
|
const db = getPool();
|
|
const {
|
|
visitor_company, visitor_count, category_id, workplace_id,
|
|
visit_date, visit_time, purpose_id, notes
|
|
} = requestData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET visitor_company = ?, visitor_count = ?, category_id = ?, workplace_id = ?,
|
|
visit_date = ?, visit_time = ?, purpose_id = ?, notes = ?, updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[visitor_company, visitor_count, category_id, workplace_id,
|
|
visit_date, visit_time, purpose_id, notes, requestId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const deleteVisitRequest = async (requestId) => {
|
|
const db = getPool();
|
|
const [result] = await db.query(
|
|
`DELETE FROM workplace_visit_requests WHERE request_id = ?`,
|
|
[requestId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const approveVisitRequest = async (requestId, approvedBy) => {
|
|
const db = getPool();
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = 'approved', approved_by = ?, approved_at = NOW(), updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[approvedBy, requestId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const rejectVisitRequest = async (requestId, rejectionData) => {
|
|
const db = getPool();
|
|
const { approved_by, rejection_reason } = rejectionData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = 'rejected', approved_by = ?, approved_at = NOW(),
|
|
rejection_reason = ?, updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[approved_by, rejection_reason, requestId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const updateVisitRequestStatus = async (requestId, status) => {
|
|
const db = getPool();
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = ?, updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[status, requestId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
// ==================== 방문 목적 관리 ====================
|
|
|
|
const getAllVisitPurposes = async () => {
|
|
const db = getPool();
|
|
const [rows] = await db.query(
|
|
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
|
FROM visit_purpose_types
|
|
ORDER BY display_order ASC, purpose_id ASC`
|
|
);
|
|
return rows;
|
|
};
|
|
|
|
const getActiveVisitPurposes = async () => {
|
|
const db = getPool();
|
|
const [rows] = await db.query(
|
|
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
|
FROM visit_purpose_types
|
|
WHERE is_active = TRUE
|
|
ORDER BY display_order ASC, purpose_id ASC`
|
|
);
|
|
return rows;
|
|
};
|
|
|
|
const createVisitPurpose = async (purposeData) => {
|
|
const db = getPool();
|
|
const { purpose_name, display_order = 0, is_active = true } = purposeData;
|
|
|
|
const [result] = await db.query(
|
|
`INSERT INTO visit_purpose_types (purpose_name, display_order, is_active)
|
|
VALUES (?, ?, ?)`,
|
|
[purpose_name, display_order, is_active]
|
|
);
|
|
return result.insertId;
|
|
};
|
|
|
|
const updateVisitPurpose = async (purposeId, purposeData) => {
|
|
const db = getPool();
|
|
const { purpose_name, display_order, is_active } = purposeData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE visit_purpose_types
|
|
SET purpose_name = ?, display_order = ?, is_active = ?
|
|
WHERE purpose_id = ?`,
|
|
[purpose_name, display_order, is_active, purposeId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const deleteVisitPurpose = async (purposeId) => {
|
|
const db = getPool();
|
|
const [result] = await db.query(
|
|
`DELETE FROM visit_purpose_types WHERE purpose_id = ?`,
|
|
[purposeId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
// ==================== 안전교육 기록 관리 ====================
|
|
|
|
const createTrainingRecord = async (trainingData) => {
|
|
const db = getPool();
|
|
const {
|
|
request_id, trainer_id, training_date,
|
|
training_start_time, training_end_time = null, training_topics = null
|
|
} = trainingData;
|
|
|
|
const [result] = await db.query(
|
|
`INSERT INTO safety_training_records
|
|
(request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics]
|
|
);
|
|
return result.insertId;
|
|
};
|
|
|
|
const getTrainingRecordByRequestId = async (requestId) => {
|
|
const db = getPool();
|
|
const [rows] = await db.query(
|
|
`SELECT
|
|
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
|
str.training_start_time, str.training_end_time, str.training_topics,
|
|
str.signature_data, str.completed_at, str.created_at, str.updated_at,
|
|
u.username as trainer_name, u.name as trainer_full_name
|
|
FROM safety_training_records str
|
|
INNER JOIN sso_users u ON str.trainer_id = u.user_id
|
|
WHERE str.request_id = ?`,
|
|
[requestId]
|
|
);
|
|
return rows[0];
|
|
};
|
|
|
|
const updateTrainingRecord = async (trainingId, trainingData) => {
|
|
const db = getPool();
|
|
const { training_date, training_start_time, training_end_time, training_topics } = trainingData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE safety_training_records
|
|
SET training_date = ?, training_start_time = ?, training_end_time = ?,
|
|
training_topics = ?, updated_at = NOW()
|
|
WHERE training_id = ?`,
|
|
[training_date, training_start_time, training_end_time, training_topics, trainingId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const completeTraining = async (trainingId, signatureData) => {
|
|
const db = getPool();
|
|
const [result] = await db.query(
|
|
`UPDATE safety_training_records
|
|
SET signature_data = ?, completed_at = NOW(), updated_at = NOW()
|
|
WHERE training_id = ?`,
|
|
[signatureData, trainingId]
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const getTrainingRecords = async (filters = {}) => {
|
|
const db = getPool();
|
|
let query = `
|
|
SELECT
|
|
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
|
str.training_start_time, str.training_end_time, str.training_topics,
|
|
str.completed_at, str.created_at, str.updated_at,
|
|
u.username as trainer_name, u.name as trainer_full_name,
|
|
vr.visitor_company, vr.visitor_count, vr.visit_date
|
|
FROM safety_training_records str
|
|
INNER JOIN sso_users u ON str.trainer_id = u.user_id
|
|
INNER JOIN workplace_visit_requests vr ON str.request_id = vr.request_id
|
|
WHERE 1=1
|
|
`;
|
|
|
|
const params = [];
|
|
|
|
if (filters.training_date) {
|
|
query += ` AND str.training_date = ?`;
|
|
params.push(filters.training_date);
|
|
}
|
|
if (filters.start_date && filters.end_date) {
|
|
query += ` AND str.training_date BETWEEN ? AND ?`;
|
|
params.push(filters.start_date, filters.end_date);
|
|
}
|
|
if (filters.trainer_id) {
|
|
query += ` AND str.trainer_id = ?`;
|
|
params.push(filters.trainer_id);
|
|
}
|
|
|
|
query += ` ORDER BY str.training_date DESC, str.training_start_time DESC`;
|
|
|
|
const [rows] = await db.query(query, params);
|
|
return rows;
|
|
};
|
|
|
|
// ==================== 작업장 분류/작업장 조회 ====================
|
|
|
|
const getAllCategories = async () => {
|
|
const db = getPool();
|
|
const [rows] = await db.query(
|
|
'SELECT category_id, category_name FROM workplace_categories ORDER BY category_name'
|
|
);
|
|
return rows;
|
|
};
|
|
|
|
const getWorkplacesByCategory = async (categoryId) => {
|
|
const db = getPool();
|
|
let query = 'SELECT workplace_id, workplace_name, category_id FROM workplaces';
|
|
const params = [];
|
|
if (categoryId) {
|
|
query += ' WHERE category_id = ?';
|
|
params.push(categoryId);
|
|
}
|
|
query += ' ORDER BY workplace_name';
|
|
const [rows] = await db.query(query, params);
|
|
return rows;
|
|
};
|
|
|
|
// ==================== 체크인/체크아웃 ====================
|
|
|
|
const checkIn = async (requestId, userId) => {
|
|
const db = getPool();
|
|
// 승인 또는 교육완료 상태만 체크인 가능
|
|
const [rows] = await db.query(
|
|
`SELECT request_id, requester_id, status, request_type FROM workplace_visit_requests WHERE request_id = ?`,
|
|
[requestId]
|
|
);
|
|
if (!rows.length) return { error: '신청을 찾을 수 없습니다.', status: 404 };
|
|
const req = rows[0];
|
|
|
|
const allowedStatuses = req.request_type === 'internal'
|
|
? ['approved']
|
|
: ['approved', 'training_completed'];
|
|
if (!allowedStatuses.includes(req.status)) {
|
|
return { error: `체크인 불가 상태입니다. (현재: ${req.status})`, status: 400 };
|
|
}
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = 'checked_in', check_in_time = NOW(), updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[requestId]
|
|
);
|
|
return { success: true, result };
|
|
};
|
|
|
|
const checkOut = async (requestId, userId) => {
|
|
const db = getPool();
|
|
const [rows] = await db.query(
|
|
`SELECT request_id, requester_id, status FROM workplace_visit_requests WHERE request_id = ?`,
|
|
[requestId]
|
|
);
|
|
if (!rows.length) return { error: '신청을 찾을 수 없습니다.', status: 404 };
|
|
if (rows[0].status !== 'checked_in') {
|
|
return { error: `체크아웃 불가 상태입니다. (현재: ${rows[0].status})`, status: 400 };
|
|
}
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = 'checked_out', check_out_time = NOW(), updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[requestId]
|
|
);
|
|
return { success: true, result };
|
|
};
|
|
|
|
// ==================== 통합 대시보드 ====================
|
|
|
|
const getEntryDashboard = async (date) => {
|
|
const db = getPool();
|
|
const query = `
|
|
SELECT 'visit' as source, vr.request_type, vr.visitor_company, vr.visitor_name,
|
|
vr.visitor_count, wc.category_name, w.workplace_name,
|
|
vr.visit_date as entry_date, vr.visit_time as entry_time,
|
|
vr.check_in_time, vr.check_out_time, vr.status,
|
|
u.name as reporter_name, vpt.purpose_name, NULL as source_note
|
|
FROM workplace_visit_requests vr
|
|
LEFT JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
|
LEFT JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
|
LEFT JOIN sso_users u ON vr.requester_id = u.user_id
|
|
LEFT JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
|
WHERE vr.visit_date = ? AND vr.status NOT IN ('pending','rejected')
|
|
|
|
UNION ALL
|
|
|
|
SELECT 'tbm' as source, 'internal' as request_type, NULL as visitor_company, wk.worker_name as visitor_name,
|
|
1 as visitor_count, NULL as category_name, ts.work_location as workplace_name,
|
|
ts.session_date as entry_date, TIME(ts.created_at) as entry_time,
|
|
ts.created_at as check_in_time, ts.end_time as check_out_time,
|
|
CASE WHEN ta.is_present=1 THEN 'checked_in' ELSE 'absent' END as status,
|
|
leader.worker_name as reporter_name, '작업(TBM)' as purpose_name, 'TBM 세션 기준' as source_note
|
|
FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions ts ON ta.session_id = ts.session_id
|
|
JOIN workers wk ON ta.worker_id = wk.worker_id
|
|
LEFT JOIN workers leader ON ts.leader_user_id = leader.user_id
|
|
WHERE ts.session_date = ? AND ts.status != 'cancelled'
|
|
|
|
UNION ALL
|
|
|
|
SELECT 'partner' as source, 'external' as request_type,
|
|
pc.company_name as visitor_company, pwc.worker_names as visitor_name,
|
|
pwc.actual_worker_count as visitor_count, NULL as category_name, ps.workplace_name,
|
|
DATE(pwc.check_in_time) as entry_date, TIME(pwc.check_in_time) as entry_time,
|
|
pwc.check_in_time, pwc.check_out_time,
|
|
CASE WHEN pwc.check_out_time IS NOT NULL THEN 'checked_out' ELSE 'checked_in' END as status,
|
|
u2.name as reporter_name, ps.work_description as purpose_name, NULL as source_note
|
|
FROM partner_work_checkins pwc
|
|
JOIN partner_schedules ps ON pwc.schedule_id = ps.id
|
|
JOIN partner_companies pc ON pwc.company_id = pc.id
|
|
LEFT JOIN sso_users u2 ON pwc.checked_by = u2.user_id
|
|
WHERE DATE(pwc.check_in_time) = ?
|
|
|
|
ORDER BY entry_date DESC, check_in_time DESC
|
|
`;
|
|
|
|
const [rows] = await db.query(query, [date, date, date]);
|
|
return rows;
|
|
};
|
|
|
|
const getEntryStats = async (date) => {
|
|
const db = getPool();
|
|
|
|
// 방문자 (visit source — request_type별 분리)
|
|
const [visitStats] = await db.query(
|
|
`SELECT request_type, SUM(visitor_count) as cnt
|
|
FROM workplace_visit_requests
|
|
WHERE visit_date = ? AND status = 'checked_in'
|
|
GROUP BY request_type`,
|
|
[date]
|
|
);
|
|
|
|
// 협력업체
|
|
const [partnerStats] = await db.query(
|
|
`SELECT COALESCE(SUM(actual_worker_count), 0) as cnt
|
|
FROM partner_work_checkins
|
|
WHERE DATE(check_in_time) = ? AND check_out_time IS NULL`,
|
|
[date]
|
|
);
|
|
|
|
// TBM (참고용)
|
|
const [tbmStats] = await db.query(
|
|
`SELECT COUNT(*) as cnt FROM tbm_team_assignments ta
|
|
JOIN tbm_sessions ts ON ta.session_id = ts.session_id
|
|
WHERE ts.session_date = ? AND ts.status != 'cancelled' AND ta.is_present = 1`,
|
|
[date]
|
|
);
|
|
|
|
const external = visitStats.find(v => v.request_type === 'external');
|
|
const internal = visitStats.find(v => v.request_type === 'internal');
|
|
|
|
return {
|
|
external_visit: Number(external?.cnt || 0),
|
|
internal_visit: Number(internal?.cnt || 0),
|
|
partner: Number(partnerStats[0]?.cnt || 0),
|
|
tbm: Number(tbmStats[0]?.cnt || 0)
|
|
};
|
|
};
|
|
|
|
// ==================== 부서 목록 조회 ====================
|
|
|
|
const getAllDepartments = async () => {
|
|
const db = getPool();
|
|
const [rows] = await db.query(
|
|
'SELECT department_id, department_name FROM departments WHERE is_active = 1 ORDER BY department_name'
|
|
);
|
|
return rows;
|
|
};
|
|
|
|
module.exports = {
|
|
runMigration,
|
|
createVisitRequest,
|
|
getAllVisitRequests,
|
|
getVisitRequestById,
|
|
updateVisitRequest,
|
|
deleteVisitRequest,
|
|
approveVisitRequest,
|
|
rejectVisitRequest,
|
|
updateVisitRequestStatus,
|
|
checkIn,
|
|
checkOut,
|
|
getEntryDashboard,
|
|
getEntryStats,
|
|
getAllVisitPurposes,
|
|
getActiveVisitPurposes,
|
|
createVisitPurpose,
|
|
updateVisitPurpose,
|
|
deleteVisitPurpose,
|
|
createTrainingRecord,
|
|
getTrainingRecordByRequestId,
|
|
updateTrainingRecord,
|
|
completeTraining,
|
|
getTrainingRecords,
|
|
getAllCategories,
|
|
getWorkplacesByCategory,
|
|
getAllDepartments
|
|
};
|