Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cf17e87
:sparkles: ajout de la gestion des organisations et services dans la …
ConchezRob Mar 28, 2026
26791b7
:sparkles: amélioration de la gestion des erreurs d'image dans Organi…
ConchezRob Mar 28, 2026
05bef16
:sparkles: ajout de la gestion des requêtes pour les organisations et…
ConchezRob Mar 28, 2026
759131c
:sparkles: ajout de la gestion des services dans les requêtes de cons…
ConchezRob Mar 28, 2026
04b7d56
:sparkles: ajout de la gestion de l'identifiant de service dans les c…
ConchezRob Mar 28, 2026
145bdc4
:sparkles: ajout de la gestion de l'identifiant de service dans les m…
ConchezRob Mar 28, 2026
e2737b2
:sparkles: remplacement des mises à jour d'utilisateur par des insert…
ConchezRob Mar 28, 2026
1ed83d5
:sparkles: ajout de la gestion des dépendances peer dans le fichier p…
ConchezRob Mar 28, 2026
7177693
:sparkles: selection des organisations même si pas admin
ConchezRob Mar 28, 2026
782418c
:bug: fix erreurs sur les equivalents
ConchezRob Mar 28, 2026
98df02e
:sparkles: affichage du nom du service si juste membre
ConchezRob Mar 28, 2026
410e024
:mute: suppression d'un log
ConchezRob Mar 28, 2026
102fe4c
:sparkles: ajout de la gestion des organisations et services dans la …
ConchezRob Mar 28, 2026
359c33e
:sparkles: amélioration de la gestion des erreurs d'image dans Organi…
ConchezRob Mar 28, 2026
448761d
:sparkles: ajout de la gestion des requêtes pour les organisations et…
ConchezRob Mar 28, 2026
83ce95f
:sparkles: ajout de la gestion des services dans les requêtes de cons…
ConchezRob Mar 28, 2026
b1d0e02
:sparkles: ajout de la gestion de l'identifiant de service dans les c…
ConchezRob Mar 28, 2026
83269dd
:sparkles: ajout de la gestion de l'identifiant de service dans les m…
ConchezRob Mar 28, 2026
b1418ae
:sparkles: remplacement des mises à jour d'utilisateur par des insert…
ConchezRob Mar 28, 2026
7b6c253
:sparkles: ajout de la gestion des dépendances peer dans le fichier p…
ConchezRob Mar 28, 2026
1f4de8c
:sparkles: selection des organisations même si pas admin
ConchezRob Mar 28, 2026
a4ea439
:bug: fix erreurs sur les equivalents
ConchezRob Mar 28, 2026
ca34b87
:sparkles: affichage du nom du service si juste membre
ConchezRob Mar 28, 2026
99616d1
:mute: suppression d'un log
ConchezRob Mar 28, 2026
b874ef7
Merge branch 'GS-115-Refaire-la-page-mon-organisation' of https://git…
ConchezRob Mar 28, 2026
f994848
:mute: suppression d'un log
ConchezRob Mar 28, 2026
7c11285
:white_check_mark: fix test
ConchezRob Mar 28, 2026
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
51 changes: 43 additions & 8 deletions backend/src/controllers/mo_controller.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
use axum::extract::Query;
use serde::Deserialize;
#[derive(Deserialize, Default)]
#[serde(default)]
pub struct MoQuery {
pub org_id: Option<i64>,
pub service_id: Option<i64>,
}
use crate::dto::top_polluting_site_dto::TopPollutingSite;
use crate::green_score::calculate_green_score;
use crate::service::advice_service::AdviceService;
use crate::service::equivalent_service::EquivalentService;
use crate::service::organisation_service::OrganisationService;
use crate::service::service_service::ServiceService;
use crate::models::equivalent::Equivalent;
use axum::extract::State;
use axum::Json;
Expand All @@ -25,17 +34,33 @@ pub struct MyOrganizationResponse {
pub weekly_consumption: Vec<ConsumptionDataPoint>,
pub monthly_consumption: Vec<ConsumptionDataPoint>,
pub top_polluting_sites: Vec<TopPollutingSite>,
pub is_admin: bool,
pub services: Option<Vec<crate::models::service::Service>>,
}


pub async fn mo(State(pool): State<MySqlPool>, AuthenticatedUser(user_full): AuthenticatedUser) -> Result<Json<MyOrganizationResponse>, AppError> {
pub async fn mo(
State(pool): State<MySqlPool>,
Query(query): Query<MoQuery>,
AuthenticatedUser(user_full): AuthenticatedUser
) -> Result<Json<MyOrganizationResponse>, AppError> {

let organization = user_full.organisation.first().ok_or(AppError::NotFound("User is not in an organization".to_string()))?;
let organization = if let Some(oid) = query.org_id {
user_full.organisation.iter().find(|o| o.id == oid).cloned().ok_or(AppError::NotFound("User is not in the specified organization".to_string()))?
} else {
user_full.organisation.first().cloned().ok_or(AppError::NotFound("User is not in an organization".to_string()))?
};

let organization_id = organization.id;
let user_id = user_full.user.id;

let organization_informations = OrganisationService::organization_informations(&pool, organization_id, user_id).await
let service_id = if organization.est_admin {
query.service_id
} else {
user_full.service.map(|s| s.id)
};

let organization_informations = OrganisationService::organization_informations(&pool, organization_id, user_id, service_id).await
.map_err(AppError::from)?;

let advices: Vec<String> = vec![
Expand All @@ -48,15 +73,23 @@ pub async fn mo(State(pool): State<MySqlPool>, AuthenticatedUser(user_full): Aut
let equivalents = EquivalentService::equivalent(&pool, Some(user_id), 2, organization_informations.total_consumption).await
.ok();

let daily_consumption = OrganisationService::get_daily_organization_consumption(&pool, organization_id).await
let daily_consumption = OrganisationService::get_daily_organization_consumption(&pool, organization_id, service_id).await
.map_err(AppError::from)?;
let weekly_consumption = OrganisationService::get_weekly_organization_consumption(&pool, organization_id).await
let weekly_consumption = OrganisationService::get_weekly_organization_consumption(&pool, organization_id, service_id).await
.map_err(AppError::from)?;
let monthly_consumption = OrganisationService::get_monthly_organization_consumption(&pool, organization_id).await
let monthly_consumption = OrganisationService::get_monthly_organization_consumption(&pool, organization_id, service_id).await
.map_err(AppError::from)?;
let top_polluting_sites = OrganisationService::get_top5_polluting_sites_by_organization(&pool, organization_id).await
let top_polluting_sites = OrganisationService::get_top5_polluting_sites_by_organization(&pool, organization_id, service_id).await
.map_err(AppError::from)?;

let is_admin = organization.est_admin;

let services = if is_admin {
ServiceService::get_organisation_services(&pool, organization_id).await.ok()
} else {
None
};

Ok(Json(MyOrganizationResponse {
success: true,
mo_infos: Some(organization_informations),
Expand All @@ -67,6 +100,8 @@ pub async fn mo(State(pool): State<MySqlPool>, AuthenticatedUser(user_full): Aut
daily_consumption,
weekly_consumption,
monthly_consumption,
top_polluting_sites
top_polluting_sites,
is_admin,
services,
}))
}
138 changes: 96 additions & 42 deletions backend/src/repository/monitored_website_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,32 @@ impl MonitoredWebsiteRepository {

pub async fn get_top5_polluting_sites_by_organization(
pool: &MySqlPool,
org_id: i64
org_id: i64, service_id: Option<i64>
) -> Result<Vec<TopPollutingSite>, Error> {
let results = sqlx::query_as::<_, TopPollutingSite>(
let mut query = sqlx::QueryBuilder::new(
"SELECT
mw.url_domain,
SUM(mw.carbon_footprint) as total_footprint
FROM monitored_website mw
JOIN user u
ON u.id = mw.user_id
WHERE u.organisation_id = ?
AND mw.url_domain IS NOT NULL
JOIN organisation_user ou
ON ou.user_id = u.id
WHERE ou.organisation_id = "
);
query.push_bind(org_id);

if let Some(sid) = service_id {
query.push(" AND u.service_id = ");
query.push_bind(sid);
}

query.push(" AND mw.url_domain IS NOT NULL
GROUP BY mw.url_domain
ORDER BY total_footprint
DESC LIMIT 5"
)
.bind(org_id)
DESC LIMIT 5");

let results = query.build_query_as::<TopPollutingSite>()
.fetch_all(pool)
.await?;
Ok(results)
Expand Down Expand Up @@ -96,19 +106,27 @@ impl MonitoredWebsiteRepository {

pub async fn average_daily_carbon_footprint_for_organization(
pool: &MySqlPool,
org_id: i64
org_id: i64, service_id: Option<i64>
) -> f64 {
sqlx::query_as::<_, (f64,)>(
let mut query = sqlx::QueryBuilder::new(
"SELECT ROUND(
COALESCE(
SUM(mw.carbon_footprint) / NULLIF(DATEDIFF(CURDATE(), MIN(DATE(mw.creation_date))) + 1, 0)
, 0)
, 2) AS average_daily_carbon_footprint
FROM monitored_website mw
JOIN user u ON u.id = mw.user_id
WHERE u.organisation_id = ?"
)
.bind(org_id)
JOIN organisation_user ou ON ou.user_id = u.id
WHERE ou.organisation_id = "
);
query.push_bind(org_id);

if let Some(sid) = service_id {
query.push(" AND u.service_id = ");
query.push_bind(sid);
}

query.build_query_as::<(f64,)>()
.fetch_one(pool)
.await
.map(|(val,)| val)
Expand All @@ -117,16 +135,25 @@ impl MonitoredWebsiteRepository {

pub async fn total_organization_consumption(
pool: &MySqlPool,
org_id: i64
org_id: i64, service_id: Option<i64>
) -> Result<Option<f64>, Error> {
sqlx::query_as::<_, (Option<f64>,)>(
"SELECT SUM(mw.carbon_footprint) as total_consumption
FROM monitored_website mw
JOIN user u
ON mw.user_id = u.id
WHERE u.organisation_id = ?",
)
.bind(org_id)
let mut query = sqlx::QueryBuilder::new(
"SELECT SUM(mw.carbon_footprint) as total_consumption
FROM monitored_website mw
JOIN user u
ON mw.user_id = u.id
JOIN organisation_user ou
ON ou.user_id = u.id
WHERE ou.organisation_id = "
);
query.push_bind(org_id);

if let Some(sid) = service_id {
query.push(" AND u.service_id = ");
query.push_bind(sid);
}

query.build_query_as::<(Option<f64>,)>()
.fetch_one(pool)
.await
.map(|(val,)| val)
Expand Down Expand Up @@ -237,57 +264,84 @@ impl MonitoredWebsiteRepository {
Ok(result)
}

pub async fn get_daily_organization_consumption(pool: &MySqlPool, orga_id: i64) -> Result<Vec<ConsumptionDataPoint>, Error>
pub async fn get_daily_organization_consumption(pool: &MySqlPool, orga_id: i64, service_id: Option<i64>) -> Result<Vec<ConsumptionDataPoint>, Error>
{
let result = sqlx::query_as::<_, ConsumptionDataPoint>(
let mut query = sqlx::QueryBuilder::new(
"SELECT
DATE_FORMAT(mw.creation_date, '%d/%m') as label,
SUM(mw.carbon_footprint) as value
FROM monitored_website mw
JOIN user u ON u.id = mw.user_id
WHERE u.organisation_id = ?
AND mw.creation_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
JOIN organisation_user ou ON ou.user_id = u.id
WHERE ou.organisation_id = "
);
query.push_bind(orga_id);

if let Some(sid) = service_id {
query.push(" AND u.service_id = ");
query.push_bind(sid);
}

query.push(" AND mw.creation_date >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(mw.creation_date), label
ORDER BY DATE(mw.creation_date) ASC"
)
.bind(orga_id)
ORDER BY DATE(mw.creation_date) ASC");

let result = query.build_query_as::<ConsumptionDataPoint>()
.fetch_all(pool)
.await?;
Ok(result)
}

pub async fn get_weekly_organization_consumption(pool: &MySqlPool, orga_id: i64) -> Result<Vec<ConsumptionDataPoint>, Error>
pub async fn get_weekly_organization_consumption(pool: &MySqlPool, orga_id: i64, service_id: Option<i64>) -> Result<Vec<ConsumptionDataPoint>, Error>
{
let result = sqlx::query_as::<_, ConsumptionDataPoint>(
let mut query = sqlx::QueryBuilder::new(
"SELECT CONCAT('Semaine ', WEEK(mw.creation_date, 1)) as label,
SUM(mw.carbon_footprint) as value
FROM monitored_website mw
JOIN user u ON u.id = mw.user_id
WHERE u.organisation_id = ?
AND mw.creation_date >= DATE_SUB(NOW(), INTERVAL 4 WEEK)
JOIN organisation_user ou ON ou.user_id = u.id
WHERE ou.organisation_id = "
);
query.push_bind(orga_id);

if let Some(sid) = service_id {
query.push(" AND u.service_id = ");
query.push_bind(sid);
}

query.push(" AND mw.creation_date >= DATE_SUB(NOW(), INTERVAL 4 WEEK)
GROUP BY WEEK(mw.creation_date, 1), label
ORDER BY WEEK(mw.creation_date, 1) ASC"
)
.bind(orga_id)
ORDER BY WEEK(mw.creation_date, 1) ASC");

let result = query.build_query_as::<ConsumptionDataPoint>()
.fetch_all(pool)
.await?;

Ok(result)
}

pub async fn get_monthly_organization_consumption(pool: &MySqlPool, org_id: i64) -> Result<Vec<ConsumptionDataPoint>, Error>
pub async fn get_monthly_organization_consumption(pool: &MySqlPool, org_id: i64, service_id: Option<i64>) -> Result<Vec<ConsumptionDataPoint>, Error>
{
let result = sqlx::query_as::<_, ConsumptionDataPoint>(
let mut query = sqlx::QueryBuilder::new(
"SELECT DATE_FORMAT(mw.creation_date, '%m/%Y') as label,
SUM(mw.carbon_footprint) as value
FROM monitored_website mw
JOIN user u ON u.id = mw.user_id
WHERE u.organisation_id = ?
AND mw.creation_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
JOIN organisation_user ou ON ou.user_id = u.id
WHERE ou.organisation_id = "
);
query.push_bind(org_id);

if let Some(sid) = service_id {
query.push(" AND u.service_id = ");
query.push_bind(sid);
}

query.push(" AND mw.creation_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY MONTH(mw.creation_date), YEAR(mw.creation_date), label
ORDER BY YEAR(mw.creation_date), MONTH(mw.creation_date) ASC"
)
.bind(org_id)
ORDER BY YEAR(mw.creation_date), MONTH(mw.creation_date) ASC");

let result = query.build_query_as::<ConsumptionDataPoint>()
.fetch_all(pool)
.await?;
Ok(result)
Expand Down
14 changes: 6 additions & 8 deletions backend/src/service/monitored_website_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,28 +117,26 @@ impl MonitoredWebsiteService {
.collect())
}

pub async fn average_daily_carbon_footprint_for_organization(pool: &MySqlPool, org_id: i64) -> f64 {
let avg = MonitoredWebsiteRepository::average_daily_carbon_footprint_for_organization(pool, org_id).await;
(avg * 100.0).round() / 100.0
pub async fn average_daily_carbon_footprint_for_organization(pool: &MySqlPool, org_id: i64, service_id: Option<i64>) -> f64 {
MonitoredWebsiteRepository::average_daily_carbon_footprint_for_organization(pool, org_id, service_id).await
}

pub async fn get_daily_consumption_by_user(
pool: &MySqlPool,
user_id: i64
) -> Result<Vec<ConsumptionDataPoint>, Error> {
let daily_consumption: Vec<ConsumptionDataPoint> = MonitoredWebsiteRepository::get_daily_consumption_by_user(pool, user_id).await?;
let daily_consumtion: Vec<ConsumptionDataPoint> = MonitoredWebsiteRepository::get_daily_consumption_by_user(pool, user_id).await?;

Ok(daily_consumption.into_iter()
Ok(daily_consumtion.into_iter()
.map(|consumption_data_point: ConsumptionDataPoint| ConsumptionDataPoint {
label: consumption_data_point.label,
value: (consumption_data_point.value * 100.0).round() / 100.0
})
.collect())
}

pub async fn total_organization_consumption(pool: &MySqlPool, org_id: i64) -> Result<Option<f64>, Error> {
let total = MonitoredWebsiteRepository::total_organization_consumption(pool, org_id).await?;
Ok(total.map(|t| (t * 100.0).round() / 100.0))
pub async fn total_organization_consumption(pool: &MySqlPool, org_id: i64, service_id: Option<i64>) -> Result<Option<f64>, Error> {
MonitoredWebsiteRepository::total_organization_consumption(pool, org_id, service_id).await
}

pub async fn get_weekly_consumption_by_user(
Expand Down
22 changes: 11 additions & 11 deletions backend/src/service/organisation_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ impl OrganisationService {
pub async fn find_id_by_siret(pool: &MySqlPool, siret: String) -> Result<Option<i64>, Error> {
OrganisationRepository::find_id_by_siret(pool, &siret).await
}
pub async fn organization_informations(pool: &MySqlPool, orga_id: i64, user_id: i64) -> Result<MyOrganizationInfos, Error>
pub async fn organization_informations(pool: &MySqlPool, orga_id: i64, user_id: i64, service_id: Option<i64>) -> Result<MyOrganizationInfos, Error>
{
let name: String = OrganisationRepository::organization_name(pool, orga_id).await.unwrap_or(None).unwrap_or_else(|| "Organisation inconnue".to_string());
let average_daily_carbon_footprint: f64 = MonitoredWebsiteService::average_daily_carbon_footprint_for_organization(pool, orga_id).await;
let total_consumption: f64 = MonitoredWebsiteService::total_organization_consumption(pool, orga_id).await.unwrap_or(None).unwrap_or(0.0);
let average_daily_carbon_footprint: f64 = MonitoredWebsiteService::average_daily_carbon_footprint_for_organization(pool, orga_id, service_id).await;
let total_consumption: f64 = MonitoredWebsiteService::total_organization_consumption(pool, orga_id, service_id).await.unwrap_or(None).unwrap_or(0.0);

let equivalent = EquivalentService::equivalent(pool, Some(user_id), 1, total_consumption).await.ok().and_then(|mut v| v.pop());

Expand All @@ -35,20 +35,20 @@ impl OrganisationService {
})
}

pub async fn get_daily_organization_consumption(pool: &MySqlPool, orga_id: i64) -> Result<Vec<ConsumptionDataPoint>, Error> {
MonitoredWebsiteRepository::get_daily_organization_consumption(pool, orga_id).await
pub async fn get_daily_organization_consumption(pool: &MySqlPool, orga_id: i64, service_id: Option<i64>) -> Result<Vec<ConsumptionDataPoint>, Error> {
MonitoredWebsiteRepository::get_daily_organization_consumption(pool, orga_id, service_id).await
}

pub async fn get_weekly_organization_consumption(pool: &MySqlPool, orga_id: i64) -> Result<Vec<ConsumptionDataPoint>, Error> {
MonitoredWebsiteRepository::get_weekly_organization_consumption(pool, orga_id).await
pub async fn get_weekly_organization_consumption(pool: &MySqlPool, orga_id: i64, service_id: Option<i64>) -> Result<Vec<ConsumptionDataPoint>, Error> {
MonitoredWebsiteRepository::get_weekly_organization_consumption(pool, orga_id, service_id).await
}

pub async fn get_monthly_organization_consumption(pool: &MySqlPool, orga_id: i64) -> Result<Vec<ConsumptionDataPoint>, Error> {
MonitoredWebsiteRepository::get_monthly_organization_consumption(pool, orga_id).await
pub async fn get_monthly_organization_consumption(pool: &MySqlPool, orga_id: i64, service_id: Option<i64>) -> Result<Vec<ConsumptionDataPoint>, Error> {
MonitoredWebsiteRepository::get_monthly_organization_consumption(pool, orga_id, service_id).await
}

pub async fn get_top5_polluting_sites_by_organization(pool: &MySqlPool, org_id: i64) -> Result<Vec<TopPollutingSite>, Error> {
let top_polluting_sites: Vec<TopPollutingSite> = MonitoredWebsiteRepository::get_top5_polluting_sites_by_organization(pool, org_id).await?;
pub async fn get_top5_polluting_sites_by_organization(pool: &MySqlPool, org_id: i64, service_id: Option<i64>) -> Result<Vec<TopPollutingSite>, Error> {
let top_polluting_sites: Vec<TopPollutingSite> = MonitoredWebsiteRepository::get_top5_polluting_sites_by_organization(pool, org_id, service_id).await?;

Ok(top_polluting_sites.into_iter()
.map(|top_polluting_site: TopPollutingSite| TopPollutingSite {
Expand Down
Loading
Loading