UI elements for scheduling

This commit is contained in:
overcuriousity 2025-11-07 10:17:35 +01:00
parent c87b5622ca
commit 2814d28f95
9 changed files with 181 additions and 3 deletions

2
.gitignore vendored
View file

@ -112,3 +112,5 @@ playwright-report/
electron-builder-appx.yaml
SCHEDULING_FEATURE_IMPLEMENTATION.md
SCHEDULING_ROADMAP.md

View file

@ -21,6 +21,23 @@
>
{{ T.GCF.SCHEDULE.MONTH | translate }}
</button>
<mat-form-field
class="task-placement-select"
appearance="outline"
>
<mat-label>{{ T.GCF.SCHEDULE.L_TASK_PLACEMENT_STRATEGY | translate }}</mat-label>
<mat-select
[value]="scheduleConfig()?.taskPlacementStrategy || 'DEFAULT'"
(selectionChange)="updateTaskPlacementStrategy($event.value)"
>
@for (strategy of taskPlacementStrategies; track strategy.value) {
<mat-option [value]="strategy.value">
{{ strategy.label | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}

View file

@ -297,10 +297,11 @@ button.isActive2 {
.week-month-selector {
display: flex;
align-items: center;
gap: var(--s);
border-radius: var(--card-border-radius);
overflow: hidden;
overflow: visible;
margin-right: calc(var(--s) * 0.25);
box-shadow: var(--whiteframe-shadow-2dp);
@include mq(xs) {
margin-right: var(--s);
@ -308,6 +309,53 @@ button.isActive2 {
@include mq(xs, max) {
margin-right: calc(var(--s) * 0.125);
gap: calc(var(--s) * 0.5);
}
}
.task-placement-select {
margin: 0;
font-size: 12px;
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
::ng-deep .mat-mdc-text-field-wrapper {
padding: 0;
}
::ng-deep .mat-mdc-form-field-infix {
min-height: 36px;
padding: 6px 12px 6px 8px;
}
::ng-deep .mat-mdc-select {
font-size: 12px;
}
::ng-deep .mat-mdc-select-value {
padding-left: 4px;
}
::ng-deep .mat-mdc-select-arrow-wrapper {
padding-right: 4px;
}
@include mq(xs, max) {
font-size: 11px;
::ng-deep .mat-mdc-form-field-infix {
min-height: 32px;
padding: 4px 10px 4px 6px;
}
}
}
// Style for dropdown panel options
::ng-deep .mat-mdc-select-panel {
.mat-mdc-option {
padding-left: 16px;
padding-right: 16px;
}
}

View file

@ -44,6 +44,9 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { MetricService } from '../../features/metric/metric.service';
import { DateService } from '../../core/date/date.service';
import { FocusButtonComponent } from './focus-button/focus-button.component';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatOption, MatSelect } from '@angular/material/select';
import { TaskPlacementStrategy } from '../../features/config/global-config.model';
@Component({
selector: 'main-header',
@ -65,6 +68,10 @@ import { FocusButtonComponent } from './focus-button/focus-button.component';
PlayButtonComponent,
DesktopPanelButtonsComponent,
FocusButtonComponent,
MatFormField,
MatSelect,
MatOption,
MatLabel,
],
})
export class MainHeaderComponent implements OnDestroy {
@ -152,6 +159,25 @@ export class MainHeaderComponent implements OnDestroy {
this._metricService.getFocusSummaryForDay(this._dateService.todayStr()),
);
scheduleConfig = toSignal(
this.globalConfigService.cfg$.pipe(map((cfg) => cfg?.schedule)),
);
taskPlacementStrategies: { value: TaskPlacementStrategy; label: string }[] = [
{ value: 'DEFAULT', label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_DEFAULT },
{
value: 'SHORTEST_FIRST',
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_SHORTEST_FIRST,
},
{
value: 'LONGEST_FIRST',
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_LONGEST_FIRST,
},
{ value: 'OLDEST_FIRST', label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_OLDEST_FIRST },
{ value: 'NEWEST_FIRST', label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_NEWEST_FIRST },
{ value: 'BEST_FIT', label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_BEST_FIT },
];
private _subs: Subscription = new Subscription();
selectedTimeView = computed(() => this.layoutService.selectedTimeView());
@ -160,6 +186,12 @@ export class MainHeaderComponent implements OnDestroy {
this.layoutService.selectedTimeView.set(view);
}
updateTaskPlacementStrategy(strategy: TaskPlacementStrategy): void {
this.globalConfigService.updateSection('schedule', {
taskPlacementStrategy: strategy,
});
}
ngOnDestroy(): void {
this._subs.unsubscribe();
}

View file

@ -173,6 +173,8 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
isLunchBreakEnabled: false,
lunchBreakStart: '13:00',
lunchBreakEnd: '14:00',
isAllowTaskSplitting: true,
taskPlacementStrategy: 'DEFAULT',
},
sync: {

View file

@ -81,5 +81,46 @@ export const SCHEDULE_FORM_CFG: ConfigFormSection<ScheduleConfig> = {
},
},
},
{
key: 'isAllowTaskSplitting',
type: 'checkbox',
templateOptions: {
label: T.GCF.SCHEDULE.L_IS_ALLOW_TASK_SPLITTING,
},
},
{
key: 'taskPlacementStrategy',
type: 'select',
templateOptions: {
label: T.GCF.SCHEDULE.L_TASK_PLACEMENT_STRATEGY,
required: true,
options: [
{
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_DEFAULT,
value: 'DEFAULT',
},
{
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_SHORTEST_FIRST,
value: 'SHORTEST_FIRST',
},
{
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_LONGEST_FIRST,
value: 'LONGEST_FIRST',
},
{
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_OLDEST_FIRST,
value: 'OLDEST_FIRST',
},
{
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_NEWEST_FIRST,
value: 'NEWEST_FIRST',
},
{
label: T.GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_BEST_FIT,
value: 'BEST_FIT',
},
],
},
},
],
};

View file

@ -141,6 +141,14 @@ export type SyncConfig = Readonly<{
localFileSync?: LocalFileSyncConfig;
}>;
export type TaskPlacementStrategy =
| 'DEFAULT'
| 'BEST_FIT'
| 'SHORTEST_FIRST'
| 'LONGEST_FIRST'
| 'OLDEST_FIRST'
| 'NEWEST_FIRST';
export type ScheduleConfig = Readonly<{
isWorkStartEndEnabled: boolean;
workStart: string;
@ -148,6 +156,8 @@ export type ScheduleConfig = Readonly<{
isLunchBreakEnabled: boolean;
lunchBreakStart: string;
lunchBreakEnd: string;
isAllowTaskSplitting: boolean;
taskPlacementStrategy: TaskPlacementStrategy;
}>;
export type ReminderConfig = Readonly<{

View file

@ -1920,6 +1920,22 @@ const T = {
WORK_START_END_DESCRIPTION: 'GCF.SCHEDULE.WORK_START_END_DESCRIPTION',
WEEK: 'GCF.SCHEDULE.WEEK',
MONTH: 'GCF.SCHEDULE.MONTH',
L_IS_ALLOW_TASK_SPLITTING: 'GCF.SCHEDULE.L_IS_ALLOW_TASK_SPLITTING',
L_TASK_PLACEMENT_STRATEGY: 'GCF.SCHEDULE.L_TASK_PLACEMENT_STRATEGY',
TASK_PLACEMENT_STRATEGY_DEFAULT: 'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_DEFAULT',
TASK_PLACEMENT_STRATEGY_BEST_FIT: 'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_BEST_FIT',
TASK_PLACEMENT_STRATEGY_SHORTEST_FIRST:
'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_SHORTEST_FIRST',
TASK_PLACEMENT_STRATEGY_LONGEST_FIRST:
'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_LONGEST_FIRST',
TASK_PLACEMENT_STRATEGY_OLDEST_FIRST:
'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_OLDEST_FIRST',
TASK_PLACEMENT_STRATEGY_NEWEST_FIRST:
'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_NEWEST_FIRST',
TASK_PLACEMENT_STRATEGY_DEFAULT_HELP:
'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_DEFAULT_HELP',
TASK_PLACEMENT_STRATEGY_BEST_FIT_HELP:
'GCF.SCHEDULE.TASK_PLACEMENT_STRATEGY_BEST_FIT_HELP',
},
SHORT_SYNTAX: {
HELP: 'GCF.SHORT_SYNTAX.HELP',

View file

@ -1889,7 +1889,17 @@
"TITLE": "Schedule",
"WORK_START_END_DESCRIPTION": "e.g. 17:00",
"WEEK": "Week",
"MONTH": "Month"
"MONTH": "Month",
"L_IS_ALLOW_TASK_SPLITTING": "Allow task splitting across time boundaries",
"L_TASK_PLACEMENT_STRATEGY": "Strategy",
"TASK_PLACEMENT_STRATEGY_DEFAULT": "Default (Sequential)",
"TASK_PLACEMENT_STRATEGY_BEST_FIT": "Best Fit (Experimental)",
"TASK_PLACEMENT_STRATEGY_SHORTEST_FIRST": "Shortest Task First",
"TASK_PLACEMENT_STRATEGY_LONGEST_FIRST": "Longest Task First",
"TASK_PLACEMENT_STRATEGY_OLDEST_FIRST": "Oldest Task First",
"TASK_PLACEMENT_STRATEGY_NEWEST_FIRST": "Newest Task First",
"TASK_PLACEMENT_STRATEGY_DEFAULT_HELP": "Default: Uses simple sequential placement. Tasks are placed one after another in the order they appear in your task list, starting from the work start time or current time.",
"TASK_PLACEMENT_STRATEGY_BEST_FIT_HELP": "Best Fit: Uses best-fit bin packing algorithm to minimize task splitting by placing tasks into gaps between scheduled items. Tasks are sorted by remaining time (shortest first) for optimal gap utilization."
},
"SHORT_SYNTAX": {
"HELP": "<p>Here you can control short syntax options when creating a task</p>",