Files
finance_app/resources/js/Pages/Expenses/Index.vue
2026-03-09 01:00:49 +08:00

582 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref } from "vue";
import { computed } from "vue";
import { useForm, usePage } from "@inertiajs/vue3";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
const props = defineProps({
expenses: Array,
categories: Array,
rules: Array,
});
// 規則管理
const showRules = ref(false);
const ruleForm = useForm({
keyword: "",
category_id: "",
});
const flash = computed(() => usePage().props.flash);
// ── 分類管理 ──
const showCategories = ref(false);
const categoryForm = useForm({
name: "",
color: "#3B82F6",
});
const submitCategory = () => {
categoryForm.post(route("categories.store"), {
onSuccess: () => categoryForm.reset(),
});
};
const submitRule = () => {
ruleForm.post(route("category-rules.store"), {
onSuccess: () => ruleForm.reset(),
});
};
const destroyRule = (id) => {
if (confirm("確定刪除此規則?")) {
useForm({}).delete(route("category-rules.destroy", id));
}
};
const editingCategory = ref(null);
const editCategoryForm = useForm({ name: "", color: "" });
const startEditCategory = (cat) => {
editingCategory.value = cat.id;
editCategoryForm.name = cat.name;
editCategoryForm.color = cat.color;
};
const submitEditCategory = (id) => {
editCategoryForm.put(route("categories.update", id), {
onSuccess: () => (editingCategory.value = null),
});
};
const destroyCategory = (id) => {
if (confirm("確定刪除?該分類的支出會變成未分類。")) {
useForm({}).delete(route("categories.destroy", id));
}
};
// ── 支出 ──
const form = useForm({
amount: "",
category_id: "",
note: "",
date: new Date().toISOString().split("T")[0],
});
const submit = () => {
form.post(route("expenses.store"), {
onSuccess: () => form.reset(),
});
};
const formatDate = (date) => (date ? date.toString().split("T")[0] : "");
const editing = ref(null);
const editForm = useForm({
amount: "",
category_id: "",
note: "",
date: "",
});
const startEdit = (expense) => {
editing.value = expense.id;
editForm.amount = expense.amount;
editForm.category_id = expense.category_id ?? "";
editForm.note = expense.note ?? "";
editForm.date = formatDate(expense.date);
};
const submitEdit = (id) => {
editForm.put(route("expenses.update", id), {
onSuccess: () => (editing.value = null),
});
};
const destroy = (id) => {
if (confirm("確定刪除?")) {
useForm({}).delete(route("expenses.destroy", id));
}
};
const selected = ref([]);
const toggleAll = (e) => {
selected.value = e.target.checked ? props.expenses.map((ex) => ex.id) : [];
};
const deleteSelected = () => {
if (selected.value.length === 0) return;
if (!confirm(`確定刪除 ${selected.value.length} 筆?`)) return;
useForm({ ids: selected.value }).delete(route("expenses.destroyBatch"), {
onSuccess: () => (selected.value = []),
});
};
</script>
<template>
<AuthenticatedLayout>
<div
v-if="flash?.message"
class="bg-green-50 text-green-700 p-4 rounded mb-4"
>
{{ flash.message }}
</div>
<template #header>
<div class="flex justify-between items-center">
<h2 class="text-xl font-semibold">記帳</h2>
</div>
</template>
<div class="py-8 max-w-4xl mx-auto px-4 space-y-6">
<!-- 分類管理 -->
<div class="bg-white rounded-lg shadow p-6">
<div
class="flex justify-between items-center cursor-pointer"
@click="showCategories = !showCategories"
>
<h3 class="text-lg font-medium">分類管理</h3>
<span class="text-gray-400 text-sm">
{{ showCategories ? "▲ 收起" : "▼ 展開" }}
</span>
</div>
<div v-if="showCategories" class="mt-4 space-y-4">
<!-- 新增分類 -->
<form
@submit.prevent="submitCategory"
class="flex gap-3 items-end"
>
<div class="flex-1">
<label class="block text-sm text-gray-600 mb-1"
>名稱</label
>
<input
v-model="categoryForm.name"
type="text"
class="w-full border rounded px-3 py-2"
placeholder="分類名稱"
required
/>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1"
>顏色</label
>
<input
v-model="categoryForm.color"
type="color"
class="h-10 w-16 border rounded cursor-pointer"
/>
</div>
<button
type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
新增
</button>
</form>
<!-- 分類列表 -->
<div class="divide-y border rounded">
<div
v-for="cat in categories"
:key="cat.id"
class="px-4 py-3"
>
<!-- 一般顯示 -->
<div
v-if="editingCategory !== cat.id"
class="flex items-center justify-between"
>
<div class="flex items-center gap-3">
<span
class="w-5 h-5 rounded-full"
:style="{ background: cat.color }"
></span>
<span>{{ cat.name }}</span>
</div>
<div class="space-x-3">
<button
@click="startEditCategory(cat)"
class="text-blue-400 hover:text-blue-600 text-sm"
>
編輯
</button>
<button
@click="destroyCategory(cat.id)"
class="text-red-400 hover:text-red-600 text-sm"
>
刪除
</button>
</div>
</div>
<!-- 編輯中 -->
<div v-else class="flex items-center gap-3">
<input
v-model="editCategoryForm.color"
type="color"
class="h-8 w-12 border rounded cursor-pointer"
/>
<input
v-model="editCategoryForm.name"
type="text"
class="flex-1 border rounded px-3 py-1 text-sm"
/>
<button
@click="submitEditCategory(cat.id)"
class="text-green-500 hover:text-green-700 text-sm"
>
儲存
</button>
<button
@click="editingCategory = null"
class="text-gray-400 hover:text-gray-600 text-sm"
>
取消
</button>
</div>
</div>
<div
v-if="categories.length === 0"
class="px-4 py-6 text-center text-gray-400 text-sm"
>
還沒有分類
</div>
</div>
</div>
</div>
<!-- 自動分類規則 -->
<div class="bg-white rounded-lg shadow p-6">
<div
class="flex justify-between items-center cursor-pointer"
@click="showRules = !showRules"
>
<h3 class="text-lg font-medium">自動分類規則</h3>
<span class="text-gray-400 text-sm">
{{ showRules ? "▲ 收起" : "▼ 展開" }}
</span>
</div>
<div v-if="showRules" class="mt-4 space-y-4">
<!-- 新增規則 -->
<form
@submit.prevent="submitRule"
class="flex gap-3 items-end"
>
<div class="flex-1">
<label class="block text-sm text-gray-600 mb-1"
>關鍵字</label
>
<input
v-model="ruleForm.keyword"
type="text"
class="w-full border rounded px-3 py-2"
placeholder="例如:全家、家樂福"
required
/>
</div>
<div class="flex-1">
<label class="block text-sm text-gray-600 mb-1"
>對應分類</label
>
<select
v-model="ruleForm.category_id"
class="w-full border rounded px-3 py-2"
required
>
<option value="">選擇分類</option>
<option
v-for="cat in categories"
:key="cat.id"
:value="cat.id"
>
{{ cat.name }}
</option>
</select>
</div>
<button
type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
新增
</button>
</form>
<!-- 規則列表 -->
<div class="divide-y border rounded">
<div
v-for="rule in rules"
:key="rule.id"
class="px-4 py-3 flex items-center justify-between"
>
<div class="flex items-center gap-3">
<span
class="font-mono bg-gray-100 px-2 py-1 rounded text-sm"
>
{{ rule.keyword }}
</span>
<span class="text-gray-400"></span>
<span
class="px-2 py-1 rounded text-xs text-white"
:style="{
background: rule.category?.color,
}"
>
{{ rule.category?.name }}
</span>
</div>
<button
@click="destroyRule(rule.id)"
class="text-red-400 hover:text-red-600 text-sm"
>
刪除
</button>
</div>
<div
v-if="rules.length === 0"
class="px-4 py-6 text-center text-gray-400 text-sm"
>
還沒有規則匯入發票時分類將會是空白
</div>
</div>
</div>
</div>
<!-- 新增支出 -->
<div class="bg-white rounded-lg shadow p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium">新增支出</h3>
<a
:href="route('expenses.import')"
class="bg-blue-500 text-white px-4 py-2 rounded text-sm hover:bg-blue-600"
>
匯入發票
</a>
</div>
<form @submit.prevent="submit" class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm text-gray-600 mb-1"
>金額</label
>
<input
v-model="form.amount"
type="number"
step="1"
class="w-full border rounded px-3 py-2"
placeholder="0"
required
/>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1"
>日期</label
>
<input
v-model="form.date"
type="date"
class="w-full border rounded px-3 py-2"
required
/>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1"
>分類</label
>
<select
v-model="form.category_id"
class="w-full border rounded px-3 py-2"
>
<option value="">未分類</option>
<option
v-for="cat in categories"
:key="cat.id"
:value="cat.id"
>
{{ cat.name }}
</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1"
>備註</label
>
<input
v-model="form.note"
type="text"
class="w-full border rounded px-3 py-2"
placeholder="備註..."
/>
</div>
<div class="col-span-2">
<button
type="submit"
class="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
>
新增
</button>
</div>
</form>
</div>
<!-- 支出列表 -->
<div class="bg-white rounded-lg shadow">
<div
class="px-4 py-3 border-b flex items-center justify-between"
>
<span class="text-sm text-gray-500">
{{ expenses.length }}
</span>
<button
@click="deleteSelected"
:class="
selected.length > 0
? 'opacity-100'
: 'opacity-0 pointer-events-none'
"
class="bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600 transition-opacity"
>
刪除選取 ({{ selected.length }})
</button>
</div>
<table class="w-full">
<thead class="bg-gray-50 text-sm text-gray-600">
<tr>
<th class="px-4 py-3">
<input type="checkbox" @change="toggleAll" />
</th>
<th class="px-4 py-3 text-left">日期</th>
<th class="px-4 py-3 text-left">分類</th>
<th class="px-4 py-3 text-left">備註</th>
<th class="px-4 py-3 text-right">金額</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<template v-for="expense in expenses" :key="expense.id">
<tr
v-if="editing !== expense.id"
class="hover:bg-gray-50"
>
<td class="px-4 py-3">
<input
type="checkbox"
:value="expense.id"
v-model="selected"
/>
</td>
<td class="px-4 py-3 text-sm">
{{ formatDate(expense.date) }}
</td>
<td class="px-4 py-3 text-sm">
<span
v-if="expense.category"
class="px-2 py-1 rounded text-xs text-white"
:style="{
background: expense.category.color,
}"
>
{{ expense.category.name }}
</span>
<span v-else class="text-gray-400 text-xs"
>未分類</span
>
</td>
<td class="px-4 py-3 text-sm text-gray-600">
{{ expense.note }}
</td>
<td class="px-4 py-3 text-right font-medium">
${{
Number(expense.amount).toLocaleString()
}}
</td>
<td class="px-4 py-3 text-right space-x-2">
<button
@click="startEdit(expense)"
class="text-blue-400 hover:text-blue-600 text-sm"
>
編輯
</button>
<button
@click="destroy(expense.id)"
class="text-red-400 hover:text-red-600 text-sm"
>
刪除
</button>
</td>
</tr>
<tr v-else class="bg-blue-50">
<td class="px-4 py-2">
<input
v-model="editForm.date"
type="date"
class="w-full border rounded px-2 py-1 text-sm"
/>
</td>
<td class="px-4 py-2">
<select
v-model="editForm.category_id"
class="w-full border rounded px-2 py-1 text-sm"
>
<option value="">未分類</option>
<option
v-for="cat in categories"
:key="cat.id"
:value="cat.id"
>
{{ cat.name }}
</option>
</select>
</td>
<td class="px-4 py-2">
<input
v-model="editForm.note"
type="text"
class="w-full border rounded px-2 py-1 text-sm"
/>
</td>
<td class="px-4 py-2">
<input
v-model="editForm.amount"
type="number"
class="w-full border rounded px-2 py-1 text-sm"
/>
</td>
<td class="px-4 py-2 text-right space-x-2">
<button
@click="submitEdit(expense.id)"
class="text-green-500 hover:text-green-700 text-sm"
>
儲存
</button>
<button
@click="editing = null"
class="text-gray-400 hover:text-gray-600 text-sm"
>
取消
</button>
</td>
</tr>
</template>
<tr v-if="expenses.length === 0">
<td
colspan="5"
class="px-4 py-8 text-center text-gray-400"
>
還沒有記錄
</td>
</tr>
</tbody>
</table>
</div>
</div>
</AuthenticatedLayout>
</template>