- 자료실 하위 3개 메뉴: 자료보관함, 매뉴얼, 공지사항 - 자료보관함: 폴더 트리 + 파일 업로드/다운로드/삭제 - 매뉴얼/공지사항: 게시판형 CRUD + 첨부파일 - 안전관리: 안전보건교육, TBM현황, 위험성평가, 재해예방조치 - 품질관리: 시정조치 UI 페이지 - 대시보드: 슈퍼관리자 전용 레거시 사이트 참고 카드 - 작업일보/출면일보 오류 수정 및 기능 개선 - 설비 사진 업로드, 근로계약서 종료일 수정
219 lines
13 KiB
PHP
219 lines
13 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'TBM현황 - 건설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 DAY_NAMES = ['일','월','화','수','목','금','토'];
|
|
|
|
/* ═══════ 메인 ═══════ */
|
|
function App(){
|
|
const now = new Date();
|
|
const pad = (n) => String(n).padStart(2,'0');
|
|
const todayStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}`;
|
|
const weekAgo = new Date(now); weekAgo.setDate(weekAgo.getDate() - 5);
|
|
const weekAgoStr = `${weekAgo.getFullYear()}-${pad(weekAgo.getMonth()+1)}-${pad(weekAgo.getDate())}`;
|
|
|
|
const [company, setCompany] = useState(COMPANIES[0]);
|
|
const [dateFrom, setDateFrom] = useState(weekAgoStr);
|
|
const [dateTo, setDateTo] = useState(todayStr);
|
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
|
|
/* 날짜 범위로 컬럼 동적 생성 */
|
|
const dateColumns = useMemo(() => {
|
|
const cols = [];
|
|
const start = new Date(dateFrom + 'T00:00:00');
|
|
const end = new Date(dateTo + 'T00:00:00');
|
|
if (isNaN(start) || isNaN(end) || start > end) return cols;
|
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
|
cols.push({
|
|
key: `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`,
|
|
label: `${pad(d.getMonth()+1)}/${pad(d.getDate())}(${DAY_NAMES[d.getDay()]})`,
|
|
});
|
|
}
|
|
return cols;
|
|
}, [dateFrom, dateTo]);
|
|
|
|
/* 샘플 데이터 (빈 상태 — API 연동 후 채워짐) */
|
|
const data = [];
|
|
|
|
const handleReset = () => {
|
|
setCompany(COMPANIES[0]);
|
|
setDateFrom(weekAgoStr);
|
|
setDateTo(todayStr);
|
|
};
|
|
|
|
const thCls = 'px-2 py-2 text-center font-semibold text-gray-600 text-xs border border-gray-200 whitespace-nowrap';
|
|
const tdCls = 'px-2 py-2 text-center text-gray-600 text-sm border border-gray-200';
|
|
|
|
return(
|
|
<div className="flex bg-gray-100" style={{height:'calc(100vh - 56px)'}}>
|
|
<PmisSidebar activePage="tbm"/>
|
|
<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 justify-between">
|
|
<h1 className="text-lg font-bold text-gray-800">TBM현황</h1>
|
|
<button onClick={() => setShowAdvanced(!showAdvanced)}
|
|
className="text-sm text-blue-600 hover:text-blue-800 flex items-center gap-1">
|
|
<i className="ri-search-line"></i> 상세검색
|
|
<i className={`ri-arrow-${showAdvanced ? 'up' : 'down'}-s-line text-xs`}></i>
|
|
</button>
|
|
</div>
|
|
</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={company} onChange={e => setCompany(e.target.value)}
|
|
className="border border-gray-300 rounded px-3 py-1.5 text-sm bg-white" style={{width:220}}>
|
|
{COMPANIES.map(c => <option key={c} value={c}>{c}</option>)}
|
|
</select>
|
|
<span className="text-sm text-gray-600 font-medium">기간</span>
|
|
<input type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
|
|
className="border border-gray-300 rounded px-2 py-1.5 text-sm"/>
|
|
<span className="text-gray-400">~</span>
|
|
<input type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
|
|
className="border border-gray-300 rounded px-2 py-1.5 text-sm"/>
|
|
<button className="bg-blue-700 text-white px-5 py-1.5 rounded text-sm font-semibold hover:bg-blue-800">검색</button>
|
|
<button onClick={handleReset} className="border border-gray-300 px-4 py-1.5 rounded text-sm hover:bg-gray-50">검색초기화</button>
|
|
</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 border-collapse">
|
|
<thead>
|
|
{/* 상단 헤더: 날짜 그룹 */}
|
|
<tr className="bg-gray-50">
|
|
<th className={thCls} rowSpan={2} style={{minWidth:120}}>업체명</th>
|
|
<th className={thCls} rowSpan={2} style={{minWidth:80}}>공종</th>
|
|
{dateColumns.map(col => (
|
|
<th key={col.key} className={thCls} colSpan={3}>{col.label}</th>
|
|
))}
|
|
<th className={thCls} rowSpan={2} style={{minWidth:50}}>참석<br/>인원<br/>계</th>
|
|
<th className={thCls} rowSpan={2} style={{minWidth:50}}>출력<br/>인원<br/>계</th>
|
|
<th className={thCls} rowSpan={2} style={{minWidth:55}}>참석율</th>
|
|
</tr>
|
|
{/* 하단 헤더: 참석인원 / 출력인원 / 일일참석율 */}
|
|
<tr className="bg-gray-50">
|
|
{dateColumns.map(col => (
|
|
<React.Fragment key={col.key + '-sub'}>
|
|
<th className={thCls} style={{minWidth:45}}>참석<br/>인원</th>
|
|
<th className={thCls} style={{minWidth:45}}>출력<br/>인원</th>
|
|
<th className={thCls} style={{minWidth:50}}>일일<br/>참석율</th>
|
|
</React.Fragment>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.length > 0 ? data.map((row, i) => (
|
|
<tr key={i} className="hover:bg-blue-50/30 transition">
|
|
<td className={tdCls}>{row.company}</td>
|
|
<td className={tdCls}>{row.workType}</td>
|
|
{dateColumns.map(col => {
|
|
const cell = row.dates?.[col.key] || {};
|
|
const rate = cell.output > 0 ? Math.round(cell.attend / cell.output * 100) : 0;
|
|
return (
|
|
<React.Fragment key={col.key}>
|
|
<td className={tdCls}>{cell.attend || 0}</td>
|
|
<td className={tdCls}>{cell.output || 0}</td>
|
|
<td className={tdCls}>{rate}%</td>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
<td className={tdCls + ' font-medium'}>{row.totalAttend || 0}</td>
|
|
<td className={tdCls + ' font-medium'}>{row.totalOutput || 0}</td>
|
|
<td className={tdCls + ' font-medium'}>{row.totalOutput > 0 ? Math.round(row.totalAttend / row.totalOutput * 100) : 0}%</td>
|
|
</tr>
|
|
)) : (
|
|
<tr>
|
|
<td colSpan={2 + dateColumns.length * 3 + 3} 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
|