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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ PNDS_TOKEN=
SMS_SERVICE_SHOW=
SMS_SERVICE_USERNAME=
SMS_SERVICE_PASSWORD=

DATA_INCLUSION_TOKEN=
4 changes: 4 additions & 0 deletions backend/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ const config: Configuration = {
url: "https://europe.ipx.com/restapi/v1/sms/send",
internationalDiallingCodes: ["33", "262", "508", "590", "594", "596"],
},
dataInclusion: {
url: "https://api.data.inclusion.gouv.fr/api/v1",
token: process.env.DATA_INCLUSION_TOKEN || "",
},
}

export default Object.freeze(config)
2 changes: 2 additions & 0 deletions backend/routes-loader/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import supportRoutes from "../routes/support.js"
import teleservicesRoutes from "../routes/teleservices.js"
import webhookRoutes from "../routes/webhook.js"
import moncompteproRoutes from "../routes/moncomptepro.js"
import lieuxRoutes from "../routes/lieux.js"

const api = express()

Expand All @@ -29,6 +30,7 @@ simulationRoutes(api)
supportRoutes(api)
teleservicesRoutes(api)
webhookRoutes(api)
lieuxRoutes(api)

api.all("*", function (req, res) {
res.sendStatus(404)
Expand Down
35 changes: 35 additions & 0 deletions backend/routes/lieux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import axios from "axios"
import { Express } from "express"
import config from "../config/index.js"

export default function (api: Express) {
api.route("/lieux/ccas/:codeCommune").get(async (req, res) => {
try {
const { codeCommune } = req.params

// Verify the data-inclusion token exists at runtime.
const token = config?.dataInclusion?.token
if (!token) {
console.warn(
"Missing Data Inclusion token: the /lieux/ccas endpoint requires a valid token",
)
return res
.status(503)
.json({ error: "Service unavailable - missing configuration" })
}

const url = `${config.dataInclusion.url}/structures?sources=ma-boussole-aidants&reseaux_porteurs=ccas-cias&code_commune=${codeCommune}`

const response = await axios.get(url, {
headers: {
Authorization: `Bearer ${config.dataInclusion.token}`,
},
})

res.json(response.data)
} catch (error) {
console.error("Error fetching CCAS/CIAS structures:", error)
res.status(500).json({ error: "Failed to fetch structures" })
}
})
}
4 changes: 4 additions & 0 deletions backend/types/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,8 @@ export interface Configuration {
url: string
internationalDiallingCodes: string[]
}
dataInclusion: {
url: string
token: string
}
}
105 changes: 97 additions & 8 deletions lib/benefits/lieux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,108 @@ export function normalize(lieu) {
return normalizedLieu
}

// OSM = OpenStreetMap - Format standard pour les horaires d'ouverture
const osmDayMapping: Record<string, string> = {
Mo: "lundi",
Tu: "mardi",
We: "mercredi",
Th: "jeudi",
Fr: "vendredi",
Sa: "samedi",
Su: "dimanche",
}

function parseOsmHours(osmHours: string | null) {
if (!osmHours) return undefined

const result: any[] = []
const parts = osmHours.split(";").map((p) => p.trim())

for (const part of parts) {
// PH off = Public Holidays off (fermé les jours fériés)
if (part === "PH off") continue

// Exemple attendu : "Mo-Fr 08:30-12:30" ou "Mo 08:30-12:30"
const match = part.match(/^([a-zA-Z]{2})(?:-([a-zA-Z]{2}))?\s+(.+)$/)
if (match) {
const [, start, end, timesStr] = match
const du = osmDayMapping[start]
const au = end ? osmDayMapping[end] : du

if (du && au) {
const heures = timesStr.split(",").map((t) => {
const [de, a] = t.trim().split("-")
return { de, a }
})

result.push({ du, au, heures })
}
}
}

return result.length ? result : undefined
}

export function normalizeDataInclusion(structure: any) {
const lieu: any = {
id: structure.id,
nom: structure.nom,
telephone: structure.telephone,
url: structure.site_web,
pivotLocal: "ccas",
adresse: {
codePostal: structure.code_postal,
commune: structure.commune,
lignes: [structure.adresse, structure.complement_adresse].filter(Boolean),
},
source: "boussoleaidants",
}

const horaires = parseOsmHours(structure.horaires_accueil)
if (horaires) {
lieu.horaires = horaires
}

return lieu
}

export function getBenefitLieuxTypes(benefit: any): string[] {
const lieuxTypes = benefit.lieuxTypes || benefit.institution.lieuxTypes || []
return lieuxTypes
return benefit.lieuxTypes || benefit.institution.lieuxTypes || []
}

export async function fetchLieux(
depcom: string,
types: string[],
): Promise<any[]> {
const url = `https://etablissements-publics.api.gouv.fr/v3/communes/${depcom}/${types.join(
"+",
)}`
const response = await axios.get(url)
const lieux = response.data.features.map(normalize)
return lieux
const ccasIndex = types.indexOf("ccas")
let ccasLieux: any[] = []

if (ccasIndex !== -1) {
// CCAS are fetched from data-inclusion API (etablissements-publics API doesn't support them)
try {
const response = await axios.get(`/api/lieux/ccas/${depcom}`)
if (response.data && response.data.items) {
ccasLieux = response.data.items.map(normalizeDataInclusion)
}
} catch (e) {
console.error("Failed to fetch CCAS lieux", e)
}
// Remove ccas from types for the other call
types = types.filter((t) => t !== "ccas")
}

let otherLieux: any[] = []
if (types.length > 0) {
const url = `https://etablissements-publics.api.gouv.fr/v3/communes/${depcom}/${types.join(
"+",
)}`
try {
const response = await axios.get(url)
otherLieux = response.data.features.map(normalize)
} catch (e) {
console.error("Failed to fetch other lieux", e)
}
}

return [...ccasLieux, ...otherLieux]
}
1 change: 1 addition & 0 deletions lib/types/lieu.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface LieuProperties {
commune: string
lignes: string[]
}
source?: string
}
Binary file added public/img/partners/boussoleaidants.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/partners/boussoleaidants.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/components/home/partners-section.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,10 @@ const partners: Partner[] = [
name: "étudiants.gouv.fr",
link: "https://etudiants.gouv.fr/",
},
{
id: "boussoleaidants",
name: "Ma boussole Aidants",
link: "https://maboussoleaidants.fr/",
},
]
</script>
16 changes: 15 additions & 1 deletion src/components/lieu-informations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const extractHHMM = (dateString: string) => {
<div
v-for="plage_jour in lieu.horaires"
:key="plage_jour.du"
class="fr-col-6 fr-col-lg-4"
class="fr-col-12 fr-col-md-6 fr-col-lg-4"
>
<div v-if="plage_jour.du === plage_jour.au" class="fr-text--bold">
Les {{ plage_jour.du }}s
Expand All @@ -88,6 +88,20 @@ const extractHHMM = (dateString: string) => {
</div>
</div>
</div>

<div v-if="lieu.source === 'boussoleaidants'" class="fr-mb-3w">
<div class="fr-callout fr-callout--info fr-p-2w">
<div class="fr-grid-row fr-grid-row--middle">
<div class="fr-col">
Donnée fournie par
<a href="https://maboussoleaidants.fr/" target="_blank" rel="noopener"
>Ma boussole Aidants</a
>
</div>
</div>
</div>
</div>

<div class="fr-container">
<ul
v-if="hasContact"
Expand Down
49 changes: 22 additions & 27 deletions src/styles/aides-jeunes.css
Original file line number Diff line number Diff line change
Expand Up @@ -662,27 +662,23 @@ textarea {
}

.aj-partners {
display: grid;
grid-template-columns: repeat(6, 1fr); /* 6 columns per line */
gap: 1.5rem;
align-items: center;
justify-items: center;
max-width: 500px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
}

/* First line of partners section
* 2 columns per logo => 3 logos on the first line
*/
.aj-partners > *:nth-child(-n + 3) {
grid-column: span 2;
.aj-partner-link {
flex: 0 0 auto;
min-width: 200px;
}

.aj-partner-link {
background-image: none;
}

/* Next lines of partners section
* 3 columns per logo => 2 logos per line
*/
.aj-partners > *:nth-child(n + 4) {
grid-column: span 3;
.aj-partner-link::after {
display: none;
}

.aj-partner-logo {
Expand All @@ -696,22 +692,21 @@ textarea {
opacity: 1;
}

.aj-partner-link {
background-image: none;
}

.aj-partner-link::after {
display: none;
}

@media (max-width: 767px) {
/* Consolidated partners styles for mobile */
.aj-partners {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
justify-items: center;
}

.aj-partners > *:nth-child(n) {
grid-column: span 1;
/* Specific fix to satisfy layout requests on mobile if needed, or keep standard grid */
.aj-partner-link {
min-width: auto; /* removing minimum width constraint on small screens */
width: 100%;
display: flex;
justify-content: center;
}

.aj-partner-logo {
Expand Down
Loading