922 lines
37 KiB
HTML
922 lines
37 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}복리/접대비 분류{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set filters = benefit_report.filters %}
|
|
<style>
|
|
.report-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-end;
|
|
gap: 18px;
|
|
padding-bottom: 14px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.report-head h1 {
|
|
font-size: 26px;
|
|
line-height: 1.2;
|
|
}
|
|
.report-head p {
|
|
margin-top: 6px;
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
}
|
|
.report-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
}
|
|
.report-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 38px;
|
|
padding: 0 14px;
|
|
border: 1px solid #111;
|
|
background: #111;
|
|
color: #fff;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
white-space: nowrap;
|
|
}
|
|
.report-filter {
|
|
display: grid;
|
|
grid-template-columns: 82px 82px 136px 164px 150px 180px 62px;
|
|
gap: 10px;
|
|
align-items: end;
|
|
padding: 14px 0;
|
|
border-bottom: 1px solid var(--line);
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
.report-filter .field {
|
|
min-width: 0;
|
|
}
|
|
.report-filter input,
|
|
.report-filter select {
|
|
width: 100%;
|
|
min-height: 38px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
padding: 0 10px;
|
|
}
|
|
.report-filter button {
|
|
min-height: 38px;
|
|
border-radius: 8px;
|
|
border: 1px solid #111;
|
|
background: #111;
|
|
color: #fff;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
}
|
|
.report-kpis {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
gap: 14px;
|
|
padding: 16px 0 4px;
|
|
}
|
|
.report-kpi {
|
|
border-bottom: 1px solid var(--line);
|
|
padding-bottom: 12px;
|
|
}
|
|
.report-kpi span {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
}
|
|
.report-kpi strong {
|
|
display: block;
|
|
margin-top: 6px;
|
|
font-size: 24px;
|
|
line-height: 1.15;
|
|
}
|
|
.report-toggle-kpi {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: flex-end;
|
|
border-bottom: 1px solid var(--line);
|
|
padding-bottom: 12px;
|
|
}
|
|
.report-toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 30px;
|
|
padding: 0 10px;
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
color: #111;
|
|
border-radius: 0;
|
|
text-decoration: none;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
white-space: nowrap;
|
|
}
|
|
.report-toggle.is-on {
|
|
border-color: #111;
|
|
background: #111;
|
|
color: #fff;
|
|
}
|
|
.report-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(320px, 0.88fr) minmax(520px, 1.12fr);
|
|
gap: 18px;
|
|
align-items: start;
|
|
margin-top: 18px;
|
|
}
|
|
.report-summary-band {
|
|
margin-top: 16px;
|
|
}
|
|
.report-section h2 {
|
|
font-size: 17px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.report-table-wrap {
|
|
overflow: auto;
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
.report-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 12px;
|
|
line-height: 1.15;
|
|
min-width: 860px;
|
|
}
|
|
.report-table th {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1;
|
|
background: #f5f6f8;
|
|
color: #4b5563;
|
|
text-align: left;
|
|
font-size: 11px;
|
|
padding: 4px 5px;
|
|
border-bottom: 1px solid var(--line);
|
|
white-space: nowrap;
|
|
}
|
|
.report-table td {
|
|
padding: 2px 5px;
|
|
border-bottom: 1px solid #eceff3;
|
|
vertical-align: middle;
|
|
}
|
|
.report-table .number {
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.report-table .desc {
|
|
min-width: 260px;
|
|
line-height: 1.25;
|
|
white-space: nowrap;
|
|
}
|
|
.report-table .counterpart {
|
|
min-width: 300px;
|
|
max-width: 420px;
|
|
line-height: 1.25;
|
|
white-space: nowrap;
|
|
position: relative;
|
|
}
|
|
.counterpart-text {
|
|
display: block;
|
|
max-width: 420px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.counterpart-hover-tooltip {
|
|
position: fixed;
|
|
display: none;
|
|
z-index: 1400;
|
|
min-width: 520px;
|
|
max-width: min(820px, 72vw);
|
|
padding: 8px 10px;
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
color: #111;
|
|
white-space: pre-line;
|
|
line-height: 1.45;
|
|
box-shadow: none;
|
|
pointer-events: none;
|
|
}
|
|
.counterpart-hover-tooltip.is-open {
|
|
display: block;
|
|
}
|
|
#voucherDetailBody td {
|
|
white-space: nowrap;
|
|
}
|
|
.report-table button.link-cell {
|
|
all: unset;
|
|
display: inline;
|
|
border: 0 !important;
|
|
background: transparent !important;
|
|
box-shadow: none !important;
|
|
color: #111;
|
|
font: inherit;
|
|
font-weight: inherit;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
padding: 0 !important;
|
|
margin: 0;
|
|
min-height: 0;
|
|
line-height: inherit;
|
|
border-radius: 0 !important;
|
|
}
|
|
.report-table button.link-cell:hover,
|
|
.report-table button.link-cell:focus,
|
|
.report-table button.link-cell:active {
|
|
background: transparent !important;
|
|
box-shadow: none !important;
|
|
text-decoration: none !important;
|
|
outline: none;
|
|
}
|
|
.summary-account-group {
|
|
display: -webkit-box;
|
|
max-width: 58px;
|
|
overflow: hidden;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
line-height: 1.15;
|
|
word-break: keep-all;
|
|
}
|
|
.summary-person-cell,
|
|
.summary-person-cell .link-cell {
|
|
display: -webkit-box !important;
|
|
max-width: 128px;
|
|
overflow: hidden;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
line-height: 1.15;
|
|
word-break: keep-all;
|
|
}
|
|
.category-save-cell {
|
|
display: block;
|
|
min-width: 138px;
|
|
}
|
|
.category-save-cell select {
|
|
min-height: 20px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 0;
|
|
background: #fff;
|
|
font-size: 12px;
|
|
padding: 0 3px;
|
|
width: 100%;
|
|
}
|
|
.report-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 0;
|
|
padding: 0;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
font-weight: 800;
|
|
white-space: nowrap;
|
|
}
|
|
.report-muted {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
.detail-more-row {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding-top: 10px;
|
|
}
|
|
.detail-more-row button {
|
|
min-height: 30px;
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
border-radius: 0;
|
|
padding: 0 12px;
|
|
cursor: pointer;
|
|
font-weight: 800;
|
|
}
|
|
.modal-detail-toggle {
|
|
margin-top: 12px;
|
|
min-height: 30px;
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
border-radius: 0;
|
|
padding: 0 12px;
|
|
cursor: pointer;
|
|
font-weight: 800;
|
|
}
|
|
.modal-detail-panel {
|
|
display: none;
|
|
margin-top: 10px;
|
|
}
|
|
.modal-detail-panel.is-open {
|
|
display: block;
|
|
}
|
|
.report-modal-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(17, 24, 39, 0.42);
|
|
z-index: 1000;
|
|
padding: 20px;
|
|
}
|
|
.report-modal-backdrop.is-open {
|
|
display: flex;
|
|
}
|
|
.report-modal {
|
|
width: min(1680px, calc(100vw - 24px));
|
|
max-height: min(860px, calc(100vh - 24px));
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
box-shadow: none;
|
|
display: grid;
|
|
grid-template-rows: auto 1fr;
|
|
overflow: hidden;
|
|
}
|
|
.report-modal-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--line);
|
|
}
|
|
.report-modal-head strong {
|
|
font-size: 16px;
|
|
}
|
|
.report-modal-head button {
|
|
border: 1px solid var(--line);
|
|
background: #fff;
|
|
border-radius: 7px;
|
|
min-height: 32px;
|
|
padding: 0 10px;
|
|
cursor: pointer;
|
|
font-weight: 800;
|
|
}
|
|
.report-modal-body {
|
|
overflow: auto;
|
|
padding: 12px 16px 16px;
|
|
}
|
|
.report-modal .report-table {
|
|
min-width: 1680px;
|
|
}
|
|
.report-modal .report-table th,
|
|
.report-modal .report-table td {
|
|
white-space: nowrap;
|
|
}
|
|
.report-modal .report-table .desc {
|
|
min-width: 460px;
|
|
white-space: nowrap;
|
|
}
|
|
.report-modal .report-table .counterpart {
|
|
min-width: 360px;
|
|
white-space: nowrap;
|
|
}
|
|
.report-modal .counterpart-text {
|
|
max-width: 560px;
|
|
}
|
|
@media (max-width: 980px) {
|
|
.report-head,
|
|
.report-grid {
|
|
display: grid;
|
|
}
|
|
.report-kpis {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<section class="panel">
|
|
<div class="report-head">
|
|
<div>
|
|
<h1>복리후생비/접대비 분류</h1>
|
|
<p>{{ filters.start_year }}년부터 {{ filters.end_year }}년까지 WEHAGO 전표 기준</p>
|
|
</div>
|
|
<div class="report-actions">
|
|
<a class="report-button" href="/wehago-benefit-entertainment/export?start_year={{ filters.start_year }}&end_year={{ filters.end_year }}&account_group={{ filters.account_group|urlencode }}&category={{ filters.category|urlencode }}&person_keyword={{ filters.person_keyword|urlencode }}&desc_keyword={{ filters.desc_keyword|urlencode }}&include_adjustments={{ '1' if filters.include_adjustments else '0' }}">엑셀 다운로드</a>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="report-filter" method="get" action="/wehago-benefit-entertainment">
|
|
<div class="field">
|
|
<label for="start_year">시작연도</label>
|
|
<input id="start_year" name="start_year" value="{{ filters.start_year }}" inputmode="numeric">
|
|
</div>
|
|
<div class="field">
|
|
<label for="end_year">종료연도</label>
|
|
<input id="end_year" name="end_year" value="{{ filters.end_year }}" inputmode="numeric">
|
|
</div>
|
|
<div class="field">
|
|
<label for="account_group">구분</label>
|
|
<select id="account_group" name="account_group">
|
|
<option value="all" {% if filters.account_group in ['all', ''] %}selected{% endif %}>전체</option>
|
|
{% for option in benefit_report.account_group_options %}
|
|
<option value="{{ option }}" {% if filters.account_group == option %}selected{% endif %}>{{ option }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="category">분류</label>
|
|
<select id="category" name="category">
|
|
<option value="all" {% if filters.category in ['all', ''] %}selected{% endif %}>전체</option>
|
|
{% for option in benefit_report.category_options %}
|
|
<option value="{{ option }}" {% if filters.category == option %}selected{% endif %}>{{ option }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="person_keyword">개인/거래처</label>
|
|
<input id="person_keyword" name="person_keyword" value="{{ filters.person_keyword }}" autocomplete="off">
|
|
</div>
|
|
<div class="field">
|
|
<label for="desc_keyword">적요</label>
|
|
<input id="desc_keyword" name="desc_keyword" value="{{ filters.desc_keyword }}" autocomplete="off">
|
|
</div>
|
|
<input type="hidden" name="include_adjustments" value="{{ '1' if filters.include_adjustments else '0' }}">
|
|
<button type="submit">검색</button>
|
|
</form>
|
|
|
|
<div class="report-kpis">
|
|
<div class="report-kpi"><span>총 금액</span><strong>{{ "{:,.0f}".format(benefit_report.total_amount) }}</strong></div>
|
|
<div class="report-kpi"><span>전표 행</span><strong>{{ "{:,}".format(benefit_report.row_count) }}</strong></div>
|
|
<div class="report-kpi"><span>거래처 요약</span><strong>{{ "{:,}".format(benefit_report.vendor_summary_rows|length) }}</strong></div>
|
|
<div class="report-kpi"><span>직급 확인</span><strong>{{ "{:,}".format(benefit_report.hanmac_grade_match_count) }}</strong></div>
|
|
<div class="report-kpi"><span>미리보기</span><strong>{{ "{:,}".format(benefit_report.shown_count) }}</strong></div>
|
|
<div class="report-toggle-kpi">
|
|
<a class="report-toggle {% if filters.include_adjustments %}is-on{% endif %}" href="/wehago-benefit-entertainment?start_year={{ filters.start_year }}&end_year={{ filters.end_year }}&account_group={{ filters.account_group|urlencode }}&category={{ filters.category|urlencode }}&person_keyword={{ filters.person_keyword|urlencode }}&desc_keyword={{ filters.desc_keyword|urlencode }}&include_adjustments={{ '0' if filters.include_adjustments else '1' }}">
|
|
대체/원가/손익 {{ '포함' if filters.include_adjustments else '제외' }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<section class="report-section report-summary-band">
|
|
<h2>분류/연도별 합계</h2>
|
|
<div class="report-table-wrap">
|
|
<table class="report-table">
|
|
<thead>
|
|
<tr>
|
|
<th>구분</th>
|
|
<th>분류</th>
|
|
{% for year in benefit_report.summary_years %}
|
|
<th class="number">{{ year }}</th>
|
|
{% endfor %}
|
|
<th class="number">합계</th>
|
|
<th class="number">건수</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in benefit_report.category_year_summary_rows %}
|
|
{% set is_drill_category = (row.account_group == '복리후생비' and row.category in ['임원 개인성 비용', '기타', '경조사']) or (row.account_group == '접대비' and row.category in ['기타', '경조사', '운동비']) %}
|
|
<tr>
|
|
<td>{{ row.account_group }}</td>
|
|
<td><span class="report-pill">{{ row.category }}</span></td>
|
|
{% for year in benefit_report.summary_years %}
|
|
<td class="number">
|
|
{% if is_drill_category and row.year_amounts.get(year, 0) %}
|
|
<button type="button" class="link-cell" data-category-year-detail data-account-group="{{ row.account_group }}" data-category="{{ row.category }}" data-year="{{ year }}">{{ "{:,.0f}".format(row.year_amounts.get(year, 0)) }}</button>
|
|
{% else %}
|
|
{{ "{:,.0f}".format(row.year_amounts.get(year, 0)) }}
|
|
{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
<td class="number">
|
|
{% if is_drill_category and row.total_amount %}
|
|
<button type="button" class="link-cell" data-category-year-detail data-account-group="{{ row.account_group }}" data-category="{{ row.category }}" data-year="all">{{ "{:,.0f}".format(row.total_amount) }}</button>
|
|
{% else %}
|
|
{{ "{:,.0f}".format(row.total_amount) }}
|
|
{% endif %}
|
|
</td>
|
|
<td class="number">
|
|
{% if is_drill_category and row.row_count %}
|
|
<button type="button" class="link-cell" data-category-year-detail data-account-group="{{ row.account_group }}" data-category="{{ row.category }}" data-year="all">{{ "{:,}".format(row.row_count) }}</button>
|
|
{% else %}
|
|
{{ "{:,}".format(row.row_count) }}
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="{{ benefit_report.summary_years|length + 4 }}">조회된 자료가 없습니다.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="report-grid">
|
|
<section class="report-section">
|
|
<h2>거래처별 요약</h2>
|
|
<div class="report-table-wrap">
|
|
<table class="report-table">
|
|
<thead>
|
|
<tr>
|
|
<th>구분</th>
|
|
<th>분류</th>
|
|
<th>거래처</th>
|
|
<th>개인/귀속</th>
|
|
<th>직급</th>
|
|
<th class="number">금액</th>
|
|
<th class="number">건수</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for row in benefit_report.vendor_summary_rows %}
|
|
<tr>
|
|
<td><span class="summary-account-group">{{ row.account_group }}</span></td>
|
|
<td><span class="report-pill">{{ row.category }}</span></td>
|
|
<td>{{ row.vendor_name }}</td>
|
|
<td class="summary-person-cell"><button type="button" class="link-cell" data-summary-detail data-field="person" data-vendor="{{ row.vendor_name }}" data-category="{{ row.category }}" data-account-group="{{ row.account_group }}">{{ row.person_name or "-" }}</button></td>
|
|
<td>{{ row.hanmac_member_grade or "" }}</td>
|
|
<td class="number"><button type="button" class="link-cell" data-summary-detail data-field="amount" data-vendor="{{ row.vendor_name }}" data-category="{{ row.category }}" data-account-group="{{ row.account_group }}">{{ "{:,.0f}".format(row.amount) }}</button></td>
|
|
<td class="number"><button type="button" class="link-cell" data-summary-detail data-field="count" data-vendor="{{ row.vendor_name }}" data-category="{{ row.category }}" data-account-group="{{ row.account_group }}">{{ "{:,}".format(row.row_count) }}</button></td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="7">조회된 자료가 없습니다.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="report-section">
|
|
<h2>전표 상세</h2>
|
|
<div class="report-table-wrap">
|
|
<table class="report-table">
|
|
<thead>
|
|
<tr>
|
|
<th>연도</th>
|
|
<th>일자</th>
|
|
<th>전표번호</th>
|
|
<th>계정</th>
|
|
<th>거래처</th>
|
|
<th>직급</th>
|
|
<th>분류</th>
|
|
<th class="number">차변금액</th>
|
|
<th class="number">대변금액</th>
|
|
<th>적요</th>
|
|
<th>상대계정</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="voucherDetailBody">
|
|
{% for row in benefit_report.detail_rows %}
|
|
<tr data-detail-row>
|
|
<td>{{ row.year }}</td>
|
|
<td>{{ row.ledger_date }}</td>
|
|
<td>{{ row.voucher_no }}</td>
|
|
<td>{{ row.account_name }}</td>
|
|
<td>{{ row.vendor_name }}</td>
|
|
<td>{{ row.hanmac_member_grade or "" }}</td>
|
|
<td>
|
|
<div class="category-save-cell">
|
|
<select data-category-select data-row-id="{{ row.ledger_row_id }}">
|
|
{% for option in benefit_report.category_options %}
|
|
<option value="{{ option }}" {% if row.category == option %}selected{% endif %}>{{ option }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</td>
|
|
<td class="number">{{ "{:,.0f}".format(row.debit) }}</td>
|
|
<td class="number">{{ "{:,.0f}".format(row.credit) }}</td>
|
|
<td class="desc">{{ row.description }}</td>
|
|
<td class="counterpart" data-tooltip="{{ row.counterpart_tooltip }}"><span class="counterpart-text">{{ row.counterpart_details }}</span></td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="11">조회된 자료가 없습니다.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="detail-more-row" {% if benefit_report.row_count <= benefit_report.shown_count %}style="display:none"{% endif %}>
|
|
<button type="button" id="voucherDetailMore">더 보기</button>
|
|
</div>
|
|
<p class="report-muted" id="voucherDetailMeta">화면에는 {{ "{:,}".format(benefit_report.shown_count) }}행이 표시됩니다. 엑셀에는 현재 필터의 전체 {{ "{:,}".format(benefit_report.row_count) }}행이 포함됩니다.</p>
|
|
</section>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="report-modal-backdrop" id="summaryDetailModal" aria-hidden="true">
|
|
<div class="report-modal" role="dialog" aria-modal="true" aria-labelledby="summaryDetailTitle">
|
|
<div class="report-modal-head">
|
|
<strong id="summaryDetailTitle">상세</strong>
|
|
<button type="button" id="summaryDetailClose">닫기</button>
|
|
</div>
|
|
<div class="report-modal-body" id="summaryDetailBody"></div>
|
|
</div>
|
|
</div>
|
|
<div class="counterpart-hover-tooltip" id="counterpartHoverTooltip" aria-hidden="true"></div>
|
|
{% endblock %}
|
|
|
|
{% block script %}
|
|
<script>
|
|
(() => {
|
|
const reportData = {{ benefit_report_json|tojson }};
|
|
const modal = document.getElementById("summaryDetailModal");
|
|
const modalTitle = document.getElementById("summaryDetailTitle");
|
|
const modalBody = document.getElementById("summaryDetailBody");
|
|
const modalClose = document.getElementById("summaryDetailClose");
|
|
const detailBody = document.getElementById("voucherDetailBody");
|
|
const detailMore = document.getElementById("voucherDetailMore");
|
|
const detailMeta = document.getElementById("voucherDetailMeta");
|
|
const counterpartTooltip = document.getElementById("counterpartHoverTooltip");
|
|
const detailPageSize = 300;
|
|
const detailRows = reportData.detail_rows || [];
|
|
const categoryOptions = reportData.category_options || Array.from(document.querySelectorAll("[data-category-select] option")).map((option) => option.value);
|
|
let detailVisibleCount = document.querySelectorAll("[data-detail-row]").length;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
"\"": """,
|
|
"'": "'",
|
|
}[char]));
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
return Number(value || 0).toLocaleString("ko-KR", { maximumFractionDigits: 0 });
|
|
}
|
|
|
|
function displayVendor(value) {
|
|
const text = String(value || "").trim();
|
|
return text || "미지정";
|
|
}
|
|
|
|
function renderCategoryOptions(selectedCategory) {
|
|
return categoryOptions.map((option) => `
|
|
<option value="${escapeHtml(option)}" ${String(option) === String(selectedCategory || "") ? "selected" : ""}>${escapeHtml(option)}</option>
|
|
`).join("");
|
|
}
|
|
|
|
function renderDetailRow(row) {
|
|
return `
|
|
<tr data-detail-row>
|
|
<td>${escapeHtml(row.year)}</td>
|
|
<td>${escapeHtml(row.ledger_date)}</td>
|
|
<td>${escapeHtml(row.voucher_no)}</td>
|
|
<td>${escapeHtml(row.account_name)}</td>
|
|
<td>${escapeHtml(displayVendor(row.vendor_name))}</td>
|
|
<td>${escapeHtml(row.hanmac_member_grade || "")}</td>
|
|
<td>
|
|
<div class="category-save-cell">
|
|
<select data-category-select data-row-id="${escapeHtml(row.ledger_row_id)}">
|
|
${renderCategoryOptions(row.category)}
|
|
</select>
|
|
</div>
|
|
</td>
|
|
<td class="number">${formatNumber(row.debit)}</td>
|
|
<td class="number">${formatNumber(row.credit)}</td>
|
|
<td class="desc">${escapeHtml(row.description)}</td>
|
|
<td class="counterpart" data-tooltip="${escapeHtml(row.counterpart_tooltip || row.counterpart_details)}"><span class="counterpart-text">${escapeHtml(row.counterpart_details)}</span></td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
function updateDetailControls() {
|
|
const totalRows = Number(reportData.row_count || detailRows.length || 0);
|
|
if (detailMeta) {
|
|
detailMeta.textContent = `화면에는 ${detailVisibleCount.toLocaleString("ko-KR")}행이 표시됩니다. 엑셀에는 현재 필터의 전체 ${totalRows.toLocaleString("ko-KR")}행이 포함됩니다.`;
|
|
}
|
|
if (detailMore?.parentElement) {
|
|
detailMore.parentElement.style.display = detailVisibleCount < detailRows.length ? "flex" : "none";
|
|
}
|
|
}
|
|
|
|
function appendDetailRows() {
|
|
if (!detailBody) return;
|
|
const nextRows = detailRows.slice(detailVisibleCount, detailVisibleCount + detailPageSize);
|
|
if (!nextRows.length) {
|
|
updateDetailControls();
|
|
return;
|
|
}
|
|
detailBody.insertAdjacentHTML("beforeend", nextRows.map(renderDetailRow).join(""));
|
|
detailVisibleCount += nextRows.length;
|
|
updateDetailControls();
|
|
}
|
|
|
|
function buildVendorUsageRows(rows) {
|
|
const map = new Map();
|
|
rows.forEach((row) => {
|
|
const vendor = displayVendor(row.vendor_name);
|
|
const item = map.get(vendor) || { vendor, amount: 0, count: 0, people: new Set() };
|
|
item.amount += Number(row.amount || 0);
|
|
item.count += 1;
|
|
const person = String(row.person_name || "").trim();
|
|
if (person) item.people.add(person);
|
|
map.set(vendor, item);
|
|
});
|
|
return Array.from(map.values()).sort((a, b) => b.amount - a.amount || a.vendor.localeCompare(b.vendor, "ko"));
|
|
}
|
|
|
|
function renderDetailTable(rows, options = {}) {
|
|
const showCategory = options.showCategory !== false;
|
|
return `
|
|
<div class="report-table-wrap">
|
|
<table class="report-table">
|
|
<thead>
|
|
<tr>
|
|
<th>일자</th>
|
|
<th>전표번호</th>
|
|
<th>계정</th>
|
|
<th>거래처</th>
|
|
<th>개인/귀속</th>
|
|
${showCategory ? "<th>분류</th>" : ""}
|
|
<th class="number">금액</th>
|
|
<th>적요</th>
|
|
<th>상대계정</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rows.map((row) => `
|
|
<tr>
|
|
<td>${escapeHtml(row.ledger_date)}</td>
|
|
<td>${escapeHtml(row.voucher_no)}</td>
|
|
<td>${escapeHtml(row.account_name)}</td>
|
|
<td>${escapeHtml(displayVendor(row.vendor_name))}</td>
|
|
<td>${escapeHtml(row.person_name)}</td>
|
|
${showCategory ? `<td>${escapeHtml(row.category)}</td>` : ""}
|
|
<td class="number">${formatNumber(row.amount)}</td>
|
|
<td class="desc">${escapeHtml(row.description)}</td>
|
|
<td class="counterpart" data-tooltip="${escapeHtml(row.counterpart_tooltip || row.counterpart_details)}"><span class="counterpart-text">${escapeHtml(row.counterpart_details)}</span></td>
|
|
</tr>
|
|
`).join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function openModal(rows, meta) {
|
|
modalTitle.textContent = meta.title || `${meta.vendor || "-"} / ${meta.category || "-"} 상세`;
|
|
if (!rows.length) {
|
|
modalBody.innerHTML = '<div class="report-muted">상세 값이 없습니다.</div>';
|
|
} else {
|
|
const total = rows.reduce((sum, row) => sum + Number(row.amount || 0), 0);
|
|
if (meta.mode === "categoryYear") {
|
|
const vendorRows = buildVendorUsageRows(rows);
|
|
modalBody.innerHTML = `
|
|
<p class="report-muted">총 ${rows.length.toLocaleString("ko-KR")}건 / ${formatNumber(total)}원</p>
|
|
<div class="report-table-wrap">
|
|
<table class="report-table">
|
|
<thead>
|
|
<tr>
|
|
<th>거래처</th>
|
|
<th>개인/귀속</th>
|
|
<th class="number">금액</th>
|
|
<th class="number">건수</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${vendorRows.map((row) => `
|
|
<tr>
|
|
<td>${escapeHtml(row.vendor)}</td>
|
|
<td>${escapeHtml(Array.from(row.people).slice(0, 4).join(", ") || "-")}</td>
|
|
<td class="number">${formatNumber(row.amount)}</td>
|
|
<td class="number">${formatNumber(row.count)}</td>
|
|
</tr>
|
|
`).join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<button type="button" class="modal-detail-toggle" id="modalDetailToggle">상세 내역 열기</button>
|
|
<div class="modal-detail-panel" id="modalDetailPanel">
|
|
${renderDetailTable(rows, { showCategory: false })}
|
|
</div>
|
|
`;
|
|
} else {
|
|
modalBody.innerHTML = `
|
|
<p class="report-muted">총 ${rows.length.toLocaleString("ko-KR")}건 / ${formatNumber(total)}원</p>
|
|
${renderDetailTable(rows)}
|
|
`;
|
|
}
|
|
}
|
|
modal.classList.add("is-open");
|
|
modal.setAttribute("aria-hidden", "false");
|
|
}
|
|
|
|
function closeModal() {
|
|
modal.classList.remove("is-open");
|
|
modal.setAttribute("aria-hidden", "true");
|
|
}
|
|
|
|
function positionCounterpartTooltip(event) {
|
|
if (!counterpartTooltip || !counterpartTooltip.classList.contains("is-open")) return;
|
|
const margin = 14;
|
|
const rect = counterpartTooltip.getBoundingClientRect();
|
|
let left = event.clientX + margin;
|
|
let top = event.clientY + margin;
|
|
if (left + rect.width > window.innerWidth - margin) {
|
|
left = Math.max(margin, event.clientX - rect.width - margin);
|
|
}
|
|
if (top + rect.height > window.innerHeight - margin) {
|
|
top = Math.max(margin, event.clientY - rect.height - margin);
|
|
}
|
|
counterpartTooltip.style.left = `${left}px`;
|
|
counterpartTooltip.style.top = `${top}px`;
|
|
}
|
|
|
|
document.addEventListener("mouseover", (event) => {
|
|
const cell = event.target.closest(".counterpart[data-tooltip]");
|
|
if (!cell || !counterpartTooltip) return;
|
|
const text = cell.dataset.tooltip || "";
|
|
if (!text.trim()) return;
|
|
counterpartTooltip.textContent = text;
|
|
counterpartTooltip.classList.add("is-open");
|
|
counterpartTooltip.setAttribute("aria-hidden", "false");
|
|
positionCounterpartTooltip(event);
|
|
});
|
|
document.addEventListener("mousemove", (event) => {
|
|
if (event.target.closest(".counterpart[data-tooltip]")) {
|
|
positionCounterpartTooltip(event);
|
|
}
|
|
});
|
|
document.addEventListener("mouseout", (event) => {
|
|
const cell = event.target.closest(".counterpart[data-tooltip]");
|
|
if (!cell || !counterpartTooltip) return;
|
|
if (event.relatedTarget && cell.contains(event.relatedTarget)) return;
|
|
counterpartTooltip.classList.remove("is-open");
|
|
counterpartTooltip.setAttribute("aria-hidden", "true");
|
|
});
|
|
|
|
modalBody?.addEventListener("click", (event) => {
|
|
const toggle = event.target.closest("#modalDetailToggle");
|
|
if (!toggle) return;
|
|
const panel = document.getElementById("modalDetailPanel");
|
|
const isOpen = panel?.classList.toggle("is-open");
|
|
toggle.textContent = isOpen ? "상세 내역 접기" : "상세 내역 열기";
|
|
});
|
|
|
|
document.querySelectorAll("[data-summary-detail]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const vendor = button.dataset.vendor || "";
|
|
const category = button.dataset.category || "";
|
|
const accountGroup = button.dataset.accountGroup || "";
|
|
const rows = (reportData.detail_rows || []).filter((row) =>
|
|
displayVendor(row.vendor_name) === vendor &&
|
|
String(row.category || "") === category &&
|
|
String(row.account_group || "") === accountGroup
|
|
);
|
|
openModal(rows, { vendor, category, accountGroup });
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll("[data-category-year-detail]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const accountGroup = button.dataset.accountGroup || "";
|
|
const category = button.dataset.category || "";
|
|
const year = button.dataset.year || "all";
|
|
const rows = (reportData.detail_rows || []).filter((row) =>
|
|
String(row.account_group || "") === accountGroup &&
|
|
String(row.category || "") === category &&
|
|
(year === "all" || String(row.year || "") === year)
|
|
);
|
|
const titleYear = year === "all" ? "전체연도" : `${year}년`;
|
|
openModal(rows, {
|
|
title: `${accountGroup} / ${category} / ${titleYear} 상세`,
|
|
vendor: accountGroup,
|
|
category,
|
|
accountGroup,
|
|
mode: "categoryYear",
|
|
});
|
|
});
|
|
});
|
|
|
|
detailBody?.addEventListener("change", async (event) => {
|
|
const select = event.target.closest("[data-category-select]");
|
|
if (!select) return;
|
|
const rowId = select.dataset.rowId || "";
|
|
const category = select.value || "";
|
|
select.disabled = true;
|
|
try {
|
|
const response = await fetch("/wehago-benefit-entertainment/api/category", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ ledger_row_id: rowId, category }),
|
|
});
|
|
const payload = await response.json();
|
|
if (!response.ok || !payload.ok) {
|
|
throw new Error(payload.error || "분류 저장 중 오류가 발생했습니다.");
|
|
}
|
|
window.location.reload();
|
|
} catch (error) {
|
|
alert(error.message || "분류 저장 중 오류가 발생했습니다.");
|
|
select.disabled = false;
|
|
}
|
|
});
|
|
|
|
detailMore?.addEventListener("click", appendDetailRows);
|
|
updateDetailControls();
|
|
|
|
modalClose?.addEventListener("click", closeModal);
|
|
modal?.addEventListener("click", (event) => {
|
|
if (event.target === modal) closeModal();
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") closeModal();
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|