WEB-604: Add hour/minute time selection for partial day cashier allocation (#3108)

- Added hour (00-23) and minute (00-59) dropdowns to Create Cashier form
- Added hour/minute dropdowns to Edit Cashier form with existing time parsing
- Time fields shown conditionally when Full Day is unchecked
- Added end-time-after-start-time validation
- Clear stale invalidTimeRange error before re-validating
- Renamed setEditChargeForm to setEditCashierForm
- Added Hour and Minute i18n keys to all 13 locale files
This commit is contained in:
Anvay
2026-02-08 16:15:50 -06:00
committed by GitHub
parent 77e95c6146
commit 2f73e9a9eb
17 changed files with 244 additions and 13 deletions
@@ -83,7 +83,63 @@
}
</mat-form-field>
<div class="space flex-48">
@if (!createCashierForm.value.isFullDay) {
<div class="flex-48 layout-row gap-10px">
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Hour' | translate }})</mat-label
>
<mat-select formControlName="hourStartTime">
@for (hour of hours; track hour) {
<mat-option [value]="hour">{{ hour }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Minute' | translate }})</mat-label
>
<mat-select formControlName="minStartTime">
@for (minute of minutes; track minute) {
<mat-option [value]="minute">{{ minute }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="flex-48 layout-row gap-10px">
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Hour' | translate }})</mat-label
>
<mat-select formControlName="hourEndTime">
@for (hour of hours; track hour) {
<mat-option [value]="hour">{{ hour }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Minute' | translate }})</mat-label
>
<mat-select formControlName="minEndTime">
@for (minute of minutes; track minute) {
<mat-option [value]="minute">{{ minute }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (createCashierForm.hasError('invalidTimeRange')) {
<div class="flex-48">
<mat-error>
{{ 'labels.inputs.End Time' | translate }} {{ 'labels.commons.must' | translate }}
{{ 'labels.commons.be' | translate }} {{ 'labels.commons.greater than' | translate }}
{{ 'labels.inputs.Start Time' | translate }}
</mat-error>
</div>
}
}
<div class="space flex-100">
<mat-checkbox labelPosition="before" formControlName="isFullDay">
{{ 'labels.inputs.Full Day' | translate }}?
</mat-checkbox>
@@ -46,6 +46,10 @@ export class CreateCashierComponent implements OnInit {
cashierTemplate: any;
/** Create cashier form. */
createCashierForm: UntypedFormGroup;
/** Hours options for time selection (00-23). */
hours: string[] = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
/** Minutes options for time selection (00-59). */
minutes: string[] = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
/**
* Fetches cashier template from `resolve`
@@ -85,7 +89,11 @@ export class CreateCashierComponent implements OnInit {
'',
Validators.required
],
isFullDay: [false]
isFullDay: [true],
hourStartTime: ['00'],
minStartTime: ['00'],
hourEndTime: ['00'],
minEndTime: ['00']
});
}
@@ -104,11 +112,40 @@ export class CreateCashierComponent implements OnInit {
if (createCashierFormData.endDate instanceof Date) {
createCashierFormData.endDate = this.dateUtils.formatDate(prevEndDate, dateFormat);
}
const data = {
...createCashierFormData,
const data: any = {
staffId: createCashierFormData.staffId,
description: createCashierFormData.description,
startDate: createCashierFormData.startDate,
endDate: createCashierFormData.endDate,
isFullDay: createCashierFormData.isFullDay,
dateFormat,
locale
};
// Clear stale time-range errors before re-validating
if (this.createCashierForm.hasError('invalidTimeRange')) {
const { invalidTimeRange, ...rest } = this.createCashierForm.errors ?? {};
this.createCashierForm.setErrors(Object.keys(rest).length ? rest : null);
}
// Add time fields only when not full day
if (!createCashierFormData.isFullDay) {
const hourStart = createCashierFormData.hourStartTime;
const minStart = createCashierFormData.minStartTime;
const hourEnd = createCashierFormData.hourEndTime;
const minEnd = createCashierFormData.minEndTime;
// Validate that end time is after start time
const startMinutes = Number(hourStart) * 60 + Number(minStart);
const endMinutes = Number(hourEnd) * 60 + Number(minEnd);
if (Number.isNaN(startMinutes) || Number.isNaN(endMinutes) || endMinutes <= startMinutes) {
this.createCashierForm.setErrors({ invalidTimeRange: true });
return;
}
data.hourStartTime = hourStart;
data.minStartTime = minStart;
data.hourEndTime = hourEnd;
data.minEndTime = minEnd;
data.startTime = `${hourStart}:${minStart}`;
data.endTime = `${hourEnd}:${minEnd}`;
}
this.organizationService.createCashier(this.cashierTemplate.tellerId, data).subscribe((response: any) => {
this.router.navigate(['../'], { relativeTo: this.route });
});
@@ -92,7 +92,63 @@
}
</mat-form-field>
<div class="space flex-48">
@if (!editCashierForm.value.isFullDay) {
<div class="flex-48 layout-row gap-10px">
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Hour' | translate }})</mat-label
>
<mat-select formControlName="hourStartTime">
@for (hour of hours; track hour) {
<mat-option [value]="hour">{{ hour }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Minute' | translate }})</mat-label
>
<mat-select formControlName="minStartTime">
@for (minute of minutes; track minute) {
<mat-option [value]="minute">{{ minute }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="flex-48 layout-row gap-10px">
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Hour' | translate }})</mat-label
>
<mat-select formControlName="hourEndTime">
@for (hour of hours; track hour) {
<mat-option [value]="hour">{{ hour }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-50">
<mat-label
>{{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Minute' | translate }})</mat-label
>
<mat-select formControlName="minEndTime">
@for (minute of minutes; track minute) {
<mat-option [value]="minute">{{ minute }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (editCashierForm.hasError('invalidTimeRange')) {
<div class="flex-48">
<mat-error>
{{ 'labels.inputs.End Time' | translate }} {{ 'labels.commons.must' | translate }}
{{ 'labels.commons.be' | translate }} {{ 'labels.commons.greater than' | translate }}
{{ 'labels.inputs.Start Time' | translate }}
</mat-error>
</div>
}
}
<div class="space flex-100">
<mat-checkbox labelPosition="before" formControlName="isFullDay">
{{ 'labels.inputs.Full Day' | translate }}?
</mat-checkbox>
@@ -48,6 +48,10 @@ export class EditCashierComponent implements OnInit {
minDate = new Date(2000, 0, 1);
/** Maximum Date allowed. */
maxDate = new Date();
/** Hours options for time selection (00-23). */
hours: string[] = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
/** Minutes options for time selection (00-59). */
minutes: string[] = Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
/**
*
@@ -70,28 +74,49 @@ export class EditCashierComponent implements OnInit {
ngOnInit() {
this.maxDate = this.settingsService.maxFutureDate;
this.setEditChargeForm();
this.setEditCashierForm();
}
/**
* Sets Edit Charge Form.
* Sets Edit Cashier Form.
*/
setEditChargeForm() {
setEditCashierForm() {
// Extract hours and minutes from existing time data with safe defaults
const startTime = this.cashierData.data.startTime || '09:00';
const endTime = this.cashierData.data.endTime || '17:00';
const [
hourStart = '09',
minStart = '00'
] = startTime.includes(':') ? startTime.split(':') : [
'09',
'00'
];
const [
hourEnd = '17',
minEnd = '00'
] = endTime.includes(':') ? endTime.split(':') : [
'17',
'00'
];
this.editCashierForm = this.formBuilder.group({
staffId: [{ value: this.cashierData.data.staffId, disabled: true }],
description: [this.cashierData.data.description],
startDate: [
this.cashierData.data.startDate && new Date(this.cashierData.data.startDate),
this.cashierData.data.startDate ? new Date(this.cashierData.data.startDate) : null,
Validators.required
],
endDate: [
this.cashierData.data.endDate && new Date(this.cashierData.data.endDate),
this.cashierData.data.endDate ? new Date(this.cashierData.data.endDate) : null,
Validators.required
],
isFullDay: [
this.cashierData.data.isFullDay,
Validators.required
]
],
hourStartTime: [hourStart.padStart(2, '0')],
minStartTime: [minStart.padStart(2, '0')],
hourEndTime: [hourEnd.padStart(2, '0')],
minEndTime: [minEnd.padStart(2, '0')]
});
}
@@ -110,12 +135,40 @@ export class EditCashierComponent implements OnInit {
if (editCashierFormData.endDate instanceof Date) {
editCashierFormData.endDate = this.dateUtils.formatDate(prevEndDate, dateFormat);
}
const data = {
...editCashierFormData,
const data: any = {
staffId: this.cashierData.data.staffId,
description: editCashierFormData.description,
startDate: editCashierFormData.startDate,
endDate: editCashierFormData.endDate,
isFullDay: editCashierFormData.isFullDay,
dateFormat,
locale
};
// Clear stale time-range errors before re-validating
if (this.editCashierForm.hasError('invalidTimeRange')) {
const { invalidTimeRange, ...rest } = this.editCashierForm.errors ?? {};
this.editCashierForm.setErrors(Object.keys(rest).length ? rest : null);
}
// Add time fields only when not full day
if (!editCashierFormData.isFullDay) {
const hourStart = editCashierFormData.hourStartTime;
const minStart = editCashierFormData.minStartTime;
const hourEnd = editCashierFormData.hourEndTime;
const minEnd = editCashierFormData.minEndTime;
// Validate that end time is after start time
const startMinutes = Number(hourStart) * 60 + Number(minStart);
const endMinutes = Number(hourEnd) * 60 + Number(minEnd);
if (Number.isNaN(startMinutes) || Number.isNaN(endMinutes) || endMinutes <= startMinutes) {
this.editCashierForm.setErrors({ invalidTimeRange: true });
return;
}
data.hourStartTime = hourStart;
data.minStartTime = minStart;
data.hourEndTime = hourEnd;
data.minEndTime = minEnd;
data.startTime = `${hourStart}:${minStart}`;
data.endTime = `${hourEnd}:${minEnd}`;
}
this.organizationService
.updateCashier(this.cashierData.data.tellerId, this.cashierData.data.id, data)
.subscribe((response: any) => {