feat: refine reimbursements and add parenting categories
This commit is contained in:
parent
18fa42c424
commit
99933da2c8
|
|
@ -2,6 +2,7 @@ import type { EntryType } from "@cents/domain";
|
||||||
import {
|
import {
|
||||||
Ambulance,
|
Ambulance,
|
||||||
Apple,
|
Apple,
|
||||||
|
Baby,
|
||||||
Banknote,
|
Banknote,
|
||||||
BatteryCharging,
|
BatteryCharging,
|
||||||
Bike,
|
Bike,
|
||||||
|
|
@ -34,6 +35,7 @@ import {
|
||||||
Hospital,
|
Hospital,
|
||||||
Hotel,
|
Hotel,
|
||||||
House,
|
House,
|
||||||
|
Milk,
|
||||||
ParkingCircle,
|
ParkingCircle,
|
||||||
Pencil,
|
Pencil,
|
||||||
Pill,
|
Pill,
|
||||||
|
|
@ -44,6 +46,7 @@ import {
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
ShoppingBasket,
|
ShoppingBasket,
|
||||||
Shirt,
|
Shirt,
|
||||||
|
SoapDispenserDroplet,
|
||||||
Smartphone,
|
Smartphone,
|
||||||
Soup,
|
Soup,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
|
@ -55,6 +58,8 @@ import {
|
||||||
UsersRound,
|
UsersRound,
|
||||||
WalletCards,
|
WalletCards,
|
||||||
Zap,
|
Zap,
|
||||||
|
CircleEllipsis,
|
||||||
|
PackageOpen,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "@lucide/vue";
|
} from "@lucide/vue";
|
||||||
|
|
||||||
|
|
@ -130,6 +135,24 @@ export const categories: Record<EntryType, Category[]> = {
|
||||||
{ id: "toys", label: "玩具", color: "#7559d9", tint: "#f2efff", icon: Puzzle },
|
{ id: "toys", label: "玩具", color: "#7559d9", tint: "#f2efff", icon: Puzzle },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "parenting",
|
||||||
|
label: "育儿",
|
||||||
|
color: "#de6b79",
|
||||||
|
tint: "#fff0f2",
|
||||||
|
icon: Baby,
|
||||||
|
children: [
|
||||||
|
{ id: "formula", label: "奶粉", color: "#c88945", tint: "#fff5e8", icon: Milk },
|
||||||
|
{ id: "maternal-baby", label: "母婴", color: "#de6b79", tint: "#fff0f2", icon: Baby },
|
||||||
|
{ id: "children-clothing", label: "童装", color: "#b14e79", tint: "#faeef4", icon: Shirt },
|
||||||
|
{ id: "children-toys", label: "玩具", color: "#7559d9", tint: "#f2efff", icon: Puzzle },
|
||||||
|
{ id: "children-tuition", label: "学费", color: "#4869b1", tint: "#edf2fb", icon: GraduationCap },
|
||||||
|
{ id: "children-supplements", label: "补剂", color: "#2f8768", tint: "#eaf7f1", icon: Pill },
|
||||||
|
{ id: "children-care", label: "洗护", color: "#2d8f9e", tint: "#eaf8fa", icon: SoapDispenserDroplet },
|
||||||
|
{ id: "children-paper", label: "纸品", color: "#7a8a86", tint: "#f0f4f2", icon: PackageOpen },
|
||||||
|
{ id: "children-other", label: "其他", color: "#526a78", tint: "#edf2f4", icon: CircleEllipsis },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "entertainment",
|
id: "entertainment",
|
||||||
label: "娱乐",
|
label: "娱乐",
|
||||||
|
|
|
||||||
|
|
@ -696,6 +696,32 @@ input:focus-visible {
|
||||||
color: #0d8b67;
|
color: #0d8b67;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry-reimbursement-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-value .entry-reimbursement-tag {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border: 1px solid #c5d6ea;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #5e7895;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.25;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-value .reimbursement {
|
||||||
|
color: #3478b8;
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-decoration-color: #78a5cc;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
min-height: 260px;
|
min-height: 260px;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
WalletCards,
|
WalletCards,
|
||||||
X,
|
X,
|
||||||
} from "@lucide/vue";
|
} from "@lucide/vue";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import {
|
import {
|
||||||
categories,
|
categories,
|
||||||
|
|
@ -151,11 +151,25 @@ const canRecordReimbursement = computed(() =>
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([store.loadEntries(), ledgerStore.loadLedgers()]);
|
await Promise.all([store.loadEntries(), ledgerStore.loadLedgers()]);
|
||||||
const found = store.entries.find((item) => item.id === String(route.params.id));
|
hydrateCurrentEntry();
|
||||||
if (found) hydrate(found);
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => route.params.id, () => {
|
||||||
|
hydrateCurrentEntry();
|
||||||
|
});
|
||||||
|
|
||||||
|
function hydrateCurrentEntry() {
|
||||||
|
const found = store.entries.find((item) => item.id === String(route.params.id));
|
||||||
|
if (!found) return;
|
||||||
|
categorySheetMode.value = null;
|
||||||
|
ledgerPickerOpen.value = false;
|
||||||
|
currencyPickerOpen.value = false;
|
||||||
|
reimbursementOpen.value = false;
|
||||||
|
errorMessage.value = "";
|
||||||
|
hydrate(found);
|
||||||
|
}
|
||||||
|
|
||||||
function hydrate(value: LedgerEntry) {
|
function hydrate(value: LedgerEntry) {
|
||||||
entry.value = value;
|
entry.value = value;
|
||||||
currency.value = value.currency;
|
currency.value = value.currency;
|
||||||
|
|
|
||||||
|
|
@ -999,7 +999,11 @@ async function shareLedger() {
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-value">
|
<div class="entry-value">
|
||||||
<span v-if="entryHasReimbursement(entry)" class="entry-original-amount">−{{ entryDisplayAmount(entry) }}</span>
|
<span v-if="entryHasReimbursement(entry)" class="entry-original-amount">−{{ entryDisplayAmount(entry) }}</span>
|
||||||
<strong :class="entry.type">{{ entry.type === "expense" ? "−" : "+" }}{{ entryDisplayAmount(entry, true) }}</strong>
|
<div v-if="isReimbursement(entry)" class="entry-reimbursement-value">
|
||||||
|
<span class="entry-reimbursement-tag">不计入</span>
|
||||||
|
<strong class="reimbursement">{{ entryDisplayAmount(entry, true) }}</strong>
|
||||||
|
</div>
|
||||||
|
<strong v-else :class="entry.type">{{ entry.type === "expense" ? "−" : "+" }}{{ entryDisplayAmount(entry, true) }}</strong>
|
||||||
<span>{{ formatTime(entry.occurredAt) }}</span>
|
<span>{{ formatTime(entry.occurredAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
|
||||||
14
docs/分类体系.md
14
docs/分类体系.md
|
|
@ -17,18 +17,19 @@
|
||||||
|
|
||||||
### 支出
|
### 支出
|
||||||
|
|
||||||
共 8 个。
|
共 9 个。
|
||||||
|
|
||||||
| 顺序 | 分类 | ID |
|
| 顺序 | 分类 | ID |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 1 | 餐饮 | `food` |
|
| 1 | 餐饮 | `food` |
|
||||||
| 2 | 交通 | `transport` |
|
| 2 | 交通 | `transport` |
|
||||||
| 3 | 购物 | `shopping` |
|
| 3 | 购物 | `shopping` |
|
||||||
| 4 | 娱乐 | `entertainment` |
|
| 4 | 育儿 | `parenting` |
|
||||||
| 5 | 社交 | `social` |
|
| 5 | 娱乐 | `entertainment` |
|
||||||
| 6 | 医疗 | `health` |
|
| 6 | 社交 | `social` |
|
||||||
| 7 | 教育 | `education` |
|
| 7 | 医疗 | `health` |
|
||||||
| 8 | 缴费 | `bills` |
|
| 8 | 教育 | `education` |
|
||||||
|
| 9 | 缴费 | `bills` |
|
||||||
|
|
||||||
### 收入
|
### 收入
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@
|
||||||
| 餐饮 | 外卖 `takeout`、堂食 `restaurant`、咖啡 `coffee`、零食 `snacks`、饮品 `drinks`、水果 `fruit` |
|
| 餐饮 | 外卖 `takeout`、堂食 `restaurant`、咖啡 `coffee`、零食 `snacks`、饮品 `drinks`、水果 `fruit` |
|
||||||
| 交通 | 打车 `taxi`、公交 `public-transit`、地铁 `subway`、火车 `train`、骑行 `cycling`、加油 `fuel`、充电 `charging`、停车 `parking`、高速 `highway` |
|
| 交通 | 打车 `taxi`、公交 `public-transit`、地铁 `subway`、火车 `train`、骑行 `cycling`、加油 `fuel`、充电 `charging`、停车 `parking`、高速 `highway` |
|
||||||
| 购物 | 服饰 `clothing`、食品 `shopping-food`、杂货 `groceries`、数码 `digital`、家居 `home`、玩具 `toys` |
|
| 购物 | 服饰 `clothing`、食品 `shopping-food`、杂货 `groceries`、数码 `digital`、家居 `home`、玩具 `toys` |
|
||||||
|
| 育儿 | 奶粉 `formula`、母婴 `maternal-baby`、童装 `children-clothing`、玩具 `children-toys`、学费 `children-tuition`、补剂 `children-supplements`、洗护 `children-care`、纸品 `children-paper`、其他 `children-other` |
|
||||||
| 娱乐 | 游戏 `games`、电影 `movies`、按摩 `massage`、运动 `sports`、门票 `tickets`、酒店 `hotel` |
|
| 娱乐 | 游戏 `games`、电影 `movies`、按摩 `massage`、运动 `sports`、门票 `tickets`、酒店 `hotel` |
|
||||||
| 社交 | 红包 `red-packet`、礼物 `gift`、请客 `treating`、聚会 `gathering`、捐赠 `donation`、人情 `social-obligation` |
|
| 社交 | 红包 `red-packet`、礼物 `gift`、请客 `treating`、聚会 `gathering`、捐赠 `donation`、人情 `social-obligation` |
|
||||||
| 医疗 | 药店 `pharmacy`、门诊 `outpatient`、急诊 `emergency`、疫苗 `vaccine`、住院 `hospitalization` |
|
| 医疗 | 药店 `pharmacy`、门诊 `outpatient`、急诊 `emergency`、疫苗 `vaccine`、住院 `hospitalization` |
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,30 @@ test.describe("报销:从支出详情记录", () => {
|
||||||
await page.goto("/stats");
|
await page.goto("/stats");
|
||||||
await expect(page.getByText("报销到账")).not.toBeVisible();
|
await expect(page.getByText("报销到账")).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("04-流水中的报销到账标记为不计入并使用蓝色删除线金额", async ({ page }) => {
|
||||||
|
await mockApi(page, { initialEntries: [makeEntry(), makeEntry({ id: "reimbursement", type: "income", amount: 1000, categoryId: "restaurant", reimbursementOfEntryId: "entry-lunch", note: "午餐报销" })] });
|
||||||
|
await 登录(page);
|
||||||
|
|
||||||
|
const reimbursement = page.locator(".entry-row", { hasText: "午餐报销" });
|
||||||
|
await expect(reimbursement.getByText("不计入")).toBeVisible();
|
||||||
|
await expect(reimbursement.locator(".entry-value .reimbursement")).toHaveText("¥32.00");
|
||||||
|
await expect(reimbursement.locator(".entry-value .reimbursement")).not.toContainText("+");
|
||||||
|
await expect(reimbursement.locator(".entry-value .reimbursement")).toHaveCSS("color", "rgb(52, 120, 184)");
|
||||||
|
await expect(reimbursement.locator(".entry-value .reimbursement")).toHaveCSS("text-decoration-line", "line-through");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("05-报销详情可以跳转到关联原支出", async ({ page }) => {
|
||||||
|
await mockApi(page, { initialEntries: [
|
||||||
|
makeEntry({ note: "午餐支出" }),
|
||||||
|
makeEntry({ id: "reimbursement", type: "income", amount: 1000, categoryId: "restaurant", reimbursementOfEntryId: "entry-lunch", note: "午餐报销" }),
|
||||||
|
] });
|
||||||
|
await 登录(page);
|
||||||
|
await page.goto("/entries/reimbursement");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: /关联原支出/ }).click();
|
||||||
|
await expect(page).toHaveURL(/\/entries\/entry-lunch$/);
|
||||||
|
await expect(page.getByRole("button", { name: "记录报销" })).toBeVisible();
|
||||||
|
await expect(page.locator("textarea")).toHaveValue("午餐支出");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue