678 lines
19 KiB
JavaScript
678 lines
19 KiB
JavaScript
import { w2grid, w2ui, w2popup, w2alert, w2confirm }
|
|
from 'https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js'
|
|
|
|
/* -------------------------------------------------
|
|
공통 유틸
|
|
------------------------------------------------- */
|
|
function destroyGrid(name) {
|
|
if (w2ui[name]) w2ui[name].destroy()
|
|
}
|
|
|
|
function fmtNum(val) {
|
|
return Number(val || 0).toLocaleString()
|
|
}
|
|
|
|
function fmtPrice(val) {
|
|
const v = Number(val || 0)
|
|
return Number.isInteger(v)
|
|
? v.toLocaleString()
|
|
: v.toLocaleString(undefined, { minimumFractionDigits: 2 })
|
|
}
|
|
|
|
/* -------------------------------------------------
|
|
서비스 등록 팝업
|
|
------------------------------------------------- */
|
|
export function openServiceRegisterPopup(ctx) {
|
|
|
|
destroyGrid('serviceGrid')
|
|
destroyGrid('productList')
|
|
|
|
w2popup.open({
|
|
title: '서비스 등록',
|
|
width: 1300,
|
|
height: 720,
|
|
modal: true,
|
|
body: `
|
|
<div class="service-popup">
|
|
|
|
<!-- 상단 -->
|
|
<div class="service-top">
|
|
|
|
<!-- 좌측 : 회원정보 -->
|
|
<div class="service-member">
|
|
<div><b>회원ID</b> : ${ctx.memberId}</div>
|
|
<div><b>회원명</b> : ${ctx.memberName}</div>
|
|
<div><b>회사명</b> : ${ctx.company}</div>
|
|
<div><b>사업자번호</b> : ${ctx.bizNo}</div>
|
|
|
|
<div class="purchase-date">
|
|
<label>구매일</label>
|
|
<input type="date" id="buyDate">
|
|
<button id="btnSearchBuy">조회</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 우측 : 상품 선택 -->
|
|
<div class="service-product">
|
|
<div class="title">상품 선택 (더블클릭)</div>
|
|
<div id="productList" style="height:160px;"></div>
|
|
</div>
|
|
|
|
</div>
|
|
<!-- 중단 -->
|
|
<div class="service-save-bar">
|
|
<div class="right-actions">
|
|
<button id="btnDeleteRow" class="btn-delete">삭제</button>
|
|
<button id="btnSaveService" class="btn-save">저장</button>
|
|
</div>
|
|
</div>
|
|
<div class="service-grid">
|
|
<div id="serviceGrid" style="height:260px;"></div>
|
|
</div>
|
|
|
|
<!-- 하단 합계 -->
|
|
<div class="service-summary">
|
|
<div>공급가액 <span id="sumSupply">0</span></div>
|
|
<div>부가세 <span id="sumVat">0</span></div>
|
|
<div>결제금액 <span id="sumTotal">0</span></div>
|
|
</div>
|
|
|
|
</div>
|
|
`,
|
|
onOpen(event) {
|
|
event.onComplete = () => {
|
|
|
|
createServiceGrid()
|
|
createProductList()
|
|
|
|
const buyDateInput = document.getElementById('buyDate')
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
// 기본 구매일 = 오늘
|
|
buyDateInput.value = today
|
|
|
|
// 자동 조회
|
|
loadExistingPurchase(ctx.memberId, today)
|
|
|
|
// 수동 조회 버튼
|
|
document.getElementById('btnSearchBuy').addEventListener('click', () => {
|
|
const buyDate = buyDateInput.value
|
|
if (!buyDate) {
|
|
w2alert('구매일을 선택하세요')
|
|
return
|
|
}
|
|
loadExistingPurchase(ctx.memberId, buyDate)
|
|
})
|
|
|
|
document.getElementById('btnDeleteRow')
|
|
.addEventListener('click', deleteSelectedRows)
|
|
|
|
document.getElementById('btnSaveService')
|
|
.addEventListener('click', () => saveService(ctx))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/* -------------------------------------------------
|
|
서비스 Grid
|
|
------------------------------------------------- */
|
|
function createServiceGrid() {
|
|
|
|
new w2grid({
|
|
name: 'serviceGrid',
|
|
box: '#serviceGrid',
|
|
show: {
|
|
footer: true,
|
|
selectColumn: true
|
|
},
|
|
multiSelect: false,
|
|
|
|
columns: [
|
|
{ field: 'recid', text: 'NO', size: '50px', attr: 'align=center' },
|
|
{ field: 'itm_nm', text: '상품명', size: '120px', editable: { type: 'text' } },
|
|
{ field: 'area', text: '제공량', size: '120px', editable: { type: 'text' } },
|
|
{ field: 'itm_cd', text: '상품코드', hidden: true },
|
|
{
|
|
field: 'itm_amt',
|
|
text: '단가',
|
|
size: '100px',
|
|
attr: 'align=right',
|
|
render: r => fmtPrice(r.itm_amt)
|
|
},
|
|
{
|
|
field: 'itm_qty',
|
|
text: '수량',
|
|
size: '80px',
|
|
attr: 'align=right',
|
|
editable: { type: 'int' },
|
|
render: r => fmtNum(r.itm_qty)
|
|
},
|
|
{
|
|
field: 'dis_rt',
|
|
text: '할인율',
|
|
size: '80px',
|
|
editable: { type: 'int' }
|
|
},
|
|
{
|
|
field: 'supply',
|
|
text: '공급가액',
|
|
size: '120px',
|
|
attr: 'align=right',
|
|
render: r => fmtNum(r.supply)
|
|
},
|
|
{
|
|
field: 'vat_amt',
|
|
text: '부가세',
|
|
size: '100px',
|
|
attr: 'align=right',
|
|
render: r => fmtNum(r.vat_amt)
|
|
},
|
|
{
|
|
field: 'sum_amt',
|
|
text: '결제금액',
|
|
size: '120px',
|
|
attr: 'align=right',
|
|
render: r => fmtNum(r.sum_amt)
|
|
},
|
|
{
|
|
field: 'itm_area',
|
|
text: '적용면적(m²)',
|
|
size: '100px',
|
|
attr: 'align=right',
|
|
editable: { type: 'int' },
|
|
render: r => fmtNum(r.itm_area)
|
|
},
|
|
{
|
|
field: 'add_area',
|
|
text: '추가면적(m²)',
|
|
size: '110px',
|
|
attr: 'align=right',
|
|
editable: { type: 'int' },
|
|
render: r => fmtNum(r.add_area)
|
|
},
|
|
{
|
|
field: 'sum_area',
|
|
text: '합계면적(m²)',
|
|
size: '110px',
|
|
attr: 'align=right',
|
|
editable: { type: 'int' },
|
|
render: r => fmtNum(r.sum_area)
|
|
},
|
|
{
|
|
field: 'end_dt',
|
|
text: '만료일자',
|
|
size: '110px',
|
|
editable: { type: 'date' }
|
|
},
|
|
{
|
|
field: 'ok_yn',
|
|
text: '승인여부',
|
|
size: '80px',
|
|
attr: 'align=center',
|
|
editable: {
|
|
type: 'combo',
|
|
items: [
|
|
{ id: 'N', text: '미승인' },
|
|
{ id: 'Y', text: '승인' }
|
|
],
|
|
openOnFocus: true
|
|
},
|
|
render(record) {
|
|
if (record.ok_yn === 'Y') return '승인'
|
|
return '미승인'
|
|
}
|
|
},
|
|
{ field: 'rmks', text: '비고', size: '150px', editable: { type: 'text' } }
|
|
],
|
|
|
|
records: [],
|
|
|
|
/* -----------------------------
|
|
일반 필드 처리 (date 제외)
|
|
----------------------------- */
|
|
onChange(event) {
|
|
event.onComplete = (ev) => {
|
|
const g = w2ui.serviceGrid
|
|
const field = g.columns[ev.column].field
|
|
|
|
let val = ev.value_new
|
|
if (typeof val === 'object' && val !== null) val = val.id
|
|
|
|
g.set(ev.recid, { [field]: val })
|
|
|
|
if (['itm_qty','dis_rt','add_area'].includes(field)) {
|
|
recalcRow(ev.recid)
|
|
}
|
|
}
|
|
},
|
|
|
|
/* -----------------------------
|
|
날짜 전용 처리 (핵심)
|
|
----------------------------- */
|
|
onEditField(event) {
|
|
if (event.field !== 'end_dt') return;
|
|
|
|
event.onComplete = () => {
|
|
const g = w2ui.serviceGrid;
|
|
const recid = event.recid;
|
|
|
|
const input = document.querySelector(
|
|
`#grid_${g.name}_edit input`
|
|
);
|
|
if (!input) return;
|
|
|
|
input.addEventListener('change', () => {
|
|
|
|
// 🔥 날짜는 문자열 그대로 즉시 확정
|
|
g.set(recid, {
|
|
end_dt: input.value // YYYY-MM-DD
|
|
});
|
|
|
|
// 🔥 w2ui 방식으로 edit 종료
|
|
input.blur();
|
|
|
|
g.refreshRow(recid);
|
|
});
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/* -------------------------------------------------
|
|
상품 목록
|
|
------------------------------------------------- */
|
|
function createProductList() {
|
|
|
|
new w2grid({
|
|
name: 'productList',
|
|
box: '#productList',
|
|
url: '/kngil/bbs/adm_product_popup.php',
|
|
columns: [
|
|
{ field: 'itm_nm', text: '상품명', size: '120px' },
|
|
{
|
|
field: 'area',
|
|
text: '제공량',
|
|
size: '100px',
|
|
attr: 'align=right',
|
|
render(record) {
|
|
return Number(record.area || 0).toLocaleString()
|
|
}
|
|
},
|
|
{
|
|
field: 'itm_amt',
|
|
text: '단가',
|
|
size: '120px',
|
|
attr: 'align=right',
|
|
render(record) {
|
|
const v = Number(record.itm_amt || 0)
|
|
// 정수면 소수점 제거
|
|
return Number.isInteger(v)
|
|
? v.toLocaleString()
|
|
: v.toLocaleString(undefined, { minimumFractionDigits: 2 })
|
|
}
|
|
}
|
|
],
|
|
onDblClick(event) {
|
|
event.onComplete = () => {
|
|
const recid = event.detail?.recid
|
|
if (!recid) return
|
|
|
|
const rec = this.get(recid)
|
|
if (!rec) return
|
|
|
|
addServiceFromProduct(rec)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
/* -------------------------------------------------
|
|
상품 → 서비스 Grid 추가
|
|
------------------------------------------------- */
|
|
function addServiceFromProduct(p) {
|
|
|
|
const g = w2ui.serviceGrid
|
|
const recid = g.records.length + 1
|
|
|
|
const qty = 1
|
|
const area = Number(p.area || 0)
|
|
const unit = Number(p.itm_amt || 0)
|
|
|
|
const today = new Date()
|
|
const endDt = `${today.getFullYear()}-12-31`
|
|
|
|
g.add({
|
|
recid,
|
|
itm_cd: p.itm_cd,
|
|
itm_nm: p.itm_nm,
|
|
|
|
area: area, // 제공량
|
|
itm_qty: qty,
|
|
itm_amt: unit,
|
|
dis_rt: 0,
|
|
|
|
itm_area: area * qty,
|
|
add_area: 0,
|
|
sum_area: area * qty,
|
|
|
|
supply: unit * qty,
|
|
vat_amt: Math.floor(unit * qty * 0.1),
|
|
sum_amt: Math.floor(unit * qty * 1.1),
|
|
|
|
end_dt: endDt,
|
|
ok_yn: 'N',
|
|
rmks: '',
|
|
|
|
_new: true
|
|
})
|
|
|
|
recalcRow(recid)
|
|
}
|
|
|
|
|
|
|
|
/* -------------------------------------------------
|
|
행 삭제
|
|
------------------------------------------------- */
|
|
function deleteSelectedRows() {
|
|
|
|
const g = w2ui.serviceGrid
|
|
const sel = g.getSelection()
|
|
|
|
if (!sel.length) {
|
|
w2alert('삭제할 서비스를 선택하세요')
|
|
return
|
|
}
|
|
|
|
const recid = sel[0]
|
|
const row = g.get(recid)
|
|
|
|
if (!row || !row.sq_no) {
|
|
w2alert('삭제할 수 없는 항목입니다.')
|
|
return
|
|
}
|
|
|
|
if (row.ok_yn === 'Y') {
|
|
w2alert('승인된 항목은 삭제할 수 없습니다.')
|
|
return
|
|
}
|
|
|
|
w2confirm('선택한 서비스를 삭제하시겠습니까?')
|
|
.yes(() => {
|
|
deleteServiceImmediately(row)
|
|
})
|
|
}
|
|
|
|
function deleteServiceImmediately(row) {
|
|
console.log({
|
|
action: 'delete',
|
|
member_id: row.member_id,
|
|
sq_no: row.sq_no
|
|
})
|
|
|
|
fetch('/kngil/bbs/adm_service.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'delete',
|
|
member_id: row.member_id, // ✅ 여기 중요
|
|
sq_no: row.sq_no
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(res => {
|
|
if (res.status === 'success') {
|
|
w2alert('삭제되었습니다.')
|
|
w2ui.serviceGrid.remove(row.recid)
|
|
calcSummary()
|
|
} else {
|
|
w2alert(res.message || '삭제 실패')
|
|
}
|
|
})
|
|
.catch(() => w2alert('서버 오류'))
|
|
}
|
|
|
|
|
|
function isServiceItem(r) {
|
|
return r.itm_cd && r.itm_cd.startsWith('ZET01')
|
|
|| r.itm_nm === '서비스'
|
|
}
|
|
|
|
/* -------------------------------------------------
|
|
기존 구매 불러오기
|
|
------------------------------------------------- */
|
|
function loadExistingPurchase(memberId, buyDate) {
|
|
|
|
fetch('/kngil/bbs/adm_service.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'list',
|
|
member_id: memberId,
|
|
buy_date: buyDate
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(json => {
|
|
|
|
if (json.status !== 'success') {
|
|
w2alert(json.message || '조회 실패')
|
|
return
|
|
}
|
|
|
|
const g = w2ui.serviceGrid
|
|
g.clear()
|
|
|
|
json.records.forEach((r, i) => {
|
|
g.add({
|
|
recid: i + 1,
|
|
member_id: memberId,
|
|
sq_no: r.sq_no,
|
|
|
|
itm_cd: r.itm_cd,
|
|
itm_nm: r.itm_nm,
|
|
itm_area: Number(r.itm_area || 0),
|
|
add_area: Number(r.add_area || 0),
|
|
itm_amt: Number(r.itm_amt),
|
|
itm_qty: Number(r.itm_qty),
|
|
|
|
// area
|
|
area: isServiceItem(r)
|
|
? 1
|
|
: (r.itm_qty ? Number(r.itm_area) / Number(r.itm_qty) : 0),
|
|
|
|
dis_rt: Number(r.dis_rt || 0), // 문자열 → 숫자
|
|
supply: Number(r.buy_amt),
|
|
vat_amt: Number(r.vat_amt),
|
|
sum_amt: Number(r.sum_amt),
|
|
|
|
end_dt: r.end_dt || '',
|
|
ok_yn: r.ok_yn || 'N',
|
|
rmks: r.rmks ?? '', // undefined 방지
|
|
|
|
_existing: true
|
|
})
|
|
|
|
g.set(i + 1, {
|
|
w2ui: { style: 'background:#f3f3f3;color:#555' }
|
|
})
|
|
})
|
|
|
|
g.refresh()
|
|
// 🔥 모든 행 재계산
|
|
g.records.forEach(r => {
|
|
if (!r._deleted) {
|
|
recalcRow(r.recid)
|
|
}
|
|
})
|
|
calcSummary()
|
|
})
|
|
.catch(() => w2alert('서버 오류'))
|
|
}
|
|
|
|
/* -------------------------------------------------
|
|
행 단위 재계산
|
|
------------------------------------------------- */
|
|
function recalcRow(recid) {
|
|
|
|
const g = w2ui.serviceGrid
|
|
const r = g.get(recid)
|
|
if (!r) return
|
|
|
|
const qty = Number(r.itm_qty || 0)
|
|
const unit = Number(r.itm_amt || 0)
|
|
const rate = Number(r.dis_rt || 0)
|
|
const area = Number(r.area || 0)
|
|
const add = Number(r.add_area || 0)
|
|
|
|
// 🔥 적용면적 = 제공량 * 수량
|
|
r.itm_area = area * qty
|
|
|
|
// 🔥 공급가액 = 단가 * 수량 * (1 - 할인율)
|
|
const base = unit * qty
|
|
const discount = Math.floor(base * (rate / 100))
|
|
r.supply = base - discount
|
|
|
|
// 🔥 부가세 / 결제금액
|
|
r.vat_amt = Math.floor(r.supply * 0.1)
|
|
r.sum_amt = r.supply + r.vat_amt
|
|
|
|
// 🔥 합계면적
|
|
r.sum_area = r.itm_area + add
|
|
|
|
g.refreshRow(recid)
|
|
calcSummary()
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -------------------------------------------------
|
|
합계 계산
|
|
------------------------------------------------- */
|
|
function calcSummary() {
|
|
|
|
let supply = 0
|
|
let vat = 0
|
|
let total = 0
|
|
|
|
w2ui.serviceGrid.records.forEach(r => {
|
|
if (r._deleted) return
|
|
supply += Number(r.supply || 0)
|
|
vat += Number(r.vat_amt || 0)
|
|
total += Number(r.sum_amt || 0)
|
|
})
|
|
|
|
document.getElementById('sumSupply').innerText = supply.toLocaleString()
|
|
document.getElementById('sumVat').innerText = vat.toLocaleString()
|
|
document.getElementById('sumTotal').innerText = total.toLocaleString()
|
|
}
|
|
|
|
function saveService(ctx) {
|
|
|
|
const buyDate = document.getElementById('buyDate').value
|
|
if (!buyDate) {
|
|
w2alert('구매일을 선택하세요')
|
|
return
|
|
}
|
|
|
|
const g = w2ui.serviceGrid
|
|
|
|
// 🔥🔥🔥 핵심: 현재 편집중인 셀 강제 반영
|
|
g.save()
|
|
|
|
const items = g.records.map(r => ({
|
|
sq_no: r.sq_no || null,
|
|
itm_cd: r.itm_cd,
|
|
itm_area: r.itm_area,
|
|
add_area: r.add_area || 0,
|
|
itm_amt: r.itm_amt,
|
|
itm_qty: r.itm_qty,
|
|
|
|
dis_rt: r.dis_rt,
|
|
buy_amt: r.supply,
|
|
vat_amt: r.vat_amt,
|
|
sum_amt: r.sum_amt,
|
|
end_dt: dateToYMD(r.end_dt),
|
|
ok_yn: r.ok_yn || 'N',
|
|
rmks: r.rmks,
|
|
|
|
_new: r._new || false,
|
|
_existing: r._existing || false,
|
|
_deleted: r._deleted || false
|
|
}))
|
|
|
|
fetch('/kngil/bbs/adm_service.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
action: 'save',
|
|
member_id: ctx.memberId,
|
|
buy_date: buyDate,
|
|
items
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(res => {
|
|
if (res.status === 'success') {
|
|
w2alert('저장되었습니다.')
|
|
loadExistingPurchase(ctx.memberId, buyDate)
|
|
} else {
|
|
w2alert(res.message || '저장 실패')
|
|
}
|
|
})
|
|
}
|
|
|
|
function normalizeDate(val) {
|
|
if (!val) return null
|
|
|
|
// 이미 YYYY-MM-DD면 그대로
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
|
|
return val
|
|
}
|
|
|
|
// MM/DD/YYYY → YYYY-MM-DD
|
|
const d = new Date(val)
|
|
if (isNaN(d)) return null
|
|
|
|
return d.toISOString().slice(0, 10)
|
|
}
|
|
|
|
|
|
function dateToYMD(val) {
|
|
if (!val) return null
|
|
|
|
// Date 객체
|
|
if (val instanceof Date) {
|
|
return [
|
|
val.getFullYear(),
|
|
String(val.getMonth() + 1).padStart(2, '0'),
|
|
String(val.getDate()).padStart(2, '0')
|
|
].join('-')
|
|
}
|
|
|
|
// 문자열 (MM/DD/YYYY or YYYY-MM-DD)
|
|
if (typeof val === 'string') {
|
|
|
|
// 이미 YYYY-MM-DD
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(val)) {
|
|
return val
|
|
}
|
|
|
|
// MM/DD/YYYY
|
|
const d = new Date(val)
|
|
if (!isNaN(d)) {
|
|
return [
|
|
d.getFullYear(),
|
|
String(d.getMonth() + 1).padStart(2, '0'),
|
|
String(d.getDate()).padStart(2, '0')
|
|
].join('-')
|
|
}
|
|
}
|
|
|
|
return null
|
|
} |