Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/app/modules/quest/my-slots/my-slots.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import * as moment from 'moment';

import { Queteur } from '../../../model/queteur';
import { Tronc } from '../../../model/tronc';
import { CloudFunctionService } from '../../../services/cloud-functions/cloud-function.service';
Expand Down Expand Up @@ -49,7 +47,7 @@ export class MySlotsComponent implements OnInit {

handleTroncDeparture(tronc: Tronc) {
const update = {
date: moment(tronc.depart).subtract(2, 'hours').format('YYYY-MM-DD HH:mm:ss'),
date: new Date(tronc.depart).toISOString(),
tqId: tronc.tronc_queteur_id,
isDepart: true
};
Expand All @@ -65,7 +63,7 @@ export class MySlotsComponent implements OnInit {

handleTroncArrival(tronc: Tronc) {
const update = {
date: moment(tronc.arrivee).subtract(2, 'hours').format('YYYY-MM-DD HH:mm:ss'),
date: new Date(tronc.arrivee).toISOString(),
tqId: tronc.tronc_queteur_id,
isDepart: false
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
<li>Tronc n° {{tronc.value.tronc_id}}</li>
<li *ngIf="type === 'arrival'">
Parti depuis
<b>{{tronc.value.depart| date:'H'}}h{{tronc.value.depart| date:'mm'}}</b> le
<b>{{tronc.value.depart| date:'fullDate':'+0200':'fr-FR'}}</b>
<b>{{tronc.value.depart| parisDate:'H'}}h{{tronc.value.depart| parisDate:'mm'}}</b> le
<b>{{tronc.value.depart| parisDate:'fullDate'}}</b>
</li>
<li *ngIf="type === 'departure'">
Départ planifié à
<b>{{tronc.value.depart_theorique| date:'H'}}h{{tronc.value.depart_theorique| date:'mm'}}</b> le
<b>{{tronc.value.depart_theorique| date:'fullDate':'+0200':'fr-FR'}}</b>
<b>{{tronc.value.depart_theorique| parisDate:'H'}}h{{tronc.value.depart_theorique| parisDate:'mm'}}</b> le
<b>{{tronc.value.depart_theorique| parisDate:'fullDate'}}</b>
</li>
</ul>
</div>
Expand Down Expand Up @@ -65,14 +65,14 @@
<div *ngIf="tronc.value && type == 'departure'">

Enregister un départ du tronc n° {{tronc.value.tronc_id}} à
<b>{{startDate.value| date:'H'}}h{{startDate.value| date:'mm'}}</b> le
<b>{{startDate.value| date:'fullDate':'+0200':'fr-FR'}}</b> à destination de
<b>{{startDate.value| parisDate:'H'}}h{{startDate.value| parisDate:'mm'}}</b> le
<b>{{startDate.value| parisDate:'fullDate'}}</b> à destination de
{{tronc.value.name}}
</div>
<div *ngIf="tronc.value && type != 'departure'">
Enregister un retour du tronc n° {{tronc.value.tronc_id}} à
<b>{{startDate.value| date:'H'}}h{{startDate.value| date:'mm'}}</b> le
<b>{{startDate.value| date:'fullDate':'+0200':'fr-FR'}}</b> en provenance de
<b>{{startDate.value| parisDate:'H'}}h{{startDate.value| parisDate:'mm'}}</b> le
<b>{{startDate.value| parisDate:'fullDate'}}</b> en provenance de
{{tronc.value.name}}
</div>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ <h1 mat-dialog-title>ID: {{troncStat.id}}</h1>
<ul>
<li>Point de quête: {{troncStat.point_quete}}</li>
<li>Tronc n° {{troncStat.tronc_id}}</li>
<li>Départ théorique: {{troncStat.depart_theorique|date:'medium':'+0200':'fr-FR'}}</li>
<li>Départ: {{troncStat.depart|date:'medium':'+0200':'fr-FR'}}</li>
<li>Retour: {{troncStat.retour|date:'medium':'+0200':'fr-FR'}}</li>
<li>Comptage: {{troncStat.comptage|date:'medium':'+0200':'fr-FR'}}</li>
<li>Départ théorique: {{troncStat.depart_theorique|parisDate}}</li>
<li>Départ: {{troncStat.depart|parisDate}}</li>
<li>Retour: {{troncStat.retour|parisDate}}</li>
<li>Comptage: {{troncStat.comptage|parisDate}}</li>
<li>Total: {{troncStat.amount}}€</li>
<li>Don CB: {{troncStat.don_creditcard}}€</li>
<li>Don chèque: {{troncStat.don_cheque}}€</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<button matLine>
</button>
<p matLine>
Départ le <b>{{troncStat.depart| date:'medium':'+0200':'fr-FR'}}</b> sur le lieu suivant:
Départ le <b>{{troncStat.depart| parisDate}}</b> sur le lieu suivant:
<b>{{troncStat.point_quete}}</b>
</p>
<p matLine>
Expand Down
70 changes: 70 additions & 0 deletions src/app/pipes/paris-date.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {ParisDatePipe} from './paris-date.pipe';

describe('ParisDatePipe', () => {
const pipe = new ParisDatePipe();

it('create an instance', () => {
expect(pipe).toBeTruthy();
});

it('returns empty string for null', () => {
expect(pipe.transform(null)).toBe('');
});

it('returns empty string for undefined', () => {
expect(pipe.transform(undefined)).toBe('');
});

it('returns empty string for empty string', () => {
expect(pipe.transform('')).toBe('');
});

it('returns empty string for invalid date', () => {
expect(pipe.transform('not-a-date')).toBe('');
});

it('summer ISO Z input → Paris hour is UTC+2', () => {
expect(pipe.transform('2026-05-24T07:00:00Z', 'H')).toBe('9');
});

it('summer ISO Z input → Paris minute is unchanged', () => {
expect(pipe.transform('2026-05-24T07:30:00Z', 'mm')).toBe('30');
});

it('winter ISO Z input → Paris hour is UTC+1', () => {
expect(pipe.transform('2026-01-15T07:00:00Z', 'H')).toBe('8');
});

it('legacy naive string (no Z) is interpreted as UTC', () => {
expect(pipe.transform('2026-05-24 07:00:00', 'H')).toBe('9');
});

it('legacy naive string with T separator is interpreted as UTC', () => {
expect(pipe.transform('2026-05-24T07:00:00', 'H')).toBe('9');
});

it('string with explicit offset is respected', () => {
expect(pipe.transform('2026-05-24T09:00:00+02:00', 'H')).toBe('9');
});

it('Date object input is accepted', () => {
const d = new Date(Date.UTC(2026, 4, 24, 7, 0, 0));
expect(pipe.transform(d, 'H')).toBe('9');
});

it('fullDate format produces a non-empty French date string', () => {
const out = pipe.transform('2026-05-24T07:00:00Z', 'fullDate');
expect(out).toContain('2026');
expect(out).toContain('mai');
});

it('medium format includes year and time digits', () => {
const out = pipe.transform('2026-05-24T07:00:00Z', 'medium');
expect(out).toContain('2026');
expect(out).toMatch(/09/);
});

it('crossing midnight in Paris from UTC keeps correct date', () => {
expect(pipe.transform('2026-05-23T23:30:00Z', 'H')).toBe('1');
});
});
64 changes: 64 additions & 0 deletions src/app/pipes/paris-date.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Pipe, PipeTransform} from '@angular/core';

export type ParisDateFormat = 'medium' | 'fullDate' | 'H' | 'mm';

@Pipe({
name: 'parisDate'
})
export class ParisDatePipe implements PipeTransform {

transform(value: string | Date | null | undefined, format: ParisDateFormat = 'medium'): string {
if (value === null || value === undefined || value === '') {
return '';
}
const date = typeof value === 'string' ? this.parseString(value) : value;
if (!(date instanceof Date) || isNaN(date.getTime())) {
return '';
}

switch (format) {
case 'H':
return this.parisHour(date);
case 'mm':
return this.parisMinute(date);
case 'fullDate':
return this.formatWith(date, {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
});
case 'medium':
default:
return this.formatWith(date, {
day: 'numeric', month: 'short', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
});
}
}

private parseString(s: string): Date {
if (/Z|[+\-]\d{2}:?\d{2}$/.test(s)) {
return new Date(s);
}
return new Date(s.replace(' ', 'T') + 'Z');
}

private formatWith(date: Date, opts: Intl.DateTimeFormatOptions): string {
return new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', ...opts }).format(date);
}

private parisHour(date: Date): string {
const parts = new Intl.DateTimeFormat('fr-FR', {
timeZone: 'Europe/Paris', hour: '2-digit', hour12: false
}).formatToParts(date);
const hour = (parts.find(p => p.type === 'hour') || { value: '0' }).value;
return String(parseInt(hour, 10));
}

private parisMinute(date: Date): string {
const parts = new Intl.DateTimeFormat('fr-FR', {
timeZone: 'Europe/Paris', minute: '2-digit', hour: '2-digit', hour12: false
}).formatToParts(date);
const minute = (parts.find(p => p.type === 'minute') || { value: '00' }).value;
return minute.length < 2 ? '0' + minute : minute;
}

}
14 changes: 12 additions & 2 deletions src/app/services/cloud-functions/cloud-function.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,24 @@ export class CloudFunctionService {

private parseTroncDates(row: any): Tronc {
if (row.depart_theorique) {
row.depart_theorique = new Date(row.depart_theorique);
row.depart_theorique = this.parseAsUtcIfNaive(row.depart_theorique);
}
if (row.depart) {
row.depart = new Date(row.depart);
row.depart = this.parseAsUtcIfNaive(row.depart);
}
return row as Tronc;
}

private parseAsUtcIfNaive(value: string | Date): Date {
if (value instanceof Date) {
return value;
}
if (/Z|[+\-]\d{2}:?\d{2}$/.test(value)) {
return new Date(value);
}
return new Date(value.replace(' ', 'T') + 'Z');
}

private readULDetailsFromCache(token: string): ULDetails | null {
const key = this.ulDetailsCachePrefix + token;
try {
Expand Down
5 changes: 3 additions & 2 deletions src/app/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';

import { OwlDateTimeModule, OwlNativeDateTimeModule, OWL_DATE_TIME_LOCALE } from 'ng-pick-datetime-ex';

import { ParisDatePipe } from './pipes/paris-date.pipe';
import { TimePipe } from './pipes/time.pipe';
import { WeightPipe } from './pipes/weight.pipe';

Expand All @@ -40,11 +41,11 @@ const MatModules = [
MatStepperModule, MatExpansionModule, MatChipsModule, MatGridListModule, MatListModule];

@NgModule({
declarations: [TimePipe, WeightPipe],
declarations: [ParisDatePipe, TimePipe, WeightPipe],
imports: [FormsModule, ReactiveFormsModule, FlexLayoutModule, MatModules,
OwlDateTimeModule, OwlNativeDateTimeModule],
exports: [
FormsModule, ReactiveFormsModule, TimePipe, WeightPipe,
FormsModule, ReactiveFormsModule, ParisDatePipe, TimePipe, WeightPipe,
OwlDateTimeModule, OwlNativeDateTimeModule,
MatModules, FlexLayoutModule],
providers: [
Expand Down