- 자료실 하위 3개 메뉴: 자료보관함, 매뉴얼, 공지사항 - 자료보관함: 폴더 트리 + 파일 업로드/다운로드/삭제 - 매뉴얼/공지사항: 게시판형 CRUD + 첨부파일 - 안전관리: 안전보건교육, TBM현황, 위험성평가, 재해예방조치 - 품질관리: 시정조치 UI 페이지 - 대시보드: 슈퍼관리자 전용 레거시 사이트 참고 카드 - 작업일보/출면일보 오류 수정 및 기능 개선 - 설비 사진 업로드, 근로계약서 종료일 수정
209 lines
13 KiB
PHP
209 lines
13 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', '위험성 평가 - 건설PMIS')
|
|
|
|
@section('content')
|
|
<div id="root"></div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" rel="stylesheet" />
|
|
@include('partials.react-cdn')
|
|
<script type="text/babel">
|
|
@verbatim
|
|
const { useState, useMemo } = React;
|
|
|
|
/* ═══════ 사이드바 ═══════ */
|
|
const PMIS_MENUS=[
|
|
{icon:'ri-building-2-line',label:'BIM 관리',id:'bim',children:[{label:'BIM 뷰어',id:'bim-viewer',url:'/juil/construction-pmis/bim-viewer'}]},
|
|
{icon:'ri-line-chart-line',label:'시공관리',id:'construction',children:[
|
|
{label:'인원관리',id:'workforce',url:'/juil/construction-pmis/workforce'},
|
|
{label:'장비관리',id:'equipment',url:'/juil/construction-pmis/equipment'},
|
|
{label:'자재관리',id:'materials',url:'/juil/construction-pmis/materials'},
|
|
{label:'공사량관리',id:'work-volume',url:'/juil/construction-pmis/work-volume'},
|
|
{label:'출면일보',id:'daily-attendance',url:'/juil/construction-pmis/daily-attendance'},
|
|
{label:'작업일보',id:'daily-report',url:'/juil/construction-pmis/daily-report'},
|
|
]},
|
|
{icon:'ri-file-list-3-line',label:'품질관리',id:'quality',children:[
|
|
{label:'시정조치',id:'corrective-action',url:'/juil/construction-pmis/corrective-action'},
|
|
]},
|
|
{icon:'ri-shield-check-line',label:'안전관리',id:'safety',children:[
|
|
{label:'안전보건교육',id:'safety-education',url:'/juil/construction-pmis/safety-education'},
|
|
{label:'TBM현장',id:'tbm',url:'/juil/construction-pmis/tbm'},
|
|
{label:'위험성 평가',id:'risk-assessment',url:'/juil/construction-pmis/risk-assessment'},
|
|
{label:'재해예방조치',id:'disaster-prevention',url:'/juil/construction-pmis/disaster-prevention'},
|
|
]},
|
|
{icon:'ri-folder-line',label:'자료실',id:'archive',children:[
|
|
{label:'자료보관함',id:'archive-files',url:'/juil/construction-pmis/archive-files'},
|
|
{label:'매뉴얼',id:'archive-manual',url:'/juil/construction-pmis/archive-manual'},
|
|
{label:'공지사항',id:'archive-notice',url:'/juil/construction-pmis/archive-notice'},
|
|
]},
|
|
];
|
|
|
|
function PmisSidebar({activePage}){
|
|
const[profile,setProfile]=useState(null);
|
|
const[expanded,setExpanded]=useState(()=>{for(const m of PMIS_MENUS){if(m.children?.some(c=>c.id===activePage))return m.id}return null});
|
|
React.useEffect(()=>{fetch('/juil/construction-pmis/profile',{headers:{Accept:'application/json'}}).then(r=>r.json()).then(d=>setProfile(d.worker)).catch(()=>{})},[]);
|
|
return(
|
|
<div className="bg-white border-r border-gray-200 shadow-sm flex flex-col shrink-0" style={{width:200}}>
|
|
<a href="/juil/construction-pmis" className="flex items-center gap-2 px-4 py-3 text-sm text-blue-600 hover:bg-blue-50 border-b border-gray-100 transition"><i className="ri-arrow-left-s-line text-lg"></i> PMIS 대시보드</a>
|
|
<div className="p-3 border-b border-gray-100 text-center">
|
|
<div className="w-12 h-12 mx-auto mb-1 rounded-full bg-gray-100 border-2 border-gray-200 flex items-center justify-center">
|
|
{profile?.profile_photo_path?<img src={profile.profile_photo_path} className="w-full h-full rounded-full object-cover"/>:<i className="ri-user-3-line text-xl text-gray-300"></i>}
|
|
</div>
|
|
<div className="text-sm font-bold text-gray-800">{profile?.name||'...'}</div>
|
|
<div className="text-xs text-gray-500 mt-0.5">{profile?.department||''}</div>
|
|
</div>
|
|
<div className="flex-1 overflow-auto py-1">
|
|
{PMIS_MENUS.map(m=>(
|
|
<div key={m.id}>
|
|
<div onClick={()=>setExpanded(expanded===m.id?null:m.id)} className={`flex items-center gap-2 px-4 py-2.5 text-sm cursor-pointer transition ${expanded===m.id?'bg-blue-50 text-blue-700 font-semibold':'text-gray-600 hover:bg-gray-50'}`}>
|
|
<i className={`${m.icon} text-base`}></i>{m.label}
|
|
{m.children&&<i className={`ml-auto ${expanded===m.id?'ri-arrow-down-s-line':'ri-arrow-right-s-line'} text-gray-400 text-xs`}></i>}
|
|
</div>
|
|
{expanded===m.id&&m.children?.map(c=>(<a key={c.id} href={c.url} className={`block pl-10 pr-4 py-2 text-sm transition ${c.id===activePage?'bg-blue-100 text-blue-800 font-semibold border-l-2 border-blue-600':'text-gray-500 hover:text-blue-600 hover:bg-gray-50'}`}>{c.label}</a>))}
|
|
</div>))}
|
|
</div>
|
|
</div>);
|
|
}
|
|
|
|
/* ═══════ 선택 옵션 ═══════ */
|
|
const COMPANIES = [
|
|
'(주)시아산업개발 - 가설울타리설치해체공사',
|
|
'(주)신한에스엔지 - 데크플레이트공사',
|
|
'(주)신화씨이엔씨 - 연약지반보강공사',
|
|
'(주)에이씨알텍 - 방열공사',
|
|
'(주)주일기업 - 방화셔터공사',
|
|
];
|
|
const ROUND_STATUS_OPTIONS = ['차수 진행 중','차수 완료'];
|
|
const PROGRESS_OPTIONS = ['검토요청','작성중','검토중(확인자)','검토중'];
|
|
|
|
/* ═══════ 메인 ═══════ */
|
|
function App(){
|
|
const [roundFilter, setRoundFilter] = useState('');
|
|
const [roundStatus, setRoundStatus] = useState('');
|
|
const [progressFilter, setProgressFilter] = useState('');
|
|
const [companyFilter, setCompanyFilter] = useState('');
|
|
const [checked, setChecked] = useState({});
|
|
|
|
/* 샘플 데이터 (빈 상태 — API 연동 후 채워짐) */
|
|
const data = [];
|
|
|
|
const allChecked = data.length > 0 && data.every(d => checked[d.id]);
|
|
|
|
const thCls = 'px-3 py-2.5 text-center font-semibold text-gray-600 text-sm whitespace-nowrap';
|
|
|
|
return(
|
|
<div className="flex bg-gray-100" style={{height:'calc(100vh - 56px)'}}>
|
|
<PmisSidebar activePage="risk-assessment"/>
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* 헤더 */}
|
|
<div className="bg-white border-b border-gray-200 px-6 py-4">
|
|
<div className="flex items-center gap-2 text-xs text-gray-400 mb-2">
|
|
<i className="ri-home-4-line"></i><span>Home</span> > <span>안전관리</span> > <span className="text-gray-600">위험성 평가</span>
|
|
</div>
|
|
<h1 className="text-lg font-bold text-gray-800">위험성 평가</h1>
|
|
</div>
|
|
|
|
{/* 필터 바 */}
|
|
<div className="bg-white border-b border-gray-200 px-6 py-3">
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<select value={roundFilter} onChange={e => setRoundFilter(e.target.value)}
|
|
className="border border-gray-300 rounded px-3 py-1.5 text-sm bg-white">
|
|
<option value="">차수 선택</option>
|
|
<option value="1">1차</option>
|
|
<option value="2">2차</option>
|
|
<option value="3">3차</option>
|
|
</select>
|
|
<select value={roundStatus} onChange={e => setRoundStatus(e.target.value)}
|
|
className="border border-gray-300 rounded px-3 py-1.5 text-sm bg-white">
|
|
<option value="">차수 진행상태 선택</option>
|
|
{ROUND_STATUS_OPTIONS.map(s => <option key={s} value={s}>{s}</option>)}
|
|
</select>
|
|
<select value={progressFilter} onChange={e => setProgressFilter(e.target.value)}
|
|
className="border border-gray-300 rounded px-3 py-1.5 text-sm bg-white">
|
|
<option value="">평가 진행상태 선택</option>
|
|
{PROGRESS_OPTIONS.map(s => <option key={s} value={s}>{s}</option>)}
|
|
</select>
|
|
<select value={companyFilter} onChange={e => setCompanyFilter(e.target.value)}
|
|
className="border border-gray-300 rounded px-3 py-1.5 text-sm bg-white" style={{width:280}}>
|
|
<option value="">업체 선택</option>
|
|
{COMPANIES.map(c => <option key={c} value={c}>{c}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 테이블 */}
|
|
<div className="flex-1 overflow-auto p-6">
|
|
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className={thCls} style={{width:35}}>
|
|
<input type="checkbox" checked={allChecked} onChange={() => {
|
|
const c = {}; if (!allChecked) data.forEach(d => c[d.id] = true);
|
|
setChecked(c);
|
|
}}/>
|
|
</th>
|
|
<th className={thCls} style={{width:60}}>차수</th>
|
|
<th className={thCls}>업체명</th>
|
|
<th className={thCls}>공종명</th>
|
|
<th className={thCls} style={{width:90}}>평가담당자</th>
|
|
<th className={thCls} style={{width:90}}>근로자대표</th>
|
|
<th className={thCls} style={{width:80}}>확인자</th>
|
|
<th className={thCls} style={{width:100}}>평가일자</th>
|
|
<th className={thCls} style={{width:140}}>적용기간</th>
|
|
<th className={thCls} style={{width:85}}>진행상태</th>
|
|
<th className={thCls} style={{width:50}}>보기</th>
|
|
<th className={thCls} style={{width:70}}>수기서류</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.length > 0 ? data.map((d, i) => (
|
|
<tr key={d.id} className="border-b border-gray-100 hover:bg-blue-50/30 transition">
|
|
<td className="px-3 py-2.5 text-center">
|
|
<input type="checkbox" checked={!!checked[d.id]} onChange={() => setChecked(p => ({...p, [d.id]: !p[d.id]}))} />
|
|
</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-700">{d.round}차</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-700">{d.company}</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-700">{d.workType}</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-700">{d.assessor}</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-700">{d.workerRep}</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-700">{d.verifier}</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-600">{d.assessDate}</td>
|
|
<td className="px-3 py-2.5 text-center text-gray-600">{d.applyPeriod}</td>
|
|
<td className="px-3 py-2.5 text-center">
|
|
<span className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
d.status === '승인완료' ? 'bg-green-100 text-green-700' :
|
|
d.status === '검토중' ? 'bg-blue-100 text-blue-700' :
|
|
'bg-yellow-100 text-yellow-700'
|
|
}`}>{d.status}</span>
|
|
</td>
|
|
<td className="px-3 py-2.5 text-center">
|
|
<button className="text-blue-500 hover:text-blue-700"><i className="ri-eye-line"></i></button>
|
|
</td>
|
|
<td className="px-3 py-2.5 text-center">
|
|
<button className="text-gray-400 hover:text-gray-600"><i className="ri-file-text-line"></i></button>
|
|
</td>
|
|
</tr>
|
|
)) : (
|
|
<tr>
|
|
<td colSpan={12} className="text-center py-16 text-gray-400">조회된 데이터가 없습니다.</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>);
|
|
}
|
|
|
|
ReactDOM.render(<App/>, document.getElementById('root'));
|
|
@endverbatim
|
|
</script>
|
|
@endpush
|