feat: 개인 PC 수정 이력(Log) 시스템 구현 및 UI 개선
This commit is contained in:
16
index.html
16
index.html
@@ -172,12 +172,14 @@
|
|||||||
|
|
||||||
<!-- PC Asset Modal -->
|
<!-- PC Asset Modal -->
|
||||||
<div id="pc-asset-modal" class="modal-overlay hidden">
|
<div id="pc-asset-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-content">
|
<div class="modal-content wide">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="pc-modal-title">개인PC 상세 정보</h2>
|
<h2 id="pc-modal-title">개인PC 상세 정보</h2>
|
||||||
<button id="btn-close-pc-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
<button id="btn-close-pc-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<div class="modal-body-split">
|
||||||
|
<div class="modal-form-area">
|
||||||
<form id="pc-asset-form" class="grid-form">
|
<form id="pc-asset-form" class="grid-form">
|
||||||
<input type="hidden" id="pc-asset-id" />
|
<input type="hidden" id="pc-asset-id" />
|
||||||
<input type="hidden" id="pc-asset-type" value="개인PC" />
|
<input type="hidden" id="pc-asset-type" value="개인PC" />
|
||||||
@@ -248,7 +250,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="pc-금액">금액</label>
|
<label for="pc-금액">금액</label>
|
||||||
<input type="text" id="pc-금액" placeholder="ex) 1,000,000" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
<input type="text" id="pc-금액" placeholder="ex) 1,000,000" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -265,6 +267,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-history-area">
|
||||||
|
<div class="history-header">
|
||||||
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 수정 이력</h3>
|
||||||
|
</div>
|
||||||
|
<div id="pc-history-list" class="history-timeline">
|
||||||
|
<div class="empty-history">이력이 없습니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button id="btn-delete-pc-asset" class="btn btn-outline btn-danger">삭제</button>
|
<button id="btn-delete-pc-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { state } from '../../state';
|
import { state } from '../../state';
|
||||||
import { HardwareAsset } from '../../excelHandler';
|
import { HardwareAsset, HardwareLog } from '../../excelHandler';
|
||||||
import { openModal } from './BaseModal';
|
import { openModal } from './BaseModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 개인PC 모달 초기화 및 로직 제어
|
* 개인PC 모달 초기화 및 로직 제어
|
||||||
*/
|
*/
|
||||||
export function initPCModal(renderContent: () => void, closeModals: () => void) {
|
export function initPCModal(renderContent: () => void, closeModals: () => void) {
|
||||||
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
|
|
||||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||||
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
|
const btnSavePc = document.getElementById('btn-save-pc-asset') as HTMLButtonElement;
|
||||||
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
|
const btnDeletePc = document.getElementById('btn-delete-pc-asset') as HTMLButtonElement;
|
||||||
@@ -48,7 +47,27 @@ export function initPCModal(renderContent: () => void, closeModals: () => void)
|
|||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
const idx = state.masterData.hw.findIndex(a => a.id === id);
|
const idx = state.masterData.hw.findIndex(a => a.id === id);
|
||||||
if(idx !== -1) state.masterData.hw[idx] = newAsset;
|
if(idx !== -1) {
|
||||||
|
const oldAsset = state.masterData.hw[idx];
|
||||||
|
const changes = getChangeDetails(oldAsset, newAsset);
|
||||||
|
|
||||||
|
if (changes) {
|
||||||
|
// 로그인 기능이 없으므로 '관리자'가 로그인한 것으로 가정
|
||||||
|
const modifier = '관리자';
|
||||||
|
|
||||||
|
const log: HardwareLog = {
|
||||||
|
id: Math.random().toString(36).substring(2, 9),
|
||||||
|
assetId: id,
|
||||||
|
date: new Date().toLocaleString(),
|
||||||
|
details: changes,
|
||||||
|
user: modifier
|
||||||
|
};
|
||||||
|
|
||||||
|
state.masterData.logs.push(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.masterData.hw[idx] = newAsset;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state.masterData.hw.push(newAsset);
|
state.masterData.hw.push(newAsset);
|
||||||
}
|
}
|
||||||
@@ -63,6 +82,7 @@ export function initPCModal(renderContent: () => void, closeModals: () => void)
|
|||||||
const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value;
|
const id = (document.getElementById('pc-asset-id') as HTMLInputElement).value;
|
||||||
if (confirm('삭제하시겠습니까?')) {
|
if (confirm('삭제하시겠습니까?')) {
|
||||||
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
|
state.masterData.hw = state.masterData.hw.filter(a => a.id !== id);
|
||||||
|
// 관련 로그도 삭제할지 여부는 정책에 따라 (여기서는 유지)
|
||||||
closeModals();
|
closeModals();
|
||||||
renderContent();
|
renderContent();
|
||||||
}
|
}
|
||||||
@@ -71,12 +91,11 @@ export function initPCModal(renderContent: () => void, closeModals: () => void)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 개인PC 상세 모달 열기
|
* 개인PC 상세 모달 열기
|
||||||
* @param asset 수정 시 자산 데이터, 신규 시 undefined
|
|
||||||
*/
|
*/
|
||||||
export function openPcModal(asset?: HardwareAsset) {
|
export function openPcModal(asset?: HardwareAsset) {
|
||||||
const pcModal = document.getElementById('pc-asset-modal') as HTMLDivElement;
|
|
||||||
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
const pcForm = document.getElementById('pc-asset-form') as HTMLFormElement;
|
||||||
const deleteBtn = document.getElementById('btn-delete-pc-asset')!;
|
const deleteBtn = document.getElementById('btn-delete-pc-asset')!;
|
||||||
|
const historyArea = document.querySelector('.modal-history-area') as HTMLElement;
|
||||||
|
|
||||||
openModal('pc-asset-modal');
|
openModal('pc-asset-modal');
|
||||||
pcForm.reset();
|
pcForm.reset();
|
||||||
@@ -84,6 +103,7 @@ export function openPcModal(asset?: HardwareAsset) {
|
|||||||
if (asset) {
|
if (asset) {
|
||||||
document.getElementById('pc-modal-title')!.textContent = '개인PC 상세 정보 수정';
|
document.getElementById('pc-modal-title')!.textContent = '개인PC 상세 정보 수정';
|
||||||
deleteBtn.style.display = 'block';
|
deleteBtn.style.display = 'block';
|
||||||
|
if (historyArea) historyArea.style.display = 'flex';
|
||||||
|
|
||||||
(document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id;
|
(document.getElementById('pc-asset-id') as HTMLInputElement).value = asset.id;
|
||||||
(document.getElementById('pc-법인') as HTMLSelectElement).value = asset.법인;
|
(document.getElementById('pc-법인') as HTMLSelectElement).value = asset.법인;
|
||||||
@@ -98,14 +118,78 @@ export function openPcModal(asset?: HardwareAsset) {
|
|||||||
(document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || '';
|
(document.getElementById('pc-HDD1') as HTMLInputElement).value = asset.HDD1 || '';
|
||||||
(document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || '';
|
(document.getElementById('pc-HDD2') as HTMLInputElement).value = asset.HDD2 || '';
|
||||||
(document.getElementById('pc-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
(document.getElementById('pc-구매일') as HTMLInputElement).value = asset.구매일 || '';
|
||||||
(document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 ? Number(asset.금액.replace(/,/g, '')).toLocaleString() : '';
|
(document.getElementById('pc-금액') as HTMLInputElement).value = asset.금액 ? asset.금액.replace(/,/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ',') : '';
|
||||||
(document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
(document.getElementById('pc-납품업체') as HTMLInputElement).value = asset.납품업체 || '';
|
||||||
(document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : '';
|
(document.getElementById('pc-품의서명') as HTMLElement).innerText = asset.품의서명 ? `📎${asset.품의서명}` : '';
|
||||||
|
|
||||||
|
renderHistory(asset.id);
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('pc-modal-title')!.textContent = '새 개인PC 자산 추가';
|
document.getElementById('pc-modal-title')!.textContent = '새 개인PC 자산 추가';
|
||||||
deleteBtn.style.display = 'none';
|
deleteBtn.style.display = 'none';
|
||||||
|
if (historyArea) historyArea.style.display = 'none'; // 신규 시 이력 숨김
|
||||||
|
|
||||||
(document.getElementById('pc-asset-id') as HTMLInputElement).value = '';
|
(document.getElementById('pc-asset-id') as HTMLInputElement).value = '';
|
||||||
(document.getElementById('pc-법인') as HTMLSelectElement).value = '한맥';
|
(document.getElementById('pc-법인') as HTMLSelectElement).value = '한맥';
|
||||||
(document.getElementById('pc-품의서명') as HTMLElement).innerText = '';
|
(document.getElementById('pc-품의서명') as HTMLElement).innerText = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 변경 사항 감지 및 문자열 생성
|
||||||
|
*/
|
||||||
|
function getChangeDetails(oldAsset: HardwareAsset, newAsset: HardwareAsset): string {
|
||||||
|
const changes: string[] = [];
|
||||||
|
const fields = [
|
||||||
|
{ key: '법인', label: '법인' },
|
||||||
|
{ key: '자산코드', label: '자산코드' },
|
||||||
|
{ key: '사용자', label: '사용자' },
|
||||||
|
{ key: '위치', label: '위치' },
|
||||||
|
{ key: 'CPU', label: 'CPU' },
|
||||||
|
{ key: 'GPU', label: 'GPU' },
|
||||||
|
{ key: 'RAM', label: 'RAM' },
|
||||||
|
{ key: 'SSD1', label: 'SSD1' },
|
||||||
|
{ key: 'SSD2', label: 'SSD2' },
|
||||||
|
{ key: 'HDD1', label: 'HDD1' },
|
||||||
|
{ key: 'HDD2', label: 'HDD2' },
|
||||||
|
{ key: '구매일', label: '구매일' },
|
||||||
|
{ key: '금액', label: '금액' },
|
||||||
|
{ key: '납품업체', label: '납품업체' },
|
||||||
|
{ key: '품의서명', label: '품의서' },
|
||||||
|
];
|
||||||
|
|
||||||
|
fields.forEach(field => {
|
||||||
|
const oldVal = (oldAsset as any)[field.key] || '';
|
||||||
|
const newVal = (newAsset as any)[field.key] || '';
|
||||||
|
|
||||||
|
if (oldVal !== newVal) {
|
||||||
|
changes.push(`${field.label}: ${oldVal || '없음'} → ${newVal || '없음'}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return changes.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이력 리스트 렌더링
|
||||||
|
*/
|
||||||
|
function renderHistory(assetId: string) {
|
||||||
|
const historyList = document.getElementById('pc-history-list');
|
||||||
|
if (!historyList) return;
|
||||||
|
|
||||||
|
const logs = state.masterData.logs
|
||||||
|
.filter(l => l.assetId === assetId)
|
||||||
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||||
|
|
||||||
|
if (logs.length === 0) {
|
||||||
|
historyList.innerHTML = '<div class="empty-history">이력이 없습니다.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
historyList.innerHTML = logs.map(log => `
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-date">${log.date}</div>
|
||||||
|
<div class="history-user">수정자: ${log.user}</div>
|
||||||
|
<div class="history-details">${log.details.replace(/\n/g, '<br>')}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,10 +61,19 @@ export interface SWUser {
|
|||||||
신청서명: string;
|
신청서명: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HardwareLog {
|
||||||
|
id: string;
|
||||||
|
assetId: string;
|
||||||
|
date: string;
|
||||||
|
details: string;
|
||||||
|
user: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MasterAssetData {
|
export interface MasterAssetData {
|
||||||
hw: HardwareAsset[];
|
hw: HardwareAsset[];
|
||||||
sw: SoftwareAsset[];
|
sw: SoftwareAsset[];
|
||||||
swUsers: SWUser[];
|
swUsers: SWUser[];
|
||||||
|
logs: HardwareLog[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
|
const HW_TABS = ['개인PC', '서버', '스토리지', '전산비품'];
|
||||||
@@ -76,6 +85,7 @@ const STORAGE_HEADERS = ['법인', '유형', '자산코드', '명칭', '위치',
|
|||||||
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
const SUB_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '구독일', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||||
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
const PERM_SW_HEADERS = ['ID', '분야', '법인', '부서', '제품명', '구매일', '유지보수여부', '금액', '수량', '계정명', '납품업체', '비고'];
|
||||||
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
|
const SW_USER_HEADERS = ['id', 'swId', '법인', '부서', '팀', '직위', '이름', '사용기간', '신청서명'];
|
||||||
|
const HISTORY_HEADERS = ['id', 'assetId', 'date', 'details', 'user'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 템플릿 엑셀 다중 시트로 다운로드
|
* 템플릿 엑셀 다중 시트로 다운로드
|
||||||
@@ -112,6 +122,11 @@ export function downloadTemplate() {
|
|||||||
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||||
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
||||||
|
|
||||||
|
// History 시트 (템플릿에도 포함)
|
||||||
|
const historyWs = XLSX.utils.aoa_to_sheet([HISTORY_HEADERS]);
|
||||||
|
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
|
||||||
|
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
|
||||||
|
|
||||||
XLSX.writeFile(wb, 'itam_assets_template.xlsx');
|
XLSX.writeFile(wb, 'itam_assets_template.xlsx');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +196,15 @@ export function exportToExcel(masterData: MasterAssetData) {
|
|||||||
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
swUserWs['!cols'] = [{wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:15}, {wch:20}, {wch:25}];
|
||||||
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
XLSX.utils.book_append_sheet(wb, swUserWs, 'SW_사용자');
|
||||||
|
|
||||||
|
// History
|
||||||
|
const historyWsData = [
|
||||||
|
HISTORY_HEADERS,
|
||||||
|
...masterData.logs.map(l => [l.id, l.assetId, l.date, l.details, l.user])
|
||||||
|
];
|
||||||
|
const historyWs = XLSX.utils.aoa_to_sheet(historyWsData);
|
||||||
|
historyWs['!cols'] = [{wch:15}, {wch:20}, {wch:20}, {wch:50}, {wch:15}];
|
||||||
|
XLSX.utils.book_append_sheet(wb, historyWs, 'History');
|
||||||
|
|
||||||
const dateStr = new Date().toISOString().split('T')[0];
|
const dateStr = new Date().toISOString().split('T')[0];
|
||||||
XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`);
|
XLSX.writeFile(wb, `itam_assets_master_${dateStr}.xlsx`);
|
||||||
}
|
}
|
||||||
@@ -200,6 +224,7 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
const hwAssets: HardwareAsset[] = [];
|
const hwAssets: HardwareAsset[] = [];
|
||||||
const swAssets: SoftwareAsset[] = [];
|
const swAssets: SoftwareAsset[] = [];
|
||||||
const swUsers: SWUser[] = [];
|
const swUsers: SWUser[] = [];
|
||||||
|
const logs: HardwareLog[] = [];
|
||||||
|
|
||||||
workbook.SheetNames.forEach(sheetName => {
|
workbook.SheetNames.forEach(sheetName => {
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
@@ -314,9 +339,21 @@ export async function parseExcel(file: File): Promise<MasterAssetData> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sheetName === 'History') {
|
||||||
|
json.forEach(row => {
|
||||||
|
logs.push({
|
||||||
|
id: row['id'] ? String(row['id']) : Math.random().toString(36).substring(2, 9),
|
||||||
|
assetId: row['assetId'] ? String(row['assetId']) : '',
|
||||||
|
date: row['date'] || '',
|
||||||
|
details: row['details'] || '',
|
||||||
|
user: row['user'] || '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
resolve({ hw: hwAssets, sw: swAssets, swUsers });
|
resolve({ hw: hwAssets, sw: swAssets, swUsers, logs });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 } from 'lucide';
|
import { createIcons, Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History } from 'lucide';
|
||||||
import { downloadTemplate, exportToExcel, parseExcel } from './excelHandler';
|
import { downloadTemplate, exportToExcel, parseExcel } from './excelHandler';
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import { initSidebar } from './components/Sidebar';
|
import { initSidebar } from './components/Sidebar';
|
||||||
@@ -22,7 +22,7 @@ const btnExport = document.getElementById('btn-export-excel') as HTMLButtonEleme
|
|||||||
|
|
||||||
// Initialize Icons
|
// Initialize Icons
|
||||||
createIcons({
|
createIcons({
|
||||||
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2 }
|
icons: { Download, Upload, FileSpreadsheet, Plus, X, LayoutDashboard, Monitor, Server, Database, Laptop, CalendarClock, Key, Cpu, Layers, Users, Paperclip, Edit2, History }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Components
|
// Initialize Components
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export interface AppState {
|
|||||||
|
|
||||||
// --- Initial State ---
|
// --- Initial State ---
|
||||||
export const state: AppState = {
|
export const state: AppState = {
|
||||||
masterData: generateDummyData(),
|
masterData: {
|
||||||
|
...generateDummyData(),
|
||||||
|
logs: [] // 초기 로그 배열 추가
|
||||||
|
},
|
||||||
activeCategory: 'hw',
|
activeCategory: 'hw',
|
||||||
activeSubTab: '대시보드',
|
activeSubTab: '대시보드',
|
||||||
activeCharts: []
|
activeCharts: []
|
||||||
|
|||||||
@@ -63,3 +63,99 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-actions { display: flex; gap: 0.5rem; }
|
.footer-actions { display: flex; gap: 0.5rem; }
|
||||||
|
|
||||||
|
/* Wide Modal for History */
|
||||||
|
.modal-content.wide {
|
||||||
|
max-width: 950px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body-split {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
min-height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-form-area {
|
||||||
|
flex: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-history-area {
|
||||||
|
flex: 0.8;
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-header h3 {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-timeline {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 500px;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -7px;
|
||||||
|
top: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--white);
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-item:last-child {
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-user {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-details {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-history {
|
||||||
|
padding: 2rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user