From 704f58b1a17f3ac2936cad4d9aea470d3b50bbf7 Mon Sep 17 00:00:00 2001 From: SunCheng Date: Fri, 30 Jan 2026 10:41:19 +0800 Subject: [PATCH] fix --- Common/ServiceExtension.cs | 4 +- Entity/TransactionRecord.cs | 4 +- Repository/BudgetRepository.cs | 4 +- Repository/EmailMessageRepository.cs | 12 +- Repository/MessageRecordRepository.cs | 2 +- Repository/TransactionPeriodicRepository.cs | 14 +- Service/AI/OpenAiService.cs | 28 +- Service/AI/TextSegmentService.cs | 4 +- Service/Budget/BudgetSavingsService.cs | 50 +- Service/Budget/BudgetStatsService.cs | 128 +- Service/EmailServices/EmailFetchService.cs | 10 +- Service/EmailServices/EmailHandleService.cs | 2 +- .../EmailParse/EmailParseForm95555.cs | 2 +- .../EmailParse/EmailParseFormCcsvc.cs | 4 +- .../EmailParse/IEmailParseServices.cs | 12 +- Service/EmailServices/EmailSyncService.cs | 6 +- Service/ImportService.cs | 2 +- Service/Jobs/BudgetArchiveJob.cs | 2 +- Service/Jobs/DbBackupJob.cs | 4 +- Service/Jobs/EmailSyncJob.cs | 6 +- Service/Message/MessageService.cs | 8 +- .../Transaction/TransactionPeriodicService.cs | 22 +- Web/src/api/transactionRecord.js | 1 + Web/src/assets/theme.css | 184 + Web/src/components/TransactionDetail.vue | 4 +- Web/src/main.js | 1 + Web/src/views/CalendarV2.vue | 630 +++ WebApi.Test/Budget/BudgetStatsTest.cs | 66 +- .../Repository/BudgetRepositoryTest.cs | 2 +- .../Repository/ConfigRepositoryTest.cs | 2 +- .../Repository/EmailMessageRepositoryTest.cs | 6 +- .../PushSubscriptionRepositoryTest.cs | 2 +- .../TransactionPeriodicRepositoryTest.cs | 4 +- .../TransactionRecordRepositoryTest.cs | 6 +- .../TransactionPeriodicServiceTest.cs | 10 +- .../TransactionStatisticsServiceTest.cs | 24 +- WebApi/Controllers/Dto/BaseResponse.cs | 2 +- WebApi/Controllers/Dto/PagedResponse.cs | 4 +- WebApi/Controllers/JobController.cs | 2 +- WebApi/Controllers/LogController.cs | 92 +- WebApi/Controllers/NotificationController.cs | 2 +- .../TransactionCategoryController.cs | 2 +- .../TransactionRecordController.cs | 37 +- WebApi/Middleware/RequestIdMiddleware.cs | 6 +- WebApi/Program.cs | 2 +- v2.pen | 4954 +++++++++++++++++ 46 files changed, 6074 insertions(+), 301 deletions(-) create mode 100644 Web/src/assets/theme.css create mode 100644 Web/src/views/CalendarV2.vue create mode 100644 v2.pen diff --git a/Common/ServiceExtension.cs b/Common/ServiceExtension.cs index 6bfc6d8..c3d47dd 100644 --- a/Common/ServiceExtension.cs +++ b/Common/ServiceExtension.cs @@ -29,7 +29,7 @@ public static class ServiceExtension // 注册所有服务实现 RegisterServices(services, serviceAssembly); - + // 注册所有仓储实现 RegisterRepositories(services, repositoryAssembly); @@ -74,7 +74,7 @@ public static class ServiceExtension foreach (var type in types) { var interfaces = type.GetInterfaces() - .Where(i => i.Name.StartsWith("I") + .Where(i => i.Name.StartsWith("I") && i is { Namespace: "Repository", IsGenericType: false }); // 排除泛型接口如 IBaseRepository foreach (var @interface in interfaces) diff --git a/Entity/TransactionRecord.cs b/Entity/TransactionRecord.cs index 1cad578..2a05de2 100644 --- a/Entity/TransactionRecord.cs +++ b/Entity/TransactionRecord.cs @@ -18,7 +18,7 @@ public class TransactionRecord : BaseEntity /// /// 交易金额 /// -public decimal Amount { get; set; } + public decimal Amount { get; set; } /// /// 交易后余额 @@ -60,7 +60,7 @@ public decimal Amount { get; set; } /// public string ImportNo { get; set; } = string.Empty; -/// + /// /// 导入来源 /// public string ImportFrom { get; set; } = string.Empty; diff --git a/Repository/BudgetRepository.cs b/Repository/BudgetRepository.cs index 3c16a60..edeeda4 100644 --- a/Repository/BudgetRepository.cs +++ b/Repository/BudgetRepository.cs @@ -3,7 +3,7 @@ public interface IBudgetRepository : IBaseRepository { Task GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate); - + Task UpdateBudgetCategoryNameAsync(string oldName, string newName, TransactionType type); } @@ -35,7 +35,7 @@ public class BudgetRepository(IFreeSql freeSql) : BaseRepository(f public async Task UpdateBudgetCategoryNameAsync(string oldName, string newName, TransactionType type) { var records = await FreeSql.Select() - .Where(b => b.SelectedCategories.Contains(oldName) && + .Where(b => b.SelectedCategories.Contains(oldName) && ((type == TransactionType.Expense && b.Category == BudgetCategory.Expense) || (type == TransactionType.Income && b.Category == BudgetCategory.Income))) .ToListAsync(); diff --git a/Repository/EmailMessageRepository.cs b/Repository/EmailMessageRepository.cs index ea1a09c..34cb484 100644 --- a/Repository/EmailMessageRepository.cs +++ b/Repository/EmailMessageRepository.cs @@ -3,7 +3,7 @@ public interface IEmailMessageRepository : IBaseRepository { Task ExistsAsync(string md5); - + /// /// 分页获取邮件列表(游标分页) /// @@ -12,7 +12,7 @@ public interface IEmailMessageRepository : IBaseRepository /// 每页数量 /// 邮件列表、最后接收时间和最后ID Task<(List list, DateTime? lastReceivedDate, long lastId)> GetPagedListAsync(DateTime? lastReceivedDate, long? lastId, int pageSize = 20); - + /// /// 获取总数 /// @@ -31,20 +31,20 @@ public class EmailMessageRepository(IFreeSql freeSql) : BaseRepository list, DateTime? lastReceivedDate, long lastId)> GetPagedListAsync(DateTime? lastReceivedDate, long? lastId, int pageSize = 20) { var query = FreeSql.Select(); - + // 如果提供了游标,则获取小于游标位置的记录 if (lastReceivedDate.HasValue && lastId.HasValue) { - query = query.Where(e => e.ReceivedDate < lastReceivedDate.Value || + query = query.Where(e => e.ReceivedDate < lastReceivedDate.Value || (e.ReceivedDate == lastReceivedDate.Value && e.Id < lastId.Value)); } - + var list = await query .OrderByDescending(e => e.ReceivedDate) .OrderByDescending(e => e.Id) .Page(1, pageSize) .ToListAsync(); - + var lastRecord = list.Count > 0 ? list.Last() : null; return (list, lastRecord?.ReceivedDate, lastRecord?.Id ?? 0); } diff --git a/Repository/MessageRecordRepository.cs b/Repository/MessageRecordRepository.cs index a673d3e..92519f8 100644 --- a/Repository/MessageRecordRepository.cs +++ b/Repository/MessageRecordRepository.cs @@ -15,7 +15,7 @@ public class MessageRecordRepository(IFreeSql freeSql) : BaseRepository /// 周期性账单仓储实现 /// -public class TransactionPeriodicRepository(IFreeSql freeSql) +public class TransactionPeriodicRepository(IFreeSql freeSql) : BaseRepository(freeSql), ITransactionPeriodicRepository { public async Task> GetPagedListAsync( - int pageIndex, - int pageSize, + int pageIndex, + int pageSize, string? searchKeyword = null) { var query = FreeSql.Select(); @@ -42,8 +42,8 @@ public class TransactionPeriodicRepository(IFreeSql freeSql) // 搜索关键词 if (!string.IsNullOrWhiteSpace(searchKeyword)) { - query = query.Where(x => - x.Reason.Contains(searchKeyword) || + query = query.Where(x => + x.Reason.Contains(searchKeyword) || x.Classify.Contains(searchKeyword)); } @@ -60,8 +60,8 @@ public class TransactionPeriodicRepository(IFreeSql freeSql) if (!string.IsNullOrWhiteSpace(searchKeyword)) { - query = query.Where(x => - x.Reason.Contains(searchKeyword) || + query = query.Where(x => + x.Reason.Contains(searchKeyword) || x.Classify.Contains(searchKeyword)); } diff --git a/Service/AI/OpenAiService.cs b/Service/AI/OpenAiService.cs index 151a2f6..347fdb5 100644 --- a/Service/AI/OpenAiService.cs +++ b/Service/AI/OpenAiService.cs @@ -18,8 +18,8 @@ public class OpenAiService( public async Task ChatAsync(string systemPrompt, string userPrompt) { var cfg = aiSettings.Value; - if (string.IsNullOrWhiteSpace(cfg.Endpoint) || - string.IsNullOrWhiteSpace(cfg.Key) || + if (string.IsNullOrWhiteSpace(cfg.Endpoint) || + string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); @@ -44,7 +44,7 @@ public class OpenAiService( var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); - + try { using var resp = await http.PostAsync(url, content); @@ -75,8 +75,8 @@ public class OpenAiService( public async Task ChatAsync(string prompt) { var cfg = aiSettings.Value; - if (string.IsNullOrWhiteSpace(cfg.Endpoint) || - string.IsNullOrWhiteSpace(cfg.Key) || + if (string.IsNullOrWhiteSpace(cfg.Endpoint) || + string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); @@ -100,7 +100,7 @@ public class OpenAiService( var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); - + try { using var resp = await http.PostAsync(url, content); @@ -131,8 +131,8 @@ public class OpenAiService( public async IAsyncEnumerable ChatStreamAsync(string prompt) { var cfg = aiSettings.Value; - if (string.IsNullOrWhiteSpace(cfg.Endpoint) || - string.IsNullOrWhiteSpace(cfg.Key) || + if (string.IsNullOrWhiteSpace(cfg.Endpoint) || + string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); @@ -157,11 +157,11 @@ public class OpenAiService( var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); - + using var request = new HttpRequestMessage(HttpMethod.Post, url); request.Content = content; using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - + if (!resp.IsSuccessStatusCode) { var err = await resp.Content.ReadAsStringAsync(); @@ -201,8 +201,8 @@ public class OpenAiService( public async IAsyncEnumerable ChatStreamAsync(string systemPrompt, string userPrompt) { var cfg = aiSettings.Value; - if (string.IsNullOrWhiteSpace(cfg.Endpoint) || - string.IsNullOrWhiteSpace(cfg.Key) || + if (string.IsNullOrWhiteSpace(cfg.Endpoint) || + string.IsNullOrWhiteSpace(cfg.Key) || string.IsNullOrWhiteSpace(cfg.Model)) { logger.LogWarning("未配置 OpenAI/DeepSeek 接口,无法调用 AI"); @@ -228,12 +228,12 @@ public class OpenAiService( var url = cfg.Endpoint.TrimEnd('/') + "/chat/completions"; var json = JsonSerializer.Serialize(payload); using var content = new StringContent(json, Encoding.UTF8, "application/json"); - + // 使用 SendAsync 来支持 HttpCompletionOption using var request = new HttpRequestMessage(HttpMethod.Post, url); request.Content = content; using var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); - + if (!resp.IsSuccessStatusCode) { var err = await resp.Content.ReadAsStringAsync(); diff --git a/Service/AI/TextSegmentService.cs b/Service/AI/TextSegmentService.cs index 8a06163..c234eda 100644 --- a/Service/AI/TextSegmentService.cs +++ b/Service/AI/TextSegmentService.cs @@ -38,7 +38,7 @@ public class TextSegmentService : ITextSegmentService _logger = logger; _segmenter = new JiebaSegmenter(); _extractor = new TfidfExtractor(); - + // 仅添加JiebaNet词典中可能缺失的特定业务词汇 AddCustomWords(); } @@ -109,7 +109,7 @@ public class TextSegmentService : ITextSegmentService filteredKeywords.Add(text.Length > 10 ? text.Substring(0, 10) : text); } - _logger.LogDebug("从文本 '{Text}' 中提取关键词: {Keywords}", + _logger.LogDebug("从文本 '{Text}' 中提取关键词: {Keywords}", text, string.Join(", ", filteredKeywords)); return filteredKeywords; diff --git a/Service/Budget/BudgetSavingsService.cs b/Service/Budget/BudgetSavingsService.cs index 41e551e..be8412b 100644 --- a/Service/Budget/BudgetSavingsService.cs +++ b/Service/Budget/BudgetSavingsService.cs @@ -375,14 +375,14 @@ public class BudgetSavingsService( var currentActual = 0m; if (!string.IsNullOrEmpty(savingsCategories)) { - var cats = new HashSet(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries)); - foreach(var kvp in transactionClassify) - { - if (cats.Contains(kvp.Key.Item1)) - { - currentActual += kvp.Value; - } - } + var cats = new HashSet(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries)); + foreach (var kvp in transactionClassify) + { + if (cats.Contains(kvp.Key.Item1)) + { + currentActual += kvp.Value; + } + } } var record = new BudgetRecord @@ -595,16 +595,14 @@ public class BudgetSavingsService( """); } description.AppendLine(""); - + description.AppendLine($"""

预算收入合计: - { - currentMonthlyIncomeItems.Sum(i => i.limit * i.factor) - + currentYearlyIncomeItems.Sum(i => i.limit) - :N0} + {currentMonthlyIncomeItems.Sum(i => i.limit * i.factor) + + currentYearlyIncomeItems.Sum(i => i.limit):N0}

@@ -644,7 +642,7 @@ public class BudgetSavingsService( """); } description.AppendLine(""); - + archiveExpenseDiff = archiveExpenseItems.Sum(i => i.limit * i.months.Length) - archiveExpenseItems.Sum(i => i.current); description.AppendLine($"""

@@ -714,10 +712,8 @@ public class BudgetSavingsService( 支出预算合计: - { - currentMonthlyExpenseItems.Sum(i => i.limit * i.factor) - + currentYearlyExpenseItems.Sum(i => i.limit) - :N0} + {currentMonthlyExpenseItems.Sum(i => i.limit * i.factor) + + currentYearlyExpenseItems.Sum(i => i.limit):N0}

@@ -769,18 +765,18 @@ public class BudgetSavingsService( #endregion var savingsCategories = await configService.GetConfigByKeyAsync("SavingsCategories") ?? string.Empty; - + var currentActual = 0m; if (!string.IsNullOrEmpty(savingsCategories)) { - var cats = new HashSet(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries)); - foreach(var kvp in transactionClassify) - { - if (cats.Contains(kvp.Key.Item1)) - { - currentActual += kvp.Value; - } - } + var cats = new HashSet(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries)); + foreach (var kvp in transactionClassify) + { + if (cats.Contains(kvp.Key.Item1)) + { + currentActual += kvp.Value; + } + } } var record = new BudgetRecord diff --git a/Service/Budget/BudgetStatsService.cs b/Service/Budget/BudgetStatsService.cs index 92ff938..e468536 100644 --- a/Service/Budget/BudgetStatsService.cs +++ b/Service/Budget/BudgetStatsService.cs @@ -21,7 +21,7 @@ public class BudgetStatsService( { public async Task GetCategoryStatsAsync(BudgetCategory category, DateTime referenceDate) { - logger.LogInformation("开始计算分类统计信息: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}", + logger.LogInformation("开始计算分类统计信息: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}", category, referenceDate); var result = new BudgetCategoryStats(); @@ -31,20 +31,20 @@ public class BudgetStatsService( // 获取月度统计 logger.LogDebug("开始计算月度统计"); result.Month = await CalculateMonthlyCategoryStatsAsync(category, referenceDate); - logger.LogInformation("月度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%", + logger.LogInformation("月度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%", result.Month.Count, result.Month.Limit, result.Month.Current, result.Month.Rate); // 获取年度统计 logger.LogDebug("开始计算年度统计"); result.Year = await CalculateYearlyCategoryStatsAsync(category, referenceDate); - logger.LogInformation("年度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%", + logger.LogInformation("年度统计计算完成: Count={Count}, Limit={Limit}, Current={Current}, Rate={Rate:F2}%", result.Year.Count, result.Year.Limit, result.Year.Current, result.Year.Rate); logger.LogInformation("分类统计信息计算完成"); } catch (Exception ex) { - logger.LogError(ex, "计算分类统计信息时发生错误: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}", + logger.LogError(ex, "计算分类统计信息时发生错误: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM-dd}", category, referenceDate); throw; } @@ -54,7 +54,7 @@ public class BudgetStatsService( private async Task CalculateMonthlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate) { - logger.LogDebug("开始计算月度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM}", + logger.LogDebug("开始计算月度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy-MM}", category, referenceDate); var result = new BudgetStatsDto @@ -94,7 +94,7 @@ public class BudgetStatsService( // 使用 GetAllBudgetsWithArchiveAsync 中已经计算好的 Current 值 var totalCurrent = budgets.Sum(b => b.Current); logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", totalCurrent, budgets.Count); - + var transactionType = category switch { BudgetCategory.Expense => TransactionType.Expense, @@ -129,7 +129,7 @@ public class BudgetStatsService( decimal accumulated = 0; var daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month); logger.LogDebug("本月天数: {DaysInMonth}", daysInMonth); - + for (var i = 1; i <= daysInMonth; i++) { var currentDate = new DateTime(startDate.Year, startDate.Month, i); @@ -143,21 +143,21 @@ public class BudgetStatsService( if (dailyStats.TryGetValue(currentDate.Date, out var amount)) { accumulated += amount; - logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 金额={Amount}, 累计={Accumulated}", + logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 金额={Amount}, 累计={Accumulated}", currentDate, amount, accumulated); } else { - logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 无交易数据,累计={Accumulated}", + logger.LogTrace("日期 {CurrentDate:yyyy-MM-dd}: 无交易数据,累计={Accumulated}", currentDate, accumulated); } - + // 对每一天的累计值应用硬性预算调整 var adjustedAccumulated = accumulated; if (transactionType == TransactionType.Expense) { var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList(); - + // 检查是否为当前月份 var isCurrentMonth = referenceDate.Year == now.Year && referenceDate.Month == now.Month; if (isCurrentMonth && currentDate.Date <= now.Date) @@ -166,31 +166,31 @@ public class BudgetStatsService( // totalMandatoryVirtual是所有硬性预算的虚拟消耗 // 但如果硬性预算有实际交易,accumulated中已经包含了,会重复 // 所以需要:accumulated + (totalMandatoryVirtual - 硬性预算的实际交易部分) - + // 更简单的理解: // - 如果某个硬性预算本月完全没有交易记录,它的虚拟值应该加到accumulated上 // - 如果某个硬性预算有部分交易记录,应该补齐到虚拟值 // - 实现:取 max(accumulated, totalMandatoryVirtual) 是不对的 // - 正确:accumulated + 硬性预算中没有实际交易的那部分的虚拟值 - + // 由于无法精确区分,采用近似方案: // 计算所有硬性预算的Current总和,这个值已经包含了虚拟消耗(在CalculateCurrentAmountAsync中处理) // 计算非硬性预算的交易累计(这部分在accumulated中) // 但accumulated是所有交易的累计,包括硬性预算的实际交易 - + // 最终简化方案: // dailyStats包含所有实际交易(包括硬性预算的实际交易) // 对于没有实际交易的硬性预算,它们的虚拟消耗没有在dailyStats中 // 所以:adjustedAccumulated = accumulated + 没有实际交易的硬性预算的虚拟消耗 - + // 实用方法:每个硬性预算,取 max(它在dailyStats中的累计, 虚拟值) // 但我们无法从dailyStats中提取单个预算的数据 - + // 终极简化:如果硬性预算的Current值等于虚拟值,说明没有实际交易 // 这种情况下,accumulated中不包含这部分,需要加上虚拟值 // 如果Current值大于虚拟值,说明有实际交易,accumulated中已包含,不需要调整 - + decimal mandatoryAdjustment = 0; foreach (var budget in mandatoryBudgets) { @@ -205,16 +205,16 @@ public class BudgetStatsService( var dayOfYear = currentDate.DayOfYear; dailyVirtual = budget.Limit * dayOfYear / daysInYear; } - + // 如果budget.Current约等于整月的虚拟值,说明没有实际交易 // 但Current是整个月的,dailyVirtual是到当前天的 // 需要判断该预算是否有实际交易记录 // 简化:假设如果硬性预算的Current等于虚拟值(误差<1元),就没有实际交易 - - var monthlyVirtual = budget.Type == BudgetPeriodType.Month + + var monthlyVirtual = budget.Type == BudgetPeriodType.Month ? budget.Limit * now.Day / daysInMonth : budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365); - + if (Math.Abs(budget.Current - monthlyVirtual) < 1) { // 没有实际交易,需要添加虚拟消耗 @@ -223,14 +223,14 @@ public class BudgetStatsService( currentDate, budget.Name, dailyVirtual); } } - + adjustedAccumulated += mandatoryAdjustment; } } - + result.Trend.Add(adjustedAccumulated); } - + logger.LogDebug("趋势图数据计算完成(已应用硬性预算调整)"); } else @@ -265,7 +265,7 @@ public class BudgetStatsService( logger.LogInformation("【使用率】{Current}元 ÷ {Limit}元 × 100% = {Rate:F2}%", result.Current, totalLimit, result.Rate); // 5. 生成预算明细汇总日志 - var budgetDetails = budgets.Select(b => + var budgetDetails = budgets.Select(b => { var limit = CalculateBudgetLimit(b, BudgetPeriodType.Month, referenceDate); var prefix = b.IsArchive ? $"({b.ArchiveMonth}月归档)" : ""; @@ -285,7 +285,7 @@ public class BudgetStatsService( private async Task CalculateYearlyCategoryStatsAsync(BudgetCategory category, DateTime referenceDate) { - logger.LogDebug("开始计算年度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy}", + logger.LogDebug("开始计算年度分类统计: Category={Category}, ReferenceDate={ReferenceDate:yyyy}", category, referenceDate); var result = new BudgetStatsDto @@ -338,7 +338,7 @@ public class BudgetStatsService( // 计算当前实际值,考虑硬性预算的特殊逻辑 var totalCurrent = budgets.Sum(b => b.Current); - logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", + logger.LogInformation("【实际值来源】累加各预算的实际值: {TotalCurrent}元(共{Count}个预算)", totalCurrent, budgets.Count); var now = dateTimeProvider.Now; @@ -380,21 +380,21 @@ public class BudgetStatsService( if (dailyStats.TryGetValue(currentMonthDate, out var amount)) { accumulated += amount; - logger.LogTrace("月份 {Month:yyyy-MM}: 金额={Amount}, 累计={Accumulated}", + logger.LogTrace("月份 {Month:yyyy-MM}: 金额={Amount}, 累计={Accumulated}", currentMonthDate, amount, accumulated); } else { - logger.LogTrace("月份 {Month:yyyy-MM}: 无交易数据,累计={Accumulated}", + logger.LogTrace("月份 {Month:yyyy-MM}: 无交易数据,累计={Accumulated}", currentMonthDate, accumulated); } - + // 对每个月的累计值应用硬性预算调整 var adjustedAccumulated = accumulated; if (transactionType == TransactionType.Expense) { var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList(); - + // 检查是否为当前年份 var isCurrentYear = referenceDate.Year == now.Year; if (isCurrentYear && currentMonthDate <= now) @@ -403,7 +403,7 @@ public class BudgetStatsService( foreach (var budget in mandatoryBudgets) { decimal monthlyVirtual = 0; - + if (budget.Type == BudgetPeriodType.Month) { // 月度硬性预算:如果该月已完成,累加整月;如果是当前月,按天数比例 @@ -424,12 +424,12 @@ public class BudgetStatsService( var dayOfYear = i < now.Month ? lastDayOfMonth.DayOfYear : now.DayOfYear; monthlyVirtual = budget.Limit * dayOfYear / daysInYear; } - + // 判断该硬性预算是否有实际交易 - var yearlyVirtual = budget.Type == BudgetPeriodType.Month + var yearlyVirtual = budget.Type == BudgetPeriodType.Month ? budget.Limit * now.Month + (budget.Limit * now.Day / DateTime.DaysInMonth(now.Year, now.Month)) - budget.Limit : budget.Limit * now.DayOfYear / (DateTime.IsLeapYear(now.Year) ? 366 : 365); - + if (Math.Abs(budget.Current - yearlyVirtual) < 1) { // 没有实际交易,需要添加虚拟消耗 @@ -438,11 +438,11 @@ public class BudgetStatsService( currentMonthDate, budget.Name, monthlyVirtual); } } - + adjustedAccumulated += mandatoryAdjustment; } } - + result.Trend.Add(adjustedAccumulated); } @@ -480,7 +480,7 @@ public class BudgetStatsService( logger.LogInformation("【使用率】{Current}元 ÷ {Limit}元 × 100% = {Rate:F2}%", result.Current, totalLimit, result.Rate); // 5. 生成预算明细汇总日志 - var budgetDetails = budgets.Select(b => + var budgetDetails = budgets.Select(b => { var limit = CalculateBudgetLimit(b, BudgetPeriodType.Year, referenceDate); var prefix = b.IsArchive ? "(归档)" : b.RemainingMonths > 0 ? $"(剩余{b.RemainingMonths}月)" : ""; @@ -503,7 +503,7 @@ public class BudgetStatsService( BudgetPeriodType statType, DateTime referenceDate) { - logger.LogDebug("开始获取预算数据: Category={Category}, StatType={StatType}, ReferenceDate={ReferenceDate:yyyy-MM-dd}", + logger.LogDebug("开始获取预算数据: Category={Category}, StatType={StatType}, ReferenceDate={ReferenceDate:yyyy-MM-dd}", category, statType, referenceDate); var result = new List(); @@ -516,7 +516,7 @@ public class BudgetStatsService( if (statType == BudgetPeriodType.Year) { logger.LogDebug("年度统计:开始获取整年的预算数据"); - + // 获取当前有效的预算(用于当前月及未来月) var currentBudgets = await budgetRepository.GetAllAsync(); var currentBudgetsDict = currentBudgets @@ -526,7 +526,7 @@ public class BudgetStatsService( // 用于跟踪已处理的预算ID,避免重复 var processedBudgetIds = new HashSet(); - + // 1. 处理历史归档月份(1月到当前月-1) if (referenceDate.Year == now.Year && now.Month > 1) { @@ -585,7 +585,7 @@ public class BudgetStatsService( } } } - + // 2. 处理当前月及未来月(使用当前预算) logger.LogDebug("开始处理当前及未来月份预算"); foreach (var budget in currentBudgetsDict.Values) @@ -675,7 +675,7 @@ public class BudgetStatsService( // 获取归档数据 logger.LogDebug("开始获取归档数据: Year={Year}, Month={Month}", year, month); var archive = await budgetArchiveRepository.GetArchiveAsync(year, month); - + if (archive != null) { var itemCount = archive.Content.Count(); @@ -714,7 +714,7 @@ public class BudgetStatsService( var budgets = await budgetRepository.GetAllAsync(); var budgetCount = budgets.Count(); logger.LogDebug("获取到 {BudgetCount} 个预算", budgetCount); - + foreach (var budget in budgets) { if (budget.Category == category && ShouldIncludeBudget(budget, statType)) @@ -845,7 +845,7 @@ public class BudgetStatsService( logger.LogInformation("预算 {BudgetName} 限额计算: 原始预算={OriginalLimit}, 计算后限额={CalculatedLimit}, 算法: {Algorithm}", budget.Name, budget.Limit, itemLimit, algorithmDescription); - + return itemLimit; } @@ -888,13 +888,13 @@ public class BudgetStatsService( private decimal ApplyMandatoryBudgetAdjustment(List budgets, decimal currentTotal, DateTime referenceDate, BudgetPeriodType statType) { - logger.LogDebug("开始应用硬性预算调整: 当前总计={CurrentTotal}, 统计类型={StatType}, 参考日期={ReferenceDate:yyyy-MM-dd}", + logger.LogDebug("开始应用硬性预算调整: 当前总计={CurrentTotal}, 统计类型={StatType}, 参考日期={ReferenceDate:yyyy-MM-dd}", currentTotal, statType, referenceDate); var now = dateTimeProvider.Now; var adjustedTotal = currentTotal; var mandatoryBudgets = budgets.Where(b => b.IsMandatoryExpense).ToList(); - + logger.LogDebug("找到 {MandatoryCount} 个硬性预算", mandatoryBudgets.Count); var mandatoryIndex = 0; @@ -906,14 +906,14 @@ public class BudgetStatsService( if (statType == BudgetPeriodType.Month) { isCurrentPeriod = referenceDate.Year == now.Year && referenceDate.Month == now.Month; - logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查月度周期: 参考月份={RefMonth}, 当前月份={CurrentMonth}, 是否为当前周期={IsCurrent}", - mandatoryIndex, mandatoryBudgets.Count, budget.Name, + logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查月度周期: 参考月份={RefMonth}, 当前月份={CurrentMonth}, 是否为当前周期={IsCurrent}", + mandatoryIndex, mandatoryBudgets.Count, budget.Name, referenceDate.ToString("yyyy-MM"), now.ToString("yyyy-MM"), isCurrentPeriod); } else // Year { isCurrentPeriod = referenceDate.Year == now.Year; - logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查年度周期: 参考年份={RefYear}, 当前年份={CurrentYear}, 是否为当前周期={IsCurrent}", + logger.LogInformation("硬性预算 {MandatoryIndex}/{MandatoryCount}: {BudgetName} - 检查年度周期: 参考年份={RefYear}, 当前年份={CurrentYear}, 是否为当前周期={IsCurrent}", mandatoryIndex, mandatoryBudgets.Count, budget.Name, referenceDate.Year, now.Year, isCurrentPeriod); } @@ -923,7 +923,7 @@ public class BudgetStatsService( // 计算硬性预算的应累加值 decimal mandatoryAccumulation = 0; var accumulationAlgorithm = ""; - + if (budget.Type == BudgetPeriodType.Month) { // 月度硬性预算按天数比例累加 @@ -931,7 +931,7 @@ public class BudgetStatsService( var daysElapsed = now.Day; mandatoryAccumulation = budget.Limit * daysElapsed / daysInMonth; accumulationAlgorithm = $"月度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInMonth} = {mandatoryAccumulation:F2}"; - logger.LogDebug("月度硬性预算 {BudgetName}: 限额={Limit}, 本月天数={DaysInMonth}, 已过天数={DaysElapsed}, 应累加值={Accumulation}", + logger.LogDebug("月度硬性预算 {BudgetName}: 限额={Limit}, 本月天数={DaysInMonth}, 已过天数={DaysElapsed}, 应累加值={Accumulation}", budget.Name, budget.Limit, daysInMonth, daysElapsed, mandatoryAccumulation); } else if (budget.Type == BudgetPeriodType.Year) @@ -941,7 +941,7 @@ public class BudgetStatsService( var daysElapsed = now.DayOfYear; mandatoryAccumulation = budget.Limit * daysElapsed / daysInYear; accumulationAlgorithm = $"年度硬性预算按天数比例: {budget.Limit} × {daysElapsed} ÷ {daysInYear} = {mandatoryAccumulation:F2}"; - logger.LogDebug("年度硬性预算 {BudgetName}: 限额={Limit}, 本年天数={DaysInYear}, 已过天数={DaysElapsed}, 应累加值={Accumulation}", + logger.LogDebug("年度硬性预算 {BudgetName}: 限额={Limit}, 本年天数={DaysInYear}, 已过天数={DaysElapsed}, 应累加值={Accumulation}", budget.Name, budget.Limit, daysInYear, daysElapsed, mandatoryAccumulation); } @@ -958,7 +958,7 @@ public class BudgetStatsService( } else { - logger.LogDebug("硬性预算 {BudgetName} 未触发调整: 当前总计={CurrentTotal} >= 应累加值={MandatoryAccumulation}", + logger.LogDebug("硬性预算 {BudgetName} 未触发调整: 当前总计={CurrentTotal} >= 应累加值={MandatoryAccumulation}", budget.Name, adjustedTotal, mandatoryAccumulation); } } @@ -1029,7 +1029,7 @@ public class BudgetStatsService( var budgetLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Month, referenceDate); var typeLabel = budget.IsMandatoryExpense ? "硬性" : "普通"; var archiveLabel = budget.IsArchive ? $" ({budget.ArchiveMonth}月归档)" : ""; - + description.AppendLine($""" {budget.Name}{archiveLabel} @@ -1138,8 +1138,8 @@ public class BudgetStatsService( { var budgetLimit = CalculateBudgetLimit(budget, BudgetPeriodType.Year, referenceDate); var typeStr = budget.IsCurrentMonth ? "当前月" : "未来月"; - var calcStr = budget.IsCurrentMonth - ? $"1月×{budget.Limit:N0}" + var calcStr = budget.IsCurrentMonth + ? $"1月×{budget.Limit:N0}" : $"{budget.RemainingMonths}月×{budget.Limit:N0}"; description.AppendLine($""" @@ -1195,7 +1195,7 @@ public class BudgetStatsService( description.AppendLine($"

计算公式

"); description.AppendLine($"

年度预算额度合计:"); var limitParts = new List(); - + // 归档月度预算部分 foreach (var group in archivedMonthlyBudgets.GroupBy(b => b.Id)) { @@ -1204,7 +1204,7 @@ public class BudgetStatsService( var groupTotalLimit = first.Limit * count; limitParts.Add($"{first.Name}(归档{count}月×{first.Limit:N0}={groupTotalLimit:N0})"); } - + // 当前月度预算部分 foreach (var budget in currentMonthlyBudgets) { @@ -1218,18 +1218,18 @@ public class BudgetStatsService( limitParts.Add($"{budget.Name}(剩余{budget.RemainingMonths}月×{budget.Limit:N0}={budgetLimit:N0})"); } } - + // 年度预算部分 foreach (var budget in archivedYearlyBudgets.Concat(currentYearlyBudgets)) { limitParts.Add($"{budget.Name}({budget.Limit:N0})"); } - + description.AppendLine($"{string.Join(" + ", limitParts)} = {totalLimit:N0}

"); description.AppendLine($"

实际{categoryName}合计:"); var currentParts = new List(); - + // 归档月度预算的实际值 foreach (var group in archivedMonthlyBudgets.GroupBy(b => b.Id)) { @@ -1237,13 +1237,13 @@ public class BudgetStatsService( var groupTotalCurrent = group.Sum(b => b.Current); currentParts.Add($"{first.Name}(归档{groupTotalCurrent:N1})"); } - + // 年度预算的实际值 foreach (var budget in archivedYearlyBudgets.Concat(currentYearlyBudgets)) { currentParts.Add($"{budget.Name}({budget.Current:N1})"); } - + description.AppendLine($"{string.Join(" + ", currentParts)} = {totalCurrent:N1}

"); var rate = totalLimit > 0 ? totalCurrent / totalLimit * 100 : 0; diff --git a/Service/EmailServices/EmailFetchService.cs b/Service/EmailServices/EmailFetchService.cs index 644144b..9633773 100644 --- a/Service/EmailServices/EmailFetchService.cs +++ b/Service/EmailServices/EmailFetchService.cs @@ -74,13 +74,13 @@ public class EmailFetchService(ILogger logger) : IEmailFetchS _useSsl = useSsl; _email = email; _password = password; - + // 如果已连接,先断开 if (_imapClient?.IsConnected == true) { await DisconnectAsync(); } - + _imapClient = new ImapClient(); if (useSsl) @@ -206,7 +206,7 @@ public class EmailFetchService(ILogger logger) : IEmailFetchS // 标记邮件为已读(设置Seen标记) await inbox.AddFlagsAsync(uid, MessageFlags.Seen, silent: false); - + _logger.LogDebug("邮件 {Uid} 标记已读操作已提交", uid); } catch (Exception ex) @@ -240,13 +240,13 @@ public class EmailFetchService(ILogger logger) : IEmailFetchS } return _imapClient?.IsConnected == true; } - + if (string.IsNullOrEmpty(_host) || string.IsNullOrEmpty(_email)) { _logger.LogWarning("未初始化连接信息,无法自动重连"); return false; } - + _logger.LogInformation("检测到连接断开,尝试重新连接到 {Email}...", _email); return await ConnectAsync(_host, _port, _useSsl, _email, _password); } diff --git a/Service/EmailServices/EmailHandleService.cs b/Service/EmailServices/EmailHandleService.cs index 0ab307b..8c5629d 100644 --- a/Service/EmailServices/EmailHandleService.cs +++ b/Service/EmailServices/EmailHandleService.cs @@ -179,7 +179,7 @@ public class EmailHandleService( { var clone = records.ToArray().DeepClone(); - if(clone?.Any() != true) + if (clone?.Any() != true) { return; } diff --git a/Service/EmailServices/EmailParse/EmailParseForm95555.cs b/Service/EmailServices/EmailParse/EmailParseForm95555.cs index 09daef0..f051eb8 100644 --- a/Service/EmailServices/EmailParse/EmailParseForm95555.cs +++ b/Service/EmailServices/EmailParse/EmailParseForm95555.cs @@ -72,7 +72,7 @@ public class EmailParseForm95555( var balanceStr = match.Groups["balance"].Value; var typeStr = match.Groups["type"].Value; var reason = match.Groups["reason"].Value; - if(string.IsNullOrEmpty(reason)) + if (string.IsNullOrEmpty(reason)) { reason = typeStr; } diff --git a/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs b/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs index d9e6c2c..2036fdc 100644 --- a/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs +++ b/Service/EmailServices/EmailParse/EmailParseFormCcsvc.cs @@ -13,7 +13,7 @@ public partial class EmailParseFormCcsvc( { [GeneratedRegex("<.*?>")] private static partial Regex HtmlRegex(); - + public override bool CanParse(string from, string subject, string body) { if (!from.Contains("ccsvc@message.cmbchina.com")) @@ -141,7 +141,7 @@ public partial class EmailParseFormCcsvc( } // 招商信用卡特殊,消费金额为正数,退款为负数 - if(amount > 0) + if (amount > 0) { type = TransactionType.Expense; } diff --git a/Service/EmailServices/EmailParse/IEmailParseServices.cs b/Service/EmailServices/EmailParse/IEmailParseServices.cs index 9195076..3ddaed4 100644 --- a/Service/EmailServices/EmailParse/IEmailParseServices.cs +++ b/Service/EmailServices/EmailParse/IEmailParseServices.cs @@ -47,7 +47,7 @@ public abstract class EmailParseServicesBase( // AI兜底 result = await ParseByAiAsync(emailContent) ?? []; - if(result.Length == 0) + if (result.Length == 0) { logger.LogWarning("AI解析邮件内容也未能提取到任何交易记录"); } @@ -65,10 +65,10 @@ public abstract class EmailParseServicesBase( )[]> ParseEmailContentAsync(string emailContent); private async Task<( - string card, - string reason, - decimal amount, - decimal balance, + string card, + string reason, + decimal amount, + decimal balance, TransactionType type, DateTime? occurredAt )[]?> ParseByAiAsync(string body) @@ -175,7 +175,7 @@ public abstract class EmailParseServicesBase( } var occurredAt = (DateTime?)null; - if(DateTime.TryParse(occurredAtStr, out var occurredAtValue)) + if (DateTime.TryParse(occurredAtStr, out var occurredAtValue)) { occurredAt = occurredAtValue; } diff --git a/Service/EmailServices/EmailSyncService.cs b/Service/EmailServices/EmailSyncService.cs index 9fcd415..96a9319 100644 --- a/Service/EmailServices/EmailSyncService.cs +++ b/Service/EmailServices/EmailSyncService.cs @@ -199,12 +199,12 @@ public class EmailSyncService( message.TextBody ?? message.HtmlBody ?? string.Empty ) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3))) { - #if DEBUG +#if DEBUG logger.LogDebug("DEBUG 模式下,跳过标记已读步骤"); - #else +#else // 标记邮件为已读 await emailFetchService.MarkAsReadAsync(uid); - #endif +#endif } } catch (Exception ex) diff --git a/Service/ImportService.cs b/Service/ImportService.cs index 7c3d1ca..e30c18a 100644 --- a/Service/ImportService.cs +++ b/Service/ImportService.cs @@ -283,7 +283,7 @@ public class ImportService( DateTime GetDateTimeValue(IDictionary row, string key) { - if(!row.ContainsKey(key)) + if (!row.ContainsKey(key)) { return DateTime.MinValue; } diff --git a/Service/Jobs/BudgetArchiveJob.cs b/Service/Jobs/BudgetArchiveJob.cs index fbafcee..141cd71 100644 --- a/Service/Jobs/BudgetArchiveJob.cs +++ b/Service/Jobs/BudgetArchiveJob.cs @@ -24,7 +24,7 @@ public class BudgetArchiveJob( using var scope = serviceProvider.CreateScope(); var budgetService = scope.ServiceProvider.GetRequiredService(); - + // 归档月度数据 var result = await budgetService.ArchiveBudgetsAsync(year, month); diff --git a/Service/Jobs/DbBackupJob.cs b/Service/Jobs/DbBackupJob.cs index 4208fc4..bf1a357 100644 --- a/Service/Jobs/DbBackupJob.cs +++ b/Service/Jobs/DbBackupJob.cs @@ -15,7 +15,7 @@ public class DbBackupJob( try { logger.LogInformation("开始执行数据库备份任务"); - + // 数据库文件路径 (基于 appsettings.json 中的配置: database/EmailBill.db) var dbPath = Path.Combine(env.ContentRootPath, "database", "EmailBill.db"); var backupDir = Path.Combine(env.ContentRootPath, "database", "backups"); @@ -48,7 +48,7 @@ public class DbBackupJob( var filesToDelete = files.Skip(20); foreach (var file in filesToDelete) { - try + try { file.Delete(); logger.LogInformation("删除过期备份: {Name}", file.Name); diff --git a/Service/Jobs/EmailSyncJob.cs b/Service/Jobs/EmailSyncJob.cs index 0b29d5b..06f0001 100644 --- a/Service/Jobs/EmailSyncJob.cs +++ b/Service/Jobs/EmailSyncJob.cs @@ -144,12 +144,12 @@ public class EmailSyncJob( message.TextBody ?? message.HtmlBody ?? string.Empty ) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3))) { - #if DEBUG +#if DEBUG logger.LogDebug("DEBUG 模式下,跳过标记已读步骤"); - #else +#else // 标记邮件为已读 await emailFetchService.MarkAsReadAsync(uid); - #endif +#endif } } catch (Exception ex) diff --git a/Service/Message/MessageService.cs b/Service/Message/MessageService.cs index f3570bf..cee6c93 100644 --- a/Service/Message/MessageService.cs +++ b/Service/Message/MessageService.cs @@ -30,9 +30,9 @@ public class MessageService(IMessageRecordRepository messageRepo, INotificationS } public async Task AddAsync( - string title, - string content, - MessageType type = MessageType.Text, + string title, + string content, + MessageType type = MessageType.Text, string? url = null ) { @@ -56,7 +56,7 @@ public class MessageService(IMessageRecordRepository messageRepo, INotificationS { var message = await messageRepo.GetByIdAsync(id); if (message == null) return false; - + message.IsRead = true; message.UpdateTime = DateTime.Now; return await messageRepo.UpdateAsync(message); diff --git a/Service/Transaction/TransactionPeriodicService.cs b/Service/Transaction/TransactionPeriodicService.cs index cd65b8a..c5859a0 100644 --- a/Service/Transaction/TransactionPeriodicService.cs +++ b/Service/Transaction/TransactionPeriodicService.cs @@ -31,10 +31,10 @@ public class TransactionPeriodicService( try { logger.LogInformation("开始执行周期性账单检查..."); - + var pendingBills = await periodicRepository.GetPendingPeriodicBillsAsync(); var billsList = pendingBills.ToList(); - + logger.LogInformation("找到 {Count} 条需要执行的周期性账单", billsList.Count); foreach (var bill in billsList) @@ -61,10 +61,10 @@ public class TransactionPeriodicService( }; var success = await transactionRepository.AddAsync(transaction); - + if (success) { - logger.LogInformation("成功创建周期性账单交易记录: {Reason}, 金额: {Amount}", + logger.LogInformation("成功创建周期性账单交易记录: {Reason}, 金额: {Amount}", bill.Reason, bill.Amount); // 创建未读消息 @@ -80,8 +80,8 @@ public class TransactionPeriodicService( var now = DateTime.Now; var nextTime = CalculateNextExecuteTime(bill, now); await periodicRepository.UpdateExecuteTimeAsync(bill.Id, now, nextTime); - - logger.LogInformation("周期性账单 {Id} 下次执行时间: {NextTime}", + + logger.LogInformation("周期性账单 {Id} 下次执行时间: {NextTime}", bill.Id, nextTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "无"); } else @@ -114,7 +114,7 @@ public class TransactionPeriodicService( } var today = DateTime.Today; - + // 如果从未执行过,需要执行 if (bill.LastExecuteTime == null) { @@ -236,7 +236,7 @@ public class TransactionPeriodicService( return null; var currentDayOfWeek = (int)baseTime.DayOfWeek; - + // 找下一个执行日 var nextDay = executeDays.FirstOrDefault(d => d > currentDayOfWeek); if (nextDay > 0) @@ -244,7 +244,7 @@ public class TransactionPeriodicService( var daysToAdd = nextDay - currentDayOfWeek; return baseTime.Date.AddDays(daysToAdd); } - + // 下周的第一个执行日 var firstDay = executeDays.First(); var daysUntilNextWeek = 7 - currentDayOfWeek + firstDay; @@ -293,7 +293,7 @@ public class TransactionPeriodicService( var currentQuarterStartMonth = ((baseTime.Month - 1) / 3) * 3 + 1; var nextQuarterStartMonth = currentQuarterStartMonth + 3; var nextQuarterYear = baseTime.Year; - + if (nextQuarterStartMonth > 12) { nextQuarterStartMonth = 1; @@ -318,7 +318,7 @@ public class TransactionPeriodicService( // 处理闰年情况 var daysInYear = DateTime.IsLeapYear(nextYear) ? 366 : 365; var actualDay = Math.Min(dayOfYear, daysInYear); - + return new DateTime(nextYear, 1, 1).AddDays(actualDay - 1); } } diff --git a/Web/src/api/transactionRecord.js b/Web/src/api/transactionRecord.js index de2840a..e57d32a 100644 --- a/Web/src/api/transactionRecord.js +++ b/Web/src/api/transactionRecord.js @@ -82,6 +82,7 @@ export const createTransaction = (data) => { * @param {number} data.balance - 交易后余额 * @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支) * @param {string} data.classify - 交易分类 + * @param {string} [data.occurredAt] - 交易时间 * @returns {Promise<{success: boolean}>} */ export const updateTransaction = (data) => { diff --git a/Web/src/assets/theme.css b/Web/src/assets/theme.css new file mode 100644 index 0000000..3365887 --- /dev/null +++ b/Web/src/assets/theme.css @@ -0,0 +1,184 @@ +/** + * EmailBill 主题系统 - 根据 v2.pen 设计稿 + * 用于保持整个应用色彩和布局一致性 + */ + +:root { + /* ============ 颜色变量 - 浅色主题 ============ */ + + /* 背景色 */ + --bg-primary: #FFFFFF; + --bg-secondary: #F6F7F8; + --bg-tertiary: #F3F4F6; + --bg-button: #F5F5F5; + + /* 文字颜色 */ + --text-primary: #1A1A1A; + --text-secondary: #6B7280; + --text-tertiary: #9CA3AF; + + /* 强调色 */ + --accent-primary: #FF6B6B; + --accent-danger: #EF4444; + --accent-warning: #D97706; + --accent-warning-bg: #FFFBEB; + --accent-success: #22C55E; + --accent-success-bg: #F0FDF4; + --accent-info: #6366F1; + --accent-info-bg: #E0E7FF; + + /* 图标色 */ + --icon-star: #FF6B6B; + --icon-coffee: #FCD34D; + + /* ============ 布局变量 ============ */ + + /* 间距 */ + --spacing-xs: 2px; + --spacing-sm: 4px; + --spacing-md: 8px + --spacing-lg: 12px; + --spacing-xl: 16px; + --spacing-2xl: 20px; + --spacing-3xl: 24px; + + /* 圆角 */ + --radius-sm: 12px; + --radius-md: 16px; + --radius-lg: 20px; + --radius-full: 22px; + + /* 字体大小 */ + --font-xs: 9px; + --font-sm: 11px; + --font-base: 12px; + --font-md: 13px; + --font-lg: 15px; + --font-xl: 18px; + --font-2xl: 24px; + --font-3xl: 32px; + + /* 字体粗细 */ + --font-medium: 500; + --font-semibold: 600; + --font-bold: 700; + --font-extrabold: 800; + + /* 字体 */ + --font-primary: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-display: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + + /* 阴影 (可选) */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.05); +} + +/* ============ 深色主题 ============ */ +[data-theme="dark"] { + /* 背景色 */ + --bg-primary: #09090B; + --bg-secondary: #18181b; + --bg-tertiary: #27272a; + --bg-button: #27272a; + + /* 文字颜色 */ + --text-primary: #f4f4f5; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + + /* 强调色 (深色主题调整) */ + --accent-primary: #FF6B6B; + --accent-danger: #f87171; + --accent-warning: #fbbf24; + --accent-warning-bg: #451a03; + --accent-success: #4ade80; + --accent-success-bg: #064e3b; + --accent-info: #818cf8; + --accent-info-bg: #312e81; + + /* 图标色 (深色主题) */ + --icon-star: #FF6B6B; + --icon-coffee: #FCD34D; +} + +/* ============ 通用工具类 ============ */ + +/* 文字 */ +.text-primary { + color: var(--text-primary); +} + +.text-secondary { + color: var(--text-secondary); +} + +.text-tertiary { + color: var(--text-tertiary); +} + +.text-danger { + color: var(--accent-danger); +} + +/* 背景 */ +.bg-primary { + background-color: var(--bg-primary); +} + +.bg-secondary { + background-color: var(--bg-secondary); +} + +.bg-tertiary { + background-color: var(--bg-tertiary); +} + +/* 布局容器 */ +.container-fluid { + width: 100%; + max-width: 402px; + margin: 0 auto; +} + +/* Flex 布局 */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-center { + justify-content: center; +} + +/* 间距 */ +.gap-xs { gap: var(--spacing-xs); } +.gap-sm { gap: var(--spacing-sm); } +.gap-md { gap: var(--spacing-md); } +.gap-lg { gap: var(--spacing-lg); } +.gap-xl { gap: var(--spacing-xl); } +.gap-2xl { gap: var(--spacing-2xl); } +.gap-3xl { gap: var(--spacing-3xl); } + +/* 内边距 */ +.p-sm { padding: var(--spacing-md); } +.p-md { padding: var(--spacing-xl); } +.p-lg { padding: var(--spacing-2xl); } +.p-xl { padding: var(--spacing-3xl); } + +/* 圆角 */ +.rounded-sm { border-radius: var(--radius-sm); } +.rounded-md { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-full { border-radius: var(--radius-full); } diff --git a/Web/src/components/TransactionDetail.vue b/Web/src/components/TransactionDetail.vue index 0fffcda..c890809 100644 --- a/Web/src/components/TransactionDetail.vue +++ b/Web/src/components/TransactionDetail.vue @@ -275,7 +275,7 @@ const handleTypeChange = () => { const onConfirmDate = ({ selectedValues }) => { const dateStr = selectedValues.join('-') const timeStr = currentTime.value.join(':') - editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString() + editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss') showDatePicker.value = false // 接着选时间 showTimePicker.value = true @@ -285,7 +285,7 @@ const onConfirmTime = ({ selectedValues }) => { currentTime.value = selectedValues const dateStr = currentDate.value.join('-') const timeStr = selectedValues.join(':') - editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString() + editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss') showTimePicker.value = false } diff --git a/Web/src/main.js b/Web/src/main.js index a82eab1..5bcb612 100644 --- a/Web/src/main.js +++ b/Web/src/main.js @@ -1,4 +1,5 @@ import './assets/main.css' +import './assets/theme.css' import './styles/common.css' import './styles/rich-content.css' diff --git a/Web/src/views/CalendarV2.vue b/Web/src/views/CalendarV2.vue new file mode 100644 index 0000000..d79365f --- /dev/null +++ b/Web/src/views/CalendarV2.vue @@ -0,0 +1,630 @@ + + + + + diff --git a/WebApi.Test/Budget/BudgetStatsTest.cs b/WebApi.Test/Budget/BudgetStatsTest.cs index db4f0bb..d53a9c2 100644 --- a/WebApi.Test/Budget/BudgetStatsTest.cs +++ b/WebApi.Test/Budget/BudgetStatsTest.cs @@ -21,7 +21,7 @@ public class BudgetStatsTest : BaseTest public BudgetStatsTest() { _dateTimeProvider.Now.Returns(new DateTime(2024, 1, 15)); - + IBudgetStatsService budgetStatsService = new BudgetStatsService( _budgetRepository, _budgetArchiveRepository, @@ -29,7 +29,7 @@ public class BudgetStatsTest : BaseTest _dateTimeProvider, Substitute.For>() ); - + _service = new BudgetService( _budgetRepository, _budgetArchiveRepository, @@ -89,7 +89,7 @@ public class BudgetStatsTest : BaseTest _budgetRepository.GetAllAsync().Returns(budgets); _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(0m); // 实际支出的金额为0 - + _dateTimeProvider.Now.Returns(referenceDate); _transactionStatisticsService.GetFilteredTrendStatisticsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); @@ -117,7 +117,7 @@ public class BudgetStatsTest : BaseTest }; _budgetRepository.GetAllAsync().Returns(budgets); - + // 月度统计使用趋势统计数据(只包含月度预算的分类) _transactionStatisticsService.GetFilteredTrendStatisticsAsync( Arg.Is(d => d.Year == 2024 && d.Month == 1 && d.Day == 1), @@ -128,7 +128,7 @@ public class BudgetStatsTest : BaseTest { { new DateTime(2024, 1, 15), 800m } // 1月15日月度吃饭累计800 }); - + // 年度统计使用GetCurrentAmountAsync _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(args => @@ -136,22 +136,22 @@ public class BudgetStatsTest : BaseTest var b = (BudgetRecord)args[0]; var startDate = (DateTime)args[1]; var endDate = (DateTime)args[2]; - + // 月度范围查询 - 月度吃饭(1月) if (startDate.Month == 1 && startDate.Day == 1 && endDate.Month == 1) { return b.Name == "月度吃饭" ? 800m : 0m; } - + // 年度范围查询 - 年度旅游 if (startDate.Month == 1 && startDate.Day == 1 && endDate.Month == 12) { return b.Name == "年度旅游" ? 2000m : 0m; } - + return 0m; }); - + // 年度趋势统计(包含所有分类) _transactionStatisticsService.GetFilteredTrendStatisticsAsync( Arg.Is(d => d.Year == 2024 && d.Month == 1 && d.Day == 1), @@ -172,7 +172,7 @@ public class BudgetStatsTest : BaseTest result.Month.Limit.Should().Be(3000); // 月度吃饭3000 result.Month.Current.Should().Be(800); // 月度吃饭已用800(从GetCurrentAmountAsync获取) result.Month.Count.Should().Be(1); // 只包含1个月度预算 - + // 年度统计中:包含所有预算(月度预算按剩余月份折算) // 1月时,月度预算分为:当前月(1月) + 剩余月份(2-12月共11个月) result.Year.Limit.Should().Be(12000 + (3000 * 12)); // 年度旅游12000 + 月度吃饭折算年度(3000*12=36000) = 48000 @@ -239,7 +239,7 @@ public class BudgetStatsTest : BaseTest { // Arrange var referenceDate = new DateTime(2024, 1, 15); - + // 设置预算:包含月度预算和年度预算 var budgets = new List { @@ -257,7 +257,7 @@ public class BudgetStatsTest : BaseTest }; _budgetRepository.GetAllAsync().Returns(budgets); - + // 设置月度预算的当前金额 _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(args => @@ -272,7 +272,7 @@ public class BudgetStatsTest : BaseTest _ => 0m }; }); - + // 设置月度趋势统计数据:只包含月度预算相关的分类(餐饮、零食、交通) // 注意:不应包含年度预算的分类(旅游、度假、奖金、年终奖) _transactionStatisticsService.GetFilteredTrendStatisticsAsync( @@ -284,7 +284,7 @@ public class BudgetStatsTest : BaseTest { { new DateTime(2024, 1, 15), 1500m } // 1月15日累计1500(吃喝1200+交通300,不包含年度旅游2000) }); - + // 设置年度趋势统计数据:包含所有预算相关的分类 _transactionStatisticsService.GetFilteredTrendStatisticsAsync( Arg.Is(d => d.Year == 2024 && d.Month == 1 && d.Day == 1), @@ -296,7 +296,7 @@ public class BudgetStatsTest : BaseTest { { new DateTime(2024, 1, 1), 3500m } // 1月累计3500(吃喝1200+交通300+年度旅游2000) }); - + // 设置收入相关的趋势统计数据 _transactionStatisticsService.GetFilteredTrendStatisticsAsync( Arg.Is(d => d.Year == 2024 && d.Month == 1), @@ -304,7 +304,7 @@ public class BudgetStatsTest : BaseTest TransactionType.Income, Arg.Any>()) .Returns(new Dictionary()); // 月度收入为空 - + _transactionStatisticsService.GetFilteredTrendStatisticsAsync( Arg.Is(d => d.Year == 2024 && d.Month == 1 && d.Day == 1), Arg.Is(d => d.Year == 2024 && d.Month == 12 && d.Day == 31), @@ -318,7 +318,7 @@ public class BudgetStatsTest : BaseTest // Act - 测试支出统计 var expenseResult = await _service.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); - + // Act - 测试收入统计 var incomeResult = await _service.GetCategoryStatsAsync(BudgetCategory.Income, referenceDate); @@ -345,17 +345,17 @@ public class BudgetStatsTest : BaseTest incomeResult.Year.Count.Should().Be(1); // 包含1个年度收入预算 } - + [Fact] public async Task GetCategoryStats_年度_3月_2月预算变更_Test() { // Arrange // 测试场景:2024年3月查看年度预算统计,其中2月份发生了预算变更(吃喝预算从2000增加到2500) var referenceDate = new DateTime(2024, 3, 15); - + // 设置当前时间,确保3月被认为是当前月份 _dateTimeProvider.Now.Returns(new DateTime(2024, 3, 15)); - + // 当前3月份有效的预算 var currentBudgets = new List { @@ -426,11 +426,11 @@ public class BudgetStatsTest : BaseTest // 设置仓储响应 _budgetRepository.GetAllAsync().Returns(currentBudgets); - + // 设置归档仓储响应 _budgetArchiveRepository.GetArchiveAsync(2024, 2).Returns(febArchive); _budgetArchiveRepository.GetArchiveAsync(2024, 1).Returns(janArchive); - + // 设置月度预算的当前金额查询(仅用于3月份) _budgetRepository.GetCurrentAmountAsync(Arg.Any(), Arg.Is(d => d.Month == 3), Arg.Is(d => d.Month == 3)) .Returns(args => @@ -443,11 +443,11 @@ public class BudgetStatsTest : BaseTest _ => 0m }; }); - + // 年度旅游的年度金额查询 _budgetRepository.GetCurrentAmountAsync( - Arg.Is(b => b.Id == 3), - Arg.Is(d => d.Month == 1), + Arg.Is(b => b.Id == 3), + Arg.Is(d => d.Month == 1), Arg.Is(d => d.Month == 12)) .Returns(2500m); // 年度旅游1-3月已花费2500 @@ -477,11 +477,11 @@ public class BudgetStatsTest : BaseTest // 3月累计:月度预算1000 + 年度旅游2500 = 3500 { new DateTime(2024, 3, 1), 3500m } }); - + // 补充:年度旅游的GetCurrentAmountAsync调用(用于计算Current) _budgetRepository.GetCurrentAmountAsync( - Arg.Is(b => b.Id == 3), - Arg.Is(d => d.Month == 1), + Arg.Is(b => b.Id == 3), + Arg.Is(d => d.Month == 1), Arg.Is(d => d.Month == 12)) .Returns(2500m); // 年度旅游1-3月已花费2500 @@ -494,7 +494,7 @@ public class BudgetStatsTest : BaseTest _dateTimeProvider, Substitute.For>() ); - + var result = await budgetStatsService.GetCategoryStatsAsync(BudgetCategory.Expense, referenceDate); // Assert - 月度统计(3月份) @@ -502,7 +502,7 @@ public class BudgetStatsTest : BaseTest result.Month.Limit.Should().Be(3000); // 吃喝2500 + 交通500(使用变更后的预算) result.Month.Current.Should().Be(1000); // 吃喝800 + 交通200 result.Month.Count.Should().Be(2); // 包含2个月度预算 - + // Assert - 年度统计(需要考虑预算变更和剩余月份) // 新逻辑: // 1. 对于归档数据,直接使用归档的限额,不折算 @@ -515,7 +515,7 @@ public class BudgetStatsTest : BaseTest // 年度旅游:12000 // 总计:2500 + 2500 + 30000 + 12000 = 47000 result.Year.Limit.Should().Be(47000); - + // 预期年度实际金额: // 根据趋势统计数据,3月累计: 月度预算1000 + 年度旅游2500 = 3500 // 但业务代码会累加所有预算项的Current值: @@ -528,7 +528,7 @@ public class BudgetStatsTest : BaseTest // - 年度旅游:2500 // 总计:1500+250+1800+300+800+200+2500 = 7350 result.Year.Current.Should().Be(7350); - + // 应该包含: // - 1月归档的月度预算:吃喝、1个 // - 1月归档的月度预算:交通、1个 @@ -541,7 +541,7 @@ public class BudgetStatsTest : BaseTest // - 年度旅游:1个 // 总计:9个 result.Year.Count.Should().Be(9); - + // 验证使用率计算正确 result.Month.Rate.Should().BeApproximately(1000m / 3000m * 100, 0.01m); // 年度使用率:7350 / 47000 * 100 = 15.64% diff --git a/WebApi.Test/Repository/BudgetRepositoryTest.cs b/WebApi.Test/Repository/BudgetRepositoryTest.cs index 3cef877..14edde8 100644 --- a/WebApi.Test/Repository/BudgetRepositoryTest.cs +++ b/WebApi.Test/Repository/BudgetRepositoryTest.cs @@ -60,7 +60,7 @@ public class BudgetRepositoryTest : TransactionTestBase var b1_updated = all.First(b => b.Name == "B1"); b1_updated.SelectedCategories.Should().Contain("美食"); b1_updated.SelectedCategories.Should().NotContain("餐饮"); - + var b2_updated = all.First(b => b.Name == "B2"); b2_updated.SelectedCategories.Should().Be("美食"); } diff --git a/WebApi.Test/Repository/ConfigRepositoryTest.cs b/WebApi.Test/Repository/ConfigRepositoryTest.cs index c54b9bc..770fee3 100644 --- a/WebApi.Test/Repository/ConfigRepositoryTest.cs +++ b/WebApi.Test/Repository/ConfigRepositoryTest.cs @@ -13,7 +13,7 @@ public class ConfigRepositoryTest : RepositoryTestBase public async Task GetByKeyAsync_获取配置_Test() { await _repository.AddAsync(new ConfigEntity { Key = "k1", Value = "v1" }); - + var config = await _repository.GetByKeyAsync("k1"); config.Should().NotBeNull(); config.Value.Should().Be("v1"); diff --git a/WebApi.Test/Repository/EmailMessageRepositoryTest.cs b/WebApi.Test/Repository/EmailMessageRepositoryTest.cs index e7d1f08..39b3dda 100644 --- a/WebApi.Test/Repository/EmailMessageRepositoryTest.cs +++ b/WebApi.Test/Repository/EmailMessageRepositoryTest.cs @@ -13,10 +13,10 @@ public class EmailMessageRepositoryTest : RepositoryTestBase public async Task ExistsAsync_检查存在_Test() { await _repository.AddAsync(new EmailMessage { Md5 = "md5_value", Subject = "Test" }); - + var msg = await _repository.ExistsAsync("md5_value"); msg.Should().NotBeNull(); - + var notfound = await _repository.ExistsAsync("other"); notfound.Should().BeNull(); } @@ -33,7 +33,7 @@ public class EmailMessageRepositoryTest : RepositoryTestBase // Assuming ID order follows insertion (mostly true for snowflakes if generated sequentially) // But ReceivedDate is the primary sort in logic usually. // Let's verify standard cursor pagination usually sorts by Date DESC, ID DESC. - + await _repository.AddAsync(m1); await _repository.AddAsync(m2); await _repository.AddAsync(m3); diff --git a/WebApi.Test/Repository/PushSubscriptionRepositoryTest.cs b/WebApi.Test/Repository/PushSubscriptionRepositoryTest.cs index 9ee1c00..aa4292f 100644 --- a/WebApi.Test/Repository/PushSubscriptionRepositoryTest.cs +++ b/WebApi.Test/Repository/PushSubscriptionRepositoryTest.cs @@ -13,7 +13,7 @@ public class PushSubscriptionRepositoryTest : RepositoryTestBase public async Task GetByEndpointAsync_通过Endpoint获取_Test() { await _repository.AddAsync(new PushSubscription { Endpoint = "ep1" }); - + var sub = await _repository.GetByEndpointAsync("ep1"); sub.Should().NotBeNull(); sub.Endpoint.Should().Be("ep1"); diff --git a/WebApi.Test/Repository/TransactionPeriodicRepositoryTest.cs b/WebApi.Test/Repository/TransactionPeriodicRepositoryTest.cs index 2f6c5aa..208533e 100644 --- a/WebApi.Test/Repository/TransactionPeriodicRepositoryTest.cs +++ b/WebApi.Test/Repository/TransactionPeriodicRepositoryTest.cs @@ -14,10 +14,10 @@ public class TransactionPeriodicRepositoryTest : TransactionTestBase { // 应该执行的:NextExecuteTime <= Now await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill1", NextExecuteTime = DateTime.Now.AddDays(-1), IsEnabled = true }); - + // 不该执行的:NextExecuteTime > Now await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill2", NextExecuteTime = DateTime.Now.AddDays(1), IsEnabled = true }); - + // 不该执行的:未激活 await _repository.AddAsync(new TransactionPeriodic { Reason = "Bill3", NextExecuteTime = DateTime.Now.AddDays(-1), IsEnabled = false }); diff --git a/WebApi.Test/Repository/TransactionRecordRepositoryTest.cs b/WebApi.Test/Repository/TransactionRecordRepositoryTest.cs index 3c700a3..0afac23 100644 --- a/WebApi.Test/Repository/TransactionRecordRepositoryTest.cs +++ b/WebApi.Test/Repository/TransactionRecordRepositoryTest.cs @@ -47,11 +47,11 @@ public class TransactionRecordRepositoryTest : TransactionTestBase var results = await _repository.QueryAsync( startDate: new DateTime(2023, 1, 1), endDate: new DateTime(2023, 2, 28)); // Include Feb - + results.Should().HaveCount(2); } - [Fact] + [Fact] public async Task QueryAsync_按年月筛选_Test() { await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 1, 15))); @@ -99,7 +99,7 @@ public class TransactionRecordRepositoryTest : TransactionTestBase var records = await _repository.QueryAsync(reason: "麦当劳"); records.All(r => r.Classify == "快餐").Should().BeTrue(); - + var kfc = await _repository.QueryAsync(reason: "肯德基"); kfc.First().Classify.Should().Be("餐饮"); } diff --git a/WebApi.Test/Transaction/TransactionPeriodicServiceTest.cs b/WebApi.Test/Transaction/TransactionPeriodicServiceTest.cs index 052a1ba..9b2c3c8 100644 --- a/WebApi.Test/Transaction/TransactionPeriodicServiceTest.cs +++ b/WebApi.Test/Transaction/TransactionPeriodicServiceTest.cs @@ -51,7 +51,7 @@ public class TransactionPeriodicServiceTest : BaseTest // Assert // Service inserts Amount directly from periodicBill.Amount (100 is positive) await _transactionRepository.Received(1).AddAsync(Arg.Is(t => - t.Amount == 100m && + t.Amount == 100m && t.Type == TransactionType.Expense && t.Classify == "餐饮" && t.Reason == "每日餐费" && @@ -69,7 +69,7 @@ public class TransactionPeriodicServiceTest : BaseTest await _periodicRepository.Received(1).UpdateExecuteTimeAsync( Arg.Is(1L), - Arg.Any(), + Arg.Any(), Arg.Any() ); } @@ -149,7 +149,7 @@ public class TransactionPeriodicServiceTest : BaseTest m.Content.Contains("每月工资") )); } - + [Fact] public async Task ExecutePeriodicBillsAsync_未达到执行时间() { @@ -158,7 +158,7 @@ public class TransactionPeriodicServiceTest : BaseTest { Id = 1, PeriodicType = PeriodicType.Weekly, - PeriodicConfig = "1,3,5", + PeriodicConfig = "1,3,5", Amount = 200m, Type = TransactionType.Expense, Classify = "交通", @@ -191,7 +191,7 @@ public class TransactionPeriodicServiceTest : BaseTest Classify = "餐饮", Reason = "每日餐费", IsEnabled = true, - LastExecuteTime = DateTime.Today, + LastExecuteTime = DateTime.Today, NextExecuteTime = DateTime.Today.AddDays(1) }; diff --git a/WebApi.Test/Transaction/TransactionStatisticsServiceTest.cs b/WebApi.Test/Transaction/TransactionStatisticsServiceTest.cs index 4b6ec85..47f9e71 100644 --- a/WebApi.Test/Transaction/TransactionStatisticsServiceTest.cs +++ b/WebApi.Test/Transaction/TransactionStatisticsServiceTest.cs @@ -93,7 +93,7 @@ public class TransactionStatisticsServiceTest : BaseTest // Mock Logic: filter by year (Arg[0]) and month (Arg[1]) and type (Arg[4]) if provided _transactionRepository.QueryAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() - ).Returns(callInfo => + ).Returns(callInfo => { var y = callInfo.ArgAt(0); var m = callInfo.ArgAt(1); @@ -106,7 +106,7 @@ public class TransactionStatisticsServiceTest : BaseTest // In GetTrendStatisticsAsync: transactionRepository.QueryAsync(year: targetYear, month: targetMonth...) // It does NOT pass type. So type is null. // But Service THEN filters by Type in memory. - + return query.ToList(); }); @@ -178,7 +178,7 @@ public class TransactionStatisticsServiceTest : BaseTest // Assert result[("餐饮", TransactionType.Expense)].Should().Be(-150m); // Expect Negative (Sum of amounts) } - + // Additional tests from original file to maintain coverage, with minimal adjustments if needed [Fact] public async Task GetCategoryStatisticsAsync_支出分类() @@ -190,18 +190,18 @@ public class TransactionStatisticsServiceTest : BaseTest new() { Amount = -50m, Type = TransactionType.Expense, Classify = "餐饮" }, new() { Amount = -200m, Type = TransactionType.Expense, Classify = "交通" } }; - + // Mock filtering by Type - _transactionRepository.QueryAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() - ).Returns(callInfo => - { - var type = callInfo.ArgAt(4); - return testData.Where(t => !type.HasValue || t.Type == type).ToList(); - }); + _transactionRepository.QueryAsync( + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any() + ).Returns(callInfo => + { + var type = callInfo.ArgAt(4); + return testData.Where(t => !type.HasValue || t.Type == type).ToList(); + }); var result = await _service.GetCategoryStatisticsAsync(year, month, TransactionType.Expense); - + result.First(c => c.Classify == "餐饮").Amount.Should().Be(150m); result.First(c => c.Classify == "交通").Amount.Should().Be(200m); } diff --git a/WebApi/Controllers/Dto/BaseResponse.cs b/WebApi/Controllers/Dto/BaseResponse.cs index b0fae8c..760cddc 100644 --- a/WebApi/Controllers/Dto/BaseResponse.cs +++ b/WebApi/Controllers/Dto/BaseResponse.cs @@ -94,7 +94,7 @@ public class BaseResponse : BaseResponse /// 返回数据 /// public T? Data { get; set; } - + public new static BaseResponse Fail(string message) { return new BaseResponse diff --git a/WebApi/Controllers/Dto/PagedResponse.cs b/WebApi/Controllers/Dto/PagedResponse.cs index 1cb5acb..2bf18c5 100644 --- a/WebApi/Controllers/Dto/PagedResponse.cs +++ b/WebApi/Controllers/Dto/PagedResponse.cs @@ -3,7 +3,7 @@ public class PagedResponse : BaseResponse { public long LastId { get; set; } - + /// /// 最后一条记录的时间(用于游标分页) /// @@ -13,7 +13,7 @@ public class PagedResponse : BaseResponse /// 总记录数 /// public int Total { get; set; } - + public new static PagedResponse Fail(string message) { return new PagedResponse diff --git a/WebApi/Controllers/JobController.cs b/WebApi/Controllers/JobController.cs index fa4cef8..cb1467d 100644 --- a/WebApi/Controllers/JobController.cs +++ b/WebApi/Controllers/JobController.cs @@ -21,7 +21,7 @@ public class JobController(ISchedulerFactory schedulerFactory, ILogger logger) : ControllerBase /// /// 获取日志列表(分页) /// -[HttpGet] -public async Task> GetListAsync( - [FromQuery] int pageIndex = 1, - [FromQuery] int pageSize = 50, - [FromQuery] string? searchKeyword = null, - [FromQuery] string? logLevel = null, - [FromQuery] string? date = null, - [FromQuery] string? className = null -) + [HttpGet] + public async Task> GetListAsync( + [FromQuery] int pageIndex = 1, + [FromQuery] int pageSize = 50, + [FromQuery] string? searchKeyword = null, + [FromQuery] string? logLevel = null, + [FromQuery] string? date = null, + [FromQuery] string? className = null + ) { try { @@ -221,7 +221,7 @@ public async Task> GetListAsync( logger.LogError(ex, "获取类名列表失败"); return $"获取类名列表失败: {ex.Message}".Fail(); } -} + } /// /// 合并多行日志(已废弃,现在在流式读取中处理) @@ -385,27 +385,27 @@ public async Task> GetListAsync( /// /// 读取日志 /// -private async Task<(List entries, int total)> ReadLogsAsync( - string path, - int pageIndex, - int pageSize, - string? searchKeyword, - string? logLevel, - string? className) -{ - var allLines = await ReadAllLinesAsync(path); - - var merged = MergeMultiLineLog(allLines); - - var parsed = new List(); - foreach (var line in merged) + private async Task<(List entries, int total)> ReadLogsAsync( + string path, + int pageIndex, + int pageSize, + string? searchKeyword, + string? logLevel, + string? className) { - var entry = ParseLogLine(line); - if (entry != null && PassFilter(entry, searchKeyword, logLevel, className)) + var allLines = await ReadAllLinesAsync(path); + + var merged = MergeMultiLineLog(allLines); + + var parsed = new List(); + foreach (var line in merged) { - parsed.Add(entry); + var entry = ParseLogLine(line); + if (entry != null && PassFilter(entry, searchKeyword, logLevel, className)) + { + parsed.Add(entry); + } } - } parsed.Reverse(); @@ -419,28 +419,28 @@ private async Task<(List entries, int total)> ReadLogsAsync( /// /// 检查日志条目是否通过过滤条件 /// -private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel, string? className) -{ - if (!string.IsNullOrEmpty(searchKeyword) && - !logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase)) + private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel, string? className) { - return false; - } + if (!string.IsNullOrEmpty(searchKeyword) && + !logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase)) + { + return false; + } - if (!string.IsNullOrEmpty(logLevel) && - !logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + if (!string.IsNullOrEmpty(logLevel) && + !logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase)) + { + return false; + } - if (!string.IsNullOrEmpty(className) && - !logEntry.ClassName.Equals(className, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + if (!string.IsNullOrEmpty(className) && + !logEntry.ClassName.Equals(className, StringComparison.OrdinalIgnoreCase)) + { + return false; + } - return true; -} + return true; + } /// /// 读取文件所有行(支持共享读取) diff --git a/WebApi/Controllers/NotificationController.cs b/WebApi/Controllers/NotificationController.cs index ae137fb..502bfaa 100644 --- a/WebApi/Controllers/NotificationController.cs +++ b/WebApi/Controllers/NotificationController.cs @@ -32,7 +32,7 @@ public class NotificationController(INotificationService notificationService) : return ex.Message.Fail(); } } - + public async Task TestNotification([FromQuery] string message) { try diff --git a/WebApi/Controllers/TransactionCategoryController.cs b/WebApi/Controllers/TransactionCategoryController.cs index a8c3818..18e2be7 100644 --- a/WebApi/Controllers/TransactionCategoryController.cs +++ b/WebApi/Controllers/TransactionCategoryController.cs @@ -26,7 +26,7 @@ public class TransactionCategoryController( { categories = (await categoryRepository.GetAllAsync()).ToList(); } - + return categories.Ok(); } catch (Exception ex) diff --git a/WebApi/Controllers/TransactionRecordController.cs b/WebApi/Controllers/TransactionRecordController.cs index 33bd09c..793cd3a 100644 --- a/WebApi/Controllers/TransactionRecordController.cs +++ b/WebApi/Controllers/TransactionRecordController.cs @@ -38,18 +38,18 @@ public class TransactionRecordController( : classify.Split(',', StringSplitOptions.RemoveEmptyEntries); TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null; -var list = await transactionRepository.QueryAsync( - year: year, - month: month, - startDate: startDate, - endDate: endDate, - type: transactionType, - classifies: classifies, - searchKeyword: searchKeyword, - reason: reason, - pageIndex: pageIndex, - pageSize: pageSize, - sortByAmount: sortByAmount); + var list = await transactionRepository.QueryAsync( + year: year, + month: month, + startDate: startDate, + endDate: endDate, + type: transactionType, + classifies: classifies, + searchKeyword: searchKeyword, + reason: reason, + pageIndex: pageIndex, + pageSize: pageSize, + sortByAmount: sortByAmount); var total = await transactionRepository.CountAsync( year: year, month: month, @@ -214,6 +214,12 @@ var list = await transactionRepository.QueryAsync( transaction.Type = dto.Type; transaction.Classify = dto.Classify ?? string.Empty; + // 更新交易时间 + if (!string.IsNullOrEmpty(dto.OccurredAt) && DateTime.TryParse(dto.OccurredAt, out var occurredAt)) + { + transaction.OccurredAt = occurredAt; + } + // 清除待确认状态 transaction.UnconfirmedClassify = null; transaction.UnconfirmedType = null; @@ -272,7 +278,7 @@ var list = await transactionRepository.QueryAsync( // 获取每日统计数据 var statistics = await transactionStatisticsService.GetDailyStatisticsAsync(year, month, savingClassify); - + // 按日期排序并计算累积余额 var sortedStats = statistics.OrderBy(s => s.Key).ToList(); var result = new List(); @@ -693,7 +699,7 @@ var list = await transactionRepository.QueryAsync( } } -private async Task WriteEventAsync(string eventType, string data) + private async Task WriteEventAsync(string eventType, string data) { var message = $"event: {eventType}\ndata: {data}\n\n"; await Response.WriteAsync(message); @@ -728,7 +734,8 @@ public record UpdateTransactionDto( decimal Amount, decimal Balance, TransactionType Type, - string? Classify + string? Classify, + string? OccurredAt = null ); /// diff --git a/WebApi/Middleware/RequestIdMiddleware.cs b/WebApi/Middleware/RequestIdMiddleware.cs index 3d995da..23b6945 100644 --- a/WebApi/Middleware/RequestIdMiddleware.cs +++ b/WebApi/Middleware/RequestIdMiddleware.cs @@ -14,9 +14,9 @@ public class RequestIdMiddleware public async Task InvokeAsync(HttpContext context) { var requestId = context.Request.Headers["X-Request-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString("N"); - + context.Items["RequestId"] = requestId; - + using (LogContext.PushProperty("RequestId", requestId)) { await _next(context); @@ -30,7 +30,7 @@ public static class RequestIdExtensions { return context.Items["RequestId"] as string; } - + public static IApplicationBuilder UseRequestId(this IApplicationBuilder builder) { return builder.UseMiddleware(); diff --git a/WebApi/Program.cs b/WebApi/Program.cs index 0dc3548..85205ca 100644 --- a/WebApi/Program.cs +++ b/WebApi/Program.cs @@ -110,7 +110,7 @@ var fsql = new FreeSqlBuilder() .UseAutoSyncStructure(true) .UseLazyLoading(true) .UseMonitorCommand( - cmd => + cmd => { Log.Verbose("执行SQL: {Sql}", cmd.CommandText); } diff --git a/v2.pen b/v2.pen new file mode 100644 index 0000000..34176e0 --- /dev/null +++ b/v2.pen @@ -0,0 +1,4954 @@ +{ + "version": "2.6", + "children": [ + { + "type": "frame", + "id": "bi8Au", + "x": -1331, + "y": 369, + "name": "Frame", + "clip": true, + "width": 2770, + "height": 1217, + "fill": "#a2a2a2ff", + "layout": "none", + "children": [ + { + "type": "frame", + "id": "PlVHb", + "x": 440, + "y": 14, + "name": "Calendar (Dark)", + "width": 402, + "fill": "#09090B", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "gZpES", + "name": "header", + "width": "fill_container", + "gap": 4, + "padding": [ + 8, + 24 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KchUY", + "name": "headerContent", + "width": 142, + "height": 39, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "y8X8F", + "name": "subtitle", + "fill": "#a1a1aa", + "content": "January 2026", + "fontFamily": "DM Sans", + "fontSize": 24, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "f1LMO", + "name": "notifBtn", + "width": 44, + "height": 44, + "fill": "#27272a", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "UMWrV", + "name": "icon", + "width": 20, + "height": 20, + "iconFontName": "bell", + "iconFontFamily": "lucide", + "fill": "#f4f4f5" + } + ] + } + ] + }, + { + "type": "frame", + "id": "2f9Lt", + "name": "calContainer", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "9uhuL", + "name": "weekDays", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "text", + "id": "bM7Rv", + "name": "dayMon", + "fill": "#71717a", + "content": "M", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Yq6GF", + "name": "dayTue", + "fill": "#71717a", + "content": "T", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "CmxXJ", + "name": "dayWed", + "fill": "#71717a", + "content": "W", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "mtCPr", + "name": "dayThu", + "fill": "#71717a", + "content": "T", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "OrNBC", + "name": "dayFri", + "fill": "#71717a", + "content": "F", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "wr9aj", + "name": "daySat", + "fill": "#71717a", + "content": "S", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "vkGTs", + "name": "daySun", + "fill": "#71717a", + "content": "S", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "8OVHm", + "name": "calGrid", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "PoZdj", + "name": "weekRow1", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "dbYbN", + "name": "d1", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KRJ9P", + "name": "dc1", + "width": 32, + "height": 32, + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "5Wsnk", + "name": "t1", + "fill": "#52525b", + "content": "29", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "IjHl0", + "name": "am1", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "cBULU", + "name": "d2", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "PRMhS", + "name": "dc2", + "width": 32, + "height": 32, + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "GyZ9e", + "name": "t2", + "fill": "#52525b", + "content": "30", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "K7WwK", + "name": "am2", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "vN0jc", + "name": "d3", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "pb3in", + "name": "dc3", + "width": 32, + "height": 32, + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "owoDO", + "name": "t3", + "fill": "#52525b", + "content": "31", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "zRuua", + "name": "am3", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "y4VZo", + "name": "d4", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UOMXP", + "name": "dc4", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R3LzE", + "name": "t4", + "fill": "#f4f4f5", + "content": "1", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "U63Y9", + "name": "am4", + "fill": "#71717a", + "content": "128", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "yaSMg", + "name": "d5", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "c6drb", + "name": "dc5", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dKXbg", + "name": "t5", + "fill": "#f4f4f5", + "content": "2", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "UoLQr", + "name": "am5", + "fill": "#71717a", + "content": "45", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "qaMDR", + "name": "d6", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gUgyM", + "name": "dc6", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tCNCN", + "name": "t6", + "fill": "#f4f4f5", + "content": "3", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "RWZ6K", + "name": "am6", + "fill": "#71717a", + "content": "230", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "1VQYS", + "name": "d7", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "oAc5u", + "name": "dc7", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "FSZDz", + "name": "t7", + "fill": "#f4f4f5", + "content": "4", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "Ya1v0", + "name": "am7", + "fill": "#71717a", + "content": "12", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "B9B0z", + "name": "weekRow2", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "NXFJE", + "name": "d8", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "GcXgt", + "name": "dc8", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tWHpg", + "name": "t8", + "fill": "#f4f4f5", + "content": "5", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "8hc20", + "name": "am8", + "fill": "#71717a", + "content": "88", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "YL5R1", + "name": "d9", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "QVmej", + "name": "dc9", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uH1vo", + "name": "t9", + "fill": "#f4f4f5", + "content": "6", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "1sMYh", + "name": "am9", + "fill": "#f87171", + "content": "223", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "lb4DB", + "name": "d10", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "5B45F", + "name": "dc10", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "c6QXz", + "name": "t10", + "fill": "#f4f4f5", + "content": "7", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "uuanY", + "name": "am10", + "fill": "#71717a", + "content": "15", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Zy1uP", + "name": "d11", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "plRut", + "name": "dc11", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WjwqP", + "name": "t11", + "fill": "#f4f4f5", + "content": "8", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "jTBmH", + "name": "am11", + "fill": "#71717a", + "content": "34", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "pxQYa", + "name": "d12", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "N43l5", + "name": "dc12", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "z4Be0", + "name": "t12", + "fill": "#f4f4f5", + "content": "9", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "oMqUr", + "name": "am12", + "fill": "#71717a", + "content": "120", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "4Izh9", + "name": "d13", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "T2eDU", + "name": "dc13", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "SFJdY", + "name": "t13", + "fill": "#f4f4f5", + "content": "10", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "Ka4QA", + "name": "am13", + "fill": "#71717a", + "content": "56", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "3jd1g", + "name": "d14", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "GzO0j", + "name": "dc14", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Cbwzh", + "name": "t14", + "fill": "#f4f4f5", + "content": "11", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "Oj9rX", + "name": "am14", + "fill": "#f87171", + "content": "442", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "2G5FH", + "name": "weekRow3", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "IiGDk", + "name": "d15", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "QSwXv", + "name": "dc15", + "width": 32, + "height": 32, + "fill": "#FF6B6B", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "MEoRp", + "name": "t15", + "fill": "#FFFFFF", + "content": "12", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "UA6Uw", + "name": "am15", + "fill": "#FF6B6B", + "content": "88", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "5cuAY", + "name": "d16", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "6BPm7", + "name": "dc16", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IXOUv", + "name": "t16", + "fill": "#f4f4f5", + "content": "13", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "ZpAVN", + "name": "am16", + "fill": "#71717a", + "content": "12", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "oKVGz", + "name": "d17", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "2If3g", + "name": "dc17", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "xsR3f", + "name": "t17", + "fill": "#f4f4f5", + "content": "14", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "tqpv6", + "name": "am17", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "GwHZ1", + "name": "d18", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "DcFj1", + "name": "dc18", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vKyRP", + "name": "t18", + "fill": "#f4f4f5", + "content": "15", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "wN52j", + "name": "am18", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "TUJJ0", + "name": "d19", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EVFSp", + "name": "dc19", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CTLn4", + "name": "t19", + "fill": "#f4f4f5", + "content": "16", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "NFNmG", + "name": "am19", + "fill": "#f87171", + "content": "542", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "c96XU", + "name": "d20", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "lkItC", + "name": "dc20", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RChCK", + "name": "t20", + "fill": "#f4f4f5", + "content": "17", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "3JM0X", + "name": "am20", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "dPyYV", + "name": "d21", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "PejUl", + "name": "dc21", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zssXc", + "name": "t21", + "fill": "#f4f4f5", + "content": "18", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "B5z1P", + "name": "am21", + "fill": "#71717a", + "content": "56", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "A88XH", + "name": "weekRow4", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "G6LcR", + "name": "d22", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "1hBSX", + "name": "dc22", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZrJQh", + "name": "t22", + "fill": "#f4f4f5", + "content": "19", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "ehhJi", + "name": "am22", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "y8YpI", + "name": "d23", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "i1JSh", + "name": "dc23", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zQzCM", + "name": "t23", + "fill": "#f4f4f5", + "content": "20", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "wysQz", + "name": "am23", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "frJ4V", + "name": "d24", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "t4m6z", + "name": "dc24", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rs2zW", + "name": "t24", + "fill": "#f4f4f5", + "content": "21", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "IbwTE", + "name": "am24", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "gFbFw", + "name": "d25", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "8RX85", + "name": "dc25", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gUEjR", + "name": "t25", + "fill": "#f4f4f5", + "content": "22", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "MiEIb", + "name": "am25", + "fill": "#71717a", + "content": "125", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "mTHMU", + "name": "d26", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "OMJpQ", + "name": "dc26", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H4pzG", + "name": "t26", + "fill": "#f4f4f5", + "content": "23", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "2p2Tu", + "name": "am26", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "tLtM7", + "name": "d27", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "78SmJ", + "name": "dc27", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l4IJO", + "name": "t27", + "fill": "#f4f4f5", + "content": "24", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "lFzIc", + "name": "am27", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "iVLMS", + "name": "d28", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IPzvR", + "name": "dc28", + "width": 32, + "height": 32, + "fill": "#27272a", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YQW7p", + "name": "t28", + "fill": "#f4f4f5", + "content": "25", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "2reDd", + "name": "am28", + "fill": "#71717a", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "Xm1yI", + "name": "stats", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "WDuPW", + "name": "statsHeader", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KQpEH", + "name": "statsTitle", + "fill": "#f4f4f5", + "content": "Daily Stats", + "fontFamily": "Bricolage Grotesque", + "fontSize": 18, + "fontWeight": "700" + }, + { + "type": "text", + "id": "pCjJG", + "name": "statsDate", + "fill": "#a1a1aa", + "content": "Jan 12, 2026", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "UeZs3", + "name": "statsCard", + "width": "fill_container", + "fill": "#18181b", + "cornerRadius": 20, + "layout": "vertical", + "gap": 12, + "padding": 20, + "children": [ + { + "type": "frame", + "id": "OfF2p", + "name": "statsRow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "sCMmg", + "name": "statsLabel", + "fill": "#a1a1aa", + "content": "Total Spent", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "0M3qd", + "name": "b1", + "fill": "#451a03", + "cornerRadius": 12, + "padding": [ + 6, + 10 + ], + "children": [ + { + "type": "text", + "id": "SLQhU", + "name": "t29", + "fill": "#fbbf24", + "content": "Daily Limit: 2500", + "fontFamily": "DM Sans", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "text", + "id": "Bigj4", + "name": "statsVal", + "fill": "#f4f4f5", + "content": "¥ 1,248.50", + "fontFamily": "Bricolage Grotesque", + "fontSize": 32, + "fontWeight": "800" + } + ] + } + ] + }, + { + "type": "frame", + "id": "rINjA", + "name": "txnList", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "YJSsM", + "name": "txnHeader", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "enDNe", + "name": "txnTitle", + "fill": "#f4f4f5", + "content": "Transactions", + "fontFamily": "Bricolage Grotesque", + "fontSize": 18, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "Z5Pf4", + "name": "rightActions", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "qR22i", + "name": "b2", + "fill": "#064e3b", + "cornerRadius": 12, + "padding": [ + 6, + 12 + ], + "children": [ + { + "type": "text", + "id": "z9Gyq", + "name": "t30", + "fill": "#4ade80", + "content": "3 Items", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "xB6Pm", + "name": "smartBtn", + "fill": "#312e81", + "cornerRadius": 12, + "gap": 6, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "U9fdo", + "name": "iconSmart", + "width": 14, + "height": 14, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#6366F1" + }, + { + "type": "text", + "id": "mAvBq", + "name": "labelSmart", + "fill": "#818cf8", + "content": "Smart", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "8wsnb", + "name": "tCard1", + "width": "fill_container", + "fill": "#18181b", + "cornerRadius": 16, + "gap": 14, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "D468k", + "name": "tCat1", + "width": 44, + "height": 44, + "fill": "#27272a", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "uyr5m", + "name": "icon2", + "width": 20, + "height": 20, + "iconFontName": "star", + "iconFontFamily": "lucide", + "fill": "#FF6B6B" + } + ] + }, + { + "type": "frame", + "id": "b1hWR", + "name": "tContent1", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "iEA77", + "name": "tName1", + "fill": "#f4f4f5", + "content": "Lunch", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "text", + "id": "dUUKV", + "name": "tTime1", + "fill": "#71717a", + "content": "12:30 PM", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "rQZsd", + "name": "tAmount1", + "fill": "#f4f4f5", + "content": "-58.00", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "B65gq", + "name": "tCard2", + "width": "fill_container", + "fill": "#18181b", + "cornerRadius": 16, + "gap": 14, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Y9qhs", + "name": "tCat2", + "width": 44, + "height": 44, + "fill": "#27272a", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "pCURs", + "name": "icon3", + "width": 20, + "height": 20, + "iconFontName": "coffee", + "iconFontFamily": "lucide", + "fill": "#FCD34D" + } + ] + }, + { + "type": "frame", + "id": "bmO5H", + "name": "tContent2", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "MfTBA", + "name": "tName2", + "fill": "#f4f4f5", + "content": "Coffee", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "text", + "id": "VENve", + "name": "tTime2", + "fill": "#71717a", + "content": "08:15 AM", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "mnUuS", + "name": "tAmount2", + "fill": "#f4f4f5", + "content": "-24.50", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "700" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "uxvyO", + "x": 12, + "y": 14, + "name": "Calendar", + "width": 402, + "fill": "#FFFFFF", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "MegAe", + "name": "header", + "rotation": 0.0027546259757699116, + "width": "fill_container", + "gap": 4, + "padding": [ + 8, + 24 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "lIUUB", + "name": "headerContent", + "width": 142, + "height": 39, + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "TRl09", + "name": "subtitle", + "fill": "#1a1a1a", + "content": "January 2026", + "fontFamily": "DM Sans", + "fontSize": 24, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "nHzgV", + "name": "notifBtn", + "width": 44, + "height": 44, + "fill": "#F5F5F5", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "agodm", + "name": "icon", + "width": 20, + "height": 20, + "iconFontName": "bell", + "iconFontFamily": "lucide", + "fill": "#1A1A1A" + } + ] + } + ] + }, + { + "type": "frame", + "id": "9YwOQ", + "name": "calContainer", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "Enjcs", + "name": "weekDays", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "text", + "id": "aezRw", + "name": "dayMon", + "fill": "#9CA3AF", + "content": "M", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "N2dL2", + "name": "dayTue", + "fill": "#9CA3AF", + "content": "T", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Ke1I4", + "name": "dayWed", + "fill": "#9CA3AF", + "content": "W", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "3dtCH", + "name": "dayThu", + "fill": "#9CA3AF", + "content": "T", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "LZt56", + "name": "dayFri", + "fill": "#9CA3AF", + "content": "F", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "Hx4gg", + "name": "daySat", + "fill": "#9CA3AF", + "content": "S", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + }, + { + "type": "text", + "id": "XOxOU", + "name": "daySun", + "fill": "#9CA3AF", + "content": "S", + "textAlign": "center", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "MzWmb", + "name": "calGrid", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "XgyNi", + "name": "weekRow1", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "JBD2B", + "name": "d1", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XhOQb", + "name": "dc1", + "width": 32, + "height": 32, + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "dWILj", + "name": "t1", + "fill": "#E5E7EB", + "content": "29", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "QbkMj", + "name": "am1", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "QtOsX", + "name": "d2", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "RPQpK", + "name": "dc2", + "width": 32, + "height": 32, + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CBkej", + "name": "t2", + "fill": "#E5E7EB", + "content": "30", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "YJFdL", + "name": "am2", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Pii3V", + "name": "d3", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ue39l", + "name": "dc3", + "width": 32, + "height": 32, + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CHPSF", + "name": "t3", + "fill": "#E5E7EB", + "content": "31", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "7Or04", + "name": "am3", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "I7lCG", + "name": "d4", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JQoG1", + "name": "dc4", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "RPCCr", + "name": "t4", + "fill": "#1A1A1A", + "content": "1", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "gpAZh", + "name": "am4", + "fill": "#9CA3AF", + "content": "128", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "mXGeX", + "name": "d5", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "URBQs", + "name": "dc5", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "S9S3w", + "name": "t5", + "fill": "#1A1A1A", + "content": "2", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "N52TS", + "name": "am5", + "fill": "#9CA3AF", + "content": "45", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "loEBf", + "name": "d6", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ON05s", + "name": "dc6", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "6FPhN", + "name": "t6", + "fill": "#1A1A1A", + "content": "3", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "2UghI", + "name": "am6", + "fill": "#9CA3AF", + "content": "230", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "1PGUK", + "name": "d7", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ned05", + "name": "dc7", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CIR5a", + "name": "t7", + "fill": "#1A1A1A", + "content": "4", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "7BADs", + "name": "am7", + "fill": "#9CA3AF", + "content": "12", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Tz6sw", + "name": "weekRow2", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "v6FYA", + "name": "d8", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "WrZZI", + "name": "dc8", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UdO3y", + "name": "t8", + "fill": "#1A1A1A", + "content": "5", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "fy2Yh", + "name": "am8", + "fill": "#9CA3AF", + "content": "88", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "1Oq9t", + "name": "d9", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "AjEZ8", + "name": "dc9", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qLQsc", + "name": "t9", + "fill": "#1A1A1A", + "content": "6", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "3F9fG", + "name": "am9", + "fill": "#EF4444", + "content": "223", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "srFBq", + "name": "d10", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "enmB7", + "name": "dc10", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TfE49", + "name": "t10", + "fill": "#1A1A1A", + "content": "7", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "jCZPU", + "name": "am10", + "fill": "#9CA3AF", + "content": "15", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "kS4Kf", + "name": "d11", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "9qTqV", + "name": "dc11", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Wjqtw", + "name": "t11", + "fill": "#1A1A1A", + "content": "8", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "fxn2U", + "name": "am11", + "fill": "#9CA3AF", + "content": "34", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "f1EBM", + "name": "d12", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "InU2S", + "name": "dc12", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "CSPDo", + "name": "t12", + "fill": "#1A1A1A", + "content": "9", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "NrqQD", + "name": "am12", + "fill": "#9CA3AF", + "content": "120", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "I1fba", + "name": "d13", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EAgBn", + "name": "dc13", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "scRqy", + "name": "t13", + "fill": "#1A1A1A", + "content": "10", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "JOjx8", + "name": "am13", + "fill": "#9CA3AF", + "content": "56", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "JzeYH", + "name": "d14", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "kk5e3", + "name": "dc14", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "451de", + "name": "t14", + "fill": "#1A1A1A", + "content": "11", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "FcKGz", + "name": "am14", + "fill": "#EF4444", + "content": "442", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "qeTBg", + "name": "weekRow3", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "QBeoF", + "name": "d15", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Y4Kg7", + "name": "dc15", + "width": 32, + "height": 32, + "fill": "#FF6B6B", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "8SaXC", + "name": "t15", + "fill": "#FFFFFF", + "content": "12", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "700" + } + ] + }, + { + "type": "text", + "id": "uB5CO", + "name": "am15", + "fill": "#FF6B6B", + "content": "88", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "heNOl", + "name": "d16", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "CKGtu", + "name": "dc16", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "PgD7j", + "name": "t16", + "fill": "#1A1A1A", + "content": "13", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "Ycdcs", + "name": "am16", + "fill": "#9CA3AF", + "content": "12", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "hFEQc", + "name": "d17", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "mXxIa", + "name": "dc17", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "tCINE", + "name": "t17", + "fill": "#1A1A1A", + "content": "14", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "Lj583", + "name": "am17", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "XJVek", + "name": "d18", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IxLqo", + "name": "dc18", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gwmL4", + "name": "t18", + "fill": "#1A1A1A", + "content": "15", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "bNCuc", + "name": "am18", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "FPohm", + "name": "d19", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "TVF3v", + "name": "dc19", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "63eTB", + "name": "t19", + "fill": "#1A1A1A", + "content": "16", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "xhfrp", + "name": "am19", + "fill": "#EF4444", + "content": "542", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "qFLHx", + "name": "d20", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "wGsmZ", + "name": "dc20", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wNBoF", + "name": "t20", + "fill": "#1A1A1A", + "content": "17", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "dkPzp", + "name": "am20", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "GTesZ", + "name": "d21", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "3ophX", + "name": "dc21", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JWTIJ", + "name": "t21", + "fill": "#1A1A1A", + "content": "18", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "N2f1o", + "name": "am21", + "fill": "#9CA3AF", + "content": "56", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "BL0GO", + "name": "weekRow4", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "raA89", + "name": "d22", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "QGMW6", + "name": "dc22", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "OX8Sq", + "name": "t22", + "fill": "#1A1A1A", + "content": "19", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "MmNjH", + "name": "am22", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "3fEct", + "name": "d23", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JFV6n", + "name": "dc23", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "A4z3Z", + "name": "t23", + "fill": "#1A1A1A", + "content": "20", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "75jXV", + "name": "am23", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "YhsCg", + "name": "d24", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "4bAao", + "name": "dc24", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "JtMNu", + "name": "t24", + "fill": "#1A1A1A", + "content": "21", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "uK3E1", + "name": "am24", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "f8Paa", + "name": "d25", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "0ePSV", + "name": "dc25", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ObCLU", + "name": "t25", + "fill": "#1A1A1A", + "content": "22", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "kC1um", + "name": "am25", + "fill": "#9CA3AF", + "content": "125", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "XVmli", + "name": "d26", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XCBcY", + "name": "dc26", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "kP95q", + "name": "t26", + "fill": "#1A1A1A", + "content": "23", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "RK4Ry", + "name": "am26", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "vL4sE", + "name": "d27", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "tP6nn", + "name": "dc27", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "VrxS3", + "name": "t27", + "fill": "#1A1A1A", + "content": "24", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "mvwdI", + "name": "am27", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "nx6xt", + "name": "d28", + "width": 44, + "layout": "vertical", + "gap": 2, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "gUQjY", + "name": "dc28", + "width": 32, + "height": 32, + "fill": "#F3F4F6", + "cornerRadius": 16, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ro5rV", + "name": "t28", + "fill": "#1A1A1A", + "content": "25", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "600" + } + ] + }, + { + "type": "text", + "id": "xXWh0", + "name": "am28", + "fill": "#9CA3AF", + "content": " ", + "fontFamily": "DM Sans", + "fontSize": 9, + "fontWeight": "500" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "vfW2m", + "name": "stats", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "ycEV7", + "name": "statsHeader", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LMdOg", + "name": "statsTitle", + "fill": "#1A1A1A", + "content": "Daily Stats", + "fontFamily": "Bricolage Grotesque", + "fontSize": 18, + "fontWeight": "700" + }, + { + "type": "text", + "id": "1TSK3", + "name": "statsDate", + "fill": "#6B7280", + "content": "Jan 12, 2026", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "JY4Fd", + "name": "statsCard", + "width": "fill_container", + "fill": "#F6F7F8", + "cornerRadius": 20, + "layout": "vertical", + "gap": 12, + "padding": 20, + "children": [ + { + "type": "frame", + "id": "UiJYD", + "name": "statsRow", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "QBHWG", + "name": "statsLabel", + "fill": "#6B7280", + "content": "Total Spent", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "frame", + "id": "6TcZ1", + "name": "b1", + "fill": "#FFFBEB", + "cornerRadius": 12, + "padding": [ + 6, + 10 + ], + "children": [ + { + "type": "text", + "id": "znYtx", + "name": "t29", + "fill": "#D97706", + "content": "Daily Limit: 2500", + "fontFamily": "DM Sans", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + }, + { + "type": "text", + "id": "nNirO", + "name": "statsVal", + "fill": "#1A1A1A", + "content": "¥ 1,248.50", + "fontFamily": "Bricolage Grotesque", + "fontSize": 32, + "fontWeight": "800" + } + ] + } + ] + }, + { + "type": "frame", + "id": "zIeUQ", + "name": "txnList", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "eL4ER", + "name": "txnHeader", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "78oRL", + "name": "txnTitle", + "fill": "#1A1A1A", + "content": "Transactions", + "fontFamily": "Bricolage Grotesque", + "fontSize": 18, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "QEbB1", + "name": "rightActions", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "2qH3a", + "name": "b2", + "fill": "#F0FDF4", + "cornerRadius": 12, + "padding": [ + 6, + 12 + ], + "children": [ + { + "type": "text", + "id": "DkXPS", + "name": "t30", + "fill": "#22C55E", + "content": "3 Items", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "NnlVI", + "name": "smartBtn", + "fill": "#E0E7FF", + "cornerRadius": 12, + "gap": 6, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "3soq6", + "name": "iconSmart", + "width": 14, + "height": 14, + "iconFontName": "sparkles", + "iconFontFamily": "lucide", + "fill": "#6366F1" + }, + { + "type": "text", + "id": "nbGbR", + "name": "labelSmart", + "fill": "#6366F1", + "content": "Smart", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "5H1y6", + "name": "tCard1", + "width": "fill_container", + "fill": "#F6F7F8", + "cornerRadius": 16, + "gap": 14, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "jJBQ3", + "name": "tCat1", + "width": 44, + "height": 44, + "fill": "#FFFFFF", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "99rCw", + "name": "icon2", + "width": 20, + "height": 20, + "iconFontName": "star", + "iconFontFamily": "lucide", + "fill": "#FF6B6B" + } + ] + }, + { + "type": "frame", + "id": "1R9if", + "name": "tContent1", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "tD6vR", + "name": "tName1", + "fill": "#1A1A1A", + "content": "Lunch", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "text", + "id": "PmzGy", + "name": "tTime1", + "fill": "#9CA3AF", + "content": "12:30 PM", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "0u2rh", + "name": "tAmount1", + "fill": "#1A1A1A", + "content": "-58.00", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "GgQ7u", + "name": "tCard2", + "width": "fill_container", + "fill": "#F6F7F8", + "cornerRadius": 16, + "gap": 14, + "padding": 16, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HVEFt", + "name": "tCat2", + "width": 44, + "height": 44, + "fill": "#FFFFFF", + "cornerRadius": 22, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon_font", + "id": "CiSSG", + "name": "icon3", + "width": 20, + "height": 20, + "iconFontName": "coffee", + "iconFontFamily": "lucide", + "fill": "#FCD34D" + } + ] + }, + { + "type": "frame", + "id": "4Bu2K", + "name": "tContent2", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "WeLFp", + "name": "tName2", + "fill": "#1A1A1A", + "content": "Coffee", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "600" + }, + { + "type": "text", + "id": "MpyeL", + "name": "tTime2", + "fill": "#9CA3AF", + "content": "08:15 AM", + "fontFamily": "DM Sans", + "fontSize": 13, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "jUaEO", + "name": "tAmount2", + "fill": "#1A1A1A", + "content": "-24.50", + "fontFamily": "DM Sans", + "fontSize": 15, + "fontWeight": "700" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "5SxtQ", + "x": -464, + "y": 381, + "name": "Stats V4 (SVG Fixed)", + "width": 446, + "height": 1521, + "fill": "#0C0C0C", + "cornerRadius": 16, + "layout": "vertical", + "gap": 24, + "padding": [ + 32, + 24 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "AgxRj", + "name": "headerSection", + "width": "fill_container", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zhPfk", + "name": "headerTitle", + "fill": "#FFFFFF", + "content": "January 2026", + "fontFamily": "Sora", + "fontSize": 18, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "9P4uj", + "name": "headerSubtitle", + "fill": "#8A8A8A", + "content": "Expenses", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mtdwX", + "name": "totalArea", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uQiaE", + "name": "totalLabel", + "fill": "#8A8A8A", + "content": "Total Expenses", + "textAlign": "center", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "normal", + "letterSpacing": 1.5 + }, + { + "type": "text", + "id": "S4Fs3", + "name": "totalValue", + "fill": "#FFFFFF", + "content": "¥4,030", + "textAlign": "center", + "fontFamily": "Sora", + "fontSize": 56, + "fontWeight": "normal", + "letterSpacing": -2.5 + } + ] + }, + { + "type": "frame", + "id": "AUnCh", + "name": "header", + "width": "fill_container", + "height": 70, + "padding": [ + 20, + 24 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "OgWZe", + "name": "dateWrap", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "WGpyI", + "name": "dateTxt", + "fill": "#f4f4f5", + "content": "January 2026", + "fontFamily": "Bricolage Grotesque", + "fontSize": 20, + "fontWeight": "700" + }, + { + "type": "path", + "id": "nKPIO", + "name": "dateIcon", + "width": 16, + "height": 16 + } + ] + }, + { + "type": "path", + "id": "53ZP5", + "name": "chatIcon", + "width": 24, + "height": 24 + } + ] + }, + { + "type": "frame", + "id": "6top3", + "name": "statCards", + "width": "fill_container", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "BkvW7", + "name": "avgCard", + "width": "fill_container", + "fill": "#1A1A1A", + "cornerRadius": 12, + "layout": "vertical", + "gap": 8, + "padding": 16, + "children": [ + { + "type": "text", + "id": "LWquF", + "name": "avgValue", + "fill": "#FFFFFF", + "content": "¥1,343", + "fontFamily": "Sora", + "fontSize": 24, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "85Pqz", + "name": "avgLabel", + "fill": "#8A8A8A", + "content": "Daily Average", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "64JhR", + "name": "itemCard", + "width": "fill_container", + "fill": "#1A1A1A", + "cornerRadius": 12, + "layout": "vertical", + "gap": 8, + "padding": 16, + "children": [ + { + "type": "text", + "id": "hd4i6", + "name": "itemValue", + "fill": "#FFFFFF", + "content": "25", + "fontFamily": "Sora", + "fontSize": 24, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "s7zBo", + "name": "itemLabel", + "fill": "#8A8A8A", + "content": "Total Items", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "1LyRk", + "name": "finalDivider", + "width": "fill_container", + "height": 1, + "fill": "#2A2A2A" + }, + { + "type": "frame", + "id": "E8Pl1", + "name": "progressSection", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "tlLKR", + "name": "progressBar", + "clip": true, + "width": "fill_container", + "height": 6, + "cornerRadius": 3, + "children": [ + { + "type": "frame", + "id": "d8iv1", + "name": "foodProgress", + "width": 120, + "height": "fill_container", + "fill": "#FF3B30" + }, + { + "type": "frame", + "id": "QKEEF", + "name": "transportProgress", + "width": 120, + "height": "fill_container", + "fill": "#3B82F6" + }, + { + "type": "frame", + "id": "A9PpK", + "name": "shoppingProgress", + "width": 118, + "height": "fill_container", + "fill": "#8B5CF6" + } + ] + }, + { + "type": "frame", + "id": "G97sN", + "name": "legendRow", + "width": "fill_container", + "gap": 16, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "B5u2r", + "name": "foodLegend", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JXt6W", + "name": "foodDot", + "width": 8, + "height": 8, + "fill": "#FF3B30", + "cornerRadius": 4 + }, + { + "type": "text", + "id": "cQYfJ", + "name": "foodText", + "fill": "#8A8A8A", + "content": "Food", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "zNx0h", + "name": "transportLegend", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "YbZIK", + "name": "transportDot", + "width": 8, + "height": 8, + "fill": "#3B82F6", + "cornerRadius": 4 + }, + { + "type": "text", + "id": "zNN1M", + "name": "transportText", + "fill": "#8A8A8A", + "content": "Transport", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "CBwOl", + "name": "shoppingLegend", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fJaZb", + "name": "shoppingDot", + "width": 8, + "height": 8, + "fill": "#8B5CF6", + "cornerRadius": 4 + }, + { + "type": "text", + "id": "InVQ4", + "name": "shoppingText", + "fill": "#8A8A8A", + "content": "Shopping", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "JuK2m", + "name": "categorySection", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "FF8uc", + "name": "categoryHeader", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "LjVs8", + "name": "categoryLine", + "width": 24, + "height": 2, + "fill": "#FF3B30" + }, + { + "type": "text", + "id": "AC0ei", + "name": "categoryLabel", + "fill": "#8A8A8A", + "content": "CATEGORIES", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 2 + } + ] + }, + { + "type": "frame", + "id": "ytvad", + "name": "listSection", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "1GHZJ", + "name": "card1", + "width": "fill_container", + "fill": "#1A1A1A", + "cornerRadius": 12, + "gap": 16, + "padding": 16, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "0L649", + "name": "card1Left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "81DZv", + "name": "card1Icon", + "metadata": { + "type": "unsplash", + "username": "maartenvnmlct", + "link": "https://unsplash.com/@maartenvnmlct", + "author": "Maarten" + }, + "width": 40, + "height": 40, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1761245193924-53a5a4bed9ef?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fDE3Njk2NjY5ODF8&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 20, + "layout": "none" + }, + { + "type": "frame", + "id": "0k8Or", + "name": "card1Text", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "tC3eF", + "name": "card1Title", + "fill": "#FFFFFF", + "content": "Food & Dining", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "mD5ou", + "name": "card1Sub", + "fill": "#8A8A8A", + "content": "12 items", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "hvR91", + "name": "card1Right", + "fill": "#FFFFFF", + "content": "-¥840", + "fontFamily": "Sora", + "fontSize": 16, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EziPa", + "name": "card2", + "width": "fill_container", + "fill": "#1A1A1A", + "cornerRadius": 12, + "gap": 16, + "padding": 16, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "ddTYH", + "name": "card2Left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Fvpdk", + "name": "card2Icon", + "metadata": { + "type": "unsplash", + "username": "raphaelrousseauphotography", + "link": "https://unsplash.com/@raphaelrousseauphotography", + "author": "Raphael Rousseau" + }, + "width": 40, + "height": 40, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1768839898435-d685ae66331c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fDE3Njk2NjY5ODF8&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 20, + "layout": "none" + }, + { + "type": "frame", + "id": "NsbZA", + "name": "card2Text", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "m22ip", + "name": "card2Title", + "fill": "#FFFFFF", + "content": "Transport", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "6Qww5", + "name": "card2Sub", + "fill": "#8A8A8A", + "content": "8 trips", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "6V8v3", + "name": "card2Right", + "fill": "#FFFFFF", + "content": "-¥620", + "fontFamily": "Sora", + "fontSize": 16, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yVthk", + "name": "card3", + "width": "fill_container", + "fill": "#1A1A1A", + "cornerRadius": 12, + "gap": 16, + "padding": 16, + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "SdQDB", + "name": "card3Left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "7TbFz", + "name": "card3Icon", + "metadata": { + "type": "unsplash", + "username": "silverkblack", + "link": "https://unsplash.com/@silverkblack", + "author": "Vitaly Gariev" + }, + "width": 40, + "height": 40, + "fill": { + "type": "image", + "enabled": true, + "url": "https://images.unsplash.com/photo-1758525223453-06095a7459ce?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4NDM0ODN8MHwxfHJhbmRvbXx8fHx8fHx8fDE3Njk2NjY5ODJ8&ixlib=rb-4.1.0&q=80&w=1080", + "mode": "fill" + }, + "cornerRadius": 20, + "layout": "none" + }, + { + "type": "frame", + "id": "yixKH", + "name": "card3Text", + "layout": "vertical", + "gap": 4, + "children": [ + { + "type": "text", + "id": "eHVkB", + "name": "card3Title", + "fill": "#FFFFFF", + "content": "Shopping", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ILEMU", + "name": "card3Sub", + "fill": "#8A8A8A", + "content": "5 items", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "xl30n", + "name": "card3Right", + "fill": "#FFFFFF", + "content": "-¥650", + "fontFamily": "Sora", + "fontSize": 16, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "KIXMB", + "name": "content", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "gap": 16, + "padding": [ + 0, + 24, + 24, + 24 + ], + "children": [ + { + "type": "frame", + "id": "8XMYQ", + "name": "chartCard", + "width": "fill_container", + "fill": "#18181b", + "cornerRadius": 24, + "layout": "vertical", + "gap": 24, + "padding": 24, + "children": [ + { + "type": "frame", + "id": "ce8uZ", + "name": "ccHeader", + "width": "fill_container", + "justifyContent": "space_between", + "children": [ + { + "type": "text", + "id": "CUB0A", + "name": "ccTitle", + "fill": "#f4f4f5", + "content": "Expenses", + "fontFamily": "DM Sans", + "fontSize": 16, + "fontWeight": "600" + }, + { + "type": "text", + "id": "LRlb5", + "name": "ccAmt", + "fill": "#a1a1aa", + "content": "¥ 2,520", + "fontFamily": "Bricolage Grotesque", + "fontSize": 16, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "X9Vx2", + "name": "chartRow", + "width": "fill_container", + "gap": 24, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "JmTXA", + "name": "svgBox", + "width": 160, + "height": 160, + "layout": "none", + "children": [ + { + "type": "path", + "id": "OABrB", + "x": 0, + "y": 0, + "name": "slice1", + "fill": "#f59e0b", + "width": 160, + "height": 160 + }, + { + "type": "path", + "id": "jYhTp", + "x": 0, + "y": 0, + "name": "slice2", + "fill": "#3b82f6", + "width": 160, + "height": 160 + }, + { + "type": "path", + "id": "FlwfN", + "x": 0, + "y": 0, + "name": "slice3", + "fill": "#ec4899", + "width": 160, + "height": 160 + }, + { + "type": "ellipse", + "id": "GdBes", + "x": 30, + "y": 30, + "name": "hole", + "fill": "#18181b", + "width": 100, + "height": 100 + }, + { + "type": "frame", + "id": "YmYtY", + "x": 0, + "y": 0, + "name": "centerFrame", + "width": 160, + "height": 160, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "bk1NG", + "name": "centerTxt", + "fill": "#71717a", + "content": "Total", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "iQy2O", + "name": "legendRow", + "width": "fill_container", + "padding": [ + 0, + 12 + ], + "justifyContent": "space_between", + "children": [ + { + "type": "frame", + "id": "fSyOk", + "name": "l1", + "layout": "vertical", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "briMI", + "name": "l1Dot", + "fill": "#f59e0b", + "width": 8, + "height": 8 + }, + { + "type": "text", + "id": "TWYYu", + "name": "l1Txt", + "fill": "#f4f4f5", + "content": "Food", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EZcAF", + "name": "l1Pct", + "fill": "#71717a", + "content": "33%", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Ehjk4", + "name": "l2", + "layout": "vertical", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "w1Qb8", + "name": "l2Dot", + "fill": "#3b82f6", + "width": 8, + "height": 8 + }, + { + "type": "text", + "id": "4DosX", + "name": "l2Txt", + "fill": "#f4f4f5", + "content": "Transport", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pK1dD", + "name": "l2Pct", + "fill": "#71717a", + "content": "33%", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wu9Cc", + "name": "l3", + "layout": "vertical", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "t0vBe", + "name": "l3Dot", + "fill": "#ec4899", + "width": 8, + "height": 8 + }, + { + "type": "text", + "id": "UdWlw", + "name": "l3Txt", + "fill": "#f4f4f5", + "content": "Shopping", + "fontFamily": "DM Sans", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "nAMYg", + "name": "l3Pct", + "fill": "#71717a", + "content": "33%", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "HU4yM", + "name": "CategoryGrid", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "tmAdW", + "name": "ci1", + "width": "fill_container", + "height": 72, + "fill": "#18181b", + "cornerRadius": 16, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "5gmc6", + "name": "ci1Left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "UMsdW", + "name": "ci1Icon", + "width": 40, + "height": 40, + "fill": "#27272a", + "cornerRadius": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jdOef", + "name": "ci1Emoji", + "content": "🍔", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bugbO", + "name": "ci1Text", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "Z0h6e", + "name": "ci1Name", + "fill": "#f4f4f5", + "content": "Food & Dining", + "fontFamily": "DM Sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "gwJsz", + "name": "ci1Sub", + "fill": "#71717a", + "content": "12 txns", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "g58W4", + "name": "ci1Right", + "fill": "#f4f4f5", + "content": "- ¥840", + "fontFamily": "Bricolage Grotesque", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "h1g4Q", + "name": "ci2", + "width": "fill_container", + "height": 72, + "fill": "#18181b", + "cornerRadius": 16, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IwdqZ", + "name": "ci2Left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "FRUpc", + "name": "ci2Icon", + "width": 40, + "height": 40, + "fill": "#27272a", + "cornerRadius": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "hyt7m", + "name": "ci2Emoji", + "content": "🚇", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "RjBVz", + "name": "ci2Text", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "SAZBV", + "name": "ci2Name", + "fill": "#f4f4f5", + "content": "Transport", + "fontFamily": "DM Sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "pN7Du", + "name": "ci2Sub", + "fill": "#71717a", + "content": "8 txns", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "T8Gqv", + "name": "ci2Right", + "fill": "#f4f4f5", + "content": "- ¥620", + "fontFamily": "Bricolage Grotesque", + "fontSize": 14, + "fontWeight": "600" + } + ] + }, + { + "type": "frame", + "id": "VHheL", + "name": "ci3", + "width": "fill_container", + "height": 72, + "fill": "#18181b", + "cornerRadius": 16, + "padding": [ + 0, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "kYgLv", + "name": "ci3Left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "6pj68", + "name": "ci3Icon", + "width": 40, + "height": 40, + "fill": "#27272a", + "cornerRadius": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BpZke", + "name": "ci3Emoji", + "content": "🛍️", + "fontFamily": "Inter", + "fontSize": 18, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AmZVA", + "name": "ci3Text", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "Lzb3y", + "name": "ci3Name", + "fill": "#f4f4f5", + "content": "Shopping", + "fontFamily": "DM Sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "q5Tfg", + "name": "ci3Sub", + "fill": "#71717a", + "content": "4 txns", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "text", + "id": "ckZ4L", + "name": "ci3Right", + "fill": "#f4f4f5", + "content": "- ¥450", + "fontFamily": "Bricolage Grotesque", + "fontSize": 14, + "fontWeight": "600" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "t0wmE", + "name": "navigation", + "width": "fill_container", + "height": 83, + "fill": "#09090B", + "justifyContent": "space_around", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "N4EkI", + "name": "nav1", + "layout": "vertical", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "path", + "id": "AXq3Q", + "name": "nav1Icon", + "width": 24, + "height": 24 + }, + { + "type": "text", + "id": "NitpS", + "name": "nav1Txt", + "fill": "#71717a", + "content": "Home", + "fontFamily": "DM Sans", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "wYYDU", + "name": "nav2", + "layout": "vertical", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "path", + "id": "BPGEq", + "name": "nav2Icon", + "width": 24, + "height": 24 + }, + { + "type": "text", + "id": "9q0Tg", + "name": "nav2Txt", + "fill": "#71717a", + "content": "Chat", + "fontFamily": "DM Sans", + "fontSize": 10, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Z1RTB", + "name": "nav3", + "width": 48, + "height": 48, + "fill": "#f4f4f5", + "cornerRadius": 24, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "path", + "id": "nbDqm", + "name": "nav3Icon", + "width": 24, + "height": 24 + } + ] + }, + { + "type": "frame", + "id": "U5Na8", + "name": "nav4", + "layout": "vertical", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "path", + "id": "J7U2k", + "name": "nav4Icon", + "width": 24, + "height": 24 + }, + { + "type": "text", + "id": "Fxphz", + "name": "nav4Txt", + "fill": "#f4f4f5", + "content": "Stats", + "fontFamily": "DM Sans", + "fontSize": 10, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "3ZAKT", + "name": "nav5", + "layout": "vertical", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "path", + "id": "rnT5I", + "name": "nav5Icon", + "width": 24, + "height": 24 + }, + { + "type": "text", + "id": "pzUFj", + "name": "nav5Txt", + "fill": "#71717a", + "content": "Profile", + "fontFamily": "DM Sans", + "fontSize": 10, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file