298 lines
9.3 KiB
TypeScript
298 lines
9.3 KiB
TypeScript
import { Component, OnInit, inject } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { Router } from '@angular/router';
|
|
import { OedbApi } from '../../services/oedb-api';
|
|
import oedb from '../../../oedb-types';
|
|
|
|
interface Event {
|
|
id?: string;
|
|
properties: {
|
|
what?: string;
|
|
createdate?: string;
|
|
start?: string;
|
|
stop?: string;
|
|
label?: string;
|
|
name?: string;
|
|
where?: string;
|
|
[key: string]: any;
|
|
};
|
|
geometry?: any;
|
|
}
|
|
|
|
interface WhatCount {
|
|
what: string;
|
|
count: number;
|
|
label: string;
|
|
emoji: string;
|
|
}
|
|
|
|
interface CreationMonth {
|
|
month: string;
|
|
count: number;
|
|
}
|
|
|
|
interface CreationWeek {
|
|
week: string;
|
|
count: number;
|
|
}
|
|
|
|
interface DurationBucket {
|
|
days: string;
|
|
count: number;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-stats',
|
|
standalone: true,
|
|
imports: [CommonModule],
|
|
templateUrl: './stats.html',
|
|
styleUrl: './stats.scss'
|
|
})
|
|
export class Stats implements OnInit {
|
|
private oedbApi = inject(OedbApi);
|
|
private router = inject(Router);
|
|
|
|
isLoading = true;
|
|
events: Event[] = [];
|
|
whatCounts: WhatCount[] = [];
|
|
creationHistogram: CreationMonth[] = [];
|
|
creationHistogramByWeek: CreationWeek[] = [];
|
|
durationDistribution: DurationBucket[] = [];
|
|
recentEvents: Event[] = [];
|
|
|
|
meetingTypes: WhatCount[] = [];
|
|
reportingTypes: WhatCount[] = [];
|
|
|
|
ngOnInit() {
|
|
this.loadStats();
|
|
}
|
|
|
|
loadStats() {
|
|
this.isLoading = true;
|
|
this.oedbApi.getEvents({ limit: 10000 }).subscribe({
|
|
next: (response: any) => {
|
|
this.events = Array.isArray(response?.features) ? response.features : [];
|
|
this.processStats();
|
|
this.isLoading = false;
|
|
},
|
|
error: (err) => {
|
|
console.error('Error loading stats:', err);
|
|
this.isLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
processStats() {
|
|
// Compter par catégorie what
|
|
const whatMap: Record<string, number> = {};
|
|
for (const event of this.events) {
|
|
const what = event.properties?.what || 'non-défini';
|
|
whatMap[what] = (whatMap[what] || 0) + 1;
|
|
}
|
|
|
|
this.whatCounts = Object.entries(whatMap)
|
|
.map(([what, count]) => {
|
|
const preset = (oedb.presets.what as any)[what];
|
|
return {
|
|
what,
|
|
count,
|
|
label: preset?.label || what,
|
|
emoji: preset?.emoji || '📌'
|
|
};
|
|
})
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 20); // Top 20
|
|
|
|
// Histogramme de création par mois
|
|
const creationMap: Record<string, number> = {};
|
|
for (const event of this.events) {
|
|
const createdate = event.properties?.createdate;
|
|
if (createdate) {
|
|
const date = new Date(createdate);
|
|
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
creationMap[monthKey] = (creationMap[monthKey] || 0) + 1;
|
|
}
|
|
}
|
|
this.creationHistogram = Object.entries(creationMap)
|
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
.map(([month, count]) => ({ month, count }));
|
|
|
|
// Histogramme de création par semaine
|
|
const creationWeekMap: Record<string, number> = {};
|
|
for (const event of this.events) {
|
|
const createdate = event.properties?.createdate;
|
|
if (createdate) {
|
|
const date = new Date(createdate);
|
|
const weekKey = this.getWeekKey(date);
|
|
creationWeekMap[weekKey] = (creationWeekMap[weekKey] || 0) + 1;
|
|
}
|
|
}
|
|
this.creationHistogramByWeek = Object.entries(creationWeekMap)
|
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
.map(([week, count]) => ({ week, count }));
|
|
|
|
// Distribution des durées
|
|
const durationMap: Record<string, number> = {
|
|
'0': 0, // 0 jour
|
|
'1': 0, // 1 jour
|
|
'2-7': 0, // 2-7 jours
|
|
'8-30': 0, // 8-30 jours
|
|
'31-90': 0, // 31-90 jours
|
|
'90+': 0 // Plus de 90 jours
|
|
};
|
|
for (const event of this.events) {
|
|
const start = event.properties?.start;
|
|
const stop = event.properties?.stop;
|
|
if (start && stop) {
|
|
const startDate = new Date(start);
|
|
const stopDate = new Date(stop);
|
|
const days = Math.floor((stopDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
if (days === 0) durationMap['0']++;
|
|
else if (days === 1) durationMap['1']++;
|
|
else if (days >= 2 && days <= 7) durationMap['2-7']++;
|
|
else if (days >= 8 && days <= 30) durationMap['8-30']++;
|
|
else if (days >= 31 && days <= 90) durationMap['31-90']++;
|
|
else durationMap['90+']++;
|
|
}
|
|
}
|
|
this.durationDistribution = Object.entries(durationMap)
|
|
.map(([days, count]) => ({ days, count }));
|
|
|
|
// 10 événements les plus récents
|
|
this.recentEvents = [...this.events]
|
|
.filter(e => e.properties?.createdate)
|
|
.sort((a, b) => {
|
|
const dateA = new Date(a.properties.createdate || 0).getTime();
|
|
const dateB = new Date(b.properties.createdate || 0).getTime();
|
|
return dateB - dateA;
|
|
})
|
|
.slice(0, 10);
|
|
|
|
// Identifier les types de réunions et signalements
|
|
this.identifySpecialTypes();
|
|
}
|
|
|
|
identifySpecialTypes() {
|
|
const whatPresets = oedb.presets.what as Record<string, any>;
|
|
const meetingKeywords = ['meeting', 'conférence', 'conférence', 'réunion', 'event', 'gathering', 'assembly'];
|
|
const reportingKeywords = ['signalement', 'incident', 'accident', 'obstacle', 'interruption', 'damage', 'obstruction'];
|
|
|
|
const meetingSet = new Set<string>();
|
|
const reportingSet = new Set<string>();
|
|
|
|
for (const [key, preset] of Object.entries(whatPresets)) {
|
|
const label = (preset.label || '').toLowerCase();
|
|
const description = (preset.description || '').toLowerCase();
|
|
const what = key.toLowerCase();
|
|
|
|
// Vérifier si c'est une réunion
|
|
const isMeeting = meetingKeywords.some(kw =>
|
|
label.includes(kw) || description.includes(kw) || what.includes(kw)
|
|
) || key.includes('community') || key.includes('culture');
|
|
|
|
// Vérifier si c'est un signalement
|
|
const isReporting = reportingKeywords.some(kw =>
|
|
label.includes(kw) || description.includes(kw) || what.includes(kw)
|
|
) || key.includes('traffic') || key.includes('hazard') || key.includes('weather');
|
|
|
|
// Vérifier si présence ou distance
|
|
if (isMeeting) {
|
|
const isOnline = label.includes('en ligne') || label.includes('online') ||
|
|
description.includes('en ligne') || description.includes('online') ||
|
|
what.includes('online') || preset.properties?.online;
|
|
|
|
if (this.events.some(e => e.properties?.what === key)) {
|
|
meetingSet.add(key);
|
|
}
|
|
}
|
|
|
|
if (isReporting && this.events.some(e => e.properties?.what === key)) {
|
|
reportingSet.add(key);
|
|
}
|
|
}
|
|
|
|
this.meetingTypes = Array.from(meetingSet)
|
|
.map(what => {
|
|
const preset = whatPresets[what];
|
|
const count = this.events.filter(e => e.properties?.what === what).length;
|
|
return {
|
|
what,
|
|
count,
|
|
label: preset?.label || what,
|
|
emoji: preset?.emoji || '📌'
|
|
};
|
|
})
|
|
.sort((a, b) => b.count - a.count);
|
|
|
|
this.reportingTypes = Array.from(reportingSet)
|
|
.map(what => {
|
|
const preset = whatPresets[what];
|
|
const count = this.events.filter(e => e.properties?.what === what).length;
|
|
return {
|
|
what,
|
|
count,
|
|
label: preset?.label || what,
|
|
emoji: preset?.emoji || '📌'
|
|
};
|
|
})
|
|
.sort((a, b) => b.count - a.count);
|
|
}
|
|
|
|
navigateToWhat(what: string) {
|
|
this.router.navigate(['/agenda'], { queryParams: { what } });
|
|
}
|
|
|
|
getMaxCount(): number {
|
|
return Math.max(...this.creationHistogram.map(h => h.count), 1);
|
|
}
|
|
|
|
getMaxWeekCount(): number {
|
|
return Math.max(...this.creationHistogramByWeek.map(h => h.count), 1);
|
|
}
|
|
|
|
getWeekKey(date: Date): string {
|
|
// Obtenir le premier jour de la semaine (lundi)
|
|
const d = new Date(date.getTime()); // Copier la date sans la modifier
|
|
const day = d.getDay();
|
|
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Ajuster pour que lundi soit 0
|
|
const monday = new Date(d.getFullYear(), d.getMonth(), diff);
|
|
|
|
// Obtenir le numéro de semaine ISO (semaine commençant le lundi)
|
|
const weekNumber = this.getISOWeek(monday);
|
|
return `${monday.getFullYear()}-S${String(weekNumber).padStart(2, '0')}`;
|
|
}
|
|
|
|
getISOWeek(date: Date): number {
|
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
const dayNum = d.getUTCDay() || 7;
|
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
|
}
|
|
|
|
formatWeek(weekKey: string): string {
|
|
const [year, week] = weekKey.split('-S');
|
|
return `S${week} ${year}`;
|
|
}
|
|
|
|
getMaxDurationCount(): number {
|
|
return Math.max(...this.durationDistribution.map(d => d.count), 1);
|
|
}
|
|
|
|
formatMonth(monthKey: string): string {
|
|
const [year, month] = monthKey.split('-');
|
|
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
|
|
return `${months[parseInt(month) - 1]} ${year}`;
|
|
}
|
|
|
|
formatDate(dateString: string): string {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('fr-FR', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
}
|