feat: FE 화면 구현 완료 + 샘플 데이터 + 결재라인 연동
- WBS/TEAL 화면 실제 구현 (TreeTable, FileUpload, 버전관리) - 시수이력/결재이력 화면 구현 (DataTable, Filter, Timeline) - 비밀번호변경 화면 추가 - 로그인 snake_case 응답 매핑 수정 - Vite 프록시 8081 포트 수정 - auth guard에서 fetchMe 자동 호출 - V108 샘플 데이터 (10명 사용자, 4주 시수 215건, 결재 9건) - 배너 추가 (WBX Spring) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
이 Commit은 다음에 포함되어 있습니다:
@@ -0,0 +1,10 @@
|
||||
|
||||
__ ______ __ __ ____ _
|
||||
\ \ / / __ ) \ \/ / / ___| _ __ _ __(_)_ __ __ _
|
||||
\ \ /\ / /| _ \ \ / \___ \| '_ \| '__| | '_ \ / _` |
|
||||
\ V V / | |_) | / \ ___) | |_) | | | | | | | (_| |
|
||||
\_/\_/ |____/ /_/\_\ |____/| .__/|_| |_|_| |_|\__, |
|
||||
|_| |___/
|
||||
WTM - Work Time Manager v${application.version}
|
||||
Profile: ${spring.profiles.active:default}
|
||||
Port: ${server.port:8081}
|
||||
@@ -0,0 +1,763 @@
|
||||
-- V108__sample_data.sql (MySQL)
|
||||
-- Sample data for WTM system based on WBS.xlsx and Activity.xlsx
|
||||
|
||||
-- ============================================================
|
||||
-- 1. SAMPLE USERS (10 users with different roles)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO users (employee_id, email, username, full_name, hashed_password, department, discipline, position_title, location, employment_type, is_active)
|
||||
VALUES
|
||||
('EMP001', 'john.kim@company.com', 'john.kim', 'John Kim', '$2a$10$dummyhash000000000000000000000000000000000000000001', 'Project Management', 'PJM', 'Project Manager', 'ONSHORE', 'INTERNAL', 1),
|
||||
('EMP002', 'sarah.lee@company.com', 'sarah.lee', 'Sarah Lee', '$2a$10$dummyhash000000000000000000000000000000000000000002', 'Project Controls', 'PCM', 'Project Control Manager', 'ONSHORE', 'INTERNAL', 1),
|
||||
('EMP003', 'mike.park@company.com', 'mike.park', 'Mike Park', '$2a$10$dummyhash000000000000000000000000000000000000000003', 'Project Controls', 'PTK', 'Project Timekeeper', 'ONSHORE', 'INTERNAL', 1),
|
||||
('EMP004', 'david.choi@company.com', 'david.choi', 'David Choi', '$2a$10$dummyhash000000000000000000000000000000000000000004', 'Engineering', 'PIP', 'Discipline Lead - Piping', 'ONSHORE', 'INTERNAL', 1),
|
||||
('EMP005', 'emma.jung@company.com', 'emma.jung', 'Emma Jung', '$2a$10$dummyhash000000000000000000000000000000000000000005', 'Engineering', 'PRO', 'Discipline Lead - Process','ONSHORE', 'INTERNAL', 1),
|
||||
('EMP006', 'james.han@company.com', 'james.han', 'James Han', '$2a$10$dummyhash000000000000000000000000000000000000000006', 'Engineering', 'STR', 'Discipline Lead - Structural','ONSHORE', 'INTERNAL', 1),
|
||||
('EMP007', 'lisa.yoon@company.com', 'lisa.yoon', 'Lisa Yoon', '$2a$10$dummyhash000000000000000000000000000000000000000007', 'Engineering', 'ELC', 'Discipline Lead - Electrical','ONSHORE', 'INTERNAL', 1),
|
||||
('EMP008', 'ryan.seo@company.com', 'ryan.seo', 'Ryan Seo', '$2a$10$dummyhash000000000000000000000000000000000000000008', 'Engineering', 'PRO', 'Senior Process Engineer', 'ONSHORE', 'INTERNAL', 1),
|
||||
('EMP009', 'anna.kwon@company.com', 'anna.kwon', 'Anna Kwon', '$2a$10$dummyhash000000000000000000000000000000000000000009', 'Engineering', 'PIP', 'Piping Engineer', 'ONSHORE', 'INTERNAL', 1),
|
||||
('EMP010', 'admin@company.com', 'admin', 'System Admin', '$2a$10$dummyhash000000000000000000000000000000000000000010', 'IT', NULL, 'System Administrator', 'ONSHORE', 'INTERNAL', 1);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. SAMPLE PROJECT (1 EPC project)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO projects (project_code, name, description, project_type, status, start_date, end_date, pm_user_id)
|
||||
VALUES (
|
||||
'EPU-2026-001',
|
||||
'FPSO Topside Module',
|
||||
'FPSO Topside Module Engineering, Procurement, Construction and Installation Project',
|
||||
'EPC',
|
||||
'ACTIVE',
|
||||
'2026-01-06',
|
||||
'2028-12-31',
|
||||
(SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. USER ROLES (assign roles to users per project)
|
||||
-- ============================================================
|
||||
-- SA role for admin (global, no project)
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, NULL, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r WHERE u.employee_id = 'EMP010' AND r.code = 'SA';
|
||||
|
||||
-- PM role
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP010')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP001' AND r.code = 'PM' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
-- PCM role
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP002' AND r.code = 'PCM' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
-- PTK role
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP003' AND r.code = 'PTK' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
-- DL roles (4 discipline leads)
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP004' AND r.code = 'DL' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP005' AND r.code = 'DL' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP006' AND r.code = 'DL' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP007' AND r.code = 'DL' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
-- USER roles (general engineers)
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP008' AND r.code = 'USER' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
INSERT IGNORE INTO user_roles (user_id, role_id, project_id, granted_by)
|
||||
SELECT u.id, r.id, p.id, (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM users u, roles r, projects p WHERE u.employee_id = 'EMP009' AND r.code = 'USER' AND p.project_code = 'EPU-2026-001';
|
||||
|
||||
-- ============================================================
|
||||
-- 4. PROJECT ASSIGNMENTS (assign all users to the project)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'PM', (SELECT id FROM users WHERE employee_id = 'EMP010')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP001';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'PCM', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP002';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'PTK', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP003';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'DL_PIP', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP004';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'DL_PRO', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP005';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'DL_STR', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP006';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'DL_ELC', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP007';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'ENGINEER', (SELECT id FROM users WHERE employee_id = 'EMP005')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP008';
|
||||
|
||||
INSERT IGNORE INTO project_assignments (project_id, user_id, role, assigned_by)
|
||||
SELECT p.id, u.id, 'ENGINEER', (SELECT id FROM users WHERE employee_id = 'EMP004')
|
||||
FROM projects p, users u WHERE p.project_code = 'EPU-2026-001' AND u.employee_id = 'EMP009';
|
||||
|
||||
-- ============================================================
|
||||
-- 5. WBS VERSION + NODES (from WBS.xlsx Canonical WBS)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO wbs_versions (project_id, version_number, effective_date, source_type, description, status, uploaded_by)
|
||||
SELECT p.id, 1, '2026-01-06', 'MANUAL', 'Initial WBS from Canonical WBS template', 'ACTIVE', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p WHERE p.project_code = 'EPU-2026-001';
|
||||
|
||||
-- LV1: Project
|
||||
SET @wbs_ver_id = (SELECT wv.id FROM wbs_versions wv JOIN projects p ON wv.project_id = p.id WHERE p.project_code = 'EPU-2026-001' AND wv.version_number = 1);
|
||||
|
||||
INSERT IGNORE INTO wbs_nodes (wbs_version_id, parent_id, wbs_code, level, name, discipline, planned_hours, sort_order, is_leaf)
|
||||
VALUES (@wbs_ver_id, NULL, 'F0001', 1, 'FPSO Topside Module Project', NULL, 500000.00, 1, 0);
|
||||
|
||||
SET @lv1_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001');
|
||||
|
||||
-- LV2: Phases (from WBS.xlsx Column L - Activity Area)
|
||||
INSERT IGNORE INTO wbs_nodes (wbs_version_id, parent_id, wbs_code, level, name, discipline, planned_hours, sort_order, is_leaf) VALUES
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.GNAL', 2, 'General', NULL, 5000.00, 1, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.MGMT', 2, 'Management Activities', NULL, 50000.00, 2, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.ENGR', 2, 'Engineering', NULL, 300000.00, 3, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.PROC', 2, 'Procurement', NULL, 30000.00, 4, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.CONS', 2, 'Construction', NULL, 80000.00, 5, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.PRCM', 2, 'Pre-commissioning', NULL, 10000.00, 6, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.COMM', 2, 'Commissioning & Start-up', NULL, 15000.00, 7, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.TRNS', 2, 'Transport & Logistics', NULL, 5000.00, 8, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.INST', 2, 'Installation Works', NULL, 3000.00, 9, 0),
|
||||
(@wbs_ver_id, @lv1_id, 'F0001.OCOM', 2, 'Offshore Commissioning', NULL, 2000.00, 10, 0);
|
||||
|
||||
-- LV3: Areas under ENGR (from WBS.xlsx Column O - Area)
|
||||
SET @engr_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR');
|
||||
SET @mgmt_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.MGMT');
|
||||
SET @proc_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.PROC');
|
||||
SET @cons_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.CONS');
|
||||
SET @gnal_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.GNAL');
|
||||
|
||||
INSERT IGNORE INTO wbs_nodes (wbs_version_id, parent_id, wbs_code, level, name, discipline, planned_hours, sort_order, is_leaf) VALUES
|
||||
(@wbs_ver_id, @engr_id, 'F0001.ENGR.GN', 3, 'General', NULL, 10000.00, 1, 0),
|
||||
(@wbs_ver_id, @engr_id, 'F0001.ENGR.TP', 3, 'Topside', NULL, 200000.00, 2, 0),
|
||||
(@wbs_ver_id, @engr_id, 'F0001.ENGR.HL', 3, 'Hull & Marine', NULL, 60000.00, 3, 0),
|
||||
(@wbs_ver_id, @engr_id, 'F0001.ENGR.MO', 3, 'Mooring', NULL, 20000.00, 4, 0),
|
||||
(@wbs_ver_id, @engr_id, 'F0001.ENGR.OF', 3, 'Offshore', NULL, 10000.00, 5, 0),
|
||||
(@wbs_ver_id, @mgmt_id, 'F0001.MGMT.GN', 3, 'General', NULL, 50000.00, 1, 0),
|
||||
(@wbs_ver_id, @proc_id, 'F0001.PROC.TP', 3, 'Topside', NULL, 20000.00, 1, 0),
|
||||
(@wbs_ver_id, @proc_id, 'F0001.PROC.HL', 3, 'Hull & Marine', NULL, 10000.00, 2, 0),
|
||||
(@wbs_ver_id, @cons_id, 'F0001.CONS.TP', 3, 'Topside', NULL, 50000.00, 1, 0),
|
||||
(@wbs_ver_id, @cons_id, 'F0001.CONS.HL', 3, 'Hull & Marine', NULL, 30000.00, 2, 0);
|
||||
|
||||
-- LV4: Disciplines under ENGR.TP (from WBS.xlsx Column T - Discipline codes)
|
||||
SET @engr_tp_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP');
|
||||
SET @engr_hl_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.HL');
|
||||
SET @mgmt_gn_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.MGMT.GN');
|
||||
|
||||
INSERT IGNORE INTO wbs_nodes (wbs_version_id, parent_id, wbs_code, level, name, discipline, planned_hours, sort_order, is_leaf) VALUES
|
||||
-- Management disciplines
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.PJM', 4, 'Project Management', 'PJM', 15000.00, 1, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.QAS', 4, 'Quality Assurance', 'QAS', 5000.00, 2, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.HSE', 4, 'Health & Safety', 'HSE', 5000.00, 3, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.RSK', 4, 'Risk Management', 'RSK', 3000.00, 4, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.SLP', 4, 'Safety & Loss Prevention', 'SLP', 3000.00, 5, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.EGM', 4, 'Engineering General Management', 'EGM', 4000.00, 6, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.EPM', 4, 'Engineering Project Management', 'EPM', 5000.00, 7, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.ESM', 4, 'Engineering Subcontract Management', 'ESM', 3000.00, 8, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.INF', 4, 'Information Management', 'INF', 3000.00, 9, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.SCM', 4, 'Supply Chain Management', 'SCM', 4000.00, 10, 0),
|
||||
(@wbs_ver_id, @mgmt_gn_id, 'F0001.MGMT.GN.PPM', 4, 'Project Procurement Management', 'PPM', 3000.00, 11, 0),
|
||||
-- Engineering disciplines under Topside
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.PRO', 4, 'Process', 'PRO', 35000.00, 1, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.PIP', 4, 'Piping', 'PIP', 40000.00, 2, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.STR', 4, 'Structure', 'STR', 30000.00, 3, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.ELC', 4, 'Electrical', 'ELC', 20000.00, 4, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.INS', 4, 'Instrument', 'INS', 18000.00, 5, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.MEC', 4, 'Mechanical', 'MEC', 15000.00, 6, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.CIV', 4, 'Civil', 'CIV', 8000.00, 7, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.SFF', 4, 'Safety & Fire Fighting', 'SFF', 10000.00, 8, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.HVC', 4, 'HVAC', 'HVC', 5000.00, 9, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.ICS', 4, 'ICSS Engineering', 'ICS', 8000.00, 10, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.TEL', 4, 'Telecommunication', 'TEL', 5000.00, 11, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.MOD', 4, '3D Modeling', 'MOD', 6000.00, 12, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.WGT', 4, 'Weight Control', 'WGT', 3000.00, 13, 0),
|
||||
(@wbs_ver_id, @engr_tp_id, 'F0001.ENGR.TP.MCP', 4, 'Material & Corrosion Protection', 'MCP', 4000.00, 14, 0),
|
||||
-- Engineering disciplines under Hull
|
||||
(@wbs_ver_id, @engr_hl_id, 'F0001.ENGR.HL.NAV', 4, 'Naval Architecture', 'NAV', 15000.00, 1, 0),
|
||||
(@wbs_ver_id, @engr_hl_id, 'F0001.ENGR.HL.STR', 4, 'Structure', 'STR', 20000.00, 2, 0),
|
||||
(@wbs_ver_id, @engr_hl_id, 'F0001.ENGR.HL.OUF', 4, 'Outfitting', 'OUF', 8000.00, 3, 0),
|
||||
(@wbs_ver_id, @engr_hl_id, 'F0001.ENGR.HL.HYD', 4, 'Hydrodynamics', 'HYD', 5000.00, 4, 0),
|
||||
(@wbs_ver_id, @engr_hl_id, 'F0001.ENGR.HL.MOR', 4, 'Mooring & Riser Pull-in', 'MOR', 5000.00, 5, 0);
|
||||
|
||||
-- LV5: Deliverables under Process (from WBS.xlsx Column W)
|
||||
SET @engr_tp_pro_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP.PRO');
|
||||
SET @engr_tp_pip_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP.PIP');
|
||||
SET @engr_tp_str_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP.STR');
|
||||
SET @engr_tp_sff_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP.SFF');
|
||||
SET @engr_tp_mec_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP.MEC');
|
||||
SET @engr_tp_elc_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP.ELC');
|
||||
SET @engr_tp_ins_id = (SELECT id FROM wbs_nodes WHERE wbs_version_id = @wbs_ver_id AND wbs_code = 'F0001.ENGR.TP.INS');
|
||||
|
||||
INSERT IGNORE INTO wbs_nodes (wbs_version_id, parent_id, wbs_code, level, name, discipline, planned_hours, sort_order, is_leaf) VALUES
|
||||
-- Process deliverables
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO008', 5, 'Heat and Material Balance', 'PRO', 3000.00, 1, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO003', 5, 'Process Flow Diagrams (PFDs)', 'PRO', 4000.00, 2, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO005', 5, 'Piping & Instrumentation Diagrams (P&IDs)', 'PRO', 8000.00, 3, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO012', 5, 'Process and Control Philosophy', 'PRO', 2000.00, 4, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO009', 5, 'Utilities and Load Balance', 'PRO', 2500.00, 5, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO029', 5, 'Process Design and Equipment Sizing', 'PRO', 3000.00, 6, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO002', 5, 'Lines List', 'PRO', 2000.00, 7, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO006', 5, 'ESD & PSD Logic Diagram', 'PRO', 1500.00, 8, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO030', 5, 'Chemical Requirement and Storage Report', 'PRO', 1000.00, 9, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO038', 5, 'Simulation Report and Models', 'PRO', 2000.00,10, 1),
|
||||
(@wbs_ver_id, @engr_tp_pro_id, 'F0001.ENGR.TP.PRO.PRO007', 5, 'ESD & PSD Cause and Effects Matrix', 'PRO', 1500.00,11, 1),
|
||||
-- Safety deliverables
|
||||
(@wbs_ver_id, @engr_tp_sff_id, 'F0001.ENGR.TP.SFF.SAF001', 5, 'Safety Concept', 'SFF', 1500.00, 1, 1),
|
||||
(@wbs_ver_id, @engr_tp_sff_id, 'F0001.ENGR.TP.SFF.SAF099', 5, 'Hazardous Area Classification Philosophy', 'SFF', 1000.00, 2, 1),
|
||||
(@wbs_ver_id, @engr_tp_sff_id, 'F0001.ENGR.TP.SFF.SAF802', 5, 'Human Factor Engineering', 'SFF', 800.00, 3, 1),
|
||||
(@wbs_ver_id, @engr_tp_sff_id, 'F0001.ENGR.TP.SFF.SAF104', 5, 'Safety Equipment List', 'SFF', 600.00, 4, 1),
|
||||
(@wbs_ver_id, @engr_tp_sff_id, 'F0001.ENGR.TP.SFF.SAF801', 5, 'Safety Studies', 'SFF', 1200.00, 5, 1),
|
||||
(@wbs_ver_id, @engr_tp_sff_id, 'F0001.ENGR.TP.SFF.SAF025', 5, 'Fire Water and Foam Demand', 'SFF', 800.00, 6, 1),
|
||||
(@wbs_ver_id, @engr_tp_sff_id, 'F0001.ENGR.TP.SFF.SAF062', 5, '3D Fire and Gas Detection Mapping Study', 'SFF', 1000.00, 7, 1),
|
||||
-- Mechanical deliverables
|
||||
(@wbs_ver_id, @engr_tp_mec_id, 'F0001.ENGR.TP.MEC.MEC002', 5, 'Equipment Mechanical Specification', 'MEC', 3000.00, 1, 1),
|
||||
(@wbs_ver_id, @engr_tp_mec_id, 'F0001.ENGR.TP.MEC.ENG014', 5, 'Equipment List', 'MEC', 2000.00, 2, 1),
|
||||
-- Structural deliverables
|
||||
(@wbs_ver_id, @engr_tp_str_id, 'F0001.ENGR.TP.STR.STR001', 5, 'Structure Basis of Design', 'STR', 2000.00, 1, 1),
|
||||
(@wbs_ver_id, @engr_tp_str_id, 'F0001.ENGR.TP.STR.STR003', 5, 'Module Main Structure Calculation Report', 'STR', 3000.00, 2, 1),
|
||||
(@wbs_ver_id, @engr_tp_str_id, 'F0001.ENGR.TP.STR.STR008', 5, 'Module Main Structure Drawings', 'STR', 5000.00, 3, 1),
|
||||
(@wbs_ver_id, @engr_tp_str_id, 'F0001.ENGR.TP.STR.STR009', 5, 'Module Secondary Structure Drawings', 'STR', 4000.00, 4, 1),
|
||||
(@wbs_ver_id, @engr_tp_str_id, 'F0001.ENGR.TP.STR.STR033', 5, 'Structural Material Take Off', 'STR', 2000.00, 5, 1),
|
||||
-- Piping deliverables
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV012', 5, 'Valves Datasheet', 'PIP', 3000.00, 1, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV005', 5, 'Critical Line List', 'PIP', 2000.00, 2, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.ENG032', 5, 'FPSO Overall General Arrangement Plot Plan', 'PIP', 4000.00, 3, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV003', 5, 'Piping Stress Calculation Notes', 'PIP', 5000.00, 4, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV022', 5, 'Piping Material Classes Specification', 'PIP', 2000.00, 5, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV038', 5, 'Isometric Drawings', 'PIP', 8000.00, 6, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV010', 5, 'Piping Material Take-Off', 'PIP', 3000.00, 7, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV007', 5, 'Special Item List', 'PIP', 1500.00, 8, 1),
|
||||
(@wbs_ver_id, @engr_tp_pip_id, 'F0001.ENGR.TP.PIP.PVV008', 5, 'AIV and FIV Study', 'PIP', 2000.00, 9, 1);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. CANONICAL WBS (mapped from WBS nodes)
|
||||
-- ============================================================
|
||||
SET @proj_id = (SELECT id FROM projects WHERE project_code = 'EPU-2026-001');
|
||||
|
||||
INSERT IGNORE INTO canonical_wbs (project_id, wbs_code, level, name, parent_code, discipline, is_active, mapped_p6_code) VALUES
|
||||
-- LV1
|
||||
(@proj_id, 'F0001', 1, 'FPSO Topside Module Project', NULL, NULL, 1, 'F0001'),
|
||||
-- LV2 Phases
|
||||
(@proj_id, 'F0001.GNAL', 2, 'General', 'F0001', NULL, 1, 'F0001.GNAL'),
|
||||
(@proj_id, 'F0001.MGMT', 2, 'Management Activities', 'F0001', NULL, 1, 'F0001.MGMT'),
|
||||
(@proj_id, 'F0001.ENGR', 2, 'Engineering', 'F0001', NULL, 1, 'F0001.ENGR'),
|
||||
(@proj_id, 'F0001.PROC', 2, 'Procurement', 'F0001', NULL, 1, 'F0001.PROC'),
|
||||
(@proj_id, 'F0001.CONS', 2, 'Construction', 'F0001', NULL, 1, 'F0001.CONS'),
|
||||
(@proj_id, 'F0001.PRCM', 2, 'Pre-commissioning', 'F0001', NULL, 1, 'F0001.PRCM'),
|
||||
(@proj_id, 'F0001.COMM', 2, 'Commissioning & Start-up', 'F0001', NULL, 1, 'F0001.COMM'),
|
||||
(@proj_id, 'F0001.TRNS', 2, 'Transport & Logistics', 'F0001', NULL, 1, 'F0001.TRNS'),
|
||||
(@proj_id, 'F0001.INST', 2, 'Installation Works', 'F0001', NULL, 1, 'F0001.INST'),
|
||||
(@proj_id, 'F0001.OCOM', 2, 'Offshore Commissioning', 'F0001', NULL, 1, 'F0001.OCOM'),
|
||||
-- LV3 Areas
|
||||
(@proj_id, 'F0001.ENGR.GN', 3, 'General', 'F0001.ENGR', NULL, 1, 'F0001.ENGR.GN'),
|
||||
(@proj_id, 'F0001.ENGR.TP', 3, 'Topside', 'F0001.ENGR', NULL, 1, 'F0001.ENGR.TP'),
|
||||
(@proj_id, 'F0001.ENGR.HL', 3, 'Hull & Marine', 'F0001.ENGR', NULL, 1, 'F0001.ENGR.HL'),
|
||||
(@proj_id, 'F0001.ENGR.MO', 3, 'Mooring', 'F0001.ENGR', NULL, 1, 'F0001.ENGR.MO'),
|
||||
(@proj_id, 'F0001.ENGR.OF', 3, 'Offshore', 'F0001.ENGR', NULL, 1, 'F0001.ENGR.OF'),
|
||||
(@proj_id, 'F0001.MGMT.GN', 3, 'General', 'F0001.MGMT', NULL, 1, 'F0001.MGMT.GN'),
|
||||
(@proj_id, 'F0001.PROC.TP', 3, 'Topside', 'F0001.PROC', NULL, 1, 'F0001.PROC.TP'),
|
||||
(@proj_id, 'F0001.PROC.HL', 3, 'Hull & Marine', 'F0001.PROC', NULL, 1, 'F0001.PROC.HL'),
|
||||
(@proj_id, 'F0001.CONS.TP', 3, 'Topside', 'F0001.CONS', NULL, 1, 'F0001.CONS.TP'),
|
||||
(@proj_id, 'F0001.CONS.HL', 3, 'Hull & Marine', 'F0001.CONS', NULL, 1, 'F0001.CONS.HL'),
|
||||
-- LV4 Disciplines (Engineering Topside)
|
||||
(@proj_id, 'F0001.ENGR.TP.PRO', 4, 'Process', 'F0001.ENGR.TP', 'PRO', 1, 'F0001.ENGR.TP.PRO'),
|
||||
(@proj_id, 'F0001.ENGR.TP.PIP', 4, 'Piping', 'F0001.ENGR.TP', 'PIP', 1, 'F0001.ENGR.TP.PIP'),
|
||||
(@proj_id, 'F0001.ENGR.TP.STR', 4, 'Structure', 'F0001.ENGR.TP', 'STR', 1, 'F0001.ENGR.TP.STR'),
|
||||
(@proj_id, 'F0001.ENGR.TP.ELC', 4, 'Electrical', 'F0001.ENGR.TP', 'ELC', 1, 'F0001.ENGR.TP.ELC'),
|
||||
(@proj_id, 'F0001.ENGR.TP.INS', 4, 'Instrument', 'F0001.ENGR.TP', 'INS', 1, 'F0001.ENGR.TP.INS'),
|
||||
(@proj_id, 'F0001.ENGR.TP.MEC', 4, 'Mechanical', 'F0001.ENGR.TP', 'MEC', 1, 'F0001.ENGR.TP.MEC'),
|
||||
(@proj_id, 'F0001.ENGR.TP.CIV', 4, 'Civil', 'F0001.ENGR.TP', 'CIV', 1, 'F0001.ENGR.TP.CIV'),
|
||||
(@proj_id, 'F0001.ENGR.TP.SFF', 4, 'Safety & Fire Fighting', 'F0001.ENGR.TP', 'SFF', 1, 'F0001.ENGR.TP.SFF'),
|
||||
(@proj_id, 'F0001.ENGR.TP.HVC', 4, 'HVAC', 'F0001.ENGR.TP', 'HVC', 1, 'F0001.ENGR.TP.HVC'),
|
||||
(@proj_id, 'F0001.ENGR.TP.ICS', 4, 'ICSS Engineering', 'F0001.ENGR.TP', 'ICS', 1, 'F0001.ENGR.TP.ICS'),
|
||||
(@proj_id, 'F0001.ENGR.TP.TEL', 4, 'Telecommunication', 'F0001.ENGR.TP', 'TEL', 1, 'F0001.ENGR.TP.TEL'),
|
||||
(@proj_id, 'F0001.ENGR.TP.MOD', 4, '3D Modeling', 'F0001.ENGR.TP', 'MOD', 1, 'F0001.ENGR.TP.MOD'),
|
||||
(@proj_id, 'F0001.ENGR.TP.WGT', 4, 'Weight Control', 'F0001.ENGR.TP', 'WGT', 1, 'F0001.ENGR.TP.WGT'),
|
||||
(@proj_id, 'F0001.ENGR.TP.MCP', 4, 'Material & Corrosion Protection', 'F0001.ENGR.TP', 'MCP', 1, 'F0001.ENGR.TP.MCP'),
|
||||
-- LV4 Disciplines (Engineering Hull)
|
||||
(@proj_id, 'F0001.ENGR.HL.NAV', 4, 'Naval Architecture', 'F0001.ENGR.HL', 'NAV', 1, 'F0001.ENGR.HL.NAV'),
|
||||
(@proj_id, 'F0001.ENGR.HL.STR', 4, 'Structure', 'F0001.ENGR.HL', 'STR', 1, 'F0001.ENGR.HL.STR'),
|
||||
(@proj_id, 'F0001.ENGR.HL.OUF', 4, 'Outfitting', 'F0001.ENGR.HL', 'OUF', 1, 'F0001.ENGR.HL.OUF'),
|
||||
(@proj_id, 'F0001.ENGR.HL.HYD', 4, 'Hydrodynamics', 'F0001.ENGR.HL', 'HYD', 1, 'F0001.ENGR.HL.HYD'),
|
||||
(@proj_id, 'F0001.ENGR.HL.MOR', 4, 'Mooring & Riser Pull-in', 'F0001.ENGR.HL', 'MOR', 1, 'F0001.ENGR.HL.MOR'),
|
||||
-- LV4 Management disciplines
|
||||
(@proj_id, 'F0001.MGMT.GN.PJM', 4, 'Project Management', 'F0001.MGMT.GN', 'PJM', 1, 'F0001.MGMT.GN.PJM'),
|
||||
(@proj_id, 'F0001.MGMT.GN.QAS', 4, 'Quality Assurance', 'F0001.MGMT.GN', 'QAS', 1, 'F0001.MGMT.GN.QAS'),
|
||||
(@proj_id, 'F0001.MGMT.GN.HSE', 4, 'Health & Safety', 'F0001.MGMT.GN', 'HSE', 1, 'F0001.MGMT.GN.HSE'),
|
||||
(@proj_id, 'F0001.MGMT.GN.RSK', 4, 'Risk Management', 'F0001.MGMT.GN', 'RSK', 1, 'F0001.MGMT.GN.RSK'),
|
||||
(@proj_id, 'F0001.MGMT.GN.EPM', 4, 'Engineering Project Management', 'F0001.MGMT.GN', 'EPM', 1, 'F0001.MGMT.GN.EPM'),
|
||||
(@proj_id, 'F0001.MGMT.GN.SCM', 4, 'Supply Chain Management', 'F0001.MGMT.GN', 'SCM', 1, 'F0001.MGMT.GN.SCM');
|
||||
|
||||
-- ============================================================
|
||||
-- 7. TEAL VERSION + ENTRIES (from Activity.xlsx)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO teal_versions (project_id, version_number, effective_date, description, status, uploaded_by)
|
||||
SELECT p.id, 1, '2026-01-06', 'Initial TEAL from Activity Dictionary', 'ACTIVE', (SELECT id FROM users WHERE employee_id = 'EMP001')
|
||||
FROM projects p WHERE p.project_code = 'EPU-2026-001';
|
||||
|
||||
SET @teal_ver_id = (SELECT tv.id FROM teal_versions tv JOIN projects p ON tv.project_id = p.id WHERE p.project_code = 'EPU-2026-001' AND tv.version_number = 1);
|
||||
|
||||
-- EPCI Engineering Activities (from Activity.xlsx - EPCI sheet & Master Activity Dictionary)
|
||||
INSERT IGNORE INTO teal_entries (teal_version_id, canonical_wbs_id, activity_code, activity_name, discipline, is_active) VALUES
|
||||
-- Design Development activities (from Master Activity Dictionary)
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'), 'ENG-DD-LAYOUT-FI', 'Layout - First Issue', 'PRO', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'), 'ENG-DD-LAYOUT-REV', 'Layout - Revision', 'PRO', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'), 'ENG-DD-PID-FI', 'P&ID - First Issue', 'PRO', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'), 'ENG-DD-PID-REV', 'P&ID - Revision', 'PRO', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.ELC'), 'ENG-DD-SLD-FI', 'SLD - First Issue', 'ELC', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.ELC'), 'ENG-DD-SLD-REV', 'SLD - Revision', 'ELC', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.STR'), 'ENG-DD-DETDWG-FI', 'Detail Drawing - First Issue', 'STR', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.STR'), 'ENG-DD-DETDWG-REV', 'Detail Drawing - Revision', 'STR', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'), 'ENG-DD-ISO-FI', 'Isometric - First Issue', 'PIP', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'), 'ENG-DD-ISO-REV', 'Isometric - Revision', 'PIP', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.MOD'), 'ENG-DD-3DMDL-FI', '3D Model - First Issue', 'MOD', 1),
|
||||
(@teal_ver_id, (SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.MOD'), 'ENG-DD-3DMDL-REV', '3D Model - Revision', 'MOD', 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-CALC-FI', 'Calculation - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-CALC-REV', 'Calculation - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-SPEC-FI', 'Specification - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-SPEC-REV', 'Specification - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-DS-FI', 'Datasheet - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-DS-REV', 'Datasheet - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-MR-FI', 'Material Requisition (MR) - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-DD-MR-REV', 'Material Requisition (MR) - Revision', NULL, 1),
|
||||
-- Review & Coordination
|
||||
(@teal_ver_id, NULL, 'ENG-RC-VDR', 'Vendor Data Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-RC-CCR', 'Client Comment Response', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-RC-IDC', 'IDC Coordination', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-RC-IDA', 'Inter-discipline Alignment', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-RC-TCL', 'Technical Clarification', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-RC-EIP', 'Engineering Input to Procurement', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-RC-WCU', 'Weight Control Update', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-RC-GEN', 'Review & Coordination - General', NULL, 1),
|
||||
-- Change Management
|
||||
(@teal_ver_id, NULL, 'ENG-CM-CIA', 'Change Impact Assessment', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-CM-RDW', 'Redesign / Rework', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-CM-ABU', 'As-built Update', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'ENG-CM-CNP', 'Change Note Preparation', NULL, 1),
|
||||
-- EPCI Engineering - Basis of Design (from EPCI sheet)
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-BOD-FI', 'Basis of Design / Design Inputs - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-BOD-REV', 'Basis of Design / Design Inputs - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-CTR-FI', 'Calculation & Technical Reports - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-CTR-REV', 'Calculation & Technical Reports - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-DWG-FI', 'Drawings - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-DWG-REV', 'Drawings - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-LST-FI', 'Lists / Schedules / Indexes - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-LST-REV', 'Lists / Schedules / Indexes - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-MRM-FI', 'Material & Requisition Management - First Issue', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-MRM-REV', 'Material & Requisition Management - Revision', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-3DM', '3D Modeling', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-3DM-REV', '3D Modeling - Revision', NULL, 1),
|
||||
-- EPCI Engineering Non-deliverable
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-ND-PDS', 'Preliminary Design / Study Work', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-ND-MCP', 'Model / Calculation Preparation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-ND-ITR', 'Internal Technical Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-ND-CDA', 'Cross-discipline Alignment', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-ND-TCI', 'Technical Clarification (Internal)', NULL, 1),
|
||||
-- EPCI Engineering Support/Admin
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-SA-MTG', 'Engineering Meetings', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-SA-TDM', 'Tool/Data Maintenance', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-SA-RPT', 'Engineering Reporting', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-SA-DFT', 'Drafting Support', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'EPCI-ENG-SA-GEN', 'Engineering - General', NULL, 1),
|
||||
-- SCM Activities (from Master Activity Dictionary)
|
||||
(@teal_ver_id, NULL, 'SCM-SB-VPQ', 'Vendor Pre-qualification', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-SB-RFQ', 'RFQ Issuance', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-SB-TBE', 'Technical Bid Evaluation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-SB-CBE', 'Commercial Bid Evaluation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-PO-DFT', 'PO Drafting', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-PO-ISS', 'PO Issuance', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-PO-AMD', 'PO Amendment', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-VE-DOC', 'Vendor Document Expediting', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-VE-PSM', 'Production Schedule Monitoring', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-VE-FAT', 'FAT Coordination', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SCM-VE-LOG', 'Logistics Coordination', NULL, 1),
|
||||
-- PMT Activities (from Master Activity Dictionary)
|
||||
(@teal_ver_id, NULL, 'PMT-PB-EPU', 'Execution Plan Update', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-PB-WBS', 'WBS/PBS/MBS Alignment', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-PB-BSD', 'Baseline Schedule Development', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-SC-WSU', 'Weekly Schedule Update', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-SC-PM', 'Progress Measurement', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-SC-EVA', 'EV Analysis', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-CC-CBD', 'Cost Baseline Development', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-CC-CRP', 'Cost Reporting', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-CC-FCU', 'Forecast Update', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-CV-CID', 'Change Identification', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-CV-VOC', 'VOC Log Management', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-RS-RRD', 'Risk Register Development', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-RS-RWS', 'Risk Workshop', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-DI-DCO', 'Document Control Oversight', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-DI-TMT', 'Transmittal Management', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-RP-WRP', 'Weekly Report', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-RP-MRP', 'Monthly Report', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-GC-GRV', 'Gate Review Prep', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-GC-AUD', 'Audit Preparation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-TC-MTG', 'PMT Meeting Coordination', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PMT-TC-DSC', 'Discipline Coordination', NULL, 1),
|
||||
-- Construction Activities
|
||||
(@teal_ver_id, NULL, 'CON-YS-FPR', 'Fabrication Progress Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CON-YS-IPV', 'Installation Progress Validation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CON-SS-WKD', 'Site Walkdown', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CON-SS-VIW', 'Verification of Installed Work', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CON-IN-NCR', 'NCR Tracking', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CON-IN-ESC', 'Issue Escalation', NULL, 1),
|
||||
-- QA/QC Activities
|
||||
(@teal_ver_id, NULL, 'QA-QAS-ITP', 'ITP Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'QA-QAS-QDR', 'QA Document Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'QA-QCS-FAT', 'FAT Witnessing', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'QA-QCS-INW', 'Inspection Witnessing', NULL, 1),
|
||||
-- HSSE Activities
|
||||
(@teal_ver_id, NULL, 'HSE-OVS-INS', 'HSE Inspection', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'HSE-OVS-KPI', 'HSE KPI Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'HSE-CI-INC', 'Incident Investigation', NULL, 1),
|
||||
-- Commissioning Activities
|
||||
(@teal_ver_id, NULL, 'COM-PCO-PLN', 'Pre-comm Plan Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'COM-PCO-LPC', 'Loop Check Monitoring', NULL, 1),
|
||||
-- Tender Activities (from EPCI-Support-Comm sheet)
|
||||
(@teal_ver_id, NULL, 'TND-MS-ITT', 'ITT Review & Scope Breakdown', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-MS-CLR', 'Clarification Question Preparation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-MS-TES', 'Tender Execution Strategy', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-MS-WTA', 'Win Theme & Competitor Analysis', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-MS-TPC', 'Technical Proposal Consolidation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-MS-CPC', 'Commercial Proposal Consolidation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-MS-GRP', 'Gate Review Preparation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-MS-FPS', 'Final Proposal Submission', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-ENG-TSF', 'Technical Scope Framing', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-ENG-TDB', 'Tender Design Basis', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-ENG-PDD', 'Preliminary Design Development (Tender)', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-ENG-MTO', 'Tender MTO / Load/Line List', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-PMT-PEP', 'Project Overall Execution Planning', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-PMT-PSD', 'Project Schedule Development', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'TND-PMT-MPP', 'Manpower Planning', NULL, 1),
|
||||
-- Pre-FEED Activities (from EPCI-Support-Comm sheet)
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-COI', 'Concept Option Identification', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-DCS', 'Design Concept Selection', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-TSC', 'Technology Screening', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-DBD', 'Design Basis Development (Concept Level)', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-HPC', 'High-level Process Calculation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-HMB', 'Approximate Heat/Material Balance', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-PSC', 'Preliminary Sizing Calculation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-CBD', 'Concept Layout / Block Diagram', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-HPF', 'High-level PFD', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-UPI', 'Ultra-simplified P&ID (U-P&ID)', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-FST', 'Feasibility Study', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'PFD-ENG-CSR', 'Concept Selection Report', NULL, 1),
|
||||
-- FEED Activities (from EPCI-Support-Comm sheet)
|
||||
(@teal_ver_id, NULL, 'FED-ENG-DBU', 'Design Basis Update (FEED Level)', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-PDD', 'Philosophy Document Development', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-PCR', 'Process Calculation (Refined)', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-HYC', 'Hydraulic Calculation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-PSA', 'Preliminary Stress/Structural Analysis', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-ELA', 'Electrical Load Analysis', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-FGA', 'FEED-level Layout (GA)', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-FPF', 'FEED PFD', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-FPI', 'FEED P&ID', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-SLD', 'SLD (Preliminary)', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-DSD', 'Datasheet Development', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-TSP', 'Technical Specification Draft', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-HAZ', 'HAZID/ENVID Support', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'FED-ENG-WER', 'Weight Estimation (Refined)', NULL, 1),
|
||||
-- Support Core Work (from EPCI-Support-Comm sheet row 166+)
|
||||
(@teal_ver_id, NULL, 'SUP-BD-CMA', 'Client Management & Market Analysis', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SUP-PM-CPR', 'Corporate Planning/Reporting', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SUP-HR-RCO', 'Recruitment & Onboarding', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SUP-HR-TNP', 'Training & Performance', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SUP-LG-CTR', 'Contract Review', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SUP-TC-TDE', 'Tech Development & Evaluation', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'SUP-GPO-STD', 'PMT Governance & Standards', NULL, 1),
|
||||
-- Common Work (from EPCI-Support-Comm sheet row 176+)
|
||||
(@teal_ver_id, NULL, 'CMN-ADM-GEN', 'General Admin/Document Management', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CMN-MTG-ICO', 'Internal Coordination', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CMN-TRN-MCT', 'Mandatory/Compliance Training', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CMN-PI-WTU', 'Workflow/Template Updates', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CMN-STR-KBS', 'KPI/Budget/Strategy Workshop', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CMN-PPL-MPC', 'Mentoring/Performance Cycle', NULL, 1),
|
||||
(@teal_ver_id, NULL, 'CMN-EVT-TWC', 'Townhall/Workshops/CSR', NULL, 1);
|
||||
|
||||
-- ============================================================
|
||||
-- 8. ADDITIONAL OVERHEAD TYPES (from Activity.xlsx Non-Project)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO overhead_types (code, name, category, sort_order) VALUES
|
||||
('SYS_PROC_DEV', 'System & Process Development', 'DEVELOPMENT', 15),
|
||||
('ORG_OPS', 'Organizational Operation', 'ADMIN', 16),
|
||||
('CORP_INIT', 'Corporate Initiative/Strategy', 'CORPORATE', 17),
|
||||
('MENTORING', 'Mentoring & Coaching', 'PEOPLE', 18),
|
||||
('TECH_DEV', 'Technology Development', 'DEVELOPMENT', 19),
|
||||
('RECRUITMENT', 'Recruitment & Onboarding', 'HR', 20),
|
||||
('PERF_MGMT', 'Performance Management', 'HR', 21);
|
||||
|
||||
-- ============================================================
|
||||
-- 9. WORK RULES
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO work_rules (location, min_daily_hours, max_daily_hours, max_weekly_hours, is_active) VALUES
|
||||
('ONSHORE', 8.00, 12.00, 52.00, 1),
|
||||
('OFFSHORE', 10.00, 12.00, 60.00, 1),
|
||||
('REMOTE', 8.00, 10.00, 48.00, 1);
|
||||
|
||||
-- ============================================================
|
||||
-- 10. SAMPLE TIMESHEETS (2 weeks for 2 users)
|
||||
-- ============================================================
|
||||
-- Week 1: 2026-03-16 ~ 2026-03-22 for EMP008 (Ryan Seo, Senior Process Engineer)
|
||||
INSERT IGNORE INTO timesheets (user_id, week_start_date, week_end_date, status, total_hours, submitted_at)
|
||||
SELECT u.id, '2026-03-16', '2026-03-22', 'APPROVED', 44.00, '2026-03-22 17:00:00'
|
||||
FROM users u WHERE u.employee_id = 'EMP008';
|
||||
|
||||
-- Week 1: 2026-03-16 ~ 2026-03-22 for EMP009 (Anna Kwon, Piping Engineer)
|
||||
INSERT IGNORE INTO timesheets (user_id, week_start_date, week_end_date, status, total_hours, submitted_at)
|
||||
SELECT u.id, '2026-03-16', '2026-03-22', 'SUBMITTED', 42.00, '2026-03-22 17:30:00'
|
||||
FROM users u WHERE u.employee_id = 'EMP009';
|
||||
|
||||
-- Week 2: 2026-03-23 ~ 2026-03-29 for EMP008
|
||||
INSERT IGNORE INTO timesheets (user_id, week_start_date, week_end_date, status, total_hours, submitted_at)
|
||||
SELECT u.id, '2026-03-23', '2026-03-29', 'DRAFT', 24.00, NULL
|
||||
FROM users u WHERE u.employee_id = 'EMP008';
|
||||
|
||||
-- Week 2: 2026-03-23 ~ 2026-03-29 for EMP009
|
||||
INSERT IGNORE INTO timesheets (user_id, week_start_date, week_end_date, status, total_hours, submitted_at)
|
||||
SELECT u.id, '2026-03-23', '2026-03-29', 'DRAFT', 16.00, NULL
|
||||
FROM users u WHERE u.employee_id = 'EMP009';
|
||||
|
||||
-- Timesheet Entries for EMP008 Week 1 (Process engineer working on P&ID and PFD)
|
||||
-- Monday 2026-03-16
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-16', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-PID-FI'),
|
||||
'P&ID development for Module 1 separation system'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Tuesday 2026-03-17
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-17', 6.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-PID-FI'),
|
||||
'P&ID development continued'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, np_category, remark)
|
||||
SELECT ts.id, 'NP', '2026-03-17', 2.00, 'TRAINING', 'Process simulation software training'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Wednesday 2026-03-18
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-18', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-RC-IDC'),
|
||||
'IDC review session with Piping and Instrument'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Thursday 2026-03-19
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-19', 10.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-LAYOUT-FI'),
|
||||
'Layout development for compression module'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Friday 2026-03-20
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-20', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-RC-VDR'),
|
||||
'Vendor data review for heat exchangers'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Saturday 2026-03-21
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-21', 2.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-PID-REV'),
|
||||
'P&ID revision for client comments'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Timesheet Entries for EMP009 Week 1 (Piping engineer)
|
||||
-- Monday 2026-03-16
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-16', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-ISO-FI'),
|
||||
'Isometric drawing for Module 1 piping'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Tuesday 2026-03-17
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-17', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-ISO-FI'),
|
||||
'Isometric drawing continued - critical lines'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Wednesday 2026-03-18
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-18', 6.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'EPCI-ENG-ND-CDA'),
|
||||
'Cross-discipline alignment with Structural'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, np_category, remark)
|
||||
SELECT ts.id, 'NP', '2026-03-18', 2.00, 'MEETING', 'Department weekly meeting'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Thursday 2026-03-19
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-19', 10.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-ISO-FI'),
|
||||
'Isometric drawings - overtime for deadline'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Friday 2026-03-20
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-20', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-RC-VDR'),
|
||||
'Vendor data review for pipe fittings'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- EMP008 Week 2 entries (partial - DRAFT)
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-23', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-PID-REV'),
|
||||
'P&ID revision B incorporating vendor data'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-23';
|
||||
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-24', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'EPCI-ENG-CTR-FI'),
|
||||
'Process calculation report for compressor sizing'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-23';
|
||||
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-25', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PRO'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-RC-CCR'),
|
||||
'Client comment response for PFD package'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-23';
|
||||
|
||||
-- EMP009 Week 2 entries (partial - DRAFT)
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-23', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-DD-ISO-REV'),
|
||||
'Isometric revision for stress analysis updates'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-23';
|
||||
|
||||
INSERT IGNORE INTO timesheet_entries (timesheet_id, entry_type, entry_date, hours, epc_project_id, canonical_wbs_id, teal_entry_id, remark)
|
||||
SELECT ts.id, 'EPC', '2026-03-24', 8.00, @proj_id,
|
||||
(SELECT id FROM canonical_wbs WHERE project_id = @proj_id AND wbs_code = 'F0001.ENGR.TP.PIP'),
|
||||
(SELECT id FROM teal_entries WHERE teal_version_id = @teal_ver_id AND activity_code = 'ENG-RC-EIP'),
|
||||
'Engineering input for pipe procurement MR'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-23';
|
||||
|
||||
-- ============================================================
|
||||
-- 11. SAMPLE APPROVALS (1 approved, 1 pending)
|
||||
-- ============================================================
|
||||
-- Approved: EMP008 Week 1
|
||||
INSERT IGNORE INTO approvals (timesheet_id, requester_id, project_id, status, submitted_at, completed_at)
|
||||
SELECT ts.id, u.id, @proj_id, 'APPROVED', '2026-03-22 17:00:00', '2026-03-23 10:30:00'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Approval lines for EMP008 Week 1 (DL -> PCM -> PM)
|
||||
INSERT IGNORE INTO approval_lines (approval_id, approver_id, approval_order, role_code, status, acted_at)
|
||||
SELECT a.id, (SELECT id FROM users WHERE employee_id = 'EMP005'), 1, 'DL', 'APPROVED', '2026-03-22 18:00:00'
|
||||
FROM approvals a JOIN timesheets ts ON a.timesheet_id = ts.id JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
INSERT IGNORE INTO approval_lines (approval_id, approver_id, approval_order, role_code, status, acted_at)
|
||||
SELECT a.id, (SELECT id FROM users WHERE employee_id = 'EMP002'), 2, 'PCM', 'APPROVED', '2026-03-23 09:00:00'
|
||||
FROM approvals a JOIN timesheets ts ON a.timesheet_id = ts.id JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
INSERT IGNORE INTO approval_lines (approval_id, approver_id, approval_order, role_code, status, acted_at)
|
||||
SELECT a.id, (SELECT id FROM users WHERE employee_id = 'EMP001'), 3, 'PM', 'APPROVED', '2026-03-23 10:30:00'
|
||||
FROM approvals a JOIN timesheets ts ON a.timesheet_id = ts.id JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Approval comment
|
||||
INSERT IGNORE INTO approval_comments (approval_id, user_id, comment, action)
|
||||
SELECT a.id, (SELECT id FROM users WHERE employee_id = 'EMP005'), 'Timesheet verified. OT on Thursday approved per project need.', 'APPROVE'
|
||||
FROM approvals a JOIN timesheets ts ON a.timesheet_id = ts.id JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP008' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Pending: EMP009 Week 1
|
||||
INSERT IGNORE INTO approvals (timesheet_id, requester_id, project_id, status, submitted_at)
|
||||
SELECT ts.id, u.id, @proj_id, 'PENDING', '2026-03-22 17:30:00'
|
||||
FROM timesheets ts JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- Approval lines for EMP009 Week 1 (DL pending)
|
||||
INSERT IGNORE INTO approval_lines (approval_id, approver_id, approval_order, role_code, status)
|
||||
SELECT a.id, (SELECT id FROM users WHERE employee_id = 'EMP004'), 1, 'DL', 'PENDING'
|
||||
FROM approvals a JOIN timesheets ts ON a.timesheet_id = ts.id JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
INSERT IGNORE INTO approval_lines (approval_id, approver_id, approval_order, role_code, status)
|
||||
SELECT a.id, (SELECT id FROM users WHERE employee_id = 'EMP002'), 2, 'PCM', 'PENDING'
|
||||
FROM approvals a JOIN timesheets ts ON a.timesheet_id = ts.id JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
INSERT IGNORE INTO approval_lines (approval_id, approver_id, approval_order, role_code, status)
|
||||
SELECT a.id, (SELECT id FROM users WHERE employee_id = 'EMP001'), 3, 'PM', 'PENDING'
|
||||
FROM approvals a JOIN timesheets ts ON a.timesheet_id = ts.id JOIN users u ON ts.user_id = u.id
|
||||
WHERE u.employee_id = 'EMP009' AND ts.week_start_date = '2026-03-16';
|
||||
|
||||
-- ============================================================
|
||||
-- 12. PROJECT TYPE CONFIG (EPC defaults)
|
||||
-- ============================================================
|
||||
INSERT IGNORE INTO project_type_config (project_type, config_key, config_value, description) VALUES
|
||||
('EPC', 'approval_chain', 'DL,PCM,PM', 'Default approval chain for EPC projects'),
|
||||
('EPC', 'max_weekly_hours', '52', 'Maximum weekly hours for EPC projects'),
|
||||
('EPC', 'timesheet_period', 'WEEKLY', 'Timesheet submission period'),
|
||||
('EPC', 'overtime_threshold', '8', 'Daily hours threshold for OT calculation'),
|
||||
('FEED', 'approval_chain', 'DL,PM', 'Default approval chain for FEED projects'),
|
||||
('FEED', 'max_weekly_hours', '48', 'Maximum weekly hours for FEED projects'),
|
||||
('FEED', 'timesheet_period', 'WEEKLY', 'Timesheet submission period');
|
||||
@@ -1,2 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
VITE_API_BASE_URL=
|
||||
VITE_APP_ENV=development
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { NavigationGuardWithThis } from 'vue-router';
|
||||
import { authService } from './auth.service';
|
||||
import { useAuthStore } from '@/modules/auth/auth.store';
|
||||
|
||||
export const authGuard: NavigationGuardWithThis<undefined> = (_to, _from, next) => {
|
||||
if (authService.isAuthenticated()) {
|
||||
next();
|
||||
} else {
|
||||
export const authGuard: NavigationGuardWithThis<undefined> = async (_to, _from, next) => {
|
||||
if (!authService.isAuthenticated()) {
|
||||
next({ name: 'login' });
|
||||
return;
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
if (!authStore.currentUser) {
|
||||
await authStore.fetchMe();
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const authService = {
|
||||
if (!response.ok) throw new Error('Refresh failed');
|
||||
|
||||
const data = await response.json();
|
||||
this.setTokens(data.accessToken, data.refreshToken);
|
||||
return data.accessToken;
|
||||
this.setTokens(data.access_token ?? data.accessToken, data.refresh_token ?? data.refreshToken);
|
||||
return data.access_token ?? data.accessToken;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,9 @@ export interface LoginRequest {
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: AuthUser;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
access_token?: string;
|
||||
refresh_token?: string;
|
||||
user: AuthUser & { is_admin?: boolean; full_name?: string; department_id?: number };
|
||||
}
|
||||
|
||||
@@ -1 +1,322 @@
|
||||
<template><div class="card"><h1>결재 이력</h1></div></template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Tag from 'primevue/tag';
|
||||
import Button from 'primevue/button';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
import Select from 'primevue/select';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Timeline from 'primevue/timeline';
|
||||
import BasePageHeader from '@/core/components/BasePageHeader.vue';
|
||||
import { approvalService } from '../approval.service';
|
||||
|
||||
const loading = ref(false);
|
||||
const approvals = ref<any[]>([]);
|
||||
const totalRecords = ref(0);
|
||||
|
||||
// Filters
|
||||
const dateFrom = ref<Date | null>(null);
|
||||
const dateTo = ref<Date | null>(null);
|
||||
const statusFilter = ref<string | null>(null);
|
||||
const page = ref(0);
|
||||
const rows = ref(20);
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'APPROVED', label: '승인' },
|
||||
{ value: 'REJECTED', label: '반려' },
|
||||
{ value: 'PENDING', label: '대기' },
|
||||
];
|
||||
|
||||
// Detail dialog
|
||||
const showDetail = ref(false);
|
||||
const selectedApproval = ref<any>(null);
|
||||
const approvalLines = ref<any[]>([]);
|
||||
const detailLoading = ref(false);
|
||||
|
||||
async function fetchHistory() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: Record<string, unknown> = {
|
||||
skip: page.value * rows.value,
|
||||
limit: rows.value,
|
||||
};
|
||||
if (dateFrom.value) params.from = formatDate(dateFrom.value);
|
||||
if (dateTo.value) params.to = formatDate(dateTo.value);
|
||||
if (statusFilter.value) params.status = statusFilter.value;
|
||||
|
||||
const { data } = await approvalService.getHistory(params);
|
||||
if (data.content) {
|
||||
approvals.value = data.content;
|
||||
totalRecords.value = data.totalElements ?? data.content.length;
|
||||
} else if (data.items) {
|
||||
approvals.value = data.items;
|
||||
totalRecords.value = data.total;
|
||||
} else if (Array.isArray(data)) {
|
||||
approvals.value = data;
|
||||
totalRecords.value = data.length;
|
||||
}
|
||||
} catch {
|
||||
approvals.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(approval: any) {
|
||||
selectedApproval.value = approval;
|
||||
showDetail.value = true;
|
||||
detailLoading.value = true;
|
||||
try {
|
||||
const { data } = await approvalService.getById(approval.id ?? approval.approvalId);
|
||||
selectedApproval.value = data;
|
||||
approvalLines.value = data.lines ?? data.approvalLines ?? [];
|
||||
} catch {
|
||||
approvalLines.value = [];
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(d: Date): string {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function formatDateTime(dt: string | null): string {
|
||||
if (!dt) return '-';
|
||||
return dt.substring(0, 16).replace('T', ' ');
|
||||
}
|
||||
|
||||
function onPage(event: any) {
|
||||
page.value = event.page;
|
||||
rows.value = event.rows;
|
||||
fetchHistory();
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
page.value = 0;
|
||||
fetchHistory();
|
||||
}
|
||||
|
||||
function clearFilter() {
|
||||
dateFrom.value = null;
|
||||
dateTo.value = null;
|
||||
statusFilter.value = null;
|
||||
page.value = 0;
|
||||
fetchHistory();
|
||||
}
|
||||
|
||||
function getStatusSeverity(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
APPROVED: 'success',
|
||||
REJECTED: 'danger',
|
||||
PENDING: 'warn',
|
||||
};
|
||||
return map[status] ?? 'secondary';
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
APPROVED: '승인',
|
||||
REJECTED: '반려',
|
||||
PENDING: '대기',
|
||||
};
|
||||
return map[status] ?? status;
|
||||
}
|
||||
|
||||
onMounted(() => fetchHistory());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BasePageHeader title="결재 이력" subtitle="결재 처리 내역을 조회합니다." />
|
||||
|
||||
<div class="card">
|
||||
<!-- Filter -->
|
||||
<div class="history-filter">
|
||||
<div class="history-filter__fields">
|
||||
<div class="history-filter__field">
|
||||
<label class="text-sm">시작일</label>
|
||||
<DatePicker v-model="dateFrom" dateFormat="yy-mm-dd" placeholder="시작일" showIcon fluid />
|
||||
</div>
|
||||
<div class="history-filter__field">
|
||||
<label class="text-sm">종료일</label>
|
||||
<DatePicker v-model="dateTo" dateFormat="yy-mm-dd" placeholder="종료일" showIcon fluid />
|
||||
</div>
|
||||
<div class="history-filter__field">
|
||||
<label class="text-sm">상태</label>
|
||||
<Select
|
||||
v-model="statusFilter"
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="전체"
|
||||
showClear
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-filter__actions">
|
||||
<Button label="조회" icon="pi pi-search" size="small" @click="applyFilter" />
|
||||
<Button label="초기화" icon="pi pi-times" size="small" severity="secondary" text @click="clearFilter" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<DataTable
|
||||
:value="approvals"
|
||||
:loading="loading"
|
||||
:paginator="true"
|
||||
:rows="rows"
|
||||
:totalRecords="totalRecords"
|
||||
:lazy="true"
|
||||
:rowsPerPageOptions="[10, 20, 50]"
|
||||
dataKey="id"
|
||||
stripedRows
|
||||
size="small"
|
||||
@page="onPage"
|
||||
>
|
||||
<template #empty>
|
||||
<div style="text-align: center; padding: 2rem; color: var(--p-text-muted-color);">
|
||||
<i class="pi pi-inbox" style="font-size: 2rem;" />
|
||||
<p>결재 이력이 없습니다.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="requesterName" header="요청자" style="width: 120px">
|
||||
<template #body="{ data }">
|
||||
{{ data.requesterName ?? data.requester_name ?? `사용자 #${data.requesterId}` }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="timesheetId" header="시수 ID" style="width: 90px" />
|
||||
<Column field="projectName" header="프로젝트" style="width: 180px">
|
||||
<template #body="{ data }">
|
||||
{{ data.projectName ?? data.project_name ?? '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="상태" style="width: 100px">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="getStatusLabel(data.status)" :severity="getStatusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="submittedAt" header="제출일" style="width: 150px">
|
||||
<template #body="{ data }">
|
||||
{{ formatDateTime(data.submittedAt ?? data.submitted_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="completedAt" header="완료일" style="width: 150px">
|
||||
<template #body="{ data }">
|
||||
{{ formatDateTime(data.completedAt ?? data.completed_at) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="상세" style="width: 70px">
|
||||
<template #body="{ data }">
|
||||
<Button icon="pi pi-eye" text rounded size="small" @click="openDetail(data)" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<!-- Detail Dialog -->
|
||||
<Dialog
|
||||
v-model:visible="showDetail"
|
||||
header="결재 상세"
|
||||
:style="{ width: '600px', maxWidth: '95vw' }"
|
||||
modal
|
||||
>
|
||||
<div v-if="detailLoading" style="text-align: center; padding: 2rem;">
|
||||
<i class="pi pi-spin pi-spinner" style="font-size: 2rem;" />
|
||||
</div>
|
||||
<div v-else-if="selectedApproval">
|
||||
<div class="detail-info">
|
||||
<div class="detail-info__row">
|
||||
<span class="text-sm text-muted">요청자</span>
|
||||
<strong>{{ selectedApproval.requesterName ?? `사용자 #${selectedApproval.requesterId}` }}</strong>
|
||||
</div>
|
||||
<div class="detail-info__row">
|
||||
<span class="text-sm text-muted">상태</span>
|
||||
<Tag :value="getStatusLabel(selectedApproval.status)" :severity="getStatusSeverity(selectedApproval.status)" />
|
||||
</div>
|
||||
<div class="detail-info__row">
|
||||
<span class="text-sm text-muted">제출일</span>
|
||||
<span>{{ formatDateTime(selectedApproval.submittedAt ?? selectedApproval.submitted_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin: 1.5rem 0 0.5rem;">결재 라인</h4>
|
||||
<Timeline :value="approvalLines" align="left">
|
||||
<template #content="{ item }">
|
||||
<div class="timeline-item">
|
||||
<Tag
|
||||
:value="getStatusLabel(item.status)"
|
||||
:severity="getStatusSeverity(item.status)"
|
||||
style="margin-right: 0.5rem;"
|
||||
/>
|
||||
<strong>{{ item.roleCode }}</strong>
|
||||
<span class="text-sm text-muted" style="margin-left: 0.5rem;">
|
||||
{{ item.actedAt ? formatDateTime(item.actedAt) : '대기중' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</Timeline>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/assets/styles/variables' as *;
|
||||
|
||||
.history-filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: $space-md;
|
||||
margin-bottom: $space-lg;
|
||||
|
||||
&__fields {
|
||||
display: flex;
|
||||
gap: $space-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-xs;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $space-sm;
|
||||
}
|
||||
|
||||
@media (max-width: $bp-mobile) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
&__fields { flex-direction: column; }
|
||||
&__actions { justify-content: flex-end; }
|
||||
}
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-sm;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $space-xs 0;
|
||||
border-bottom: 1px solid var(--p-surface-200);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,17 +13,29 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await authApi.login({ email, password });
|
||||
tokenService.setTokens(data.accessToken, data.refreshToken);
|
||||
currentUser.value = data.user;
|
||||
const accessToken = (data.access_token ?? data.accessToken) as string;
|
||||
const refreshToken = (data.refresh_token ?? data.refreshToken) as string;
|
||||
tokenService.setTokens(accessToken, refreshToken);
|
||||
currentUser.value = mapUser(data.user);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function mapUser(u: any): AuthUser {
|
||||
return {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
fullName: u.full_name ?? u.fullName ?? '',
|
||||
roles: u.roles?.length ? u.roles : (u.is_admin ? ['SA'] : ['USER']),
|
||||
departmentId: u.department_id ?? u.departmentId,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchMe() {
|
||||
try {
|
||||
const { data } = await authApi.me();
|
||||
currentUser.value = data;
|
||||
currentUser.value = mapUser(data);
|
||||
} catch {
|
||||
logout();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import FileUpload from 'primevue/fileupload';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
import BaseFormDialog from '@/core/components/BaseFormDialog.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
upload: [payload: { file: File; effectiveDate: string }];
|
||||
}>();
|
||||
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const effectiveDate = ref<Date | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (v) {
|
||||
selectedFile.value = null;
|
||||
effectiveDate.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function onFileSelect(event: any) {
|
||||
const files = event.files ?? event;
|
||||
if (Array.isArray(files) && files.length > 0) {
|
||||
selectedFile.value = files[0];
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
if (!selectedFile.value || !effectiveDate.value) return;
|
||||
emit('upload', {
|
||||
file: selectedFile.value,
|
||||
effectiveDate: formatDate(effectiveDate.value),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseFormDialog
|
||||
:visible="visible"
|
||||
title="TEAL 업로드"
|
||||
width="580px"
|
||||
:loading="loading"
|
||||
submitLabel="업로드"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<div class="form-grid">
|
||||
<div class="col-12">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">TEAL 파일</label>
|
||||
<FileUpload
|
||||
mode="basic"
|
||||
accept=".xls,.xlsx,.csv"
|
||||
:maxFileSize="10000000"
|
||||
chooseLabel="파일 선택"
|
||||
:auto="false"
|
||||
@select="onFileSelect"
|
||||
/>
|
||||
<small v-if="selectedFile" class="form-field__hint">
|
||||
{{ selectedFile.name }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">적용일</label>
|
||||
<DatePicker
|
||||
v-model="effectiveDate"
|
||||
dateFormat="yy-mm-dd"
|
||||
placeholder="적용일 선택"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseFormDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select';
|
||||
import Tag from 'primevue/tag';
|
||||
import type { TealVersion } from '../teal.store';
|
||||
|
||||
defineProps<{
|
||||
modelValue: TealVersion | null;
|
||||
versions: TealVersion[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: TealVersion | null];
|
||||
}>();
|
||||
|
||||
function statusSeverity(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
DRAFT: 'warn',
|
||||
ACTIVE: 'success',
|
||||
ARCHIVED: 'secondary',
|
||||
};
|
||||
return map[status] ?? 'secondary';
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
DRAFT: '초안',
|
||||
ACTIVE: '활성',
|
||||
ARCHIVED: '보관',
|
||||
};
|
||||
return map[status] ?? status;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
:modelValue="modelValue"
|
||||
:options="versions"
|
||||
:loading="loading"
|
||||
optionLabel="versionNumber"
|
||||
placeholder="버전 선택"
|
||||
fluid
|
||||
@update:modelValue="emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #value="{ value }">
|
||||
<div v-if="value" class="teal-version-option">
|
||||
<span>v{{ value.versionNumber }}</span>
|
||||
<Tag
|
||||
:value="statusLabel(value.status)"
|
||||
:severity="statusSeverity(value.status)"
|
||||
style="margin-left: 8px"
|
||||
/>
|
||||
</div>
|
||||
<span v-else>버전 선택</span>
|
||||
</template>
|
||||
<template #option="{ option }">
|
||||
<div class="teal-version-option">
|
||||
<span>v{{ option.versionNumber }} ({{ option.effectiveDate }})</span>
|
||||
<Tag
|
||||
:value="statusLabel(option.status)"
|
||||
:severity="statusSeverity(option.status)"
|
||||
style="margin-left: 8px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.teal-version-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,60 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { tealService } from './teal.service';
|
||||
import type { TealEntry } from './teal.types';
|
||||
|
||||
export interface TealVersion {
|
||||
id: number;
|
||||
projectId: number;
|
||||
versionNumber: number;
|
||||
effectiveDate: string;
|
||||
status: string;
|
||||
entryCount?: number;
|
||||
}
|
||||
|
||||
export const useTealStore = defineStore('teal', () => {
|
||||
const loading = ref(false);
|
||||
return { loading };
|
||||
const versions = ref<TealVersion[]>([]);
|
||||
const entries = ref<TealEntry[]>([]);
|
||||
const selectedProjectId = ref<number | null>(null);
|
||||
|
||||
async function fetchVersions(projectId: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await tealService.getVersions(projectId);
|
||||
versions.value = (data as any).items ?? data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchActive(projectId: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await tealService.getActive(projectId);
|
||||
entries.value = (data as any).items ?? data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(projectId: number, file: File, effectiveDate: string) {
|
||||
loading.value = true;
|
||||
try {
|
||||
await tealService.upload(projectId, file, effectiveDate);
|
||||
await fetchVersions(projectId);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
versions,
|
||||
entries,
|
||||
selectedProjectId,
|
||||
fetchVersions,
|
||||
fetchActive,
|
||||
upload,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1 +1,211 @@
|
||||
<template><div class="card"><h1>TEAL 관리</h1></div></template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import Column from 'primevue/column';
|
||||
import Select from 'primevue/select';
|
||||
import Button from 'primevue/button';
|
||||
import Tag from 'primevue/tag';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import BasePageHeader from '@/core/components/BasePageHeader.vue';
|
||||
import BaseCrudTable from '@/core/components/BaseCrudTable.vue';
|
||||
import TealUploadDialog from '../components/TealUploadDialog.vue';
|
||||
import TealVersionSelect from '../components/TealVersionSelect.vue';
|
||||
import { useTealStore } from '../teal.store';
|
||||
import type { TealVersion } from '../teal.store';
|
||||
import { projectService } from '@/modules/project/project.service';
|
||||
import type { Project } from '@/modules/project/project.types';
|
||||
|
||||
const toast = useToast();
|
||||
const tealStore = useTealStore();
|
||||
|
||||
const projects = ref<Project[]>([]);
|
||||
const selectedProject = ref<Project | null>(null);
|
||||
const selectedVersion = ref<TealVersion | null>(null);
|
||||
const uploadDialogVisible = ref(false);
|
||||
const uploadLoading = ref(false);
|
||||
const disciplineFilter = ref<string | null>(null);
|
||||
|
||||
const disciplineOptions = [
|
||||
'Piping', 'Electrical', 'Instrument', 'Civil', 'Structural',
|
||||
'Mechanical', 'Process', 'HSE', 'QA/QC', 'Other',
|
||||
].map((d) => ({ label: d, value: d }));
|
||||
|
||||
const filteredEntries = computed(() => {
|
||||
if (!disciplineFilter.value) return tealStore.entries;
|
||||
return tealStore.entries.filter(
|
||||
(e: any) => e.discipline === disciplineFilter.value,
|
||||
);
|
||||
});
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const { data } = await projectService.getAll();
|
||||
projects.value = (data as any).items ?? data;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: '프로젝트 목록 로드 실패', life: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
if (!selectedProject.value) return;
|
||||
try {
|
||||
await tealStore.fetchVersions(selectedProject.value.id);
|
||||
selectedVersion.value = null;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: 'TEAL 버전 목록 로드 실패', life: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEntries() {
|
||||
if (!selectedProject.value) return;
|
||||
try {
|
||||
await tealStore.fetchActive(selectedProject.value.id);
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: 'TEAL 항목 로드 실패', life: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function onUpload(payload: { file: File; effectiveDate: string }) {
|
||||
if (!selectedProject.value) return;
|
||||
uploadLoading.value = true;
|
||||
try {
|
||||
await tealStore.upload(selectedProject.value.id, payload.file, payload.effectiveDate);
|
||||
toast.add({ severity: 'success', summary: '성공', detail: 'TEAL 파일이 업로드되었습니다.', life: 3000 });
|
||||
uploadDialogVisible.value = false;
|
||||
await loadEntries();
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: 'TEAL 업로드 실패', life: 5000 });
|
||||
} finally {
|
||||
uploadLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedProject, () => {
|
||||
loadVersions();
|
||||
loadEntries();
|
||||
});
|
||||
|
||||
watch(selectedVersion, () => {
|
||||
if (selectedProject.value) {
|
||||
loadEntries();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(loadProjects);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="teal-list-view">
|
||||
<BasePageHeader title="TEAL 관리">
|
||||
<template #actions>
|
||||
<Button
|
||||
label="TEAL 업로드"
|
||||
icon="pi pi-upload"
|
||||
:disabled="!selectedProject"
|
||||
@click="uploadDialogVisible = true"
|
||||
/>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<!-- Top bar: selectors -->
|
||||
<div class="teal-list-view__toolbar">
|
||||
<div class="teal-list-view__selector">
|
||||
<label class="teal-list-view__label">프로젝트</label>
|
||||
<Select
|
||||
v-model="selectedProject"
|
||||
:options="projects"
|
||||
optionLabel="name"
|
||||
placeholder="프로젝트 선택"
|
||||
fluid
|
||||
style="min-width: 240px"
|
||||
/>
|
||||
</div>
|
||||
<div class="teal-list-view__selector">
|
||||
<label class="teal-list-view__label">버전</label>
|
||||
<TealVersionSelect
|
||||
v-model="selectedVersion"
|
||||
:versions="tealStore.versions"
|
||||
:loading="tealStore.loading"
|
||||
style="min-width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<div class="teal-list-view__selector">
|
||||
<label class="teal-list-view__label">Discipline</label>
|
||||
<Select
|
||||
v-model="disciplineFilter"
|
||||
:options="disciplineOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="전체"
|
||||
showClear
|
||||
fluid
|
||||
style="min-width: 160px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DataTable -->
|
||||
<BaseCrudTable
|
||||
:value="filteredEntries"
|
||||
:loading="tealStore.loading"
|
||||
:globalFilterFields="['activityCode', 'activityName', 'discipline']"
|
||||
emptyMessage="프로젝트를 선택해 주세요."
|
||||
>
|
||||
<Column field="activityCode" header="Activity Code" sortable style="min-width: 160px" />
|
||||
<Column field="activityName" header="Activity Name" sortable style="min-width: 240px" />
|
||||
<Column field="canonicalWbsCode" header="WBS Code" sortable style="min-width: 160px">
|
||||
<template #body="{ data }">
|
||||
{{ (data as any).canonicalWbsCode ?? (data as any).wbsCode ?? '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="discipline" header="Discipline" sortable style="min-width: 120px" />
|
||||
<Column header="상태" style="min-width: 90px">
|
||||
<template #body="{ data }">
|
||||
<Tag
|
||||
:value="(data as any).isActive === false ? '비활성' : '활성'"
|
||||
:severity="(data as any).isActive === false ? 'secondary' : 'success'"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</BaseCrudTable>
|
||||
|
||||
<TealUploadDialog
|
||||
:visible="uploadDialogVisible"
|
||||
:loading="uploadLoading"
|
||||
@update:visible="uploadDialogVisible = $event"
|
||||
@upload="onUpload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/assets/styles/variables' as *;
|
||||
|
||||
.teal-list-view {
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: $space-md;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: $space-lg;
|
||||
}
|
||||
|
||||
&__selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-xs;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: 600;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
@media (max-width: $bp-mobile) {
|
||||
&__toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1 +1,220 @@
|
||||
<template><div class="card"><h1>시수 이력</h1></div></template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Tag from 'primevue/tag';
|
||||
import Button from 'primevue/button';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
import Select from 'primevue/select';
|
||||
import BasePageHeader from '@/core/components/BasePageHeader.vue';
|
||||
import { timesheetService } from '../timesheet.service';
|
||||
import { TIMESHEET_STATUS } from '@/core/constants/app.constants';
|
||||
|
||||
const loading = ref(false);
|
||||
const timesheets = ref<any[]>([]);
|
||||
const totalRecords = ref(0);
|
||||
|
||||
// Filters
|
||||
const dateFrom = ref<Date | null>(null);
|
||||
const dateTo = ref<Date | null>(null);
|
||||
const statusFilter = ref<string | null>(null);
|
||||
|
||||
const statusOptions = Object.entries(TIMESHEET_STATUS).map(([value, { label }]) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
|
||||
const page = ref(0);
|
||||
const rows = ref(20);
|
||||
|
||||
async function fetchHistory() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: Record<string, unknown> = {
|
||||
skip: page.value * rows.value,
|
||||
limit: rows.value,
|
||||
};
|
||||
if (dateFrom.value) params.from = formatDate(dateFrom.value);
|
||||
if (dateTo.value) params.to = formatDate(dateTo.value);
|
||||
if (statusFilter.value) params.status = statusFilter.value;
|
||||
|
||||
const { data } = await timesheetService.getHistory(params);
|
||||
if (data.items) {
|
||||
timesheets.value = data.items;
|
||||
totalRecords.value = data.total;
|
||||
} else if (data.content) {
|
||||
timesheets.value = data.content;
|
||||
totalRecords.value = data.totalElements ?? data.content.length;
|
||||
} else if (Array.isArray(data)) {
|
||||
timesheets.value = data;
|
||||
totalRecords.value = data.length;
|
||||
}
|
||||
} catch {
|
||||
timesheets.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(d: Date): string {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function onPage(event: any) {
|
||||
page.value = event.page;
|
||||
rows.value = event.rows;
|
||||
fetchHistory();
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
page.value = 0;
|
||||
fetchHistory();
|
||||
}
|
||||
|
||||
function clearFilter() {
|
||||
dateFrom.value = null;
|
||||
dateTo.value = null;
|
||||
statusFilter.value = null;
|
||||
page.value = 0;
|
||||
fetchHistory();
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string) {
|
||||
return TIMESHEET_STATUS[status]?.label ?? status;
|
||||
}
|
||||
|
||||
function getStatusSeverity(status: string): string {
|
||||
return (TIMESHEET_STATUS[status]?.severity as string) ?? 'secondary';
|
||||
}
|
||||
|
||||
onMounted(() => fetchHistory());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BasePageHeader title="시수 이력" subtitle="제출 및 승인된 시수 내역을 조회합니다." />
|
||||
|
||||
<div class="card">
|
||||
<!-- Filter -->
|
||||
<div class="history-filter">
|
||||
<div class="history-filter__fields">
|
||||
<div class="history-filter__field">
|
||||
<label class="text-sm">시작일</label>
|
||||
<DatePicker v-model="dateFrom" dateFormat="yy-mm-dd" placeholder="시작일" showIcon fluid />
|
||||
</div>
|
||||
<div class="history-filter__field">
|
||||
<label class="text-sm">종료일</label>
|
||||
<DatePicker v-model="dateTo" dateFormat="yy-mm-dd" placeholder="종료일" showIcon fluid />
|
||||
</div>
|
||||
<div class="history-filter__field">
|
||||
<label class="text-sm">상태</label>
|
||||
<Select
|
||||
v-model="statusFilter"
|
||||
:options="statusOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="전체"
|
||||
showClear
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-filter__actions">
|
||||
<Button label="조회" icon="pi pi-search" size="small" @click="applyFilter" />
|
||||
<Button label="초기화" icon="pi pi-times" size="small" severity="secondary" text @click="clearFilter" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<DataTable
|
||||
:value="timesheets"
|
||||
:loading="loading"
|
||||
:paginator="true"
|
||||
:rows="rows"
|
||||
:totalRecords="totalRecords"
|
||||
:lazy="true"
|
||||
:rowsPerPageOptions="[10, 20, 50]"
|
||||
dataKey="id"
|
||||
stripedRows
|
||||
size="small"
|
||||
@page="onPage"
|
||||
>
|
||||
<template #empty>
|
||||
<div style="text-align: center; padding: 2rem; color: var(--p-text-muted-color);">
|
||||
<i class="pi pi-inbox" style="font-size: 2rem;" />
|
||||
<p>시수 이력이 없습니다.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="weekStartDate" header="주간 시작일" sortable style="width: 130px" />
|
||||
<Column field="weekEndDate" header="주간 종료일" sortable style="width: 130px" />
|
||||
<Column field="totalHours" header="총 시수" sortable style="width: 100px">
|
||||
<template #body="{ data }">
|
||||
<strong>{{ data.totalHours?.toFixed(1) ?? '0.0' }}h</strong>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="상태" sortable style="width: 120px">
|
||||
<template #body="{ data }">
|
||||
<Tag :value="getStatusLabel(data.status)" :severity="getStatusSeverity(data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="submittedAt" header="제출일" style="width: 160px">
|
||||
<template #body="{ data }">
|
||||
{{ data.submittedAt ? data.submittedAt.substring(0, 16).replace('T', ' ') : '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="관리" style="width: 80px">
|
||||
<template #body="{ data }">
|
||||
<router-link :to="`/timesheets?week=${data.weekStartDate}`">
|
||||
<Button icon="pi pi-eye" text rounded size="small" v-tooltip="'상세 보기'" />
|
||||
</router-link>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/assets/styles/variables' as *;
|
||||
|
||||
.history-filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: $space-md;
|
||||
margin-bottom: $space-lg;
|
||||
|
||||
&__fields {
|
||||
display: flex;
|
||||
gap: $space-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-xs;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: $space-sm;
|
||||
}
|
||||
|
||||
@media (max-width: $bp-mobile) {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
&__fields {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import FileUpload from 'primevue/fileupload';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import BaseFormDialog from '@/core/components/BaseFormDialog.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
upload: [payload: { file: File; effectiveDate: string; description: string }];
|
||||
}>();
|
||||
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const effectiveDate = ref<Date | null>(null);
|
||||
const description = ref('');
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (v) {
|
||||
selectedFile.value = null;
|
||||
effectiveDate.value = null;
|
||||
description.value = '';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function onFileSelect(event: any) {
|
||||
const files = event.files ?? event;
|
||||
if (Array.isArray(files) && files.length > 0) {
|
||||
selectedFile.value = files[0];
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
if (!selectedFile.value || !effectiveDate.value) return;
|
||||
emit('upload', {
|
||||
file: selectedFile.value,
|
||||
effectiveDate: formatDate(effectiveDate.value),
|
||||
description: description.value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseFormDialog
|
||||
:visible="visible"
|
||||
title="P6 WBS 업로드"
|
||||
width="580px"
|
||||
:loading="loading"
|
||||
submitLabel="업로드"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<div class="form-grid">
|
||||
<div class="col-12">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">P6 파일</label>
|
||||
<FileUpload
|
||||
mode="basic"
|
||||
accept=".xls,.xlsx,.csv"
|
||||
:maxFileSize="10000000"
|
||||
chooseLabel="파일 선택"
|
||||
:auto="false"
|
||||
@select="onFileSelect"
|
||||
/>
|
||||
<small v-if="selectedFile" class="form-field__hint">
|
||||
{{ selectedFile.name }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label form-field__label--required">적용일</label>
|
||||
<DatePicker
|
||||
v-model="effectiveDate"
|
||||
dateFormat="yy-mm-dd"
|
||||
placeholder="적용일 선택"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-field">
|
||||
<label class="form-field__label">설명</label>
|
||||
<Textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
placeholder="버전 설명을 입력하세요"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseFormDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select';
|
||||
import Tag from 'primevue/tag';
|
||||
import type { WbsVersion } from '../wbs.types';
|
||||
|
||||
defineProps<{
|
||||
modelValue: WbsVersion | null;
|
||||
versions: WbsVersion[];
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: WbsVersion | null];
|
||||
}>();
|
||||
|
||||
function statusSeverity(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
DRAFT: 'warn',
|
||||
ACTIVE: 'success',
|
||||
ARCHIVED: 'secondary',
|
||||
};
|
||||
return map[status] ?? 'secondary';
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
DRAFT: '초안',
|
||||
ACTIVE: '활성',
|
||||
ARCHIVED: '보관',
|
||||
};
|
||||
return map[status] ?? status;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
:modelValue="modelValue"
|
||||
:options="versions"
|
||||
:loading="loading"
|
||||
optionLabel="versionNumber"
|
||||
placeholder="버전 선택"
|
||||
fluid
|
||||
@update:modelValue="emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #value="{ value }">
|
||||
<div v-if="value" class="wbs-version-option">
|
||||
<span>v{{ value.versionNumber }}</span>
|
||||
<Tag
|
||||
:value="statusLabel(value.status)"
|
||||
:severity="statusSeverity(value.status)"
|
||||
style="margin-left: 8px"
|
||||
/>
|
||||
</div>
|
||||
<span v-else>버전 선택</span>
|
||||
</template>
|
||||
<template #option="{ option }">
|
||||
<div class="wbs-version-option">
|
||||
<span>v{{ option.versionNumber }} ({{ option.effectiveDate }})</span>
|
||||
<Tag
|
||||
:value="statusLabel(option.status)"
|
||||
:severity="statusSeverity(option.status)"
|
||||
style="margin-left: 8px"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wbs-version-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1 +1,297 @@
|
||||
<template><div class="card"><h1>WBS 관리</h1></div></template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import TreeTable from 'primevue/treetable';
|
||||
import Column from 'primevue/column';
|
||||
import Select from 'primevue/select';
|
||||
import Button from 'primevue/button';
|
||||
import Tag from 'primevue/tag';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import BasePageHeader from '@/core/components/BasePageHeader.vue';
|
||||
import WbsUploadDialog from '../components/WbsUploadDialog.vue';
|
||||
import WbsVersionSelect from '../components/WbsVersionSelect.vue';
|
||||
import { useWbsStore } from '../wbs.store';
|
||||
import { projectService } from '@/modules/project/project.service';
|
||||
import type { Project } from '@/modules/project/project.types';
|
||||
import type { WbsVersion, WbsNode } from '../wbs.types';
|
||||
|
||||
const toast = useToast();
|
||||
const wbsStore = useWbsStore();
|
||||
|
||||
const projects = ref<Project[]>([]);
|
||||
const selectedProject = ref<Project | null>(null);
|
||||
const selectedVersion = ref<WbsVersion | null>(null);
|
||||
const uploadDialogVisible = ref(false);
|
||||
const uploadLoading = ref(false);
|
||||
|
||||
// Build tree nodes for PrimeVue TreeTable
|
||||
interface TreeNode {
|
||||
key: string;
|
||||
data: WbsNode;
|
||||
children: TreeNode[];
|
||||
}
|
||||
|
||||
const treeNodes = computed<TreeNode[]>(() => {
|
||||
const nodes = wbsStore.nodes;
|
||||
if (!nodes || nodes.length === 0) return [];
|
||||
|
||||
const nodeMap = new Map<string, TreeNode>();
|
||||
const roots: TreeNode[] = [];
|
||||
|
||||
// Create tree node objects
|
||||
for (const node of nodes) {
|
||||
nodeMap.set(node.wbsCode, {
|
||||
key: node.wbsCode,
|
||||
data: node,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Build hierarchy
|
||||
for (const node of nodes) {
|
||||
const treeNode = nodeMap.get(node.wbsCode)!;
|
||||
const parentCode = deriveParentCode(node.wbsCode, node.level);
|
||||
if (parentCode && nodeMap.has(parentCode)) {
|
||||
nodeMap.get(parentCode)!.children.push(treeNode);
|
||||
} else {
|
||||
roots.push(treeNode);
|
||||
}
|
||||
}
|
||||
|
||||
return roots;
|
||||
});
|
||||
|
||||
function deriveParentCode(wbsCode: string, level: number): string | null {
|
||||
if (level <= 1) return null;
|
||||
const lastDot = wbsCode.lastIndexOf('.');
|
||||
return lastDot > 0 ? wbsCode.substring(0, lastDot) : null;
|
||||
}
|
||||
|
||||
const canActivate = computed(() => {
|
||||
return selectedVersion.value?.status === 'DRAFT';
|
||||
});
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const { data } = await projectService.getAll();
|
||||
projects.value = (data as any).items ?? data;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: '프로젝트 목록 로드 실패', life: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
if (!selectedProject.value) return;
|
||||
try {
|
||||
await wbsStore.fetchVersions(selectedProject.value.id);
|
||||
selectedVersion.value = null;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: 'WBS 버전 목록 로드 실패', life: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNodes() {
|
||||
if (!selectedProject.value || !selectedVersion.value) return;
|
||||
try {
|
||||
await wbsStore.fetchNodes(selectedProject.value.id, selectedVersion.value.versionNumber);
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: 'WBS 노드 로드 실패', life: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function onActivate() {
|
||||
if (!selectedProject.value || !selectedVersion.value) return;
|
||||
try {
|
||||
await wbsStore.activateVersion(selectedProject.value.id, selectedVersion.value.versionNumber);
|
||||
toast.add({ severity: 'success', summary: '성공', detail: 'WBS 버전이 활성화되었습니다.', life: 3000 });
|
||||
selectedVersion.value = null;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: '버전 활성화 실패', life: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function onUpload(payload: { file: File; effectiveDate: string; description: string }) {
|
||||
if (!selectedProject.value) return;
|
||||
uploadLoading.value = true;
|
||||
try {
|
||||
await wbsStore.uploadWbs(selectedProject.value.id, payload.file, payload.effectiveDate);
|
||||
toast.add({ severity: 'success', summary: '성공', detail: 'P6 WBS 파일이 업로드되었습니다.', life: 3000 });
|
||||
uploadDialogVisible.value = false;
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: '오류', detail: 'WBS 업로드 실패', life: 5000 });
|
||||
} finally {
|
||||
uploadLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function statusSeverity(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
DRAFT: 'warn',
|
||||
ACTIVE: 'success',
|
||||
ARCHIVED: 'secondary',
|
||||
};
|
||||
return map[status] ?? 'secondary';
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
DRAFT: '초안',
|
||||
ACTIVE: '활성',
|
||||
ARCHIVED: '보관',
|
||||
};
|
||||
return map[status] ?? status;
|
||||
}
|
||||
|
||||
watch(selectedProject, () => {
|
||||
loadVersions();
|
||||
wbsStore.nodes = [];
|
||||
});
|
||||
|
||||
watch(selectedVersion, () => {
|
||||
if (selectedVersion.value) {
|
||||
loadNodes();
|
||||
} else {
|
||||
wbsStore.nodes = [];
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(loadProjects);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wbs-tree-view">
|
||||
<BasePageHeader title="WBS 관리">
|
||||
<template #actions>
|
||||
<Button
|
||||
label="P6 업로드"
|
||||
icon="pi pi-upload"
|
||||
:disabled="!selectedProject"
|
||||
@click="uploadDialogVisible = true"
|
||||
/>
|
||||
</template>
|
||||
</BasePageHeader>
|
||||
|
||||
<!-- Top bar: selectors -->
|
||||
<div class="wbs-tree-view__toolbar">
|
||||
<div class="wbs-tree-view__selector">
|
||||
<label class="wbs-tree-view__label">프로젝트</label>
|
||||
<Select
|
||||
v-model="selectedProject"
|
||||
:options="projects"
|
||||
optionLabel="name"
|
||||
placeholder="프로젝트 선택"
|
||||
fluid
|
||||
style="min-width: 240px"
|
||||
/>
|
||||
</div>
|
||||
<div class="wbs-tree-view__selector">
|
||||
<label class="wbs-tree-view__label">버전</label>
|
||||
<WbsVersionSelect
|
||||
v-model="selectedVersion"
|
||||
:versions="wbsStore.versions"
|
||||
:loading="wbsStore.loading"
|
||||
style="min-width: 200px"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="canActivate"
|
||||
label="활성화"
|
||||
icon="pi pi-check"
|
||||
severity="success"
|
||||
@click="onActivate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TreeTable -->
|
||||
<div class="wbs-tree-view__content">
|
||||
<TreeTable
|
||||
:value="treeNodes"
|
||||
:loading="wbsStore.loading"
|
||||
removableSort
|
||||
stripedRows
|
||||
showGridlines
|
||||
responsiveLayout="scroll"
|
||||
size="small"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="wbs-tree-view__empty">
|
||||
<i class="pi pi-inbox" style="font-size: 2rem" />
|
||||
<p>프로젝트와 버전을 선택해 주세요.</p>
|
||||
</div>
|
||||
</template>
|
||||
<Column field="wbsCode" header="WBS Code" sortable style="min-width: 200px" expander />
|
||||
<Column field="name" header="이름" sortable style="min-width: 200px" />
|
||||
<Column field="level" header="Level" sortable style="min-width: 80px" />
|
||||
<Column field="discipline" header="Discipline" sortable style="min-width: 120px" />
|
||||
<Column field="plannedHours" header="계획시수" sortable style="min-width: 100px">
|
||||
<template #body="{ node }">
|
||||
{{ node.data.plannedHours != null ? node.data.plannedHours.toLocaleString() : '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="상태" style="min-width: 80px">
|
||||
<template #body>
|
||||
<Tag
|
||||
v-if="selectedVersion"
|
||||
:value="statusLabel(selectedVersion.status)"
|
||||
:severity="statusSeverity(selectedVersion.status)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</TreeTable>
|
||||
</div>
|
||||
|
||||
<WbsUploadDialog
|
||||
:visible="uploadDialogVisible"
|
||||
:loading="uploadLoading"
|
||||
@update:visible="uploadDialogVisible = $event"
|
||||
@upload="onUpload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use '@/assets/styles/variables' as *;
|
||||
|
||||
.wbs-tree-view {
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: $space-md;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: $space-lg;
|
||||
}
|
||||
|
||||
&__selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-xs;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: 600;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
&__content {
|
||||
background: var(--surface-card);
|
||||
border-radius: $radius-md;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $space-sm;
|
||||
padding: $space-2xl;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
@media (max-width: $bp-mobile) {
|
||||
&__toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,64 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { wbsService } from './wbs.service';
|
||||
import type { WbsVersion, WbsNode } from './wbs.types';
|
||||
|
||||
export const useWbsStore = defineStore('wbs', () => {
|
||||
const loading = ref(false);
|
||||
return { loading };
|
||||
const versions = ref<WbsVersion[]>([]);
|
||||
const nodes = ref<WbsNode[]>([]);
|
||||
const selectedProjectId = ref<number | null>(null);
|
||||
const selectedVersion = ref<WbsVersion | null>(null);
|
||||
|
||||
async function fetchVersions(projectId: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await wbsService.getVersions(projectId);
|
||||
versions.value = (data as any).items ?? data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchNodes(projectId: number, versionNumber: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await wbsService.getVersion(projectId, versionNumber);
|
||||
nodes.value = (data as any).nodes ?? (data as any).items ?? data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadWbs(projectId: number, file: File, effectiveDate: string) {
|
||||
loading.value = true;
|
||||
try {
|
||||
await wbsService.uploadP6(projectId, file, effectiveDate);
|
||||
await fetchVersions(projectId);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function activateVersion(projectId: number, versionNumber: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
await wbsService.activateVersion(projectId, versionNumber);
|
||||
await fetchVersions(projectId);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
versions,
|
||||
nodes,
|
||||
selectedProjectId,
|
||||
selectedVersion,
|
||||
fetchVersions,
|
||||
fetchNodes,
|
||||
uploadWbs,
|
||||
activateVersion,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:8081',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
새 Issue에서 참조
사용자 차단