2025-08-11 20:49:15 +09:00
|
|
|
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
|
|
|
|
<?php
|
|
|
|
|
$CURRENT_SECTION = 'approval';
|
|
|
|
|
include '../inc/header.php';
|
|
|
|
|
?>
|
|
|
|
|
<div class="container" style="max-width:1280px; margin-top:24px;">
|
|
|
|
|
<div class="card shadow-sm">
|
|
|
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
|
|
|
<strong>결재권자 풀 관리</strong>
|
|
|
|
|
|
|
|
|
|
<!-- 한 줄 툴바 -->
|
|
|
|
|
<form id="toolbar" class="sam-toolbar d-flex align-items-center gap-2 flex-nowrap" role="search" onsubmit="return false;">
|
|
|
|
|
<select class="form-select form-select-sm" id="type" style="width:140px;">
|
|
|
|
|
<option value="user">개인</option>
|
|
|
|
|
<option value="department">부서</option>
|
|
|
|
|
<option value="role">역할</option>
|
|
|
|
|
</select>
|
|
|
|
|
<input class="form-control form-control-sm" id="keyword" placeholder="이름/코드" style="width:260px;">
|
|
|
|
|
<div class="sam-actions d-flex flex-nowrap gap-2">
|
|
|
|
|
<button class="btn btn-sm btn-outline-secondary" id="find" type="button">검색</button>
|
|
|
|
|
<button class="btn btn-sm btn-primary" id="add" type="button">추가</button>
|
2025-08-10 02:36:50 +09:00
|
|
|
</div>
|
2025-08-11 20:49:15 +09:00
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-hover align-middle text-center m-0" id="poolTable">
|
|
|
|
|
<thead class="table-light">
|
|
|
|
|
<tr>
|
|
|
|
|
<th style="width:120px;">유형</th>
|
|
|
|
|
<th>대상</th>
|
|
|
|
|
<th style="width:140px;">관리</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody id="poolRows"><!-- JS 렌더 --></tbody>
|
2025-08-10 02:36:50 +09:00
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-08-11 20:49:15 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 검색/추가 모달 -->
|
|
|
|
|
<div class="modal fade" id="pickModal" tabindex="-1" aria-hidden="true">
|
|
|
|
|
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title">대상 선택</h5>
|
|
|
|
|
<button class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<!-- 검색바(모달 내부에서도 재검색 가능) -->
|
|
|
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
|
|
|
<select class="form-select form-select-sm" id="pm_type" style="width:140px;"></select>
|
|
|
|
|
<input class="form-control form-control-sm" id="pm_keyword" placeholder="이름/코드">
|
|
|
|
|
<button class="btn btn-sm btn-outline-secondary" id="pm_find" type="button">검색</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-hover align-middle text-center m-0">
|
|
|
|
|
<thead class="table-light">
|
|
|
|
|
<tr>
|
|
|
|
|
<th style="width:100px;">유형</th>
|
|
|
|
|
<th>코드</th>
|
|
|
|
|
<th>이름</th>
|
|
|
|
|
<th style="width:120px;">선택</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody id="pm_rows"><!-- JS 렌더 --></tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<div class="text-muted small">선택 즉시 풀에 추가됩니다.</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
/* 툴바 전체를 한 줄로 고정 */
|
|
|
|
|
.sam-toolbar { display:flex; flex-wrap:nowrap; gap:.5rem; }
|
|
|
|
|
.sam-toolbar > * { flex:0 0 auto; min-width:0; } /* 넓이 강제분배 방지 */
|
|
|
|
|
|
|
|
|
|
/* 버튼 한글 줄바꿈 방지 (전역 break-all 무력화) */
|
|
|
|
|
.sam-toolbar .btn,
|
|
|
|
|
.sam-actions .btn {
|
|
|
|
|
display:inline-flex;
|
|
|
|
|
align-items:center;
|
|
|
|
|
justify-content:center;
|
|
|
|
|
white-space:nowrap !important;
|
|
|
|
|
word-break:normal !important; /* ← 핵심 */
|
|
|
|
|
overflow-wrap:normal !important;/* ← 핵심 */
|
|
|
|
|
line-height:1.5;
|
|
|
|
|
padding:.25rem .6rem; /* btn-sm 기본과 유사 */
|
|
|
|
|
flex:0 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 입력 폭이 너무 좁아 버튼을 밀어내지 않도록 적당한 폭 부여 */
|
|
|
|
|
#keyword { width:260px; min-width:160px; }
|
|
|
|
|
|
|
|
|
|
/* 매우 좁은 화면일 때만 버튼 폭/패딩을 더 줄임 */
|
|
|
|
|
@media (max-width: 576px){
|
|
|
|
|
#keyword{ width:140px; }
|
|
|
|
|
.sam-toolbar .btn{ padding:.2rem .45rem; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
$(function(){
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// 1) 샘플 데이터 (실서버에선 API로 대체)
|
|
|
|
|
// -----------------------------
|
|
|
|
|
const SAMPLE = {
|
|
|
|
|
user: [
|
|
|
|
|
{ code:'u001', name:'권혁성(kevin)' },
|
|
|
|
|
{ code:'u002', name:'김슬기(sally)' },
|
|
|
|
|
{ code:'u003', name:'홍길동(hong)' },
|
|
|
|
|
],
|
|
|
|
|
department: [
|
|
|
|
|
{ code:'d001', name:'개발팀' },
|
|
|
|
|
{ code:'d002', name:'영업팀' },
|
|
|
|
|
{ code:'d003', name:'생산팀' },
|
|
|
|
|
],
|
|
|
|
|
role: [
|
|
|
|
|
{ code:'r001', name:'최고관리자' },
|
|
|
|
|
{ code:'r002', name:'일반관리자' },
|
|
|
|
|
{ code:'r003', name:'일반직원' },
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 결재권자 풀 (메모리) : {type, code, name}
|
|
|
|
|
const pool = [
|
|
|
|
|
{type:'department', code:'d001', name:'개발팀'},
|
|
|
|
|
{type:'role', code:'r002', name:'일반관리자'},
|
|
|
|
|
{type:'user', code:'u001', name:'권혁성(kevin)'},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// 2) 렌더 함수
|
|
|
|
|
// -----------------------------
|
|
|
|
|
function renderPool(){
|
|
|
|
|
if(pool.length===0){
|
|
|
|
|
$('#poolRows').html('<tr><td colspan="3" class="text-muted py-4">등록된 결재권자가 없습니다.</td></tr>');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const html = pool.map((p,idx)=>`
|
|
|
|
|
<tr>
|
|
|
|
|
<td>${labelType(p.type)}</td>
|
|
|
|
|
<td class="text-start">${escapeHtml(p.name)} <span class="text-muted">/ ${escapeHtml(p.code)}</span></td>
|
|
|
|
|
<td>
|
|
|
|
|
<button class="btn btn-sm btn-outline-danger" data-remove="${idx}">삭제</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('');
|
|
|
|
|
$('#poolRows').html(html);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPickRows(list, type){
|
|
|
|
|
if(list.length===0){
|
|
|
|
|
$('#pm_rows').html('<tr><td colspan="4" class="text-muted py-4">검색 결과가 없습니다.</td></tr>');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const html = list.map(x=>`
|
|
|
|
|
<tr>
|
|
|
|
|
<td>${labelType(type)}</td>
|
|
|
|
|
<td>${escapeHtml(x.code)}</td>
|
|
|
|
|
<td class="text-start">${escapeHtml(x.name)}</td>
|
|
|
|
|
<td><button class="btn btn-sm btn-primary" data-pick="${type}|${x.code}">선택</button></td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('');
|
|
|
|
|
$('#pm_rows').html(html);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// 3) 유틸
|
|
|
|
|
// -----------------------------
|
|
|
|
|
function labelType(t){
|
|
|
|
|
switch(t){
|
|
|
|
|
case 'user': return '개인';
|
|
|
|
|
case 'department': return '부서';
|
|
|
|
|
case 'role': return '역할';
|
|
|
|
|
default: return t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function escapeHtml(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
|
|
|
|
|
function uniquePush(arr, item){
|
|
|
|
|
if(!arr.some(x=>x.type===item.type && x.code===item.code)) arr.push(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// 4) 이벤트: 삭제
|
|
|
|
|
// -----------------------------
|
|
|
|
|
$(document).on('click','[data-remove]',function(){
|
|
|
|
|
const i = +$(this).data('remove');
|
|
|
|
|
if(i>=0){ pool.splice(i,1); renderPool(); }
|
|
|
|
|
// 실제: $.post('/api/approval/pool_delete.php', { type: pool[i].type, code: pool[i].code })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// 5) 검색 & 추가 (툴바)
|
|
|
|
|
// -----------------------------
|
|
|
|
|
$('#find').on('click', function(){
|
|
|
|
|
openPicker(false); // 조회만
|
|
|
|
|
});
|
|
|
|
|
$('#add').on('click', function(){
|
|
|
|
|
openPicker(true); // 선택 시 추가
|
2025-08-10 02:36:50 +09:00
|
|
|
});
|
2025-08-11 20:49:15 +09:00
|
|
|
|
|
|
|
|
// -----------------------------
|
|
|
|
|
// 6) 모달 열기/검색/선택
|
|
|
|
|
// -----------------------------
|
|
|
|
|
function openPicker(isAddMode){
|
|
|
|
|
const type = $('#type').val();
|
|
|
|
|
const kw = $('#keyword').val().trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
// 모달 상단 타입/키워드 동기화
|
|
|
|
|
$('#pm_type').html($('#type').html()).val(type);
|
|
|
|
|
$('#pm_keyword').val($('#keyword').val());
|
|
|
|
|
|
|
|
|
|
// 첫 렌더
|
|
|
|
|
const list = filterList(type, kw);
|
|
|
|
|
renderPickRows(list, type);
|
|
|
|
|
|
|
|
|
|
// 모달 띄우기
|
|
|
|
|
const m = new bootstrap.Modal('#pickModal');
|
|
|
|
|
m.show();
|
|
|
|
|
|
|
|
|
|
// 모달 내 검색
|
|
|
|
|
$('#pm_find').off('click').on('click', function(){
|
|
|
|
|
const t2 = $('#pm_type').val();
|
|
|
|
|
const k2 = $('#pm_keyword').val().trim().toLowerCase();
|
|
|
|
|
renderPickRows(filterList(t2, k2), t2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 선택 → 추가
|
|
|
|
|
$(document).off('click.pmPick').on('click.pmPick','[data-pick]', function(){
|
|
|
|
|
const [t,c] = String($(this).data('pick')).split('|');
|
|
|
|
|
const item = (SAMPLE[t]||[]).find(x=>x.code===c);
|
|
|
|
|
if(!item) return;
|
|
|
|
|
|
|
|
|
|
// 추가 모드일 때만 풀에 반영
|
|
|
|
|
uniquePush(pool, {type:t, code:item.code, name:item.name});
|
|
|
|
|
renderPool();
|
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('pickModal')).hide();
|
|
|
|
|
|
|
|
|
|
// 실제: $.post('/api/approval/pool_add.php', { type:t, code:item.code })
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterList(type, kw){
|
|
|
|
|
const base = SAMPLE[type] || [];
|
|
|
|
|
if(!kw) return base;
|
|
|
|
|
return base.filter(x =>
|
|
|
|
|
String(x.code).toLowerCase().includes(kw) ||
|
|
|
|
|
String(x.name).toLowerCase().includes(kw)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 최초 렌더
|
|
|
|
|
renderPool();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
2025-08-10 02:36:50 +09:00
|
|
|
<?php include '../inc/footer.php'; ?>
|