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 {
|
||||
Ambulance,
|
||||
Apple,
|
||||
Baby,
|
||||
Banknote,
|
||||
BatteryCharging,
|
||||
Bike,
|
||||
|
|
@ -34,6 +35,7 @@ import {
|
|||
Hospital,
|
||||
Hotel,
|
||||
House,
|
||||
Milk,
|
||||
ParkingCircle,
|
||||
Pencil,
|
||||
Pill,
|
||||
|
|
@ -44,6 +46,7 @@ import {
|
|||
ShoppingBag,
|
||||
ShoppingBasket,
|
||||
Shirt,
|
||||
SoapDispenserDroplet,
|
||||
Smartphone,
|
||||
Soup,
|
||||
Sparkles,
|
||||
|
|
@ -55,6 +58,8 @@ import {
|
|||
UsersRound,
|
||||
WalletCards,
|
||||
Zap,
|
||||
CircleEllipsis,
|
||||
PackageOpen,
|
||||
type LucideIcon,
|
||||
} from "@lucide/vue";
|
||||
|
||||
|
|
@ -130,6 +135,24 @@ export const categories: Record<EntryType, Category[]> = {
|
|||
{ 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",
|
||||
label: "娱乐",
|
||||
|
|
|
|||
|
|
@ -696,6 +696,32 @@ input:focus-visible {
|
|||
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 {
|
||||
min-height: 260px;
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
WalletCards,
|
||||
X,
|
||||
} from "@lucide/vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
categories,
|
||||
|
|
@ -151,11 +151,25 @@ const canRecordReimbursement = computed(() =>
|
|||
|
||||
onMounted(async () => {
|
||||
await Promise.all([store.loadEntries(), ledgerStore.loadLedgers()]);
|
||||
const found = store.entries.find((item) => item.id === String(route.params.id));
|
||||
if (found) hydrate(found);
|
||||
hydrateCurrentEntry();
|
||||
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) {
|
||||
entry.value = value;
|
||||
currency.value = value.currency;
|
||||
|
|
|
|||
|
|
@ -999,7 +999,11 @@ async function shareLedger() {
|
|||
</div>
|
||||
<div class="entry-value">
|
||||
<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>
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
|
|
|||
14
docs/分类体系.md
14
docs/分类体系.md
|
|
@ -17,18 +17,19 @@
|
|||
|
||||
### 支出
|
||||
|
||||
共 8 个。
|
||||
共 9 个。
|
||||
|
||||
| 顺序 | 分类 | ID |
|
||||
| --- | --- | --- |
|
||||
| 1 | 餐饮 | `food` |
|
||||
| 2 | 交通 | `transport` |
|
||||
| 3 | 购物 | `shopping` |
|
||||
| 4 | 娱乐 | `entertainment` |
|
||||
| 5 | 社交 | `social` |
|
||||
| 6 | 医疗 | `health` |
|
||||
| 7 | 教育 | `education` |
|
||||
| 8 | 缴费 | `bills` |
|
||||
| 4 | 育儿 | `parenting` |
|
||||
| 5 | 娱乐 | `entertainment` |
|
||||
| 6 | 社交 | `social` |
|
||||
| 7 | 医疗 | `health` |
|
||||
| 8 | 教育 | `education` |
|
||||
| 9 | 缴费 | `bills` |
|
||||
|
||||
### 收入
|
||||
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
| 餐饮 | 外卖 `takeout`、堂食 `restaurant`、咖啡 `coffee`、零食 `snacks`、饮品 `drinks`、水果 `fruit` |
|
||||
| 交通 | 打车 `taxi`、公交 `public-transit`、地铁 `subway`、火车 `train`、骑行 `cycling`、加油 `fuel`、充电 `charging`、停车 `parking`、高速 `highway` |
|
||||
| 购物 | 服饰 `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` |
|
||||
| 社交 | 红包 `red-packet`、礼物 `gift`、请客 `treating`、聚会 `gathering`、捐赠 `donation`、人情 `social-obligation` |
|
||||
| 医疗 | 药店 `pharmacy`、门诊 `outpatient`、急诊 `emergency`、疫苗 `vaccine`、住院 `hospitalization` |
|
||||
|
|
|
|||
|
|
@ -29,4 +29,30 @@ test.describe("报销:从支出详情记录", () => {
|
|||
await page.goto("/stats");
|
||||
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