최초 커밋
This commit is contained in:
@@ -0,0 +1,602 @@
|
||||
<?php
|
||||
include __DIR__ . '/layout_sales.php';
|
||||
sales_layout_start("영업 스케줄표");
|
||||
?>
|
||||
|
||||
<!-- SweetAlert2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<h2 class="text-2xl font-bold mb-3">영업 스케줄표</h2>
|
||||
|
||||
<!-- 월 선택 -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<select id="sel-year" class="border p-1">
|
||||
<?php
|
||||
for ($y=date("Y")-1; $y<=date("Y")+2; $y++) {
|
||||
echo "<option value='$y' ".($y==date("Y")?"selected":"").">$y 년</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<select id="sel-month" class="border p-1">
|
||||
<?php
|
||||
for ($m=1;$m<=12;$m++) {
|
||||
$mm = sprintf("%02d",$m);
|
||||
echo "<option value='$mm' ".($mm==date("m")?"selected":"").">$mm 월</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<!-- <button id="btn-load" class="bg-blue-600 text-white px-3 py-1 rounded">
|
||||
불러오기
|
||||
</button> -->
|
||||
</div>
|
||||
|
||||
<div id="month-container"></div>
|
||||
|
||||
<!-- ⭐ 주요 이슈 사항 영역 -->
|
||||
<div id="issue-box" class="mt-10 hidden">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 id="issue-title" class="text-xl font-bold">📌 주요 이슈 사항</h3>
|
||||
<button id="btn-save-issue"
|
||||
class="px-3 py-1 bg-amber-600 text-white rounded hover:bg-amber-700">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea id="issue-textarea"
|
||||
class="w-full border border-gray-300 rounded p-3"
|
||||
placeholder="이번 달 주요 이슈를 입력하세요."
|
||||
style="height:150px;"></textarea>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allSchedules = [];
|
||||
let employees = [];
|
||||
let weekGroups = [];
|
||||
let clientList = [];
|
||||
|
||||
/* ===============================
|
||||
거래처 로드
|
||||
=============================== */
|
||||
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})`
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
초기 로드
|
||||
=============================== */
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
await loadClients();
|
||||
loadMonth();
|
||||
});
|
||||
|
||||
/* ===============================
|
||||
주요 이슈 로드
|
||||
=============================== */
|
||||
async function loadIssue(year, month) {
|
||||
const ym = `${year}-${month}`;
|
||||
|
||||
try {
|
||||
let res = await fetch(`/egbim/bbs/sales_issue.php?action=get&issue_month=${ym}`);
|
||||
let json = await res.json();
|
||||
|
||||
document.getElementById("issue-title").innerText = `📌 ${month}월 주요 이슈 사항`;
|
||||
document.getElementById("issue-textarea").value = json.issue_text ?? "";
|
||||
} catch (e) {
|
||||
document.getElementById("issue-textarea").value = "";
|
||||
}
|
||||
|
||||
document.getElementById("issue-box").classList.remove("hidden");
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
주요 이슈 저장
|
||||
=============================== */
|
||||
document.getElementById("btn-save-issue").onclick = async function () {
|
||||
const year = document.getElementById("sel-year").value;
|
||||
const month = document.getElementById("sel-month").value;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_issue.php", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/x-www-form-urlencoded"},
|
||||
body: new URLSearchParams({
|
||||
action: "save",
|
||||
issue_month: `${year}-${month}`,
|
||||
issue_text: document.getElementById("issue-textarea").value
|
||||
})
|
||||
});
|
||||
|
||||
let json = await res.json();
|
||||
if (json.success) Swal.fire("저장 완료!", "", "success");
|
||||
};
|
||||
|
||||
/* ===============================
|
||||
평일 기준 주차 생성
|
||||
=============================== */
|
||||
function generateWeeks(year, month) {
|
||||
const weeks = [];
|
||||
const firstDay = new Date(year, month - 1, 1);
|
||||
const lastDay = new Date(year, month, 0);
|
||||
|
||||
let currentWeek = [];
|
||||
let weekNo = 1;
|
||||
|
||||
for (let d = 1; d <= lastDay.getDate(); d++) {
|
||||
const date = new Date(year, month - 1, d);
|
||||
const dow = date.getDay(); // 0=일, 1=월, ... 6=토
|
||||
|
||||
// 월요일이고, 이전 주차에 데이터 있으면 새 주차 시작
|
||||
if (dow === 1 && currentWeek.length) {
|
||||
weeks.push({
|
||||
weekNo: weekNo++,
|
||||
days: currentWeek
|
||||
});
|
||||
currentWeek = [];
|
||||
}
|
||||
|
||||
// 평일만 포함
|
||||
if (dow >= 1 && dow <= 5) {
|
||||
currentWeek.push({
|
||||
day: d,
|
||||
dow: ["","월","화","수","목","금"][dow],
|
||||
full: `${year}-${String(month).padStart(2,"0")}-${String(d).padStart(2,"0")}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 주차 push
|
||||
if (currentWeek.length) {
|
||||
weeks.push({
|
||||
weekNo: weekNo,
|
||||
days: currentWeek
|
||||
});
|
||||
}
|
||||
|
||||
return weeks;
|
||||
}
|
||||
|
||||
|
||||
/* ===============================
|
||||
현재 주차 index 계산
|
||||
=============================== */
|
||||
function getCurrentWeekIndex(year, month, weeks) {
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`;
|
||||
|
||||
if (`${year}-${month}` !== todayStr.slice(0,7)) return null;
|
||||
|
||||
for (let i = 0; i < weeks.length; i++) {
|
||||
if (weeks[i].days.some(d => d.full === todayStr)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
주차 재정렬
|
||||
=============================== */
|
||||
function reorderWeeks(weeks, currentIdx) {
|
||||
if (currentIdx === null) return weeks;
|
||||
|
||||
const current = weeks[currentIdx];
|
||||
const before = weeks.slice(0, currentIdx);
|
||||
const after = weeks.slice(currentIdx + 1);
|
||||
|
||||
return [current, ...before, ...after];
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
월 전체 로드
|
||||
=============================== */
|
||||
async function loadMonth() {
|
||||
let year = document.getElementById("sel-year").value;
|
||||
let month = document.getElementById("sel-month").value;
|
||||
|
||||
let res = await fetch(`/egbim/bbs/sales_schedules.php?action=month&year=${year}&month=${month}`);
|
||||
let json = await res.json();
|
||||
|
||||
employees = json.employees;
|
||||
allSchedules = json.schedules;
|
||||
|
||||
weekGroups = generateWeeks(year, month);
|
||||
|
||||
const idx = getCurrentWeekIndex(year, month, weekGroups);
|
||||
|
||||
const currentWeekNo = getCurrentWeekNo(year, month, weekGroups);
|
||||
window.__CURRENT_WEEK_NO__ = currentWeekNo;
|
||||
|
||||
weekGroups = reorderWeeks(weekGroups, idx);
|
||||
|
||||
renderMonth();
|
||||
await loadIssue(year, month);
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
월 → 주차 렌더링
|
||||
=============================== */
|
||||
function renderMonth() {
|
||||
let container = document.getElementById("month-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
weekGroups.forEach(week => {
|
||||
let html = `
|
||||
<div class="mt-10 ${week.weekNo === window.__CURRENT_WEEK_NO__ ? 'week-current' : ''}">
|
||||
<div class="font-bold text-xl mb-3 week-title">
|
||||
${week.weekNo}주차 : ${week.days[0].day}일 ~ ${week.days.at(-1).day}일
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow border overflow-hidden">
|
||||
<table class="w-full text-sm sales-schedule-table">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="border-r px-3 py-2 w-28 text-center">담당자</th>
|
||||
${week.days.map(d => `
|
||||
<th class="border-r px-3 py-2 text-center">
|
||||
${d.day}일(${d.dow})
|
||||
</th>
|
||||
`).join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${employees.map(emp => `
|
||||
<tr>
|
||||
<td class="border px-3 py-2 text-center bg-gray-50">${emp.emp_name}</td>
|
||||
${week.days.map(d => {
|
||||
let items = allSchedules.filter(s =>
|
||||
s.emp_no == emp.emp_no && s.schedule_date == d.full
|
||||
);
|
||||
return `
|
||||
<td class="border px-2 py-2 align-top cursor-pointer"
|
||||
onclick="openSchedulePopup('${emp.emp_no}','${emp.emp_name}','${d.full}')">
|
||||
${
|
||||
items.length
|
||||
? items.map(it => `
|
||||
<div class="mb-1 p-2 border rounded bg-gray-50">
|
||||
<div class="font-medium">${it.content}</div>
|
||||
<div class="text-xs text-gray-500">${it.client_code}</div>
|
||||
<span class="text-xs px-2 py-0.5 rounded ${
|
||||
it.status === '완료'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-yellow-100 text-yellow-700'
|
||||
}">${it.status}</span>
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div class="text-xs text-gray-300">(없음)</div>`
|
||||
}
|
||||
</td>
|
||||
`;
|
||||
}).join("")}
|
||||
</tr>
|
||||
`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML("beforeend", html);
|
||||
});
|
||||
}
|
||||
|
||||
//현재 주차 번호 구하기
|
||||
function getCurrentWeekNo(year, month, weeks) {
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,"0")}-${String(today.getDate()).padStart(2,"0")}`;
|
||||
|
||||
if (`${year}-${month}` !== todayStr.slice(0,7)) return null;
|
||||
|
||||
for (let w of weeks) {
|
||||
if (w.days.some(d => d.full === todayStr)) {
|
||||
return w.weekNo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================
|
||||
일정 리스트 팝업 (SweetAlert2)
|
||||
========================================================= */
|
||||
function openSchedulePopup(emp_no, emp_name, date) {
|
||||
|
||||
let items = allSchedules.filter(
|
||||
s => s.emp_no == emp_no && s.schedule_date == date
|
||||
);
|
||||
|
||||
const listHtml = items.length
|
||||
? items.map(it => `
|
||||
<div class="swal-schedule-card">
|
||||
<div class="swal-schedule-content">
|
||||
${it.content}
|
||||
</div>
|
||||
<div class="swal-schedule-meta">
|
||||
${it.client_code} · ${it.time_period}
|
||||
<span class="ml-2 px-2 py-0.5 rounded text-xs ${
|
||||
it.status === '완료'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-yellow-100 text-yellow-700'
|
||||
}">${it.status}</span>
|
||||
</div>
|
||||
|
||||
<div class="swal-schedule-actions">
|
||||
<button onclick="editSchedule(${it.seq_no})"
|
||||
class="px-2 py-1 text-xs rounded bg-amber-400 text-white">
|
||||
수정
|
||||
</button>
|
||||
<button onclick="deleteSchedule(${it.seq_no})"
|
||||
class="px-2 py-1 text-xs rounded bg-red-500 text-white">
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("")
|
||||
: `<div style="color:#9ca3af;">등록된 일정이 없습니다.</div>`;
|
||||
|
||||
Swal.fire({
|
||||
width: 480,
|
||||
showConfirmButton: false,
|
||||
html: `
|
||||
<div class="swal-schedule-header">
|
||||
<div>
|
||||
<div class="swal-schedule-title">${date}</div>
|
||||
<div class="swal-schedule-sub">${emp_name}</div>
|
||||
</div>
|
||||
|
||||
<button onclick="addSchedule('${emp_no}','${date}')"
|
||||
class="px-3 py-1 text-sm rounded bg-indigo-500 text-white">
|
||||
+ 일정 추가
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="swal-schedule-list">
|
||||
${listHtml}
|
||||
</div>
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* =========================================================
|
||||
신규 일정 추가
|
||||
========================================================= */
|
||||
window.addSchedule = function(emp_no, date) {
|
||||
|
||||
let clientOptions = clientList
|
||||
.map(c => `<option value="${c.id}">${c.text}</option>`)
|
||||
.join("");
|
||||
|
||||
Swal.fire({
|
||||
title: "신규 일정 추가",
|
||||
html: `
|
||||
<div class="swal-form">
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">시간대</label>
|
||||
<div class="swal-radio">
|
||||
<label><input type="radio" name="s_period" value="오전" checked> 오전</label>
|
||||
<label><input type="radio" name="s_period" value="오후"> 오후</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">상태</label>
|
||||
<div class="swal-radio">
|
||||
<label><input type="radio" name="s_status" value="예정" checked> 예정</label>
|
||||
<label><input type="radio" name="s_status" value="완료"> 완료</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">거래처</label>
|
||||
<select id="s-client" class="swal-select">
|
||||
<option value="">선택하세요</option>
|
||||
${clientOptions}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">내용</label>
|
||||
<textarea id="s-content"
|
||||
class="swal-textarea"
|
||||
placeholder="내용을 입력하세요"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "등록",
|
||||
cancelButtonText: "취소",
|
||||
width: 420
|
||||
}).then(async result => {
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
let status = document.querySelector('input[name="s_status"]:checked').value;
|
||||
let period = document.querySelector('input[name="s_period"]:checked').value;
|
||||
let client_code = document.getElementById("s-client").value;
|
||||
let content = document.getElementById("s-content").value;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "insert",
|
||||
emp_no,
|
||||
schedule_date: date,
|
||||
time_period: period,
|
||||
client_code,
|
||||
content,
|
||||
status
|
||||
})
|
||||
});
|
||||
|
||||
let j = await res.json();
|
||||
if (j.status === "ok") {
|
||||
Swal.close();
|
||||
loadMonth();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/* =========================================================
|
||||
일정 수정
|
||||
========================================================= */
|
||||
window.editSchedule = function(seq_no) {
|
||||
|
||||
let item = allSchedules.find(s => s.seq_no == seq_no);
|
||||
|
||||
let clientOptions = clientList
|
||||
.map(c => `
|
||||
<option value="${c.id}" ${c.id == item.client_code ? "selected" : ""}>
|
||||
${c.text}
|
||||
</option>
|
||||
`)
|
||||
.join("");
|
||||
|
||||
Swal.fire({
|
||||
title: "일정 수정",
|
||||
html: `
|
||||
<div class="swal-form">
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">시간대</label>
|
||||
<div class="swal-radio">
|
||||
<label>
|
||||
<input type="radio" name="e_period" value="오전"
|
||||
${item.time_period === "오전" ? "checked" : ""}>
|
||||
오전
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="e_period" value="오후"
|
||||
${item.time_period === "오후" ? "checked" : ""}>
|
||||
오후
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">상태</label>
|
||||
<div class="swal-radio">
|
||||
<label>
|
||||
<input type="radio" name="e_status" value="예정"
|
||||
${item.status === "예정" ? "checked" : ""}>
|
||||
예정
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="e_status" value="완료"
|
||||
${item.status === "완료" ? "checked" : ""}>
|
||||
완료
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">거래처</label>
|
||||
<select id="e-client" class="swal-select">
|
||||
<option value="">선택하세요</option>
|
||||
${clientOptions}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="swal-group">
|
||||
<label class="title">내용</label>
|
||||
<textarea id="e-content"
|
||||
class="swal-textarea"
|
||||
placeholder="내용을 입력하세요">${item.content}</textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "저장",
|
||||
cancelButtonText: "취소",
|
||||
width: 420
|
||||
}).then(async result => {
|
||||
|
||||
if (!result.isConfirmed) return;
|
||||
|
||||
let status = document.querySelector('input[name="e_status"]:checked').value;
|
||||
let period = document.querySelector('input[name="e_period"]:checked').value;
|
||||
let client_code = document.getElementById("e-client").value;
|
||||
let content = document.getElementById("e-content").value;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
action: "update",
|
||||
seq_no,
|
||||
emp_no: item.emp_no,
|
||||
schedule_date: item.schedule_date,
|
||||
time_period: period,
|
||||
client_code,
|
||||
content,
|
||||
status
|
||||
})
|
||||
});
|
||||
|
||||
let j = await res.json();
|
||||
if (j.status === "ok") {
|
||||
Swal.close();
|
||||
loadMonth();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/* =========================================================
|
||||
일정 삭제
|
||||
========================================================= */
|
||||
window.deleteSchedule = function(seq_no) {
|
||||
|
||||
Swal.fire({
|
||||
title:"삭제하시겠습니까?",
|
||||
icon:"warning",
|
||||
showCancelButton:true,
|
||||
confirmButtonText:"삭제"
|
||||
}).then(async r=>{
|
||||
if(!r.isConfirmed) return;
|
||||
|
||||
let res = await fetch("/egbim/bbs/sales_schedules.php",{
|
||||
method:"POST",
|
||||
headers:{"Content-Type":"application/x-www-form-urlencoded"},
|
||||
body:new URLSearchParams({
|
||||
action:"delete",
|
||||
seq_no
|
||||
})
|
||||
});
|
||||
|
||||
let j = await res.json();
|
||||
if(j.status==="ok"){
|
||||
Swal.close();
|
||||
loadMonth();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//document.getElementById("btn-load").onclick = loadMonth;
|
||||
loadMonth();
|
||||
|
||||
/* ===============================
|
||||
연도 / 월 변경 시 자동 로드
|
||||
=============================== */
|
||||
document.getElementById("sel-year").addEventListener("change", () => {
|
||||
loadMonth();
|
||||
});
|
||||
|
||||
document.getElementById("sel-month").addEventListener("change", () => {
|
||||
loadMonth();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php sales_layout_end(); ?>
|
||||
Reference in New Issue
Block a user