Skip to content
Merged
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions libs/data/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
]
}
},
"update-trainings": {
"executor": "nx:run-commands",
"options": {
"cwd": "libs/data",
"commands": [
"npx tsx src/scripts/updateBaserowTrainings.ts"
]
}
},
"generate-programs": {
"executor": "nx:run-commands",
"options": {
Expand All @@ -44,6 +53,15 @@
"parallel": false
}
},
"send-program-mails": {
"executor": "nx:run-commands",
"options": {
"cwd": "libs/data",
"commands": [
"npx tsx src/scripts/sendMailToProgramContacts.ts"
]
}
},
"test": {
"executor": "nx:run-commands",
"options": {
Expand Down
16 changes: 16 additions & 0 deletions libs/data/src/common/baserow/abstractBaserow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,20 @@ export abstract class AbstractBaserow {
}
throw Error('Baserow token not found.')
}

protected async _patchRow(tableId: number, rowId: number, data: Record<string, any>): Promise<void> {
try {
await axios.patch(`${this._baseUrl}/database/rows/table/${tableId}/${rowId}/?user_field_names=true`, data, this._axiosHeader)
} catch (error) {
console.error(`Error patching row ${rowId} in table ${tableId}:`, error)
}
}

protected async _createRow(tableId: number, data: Record<string, any>): Promise<void> {
try {
await axios.post(`${this._baseUrl}/database/rows/table/${tableId}/?user_field_names=true`, data, this._axiosHeader)
} catch (error) {
console.error(`Error creating row in table ${tableId}:`, error)
}
}
}
9 changes: 7 additions & 2 deletions libs/data/src/common/baserow/imageBaserow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ export class ImageBaserow extends AbstractBaserow {

const fileName = `${imageName}.webp`
const filePath = path.join(this._imageDirectory, fileName)
fs.writeFileSync(filePath, webpBuffer)
try {
fs.writeFileSync(filePath, webpBuffer)
} catch {
return Result.err(new Error('Error while trying to create the the local ' + imageName))
}
this._metadata[imageName] = image[0].uploaded_at
this._processedImages.add(fileName)

Expand Down Expand Up @@ -129,7 +133,7 @@ export class ImageBaserow extends AbstractBaserow {
}

private _generateImageName(imageData: ImageTable): string {
let baseName = imageData['Image URL TEE']
let baseName = this._slugify(imageData['Image URL TEE'])
if (!baseName) {
baseName = this._slugify(imageData.Titre)
}
Expand Down Expand Up @@ -169,5 +173,6 @@ export class ImageBaserow extends AbstractBaserow {
.trim()
.replace(/[\s\W-]+/g, '-') // Replace spaces and non-alphanumeric characters with hyphens
.replace(/^-+|-+$/g, '') // Remove leading or trailing hyphens
.slice(0, 50)
}
}
4 changes: 4 additions & 0 deletions libs/data/src/common/baserow/programBaserow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,8 @@ export class ProgramBaserow extends AbstractBaserow {
'Eligibilité Spécifique': conditionalValue['Eligibilité Spécifique']
}
}

async patchProgram(rowId: number, data: Record<string, any>): Promise<void> {
await this._patchRow(this._programTableId, rowId, data)
}
}
30 changes: 30 additions & 0 deletions libs/data/src/common/baserow/trainingBaserow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { AbstractBaserow } from './abstractBaserow'
import { BaserowTraining } from './types'
import { Training } from '../../trainings/types'

export class TrainingBaserow extends AbstractBaserow {
private readonly _tableId = 620771

async getAll(): Promise<BaserowTraining[]> {
return this._getTableData<BaserowTraining>(this._tableId)
}

async patchOrCreate(training: Training, existingTrainings: BaserowTraining[]): Promise<void> {
const match = existingTrainings.find((baserowTraining) => baserowTraining['Id Ademe'] === training.id)

const payload: Partial<BaserowTraining> = {
'Id Ademe': training.id,
'Futures Sessions': training.session ? JSON.stringify(Array.isArray(training.session) ? training.session : [training.session]) : '',
Titre: training.Libellé_de_la_formation,
Promesse: training.Chapeau_pour_site_web,
'Url ADEME': training.Lien_URL_vers_la_fiche_programme,
Objectifs: training.Objectifs_de_la_formation
}

if (match && match.id) {
await this._patchRow(this._tableId, match.id, payload)
} else {
await this._createRow(this._tableId, payload)
}
}
}
16 changes: 16 additions & 0 deletions libs/data/src/common/baserow/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,19 @@ export interface BaserowTestimony extends Id, BaserowSectors {
'Mise en avant': number
'Nom entreprise': string
}

export interface BaserowTraining extends Id {
'Id Ademe': string
'Futures Sessions': string
Titre: string
Promesse: string
'Url ADEME': string
Objectifs: string
}

export interface ProgramTechField {
prod_release_date: string
email_enable: boolean // TODO
last_mail_sent_date?: string
eol_mail_sent_date?: string
}
149 changes: 149 additions & 0 deletions libs/data/src/program/mailManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import path from 'path'
import { ProgramBaserow } from '../common/baserow/programBaserow'
import fs from 'fs'
import { parse } from 'csv-parse/sync'
import { fileURLToPath } from 'url'
import { ProgramTechField } from '../common/baserow/types'
import { ProgramUtils } from './programUtils'
import { jsonPrograms } from '../../static'

interface ProgramCsvRow {
filename: string
date_added: string
}

export class MailManager {
async sendProgramsMails() {
const baserow = new ProgramBaserow()
const programs = await baserow.getPrograms(false)

const today = new Date()
const prodCutoffDate = new Date('2025-08-01')

for (const program of programs) {
if (!ProgramUtils.isInProd(program)) {
continue
}
const programRowId = program.id
const tech: ProgramTechField = JSON.parse(program.tech) || {}

const prodDate = tech.prod_release_date ? new Date(tech.prod_release_date) : null
if (!prodDate || prodDate < prodCutoffDate) {
continue
}

let techChanged = false

if (!tech.last_mail_sent_date) {
// await brevo.sendInitialMail(program)
tech.last_mail_sent_date = today.toISOString()
techChanged = true
console.log(`📩 Sent initial mail for program ${programRowId}`)
} else {
const lastSent = new Date(tech.last_mail_sent_date)
const newMailMinDate = new Date(lastSent.getFullYear(), lastSent.getMonth() + 6, lastSent.getDate())
if (today >= newMailMinDate && !tech.eol_mail_sent_date) {
// await brevo.sendPeriodicMail(program)
tech.last_mail_sent_date = today.toISOString()
techChanged = true
console.log(`📩 Sent periodic mail for program ${programRowId}`)
}
}

const endDate = program.DISPOSITIF_DATE_FIN ? new Date(program.DISPOSITIF_DATE_FIN) : null
if (endDate) {
const eolMailMinDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate() - 15)

if (today >= eolMailMinDate && !tech.eol_mail_sent_date) {
// await brevo.sendEolMail(program)
tech.eol_mail_sent_date = today.toISOString()
techChanged = true
console.log(`📩 Sent EOL mail for program ${programRowId}`)
}

const todayPlus15Days = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 15)
if (tech.eol_mail_sent_date && endDate > todayPlus15Days) {
// if the eol date changed, the eol mail sent is not relevant anymore
delete tech.eol_mail_sent_date
techChanged = true
console.log(`🧼 Cleared EOL mail flag for program ${programRowId} (end_date moved)`)
}
}

if (techChanged) {
// await baserow.patchProgram(program.id, {
// tech: JSON.stringify(tech)
// })
console.log(`✅ Patched tech field for program ${programRowId}`)
await new Promise((res) => setTimeout(res, 200)) // Wait 0.2s between requeststo avoid hitting API limits
}
}
}

async populate_baserow_prod_date() {
//
// Ajouter Commande bash/git pour sortir le csv
const csvPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../static/program_main_merge_date.csv')
const csvContent = fs.readFileSync(csvPath, 'utf8')

const records = parse<ProgramCsvRow>(csvContent, {
columns: true,
skip_empty_lines: true
})

const programs = await new ProgramBaserow().getPrograms(false)

const baserow = new ProgramBaserow()
for (const record of records) {
const csvKey = record.filename.slice(0, -5)
const mergeDate = record.date_added

const matchingProgram = programs.find((program) => program['Id fiche dispositif'] === csvKey)

if (matchingProgram) {
const updatedTech = {
prod_release_date: mergeDate,
email_enable: false
}

console.log(csvKey, matchingProgram.id, updatedTech)
await baserow.patchProgram(matchingProgram.id, { tech: JSON.stringify(updatedTech) })
await new Promise((resolve) => setTimeout(resolve, 200))

console.log(`✅ Patched program ${csvKey} with merge date ${mergeDate}`)
} else {
console.log(`❌ No match found in Baserow for CSV key: ${csvKey}`)
}
}
}

async populateFutureProdDates() {
const baserow = new ProgramBaserow()
const programs = await baserow.getPrograms(false)

const today = new Date().toISOString()

// lire les entrées json;
// CHECK si program.tech est vide plutot que de cast avec un || {}

for (const program of programs) {
const programKey = program['Id fiche dispositif']
const tech: ProgramTechField = JSON.parse(program.tech) || {}

if (tech.prod_release_date) continue

const matchingJson = jsonPrograms.find((p) => p.id === programKey)
if (matchingJson) {
tech.prod_release_date = today
tech.email_enable = true

// await baserow.patchProgram(program.id, {
// tech: JSON.stringify(tech),
// })

console.log(`✅ Set prod_release_date for program ${programKey}`)
await new Promise((res) => setTimeout(res, 200))
}
}
}
}
1 change: 1 addition & 0 deletions libs/data/src/program/programTechFieldManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// TO FILL WITH METHODS INSIDE mailManger ATM.
1 change: 1 addition & 0 deletions libs/data/src/program/types/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface DataProgram extends BaserowSectors {
Statuts: Status[]
conditionalData?: ConditionalValues[]
'redirection-vers': number[]
tech: string
}

export enum Publicodes {
Expand Down
12 changes: 12 additions & 0 deletions libs/data/src/scripts/sendMailToProgramContacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MailManager } from '../program/mailManager'

console.log('Start sending mail to Program Contact if needed')

new MailManager()
.sendProgramsMails()
.then(() => {
console.log('Program mails sent to contacts')
})
.catch((error) => {
console.error('Error while sending mails to program contacts:', error)
})
12 changes: 12 additions & 0 deletions libs/data/src/scripts/updateBaserowTrainings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { TrainingFeatures } from '../trainings/trainingFeatures'

console.log('Start the Training updates')

new TrainingFeatures()
.loadTrainings()
.then(() => {
console.log('Project data generated')
})
.catch((error) => {
console.error('Error during the project data generation:', error)
})
23 changes: 23 additions & 0 deletions libs/data/src/trainings/trainingFeatures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { parseStringPromise } from 'xml2js'
import axios from 'axios'
import { Training } from './types'
import { BaserowTraining } from '../common/baserow/types'
import { TrainingBaserow } from '../common/baserow/trainingBaserow'

export class TrainingFeatures {
async loadTrainings(): Promise<Training[]> {
const xmlPath = 'https://formations.ademe.fr/tmp/flux_formations_agir.xml'
const xmlContent = await axios.get(xmlPath).then((res) => res.data)
const parsed = await parseStringPromise(xmlContent, { mergeAttrs: true, explicitArray: false })
const formations: Training[] = parsed.formations.module || []

const trainingBaserow = new TrainingBaserow()
const existingTrainings: BaserowTraining[] = await trainingBaserow.getAll()
for (const formation of formations) {
await trainingBaserow.patchOrCreate(formation, existingTrainings)
await new Promise((res) => setTimeout(res, 200))
}

return []
}
}
Loading
Loading