리펙토링 #6 참고
This commit is contained in:
469
kngil/skin/sales_results.php
Normal file
469
kngil/skin/sales_results.php
Normal file
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업실적");
|
||||
?>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-4">영업실적</h2>
|
||||
|
||||
<!-- <button class="bg-blue-600 text-white px-4 py-2 rounded mb-3" id="btn-add-result">
|
||||
신규 실적 등록
|
||||
</button> -->
|
||||
|
||||
<div id="grid_results" style="width:100%;height:720px;"></div>
|
||||
|
||||
<script type="module">
|
||||
import { w2grid } from "https://cdn.jsdelivr.net/gh/vitmalina/w2ui@master/dist/w2ui.es6.min.js";
|
||||
|
||||
/* ------------------------------------------------------
|
||||
공통 유틸
|
||||
------------------------------------------------------ */
|
||||
function clean(v) {
|
||||
return (v === undefined || v === null) ? '' : v;
|
||||
}
|
||||
function cleanNumber(v) {
|
||||
if (v === undefined || v === null || v === '' || isNaN(v)) return 0;
|
||||
return Number(v);
|
||||
}
|
||||
function normalizeListValue(v) {
|
||||
if (typeof v === "object" && v !== null && v.id) return v.id;
|
||||
if (typeof v === "string") return v;
|
||||
return '';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 직원 목록 / 거래처 목록 로드
|
||||
------------------------------------------------------ */
|
||||
let employeeList = [];
|
||||
let clientList = [];
|
||||
let productList = [];
|
||||
|
||||
async function loadEmployees() {
|
||||
let res = await fetch('/egbim/bbs/sales_members.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === "ok") {
|
||||
employeeList = json.records.map(m => ({
|
||||
id: m.emp_no,
|
||||
text: `${m.emp_name} (${m.emp_no})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClients() {
|
||||
let res = await fetch('/egbim/bbs/sales_clients.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === "ok") {
|
||||
clientList = json.records.map(c => ({
|
||||
id: c.client_code,
|
||||
text: `${c.client_name} (${c.client_code})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProducts() {
|
||||
let res = await fetch('/egbim/bbs/sales_products.php?action=list');
|
||||
let json = await res.json();
|
||||
|
||||
if (json.status === 'ok') {
|
||||
productList = json.records.map(p => ({
|
||||
id: p.code, // 저장용
|
||||
text: `${p.name} (${p.code})` // 화면 표시용
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 직원 + 거래처 모두 로드 후 GRID 생성
|
||||
------------------------------------------------------ */
|
||||
await loadEmployees();
|
||||
await loadClients();
|
||||
await loadProducts();
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 GRID
|
||||
------------------------------------------------------ */
|
||||
let grid = new w2grid({
|
||||
name: 'grid_results',
|
||||
box: '#grid_results',
|
||||
|
||||
show: {
|
||||
toolbar: true,
|
||||
footer: true,
|
||||
toolbarSave: true,
|
||||
toolbarReload: true,
|
||||
toolbarSearch: true,
|
||||
toolbarColumns: true,
|
||||
lineNumbers: true
|
||||
},
|
||||
|
||||
multiSearch: true,
|
||||
|
||||
searches: [
|
||||
{ field: 'sales_date', label: '실적일', type: 'date' },
|
||||
{ field: 'emp_no', label: '영업담당자', type: 'text' },
|
||||
{ field: 'client_code', label: '거래처명', type: 'text', operator: 'contains' },
|
||||
{ field: 'product_code', label: '제품명', type: 'text' },
|
||||
{ field: 'remarks', label: '비고', type: 'text' },
|
||||
],
|
||||
|
||||
columns: [
|
||||
{ field: 'sales_date', text: '실적일', size: '110px', editable: { type: 'date' }, sortable: true },
|
||||
|
||||
/* 🔵 영업담당자 콤보 */
|
||||
{
|
||||
field: 'emp_no',
|
||||
text: '영업담당자',
|
||||
size: '150px',
|
||||
editable: { type: 'combo', items: employeeList, showAll: true, filter: true},
|
||||
resizable: true,
|
||||
// render(record) {
|
||||
// // 1) combo 선택 직후: record.emp_no = { id, text }
|
||||
// if (typeof record.emp_no === "object" && record.emp_no !== null) {
|
||||
// return record.emp_no.text; // ← 즉시 표시됨
|
||||
// }
|
||||
|
||||
// // 2) 서버에서 불러온 값은 string → employeeList에서 매칭
|
||||
// const item = employeeList.find(e => e.id == record.emp_no);
|
||||
// return item ? item.text : record.emp_no;
|
||||
// },
|
||||
sortable: true
|
||||
},
|
||||
|
||||
/* 🔵 거래처명 콤보 — DB는 client_code 저장 */
|
||||
{
|
||||
field: 'client_code',
|
||||
text: '거래처명',
|
||||
size: '180px',
|
||||
editable: { type: 'combo', items: clientList, showAll: true, filter: true, match: 'contains', openOnFocus: true },
|
||||
// render(record, extra) {
|
||||
// let id = (typeof record.client_code === "object") ? record.client_code.id : record.client_code;
|
||||
// let item = clientList.find(c => c.id == id);
|
||||
// return item ? item.text : id;
|
||||
// },
|
||||
resizable: true,
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{
|
||||
field: 'product_code',
|
||||
text: '제품명',
|
||||
size: '200px',
|
||||
editable: { type: 'combo', items: productList, filter: true, showAll: true },
|
||||
|
||||
// combo는 render 제거해야 정상표시됨
|
||||
// render(record) {
|
||||
// let item = productList.find(p => p.id === record.product_code);
|
||||
// return item ? item.text : record.product_code;
|
||||
// },
|
||||
resizable: true,
|
||||
sortable: true
|
||||
},
|
||||
|
||||
{ field: 'quantity', text: '수량', size: '80px',
|
||||
editable: { type: 'int' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'unit_price', text: '단가', size: '100px',
|
||||
editable: { type: 'int' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'discount', text: '할인액', size: '100px',
|
||||
editable: { type: 'float' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'total_amount', text: '총금액', size: '120px',
|
||||
editable: { type: 'float' }, sortable: true, render: 'int', resizable: true, style: 'text-align:right' },
|
||||
|
||||
{ field: 'remarks', text: '비고', size: '200px', editable: { type: 'text' }, resizable: true, sortable: true },
|
||||
|
||||
{ field: 'created_at', text: '등록일', size: '160px', resizable: true, sortable: true }
|
||||
],
|
||||
|
||||
toolbar: {
|
||||
items: [
|
||||
{ id: 'add', type: 'button', text: '추가', icon: 'w2ui-icon-plus' },
|
||||
{ id: 'delete', type: 'button', text: '삭제', icon: 'w2ui-icon-cross' }
|
||||
],
|
||||
|
||||
onClick(event) {
|
||||
const g = this.owner;
|
||||
|
||||
if (event.target === 'w2ui-reload') {
|
||||
loadResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === 'add') {
|
||||
const newId = "new-" + Math.random().toString(36).substr(2, 9);
|
||||
g.add({
|
||||
recid: newId,
|
||||
sales_date: "",
|
||||
emp_no: "",
|
||||
client_code: "",
|
||||
product_code: "",
|
||||
quantity: 0,
|
||||
unit_price: 0,
|
||||
discount: 0,
|
||||
total_amount: 0,
|
||||
remarks: "",
|
||||
created_at: ""
|
||||
});
|
||||
g.refresh();
|
||||
updateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target === 'delete') {
|
||||
|
||||
const sel = g.getSelection();
|
||||
if (!sel.length) return;
|
||||
|
||||
if (!confirm("정말 삭제하시겠습니까?")) return;
|
||||
|
||||
sel.forEach(id => {
|
||||
|
||||
// 신규(new-xxx)는 DB 삭제 X
|
||||
if (String(id).startsWith("new-")) {
|
||||
g.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/egbim/bbs/sales_results.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "delete",
|
||||
seq_no: id
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
console.log("삭제 응답:", json);
|
||||
})
|
||||
.catch(err => console.error("삭제 오류:", err));
|
||||
|
||||
g.remove(id);
|
||||
});
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onChange(event) {
|
||||
event.onComplete = function(ev) {
|
||||
let rec = grid.get(ev.recid);
|
||||
if (!rec) return;
|
||||
|
||||
let field = grid.columns[ev.column].field;
|
||||
let val = ev.value_new;
|
||||
|
||||
// combo object → text 강제 변환
|
||||
if (typeof val === "object" && val !== null) {
|
||||
val = val.text;
|
||||
}
|
||||
|
||||
grid.set(ev.recid, { [field]: val });
|
||||
|
||||
// 계산 필드 자동 업데이트
|
||||
if (field === 'quantity' || field === 'unit_price' || field === 'discount') {
|
||||
let qty = parseInt(rec.quantity) || 0;
|
||||
let unit = parseInt(rec.unit_price) || 0;
|
||||
let dc = parseInt(rec.discount) || 0;
|
||||
|
||||
let total = Math.max(0, qty * unit - dc);
|
||||
grid.set(ev.recid, { total_amount: total });
|
||||
}
|
||||
|
||||
grid.refresh();
|
||||
};
|
||||
},
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔥 저장 처리 (insert + update)
|
||||
------------------------------------------------------ */
|
||||
onSave(event) {
|
||||
const changes = this.getChanges();
|
||||
if (!changes.length) return;
|
||||
|
||||
Promise.all(
|
||||
changes.map(ch => {
|
||||
|
||||
let rec = this.get(ch.recid); // 기존 record 전체 값
|
||||
|
||||
|
||||
/* -------------------------------------------
|
||||
🔵 1) 영업담당자(emp_no) 유효성 검사 및 변환
|
||||
------------------------------------------- */
|
||||
|
||||
// raw 값 가져오기 (object, string 모두 처리)
|
||||
let empRaw = ch.emp_no ?? rec.emp_no;
|
||||
console.log('1111333');
|
||||
|
||||
// 콤보일 경우 object → text 로 변환 ("홍길동 (01201)")
|
||||
if (typeof empRaw === "object" && empRaw !== null) {
|
||||
empRaw = empRaw.text;
|
||||
console.log('1111222');
|
||||
}
|
||||
|
||||
// 빈 값은 허용
|
||||
if (empRaw !== "") {
|
||||
console.log('1111444');
|
||||
// "(사번)" 추출
|
||||
let match = empRaw.match(/\((.*?)\)/);
|
||||
if (!match) {
|
||||
console.log('11115');
|
||||
alert("영업담당자 형식이 잘못되었습니다. 예: 홍길동 (01201)");
|
||||
throw "invalid emp_no format";
|
||||
}
|
||||
|
||||
let extractedEmpNo = match[1];
|
||||
|
||||
// 사번 유효성 체크
|
||||
let validEmpList = employeeList.map(e => e.id);
|
||||
if (!validEmpList.includes(extractedEmpNo)) {
|
||||
console.log('11116');
|
||||
alert("영업담당자는 목록에 있는 사람만 선택할 수 있습니다.");
|
||||
throw "invalid emp_no";
|
||||
}
|
||||
}
|
||||
|
||||
// ✔ 최종 DB에는 "홍길동 (01201)" 그대로 저장
|
||||
ch.emp_no = empRaw;
|
||||
|
||||
/* -------------------------------------------
|
||||
🔵 2) 거래처(client_code) 유효성 검사
|
||||
------------------------------------------- */
|
||||
let client = normalizeListValue(ch.client_code ?? rec.client_code);
|
||||
let validClientList = clientList.map(c => c.id);
|
||||
|
||||
let clientRaw = ch.client_code ?? rec.client_code;
|
||||
|
||||
if (typeof clientRaw === "object" && clientRaw !== null) {
|
||||
clientRaw = clientRaw.text;
|
||||
}
|
||||
|
||||
if (clientRaw !== "") {
|
||||
let match = clientRaw.match(/\(([^()]*)\)(?!.*\([^()]*\))/);
|
||||
if (!match) {
|
||||
alert("거래처명 형식이 잘못되었습니다. 예: 현대건설 (C002)");
|
||||
throw "invalid_client_code_format";
|
||||
}
|
||||
let code = match[1];
|
||||
|
||||
let validList = clientList.map(c => c.id);
|
||||
if (!validList.includes(code)) {
|
||||
alert("거래처명은 목록에서 선택해야 합니다.");
|
||||
throw "invalid_client_code";
|
||||
}
|
||||
}
|
||||
|
||||
ch.client_code = clientRaw;
|
||||
|
||||
/* -------------------------------------------
|
||||
🔵 3) 제품명(product_code) 유효성 검사
|
||||
------------------------------------------- */
|
||||
let product = normalizeListValue(ch.product_code ?? rec.product_code);
|
||||
let validProductList = productList.map(p => p.id);
|
||||
|
||||
let productRaw = ch.product_code ?? rec.product_code;
|
||||
|
||||
if (typeof productRaw === "object" && productRaw !== null) {
|
||||
productRaw = productRaw.text;
|
||||
}
|
||||
|
||||
if (productRaw !== "") {
|
||||
let match = productRaw.match(/\((.*?)\)$/);
|
||||
if (!match) {
|
||||
alert("제품명 형식이 잘못되었습니다. 예: EG-BIM (P001)");
|
||||
throw "invalid_product_code_format";
|
||||
}
|
||||
|
||||
let code = match[1];
|
||||
let validList = productList.map(p => p.id);
|
||||
|
||||
if (!validList.includes(code)) {
|
||||
alert("제품명은 목록에서 선택해야 합니다.");
|
||||
throw "invalid_product_code";
|
||||
}
|
||||
}
|
||||
|
||||
ch.product_code = productRaw;
|
||||
|
||||
let body = {
|
||||
action: String(ch.recid).startsWith("new-") ? "insert" : "update",
|
||||
seq_no: ch.recid,
|
||||
|
||||
sales_date: clean(ch.sales_date ?? rec.sales_date),
|
||||
emp_no: ch.emp_no,
|
||||
client_code: ch.client_code,
|
||||
product_code: ch.product_code,
|
||||
|
||||
quantity: cleanNumber(ch.quantity ?? rec.quantity),
|
||||
unit_price: cleanNumber(ch.unit_price ?? rec.unit_price),
|
||||
discount: cleanNumber(ch.discount ?? rec.discount),
|
||||
total_amount: cleanNumber(ch.total_amount ?? rec.total_amount),
|
||||
remarks: clean(ch.remarks ?? rec.remarks)
|
||||
};
|
||||
|
||||
return fetch("/egbim/bbs/sales_results.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(body)
|
||||
});
|
||||
|
||||
})
|
||||
).then(() => {
|
||||
loadResults();
|
||||
alert("저장되었습니다!");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------
|
||||
🔵 데이터 로드 + 합계
|
||||
------------------------------------------------------ */
|
||||
function loadResults() {
|
||||
fetch("/egbim/bbs/sales_results.php?action=list")
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.status === "ok") {
|
||||
grid.records = res.records.map(r => ({
|
||||
recid: r.seq_no,
|
||||
...r
|
||||
}));
|
||||
updateSummary();
|
||||
grid.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
loadResults();
|
||||
|
||||
function updateSummary() {
|
||||
let sumQty = 0, sumUnit = 0, sumDiscount = 0, sumTotal = 0;
|
||||
|
||||
grid.records.forEach(r => {
|
||||
sumQty += Number(r.quantity || 0);
|
||||
sumUnit += Number(r.unit_price || 0);
|
||||
sumDiscount += Number(r.discount || 0);
|
||||
sumTotal += Number(r.total_amount || 0);
|
||||
});
|
||||
|
||||
grid.summary = [
|
||||
{
|
||||
recid: 'summary',
|
||||
sales_date: '<span style="float:right;font-weight:bold;">합계</span>',
|
||||
quantity: sumQty,
|
||||
unit_price: sumUnit,
|
||||
discount: sumDiscount,
|
||||
total_amount: sumTotal,
|
||||
w2ui: { summary: true }
|
||||
}
|
||||
];
|
||||
|
||||
grid.refresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<?php sales_layout_end(); ?>
|
||||
Reference in New Issue
Block a user