CSV取込機能を実装し、Expenseモデルと関連するStore・ExpenseCategoryモデルを追加。CSVパーサーを作成し、出光CSVに対応。明細編集画面のAJAX保存機能を実装し、取込結果を表示する機能を追加。作業ログをdiary.mdに追記。

This commit is contained in:
president
2025-12-19 15:51:14 +09:00
parent 5fc2a31f50
commit 89caf3438a
11 changed files with 489 additions and 7 deletions

View File

@@ -5,6 +5,12 @@
{% block content %}
<section>
<h2>明細編集</h2>
{% if imported %}
<p>取込件数: {{ imported }} / 重複件数: {{ duplicated }} / 文字コード: {{ encoding }}</p>
{% endif %}
<form>
{% csrf_token %}
</form>
<table>
<thead>
<tr>
@@ -18,10 +24,127 @@
</tr>
</thead>
<tbody>
<tr>
<td colspan="7">データはここに表示されます。</td>
</tr>
{% for expense in expenses %}
<tr data-expense-id="{{ expense.id }}">
<td>{{ expense.use_date }}</td>
<td>{{ expense.description }}</td>
<td>{{ expense.amount }}</td>
<td>
<select class="js-expense-field" data-field="store_id">
<option value="">未設定</option>
{% for store in stores %}
<option value="{{ store.id }}" {% if expense.store_id == store.id %}selected{% endif %}>
{{ store.name }}
</option>
{% endfor %}
</select>
</td>
<td>
<select class="js-expense-field" data-field="expense_category_id">
<option value="">未設定</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if expense.expense_category_id == category.id %}selected{% endif %}>
{{ category.name }}
</option>
{% endfor %}
</select>
</td>
<td>
<label>
<input
class="js-expense-field"
data-field="is_business"
type="radio"
name="is_business_{{ expense.id }}"
value="true"
{% if expense.is_business is True %}checked{% endif %}
>
会社
</label>
<label>
<input
class="js-expense-field"
data-field="is_business"
type="radio"
name="is_business_{{ expense.id }}"
value="false"
{% if expense.is_business is False %}checked{% endif %}
>
家計
</label>
<label>
<input
class="js-expense-field"
data-field="is_business"
type="radio"
name="is_business_{{ expense.id }}"
value=""
{% if expense.is_business is None %}checked{% endif %}
>
未設定
</label>
</td>
<td>
<input
class="js-expense-field"
data-field="note"
type="text"
value="{{ expense.note }}"
>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7">データがありません。</td>
</tr>
{% endfor %}
</tbody>
</table>
<script>
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
async function saveExpense(expenseId, payload) {
const response = await fetch(`/expenses/${expenseId}/update/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error('保存に失敗しました');
}
return response.json();
}
function coerceValue(field, value) {
if (field === 'is_business') {
if (value === '') {
return null;
}
return value === 'true';
}
return value;
}
document.querySelectorAll('.js-expense-field').forEach((element) => {
const eventName = element.tagName === 'INPUT' ? 'change' : 'change';
element.addEventListener(eventName, async (event) => {
const row = event.target.closest('tr');
if (!row) {
return;
}
const expenseId = row.dataset.expenseId;
const field = event.target.dataset.field;
const value = coerceValue(field, event.target.value);
try {
await saveExpense(expenseId, { [field]: value });
} catch (error) {
alert(error.message);
}
});
});
</script>
</section>
{% endblock %}