月次レポート機能を実装し、経費の取り消し・復帰機能を追加。CSV取込画面にドラッグ&ドロップエリアを実装し、エラーメッセージ表示を追加。金額のカンマ区切り表示を全体に適用。faviconとapple-touch-iconを追加し、404エラーを回避。作業ログをdiary.mdに追記。
This commit is contained in:
@@ -4,6 +4,13 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Card Data Sorting{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%231a73e8'/%3E%3Ctext x='32' y='42' font-size='28' text-anchor='middle' fill='white' font-family='sans-serif'%3ECS%3C/text%3E%3C/svg%3E">
|
||||
<link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 180'%3E%3Crect width='180' height='180' rx='28' fill='%231a73e8'/%3E%3Ctext x='90' y='118' font-size='72' text-anchor='middle' fill='white' font-family='sans-serif'%3ECS%3C/text%3E%3C/svg%3E">
|
||||
<style>
|
||||
.amount {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
||||
@@ -5,13 +5,76 @@
|
||||
{% block content %}
|
||||
<section>
|
||||
<h2>CSV取込</h2>
|
||||
{% if error_message %}
|
||||
<p style="color: #b00020;">{{ error_message }}</p>
|
||||
{% endif %}
|
||||
<style>
|
||||
.drop-zone {
|
||||
border: 2px dashed #888;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
background: #fafafa;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.drop-zone.is-dragover {
|
||||
background: #e9f2ff;
|
||||
}
|
||||
.drop-zone input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
.drop-zone .file-name {
|
||||
margin-top: 8px;
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<p>ここにDrag & Dropエリアを配置予定。</p>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<form method="post" enctype="multipart/form-data" id="js-csv-form">
|
||||
{% csrf_token %}
|
||||
<input type="file" name="csv_file">
|
||||
<label class="drop-zone" id="js-drop-zone">
|
||||
<strong>ここにCSVをドラッグ&ドロップ</strong>
|
||||
<div>またはクリックして選択</div>
|
||||
<input type="file" name="csv_file" id="js-csv-input" accept=".csv">
|
||||
<div class="file-name" id="js-file-name">ファイル未選択</div>
|
||||
</label>
|
||||
<button type="submit">アップロード</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
const dropZone = document.querySelector('#js-drop-zone');
|
||||
const fileInput = document.querySelector('#js-csv-input');
|
||||
const fileName = document.querySelector('#js-file-name');
|
||||
|
||||
function updateFileName(files) {
|
||||
if (!files || files.length === 0) {
|
||||
fileName.textContent = 'ファイル未選択';
|
||||
return;
|
||||
}
|
||||
fileName.textContent = files[0].name;
|
||||
}
|
||||
|
||||
dropZone.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
dropZone.classList.add('is-dragover');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('is-dragover');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
dropZone.classList.remove('is-dragover');
|
||||
if (!event.dataTransfer?.files?.length) {
|
||||
return;
|
||||
}
|
||||
fileInput.files = event.dataTransfer.files;
|
||||
updateFileName(fileInput.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (event) => {
|
||||
updateFileName(event.target.files);
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}明細編集{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h2>明細編集</h2>
|
||||
<style>
|
||||
.is-canceled {
|
||||
text-decoration: line-through;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
{% if imported %}
|
||||
<p>取込件数: {{ imported }} / 重複件数: {{ duplicated }} / 文字コード: {{ encoding }}</p>
|
||||
{% endif %}
|
||||
<form>
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
<input type="checkbox" id="js-include-canceled" {% if include_canceled %}checked{% endif %}>
|
||||
取り消し済みを表示
|
||||
</label>
|
||||
</form>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -17,6 +28,7 @@
|
||||
<th>利用日</th>
|
||||
<th>利用先</th>
|
||||
<th>金額</th>
|
||||
<th>取消</th>
|
||||
<th>店舗区分</th>
|
||||
<th>経費区分</th>
|
||||
<th>区分</th>
|
||||
@@ -25,10 +37,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for expense in expenses %}
|
||||
<tr data-expense-id="{{ expense.id }}">
|
||||
<tr data-expense-id="{{ expense.id }}" class="{% if expense.is_canceled %}is-canceled{% endif %}">
|
||||
<td>{{ expense.use_date }}</td>
|
||||
<td>{{ expense.description }}</td>
|
||||
<td>{{ expense.amount }}</td>
|
||||
<td class="amount">{{ expense.amount|intcomma }}</td>
|
||||
<td>
|
||||
<button type="button" class="js-expense-cancel">
|
||||
{% if expense.is_canceled %}復帰{% else %}取消{% endif %}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<select class="js-expense-field" data-field="store_id">
|
||||
<option value="">未設定</option>
|
||||
@@ -73,7 +90,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7">データがありません。</td>
|
||||
<td colspan="8">データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -96,6 +113,21 @@
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function setCanceled(expenseId, isCanceled) {
|
||||
const response = await fetch(`/expenses/${expenseId}/cancel/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: JSON.stringify({ is_canceled: isCanceled }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('取消更新に失敗しました');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function coerceValue(field, value) {
|
||||
return value;
|
||||
}
|
||||
@@ -117,6 +149,41 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.js-expense-cancel').forEach((button) => {
|
||||
button.addEventListener('click', async (event) => {
|
||||
const row = event.target.closest('tr');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const expenseId = row.dataset.expenseId;
|
||||
const isCanceled = event.target.textContent.trim() === '取消';
|
||||
try {
|
||||
await setCanceled(expenseId, isCanceled);
|
||||
if (isCanceled && !document.querySelector('#js-include-canceled')?.checked) {
|
||||
row.remove();
|
||||
return;
|
||||
}
|
||||
row.classList.toggle('is-canceled', isCanceled);
|
||||
event.target.textContent = isCanceled ? '復帰' : '取消';
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const includeToggle = document.querySelector('#js-include-canceled');
|
||||
if (includeToggle) {
|
||||
includeToggle.addEventListener('change', (event) => {
|
||||
const url = new URL(window.location.href);
|
||||
if (event.target.checked) {
|
||||
url.searchParams.set('include_canceled', '1');
|
||||
} else {
|
||||
url.searchParams.delete('include_canceled');
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block title %}月次レポート{% endblock %}
|
||||
|
||||
@@ -8,20 +9,181 @@
|
||||
<form method="get">
|
||||
<label>
|
||||
年月
|
||||
<input type="month" name="target_month">
|
||||
<input type="month" name="target_month" value="{{ target_month }}">
|
||||
</label>
|
||||
<button type="submit">表示</button>
|
||||
</form>
|
||||
{% if error_message %}
|
||||
<p style="color: #b00020;">{{ error_message }}</p>
|
||||
{% endif %}
|
||||
<div>
|
||||
<h3>合計</h3>
|
||||
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>店舗別合計</h3>
|
||||
<p>集計結果をここに表示。</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店舗</th>
|
||||
<th>件数</th>
|
||||
<th class="amount">金額</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in report.store_totals %}
|
||||
<tr>
|
||||
<td>{{ row.store__name|default:"未設定" }}</td>
|
||||
<td>{{ row.count }}</td>
|
||||
<td class="amount">{{ row.total|intcomma }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% for row in report.store_totals %}
|
||||
<h4>{{ row.store__name|default:"未設定" }} の明細</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日付</th>
|
||||
<th>利用先</th>
|
||||
<th class="amount">金額</th>
|
||||
<th>備考</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for detail in row.details %}
|
||||
<tr>
|
||||
<td>{{ detail.use_date }}</td>
|
||||
<td>{{ detail.description }}</td>
|
||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||
<td>{{ detail.note }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<h3>経費区分別合計</h3>
|
||||
<p>集計結果をここに表示。</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>経費区分</th>
|
||||
<th>件数</th>
|
||||
<th class="amount">金額</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in report.category_totals %}
|
||||
<tr>
|
||||
<td>{{ row.expense_category__name|default:"未設定" }}</td>
|
||||
<td>{{ row.count }}</td>
|
||||
<td class="amount">{{ row.total|intcomma }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% for row in report.category_totals %}
|
||||
<h4>{{ row.expense_category__name|default:"未設定" }} の明細</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日付</th>
|
||||
<th>利用先</th>
|
||||
<th class="amount">金額</th>
|
||||
<th>備考</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for detail in row.details %}
|
||||
<tr>
|
||||
<td>{{ detail.use_date }}</td>
|
||||
<td>{{ detail.description }}</td>
|
||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||
<td>{{ detail.note }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<p>未分類件数の警告をここに表示。</p>
|
||||
<h3>区分別合計</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>区分</th>
|
||||
<th>件数</th>
|
||||
<th class="amount">金額</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in report.owner_totals %}
|
||||
<tr>
|
||||
<td>{{ row.owner_type }}</td>
|
||||
<td>{{ row.count }}</td>
|
||||
<td class="amount">{{ row.total|intcomma }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% for row in report.owner_totals %}
|
||||
<h4>{{ row.owner_type }} の明細</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日付</th>
|
||||
<th>利用先</th>
|
||||
<th class="amount">金額</th>
|
||||
<th>備考</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for detail in row.details %}
|
||||
<tr>
|
||||
<td>{{ detail.use_date }}</td>
|
||||
<td>{{ detail.description }}</td>
|
||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||
<td>{{ detail.note }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
<h3>未分類件数</h3>
|
||||
<ul>
|
||||
<li>店舗未設定: {{ report.unclassified_counts.store_missing }}</li>
|
||||
<li>経費区分未設定: {{ report.unclassified_counts.category_missing }}</li>
|
||||
<li>区分未設定: {{ report.unclassified_counts.owner_pending }}</li>
|
||||
<li>対象総件数: {{ report.unclassified_counts.total }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% load humanize %}
|
||||
<!doctype html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
@@ -8,10 +9,103 @@
|
||||
h1, h2 { margin: 0 0 8px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { border: 1px solid #333; padding: 6px; }
|
||||
.amount { text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>月次レポート</h1>
|
||||
<p>PDF出力用テンプレートです。</p>
|
||||
<p>対象年月: {{ target_month }}</p>
|
||||
<h2>合計</h2>
|
||||
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
||||
<h2>店舗別合計</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店舗</th>
|
||||
<th>件数</th>
|
||||
<th class="amount">金額</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in report.store_totals %}
|
||||
<tr>
|
||||
<td>{{ row.store__name|default:"未設定" }}</td>
|
||||
<td>{{ row.count }}</td>
|
||||
<td class="amount">{{ row.total|intcomma }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>経費区分別合計</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>経費区分</th>
|
||||
<th>件数</th>
|
||||
<th class="amount">金額</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in report.category_totals %}
|
||||
<tr>
|
||||
<td>{{ row.expense_category__name|default:"未設定" }}</td>
|
||||
<td>{{ row.count }}</td>
|
||||
<td class="amount">{{ row.total|intcomma }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>区分別合計</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>区分</th>
|
||||
<th>件数</th>
|
||||
<th class="amount">金額</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in report.owner_totals %}
|
||||
<tr>
|
||||
<td>{{ row.owner_type }}</td>
|
||||
<td>{{ row.count }}</td>
|
||||
<td class="amount">{{ row.total|intcomma }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>未分類件数</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>店舗未設定</th>
|
||||
<td>{{ report.unclassified_counts.store_missing }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>経費区分未設定</th>
|
||||
<td>{{ report.unclassified_counts.category_missing }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>区分未設定</th>
|
||||
<td>{{ report.unclassified_counts.owner_pending }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>対象総件数</th>
|
||||
<td>{{ report.unclassified_counts.total }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user