# GitLab !49: feat: enhance loading spinner with progress indication and add warnin… Original: `nessar/vac-optimizer`; GitLab MergeRequest ID 105. Target: [#127](https://gitea.nessar.fr/nessar/vac-optimizer/pulls/127). Author: admin-backup (GitLab user 3). Created: 2025-11-17T23:14:45.000Z. Last source update: 2026-05-03T16:01:05.881Z. Source state: merged; discussion locked: False. This linked record preserves source attribution, timestamps, discussion grouping, review positions and historical diffs. Current Gitea activity is separate from the original source dates. Old CI jobs and their reports are omitted by owner decision. Source assignments: admin-backup (GitLab user 3). Source requested reviewers: admin-backup (GitLab user 3). ## Discussions and activity ### Discussion ab40fc34d9f56f17f5e596b9b7c419d248ba6333 #### Source note 462 — admin-backup (GitLab user 3) — 2025-11-17T23:14:45Z system activity. ````text assigned to `@Copilot` ```` ### Discussion f578eb263228a77c13e8d3910246ff43533d69c1 #### Source note 464 — admin-backup (GitLab user 3) — 2025-11-17T23:14:46Z system activity. ````text requested review from `@Copilot` ```` ### Discussion b19de6e4f1636ee4cb5864d6f8d7dc8ced3c75b9 #### Source note 463 — admin-backup (GitLab user 3) — 2025-11-17T23:15:14Z ````text closes #16 ```` ### Discussion 9dfffc1a8aa9b415bdbc220d95dd91cd1066051a #### Source note 268 — admin-backup (GitLab user 3) — 2025-11-17T23:17:13Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "039c4c160f861e3a0e271f37b5fff76a6ad3adcf_44_45" } ``` ````text Wrapping an async function in setTimeout is an anti-pattern. The setTimeout callback should remain synchronous, and the async operations should be handled with proper promise chaining or await. Consider restructuring this to use `setTimeout(() => { void asyncFunction(); })` or better yet, replace setTimeout with a proper async delay mechanism. ```` #### Source note 272 — admin-backup (GitLab user 3) — 2025-11-17T23:17:34Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "039c4c160f861e3a0e271f37b5fff76a6ad3adcf_44_45" } ``` ````text `@copilot` open a new pull request to apply changes based on [this feedback](https://gitlab.nessar.fr/Nessar/vac-optimizer/-/merge_requests/49#discussion_r2535792120) ```` ### Discussion 5a8609f26b8a5468866f450e2c0f5eb0b7d38ace #### Source note 269 — admin-backup (GitLab user 3) — 2025-11-17T23:17:14Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_544_557" } ``` ````text Using `setTimeout(resolve, 0)` on every iteration (potentially hundreds of times) will significantly degrade performance. This creates unnecessary macrotasks and event loop overhead. Consider updating progress less frequently (e.g., every 10 iterations or based on a time threshold) or using `requestAnimationFrame` for UI updates. ```` ### Discussion 8e7a85b15e186a4fdcb15dbd98f89114897bab1b #### Source note 270 — admin-backup (GitLab user 3) — 2025-11-17T23:17:14Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_787_833" } ``` ````text The constant `MAX_DAYS_WITH_SAME_HEURISTIC` (value 9) lacks documentation explaining why this specific threshold was chosen. Add a comment explaining the rationale behind this magic number to improve maintainability. ```` ### Discussion e91c4615decb79d83535a8c18d36bbf45bb527ff #### Source note 275 — admin-backup (GitLab user 3) — 2025-11-17T23:17:14Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_505_505" } ``` ````text [nitpick] The comment should be updated to reflect the new value. It now says 'Big bonus' but the change from 10 to 15 represents a 50% increase. Consider updating the comment to explain why this specific value was chosen or remove the generic 'Big bonus' comment if the value speaks for itself. ```suggestion:-0+0 totalScore += 15; // 50% increased bonus for adjacency; adjacency is highly valued in the scoring system ``` ```` ### Discussion 93fb552a5fe24e7b2cbd44fa7262d56536b3af26 #### Source note 276 — admin-backup (GitLab user 3) — 2025-11-17T23:17:14Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "7a47b7c3865387ec9d0191938c8b5a64ad9b870f_61_61" } ``` ````text Using `!important` to override Material Design styles is an anti-pattern. Consider using a more specific CSS selector or Angular's ViewEncapsulation to properly scope styles without relying on `!important`. ```suggestion:-0+0 width: 100px; height: 100px; ``` ```` ### Discussion d13eff09204c438822ce9d9d36b07858ef3d1403 #### Source note 271 — admin-backup (GitLab user 3) — 2025-11-17T23:17:15Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_564_602" } ``` ````text [nitpick] Console warnings in production code should be avoided or wrapped in a logging service. Consider using a proper logging service that can be configured for different environments (development vs production). ```` #### Source note 277 — admin-backup (GitLab user 3) — 2025-11-17T23:28:08Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_564_602" } ``` ````text `@copilot` open a new pull request to apply changes based on [this feedback](https://gitlab.nessar.fr/Nessar/vac-optimizer/-/merge_requests/49#r2535792156); also remove constructor injection and use inject() ```` ### Discussion 1c8f21f72999404b53060774fb1e690285b8af5d #### Source note 466 — admin-backup (GitLab user 3) — 2025-11-17T23:17:15Z ````text **Review:** Commented ## Pull Request Overview This PR enhances the vacation optimization feature by adding visual progress feedback during optimization and displaying warnings when vacation days cannot be placed automatically. The optimization algorithm has been made asynchronous to allow UI updates during processing. ### Key Changes - Made vacation optimization asynchronous with progress callbacks - Added remaining vacation days tracking with a warning UI - Enhanced heuristic logic to stop optimization when too many days have equal placement quality ### Reviewed Changes Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file | File | Description | | ---- | ----------- | | src/app/services/user-input-service.ts | Added signal to track remaining unplaced vacation days | | src/app/services/calendar-service.ts | Converted optimization to async, added progress tracking and stop condition for ambiguous placements | | src/app/components/user-input/user-input.ts | Added computed properties to display remaining vacation days warning | | src/app/components/user-input/user-input.html | Added warning card UI for unplaced vacation days | | src/app/components/user-input/user-input.scss | Added styles for warning card component | | src/app/components/calendar-year/calendar-year.ts | Integrated progress tracking with loading spinner | | src/app/components/calendar-year/calendar-year.html | Updated spinner to show progress percentage | | src/app/components/calendar-year/calendar-year.scss | Styled progress spinner with percentage overlay |
```` ### Discussion 8b5352e07c984620dd3bf079ccba4fb401ce73c8 #### Source note 465 — admin-backup (GitLab user 3) — 2025-11-17T23:17:41Z ````text `@THoguet` I've opened a new pull request, #50, to work on those changes. Once the pull request is ready, I'll request review from you. ```` ### Discussion 4af972d5943542400285efdbe536ff5d4562b39c #### Source note 470 — admin-backup (GitLab user 3) — 2025-11-17T23:17:41Z system activity. ````text mentioned in merge request !50 ```` ### Discussion f3ab5d0fcfc131027c5345971729820e6ab00483 #### Source note 467 — admin-backup (GitLab user 3) — 2025-11-17T23:18:12Z system activity. ````text requested review from `@Copilot` ```` ### Discussion 5e7a99d7d4d9264fe8c0277946d04234ef653223 #### Source note 278 — admin-backup (GitLab user 3) — 2025-11-17T23:20:17Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_300_300" } ``` ````text The magic number 9 for MAX_DAYS_WITH_SAME_HEURISTIC lacks explanation. Add a comment explaining why 9 is the threshold (e.g., based on testing, represents approximately one week of vacation days, or other reasoning). ```suggestion:-0+0 private static readonly NO_VALID_STRATEGY_HEURISTIC = -1; // MAX_DAYS_WITH_SAME_HEURISTIC is set to 9 to represent approximately one week of vacation days (5 workdays + 2 weekends) plus a buffer for common extended leave patterns, based on user testing and business requirements. ``` ```` ### Discussion d2529a35a6dc1bcf6b3d0bcb571b6ba5f9278ffd #### Source note 279 — admin-backup (GitLab user 3) — 2025-11-17T23:20:17Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_544_557" } ``` ````text Using `setTimeout(resolve, 0)` in a potentially tight loop during optimization could significantly slow down the process. For a vacation with 20+ days, this adds 20+ microtask delays. Consider throttling progress updates (e.g., only update every 5% or every N days) instead of on every iteration. ```suggestion:-0+0 // Helper function to update progress, throttled to every 5% let lastReportedProgress = 0; const PROGRESS_STEP = 5; // percent const updateProgress = async () => { if (progressCallback && totalDays > 0) { const progress = Math.round((processedDays / totalDays) * 100); if (progress - lastReportedProgress >= PROGRESS_STEP || progress === 100) { progressCallback(progress); lastReportedProgress = progress; // Allow UI to update with a microtask await new Promise((resolve) => setTimeout(resolve, 0)); } ``` ```` ### Discussion 6e6edb5e5a6d69df10aff38df952870affded28f #### Source note 280 — admin-backup (GitLab user 3) — 2025-11-17T23:20:17Z LegacyDiffNote; unresolved. Source review position: ```json { "position": null, "original_position": null, "line_code": "3d779d5cb5b415447eb2a87265084f8874aabd0b_17_20" } ``` ````text The warning icon lacks an aria-label or other accessible text for screen readers. Add `aria-label=\"Warning\"` or use `aria-hidden=\"true\"` if the text description is sufficient. ```suggestion:-0+0 ``` ```` ### Discussion 458c95de59b025c2aebd63d4d0c62a2cdbc31c44 #### Source note 471 — admin-backup (GitLab user 3) — 2025-11-17T23:20:18Z ````text **Review:** Commented ## Pull Request Overview Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1) **src/app/components/user-input/user-input.ts:28** * The component uses constructor injection for `UserInputService`, but the coding guidelines specify using the `inject()` function instead. Consider changing to `private userInputService = inject(UserInputService);` at the class level for consistency with Angular best practices. ``` constructor(private userInputService: UserInputService) { ```
```` ### Discussion c07cbab837fdd606b507af0a647c40d706cfbe23 #### Source note 468 — admin-backup (GitLab user 3) — 2025-11-17T23:21:42Z system activity. ````text requested review from `@Copilot` ```` ### Discussion b525b494651e334ed6aa24b907701bc229cce756 #### Source note 469 — admin-backup (GitLab user 3) — 2025-11-17T23:30:05Z ````text `@THoguet` I've opened a new pull request, #51, to work on those changes. Once the pull request is ready, I'll request review from you. ```` ### Discussion e1ccb8c00f47780df1b02fa50f6cc9fc4c9ef49f #### Source note 474 — admin-backup (GitLab user 3) — 2025-11-17T23:30:05Z system activity. ````text mentioned in merge request !51 ```` ### Discussion 8bc9f7406f79b1604b9c259d1aab3331b8fddc93 #### Source note 281 — admin-backup (GitLab user 3) — 2025-11-17T23:35:59Z DiffNote; unresolved. Source review position: ```json { "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/components/calendar-year/calendar-year.html\n :new_path: src/app/components/calendar-year/calendar-year.html\n :position_type: text\n :old_line:\n :new_line: 6\n :line_range:\n :ignore_whitespace_change: false\n", "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/components/calendar-year/calendar-year.html\n :new_path: src/app/components/calendar-year/calendar-year.html\n :position_type: text\n :old_line:\n :new_line: 6\n :line_range:\n :ignore_whitespace_change: false\n", "line_code": "9109b4b9928db902ed369fb3cff4829b13128e5d_6_6" } ``` ````text The spinner should have an accessible label to inform screen reader users about the loading state and progress. The `mat-spinner` component should include `aria-label` or be wrapped in an element with an appropriate label. Add an `aria-label` to the spinner: ```html ``` ```suggestion:-0+0 ``` ```` ### Discussion 72e1b08bd14eb0a3e1ad3cf8b31d9dcac81b0eb3 #### Source note 282 — admin-backup (GitLab user 3) — 2025-11-17T23:35:59Z DiffNote; unresolved. Source review position: ```json { "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/services/calendar-service.ts\n :new_path: src/app/services/calendar-service.ts\n :position_type: text\n :old_line:\n :new_line: 561\n :line_range:\n :ignore_whitespace_change: false\n", "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/services/calendar-service.ts\n :new_path: src/app/services/calendar-service.ts\n :position_type: text\n :old_line:\n :new_line: 561\n :line_range:\n :ignore_whitespace_change: false\n", "line_code": "c096123e634c24e4c6dfdac15c495e299686594a_544_561" } ``` ````text Using `setTimeout(resolve, 0)` for every single day placement could cause performance issues when optimizing a large number of vacation days. Each `setTimeout` adds overhead and forces the event loop to cycle unnecessarily. Consider batching progress updates or using a minimum time interval between updates: ```typescript const updateProgress = async () => { if (progressCallback && totalDays > 0) { const progress = Math.round((processedDays / totalDays) * 100); // Only update if progress changed by at least 1% if (progress !== this.lastProgress) { progressCallback(progress); this.lastProgress = progress; // Allow UI to update with a microtask await new Promise((resolve) => setTimeout(resolve, 0)); } } }; ``` Or update progress only every N days: ```typescript if (processedDays % 5 === 0 || processedDays === totalDays) { await updateProgress(); } ``` ```suggestion:-7+0 let lastYieldTime = Date.now(); const YIELD_INTERVAL_MS = 100; const updateProgress = async () => { if (progressCallback && totalDays > 0) { const progress = Math.round((processedDays / totalDays) * 100); if (progress - lastReportedProgress >= PROGRESS_STEP || progress === 100) { progressCallback(progress); lastReportedProgress = progress; const now = Date.now(); if (now - lastYieldTime >= YIELD_INTERVAL_MS) { lastYieldTime = now; // Allow UI to update with a microtask await new Promise((resolve) => setTimeout(resolve, 0)); } ``` ```` ### Discussion fc64e964e194bfe173bdc399115e50b58b4ff5ba #### Source note 289 — admin-backup (GitLab user 3) — 2025-11-17T23:35:59Z DiffNote; unresolved. Source review position: ```json { "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/components/user-input/user-input.html\n :new_path: src/app/components/user-input/user-input.html\n :position_type: text\n :old_line:\n :new_line: 18\n :line_range:\n :ignore_whitespace_change: false\n", "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/components/user-input/user-input.html\n :new_path: src/app/components/user-input/user-input.html\n :position_type: text\n :old_line:\n :new_line: 18\n :line_range:\n :ignore_whitespace_change: false\n", "line_code": "3d779d5cb5b415447eb2a87265084f8874aabd0b_17_18" } ``` ````text The warning message should have an `aria-live` attribute to announce changes to screen reader users. When the warning appears dynamically, assistive technologies should be notified. Add `aria-live="polite"` to the `mat-card` element: ```html ``` ```suggestion:-0+0 ``` ```` ### Discussion 1a9b0e1c4f25560214390d8a636bf4034c998f66 #### Source note 291 — admin-backup (GitLab user 3) — 2025-11-17T23:36:00Z DiffNote; unresolved. Source review position: ```json { "position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/components/user-input/user-input.html\n :new_path: src/app/components/user-input/user-input.html\n :position_type: text\n :old_line:\n :new_line: 23\n :line_range:\n :ignore_whitespace_change: false\n", "original_position": "--- !ruby/object:Gitlab::Diff::Position\nattributes:\n :base_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :start_sha: 2d5cccff0ceac6890fb06c650e5aa078c1cf6b44\n :head_sha: 54c2160f0dd293f8c1c78342e155a65d891041fd\n :old_path: src/app/components/user-input/user-input.html\n :new_path: src/app/components/user-input/user-input.html\n :position_type: text\n :old_line:\n :new_line: 23\n :line_range:\n :ignore_whitespace_change: false\n", "line_code": "3d779d5cb5b415447eb2a87265084f8874aabd0b_17_23" } ``` ````text [nitpick] The warning icon should have an `aria-label` to provide context for screen readers. While `aria-hidden="true"` prevents the icon from being announced, the warning text should be properly associated with the icon's semantic meaning. Consider wrapping the content in a semantic element with an appropriate role or adding an `aria-label` to the parent span: ```html {{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement. ``` ```suggestion:-3+0 {{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement. ``` ```` ### Discussion c3548e1b972f4b88c5637d81fd6708ad7b38a402 #### Source note 472 — admin-backup (GitLab user 3) — 2025-11-17T23:36:00Z ````text **Review:** Commented ## Pull Request Overview Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments. ```` ### Discussion abfb43885063dbd6a02def51ab7f3264e4391538 #### Source note 473 — admin-backup (GitLab user 3) — 2025-11-17T23:45:05Z ````text *Merged by: THoguet at 2025-11-17 23:45:05 UTC* ```` ## State event records - {"close_after_error_tracking_resolve": false, "close_auto_resolve_prometheus_alert": false, "created_at": "2025-11-17T23:45:05+00:00", "epic_id": null, "id": 233, "imported_from": 4, "issue_id": null, "merge_request_id": 105, "namespace_id": 38, "source_commit": null, "source_merge_request_id": null, "state": 2, "user_id": 3} - {"close_after_error_tracking_resolve": false, "close_auto_resolve_prometheus_alert": false, "created_at": "2025-11-17T23:45:05+00:00", "epic_id": null, "id": 234, "imported_from": 4, "issue_id": null, "merge_request_id": 105, "namespace_id": 38, "source_commit": "3272622abfbe608161aa51332e21b1cb23e3af93", "source_merge_request_id": null, "state": 3, "user_id": 3} ## Historical diff versions ### Diff version 107 — 2026-05-03T15:59:55.89964Z Base `2d5cccff0ceac6890fb06c650e5aa078c1cf6b44`; start `2d5cccff0ceac6890fb06c650e5aa078c1cf6b44`; head `54c2160f0dd293f8c1c78342e155a65d891041fd`. Files: 8; stored commits: 6. #### `src/app/components/calendar-year/calendar-year.html` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -2,7 +2,10 @@ @if (isLoading()) {
- +
+ +
{{ loadingProgress() }}%
+

Calcul en cours...

```` #### `src/app/components/calendar-year/calendar-year.scss` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -51,25 +51,32 @@ } } -.card { - margin: 16px; -} +.spinner-container { + position: relative; + display: inline-block; -@keyframes spin { - 0% { - transform: rotate(0deg); + ::ng-deep .mat-mdc-progress-spinner { + // Override Material spinner size + width: 100px; + height: 100px; } - 100% { - transform: rotate(360deg); + .spinner-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 20px; + font-weight: bold; + color: #333; + pointer-events: none; + + @media (prefers-color-scheme: dark) { + color: #ddd; + } } } -:root { - @include mat.progress-spinner-overrides( - ( - size: 0.5rem, - active-indicator-width: 0.25rem, - ) - ); +.card { + margin: 16px; } ```` #### `src/app/components/calendar-year/calendar-year.ts` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -21,6 +21,7 @@ export class CalendarYear { private calendarSettingsService = inject(CalendarSettingsService); protected isLoading = signal(true); + protected loadingProgress = signal(0); // Use DateCacheService instead of creating Date objects private get currentMonth() { @@ -39,18 +40,26 @@ export class CalendarYear { console.log('Samedi Malin setting:', c); console.log('Optimizing vacations with data:', vacationData); this.isLoading.set(true); - // Simulate server-side calculation with a small delay - setTimeout(() => { - if (this.selectedDates) { - // Create a copy to avoid mutating the original signal data - this.selectedDates.optimizeVacations( - { ...vacationData }, - this.calendarSettingsService, - this.calendarService, - ); - } - this.isLoading.set(false); - }, 1); + this.loadingProgress.set(0); + // Defer execution to allow UI to update + queueMicrotask(() => { + void (async () => { + if (this.selectedDates) { + // Create a copy to avoid mutating the original signal data + const remainingDays = await this.selectedDates.optimizeVacations( + { ...vacationData }, + this.calendarSettingsService, + this.calendarService, + (progress: number) => { + this.loadingProgress.set(progress); + }, + ); + // Update the remaining vacation days signal + this.userInput.remainingVacationDaysSignal.set(remainingDays); + } + this.isLoading.set(false); + })(); + }); }); } ```` #### `src/app/components/user-input/user-input.html` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -14,5 +14,16 @@ + @if (hasRemainingDays()) { + + + + + {{ remainingDays() }} jour{{ remainingDays() > 1 ? 's' : '' }} de vacances non + placé{{ remainingDays() > 1 ? 's' : '' }}. Veuillez les placer manuellement. + + + + } ```` #### `src/app/components/user-input/user-input.scss` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -13,6 +13,16 @@ font-size: 0.875rem; margin-top: 0.25rem; } + + .warning-card { + margin-bottom: 1rem; + } + + .warning-content { + display: flex; + align-items: center; + gap: 0.5rem; + } } form { ```` #### `src/app/components/user-input/user-input.ts` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, effect } from '@angular/core'; +import { Component, OnInit, OnDestroy, effect, computed } from '@angular/core'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; @@ -6,6 +6,8 @@ import { UserInputService } from '../../services/user-input-service'; import { VacationsNumber } from '../../services/calendar-service'; import { debounceTime, takeUntil, Subject } from 'rxjs'; import { CalendarSettings } from '../calendar-settings/calendar-settings'; +import { MatCard, MatCardContent } from '@angular/material/card'; +import { MatIcon } from '@angular/material/icon'; @Component({ selector: 'app-user-input', @@ -15,7 +17,9 @@ import { CalendarSettings } from '../calendar-settings/calendar-settings'; MatInputModule, ReactiveFormsModule, CalendarSettings, - CalendarSettings, + MatCard, + MatCardContent, + MatIcon, ], templateUrl: './user-input.html', styleUrl: './user-input.scss', @@ -44,6 +48,13 @@ export class UserInput implements OnInit, OnDestroy { Others: new FormControl(null), }); + protected remainingDays = computed(() => { + const remaining = this.userInputService.remainingVacationDaysSignal(); + return remaining.cp + remaining.rtt + remaining.other; + }); + + protected hasRemainingDays = computed(() => this.remainingDays() > 0); + ngOnInit() { this.userInputForm.valueChanges .pipe(debounceTime(1500), takeUntil(this.destroy$)) ```` #### `src/app/services/calendar-service.ts` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -298,6 +298,7 @@ export class SelectedDates implements SelectedDateInterface { // Constant to indicate no valid strategy was found private static readonly NO_VALID_STRATEGY_HEURISTIC = -1; + private static readonly MAX_DAYS_WITH_SAME_HEURISTIC = 9; update(): void { this.datesSelected = this._datesSelected.slice(); @@ -501,7 +502,7 @@ export class SelectedDates implements SelectedDateInterface { // Check if adjacent (next day) - this is the best case! if (nextStartTime - currentEndTime === oneDayInMs) { - totalScore += 10; // Big bonus for adjacency + totalScore += 15; // 50% increased bonus for adjacency; adjacency is highly valued in the scoring system } else if (nextStartTime > currentEndTime) { // Penalty based on days between the two dates const daysBetween = Math.floor((nextStartTime - currentEndTime) / oneDayInMs) - 1; @@ -535,18 +536,43 @@ export class SelectedDates implements SelectedDateInterface { return totalScore; } - optimizeVacations( + async optimizeVacations( vacationsNumber: VacationsNumber, calendarSettingsService: CalendarSettingsService, calendarService: CalendarService, - ): void { + progressCallback?: (progress: number) => void, + ): Promise { if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber); + + const initialVacations = { ...vacationsNumber }; + const totalDays = initialVacations.cp + initialVacations.rtt + initialVacations.other; + let processedDays = 0; + + // Helper function to update progress, throttled to every 5% + let lastReportedProgress = 0; + const PROGRESS_STEP = 5; // percent + const updateProgress = async () => { + if (progressCallback && totalDays > 0) { + const progress = Math.round((processedDays / totalDays) * 100); + if (progress - lastReportedProgress >= PROGRESS_STEP || progress === 100) { + progressCallback(progress); + lastReportedProgress = progress; + // Allow UI to update with a microtask + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + }; + + await updateProgress(); + while (vacationsNumber.cp > 0) { const result = this.lookForVacation(DayType.CP, calendarService); if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) { break; } vacationsNumber.cp -= 1; + processedDays++; + await updateProgress(); } while (vacationsNumber.rtt > 0) { const result = this.lookForVacation(DayType.RTT, calendarService); @@ -554,6 +580,8 @@ export class SelectedDates implements SelectedDateInterface { break; } vacationsNumber.rtt -= 1; + processedDays++; + await updateProgress(); } while (vacationsNumber.other > 0) { const result = this.lookForVacation(DayType.OTHER, calendarService); @@ -561,7 +589,25 @@ export class SelectedDates implements SelectedDateInterface { break; } vacationsNumber.other -= 1; + processedDays++; + await updateProgress(); } + + // Final progress update (100%) + if (progressCallback) { + progressCallback(100); + } + + // Inform user if not all days were placed + const remainingDays = vacationsNumber.cp + vacationsNumber.rtt + vacationsNumber.other; + if (remainingDays > 0) { + console.warn( + `Optimization stopped: ${remainingDays} of ${totalDays} vacation days remain unplaced. ` + + `Too many days have equivalent placement quality - please place remaining days manually.`, + ); + } + + return vacationsNumber; } samediMalin(vacationsNumber: VacationsNumber): void { @@ -751,6 +797,7 @@ export class SelectedDates implements SelectedDateInterface { const tempDatesBackup = [...this._datesSelected]; let bestStrategy: { apply: () => void; heuristic: number; value: Date } | null = null; let bestHeuristic = -Infinity; + let daysWithBestHeuristic = 0; // Evaluate each candidate day for (const day of candidateDays) { @@ -767,6 +814,7 @@ export class SelectedDates implements SelectedDateInterface { // Track only if better than current best if (heuristic > bestHeuristic) { bestHeuristic = heuristic; + daysWithBestHeuristic = 1; const capturedDay = day; bestStrategy = { apply: () => { @@ -781,9 +829,25 @@ export class SelectedDates implements SelectedDateInterface { heuristic: heuristic, value: capturedDay, }; + } else if (heuristic === bestHeuristic) { + daysWithBestHeuristic++; } } + // Check if too many days have the same best heuristic after evaluating all days + if (daysWithBestHeuristic > SelectedDates.MAX_DAYS_WITH_SAME_HEURISTIC) { + // Restore backup before stopping + this._datesSelected = [...tempDatesBackup]; + this.datesSelected = this._datesSelected.slice(); + this.grouping(); + + // Stop optimization - let user decide manually + return { + apply: () => {}, + heuristic: SelectedDates.NO_VALID_STRATEGY_HEURISTIC, + }; + } + // Restore backup before returning this._datesSelected = [...tempDatesBackup]; this.datesSelected = this._datesSelected.slice(); ```` #### `src/app/services/user-input-service.ts` → `None` Modes 100644 → 100644; new=False, deleted=False, renamed=False, binary=False, too_large=False. ````diff @@ -18,4 +18,9 @@ export class UserInputService { } readonly vacationNumberSignal = signal(this.getInitialValues()); + readonly remainingVacationDaysSignal = signal({ + cp: 0, + rtt: 0, + other: 0, + }); } ````