Skip to content

Commit 140c0b9

Browse files
committed
fix: synchroniser l'irrigation par planche
1 parent 96a8435 commit 140c0b9

14 files changed

Lines changed: 757 additions & 93 deletions

File tree

src/app/api/calendrier/route.ts

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NextRequest, NextResponse } from 'next/server'
88
import { requireAuthApi, getUserId } from '@/lib/auth-utils'
99
import prisma from '@/lib/prisma'
1010
import { fetchOpenMeteoForecast, fetchOpenMeteoHistory } from '@/lib/meteo'
11+
import { grouperIrrigationsPlanifieesParPlancheEtJour } from '@/lib/irrigation-planche'
1112

1213
export async function GET(request: NextRequest) {
1314
const { error, session } = await requireAuthApi()
@@ -160,13 +161,12 @@ export async function GET(request: NextRequest) {
160161
const periodeExploitable = start <= horizonForecast
161162

162163
if (periodeExploitable && irrigationsPlanifiees.length > 0) {
163-
let hasDirectCoords = false
164-
for (const irr of irrigationsPlanifiees) {
164+
const manqueCoordonnees = irrigationsPlanifiees.some((irr) => {
165165
const lat = irr.culture.planche?.parcelleGeo?.centroidLat
166166
const lng = irr.culture.planche?.parcelleGeo?.centroidLng
167-
if (lat && lng) { hasDirectCoords = true; break }
168-
}
169-
if (!hasDirectCoords) {
167+
return !lat || !lng
168+
})
169+
if (manqueCoordonnees) {
170170
const userParcelle = await prisma.parcelleGeo.findFirst({
171171
where: { userId },
172172
select: { centroidLat: true, centroidLng: true },
@@ -269,7 +269,33 @@ export async function GET(request: NextRequest) {
269269
})
270270
}
271271

272-
// Formater les événements
272+
const irrigationEvents = grouperIrrigationsPlanifieesParPlancheEtJour(
273+
irrigationsPlanifiees.map(i => {
274+
const meteo = getIrrigationMeteo(i)
275+
return {
276+
id: i.id,
277+
type: 'irrigation' as const,
278+
especeId: i.culture.especeId,
279+
varieteId: i.culture.varieteId || null,
280+
plancheId: i.culture.planche?.nom || null,
281+
plancheName: i.culture.planche?.nom || null,
282+
ilot: i.culture.planche?.ilot || null,
283+
datePrevue: i.datePrevue.toISOString(),
284+
date: i.datePrevue.toISOString(),
285+
fait: i.fait,
286+
couleur: i.culture.espece?.couleur || null,
287+
especeNom: i.culture.espece?.nom ?? i.culture.especeId,
288+
varieteNom: i.culture.variete?.nom ?? i.culture.varieteId ?? null,
289+
cultureId: i.culture.id,
290+
retardJours: 0,
291+
pluiePrevue: meteo.pluiePrevue !== null ? Math.round(meteo.pluiePrevue * 10) / 10 : null,
292+
probablementInutile: meteo.probablementInutile,
293+
}
294+
})
295+
)
296+
297+
// Formater les événements. Les irrigations sont regroupées par planche
298+
// et par jour pour éviter plusieurs alertes pour un même passage.
273299
const events = [
274300
...culturesAvecSemis.map(c => ({
275301
id: c.id,
@@ -310,25 +336,7 @@ export async function GET(request: NextRequest) {
310336
especeNom: c.espece?.nom ?? c.especeId,
311337
varieteNom: c.variete?.nom ?? c.varieteId ?? null,
312338
})),
313-
...irrigationsPlanifiees.map(i => {
314-
const meteo = getIrrigationMeteo(i)
315-
return {
316-
id: i.id,
317-
type: 'irrigation' as const,
318-
especeId: i.culture.especeId,
319-
varieteId: i.culture.varieteId || null,
320-
plancheName: i.culture.planche?.nom || null,
321-
ilot: i.culture.planche?.ilot || null,
322-
date: i.datePrevue.toISOString(),
323-
fait: i.fait,
324-
couleur: i.culture.espece?.couleur || null,
325-
especeNom: i.culture.espece?.nom ?? i.culture.especeId,
326-
varieteNom: i.culture.variete?.nom ?? i.culture.varieteId ?? null,
327-
cultureId: i.culture.id,
328-
pluiePrevue: meteo.pluiePrevue !== null ? Math.round(meteo.pluiePrevue * 10) / 10 : null,
329-
probablementInutile: meteo.probablementInutile,
330-
}
331-
}),
339+
...irrigationEvents,
332340
]
333341

334342
// Trier par date
@@ -340,7 +348,7 @@ export async function GET(request: NextRequest) {
340348
semis: culturesAvecSemis.length,
341349
plantations: culturesAvecPlantation.length,
342350
recoltes: culturesAvecRecolte.length,
343-
irrigations: irrigationsPlanifiees.length,
351+
irrigations: irrigationEvents.length,
344352
total: events.length,
345353
},
346354
})

src/app/api/cultures/irriguer/route.ts

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import { NextRequest, NextResponse } from 'next/server'
1616
import { requireAuthApi, getUserId } from '@/lib/auth-utils'
1717
import prisma from '@/lib/prisma'
18-
import { irrigationCache, irrigationCacheKey } from '@/lib/irrigation-cache'
1918
import { fetchOpenMeteoForecast, fetchOpenMeteoHistory } from '@/lib/meteo'
2019
import type { MeteoJournaliere, MeteoPrevision } from '@/lib/meteo'
2120
import {
@@ -25,6 +24,8 @@ import {
2524
alerteSecheresse,
2625
} from '@/lib/soil-quality'
2726
import { cultureIrrigationDemarreeWhere } from '@/lib/irrigation-eligibility'
27+
import { enregistrerArrosageCultures } from '@/lib/irrigation-recording'
28+
import { surfaceCultureM2 } from '@/lib/culture-surface'
2829

2930
// ── Types internes ──────────────────────────────────────────
3031

@@ -510,9 +511,7 @@ export async function GET(request: NextRequest) {
510511
}
511512

512513
// Surface
513-
const surface = c.nbRangs && c.longueur && c.planche?.largeur
514-
? (c.nbRangs * c.longueur * c.planche.largeur)
515-
: (c.planche?.surface || 0)
514+
const surface = surfaceCultureM2(c)
516515

517516
const besoinEau = c.espece?.besoinEau || 3
518517
const retentionEauSol = c.planche?.retentionEau || null
@@ -600,6 +599,21 @@ export async function GET(request: NextRequest) {
600599
}
601600

602601
// 8. Stats
602+
// Une planche multiculture est arrosée en un seul passage. Additionner
603+
// les estimations de chaque culture ferait compter plusieurs fois la
604+
// même surface ; le besoin le plus exigeant pilote donc la planche.
605+
const consommationParPlanche = new Map<string, number>()
606+
for (const culture of culturesAvecUrgence) {
607+
const key = culture.plancheId ?? `culture:${culture.id}`
608+
consommationParPlanche.set(
609+
key,
610+
Math.max(
611+
consommationParPlanche.get(key) ?? 0,
612+
culture.consommationEauSemaine
613+
)
614+
)
615+
}
616+
603617
const stats = {
604618
total: cultures.length,
605619
nbIlots: Object.keys(parIlot).length,
@@ -609,7 +623,9 @@ export async function GET(request: NextRequest) {
609623
jamaisArrose: culturesAvecUrgence.filter(c => c.joursSansEau === null).length,
610624
prochainesIrrigations7j: culturesAvecUrgence.reduce((s, c) => s + c.prochainesIrrigations, 0),
611625
irrigationsAutoValidees: irrigationsAutoValidees.length,
612-
consommationTotaleEstimee: Math.round(culturesAvecUrgence.reduce((s, c) => s + c.consommationEauSemaine, 0)),
626+
consommationTotaleEstimee: Math.round(
627+
Array.from(consommationParPlanche.values()).reduce((s, litres) => s + litres, 0)
628+
),
613629
parTypeIrrigation: Object.entries(parTypeIrrigation).map(([type, cultures]) => ({
614630
type,
615631
count: cultures.length,
@@ -655,18 +671,18 @@ export async function PATCH(request: NextRequest) {
655671
const body = await request.json()
656672
const { cultureId, cultureIds, aIrriguer, marquerArrosage } = body
657673

658-
// Arrosage multiple (groupe par îlot)
674+
// Arrosage multiple (groupe par îlot). Chaque culture demandée étend la
675+
// mise à jour à ses colocataires actifs sur la même planche.
659676
if (marquerArrosage && cultureIds && Array.isArray(cultureIds)) {
660-
const now = new Date()
661-
await prisma.culture.updateMany({
662-
where: {
663-
id: { in: cultureIds },
664-
userId,
665-
},
666-
data: { derniereIrrigation: now },
677+
const result = await enregistrerArrosageCultures({
678+
userId,
679+
cultureIds,
680+
})
681+
return NextResponse.json({
682+
success: true,
683+
...result,
684+
date: result.dateEffective,
667685
})
668-
irrigationCache.invalidateUser(userId)
669-
return NextResponse.json({ success: true, date: now })
670686
}
671687

672688
// Arrosage simple ou toggle aIrriguer
@@ -677,32 +693,39 @@ export async function PATCH(request: NextRequest) {
677693
)
678694
}
679695

680-
// Vérifier que la culture appartient à l'utilisateur
696+
// Noter l'arrosage
697+
if (marquerArrosage) {
698+
const result = await enregistrerArrosageCultures({
699+
userId,
700+
cultureIds: [cultureId],
701+
})
702+
if (result.culturesMisesAJour === 0) {
703+
return NextResponse.json(
704+
{ error: 'Culture non trouvée' },
705+
{ status: 404 }
706+
)
707+
}
708+
return NextResponse.json({
709+
success: true,
710+
...result,
711+
date: result.dateEffective,
712+
})
713+
}
714+
715+
// Toggle aIrriguer
681716
const culture = await prisma.culture.findFirst({
682717
where: { id: cultureId, userId },
718+
select: { id: true },
683719
})
684-
685720
if (!culture) {
686721
return NextResponse.json(
687722
{ error: 'Culture non trouvée' },
688723
{ status: 404 }
689724
)
690725
}
691726

692-
// Noter l'arrosage
693-
if (marquerArrosage) {
694-
const now = new Date()
695-
const updated = await prisma.culture.update({
696-
where: { id: cultureId },
697-
data: { derniereIrrigation: now },
698-
})
699-
irrigationCache.invalidateUser(userId)
700-
return NextResponse.json({ success: true, data: updated, date: now })
701-
}
702-
703-
// Toggle aIrriguer
704727
const updated = await prisma.culture.update({
705-
where: { id: cultureId },
728+
where: { id: culture.id },
706729
data: { aIrriguer },
707730
})
708731

src/app/api/irrigations/[id]/route.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import { NextRequest, NextResponse } from 'next/server'
99
import prisma from '@/lib/prisma'
1010
import { requireAuthApi, getUserId } from '@/lib/auth-utils'
11+
import { enregistrerArrosageCultures } from '@/lib/irrigation-recording'
12+
import type { Prisma } from '@prisma/client'
1113

1214
// GET /api/irrigations/[id]
1315
export async function GET(
@@ -67,6 +69,11 @@ export async function PATCH(
6769
// Vérifier que l'irrigation appartient à l'utilisateur
6870
const existing = await prisma.irrigationPlanifiee.findFirst({
6971
where: { id, userId },
72+
include: {
73+
culture: {
74+
select: { plancheId: true },
75+
},
76+
},
7077
})
7178

7279
if (!existing) {
@@ -78,7 +85,7 @@ export async function PATCH(
7885

7986
const { fait, dateEffective, datePrevue, notes } = body
8087

81-
const updateData: any = {}
88+
const updateData: Prisma.IrrigationPlanifieeUpdateManyMutationInput = {}
8289

8390
if (typeof fait === 'boolean') {
8491
updateData.fait = fait
@@ -100,9 +107,39 @@ export async function PATCH(
100107
updateData.notes = notes
101108
}
102109

103-
const irrigation = await prisma.irrigationPlanifiee.update({
104-
where: { id },
110+
// Une ligne du calendrier représente désormais un passage par planche et
111+
// par jour. Répercuter la modification sur les anciennes lignes
112+
// culture-par-culture évite qu'un doublon réapparaisse après un déplacement
113+
// ou une validation.
114+
const debutJour = new Date(existing.datePrevue)
115+
debutJour.setHours(0, 0, 0, 0)
116+
const finJour = new Date(existing.datePrevue)
117+
finJour.setHours(23, 59, 59, 999)
118+
119+
const irrigationsDuPassage = existing.culture.plancheId
120+
? await prisma.irrigationPlanifiee.findMany({
121+
where: {
122+
userId,
123+
datePrevue: { gte: debutJour, lte: finJour },
124+
culture: {
125+
plancheId: existing.culture.plancheId,
126+
},
127+
},
128+
select: { id: true },
129+
})
130+
: [{ id }]
131+
const irrigationIds = irrigationsDuPassage.map((item) => item.id)
132+
133+
await prisma.irrigationPlanifiee.updateMany({
134+
where: {
135+
userId,
136+
id: { in: irrigationIds },
137+
},
105138
data: updateData,
139+
})
140+
141+
const irrigation = await prisma.irrigationPlanifiee.findUniqueOrThrow({
142+
where: { id },
106143
include: {
107144
culture: {
108145
include: {
@@ -112,7 +149,22 @@ export async function PATCH(
112149
},
113150
})
114151

115-
return NextResponse.json(irrigation)
152+
// Cocher une irrigation planifiée signifie qu'un passage réel a eu lieu.
153+
// On réconcilie donc aussi la date du dernier arrosage, les éventuelles
154+
// cultures voisines sur la même planche et les tâches dues en doublon.
155+
const impactArrosage = fait === true
156+
? await enregistrerArrosageCultures({
157+
userId,
158+
cultureIds: [existing.cultureId],
159+
dateEffective: irrigation.dateEffective ?? new Date(),
160+
})
161+
: null
162+
163+
return NextResponse.json({
164+
...irrigation,
165+
irrigationIds,
166+
impactArrosage,
167+
})
116168
} catch (error) {
117169
console.error('PATCH /api/irrigations/[id] error:', error)
118170
return NextResponse.json(

src/app/api/meteo/irrigation/route.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { fetchEcowittData, fetchOpenMeteoForecast, fetchOpenMeteoHistory, fetchW
1414
import { adapterConseilSousAbri, genererRecommandationIrrigation } from '@/lib/meteo-agro'
1515
import { irrigationCache, irrigationCacheKey } from '@/lib/irrigation-cache'
1616
import { cultureIrrigationDemarreeWhere } from '@/lib/irrigation-eligibility'
17+
import { grouperRecommandationsParPlanche } from '@/lib/irrigation-planche'
1718
import type { MeteoJournaliere, MeteoPrevision } from '@/lib/meteo'
1819
import type { Prisma } from '@prisma/client'
1920

@@ -78,6 +79,7 @@ export async function GET(request: NextRequest) {
7879
variete: { select: { nom: true } },
7980
planche: {
8081
select: {
82+
id: true,
8183
nom: true,
8284
type: true,
8385
irrigation: true,
@@ -133,7 +135,7 @@ export async function GET(request: NextRequest) {
133135
parCoords.get(key)!.cultures.push(culture)
134136
}
135137

136-
const recommandations = []
138+
const recommandationsCultures = []
137139

138140
for (const [, group] of parCoords) {
139141
let historique7j: MeteoJournaliere[]
@@ -203,8 +205,9 @@ export async function GET(request: NextRequest) {
203205
reco.joursSansPluie = 0
204206
}
205207

206-
recommandations.push({
208+
recommandationsCultures.push({
207209
...reco,
210+
plancheId: culture.planche!.id,
208211
varietyName: culture.variete?.nom ?? null,
209212
etatCulture: culture.plantationFaite ? 'Plantée' : 'Semée',
210213
derniereIrrigation: culture.derniereIrrigation?.toISOString() ?? null,
@@ -213,6 +216,11 @@ export async function GET(request: NextRequest) {
213216
}
214217
}
215218

219+
// Une action d'arrosage concerne la planche entière. Sur une planche
220+
// multiculture, une seule alerte est affichée et le besoin le plus
221+
// exigeant pilote la recommandation.
222+
const recommandations = grouperRecommandationsParPlanche(recommandationsCultures)
223+
216224
// Trier par urgence
217225
const ordreUrgence = { critique: 0, haute: 1, moyenne: 2, faible: 3, aucune: 4 }
218226
recommandations.sort((a, b) =>
@@ -222,6 +230,7 @@ export async function GET(request: NextRequest) {
222230
const result = {
223231
recommandations,
224232
total: recommandations.length,
233+
totalCultures: recommandationsCultures.length,
225234
urgentes: recommandations.filter(r => r.urgence === 'critique' || r.urgence === 'haute').length,
226235
}
227236

0 commit comments

Comments
 (0)