feat(usecase): add analytics methods to search history use case
- commit
- d2e410eadea042593ac394fa7051d4274e209069
- author
- brice · · 2025-12-16 19:23 UTC
- committer
- brice · · 2025-12-16 18:39 UTC
- parent
- c5f1087
- signature
- unsigned
2 files · +118 -0
M internal/application/usecase/manage_favorites.go +54 -0
| @@ -408,3 +408,57 @@ func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID | |||
| 408 | 408 | ||
| 409 | 409 | return uc.tagRepo.GetForFavorite(ctx, favID) | |
| 410 | 410 | } | |
| 411 | + | ||
| 412 | + | // UpdateFolder updates a folder's name and icon. | |
| 413 | + | func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error { | |
| 414 | + | log := logging.FromContext(ctx) | |
| 415 | + | log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder") | |
| 416 | + | ||
| 417 | + | folder, err := uc.folderRepo.FindByID(ctx, id) | |
| 418 | + | if err != nil { | |
| 419 | + | return fmt.Errorf("failed to find folder: %w", err) | |
| 420 | + | } | |
| 421 | + | if folder == nil { | |
| 422 | + | return fmt.Errorf("folder %d not found", id) | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | if name != "" { | |
| 426 | + | folder.Name = name | |
| 427 | + | } | |
| 428 | + | folder.Icon = icon | |
| 429 | + | ||
| 430 | + | if err := uc.folderRepo.Save(ctx, folder); err != nil { | |
| 431 | + | return fmt.Errorf("failed to update folder: %w", err) | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | log.Info().Int64("id", int64(id)).Msg("folder updated") | |
| 435 | + | return nil | |
| 436 | + | } | |
| 437 | + | ||
| 438 | + | // UpdateTag updates a tag's name and color. | |
| 439 | + | func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error { | |
| 440 | + | log := logging.FromContext(ctx) | |
| 441 | + | log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag") | |
| 442 | + | ||
| 443 | + | tag, err := uc.tagRepo.FindByID(ctx, id) | |
| 444 | + | if err != nil { | |
| 445 | + | return fmt.Errorf("failed to find tag: %w", err) | |
| 446 | + | } | |
| 447 | + | if tag == nil { | |
| 448 | + | return fmt.Errorf("tag %d not found", id) | |
| 449 | + | } | |
| 450 | + | ||
| 451 | + | if name != "" { | |
| 452 | + | tag.Name = name | |
| 453 | + | } | |
| 454 | + | if color != "" { | |
| 455 | + | tag.Color = color | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | if err := uc.tagRepo.Save(ctx, tag); err != nil { | |
| 459 | + | return fmt.Errorf("failed to update tag: %w", err) | |
| 460 | + | } | |
| 461 | + | ||
| 462 | + | log.Info().Int64("id", int64(id)).Msg("tag updated") | |
| 463 | + | return nil | |
| 464 | + | } | |
| @@ -408,3 +408,57 @@ func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID | |||||
| 408 | 408 | ||||
| 409 | return uc.tagRepo.GetForFavorite(ctx, favID) | 409 | return uc.tagRepo.GetForFavorite(ctx, favID) | ||
| 410 | } | 410 | } | ||
| 411 | + | ||||
| 412 | + | // UpdateFolder updates a folder's name and icon. | |||
| 413 | + | func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error { | |||
| 414 | + | log := logging.FromContext(ctx) | |||
| 415 | + | log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder") | |||
| 416 | + | ||||
| 417 | + | folder, err := uc.folderRepo.FindByID(ctx, id) | |||
| 418 | + | if err != nil { | |||
| 419 | + | return fmt.Errorf("failed to find folder: %w", err) | |||
| 420 | + | } | |||
| 421 | + | if folder == nil { | |||
| 422 | + | return fmt.Errorf("folder %d not found", id) | |||
| 423 | + | } | |||
| 424 | + | ||||
| 425 | + | if name != "" { | |||
| 426 | + | folder.Name = name | |||
| 427 | + | } | |||
| 428 | + | folder.Icon = icon | |||
| 429 | + | ||||
| 430 | + | if err := uc.folderRepo.Save(ctx, folder); err != nil { | |||
| 431 | + | return fmt.Errorf("failed to update folder: %w", err) | |||
| 432 | + | } | |||
| 433 | + | ||||
| 434 | + | log.Info().Int64("id", int64(id)).Msg("folder updated") | |||
| 435 | + | return nil | |||
| 436 | + | } | |||
| 437 | + | ||||
| 438 | + | // UpdateTag updates a tag's name and color. | |||
| 439 | + | func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error { | |||
| 440 | + | log := logging.FromContext(ctx) | |||
| 441 | + | log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag") | |||
| 442 | + | ||||
| 443 | + | tag, err := uc.tagRepo.FindByID(ctx, id) | |||
| 444 | + | if err != nil { | |||
| 445 | + | return fmt.Errorf("failed to find tag: %w", err) | |||
| 446 | + | } | |||
| 447 | + | if tag == nil { | |||
| 448 | + | return fmt.Errorf("tag %d not found", id) | |||
| 449 | + | } | |||
| 450 | + | ||||
| 451 | + | if name != "" { | |||
| 452 | + | tag.Name = name | |||
| 453 | + | } | |||
| 454 | + | if color != "" { | |||
| 455 | + | tag.Color = color | |||
| 456 | + | } | |||
| 457 | + | ||||
| 458 | + | if err := uc.tagRepo.Save(ctx, tag); err != nil { | |||
| 459 | + | return fmt.Errorf("failed to update tag: %w", err) | |||
| 460 | + | } | |||
| 461 | + | ||||
| 462 | + | log.Info().Int64("id", int64(id)).Msg("tag updated") | |||
| 463 | + | return nil | |||
| 464 | + | } | |||
M internal/application/usecase/search_history.go +64 -0
| @@ -165,3 +165,67 @@ func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error { | |||
| 165 | 165 | log.Debug().Int64("id", id).Msg("history entry deleted") | |
| 166 | 166 | return nil | |
| 167 | 167 | } | |
| 168 | + | ||
| 169 | + | // DeleteByDomain removes all history entries for a domain. | |
| 170 | + | func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error { | |
| 171 | + | log := logging.FromContext(ctx) | |
| 172 | + | log.Debug().Str("domain", domain).Msg("deleting history by domain") | |
| 173 | + | ||
| 174 | + | if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil { | |
| 175 | + | return fmt.Errorf("failed to delete history by domain: %w", err) | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | log.Info().Str("domain", domain).Msg("history deleted for domain") | |
| 179 | + | return nil | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // GetDomainStats retrieves per-domain visit statistics. | |
| 183 | + | func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) { | |
| 184 | + | log := logging.FromContext(ctx) | |
| 185 | + | log.Debug().Int("limit", limit).Msg("getting domain stats") | |
| 186 | + | ||
| 187 | + | if limit <= 0 { | |
| 188 | + | limit = 20 | |
| 189 | + | } | |
| 190 | + | ||
| 191 | + | return uc.historyRepo.GetDomainStats(ctx, limit) | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | // GetAnalytics retrieves aggregated history analytics for the homepage. | |
| 195 | + | func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) { | |
| 196 | + | log := logging.FromContext(ctx) | |
| 197 | + | log.Debug().Msg("getting history analytics") | |
| 198 | + | ||
| 199 | + | // Get basic stats | |
| 200 | + | stats, err := uc.historyRepo.GetStats(ctx) | |
| 201 | + | if err != nil { | |
| 202 | + | return nil, fmt.Errorf("failed to get history stats: %w", err) | |
| 203 | + | } | |
| 204 | + | ||
| 205 | + | // Get top domains | |
| 206 | + | domains, err := uc.historyRepo.GetDomainStats(ctx, 10) | |
| 207 | + | if err != nil { | |
| 208 | + | return nil, fmt.Errorf("failed to get domain stats: %w", err) | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // Get daily visits for last 30 days | |
| 212 | + | dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days") | |
| 213 | + | if err != nil { | |
| 214 | + | return nil, fmt.Errorf("failed to get daily visits: %w", err) | |
| 215 | + | } | |
| 216 | + | ||
| 217 | + | // Get hourly distribution | |
| 218 | + | hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx) | |
| 219 | + | if err != nil { | |
| 220 | + | return nil, fmt.Errorf("failed to get hourly distribution: %w", err) | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | return &entity.HistoryAnalytics{ | |
| 224 | + | TotalEntries: stats.TotalEntries, | |
| 225 | + | TotalVisits: stats.TotalVisits, | |
| 226 | + | UniqueDays: stats.UniqueDays, | |
| 227 | + | TopDomains: domains, | |
| 228 | + | DailyVisits: dailyVisits, | |
| 229 | + | HourlyDistribution: hourlyDist, | |
| 230 | + | }, nil | |
| 231 | + | } | |
| @@ -165,3 +165,67 @@ func (uc *SearchHistoryUseCase) Delete(ctx context.Context, id int64) error { | |||||
| 165 | log.Debug().Int64("id", id).Msg("history entry deleted") | 165 | log.Debug().Int64("id", id).Msg("history entry deleted") | ||
| 166 | return nil | 166 | return nil | ||
| 167 | } | 167 | } | ||
| 168 | + | ||||
| 169 | + | // DeleteByDomain removes all history entries for a domain. | |||
| 170 | + | func (uc *SearchHistoryUseCase) DeleteByDomain(ctx context.Context, domain string) error { | |||
| 171 | + | log := logging.FromContext(ctx) | |||
| 172 | + | log.Debug().Str("domain", domain).Msg("deleting history by domain") | |||
| 173 | + | ||||
| 174 | + | if err := uc.historyRepo.DeleteByDomain(ctx, domain); err != nil { | |||
| 175 | + | return fmt.Errorf("failed to delete history by domain: %w", err) | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | log.Info().Str("domain", domain).Msg("history deleted for domain") | |||
| 179 | + | return nil | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // GetDomainStats retrieves per-domain visit statistics. | |||
| 183 | + | func (uc *SearchHistoryUseCase) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) { | |||
| 184 | + | log := logging.FromContext(ctx) | |||
| 185 | + | log.Debug().Int("limit", limit).Msg("getting domain stats") | |||
| 186 | + | ||||
| 187 | + | if limit <= 0 { | |||
| 188 | + | limit = 20 | |||
| 189 | + | } | |||
| 190 | + | ||||
| 191 | + | return uc.historyRepo.GetDomainStats(ctx, limit) | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | // GetAnalytics retrieves aggregated history analytics for the homepage. | |||
| 195 | + | func (uc *SearchHistoryUseCase) GetAnalytics(ctx context.Context) (*entity.HistoryAnalytics, error) { | |||
| 196 | + | log := logging.FromContext(ctx) | |||
| 197 | + | log.Debug().Msg("getting history analytics") | |||
| 198 | + | ||||
| 199 | + | // Get basic stats | |||
| 200 | + | stats, err := uc.historyRepo.GetStats(ctx) | |||
| 201 | + | if err != nil { | |||
| 202 | + | return nil, fmt.Errorf("failed to get history stats: %w", err) | |||
| 203 | + | } | |||
| 204 | + | ||||
| 205 | + | // Get top domains | |||
| 206 | + | domains, err := uc.historyRepo.GetDomainStats(ctx, 10) | |||
| 207 | + | if err != nil { | |||
| 208 | + | return nil, fmt.Errorf("failed to get domain stats: %w", err) | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // Get daily visits for last 30 days | |||
| 212 | + | dailyVisits, err := uc.historyRepo.GetDailyVisitCount(ctx, "-30 days") | |||
| 213 | + | if err != nil { | |||
| 214 | + | return nil, fmt.Errorf("failed to get daily visits: %w", err) | |||
| 215 | + | } | |||
| 216 | + | ||||
| 217 | + | // Get hourly distribution | |||
| 218 | + | hourlyDist, err := uc.historyRepo.GetHourlyDistribution(ctx) | |||
| 219 | + | if err != nil { | |||
| 220 | + | return nil, fmt.Errorf("failed to get hourly distribution: %w", err) | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | return &entity.HistoryAnalytics{ | |||
| 224 | + | TotalEntries: stats.TotalEntries, | |||
| 225 | + | TotalVisits: stats.TotalVisits, | |||
| 226 | + | UniqueDays: stats.UniqueDays, | |||
| 227 | + | TopDomains: domains, | |||
| 228 | + | DailyVisits: dailyVisits, | |||
| 229 | + | HourlyDistribution: hourlyDist, | |||
| 230 | + | }, nil | |||
| 231 | + | } | |||