maj via codex

This commit is contained in:
2026-02-22 18:34:50 +01:00
parent 20af00d653
commit 55387f4b0e
90 changed files with 9902 additions and 1251 deletions

View File

@@ -9,3 +9,5 @@ REDIS_URL=redis://redis:6379
STATION_URL=http://10.0.0.8:8081/ STATION_URL=http://10.0.0.8:8081/
METEO_LAT=45.14 METEO_LAT=45.14
METEO_LON=4.12 METEO_LON=4.12
ENABLE_SCHEDULER=1
ENABLE_BOOTSTRAP=1

View File

@@ -99,7 +99,10 @@ cp data/jardin.db data/jardin_backup_$(date +%Y%m%d).db
## API ## API
Documentation interactive disponible sur http://localhost:8000/docs (Swagger UI). Documentation interactive disponible sur http://localhost:8060/docs (Swagger UI).
Guide reseau local / VM / OpenClaw:
- `docs/api_utilisation_reseau_local_openclaw.md`
Endpoints principaux : Endpoints principaux :

View File

@@ -6,6 +6,8 @@ jardin :
- [ ] ajouter les caracteristiques pour un jardin: photo, geolocalisation, type de terre, ph, ensoleillement, exposition, dimension,surface, ... - [ ] ajouter les caracteristiques pour un jardin: photo, geolocalisation, type de terre, ph, ensoleillement, exposition, dimension,surface, ...
- -
- [ ] dans l'edition du jardin definir si carré potager avec dimension x;y en cm
plante : plante :
- [ ] header : varietés => remplacer par plante ( pareil dans tous le programme) - [ ] header : varietés => remplacer par plante ( pareil dans tous le programme)
- [ ] pour une plante, ajouter des caracteristiques : photo, nom, varités, famille, resistance au froid , maladie commune et astuces , methode de semis et de plantation, ... (brainstorming) - [ ] pour une plante, ajouter des caracteristiques : photo, nom, varités, famille, resistance au froid , maladie commune et astuces , methode de semis et de plantation, ... (brainstorming)
@@ -77,4 +79,6 @@ bibliotehque photo:
- brainstorming local ai detection style yolo ( fichier consigne_yolo.md) - brainstorming local ai detection style yolo ( fichier consigne_yolo.md)
backend : backend :
- [ ] methode simple pour mettre a jours la base de donnée ; brainstorming - [ ] methode simple pour mettre a jours la base de donnée ; brainstorming
- [ ] mise a jours bdd via api puis je peut ajouter des script dans mon openclaw]

View File

@@ -7282,3 +7282,178 @@ naive tzinfo: None
aware tzinfo: UTC aware tzinfo: UTC
You've hit your limit · resets 7pm (Europe/Paris) You've hit your limit · resets 7pm (Europe/Paris)
## Mise a jour Codex - 2026-02-22
### Termine
- Task 5 (Service Open-Meteo enrichi):
- `backend/app/services/meteo.py`
- ajout aggregation journaliere `sol_0cm` depuis `hourly.soil_temperature_0cm`
- parsing defensif des valeurs numeriques
- tests: `backend/tests/test_meteo_service.py` (3 passes)
- Task 8 (Router Astuces filtres):
- `backend/app/routers/astuces.py`
- nouveaux filtres `categorie`, `tag`, `mois`
- compatibilite filtres existants `entity_type`, `entity_id`
- tests: `backend/tests/test_astuces_filters.py` (5 passes)
### Stabilisation tests
- Ajout de flags de runtime backend:
- `ENABLE_SCHEDULER` et `ENABLE_BOOTSTRAP` dans `backend/app/config.py`
- documentes dans `.env.example`
- `backend/app/main.py` respecte ces flags dans le lifespan
- `backend/tests/conftest.py` desactive scheduler/bootstrap pour les tests
- `conftest` fournit une session SQLModel par requete TestClient pour eviter les blocages thread/session
### Point restant
- `backend/tests/test_meteo.py::test_meteo_tableau_vide` reste bloquant dans ce contexte (timeout), malgre la desactivation scheduler/bootstrap.
- Les nouveaux tests unitaire meteo/astuces passent.
## Mise a jour Codex - Frontend (Tasks 9, 10, 11)
### Task 9 termine
- `frontend/src/api/meteo.ts` enrichi:
- `getTableau`, `getStationCurrent`, `getPrevisions`, `refresh`
- types `TableauRow`, `StationCurrent`, `OpenMeteoDay`
- `frontend/src/api/astuces.ts` cree
- `frontend/src/stores/astuces.ts` cree
### Task 10 termine
- `frontend/src/views/CalendrierView.vue`:
- onglet meteo refondu en tableau synthetique station + open-meteo
- widget station actuelle
- suppression ancien bloc `meteoData`
- ajout `loadTableau` + `loadStationCurrent`
### Task 11 termine
- `frontend/src/views/AstucesView.vue` cree (filtres + CRUD)
- route ajoutee: `/astuces` dans `frontend/src/router/index.ts`
- menu mobile: `frontend/src/components/AppDrawer.vue`
- menu desktop: `frontend/src/App.vue`
### Validation frontend
- `npm run lint` -> OK
- `npm run build` -> OK
## Mise a jour Codex - 2026-02-22 (Meteo, Jardin, UI)
### Migration executee (OK)
- Migration lancee dans le conteneur backend:
- `docker compose exec backend python -c "from app.migrate import run_migrations; run_migrations()"`
- Colonnes ajoutees en base SQLite:
- table `garden`: `carre_potager`, `carre_x_cm`, `carre_y_cm`
- table `astuce`: `photos`, `videos`
### Jardin: carre potager
- Backend:
- `backend/app/models/garden.py` ajoute les champs `carre_potager`, `carre_x_cm`, `carre_y_cm`
- `backend/app/migrate.py` mis a jour pour ces colonnes
- Frontend:
- `frontend/src/views/JardinsView.vue`
- ajout checkbox "Carre potager" + dimensions X/Y en cm
- conversion automatique cm -> m pour `longueur_m` / `largeur_m`
- surface calculee automatiquement si absente
### Popup edition plante responsive
- `frontend/src/views/PlantesView.vue`
- modal edition:
- smartphone: 1 colonne
- laptop/desktop: 2 colonnes (`lg:grid-cols-2`)
- notes + actions en largeur complete
### Meteo: vue unique + navigation temporelle
- Backend:
- `backend/app/routers/meteo.py`
- endpoint `/api/meteo/tableau` accepte desormais:
- `center_date=YYYY-MM-DD`
- `span` (jours avant/apres)
- Frontend:
- `frontend/src/views/CalendrierView.vue` refondu en vue meteo unique
- suppression des onglets `lunaire/meteo/taches/dictons`
- boutons navigation: `Prev`, `Today`, `Next`
- fenetre active sur +/- 15 jours autour de la date centrale
- detail a droite conserve (station, open-meteo, lunaire, dictons, saint)
### Navigation application
- Route principale renommee:
- `/meteo` -> `CalendrierView`
- Redirections conservees:
- `/calendrier` -> `/meteo`
- `/lunaire` -> `/meteo`
- Menus renommes en "Meteo":
- `frontend/src/App.vue` / `frontend/src/App.vue.js`
- `frontend/src/components/AppDrawer.vue` / `frontend/src/components/AppDrawer.vue.js`
### Validation
- Backend: compilation Python OK sur fichiers modifies
- Frontend: build OK (`npm --prefix frontend run build`)
## Mise a jour Codex - 2026-02-22 (Station/Open-Meteo, Dashboard, Outils video)
### Base de donnees meteo: mises a jour executees
- Script station locale execute avec succes:
- `python3 station_meteo/update_station_db.py`
- ecriture confirmee en base (`meteostation`):
- `current`: `2026-02-22T17:00` (pression `922.5`, vent `3.2`)
- `veille`: `2026-02-21T00:00`
- Backfill station locale (NOAA) execute:
- plage `2026-01-01` -> `2026-02-22`
- `53` jours traites, `53` upserts, `0` erreur
- Script historique Open-Meteo cree:
- `station_meteo/update_openmeteo_history_db.py`
- options: `--start-date`, `--end-date`, `--lat`, `--lon`, `--chunk-days`, `--dry-run`
- source: endpoint archive Open-Meteo
- cible: table `meteoopenmeteo` (upsert par date)
- Backfill Open-Meteo execute:
- `python3 station_meteo/update_openmeteo_history_db.py --start-date 2026-01-01 --end-date 2026-02-22`
- resultat: `53` lignes recuperees et mises a jour en base
### Dashboard / Meteo: ergonomie et visuel
- `frontend/src/views/DashboardView.vue`
- ajout d'un bloc "Condition actuelle" (icone meteo + libelle + temperature station + heure releve)
- affichage prevision sur 7 jours avec icones meteo
- suppression du scroll horizontal des cartes meteo:
- passage d'un layout `flex overflow-x-auto` a une grille responsive
- conteneur elargi (`max-w-6xl`) pour une meilleure lisibilite laptop
- `frontend/src/views/CalendrierView.vue`
- icone pression plus lisible dans le bandeau station:
- `` remplace par `🧭`
### Navigation responsive
- `frontend/src/components/AppDrawer.vue`
- correction ouverture menu en fenetre reduite laptop/tablette:
- `md:hidden` -> `lg:hidden`
- le drawer est maintenant disponible sur toutes les largeurs < `lg`
### Jardins: popup edition responsive
- `frontend/src/views/JardinsView.vue`
- popup `Nouveau/Modifier jardin` passe en responsive:
- smartphone: `1` colonne
- laptop/desktop: `2` colonnes
- modal elargi (`max-w-4xl`)
- sections longues en pleine largeur (`lg:col-span-2`)
### Outils: ajout du champ video
- Backend:
- `backend/app/models/tool.py`: ajout `video_url`
- `backend/app/migrate.py`: ajout migration auto colonne `tool.video_url`
- test ajoute: `backend/tests/test_tools.py::test_tool_with_video_url`
- Frontend:
- `frontend/src/api/tools.ts`: type `video_url?: string`
- `frontend/src/views/OutilsView.vue`:
- upload video (`accept="video/*"`)
- preview video dans le formulaire
- enregistrement `video_url` via endpoint upload
- affichage video et lien "🎬 Video" sur les cartes outils
- Base locale:
- colonne `video_url` ajoutee et verifiee dans `data/jardin.db`
### Validation technique
- Frontend builds:
- `npm --prefix frontend run build` -> OK (plusieurs executions apres changements)
- Python compilation:
- `python3 -m py_compile` sur scripts/modeles modifies -> OK
- Note tests backend:
- `pytest backend/tests/test_tools.py` reste bloque dans ce contexte d'execution,
mais les changements de schema/code compilent et la colonne DB est presente.

View File

@@ -6,3 +6,5 @@ CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",")
STATION_URL = os.getenv("STATION_URL", "http://10.0.0.8:8081/") STATION_URL = os.getenv("STATION_URL", "http://10.0.0.8:8081/")
METEO_LAT = float(os.getenv("METEO_LAT", "45.14")) METEO_LAT = float(os.getenv("METEO_LAT", "45.14"))
METEO_LON = float(os.getenv("METEO_LON", "4.12")) METEO_LON = float(os.getenv("METEO_LON", "4.12"))
ENABLE_SCHEDULER = os.getenv("ENABLE_SCHEDULER", "1").lower() in {"1", "true", "yes", "on"}
ENABLE_BOOTSTRAP = os.getenv("ENABLE_BOOTSTRAP", "1").lower() in {"1", "true", "yes", "on"}

View File

@@ -4,7 +4,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.config import CORS_ORIGINS, UPLOAD_DIR from app.config import CORS_ORIGINS, ENABLE_BOOTSTRAP, ENABLE_SCHEDULER, UPLOAD_DIR
from app.database import create_db_and_tables from app.database import create_db_and_tables
@@ -15,19 +15,20 @@ async def lifespan(app: FastAPI):
os.makedirs("/data/skyfield", exist_ok=True) os.makedirs("/data/skyfield", exist_ok=True)
except OSError: except OSError:
pass pass
import app.models # noqa — enregistre tous les modèles avant create_all if ENABLE_BOOTSTRAP:
from app.migrate import run_migrations import app.models # noqa — enregistre tous les modèles avant create_all
run_migrations() from app.migrate import run_migrations
create_db_and_tables() run_migrations()
from app.seed import run_seed create_db_and_tables()
run_seed() from app.seed import run_seed
# Démarrer le scheduler météo run_seed()
from app.services.scheduler import setup_scheduler if ENABLE_SCHEDULER:
setup_scheduler() from app.services.scheduler import setup_scheduler
setup_scheduler()
yield yield
# Arrêter le scheduler if ENABLE_BOOTSTRAP and ENABLE_SCHEDULER:
from app.services.scheduler import scheduler from app.services.scheduler import scheduler
scheduler.shutdown(wait=False) scheduler.shutdown(wait=False)
app = FastAPI(title="Jardin API", lifespan=lifespan) app = FastAPI(title="Jardin API", lifespan=lifespan)

View File

@@ -15,7 +15,17 @@ EXPECTED_COLUMNS: dict[str, list[tuple[str, str, str | None]]] = {
("url_reference", "TEXT", None), ("url_reference", "TEXT", None),
], ],
"garden": [ "garden": [
("latitude", "REAL", None),
("longitude", "REAL", None),
("altitude", "REAL", None),
("adresse", "TEXT", None),
("longueur_m", "REAL", None),
("largeur_m", "REAL", None),
("surface_m2", "REAL", None), ("surface_m2", "REAL", None),
("carre_potager", "INTEGER", "0"),
("carre_x_cm", "INTEGER", None),
("carre_y_cm", "INTEGER", None),
("photo_parcelle", "TEXT", None),
("ensoleillement", "TEXT", None), ("ensoleillement", "TEXT", None),
], ],
"task": [ "task": [
@@ -23,6 +33,24 @@ EXPECTED_COLUMNS: dict[str, list[tuple[str, str, str | None]]] = {
("date_prochaine", "TEXT", None), ("date_prochaine", "TEXT", None),
("outil_id", "INTEGER", None), ("outil_id", "INTEGER", None),
], ],
"meteostation": [
("t_min", "REAL", None),
("t_max", "REAL", None),
],
"tool": [
("photo_url", "TEXT", None),
("video_url", "TEXT", None),
("notice_fichier_url", "TEXT", None),
("boutique_nom", "TEXT", None),
("boutique_url", "TEXT", None),
("prix_achat", "REAL", None),
],
"planting": [
("boutique_nom", "TEXT", None),
("boutique_url", "TEXT", None),
("tarif_achat", "REAL", None),
("date_achat", "TEXT", None),
],
"plantvariety": [ "plantvariety": [
# ancien nom de table → migration vers "plant" si présente # ancien nom de table → migration vers "plant" si présente
], ],
@@ -36,6 +64,8 @@ EXPECTED_COLUMNS: dict[str, list[tuple[str, str, str | None]]] = {
("categorie", "TEXT", None), ("categorie", "TEXT", None),
("tags", "TEXT", None), ("tags", "TEXT", None),
("mois", "TEXT", None), ("mois", "TEXT", None),
("photos", "TEXT", None),
("videos", "TEXT", None),
], ],
} }

View File

@@ -16,4 +16,6 @@ class Astuce(SQLModel, table=True):
categorie: Optional[str] = None # "plante"|"jardin"|"tache"|"general"|"ravageur"|"maladie" categorie: Optional[str] = None # "plante"|"jardin"|"tache"|"general"|"ravageur"|"maladie"
tags: Optional[str] = None # JSON array string: '["tomate","semis"]' tags: Optional[str] = None # JSON array string: '["tomate","semis"]'
mois: Optional[str] = None # JSON array string: '[3,4,5]' ou null = toute l'année mois: Optional[str] = None # JSON array string: '[3,4,5]' ou null = toute l'année
photos: Optional[str] = None # JSON array string: '["/uploads/a.webp"]'
videos: Optional[str] = None # JSON array string: '["/uploads/b.mp4"]'
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -16,7 +16,13 @@ class Garden(SQLModel, table=True):
ombre: Optional[str] = None # ombre | mi-ombre | plein_soleil ombre: Optional[str] = None # ombre | mi-ombre | plein_soleil
sol_type: Optional[str] = None sol_type: Optional[str] = None
sol_ph: Optional[float] = None sol_ph: Optional[float] = None
longueur_m: Optional[float] = None
largeur_m: Optional[float] = None
surface_m2: Optional[float] = None surface_m2: Optional[float] = None
carre_potager: bool = False
carre_x_cm: Optional[int] = None
carre_y_cm: Optional[int] = None
photo_parcelle: Optional[str] = None
ensoleillement: Optional[str] = None ensoleillement: Optional[str] = None
grille_largeur: int = 6 grille_largeur: int = 6
grille_hauteur: int = 4 grille_hauteur: int = 4

View File

@@ -5,7 +5,7 @@ from sqlmodel import Field, SQLModel
class Media(SQLModel, table=True): class Media(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
entity_type: str # jardin|plante|outil|plantation entity_type: str # jardin|plante|adventice|outil|plantation|bibliotheque
entity_id: int entity_id: int
url: str url: str
thumbnail_url: Optional[str] = None thumbnail_url: Optional[str] = None

View File

@@ -9,6 +9,8 @@ class MeteoStation(SQLModel, table=True):
date_heure: str = Field(primary_key=True) # "2026-02-22T14:00" date_heure: str = Field(primary_key=True) # "2026-02-22T14:00"
type: str = "current" # "current" | "veille" type: str = "current" # "current" | "veille"
temp_ext: Optional[float] = None # °C extérieur temp_ext: Optional[float] = None # °C extérieur
t_min: Optional[float] = None # résumé journée (NOAA)
t_max: Optional[float] = None # résumé journée (NOAA)
temp_int: Optional[float] = None # °C intérieur (serre) temp_int: Optional[float] = None # °C intérieur (serre)
humidite: Optional[float] = None # % humidite: Optional[float] = None # %
pression: Optional[float] = None # hPa pression: Optional[float] = None # hPa

View File

@@ -12,6 +12,10 @@ class PlantingCreate(SQLModel):
date_repiquage: Optional[date] = None date_repiquage: Optional[date] = None
quantite: int = 1 quantite: int = 1
statut: str = "prevu" statut: str = "prevu"
boutique_nom: Optional[str] = None
boutique_url: Optional[str] = None
tarif_achat: Optional[float] = None
date_achat: Optional[date] = None
date_recolte_debut: Optional[date] = None date_recolte_debut: Optional[date] = None
date_recolte_fin: Optional[date] = None date_recolte_fin: Optional[date] = None
rendement_estime: Optional[float] = None rendement_estime: Optional[float] = None
@@ -29,6 +33,10 @@ class Planting(SQLModel, table=True):
date_repiquage: Optional[date] = None date_repiquage: Optional[date] = None
quantite: int = 1 quantite: int = 1
statut: str = "prevu" # prevu | en_cours | termine | echoue statut: str = "prevu" # prevu | en_cours | termine | echoue
boutique_nom: Optional[str] = None
boutique_url: Optional[str] = None
tarif_achat: Optional[float] = None
date_achat: Optional[date] = None
date_recolte_debut: Optional[date] = None date_recolte_debut: Optional[date] = None
date_recolte_fin: Optional[date] = None date_recolte_fin: Optional[date] = None
rendement_estime: Optional[float] = None rendement_estime: Optional[float] = None

View File

@@ -9,4 +9,9 @@ class Tool(SQLModel, table=True):
description: Optional[str] = None description: Optional[str] = None
categorie: Optional[str] = None # beche|fourche|griffe|arrosage|taille|autre categorie: Optional[str] = None # beche|fourche|griffe|arrosage|taille|autre
photo_url: Optional[str] = None photo_url: Optional[str] = None
video_url: Optional[str] = None
notice_fichier_url: Optional[str] = None
boutique_nom: Optional[str] = None
boutique_url: Optional[str] = None
prix_achat: Optional[float] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View File

@@ -1,3 +1,4 @@
import json
from typing import List, Optional from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select from sqlmodel import Session, select
@@ -7,10 +8,45 @@ from app.models.astuce import Astuce
router = APIRouter(tags=["astuces"]) router = APIRouter(tags=["astuces"])
def _decode_tags(raw: Optional[str]) -> list[str]:
if not raw:
return []
try:
parsed = json.loads(raw)
except Exception:
parsed = [p.strip() for p in raw.split(",") if p.strip()]
if not isinstance(parsed, list):
return []
return [str(x).strip().lower() for x in parsed if str(x).strip()]
def _decode_mois(raw: Optional[str]) -> list[int]:
if not raw:
return []
try:
parsed = json.loads(raw)
except Exception:
parsed = [p.strip() for p in raw.split(",") if p.strip()]
if not isinstance(parsed, list):
return []
result: list[int] = []
for x in parsed:
try:
month = int(x)
if 1 <= month <= 12:
result.append(month)
except (TypeError, ValueError):
continue
return result
@router.get("/astuces", response_model=List[Astuce]) @router.get("/astuces", response_model=List[Astuce])
def list_astuces( def list_astuces(
entity_type: Optional[str] = Query(None), entity_type: Optional[str] = Query(None),
entity_id: Optional[int] = Query(None), entity_id: Optional[int] = Query(None),
categorie: Optional[str] = Query(None),
tag: Optional[str] = Query(None),
mois: Optional[int] = Query(None, ge=1, le=12),
session: Session = Depends(get_session), session: Session = Depends(get_session),
): ):
q = select(Astuce) q = select(Astuce)
@@ -18,7 +54,21 @@ def list_astuces(
q = q.where(Astuce.entity_type == entity_type) q = q.where(Astuce.entity_type == entity_type)
if entity_id is not None: if entity_id is not None:
q = q.where(Astuce.entity_id == entity_id) q = q.where(Astuce.entity_id == entity_id)
return session.exec(q).all()
if categorie:
q = q.where(Astuce.categorie == categorie)
items = session.exec(q).all()
if tag:
wanted = tag.strip().lower()
items = [a for a in items if wanted in _decode_tags(a.tags)]
if mois is not None:
# mois null/empty = astuce valable toute l'année
items = [a for a in items if not _decode_mois(a.mois) or mois in _decode_mois(a.mois)]
return items
@router.post("/astuces", response_model=Astuce, status_code=status.HTTP_201_CREATED) @router.post("/astuces", response_model=Astuce, status_code=status.HTTP_201_CREATED)

View File

@@ -1,15 +1,37 @@
import os
import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import List from typing import List
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from sqlmodel import Session, select from sqlmodel import Session, select
from app.config import UPLOAD_DIR
from app.database import get_session from app.database import get_session
from app.models.garden import Garden, GardenCell, GardenImage, Measurement from app.models.garden import Garden, GardenCell, GardenImage, Measurement
router = APIRouter(tags=["jardins"]) router = APIRouter(tags=["jardins"])
def _save_garden_photo(data: bytes) -> str:
try:
from PIL import Image
import io
img = Image.open(io.BytesIO(data)).convert("RGB")
img.thumbnail((1800, 1800))
name = f"garden_{uuid.uuid4()}.webp"
path = os.path.join(UPLOAD_DIR, name)
img.save(path, "WEBP", quality=88)
return name
except Exception:
name = f"garden_{uuid.uuid4()}.bin"
path = os.path.join(UPLOAD_DIR, name)
with open(path, "wb") as f:
f.write(data)
return name
@router.get("/gardens", response_model=List[Garden]) @router.get("/gardens", response_model=List[Garden])
def list_gardens(session: Session = Depends(get_session)): def list_gardens(session: Session = Depends(get_session)):
return session.exec(select(Garden)).all() return session.exec(select(Garden)).all()
@@ -31,6 +53,31 @@ def get_garden(id: int, session: Session = Depends(get_session)):
return g return g
@router.post("/gardens/{id}/photo", response_model=Garden)
async def upload_garden_photo(
id: int,
file: UploadFile = File(...),
session: Session = Depends(get_session),
):
g = session.get(Garden, id)
if not g:
raise HTTPException(status_code=404, detail="Jardin introuvable")
content_type = file.content_type or ""
if not content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="Le fichier doit être une image")
os.makedirs(UPLOAD_DIR, exist_ok=True)
data = await file.read()
filename = _save_garden_photo(data)
g.photo_parcelle = f"/uploads/{filename}"
g.updated_at = datetime.now(timezone.utc)
session.add(g)
session.commit()
session.refresh(g)
return g
@router.put("/gardens/{id}", response_model=Garden) @router.put("/gardens/{id}", response_model=Garden)
def update_garden(id: int, data: Garden, session: Session = Depends(get_session)): def update_garden(id: int, data: Garden, session: Session = Depends(get_session)):
g = session.get(Garden, id) g = session.get(Garden, id)

View File

@@ -2,7 +2,7 @@
from datetime import date, timedelta from datetime import date, timedelta
from typing import Any, Optional from typing import Any, Optional
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text from sqlalchemy import text
from sqlmodel import Session from sqlmodel import Session
@@ -15,7 +15,7 @@ def _station_daily_summary(session: Session, iso_date: str) -> Optional[dict]:
"""Agrège les mesures horaires d'une journée en résumé.""" """Agrège les mesures horaires d'une journée en résumé."""
rows = session.exec( rows = session.exec(
text( text(
"SELECT temp_ext, pluie_mm, vent_kmh, humidite " "SELECT temp_ext, t_min, t_max, pluie_mm, vent_kmh, humidite "
"FROM meteostation WHERE substr(date_heure, 1, 10) = :d" "FROM meteostation WHERE substr(date_heure, 1, 10) = :d"
), ),
params={"d": iso_date}, params={"d": iso_date},
@@ -25,14 +25,20 @@ def _station_daily_summary(session: Session, iso_date: str) -> Optional[dict]:
return None return None
temps = [r[0] for r in rows if r[0] is not None] temps = [r[0] for r in rows if r[0] is not None]
pluies = [r[1] for r in rows if r[1] is not None] t_mins = [r[1] for r in rows if r[1] is not None]
vents = [r[2] for r in rows if r[2] is not None] t_maxs = [r[2] for r in rows if r[2] is not None]
hums = [r[3] for r in rows if r[3] is not None] pluies = [r[3] for r in rows if r[3] is not None]
vents = [r[4] for r in rows if r[4] is not None]
hums = [r[5] for r in rows if r[5] is not None]
min_candidates = temps + t_mins
max_candidates = temps + t_maxs
return { return {
"t_min": round(min(temps), 1) if temps else None, "t_min": round(min(min_candidates), 1) if min_candidates else None,
"t_max": round(max(temps), 1) if temps else None, "t_max": round(max(max_candidates), 1) if max_candidates else None,
"pluie_mm": round(sum(pluies), 1) if pluies else 0.0, # WeeWX RSS expose souvent une pluie cumulée journalière.
"pluie_mm": round(max(pluies), 1) if pluies else 0.0,
"vent_kmh": round(max(vents), 1) if vents else None, "vent_kmh": round(max(vents), 1) if vents else None,
"humidite": round(sum(hums) / len(hums), 0) if hums else None, "humidite": round(sum(hums) / len(hums), 0) if hums else None,
} }
@@ -77,22 +83,36 @@ def _open_meteo_day(session: Session, iso_date: str) -> Optional[dict]:
@router.get("/meteo/tableau") @router.get("/meteo/tableau")
def get_tableau(session: Session = Depends(get_session)) -> dict[str, Any]: def get_tableau(
"""Tableau synthétique : 7j passé + J0 + 7j futur.""" center_date: Optional[str] = Query(None, description="Date centrale YYYY-MM-DD"),
span: int = Query(7, ge=1, le=31, description="Nombre de jours avant/après la date centrale"),
session: Session = Depends(get_session),
) -> dict[str, Any]:
"""Tableau synthétique centré sur une date, avec historique + prévision."""
today = date.today() today = date.today()
center = today
if center_date:
try:
center = date.fromisoformat(center_date)
except ValueError as exc:
raise HTTPException(status_code=400, detail="center_date invalide (format YYYY-MM-DD)") from exc
rows = [] rows = []
for delta in range(-7, 8): for delta in range(-span, span + 1):
d = today + timedelta(days=delta) d = center + timedelta(days=delta)
iso = d.isoformat() iso = d.isoformat()
delta_today = (d - today).days
if delta < 0: if delta_today < 0:
row_type = "passe" row_type = "passe"
station = _station_daily_summary(session, iso) station = _station_daily_summary(session, iso)
om = None # Pas de prévision pour le passé om = _open_meteo_day(session, iso)
elif delta == 0: elif delta_today == 0:
row_type = "aujourd_hui" row_type = "aujourd_hui"
station = _station_current_row(session) station_current = _station_current_row(session) or {}
station_daily = _station_daily_summary(session, iso) or {}
station = {**station_daily, **station_current} or None
om = _open_meteo_day(session, iso) om = _open_meteo_day(session, iso)
else: else:
row_type = "futur" row_type = "futur"

View File

@@ -1,10 +1,117 @@
import os
import shutil
import time
from typing import Any
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlmodel import Session, select from sqlmodel import Session, select
from app.database import get_session from app.database import get_session
from app.models.settings import UserSettings from app.models.settings import UserSettings
from app.config import UPLOAD_DIR
router = APIRouter(tags=["réglages"]) router = APIRouter(tags=["réglages"])
_PREV_CPU_USAGE_USEC: int | None = None
_PREV_CPU_TS: float | None = None
def _read_int_from_paths(paths: list[str]) -> int | None:
for path in paths:
try:
with open(path, "r", encoding="utf-8") as f:
raw = f.read().strip().split()[0]
return int(raw)
except Exception:
continue
return None
def _read_cgroup_cpu_usage_usec() -> int | None:
# cgroup v2
try:
with open("/sys/fs/cgroup/cpu.stat", "r", encoding="utf-8") as f:
for line in f:
if line.startswith("usage_usec "):
return int(line.split()[1])
except Exception:
pass
# cgroup v1
ns = _read_int_from_paths(["/sys/fs/cgroup/cpuacct/cpuacct.usage"])
if ns is not None:
return ns // 1000
return None
def _cpu_quota_cores() -> float | None:
# cgroup v2
try:
with open("/sys/fs/cgroup/cpu.max", "r", encoding="utf-8") as f:
quota, period = f.read().strip().split()[:2]
if quota == "max":
return float(os.cpu_count() or 1)
q, p = int(quota), int(period)
if p > 0:
return max(q / p, 0.01)
except Exception:
pass
# cgroup v1
quota = _read_int_from_paths(["/sys/fs/cgroup/cpu/cpu.cfs_quota_us"])
period = _read_int_from_paths(["/sys/fs/cgroup/cpu/cpu.cfs_period_us"])
if quota is not None and period is not None and quota > 0 and period > 0:
return max(quota / period, 0.01)
return float(os.cpu_count() or 1)
def _memory_stats() -> dict[str, Any]:
used = _read_int_from_paths(
[
"/sys/fs/cgroup/memory.current", # cgroup v2
"/sys/fs/cgroup/memory/memory.usage_in_bytes", # cgroup v1
]
)
limit = _read_int_from_paths(
[
"/sys/fs/cgroup/memory.max", # cgroup v2
"/sys/fs/cgroup/memory/memory.limit_in_bytes", # cgroup v1
]
)
# Certaines limites cgroup valent "max" ou des sentinelles tres grandes.
if limit is not None and limit >= 9_000_000_000_000_000_000:
limit = None
pct = None
if used is not None and limit and limit > 0:
pct = round((used / limit) * 100, 1)
return {"used_bytes": used, "limit_bytes": limit, "used_pct": pct}
def _disk_stats() -> dict[str, Any]:
target = "/data" if os.path.isdir("/data") else "/"
total, used, free = shutil.disk_usage(target)
uploads_size = None
if os.path.isdir(UPLOAD_DIR):
try:
uploads_size = sum(
os.path.getsize(os.path.join(root, name))
for root, _, files in os.walk(UPLOAD_DIR)
for name in files
)
except Exception:
uploads_size = None
return {
"path": target,
"total_bytes": total,
"used_bytes": used,
"free_bytes": free,
"used_pct": round((used / total) * 100, 1) if total else None,
"uploads_bytes": uploads_size,
}
@router.get("/settings") @router.get("/settings")
def get_settings(session: Session = Depends(get_session)): def get_settings(session: Session = Depends(get_session)):
@@ -23,3 +130,34 @@ def update_settings(data: dict, session: Session = Depends(get_session)):
session.add(row) session.add(row)
session.commit() session.commit()
return {"ok": True} return {"ok": True}
@router.get("/settings/debug/system")
def get_debug_system_stats() -> dict[str, Any]:
"""Stats runtime du conteneur (utile pour affichage debug UI)."""
global _PREV_CPU_USAGE_USEC, _PREV_CPU_TS
now = time.monotonic()
usage_usec = _read_cgroup_cpu_usage_usec()
quota_cores = _cpu_quota_cores()
cpu_pct = None
if usage_usec is not None and _PREV_CPU_USAGE_USEC is not None and _PREV_CPU_TS is not None:
delta_usage = usage_usec - _PREV_CPU_USAGE_USEC
delta_time_usec = (now - _PREV_CPU_TS) * 1_000_000
if delta_time_usec > 0 and quota_cores and quota_cores > 0:
cpu_pct = round((delta_usage / (delta_time_usec * quota_cores)) * 100, 1)
_PREV_CPU_USAGE_USEC = usage_usec
_PREV_CPU_TS = now
return {
"source": "container-cgroup",
"cpu": {
"usage_usec_total": usage_usec,
"quota_cores": quota_cores,
"used_pct": cpu_pct,
},
"memory": _memory_stats(),
"disk": _disk_stats(),
}

View File

@@ -19,6 +19,17 @@ SIGN_TO_TYPE = {
"Bélier": "Fruit", "Lion": "Fruit", "Sagittaire": "Fruit", "Bélier": "Fruit", "Lion": "Fruit", "Sagittaire": "Fruit",
} }
SAINTS_BY_MMDD = {
"04-23": "Saint Georges",
"04-25": "Saint Marc",
"05-11": "Saint Mamert",
"05-12": "Saint Pancrace",
"05-13": "Saint Servais",
"05-14": "Saint Boniface",
"05-19": "Saint Yves",
"05-25": "Saint Urbain",
}
@dataclass @dataclass
class DayInfo: class DayInfo:
@@ -29,6 +40,7 @@ class DayInfo:
montante_descendante: str montante_descendante: str
signe: str signe: str
type_jour: str type_jour: str
saint_du_jour: str
perigee: bool perigee: bool
apogee: bool apogee: bool
noeud_lunaire: bool noeud_lunaire: bool
@@ -126,6 +138,7 @@ def build_calendar(start: date, end: date) -> list[DayInfo]:
lat, lon, dist = v_moon.ecliptic_latlon() lat, lon, dist = v_moon.ecliptic_latlon()
signe = zodiac_sign_from_lon(lon.degrees % 360.0) signe = zodiac_sign_from_lon(lon.degrees % 360.0)
type_jour = SIGN_TO_TYPE[signe] type_jour = SIGN_TO_TYPE[signe]
saint_du_jour = SAINTS_BY_MMDD.get(d.strftime("%m-%d"), "")
result.append( result.append(
DayInfo( DayInfo(
date=d.isoformat(), date=d.isoformat(),
@@ -135,6 +148,7 @@ def build_calendar(start: date, end: date) -> list[DayInfo]:
montante_descendante=montante, montante_descendante=montante,
signe=signe, signe=signe,
type_jour=type_jour, type_jour=type_jour,
saint_du_jour=saint_du_jour,
perigee=(d in perigee_days), perigee=(d in perigee_days),
apogee=(d in apogee_days), apogee=(d in apogee_days),
noeud_lunaire=(d in node_days), noeud_lunaire=(d in node_days),

View File

@@ -32,6 +32,46 @@ _DAILY_FIELDS = [
"et0_fao_evapotranspiration", "et0_fao_evapotranspiration",
] ]
_HOURLY_FIELDS = [
"soil_temperature_0cm",
]
def _to_float(value: Any) -> float | None:
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _value_at(values: list[Any], index: int, default: Any = None) -> Any:
if index < 0 or index >= len(values):
return default
return values[index]
def _daily_soil_average(raw: dict[str, Any]) -> dict[str, float]:
"""Construit un mapping ISO-date -> moyenne de soil_temperature_0cm."""
hourly = raw.get("hourly", {})
times = hourly.get("time", []) or []
soils = hourly.get("soil_temperature_0cm", []) or []
by_day: dict[str, list[float]] = {}
for idx, ts in enumerate(times):
soil = _to_float(_value_at(soils, idx))
if soil is None or not isinstance(ts, str) or len(ts) < 10:
continue
day = ts[:10]
by_day.setdefault(day, []).append(soil)
return {
day: round(sum(vals) / len(vals), 2)
for day, vals in by_day.items()
if vals
}
def fetch_and_store_forecast(lat: float = METEO_LAT, lon: float = METEO_LON) -> list[dict]: def fetch_and_store_forecast(lat: float = METEO_LAT, lon: float = METEO_LON) -> list[dict]:
"""Appelle Open-Meteo et retourne la liste des jours (past_days=7 + forecast=8). """Appelle Open-Meteo et retourne la liste des jours (past_days=7 + forecast=8).
@@ -50,6 +90,8 @@ def fetch_and_store_forecast(lat: float = METEO_LAT, lon: float = METEO_LON) ->
] ]
for field in _DAILY_FIELDS: for field in _DAILY_FIELDS:
params.append(("daily", field)) params.append(("daily", field))
for field in _HOURLY_FIELDS:
params.append(("hourly", field))
try: try:
r = httpx.get(url, params=params, timeout=15) r = httpx.get(url, params=params, timeout=15)
@@ -61,22 +103,23 @@ def fetch_and_store_forecast(lat: float = METEO_LAT, lon: float = METEO_LON) ->
daily = raw.get("daily", {}) daily = raw.get("daily", {})
dates = daily.get("time", []) dates = daily.get("time", [])
soil_by_day = _daily_soil_average(raw)
now_iso = datetime.now(timezone.utc).isoformat() now_iso = datetime.now(timezone.utc).isoformat()
rows = [] rows = []
for i, d in enumerate(dates): for i, d in enumerate(dates):
code = int(daily.get("weather_code", [0] * len(dates))[i] or 0) code = int(_value_at(daily.get("weather_code", []), i, 0) or 0)
row = { row = {
"date": d, "date": d,
"t_min": daily.get("temperature_2m_min", [None] * len(dates))[i], "t_min": _to_float(_value_at(daily.get("temperature_2m_min", []), i)),
"t_max": daily.get("temperature_2m_max", [None] * len(dates))[i], "t_max": _to_float(_value_at(daily.get("temperature_2m_max", []), i)),
"pluie_mm": daily.get("precipitation_sum", [0] * len(dates))[i] or 0.0, "pluie_mm": _to_float(_value_at(daily.get("precipitation_sum", []), i, 0.0)) or 0.0,
"vent_kmh": daily.get("wind_speed_10m_max", [0] * len(dates))[i] or 0.0, "vent_kmh": _to_float(_value_at(daily.get("wind_speed_10m_max", []), i, 0.0)) or 0.0,
"wmo": code, "wmo": code,
"label": WMO_LABELS.get(code, f"Code {code}"), "label": WMO_LABELS.get(code, f"Code {code}"),
"humidite_moy": daily.get("relative_humidity_2m_max", [None] * len(dates))[i], "humidite_moy": _to_float(_value_at(daily.get("relative_humidity_2m_max", []), i)),
"sol_0cm": None, # soil_temperature_0cm est hourly uniquement "sol_0cm": soil_by_day.get(d),
"etp_mm": daily.get("et0_fao_evapotranspiration", [None] * len(dates))[i], "etp_mm": _to_float(_value_at(daily.get("et0_fao_evapotranspiration", []), i)),
"fetched_at": now_iso, "fetched_at": now_iso,
} }
rows.append(row) rows.append(row)

View File

@@ -1,6 +1,8 @@
"""Service de collecte des données de la station météo locale WeeWX.""" """Service de collecte des données de la station météo locale WeeWX."""
import html
import logging import logging
import re import re
import unicodedata
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@@ -17,13 +19,37 @@ def _safe_float(text: str | None) -> float | None:
try: try:
cleaned = text.strip().replace(",", ".") cleaned = text.strip().replace(",", ".")
# Retirer unités courantes # Retirer unités courantes
for unit in [" °C", " %", " hPa", " km/h", " W/m²", "°C", "%", "hPa"]: for unit in [
" °C",
" %", " %",
" hPa", " mbar",
" km/h", " m/s",
" mm/h", " mm",
" W/m²", " W/m2",
"°C", "%", "hPa", "mbar",
]:
cleaned = cleaned.replace(unit, "") cleaned = cleaned.replace(unit, "")
return float(cleaned.strip()) return float(cleaned.strip())
except (ValueError, AttributeError): except (ValueError, AttributeError):
return None return None
def _normalize(text: str) -> str:
text = unicodedata.normalize("NFKD", text)
text = "".join(ch for ch in text if not unicodedata.combining(ch))
text = text.lower()
return re.sub(r"\s+", " ", text).strip()
def _to_kmh(value: float | None, unit: str | None) -> float | None:
if value is None:
return None
u = (unit or "").strip().lower()
if u == "m/s":
return round(value * 3.6, 1)
return round(value, 1)
def _direction_to_abbr(deg: float | None) -> str | None: def _direction_to_abbr(deg: float | None) -> str | None:
if deg is None: if deg is None:
return None return None
@@ -51,37 +77,51 @@ def fetch_current(base_url: str = STATION_URL) -> dict | None:
if item is None: if item is None:
return None return None
desc = item.findtext("description") or "" desc = html.unescape(item.findtext("description") or "")
result: dict = {} result: dict = {}
segments = [seg.strip() for seg in desc.split(";") if seg.strip()]
for seg in segments:
if ":" not in seg:
continue
raw_key, raw_value = seg.split(":", 1)
key = _normalize(raw_key)
value = raw_value.strip()
patterns = { if "temperature exterieure" in key or "outside temperature" in key:
"temp_ext": r"(?:Outside|Ext(?:erieur)?)\s*Temp(?:erature)?\s*[:\s]+(-?\d+(?:[.,]\d+)?)", result["temp_ext"] = _safe_float(value)
"temp_int": r"(?:Inside|Int(?:erieur)?)\s*Temp(?:erature)?\s*[:\s]+(-?\d+(?:[.,]\d+)?)", continue
"humidite": r"(?:Outside\s*)?Hum(?:idity)?\s*[:\s]+(\d+(?:[.,]\d+)?)", if "temperature interieure" in key or "inside temperature" in key:
"pression": r"(?:Bar(?:ometer)?|Pression)\s*[:\s]+(\d+(?:[.,]\d+)?)", result["temp_int"] = _safe_float(value)
"pluie_mm": r"(?:Rain(?:fall)?|Pluie)\s*[:\s]+(\d+(?:[.,]\d+)?)", continue
"vent_kmh": r"(?:Wind\s*Speed|Vent)\s*[:\s]+(\d+(?:[.,]\d+)?)", if "hygrometrie exterieure" in key or "outside humidity" in key:
"uv": r"UV\s*[:\s]+(\d+(?:[.,]\d+)?)", result["humidite"] = _safe_float(value)
"solaire": r"(?:Solar\s*Radiation|Solaire)\s*[:\s]+(\d+(?:[.,]\d+)?)", continue
} if "pression atmospherique" in key or "barometer" in key:
result["pression"] = _safe_float(value)
continue
if "precipitations" in key and "taux" not in key and "rate" not in key:
result["pluie_mm"] = _safe_float(value)
continue
if key in {"uv", "ultra-violet"} or "ultra violet" in key:
result["uv"] = _safe_float(value)
continue
if "rayonnement solaire" in key or "solar radiation" in key:
result["solaire"] = _safe_float(value)
continue
if key == "vent" or "wind" in key:
speed_match = re.search(r"(-?\d+(?:[.,]\d+)?)\s*(m/s|km/h)?", value, re.IGNORECASE)
speed_val = _safe_float(speed_match.group(1)) if speed_match else None
speed_unit = speed_match.group(2) if speed_match else None
result["vent_kmh"] = _to_kmh(speed_val, speed_unit)
for key, pattern in patterns.items(): deg_match = re.search(r"(\d{1,3}(?:[.,]\d+)?)\s*°", value)
m = re.search(pattern, desc, re.IGNORECASE) if deg_match:
result[key] = _safe_float(m.group(1)) if m else None result["vent_dir"] = _direction_to_abbr(_safe_float(deg_match.group(1)))
continue
vent_dir_m = re.search( card_match = re.search(r"\b(N|NE|E|SE|S|SO|O|NO|NNE|ENE|ESE|SSE|SSO|OSO|ONO|NNO)\b", value, re.IGNORECASE)
r"(?:Wind\s*Dir(?:ection)?)\s*[:\s]+([NSEO]{1,2}|Nord|Sud|Est|Ouest|\d+)", result["vent_dir"] = card_match.group(1).upper() if card_match else None
desc, re.IGNORECASE,
)
if vent_dir_m:
val = vent_dir_m.group(1).strip()
if val.isdigit():
result["vent_dir"] = _direction_to_abbr(float(val))
else:
result["vent_dir"] = val[:2].upper()
else:
result["vent_dir"] = None
return result if any(v is not None for v in result.values()) else None return result if any(v is not None for v in result.values()) else None
@@ -107,15 +147,28 @@ def fetch_yesterday_summary(base_url: str = STATION_URL) -> dict | None:
for line in r.text.splitlines(): for line in r.text.splitlines():
parts = line.split() parts = line.split()
if len(parts) >= 7 and parts[0].isdigit() and int(parts[0]) == day: if not parts or not parts[0].isdigit() or int(parts[0]) != day:
# Format NOAA : jour tmax tmin tmoy precip ... continue
# Format WeeWX NOAA (fréquent) :
# day mean max hh:mm min hh:mm HDD CDD rain wind_avg wind_max hh:mm dir
if len(parts) >= 11 and ":" in parts[3] and ":" in parts[5]:
return { return {
"t_max": _safe_float(parts[1]), "temp_ext": _safe_float(parts[1]),
"t_min": _safe_float(parts[2]), "t_max": _safe_float(parts[2]),
"temp_ext": _safe_float(parts[3]), "t_min": _safe_float(parts[4]),
"pluie_mm": _safe_float(parts[5]), "pluie_mm": _safe_float(parts[8]),
"vent_kmh": _safe_float(parts[6]) if len(parts) > 6 else None, "vent_kmh": _to_kmh(_safe_float(parts[10]), "m/s"),
} }
# Fallback générique (anciens formats)
return {
"t_max": _safe_float(parts[1]) if len(parts) > 1 else None,
"t_min": _safe_float(parts[2]) if len(parts) > 2 else None,
"temp_ext": _safe_float(parts[3]) if len(parts) > 3 else None,
"pluie_mm": _safe_float(parts[5]) if len(parts) > 5 else None,
"vent_kmh": _safe_float(parts[6]) if len(parts) > 6 else None,
}
except Exception as e: except Exception as e:
logger.warning(f"Station fetch_yesterday_summary error: {e}") logger.warning(f"Station fetch_yesterday_summary error: {e}")
return None return None

View File

@@ -1,29 +1,39 @@
import os
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import SQLModel, create_engine, Session from sqlmodel import SQLModel, create_engine, Session
from sqlmodel.pool import StaticPool from sqlmodel.pool import StaticPool
os.environ.setdefault("ENABLE_SCHEDULER", "0")
os.environ.setdefault("ENABLE_BOOTSTRAP", "0")
import app.models # noqa — force l'enregistrement des modèles import app.models # noqa — force l'enregistrement des modèles
from app.main import app from app.main import app
from app.database import get_session from app.database import get_session
@pytest.fixture(name="session") @pytest.fixture(name="engine")
def session_fixture(): def engine_fixture():
engine = create_engine( engine = create_engine(
"sqlite://", "sqlite://",
connect_args={"check_same_thread": False}, connect_args={"check_same_thread": False},
poolclass=StaticPool, poolclass=StaticPool,
) )
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)
return engine
@pytest.fixture(name="session")
def session_fixture(engine):
with Session(engine) as session: with Session(engine) as session:
yield session yield session
@pytest.fixture(name="client") @pytest.fixture(name="client")
def client_fixture(session: Session): def client_fixture(engine):
def get_session_override(): def get_session_override():
yield session with Session(engine) as s:
yield s
app.dependency_overrides[get_session] = get_session_override app.dependency_overrides[get_session] = get_session_override
client = TestClient(app) client = TestClient(app)

View File

@@ -0,0 +1,75 @@
"""Tests des filtres catégorie/tag/mois du router astuces."""
from app.models.astuce import Astuce
from app.routers.astuces import list_astuces
def _seed(session):
session.add(
Astuce(
titre="Tomate mildiou",
contenu="Surveiller humidité",
categorie="maladie",
tags='["tomate", "mildiou"]',
mois="[6,7,8]",
entity_type="plant",
entity_id=1,
)
)
session.add(
Astuce(
titre="Semis salade",
contenu="Semer en ligne",
categorie="plante",
tags='["salade", "semis"]',
mois="[3,4,9]",
entity_type="plant",
entity_id=2,
)
)
session.add(
Astuce(
titre="Paillage universel",
contenu="Proteger le sol",
categorie="jardin",
tags='["sol", "eau"]',
mois=None,
)
)
session.commit()
def test_filter_by_categorie(session):
_seed(session)
out = list_astuces(entity_type=None, entity_id=None, categorie="plante", tag=None, mois=None, session=session)
assert len(out) == 1
assert out[0].titre == "Semis salade"
def test_filter_by_tag(session):
_seed(session)
out = list_astuces(entity_type=None, entity_id=None, categorie=None, tag="tomate", mois=None, session=session)
assert len(out) == 1
assert out[0].titre == "Tomate mildiou"
def test_filter_by_mois_includes_all_year(session):
_seed(session)
out = list_astuces(entity_type=None, entity_id=None, categorie=None, tag=None, mois=12, session=session)
titles = {a.titre for a in out}
assert "Paillage universel" in titles
assert "Tomate mildiou" not in titles
def test_combined_filters(session):
_seed(session)
out = list_astuces(entity_type=None, entity_id=None, categorie="maladie", tag="mildiou", mois=7, session=session)
assert len(out) == 1
assert out[0].titre == "Tomate mildiou"
def test_legacy_entity_filters(session):
_seed(session)
out = list_astuces(entity_type="plant", entity_id=2, categorie=None, tag=None, mois=None, session=session)
assert len(out) == 1
assert out[0].titre == "Semis salade"

View File

@@ -0,0 +1,87 @@
"""Tests unitaires du service Open-Meteo enrichi."""
from datetime import date as real_date
import app.services.meteo as meteo
class _DummyResponse:
def __init__(self, payload: dict):
self._payload = payload
def raise_for_status(self) -> None:
return None
def json(self) -> dict:
return self._payload
def test_fetch_and_store_forecast_enriched(monkeypatch):
payload = {
"daily": {
"time": ["2026-02-21", "2026-02-22"],
"temperature_2m_min": [1.2, 2.3],
"temperature_2m_max": [8.4, 9.7],
"precipitation_sum": [0.5, 1.0],
"wind_speed_10m_max": [12.0, 15.0],
"weather_code": [3, 61],
"relative_humidity_2m_max": [88, 92],
"et0_fao_evapotranspiration": [0.9, 1.1],
},
"hourly": {
"time": [
"2026-02-21T00:00",
"2026-02-21T01:00",
"2026-02-22T00:00",
"2026-02-22T01:00",
],
"soil_temperature_0cm": [4.0, 6.0, 8.0, 10.0],
},
}
def _fake_get(*_args, **_kwargs):
return _DummyResponse(payload)
monkeypatch.setattr(meteo.httpx, "get", _fake_get)
rows = meteo.fetch_and_store_forecast(lat=45.1, lon=4.0)
assert len(rows) == 2
assert rows[0]["date"] == "2026-02-21"
assert rows[0]["label"] == "Couvert"
assert rows[0]["sol_0cm"] == 5.0
assert rows[0]["etp_mm"] == 0.9
assert rows[1]["label"] == "Pluie légère"
assert rows[1]["sol_0cm"] == 9.0
def test_fetch_and_store_forecast_handles_http_error(monkeypatch):
def _boom(*_args, **_kwargs):
raise RuntimeError("network down")
monkeypatch.setattr(meteo.httpx, "get", _boom)
rows = meteo.fetch_and_store_forecast()
assert rows == []
def test_fetch_forecast_filters_from_today(monkeypatch):
class _FakeDate(real_date):
@classmethod
def today(cls):
return cls(2026, 2, 22)
monkeypatch.setattr(meteo, "date", _FakeDate)
monkeypatch.setattr(
meteo,
"fetch_and_store_forecast",
lambda *_args, **_kwargs: [
{"date": "2026-02-21", "x": 1},
{"date": "2026-02-22", "x": 2},
{"date": "2026-02-23", "x": 3},
],
)
out = meteo.fetch_forecast(days=14)
assert [d["date"] for d in out["days"]] == ["2026-02-22", "2026-02-23"]

View File

@@ -16,3 +16,15 @@ def test_delete_tool(client):
id = r.json()["id"] id = r.json()["id"]
r2 = client.delete(f"/api/tools/{id}") r2 = client.delete(f"/api/tools/{id}")
assert r2.status_code == 204 assert r2.status_code == 204
def test_tool_with_video_url(client):
r = client.post(
"/api/tools",
json={
"nom": "Tarière",
"video_url": "/uploads/demo-outil.mp4",
},
)
assert r.status_code == 201
assert r.json()["video_url"] == "/uploads/demo-outil.mp4"

105
codex.md Normal file
View File

@@ -0,0 +1,105 @@
# Codex - Elements développes
Ce document liste les éléments développés dans le projet `jardin`.
## 1) Calendrier lunaire
- Script principal: `calendrier_lunaire/lunar_calendar.py`
- Tests: `calendrier_lunaire/test_lunar_calendar.py`
- Sorties JSON générées:
- `calendrier_lunaire/calendrier_lunaire_2026.json`
- `calendrier_lunaire/calendrier_lunaire_2027.json`
- Données/ressources:
- `calendrier_lunaire/de421.bsp`
- `calendrier_lunaire/deep_search.md`
- `calendrier_lunaire/deep_search1.md`
### Fonctions/évolutions intégrées
- Calcul des phases lunaires (nouvelle lune, quartiers, pleine lune)
- Génération annuelle en JSON
- Ajout des données saints du jour
- Ajout lever/coucher soleil et lune + durées
- Ajout transitions intra-journée (jour type / montante-descendante)
- Alignement zodiacal sidéral (constellations)
## 2) Saints et dictons
Dossier dédié: `calendrier_lunaire/saints_dictons/`
- Sources et consignes:
- `calendrier_lunaire/saints_dictons/consigne_scrap_saint_dictons.md`
- `calendrier_lunaire/saints_dictons/saints_france.json`
- Parsing:
- `calendrier_lunaire/saints_dictons/parse_saints_dictons.py`
- Scraping annuel:
- `calendrier_lunaire/saints_dictons/saint_dicton_year_scraper.py`
- Exemple de sortie:
- `calendrier_lunaire/saints_dictons/saints_2026.json`
### Fonctions/évolutions intégrées
- Format JSON cible: `date`, `saints[]`, `dictons[]`
- Support de formats de date multiples
- Ajout de logs de progression dans le scraper
- Enregistrement JSON (pas uniquement affichage terminal)
## 3) Prévisions météo Open-Meteo
- Script: `prevision meteo/open_meteo_garden_forecast.py`
- Consignes:
- `prevision meteo/consigne.md`
- `prevision meteo/consigne_open_meteo.md`
- Mapping WMO:
- `prevision meteo/wmo_code.json`
- Exemple de sortie:
- `prevision meteo/prevision meteo/output/forecast.json`
### Fonctions/évolutions intégrées
- Appel Open-Meteo avec variables hourly/current étendues
- Intégration `past_days` + `forecast_days`
- Affichage tableau synthétique
- Export JSON complet
- Correction de sérialisation JSON
## 4) Station météo locale (WeeWX)
- Script: `station_meteo/local_station_weather.py`
### Fonctions/évolutions intégrées
- Récupération des données actuelles (RSS)
- Récupération et parsing des résumés quotidiens
- Récupération de données journalières par date via option CLI
- Valeur par défaut: date de la veille si non fournie
- Normalisation des types (float/int)
- Structure JSON clarifiée: suppression de `yesterday`, ajout `day_data.date` (date complète)
- Enrichissement des blocs: `current`, `stats_today`, `astrology`, `station_info`
## 5) YOLO - Détection feuille/plante
Dossier: `test_yolo/`
- Script test: `test_yolo/test_yolo_leaf.py`
- Documentation: `test_yolo/README.md`
- Données images: `test_yolo/image/`
- Sorties:
- `test_yolo/test_yolo/output/detections.json`
- `test_yolo/test_yolo/output/annotated.jpg`
### Fonctions/évolutions intégrées
- Migration vers `ultralytics` (sans `ultralyticsplus`)
- Support modèle local ou repo Hugging Face (`best.pt`)
- Sortie JSON des détections
- Génération image annotée
- Traduction des labels vers le français (`class_name_fr`)
## 6) Assets icônes
- Icônes lune: `icons/moon/*.svg`
- `new_moon.svg`, `waxing_crescent.svg`, `first_quarter.svg`, `waxing_gibbous.svg`, `full_moon.svg`, `waning_gibbous.svg`, `last_quarter.svg`, `waning_crescent.svg`
- Icônes météo: `icons/weather/*.svg`
- Codes WMO usuels + `risque_canicule.svg` + `risque_gèle.svg`
## 7) Notes de pilotage
- Plan d'amélioration: `amelioration.md`
- Plan météo/astuces: `avancement.md` (contient plan + logs de session)

BIN
data/jardin.db Normal file → Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,134 @@
# Utilisation API en Réseau Local (VM / OpenClaw)
Ce guide explique comment exposer et consommer l'API backend Jardin depuis des machines VM de votre réseau local, y compris un poste équipé d'OpenClaw.
## 1. Vérifier l'accessibilité API sur le LAN
Le backend écoute sur le port `8060` via Docker Compose.
Depuis la machine hôte:
```bash
curl http://127.0.0.1:8060/api/health
```
Depuis une VM du réseau local (remplacez `192.168.1.50`):
```bash
curl http://192.168.1.50:8060/api/health
```
Réponse attendue:
```json
{"status":"ok"}
```
Si ça ne répond pas:
- vérifier que Docker est démarré
- vérifier que le pare-feu autorise `8060/tcp`
- vérifier l'IP LAN de l'hôte
## 2. CORS pour clients web distants (OpenClaw UI navigateur)
Si l'appel API est fait côté navigateur depuis une autre origine (ex: UI OpenClaw sur une VM), il faut autoriser cette origine dans `CORS_ORIGINS`.
Exemple `.env`:
```env
CORS_ORIGINS=http://localhost:5173,http://localhost:8061,http://192.168.1.80:3000
```
Puis redémarrer:
```bash
docker compose up -d --build backend
```
Note:
- appel serveur-à-serveur: CORS non requis
- appel navigateur: CORS requis
## 3. Endpoints utiles pour automatisation VM/OpenClaw
Santé:
```bash
curl http://192.168.1.50:8060/api/health
```
Météo tableau:
```bash
curl "http://192.168.1.50:8060/api/meteo/tableau?center_date=2026-02-22&span=15"
```
Rafraîchir jobs météo:
```bash
curl -X POST http://192.168.1.50:8060/api/meteo/refresh
```
Lire réglages:
```bash
curl http://192.168.1.50:8060/api/settings
```
Activer debug UI:
```bash
curl -X PUT http://192.168.1.50:8060/api/settings \
-H "Content-Type: application/json" \
-d '{"debug_mode":"1"}'
```
Stats debug backend (CPU/RAM/disque conteneur):
```bash
curl http://192.168.1.50:8060/api/settings/debug/system
```
Upload fichier:
```bash
curl -X POST http://192.168.1.50:8060/api/upload \
-F "file=@/chemin/fichier.mp4"
```
## 4. Scripts de mise à jour BDD (hors webapp)
Station locale -> DB:
```bash
python3 station_meteo/update_station_db.py
```
Historique Open-Meteo -> DB:
```bash
python3 station_meteo/update_openmeteo_history_db.py --start-date 2026-01-01 --end-date 2026-02-22
```
## 5. Paramètres recommandés pour OpenClaw
- Base URL API: `http://192.168.1.50:8060`
- Endpoint test: `/api/health`
- Timeout conseillé: `15-30s`
- Corps JSON: UTF-8
- Pour upload: `multipart/form-data`
## 6. Sécurité (important)
L'API actuelle est sans authentification. Sur réseau local, minimum recommandé:
- segmenter le réseau (VLAN/VM dédiées)
- filtrer par IP source (pare-feu hôte)
- ne pas exposer directement sur Internet
- idéalement: reverse proxy + authentification (Basic/Auth token) ou VPN
## 7. Swagger
Documentation interactive:
- `http://<IP_HOTE>:8060/docs`

View File

@@ -3,7 +3,7 @@ server {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
client_max_body_size 20M; client_max_body_size 200M;
location /api/ { location /api/ {
proxy_pass http://backend:8060; proxy_pass http://backend:8060;

View File

@@ -1,4 +1,14 @@
<template> <template>
<div
v-if="debugMode"
class="fixed top-2 right-2 z-[70] bg-bg-hard/95 border border-bg-soft rounded-lg px-3 py-1.5 text-[11px] text-text-muted shadow-lg"
>
<span class="text-aqua font-semibold mr-2">DEBUG</span>
<span class="mr-2">CPU {{ debugCpuLabel }}</span>
<span class="mr-2">RAM {{ debugMemLabel }}</span>
<span>Disk {{ debugDiskLabel }}</span>
</div>
<!-- Mobile: header + drawer --> <!-- Mobile: header + drawer -->
<AppHeader class="lg:hidden" @toggle-drawer="drawerOpen = !drawerOpen" /> <AppHeader class="lg:hidden" @toggle-drawer="drawerOpen = !drawerOpen" />
<AppDrawer :open="drawerOpen" @close="drawerOpen = false" /> <AppDrawer :open="drawerOpen" @close="drawerOpen = false" />
@@ -35,12 +45,132 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router'
import AppHeader from '@/components/AppHeader.vue' import AppHeader from '@/components/AppHeader.vue'
import AppDrawer from '@/components/AppDrawer.vue' import AppDrawer from '@/components/AppDrawer.vue'
import { meteoApi } from '@/api/meteo'
import { settingsApi, type DebugSystemStats } from '@/api/settings'
const drawerOpen = ref(false) const drawerOpen = ref(false)
const debugMode = ref(localStorage.getItem('debug_mode') === '1')
const debugStats = ref<DebugSystemStats | null>(null)
let debugTimer: number | null = null
function prefetchMeteoInBackground() {
// Préchargement du chunk route + données pour accélérer l'ouverture de /meteo
void import('@/views/CalendrierView.vue')
void meteoApi.preloadForMeteoView()
}
function toBool(value: unknown): boolean {
if (typeof value === 'boolean') return value
const s = String(value ?? '').toLowerCase().trim()
return s === '1' || s === 'true' || s === 'yes' || s === 'on'
}
function formatBytes(value?: number | null): string {
if (value == null || Number.isNaN(value)) return '—'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let n = value
let i = 0
while (n >= 1024 && i < units.length - 1) {
n /= 1024
i += 1
}
return `${n.toFixed(n >= 100 ? 0 : n >= 10 ? 1 : 2)}${units[i]}`
}
const debugCpuLabel = computed(() => {
const pct = debugStats.value?.cpu?.used_pct
return pct != null ? `${pct.toFixed(1)}%` : '—'
})
const debugMemLabel = computed(() => {
const used = debugStats.value?.memory?.used_bytes
const pct = debugStats.value?.memory?.used_pct
if (used == null) return '—'
return pct != null ? `${formatBytes(used)} (${pct.toFixed(1)}%)` : formatBytes(used)
})
const debugDiskLabel = computed(() => {
const used = debugStats.value?.disk?.used_bytes
const pct = debugStats.value?.disk?.used_pct
if (used == null) return '—'
return pct != null ? `${formatBytes(used)} (${pct.toFixed(1)}%)` : formatBytes(used)
})
async function fetchDebugStats() {
try {
debugStats.value = await settingsApi.getDebugSystemStats()
} catch {
debugStats.value = null
}
}
function stopDebugPolling() {
if (debugTimer != null) {
window.clearInterval(debugTimer)
debugTimer = null
}
}
function startDebugPolling() {
stopDebugPolling()
void fetchDebugStats()
debugTimer = window.setInterval(() => {
void fetchDebugStats()
}, 10000)
}
async function loadDebugModeFromApi() {
try {
const data = await settingsApi.get()
debugMode.value = toBool(data.debug_mode)
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0')
} catch {
// On garde la valeur locale.
}
}
function handleSettingsUpdated(event: Event) {
const ce = event as CustomEvent<{ debug_mode?: boolean | string }>
if (!ce.detail || ce.detail.debug_mode == null) return
debugMode.value = toBool(ce.detail.debug_mode)
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0')
}
function handleStorage(event: StorageEvent) {
if (event.key !== 'debug_mode') return
debugMode.value = event.newValue === '1'
}
onMounted(() => {
const ric = (window as Window & { requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number }).requestIdleCallback
if (ric) {
ric(() => prefetchMeteoInBackground(), { timeout: 1500 })
} else {
window.setTimeout(prefetchMeteoInBackground, 500)
}
void loadDebugModeFromApi()
window.addEventListener('settings-updated', handleSettingsUpdated as EventListener)
window.addEventListener('storage', handleStorage)
})
watch(debugMode, (enabled) => {
if (enabled) startDebugPolling()
else {
stopDebugPolling()
debugStats.value = null
}
}, { immediate: true })
onBeforeUnmount(() => {
stopDebugPolling()
window.removeEventListener('settings-updated', handleSettingsUpdated as EventListener)
window.removeEventListener('storage', handleStorage)
})
const links = [ const links = [
{ to: '/', label: 'Dashboard', icon: '🏠' }, { to: '/', label: 'Dashboard', icon: '🏠' },
@@ -51,7 +181,8 @@ const links = [
{ to: '/plantations', label: 'Plantations', icon: '🥕' }, { to: '/plantations', label: 'Plantations', icon: '🥕' },
{ to: '/taches', label: 'Tâches', icon: '✅' }, { to: '/taches', label: 'Tâches', icon: '✅' },
{ to: '/planning', label: 'Planning', icon: '📆' }, { to: '/planning', label: 'Planning', icon: '📆' },
{ to: '/calendrier', label: 'Calendrier', icon: '🌙' }, { to: '/meteo', label: 'Météo', icon: '🌦️' },
{ to: '/astuces', label: 'Astuces', icon: '💡' },
{ to: '/reglages', label: 'Réglages', icon: '⚙️' }, { to: '/reglages', label: 'Réglages', icon: '⚙️' },
] ]
</script> </script>

View File

@@ -1,24 +1,158 @@
/// <reference types="../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" /> /// <reference types="../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { ref } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { RouterLink, RouterView } from 'vue-router'; import { RouterLink, RouterView } from 'vue-router';
import AppHeader from '@/components/AppHeader.vue'; import AppHeader from '@/components/AppHeader.vue';
import AppDrawer from '@/components/AppDrawer.vue'; import AppDrawer from '@/components/AppDrawer.vue';
import { meteoApi } from '@/api/meteo';
import { settingsApi } from '@/api/settings';
const drawerOpen = ref(false); const drawerOpen = ref(false);
const debugMode = ref(localStorage.getItem('debug_mode') === '1');
const debugStats = ref(null);
let debugTimer = null;
function prefetchMeteoInBackground() {
// Préchargement du chunk route + données pour accélérer l'ouverture de /meteo
void import('@/views/CalendrierView.vue');
void meteoApi.preloadForMeteoView();
}
function toBool(value) {
if (typeof value === 'boolean')
return value;
const s = String(value ?? '').toLowerCase().trim();
return s === '1' || s === 'true' || s === 'yes' || s === 'on';
}
function formatBytes(value) {
if (value == null || Number.isNaN(value))
return '—';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let n = value;
let i = 0;
while (n >= 1024 && i < units.length - 1) {
n /= 1024;
i += 1;
}
return `${n.toFixed(n >= 100 ? 0 : n >= 10 ? 1 : 2)}${units[i]}`;
}
const debugCpuLabel = computed(() => {
const pct = debugStats.value?.cpu?.used_pct;
return pct != null ? `${pct.toFixed(1)}%` : '—';
});
const debugMemLabel = computed(() => {
const used = debugStats.value?.memory?.used_bytes;
const pct = debugStats.value?.memory?.used_pct;
if (used == null)
return '—';
return pct != null ? `${formatBytes(used)} (${pct.toFixed(1)}%)` : formatBytes(used);
});
const debugDiskLabel = computed(() => {
const used = debugStats.value?.disk?.used_bytes;
const pct = debugStats.value?.disk?.used_pct;
if (used == null)
return '—';
return pct != null ? `${formatBytes(used)} (${pct.toFixed(1)}%)` : formatBytes(used);
});
async function fetchDebugStats() {
try {
debugStats.value = await settingsApi.getDebugSystemStats();
}
catch {
debugStats.value = null;
}
}
function stopDebugPolling() {
if (debugTimer != null) {
window.clearInterval(debugTimer);
debugTimer = null;
}
}
function startDebugPolling() {
stopDebugPolling();
void fetchDebugStats();
debugTimer = window.setInterval(() => {
void fetchDebugStats();
}, 10000);
}
async function loadDebugModeFromApi() {
try {
const data = await settingsApi.get();
debugMode.value = toBool(data.debug_mode);
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0');
}
catch {
// On garde la valeur locale.
}
}
function handleSettingsUpdated(event) {
const ce = event;
if (!ce.detail || ce.detail.debug_mode == null)
return;
debugMode.value = toBool(ce.detail.debug_mode);
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0');
}
function handleStorage(event) {
if (event.key !== 'debug_mode')
return;
debugMode.value = event.newValue === '1';
}
onMounted(() => {
const ric = window.requestIdleCallback;
if (ric) {
ric(() => prefetchMeteoInBackground(), { timeout: 1500 });
}
else {
window.setTimeout(prefetchMeteoInBackground, 500);
}
void loadDebugModeFromApi();
window.addEventListener('settings-updated', handleSettingsUpdated);
window.addEventListener('storage', handleStorage);
});
watch(debugMode, (enabled) => {
if (enabled)
startDebugPolling();
else {
stopDebugPolling();
debugStats.value = null;
}
}, { immediate: true });
onBeforeUnmount(() => {
stopDebugPolling();
window.removeEventListener('settings-updated', handleSettingsUpdated);
window.removeEventListener('storage', handleStorage);
});
const links = [ const links = [
{ to: '/', label: 'Dashboard', icon: '🏠' }, { to: '/', label: 'Dashboard', icon: '🏠' },
{ to: '/jardins', label: 'Jardins', icon: '🪴' }, { to: '/jardins', label: 'Jardins', icon: '🪴' },
{ to: '/plantes', label: 'Plantes', icon: '🌱' }, { to: '/plantes', label: 'Plantes', icon: '🌱' },
{ to: '/bibliotheque', label: 'Bibliothèque', icon: '📷' },
{ to: '/outils', label: 'Outils', icon: '🔧' }, { to: '/outils', label: 'Outils', icon: '🔧' },
{ to: '/plantations', label: 'Plantations', icon: '🥕' }, { to: '/plantations', label: 'Plantations', icon: '🥕' },
{ to: '/taches', label: 'Tâches', icon: '✅' }, { to: '/taches', label: 'Tâches', icon: '✅' },
{ to: '/planning', label: 'Planning', icon: '📆' }, { to: '/planning', label: 'Planning', icon: '📆' },
{ to: '/calendrier', label: 'Calendrier', icon: '🌙' }, { to: '/meteo', label: 'Météo', icon: '🌦️' },
{ to: '/astuces', label: 'Astuces', icon: '💡' },
{ to: '/reglages', label: 'Réglages', icon: '⚙️' }, { to: '/reglages', label: 'Réglages', icon: '⚙️' },
]; ];
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {}; const __VLS_ctx = {};
let __VLS_components; let __VLS_components;
let __VLS_directives; let __VLS_directives;
if (__VLS_ctx.debugMode) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "fixed top-2 right-2 z-[70] bg-bg-hard/95 border border-bg-soft rounded-lg px-3 py-1.5 text-[11px] text-text-muted shadow-lg" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-aqua font-semibold mr-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "mr-2" },
});
(__VLS_ctx.debugCpuLabel);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "mr-2" },
});
(__VLS_ctx.debugMemLabel);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.debugDiskLabel);
}
/** @type {[typeof AppHeader, ]} */ ; /** @type {[typeof AppHeader, ]} */ ;
// @ts-ignore // @ts-ignore
const __VLS_0 = __VLS_asFunctionalComponent(AppHeader, new AppHeader({ const __VLS_0 = __VLS_asFunctionalComponent(AppHeader, new AppHeader({
@@ -119,6 +253,24 @@ const __VLS_22 = {}.RouterView;
// @ts-ignore // @ts-ignore
const __VLS_23 = __VLS_asFunctionalComponent(__VLS_22, new __VLS_22({})); const __VLS_23 = __VLS_asFunctionalComponent(__VLS_22, new __VLS_22({}));
const __VLS_24 = __VLS_23({}, ...__VLS_functionalComponentArgsRest(__VLS_23)); const __VLS_24 = __VLS_23({}, ...__VLS_functionalComponentArgsRest(__VLS_23));
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['top-2']} */ ;
/** @type {__VLS_StyleScopedClasses['right-2']} */ ;
/** @type {__VLS_StyleScopedClasses['z-[70]']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard/95']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-aqua']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:hidden']} */ ; /** @type {__VLS_StyleScopedClasses['lg:hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:flex']} */ ; /** @type {__VLS_StyleScopedClasses['lg:flex']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ; /** @type {__VLS_StyleScopedClasses['hidden']} */ ;
@@ -183,6 +335,10 @@ const __VLS_self = (await import('vue')).defineComponent({
AppHeader: AppHeader, AppHeader: AppHeader,
AppDrawer: AppDrawer, AppDrawer: AppDrawer,
drawerOpen: drawerOpen, drawerOpen: drawerOpen,
debugMode: debugMode,
debugCpuLabel: debugCpuLabel,
debugMemLabel: debugMemLabel,
debugDiskLabel: debugDiskLabel,
links: links, links: links,
}; };
}, },

View File

@@ -0,0 +1,8 @@
import client from './client';
export const astucesApi = {
list: (params) => client.get('/api/astuces', { params }).then(r => r.data),
get: (id) => client.get(`/api/astuces/${id}`).then(r => r.data),
create: (a) => client.post('/api/astuces', a).then(r => r.data),
update: (id, a) => client.put(`/api/astuces/${id}`, a).then(r => r.data),
remove: (id) => client.delete(`/api/astuces/${id}`),
};

View File

@@ -0,0 +1,31 @@
import client from './client'
export interface Astuce {
id?: number
titre: string
contenu: string
categorie?: string
tags?: string
mois?: string
photos?: string
videos?: string
source?: string
created_at?: string
}
export const astucesApi = {
list: (params?: { categorie?: string; mois?: number; tag?: string }) =>
client.get<Astuce[]>('/api/astuces', { params }).then(r => r.data),
get: (id: number) =>
client.get<Astuce>(`/api/astuces/${id}`).then(r => r.data),
create: (a: Omit<Astuce, 'id' | 'created_at'>) =>
client.post<Astuce>('/api/astuces', a).then(r => r.data),
update: (id: number, a: Partial<Astuce>) =>
client.put<Astuce>(`/api/astuces/${id}`, a).then(r => r.data),
remove: (id: number) =>
client.delete(`/api/astuces/${id}`),
}

View File

@@ -4,6 +4,11 @@ export const gardensApi = {
get: (id) => client.get(`/api/gardens/${id}`).then(r => r.data), get: (id) => client.get(`/api/gardens/${id}`).then(r => r.data),
create: (g) => client.post('/api/gardens', g).then(r => r.data), create: (g) => client.post('/api/gardens', g).then(r => r.data),
update: (id, g) => client.put(`/api/gardens/${id}`, g).then(r => r.data), update: (id, g) => client.put(`/api/gardens/${id}`, g).then(r => r.data),
uploadPhoto: (id, file) => {
const fd = new FormData();
fd.append('file', file);
return client.post(`/api/gardens/${id}/photo`, fd).then(r => r.data);
},
delete: (id) => client.delete(`/api/gardens/${id}`), delete: (id) => client.delete(`/api/gardens/${id}`),
cells: (id) => client.get(`/api/gardens/${id}/cells`).then(r => r.data), cells: (id) => client.get(`/api/gardens/${id}/cells`).then(r => r.data),
measurements: (id) => client.get(`/api/gardens/${id}/measurements`).then(r => r.data), measurements: (id) => client.get(`/api/gardens/${id}/measurements`).then(r => r.data),

View File

@@ -5,8 +5,16 @@ export interface Garden {
nom: string nom: string
description?: string description?: string
type: string type: string
longueur_m?: number
largeur_m?: number
surface_m2?: number
carre_potager?: boolean
carre_x_cm?: number
carre_y_cm?: number
photo_parcelle?: string
latitude?: number latitude?: number
longitude?: number longitude?: number
altitude?: number
adresse?: string adresse?: string
exposition?: string exposition?: string
ombre?: string ombre?: string
@@ -41,6 +49,11 @@ export const gardensApi = {
get: (id: number) => client.get<Garden>(`/api/gardens/${id}`).then(r => r.data), get: (id: number) => client.get<Garden>(`/api/gardens/${id}`).then(r => r.data),
create: (g: Partial<Garden>) => client.post<Garden>('/api/gardens', g).then(r => r.data), create: (g: Partial<Garden>) => client.post<Garden>('/api/gardens', g).then(r => r.data),
update: (id: number, g: Partial<Garden>) => client.put<Garden>(`/api/gardens/${id}`, g).then(r => r.data), update: (id: number, g: Partial<Garden>) => client.put<Garden>(`/api/gardens/${id}`, g).then(r => r.data),
uploadPhoto: (id: number, file: File) => {
const fd = new FormData()
fd.append('file', file)
return client.post<Garden>(`/api/gardens/${id}/photo`, fd).then(r => r.data)
},
delete: (id: number) => client.delete(`/api/gardens/${id}`), delete: (id: number) => client.delete(`/api/gardens/${id}`),
cells: (id: number) => client.get<GardenCell[]>(`/api/gardens/${id}/cells`).then(r => r.data), cells: (id: number) => client.get<GardenCell[]>(`/api/gardens/${id}/cells`).then(r => r.data),
measurements: (id: number) => client.get<Measurement[]>(`/api/gardens/${id}/measurements`).then(r => r.data), measurements: (id: number) => client.get<Measurement[]>(`/api/gardens/${id}/measurements`).then(r => r.data),

View File

@@ -8,6 +8,7 @@ export interface LunarDay {
montante_descendante: string montante_descendante: string
signe: string signe: string
type_jour: string type_jour: string
saint_du_jour?: string
perigee: boolean perigee: boolean
apogee: boolean apogee: boolean
noeud_lunaire: boolean noeud_lunaire: boolean

View File

@@ -1,4 +1,60 @@
import client from './client'; import client from './client';
const CACHE_TTL_MS = 5 * 60 * 1000;
const cache = new Map();
const inflight = new Map();
function todayIso() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function cacheKeyTableau(params) {
const center = params?.center_date || '';
const span = params?.span ?? '';
return `tableau:${center}:${span}`;
}
function getCached(key) {
const entry = cache.get(key);
if (!entry)
return null;
if (Date.now() > entry.expires_at) {
cache.delete(key);
return null;
}
return entry.value;
}
function setCached(key, value) {
cache.set(key, { value, expires_at: Date.now() + CACHE_TTL_MS });
}
function fetchWithCache(key, loader) {
const cached = getCached(key);
if (cached != null)
return Promise.resolve(cached);
const pending = inflight.get(key);
if (pending)
return pending;
const p = loader()
.then((value) => {
setCached(key, value);
return value;
})
.finally(() => {
inflight.delete(key);
});
inflight.set(key, p);
return p;
}
export const meteoApi = { export const meteoApi = {
getForecast: (days = 14) => client.get('/api/meteo', { params: { days } }).then(r => r.data), getForecast: (days = 14) => fetchWithCache(`forecast:${days}`, () => client.get('/api/meteo', { params: { days } }).then(r => r.data)),
getTableau: (params) => fetchWithCache(cacheKeyTableau(params), () => client.get('/api/meteo/tableau', { params }).then(r => r.data)),
getStationCurrent: () => fetchWithCache('station-current', () => client.get('/api/meteo/station/current').then(r => r.data)),
getPrevisions: (days = 7) => fetchWithCache(`previsions:${days}`, () => client.get('/api/meteo/previsions', { params: { days } }).then(r => r.data)),
preloadForMeteoView: (params) => Promise.all([
meteoApi.getTableau(params ?? { center_date: todayIso(), span: 15 }),
meteoApi.getStationCurrent(),
meteoApi.getPrevisions(7),
]).then(() => undefined),
clearCache: () => {
cache.clear();
inflight.clear();
},
refresh: () => client.post('/api/meteo/refresh').then(r => r.data),
}; };

View File

@@ -11,6 +11,134 @@ export interface MeteoDay {
icone: string icone: string
} }
export const meteoApi = { export interface StationCurrent {
getForecast: (days = 14) => client.get<{ days: MeteoDay[] }>('/api/meteo', { params: { days } }).then(r => r.data), temp_ext?: number
humidite?: number
pression?: number
pluie_mm?: number
vent_kmh?: number
vent_dir?: string
uv?: number
solaire?: number
date_heure?: string
}
export interface StationDay {
t_min?: number
t_max?: number
pluie_mm?: number
vent_kmh?: number
humidite?: number
}
export interface OpenMeteoDay {
date?: string
t_min?: number
t_max?: number
pluie_mm?: number
vent_kmh?: number
wmo?: number
label?: string
humidite_moy?: number
sol_0cm?: number
etp_mm?: number
}
export interface TableauRow {
date: string
type: 'passe' | 'aujourd_hui' | 'futur'
station: StationDay | StationCurrent | null
open_meteo: OpenMeteoDay | null
}
const CACHE_TTL_MS = 5 * 60 * 1000
type CacheEntry<T> = {
value: T
expires_at: number
}
const cache = new Map<string, CacheEntry<unknown>>()
const inflight = new Map<string, Promise<unknown>>()
function todayIso(): string {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
function cacheKeyTableau(params?: { center_date?: string; span?: number }): string {
const center = params?.center_date || ''
const span = params?.span ?? ''
return `tableau:${center}:${span}`
}
function getCached<T>(key: string): T | null {
const entry = cache.get(key)
if (!entry) return null
if (Date.now() > entry.expires_at) {
cache.delete(key)
return null
}
return entry.value as T
}
function setCached<T>(key: string, value: T): void {
cache.set(key, { value, expires_at: Date.now() + CACHE_TTL_MS })
}
function fetchWithCache<T>(key: string, loader: () => Promise<T>): Promise<T> {
const cached = getCached<T>(key)
if (cached != null) return Promise.resolve(cached)
const pending = inflight.get(key)
if (pending) return pending as Promise<T>
const p = loader()
.then((value) => {
setCached(key, value)
return value
})
.finally(() => {
inflight.delete(key)
})
inflight.set(key, p as Promise<unknown>)
return p
}
export const meteoApi = {
getForecast: (days = 14) =>
fetchWithCache(`forecast:${days}`, () =>
client.get<{ days: MeteoDay[] }>('/api/meteo', { params: { days } }).then(r => r.data),
),
getTableau: (params?: { center_date?: string; span?: number }) =>
fetchWithCache(cacheKeyTableau(params), () =>
client.get<{ rows: TableauRow[] }>('/api/meteo/tableau', { params }).then(r => r.data),
),
getStationCurrent: () =>
fetchWithCache('station-current', () =>
client.get<StationCurrent | null>('/api/meteo/station/current').then(r => r.data),
),
getPrevisions: (days = 7) =>
fetchWithCache(`previsions:${days}`, () =>
client.get<{ days: OpenMeteoDay[] }>('/api/meteo/previsions', { params: { days } }).then(r => r.data),
),
preloadForMeteoView: (params?: { center_date?: string; span?: number }) =>
Promise.all([
meteoApi.getTableau(params ?? { center_date: todayIso(), span: 15 }),
meteoApi.getStationCurrent(),
meteoApi.getPrevisions(7),
]).then(() => undefined),
clearCache: () => {
cache.clear()
inflight.clear()
},
refresh: () =>
client.post('/api/meteo/refresh').then(r => r.data),
} }

View File

@@ -8,6 +8,10 @@ export interface Planting {
date_plantation?: string date_plantation?: string
quantite: number quantite: number
statut: string statut: string
boutique_nom?: string
boutique_url?: string
tarif_achat?: number
date_achat?: string
notes?: string notes?: string
} }

View File

@@ -0,0 +1,6 @@
import client from './client';
export const settingsApi = {
get: () => client.get('/api/settings').then(r => r.data),
update: (settings) => client.put('/api/settings', settings).then(r => r.data),
getDebugSystemStats: () => client.get('/api/settings/debug/system').then(r => r.data),
};

View File

@@ -0,0 +1,33 @@
import client from './client'
export type SettingsMap = Record<string, string>
export interface DebugSystemStats {
source: string
cpu: {
usage_usec_total?: number | null
quota_cores?: number | null
used_pct?: number | null
}
memory: {
used_bytes?: number | null
limit_bytes?: number | null
used_pct?: number | null
}
disk: {
path?: string
total_bytes?: number | null
used_bytes?: number | null
free_bytes?: number | null
used_pct?: number | null
uploads_bytes?: number | null
}
}
export const settingsApi = {
get: () => client.get<SettingsMap>('/api/settings').then(r => r.data),
update: (settings: Record<string, string | number | boolean>) =>
client.put<{ ok: boolean }>('/api/settings', settings).then(r => r.data),
getDebugSystemStats: () =>
client.get<DebugSystemStats>('/api/settings/debug/system').then(r => r.data),
}

View File

@@ -7,6 +7,9 @@ export interface Task {
garden_id?: number garden_id?: number
priorite: string priorite: string
echeance?: string echeance?: string
recurrence?: string | null
frequence_jours?: number | null
date_prochaine?: string | null
statut: string statut: string
} }

View File

@@ -6,6 +6,11 @@ export interface Tool {
description?: string description?: string
categorie?: string categorie?: string
photo_url?: string photo_url?: string
video_url?: string
notice_fichier_url?: string
boutique_nom?: string
boutique_url?: string
prix_achat?: number
} }
export const toolsApi = { export const toolsApi = {

View File

@@ -1,6 +1,6 @@
<template> <template>
<Transition name="slide"> <Transition name="slide">
<div v-if="open" class="fixed inset-0 z-40 flex md:hidden" @click.self="$emit('close')"> <div v-if="open" class="fixed inset-0 z-40 flex lg:hidden" @click.self="$emit('close')">
<nav class="bg-bg-hard w-64 h-full p-6 flex flex-col gap-1 border-r border-bg-soft shadow-2xl"> <nav class="bg-bg-hard w-64 h-full p-6 flex flex-col gap-1 border-r border-bg-soft shadow-2xl">
<span class="text-green font-bold text-xl mb-6">🌿 Jardin</span> <span class="text-green font-bold text-xl mb-6">🌿 Jardin</span>
<RouterLink <RouterLink
@@ -27,7 +27,8 @@ const links = [
{ to: '/plantations', label: 'Plantations' }, { to: '/plantations', label: 'Plantations' },
{ to: '/taches', label: 'Tâches' }, { to: '/taches', label: 'Tâches' },
{ to: '/planning', label: 'Planning' }, { to: '/planning', label: 'Planning' },
{ to: '/calendrier', label: 'Calendrier' }, { to: '/meteo', label: 'Météo' },
{ to: '/astuces', label: 'Astuces' },
{ to: '/reglages', label: 'Réglages' }, { to: '/reglages', label: 'Réglages' },
] ]
</script> </script>

View File

@@ -5,11 +5,13 @@ const links = [
{ to: '/', label: 'Dashboard' }, { to: '/', label: 'Dashboard' },
{ to: '/jardins', label: 'Jardins' }, { to: '/jardins', label: 'Jardins' },
{ to: '/plantes', label: 'Plantes' }, { to: '/plantes', label: 'Plantes' },
{ to: '/bibliotheque', label: '📷 Bibliothèque' },
{ to: '/outils', label: 'Outils' }, { to: '/outils', label: 'Outils' },
{ to: '/plantations', label: 'Plantations' }, { to: '/plantations', label: 'Plantations' },
{ to: '/taches', label: 'Tâches' }, { to: '/taches', label: 'Tâches' },
{ to: '/planning', label: 'Planning' }, { to: '/planning', label: 'Planning' },
{ to: '/calendrier', label: 'Calendrier' }, { to: '/meteo', label: 'Météo' },
{ to: '/astuces', label: 'Astuces' },
{ to: '/reglages', label: 'Réglages' }, { to: '/reglages', label: 'Réglages' },
]; ];
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
@@ -35,7 +37,7 @@ if (__VLS_ctx.open) {
return; return;
__VLS_ctx.$emit('close'); __VLS_ctx.$emit('close');
} }, } },
...{ class: "fixed inset-0 z-40 flex md:hidden" }, ...{ class: "fixed inset-0 z-40 flex lg:hidden" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.nav, __VLS_intrinsicElements.nav)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.nav, __VLS_intrinsicElements.nav)({
...{ class: "bg-bg-hard w-64 h-full p-6 flex flex-col gap-1 border-r border-bg-soft shadow-2xl" }, ...{ class: "bg-bg-hard w-64 h-full p-6 flex flex-col gap-1 border-r border-bg-soft shadow-2xl" },
@@ -81,7 +83,7 @@ var __VLS_3;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ; /** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['z-40']} */ ; /** @type {__VLS_StyleScopedClasses['z-40']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['md:hidden']} */ ; /** @type {__VLS_StyleScopedClasses['lg:hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ; /** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['w-64']} */ ; /** @type {__VLS_StyleScopedClasses['w-64']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ; /** @type {__VLS_StyleScopedClasses['h-full']} */ ;

View File

@@ -0,0 +1,240 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { ref, onMounted } from 'vue';
import axios from 'axios';
const props = defineProps();
const medias = ref([]);
const loading = ref(false);
const lightbox = ref(null);
async function fetchMedias() {
loading.value = true;
try {
const r = await axios.get('/api/media', {
params: { entity_type: props.entityType, entity_id: props.entityId },
});
medias.value = r.data;
}
finally {
loading.value = false;
}
}
async function onUpload(e) {
const file = e.target.files?.[0];
if (!file)
return;
const fd = new FormData();
fd.append('file', file);
const { data: uploaded } = await axios.post('/api/upload', fd);
await axios.post('/api/media', {
entity_type: props.entityType,
entity_id: props.entityId,
url: uploaded.url,
thumbnail_url: uploaded.thumbnail_url,
});
await fetchMedias();
}
async function deleteMedia(id) {
if (!confirm('Supprimer cette photo ?'))
return;
await axios.delete(`/api/media/${id}`);
medias.value = medias.value.filter((m) => m.id !== id);
}
onMounted(fetchMedias);
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text-muted text-sm" },
});
(__VLS_ctx.medias.length);
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "cursor-pointer bg-bg-soft text-text-muted hover:text-text px-3 py-1 rounded-lg text-xs border border-bg-hard transition-colors" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (__VLS_ctx.onUpload) },
type: "file",
accept: "image/*",
capture: "environment",
...{ class: "hidden" },
});
if (__VLS_ctx.loading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
});
}
else if (!__VLS_ctx.medias.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs italic py-2" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-3 gap-2" },
});
for (const [m] of __VLS_getVForSourceType((__VLS_ctx.medias))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
__VLS_ctx.lightbox = m;
} },
key: (m.id),
...{ class: "aspect-square rounded-lg overflow-hidden bg-bg-hard relative group cursor-pointer" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (m.thumbnail_url || m.url),
alt: (m.titre || ''),
...{ class: "w-full h-full object-cover" },
});
if (m.identified_common) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "absolute bottom-0 left-0 right-0 bg-black/60 text-xs text-green px-1 py-0.5 truncate" },
});
(m.identified_common);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.deleteMedia(m.id);
} },
...{ class: "absolute top-1 right-1 bg-black/60 text-red text-xs px-1 rounded opacity-0 group-hover:opacity-100 transition-opacity" },
});
}
if (__VLS_ctx.lightbox) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.lightbox = null;
} },
...{ class: "fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "max-w-lg w-full" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.lightbox.url),
...{ class: "w-full rounded-xl" },
});
if (__VLS_ctx.lightbox.identified_species) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-center mt-2 text-text-muted text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-green font-medium" },
});
(__VLS_ctx.lightbox.identified_common);
__VLS_asFunctionalElement(__VLS_intrinsicElements.em, __VLS_intrinsicElements.em)({});
(__VLS_ctx.lightbox.identified_species);
(Math.round((__VLS_ctx.lightbox.identified_confidence || 0) * 100));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.lightbox = null;
} },
...{ class: "mt-3 w-full text-text-muted text-sm hover:text-text" },
});
}
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-between']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['italic']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['aspect-square']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['group']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['bottom-0']} */ ;
/** @type {__VLS_StyleScopedClasses['left-0']} */ ;
/** @type {__VLS_StyleScopedClasses['right-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['px-1']} */ ;
/** @type {__VLS_StyleScopedClasses['py-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['top-1']} */ ;
/** @type {__VLS_StyleScopedClasses['right-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ;
/** @type {__VLS_StyleScopedClasses['text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['px-1']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['opacity-0']} */ ;
/** @type {__VLS_StyleScopedClasses['group-hover:opacity-100']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-opacity']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/80']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-3']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
medias: medias,
loading: loading,
lightbox: lightbox,
onUpload: onUpload,
deleteMedia: deleteMedia,
};
},
__typeProps: {},
});
export default (await import('vue')).defineComponent({
setup() {
return {};
},
__typeProps: {},
});
; /* PartiallyEnd: #4569/main.vue */

View File

@@ -0,0 +1,369 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { ref, onMounted } from 'vue';
import axios from 'axios';
const emit = defineEmits();
const previewUrl = ref(null);
const imageFile = ref(null);
const loading = ref(false);
const saving = ref(false);
const results = ref([]);
const source = ref('');
const selected = ref(null);
const plants = ref([]);
const linkPlantId = ref(null);
onMounted(async () => {
const { data } = await axios.get('/api/plants');
plants.value = data;
});
async function onFileSelect(e) {
const file = e.target.files?.[0];
if (!file)
return;
imageFile.value = file;
previewUrl.value = URL.createObjectURL(file);
await identify();
}
async function identify() {
loading.value = true;
results.value = [];
selected.value = null;
try {
const fd = new FormData();
fd.append('file', imageFile.value);
const { data } = await axios.post('/api/identify', fd);
results.value = data.results;
source.value = data.source;
if (results.value.length)
selected.value = 0;
}
catch {
results.value = [];
}
finally {
loading.value = false;
}
}
async function saveAndLink() {
if (imageFile.value === null || selected.value === null)
return;
const r = results.value[selected.value];
saving.value = true;
try {
const fd = new FormData();
fd.append('file', imageFile.value);
const { data: uploaded } = await axios.post('/api/upload', fd);
// Toujours sauvegarder : lié à une plante si choisie, sinon dans la bibliothèque générale
await axios.post('/api/media', {
entity_type: linkPlantId.value !== null ? 'plante' : 'bibliotheque',
entity_id: linkPlantId.value ?? 0,
url: uploaded.url,
thumbnail_url: uploaded.thumbnail_url,
identified_species: r.species,
identified_common: r.common_name,
identified_confidence: r.confidence,
identified_source: source.value,
});
emit('identified', { ...r, imageUrl: uploaded.url, plantId: linkPlantId.value });
emit('close');
}
finally {
saving.value = false;
}
}
function reset() {
previewUrl.value = null;
imageFile.value = null;
results.value = [];
selected.value = null;
}
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
__VLS_ctx.$emit('close');
} },
...{ class: "fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-bold text-lg mb-4" },
});
if (!__VLS_ctx.previewUrl) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "border-2 border-dashed border-bg-soft rounded-xl p-8 text-center mb-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-sm mb-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "cursor-pointer bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (__VLS_ctx.onFileSelect) },
type: "file",
accept: "image/*",
capture: "environment",
...{ class: "hidden" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.previewUrl),
...{ class: "w-full rounded-lg mb-4 max-h-48 object-cover" },
});
if (__VLS_ctx.loading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm text-center py-4" },
});
}
else if (__VLS_ctx.results.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-xs mb-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-yellow font-mono" },
});
(__VLS_ctx.source);
for (const [r, i] of __VLS_getVForSourceType((__VLS_ctx.results))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!!(!__VLS_ctx.previewUrl))
return;
if (!!(__VLS_ctx.loading))
return;
if (!(__VLS_ctx.results.length))
return;
__VLS_ctx.selected = i;
} },
key: (i),
...{ class: "mb-2 p-3 rounded-lg border cursor-pointer transition-colors" },
...{ class: (__VLS_ctx.selected === i
? 'border-green bg-green/10'
: 'border-bg-soft bg-bg hover:border-green/50') },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text font-medium text-sm" },
});
(r.common_name || r.species);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs italic" },
});
(r.species);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-green text-sm font-bold" },
});
(Math.round(r.confidence * 100));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-1 h-1 bg-bg-soft rounded-full overflow-hidden" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
...{ class: "h-full bg-green rounded-full transition-all" },
...{ style: ({ width: `${r.confidence * 100}%` }) },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-4 flex flex-col gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.linkPlantId),
...{ class: "w-full bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm outline-none focus:border-green" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: (null),
});
for (const [p] of __VLS_getVForSourceType((__VLS_ctx.plants))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
key: (p.id),
value: (p.id),
});
(p.nom_commun);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.saveAndLink) },
disabled: (__VLS_ctx.selected === null || __VLS_ctx.saving),
...{ class: "flex-1 bg-green text-bg py-2 rounded-lg text-sm font-semibold hover:opacity-90 disabled:opacity-40" },
});
(__VLS_ctx.saving ? 'Enregistrement...' : 'Enregistrer');
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(!__VLS_ctx.previewUrl))
return;
if (!!(__VLS_ctx.loading))
return;
if (!(__VLS_ctx.results.length))
return;
__VLS_ctx.$emit('close');
} },
...{ class: "px-4 py-2 text-text-muted hover:text-text text-sm" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm text-center py-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.reset) },
...{ class: "block mt-2 mx-auto text-green hover:underline text-xs" },
});
}
}
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/70']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-6']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-md']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['border-2']} */ ;
/** @type {__VLS_StyleScopedClasses['border-dashed']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-48']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['py-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['font-mono']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-between']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['italic']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['h-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-all']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-4']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-col']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-40']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['py-4']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
previewUrl: previewUrl,
loading: loading,
saving: saving,
results: results,
source: source,
selected: selected,
plants: plants,
linkPlantId: linkPlantId,
onFileSelect: onFileSelect,
saveAndLink: saveAndLink,
reset: reset,
};
},
__typeEmits: {},
});
export default (await import('vue')).defineComponent({
setup() {
return {};
},
__typeEmits: {},
});
; /* PartiallyEnd: #4569/main.vue */

View File

@@ -6,14 +6,17 @@ export default createRouter({
{ path: '/jardins', component: () => import('@/views/JardinsView.vue') }, { path: '/jardins', component: () => import('@/views/JardinsView.vue') },
{ path: '/jardins/:id', component: () => import('@/views/JardinDetailView.vue') }, { path: '/jardins/:id', component: () => import('@/views/JardinDetailView.vue') },
{ path: '/plantes', component: () => import('@/views/PlantesView.vue') }, { path: '/plantes', component: () => import('@/views/PlantesView.vue') },
{ path: '/bibliotheque', component: () => import('@/views/BibliothequeView.vue') },
{ path: '/outils', component: () => import('@/views/OutilsView.vue') }, { path: '/outils', component: () => import('@/views/OutilsView.vue') },
{ path: '/plantations', component: () => import('@/views/PlantationsView.vue') }, { path: '/plantations', component: () => import('@/views/PlantationsView.vue') },
{ path: '/planning', component: () => import('@/views/PlanningView.vue') }, { path: '/planning', component: () => import('@/views/PlanningView.vue') },
{ path: '/taches', component: () => import('@/views/TachesView.vue') }, { path: '/taches', component: () => import('@/views/TachesView.vue') },
{ path: '/calendrier', component: () => import('@/views/CalendrierView.vue') }, { path: '/meteo', component: () => import('@/views/CalendrierView.vue') },
{ path: '/astuces', component: () => import('@/views/AstucesView.vue') },
{ path: '/reglages', component: () => import('@/views/ReglagesView.vue') }, { path: '/reglages', component: () => import('@/views/ReglagesView.vue') },
// Redirect des anciens liens // Redirect des anciens liens
{ path: '/varietes', redirect: '/plantes' }, { path: '/varietes', redirect: '/plantes' },
{ path: '/lunaire', redirect: '/calendrier' }, { path: '/calendrier', redirect: '/meteo' },
{ path: '/lunaire', redirect: '/meteo' },
], ],
}); });

View File

@@ -12,10 +12,12 @@ export default createRouter({
{ path: '/plantations', component: () => import('@/views/PlantationsView.vue') }, { path: '/plantations', component: () => import('@/views/PlantationsView.vue') },
{ path: '/planning', component: () => import('@/views/PlanningView.vue') }, { path: '/planning', component: () => import('@/views/PlanningView.vue') },
{ path: '/taches', component: () => import('@/views/TachesView.vue') }, { path: '/taches', component: () => import('@/views/TachesView.vue') },
{ path: '/calendrier', component: () => import('@/views/CalendrierView.vue') }, { path: '/meteo', component: () => import('@/views/CalendrierView.vue') },
{ path: '/astuces', component: () => import('@/views/AstucesView.vue') },
{ path: '/reglages', component: () => import('@/views/ReglagesView.vue') }, { path: '/reglages', component: () => import('@/views/ReglagesView.vue') },
// Redirect des anciens liens // Redirect des anciens liens
{ path: '/varietes', redirect: '/plantes' }, { path: '/varietes', redirect: '/plantes' },
{ path: '/lunaire', redirect: '/calendrier' }, { path: '/calendrier', redirect: '/meteo' },
{ path: '/lunaire', redirect: '/meteo' },
], ],
}) })

View File

@@ -0,0 +1,33 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import { astucesApi } from '@/api/astuces';
export const useAstucesStore = defineStore('astuces', () => {
const astuces = ref([]);
const loading = ref(false);
async function fetchAll(params) {
loading.value = true;
try {
astuces.value = await astucesApi.list(params);
}
finally {
loading.value = false;
}
}
async function create(a) {
const created = await astucesApi.create(a);
astuces.value.unshift(created);
return created;
}
async function update(id, data) {
const updated = await astucesApi.update(id, data);
const idx = astuces.value.findIndex(x => x.id === id);
if (idx !== -1)
astuces.value[idx] = updated;
return updated;
}
async function remove(id) {
await astucesApi.remove(id);
astuces.value = astuces.value.filter(a => a.id !== id);
}
return { astuces, loading, fetchAll, create, update, remove };
});

View File

@@ -0,0 +1,37 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { astucesApi, type Astuce } from '@/api/astuces'
export const useAstucesStore = defineStore('astuces', () => {
const astuces = ref<Astuce[]>([])
const loading = ref(false)
async function fetchAll(params?: { categorie?: string; mois?: number; tag?: string }) {
loading.value = true
try {
astuces.value = await astucesApi.list(params)
} finally {
loading.value = false
}
}
async function create(a: Omit<Astuce, 'id' | 'created_at'>) {
const created = await astucesApi.create(a)
astuces.value.unshift(created)
return created
}
async function update(id: number, data: Partial<Astuce>) {
const updated = await astucesApi.update(id, data)
const idx = astuces.value.findIndex(x => x.id === id)
if (idx !== -1) astuces.value[idx] = updated
return updated
}
async function remove(id: number) {
await astucesApi.remove(id)
astuces.value = astuces.value.filter(a => a.id !== id)
}
return { astuces, loading, fetchAll, create, update, remove }
})

View File

@@ -14,9 +14,16 @@ export const useGardensStore = defineStore('gardens', () => {
gardens.value.push(created); gardens.value.push(created);
return created; return created;
} }
async function update(id, g) {
const updated = await gardensApi.update(id, g);
const idx = gardens.value.findIndex(x => x.id === id);
if (idx !== -1)
gardens.value[idx] = updated;
return updated;
}
async function remove(id) { async function remove(id) {
await gardensApi.delete(id); await gardensApi.delete(id);
gardens.value = gardens.value.filter(g => g.id !== id); gardens.value = gardens.value.filter(g => g.id !== id);
} }
return { gardens, loading, fetchAll, create, remove }; return { gardens, loading, fetchAll, create, update, remove };
}); });

View File

@@ -14,9 +14,16 @@ export const usePlantingsStore = defineStore('plantings', () => {
plantings.value.push(created); plantings.value.push(created);
return created; return created;
} }
async function update(id, p) {
const updated = await plantingsApi.update(id, p);
const idx = plantings.value.findIndex(x => x.id === id);
if (idx !== -1)
plantings.value[idx] = updated;
return updated;
}
async function remove(id) { async function remove(id) {
await plantingsApi.delete(id); await plantingsApi.delete(id);
plantings.value = plantings.value.filter(p => p.id !== id); plantings.value = plantings.value.filter(p => p.id !== id);
} }
return { plantings, loading, fetchAll, create, remove }; return { plantings, loading, fetchAll, create, update, remove };
}); });

View File

@@ -14,6 +14,13 @@ export const useTasksStore = defineStore('tasks', () => {
tasks.value.push(created); tasks.value.push(created);
return created; return created;
} }
async function update(id, data) {
const updated = await tasksApi.update(id, data);
const idx = tasks.value.findIndex(t => t.id === id);
if (idx !== -1)
tasks.value[idx] = updated;
return updated;
}
async function updateStatut(id, statut) { async function updateStatut(id, statut) {
const t = tasks.value.find(t => t.id === id); const t = tasks.value.find(t => t.id === id);
if (!t) if (!t)
@@ -25,5 +32,5 @@ export const useTasksStore = defineStore('tasks', () => {
await tasksApi.delete(id); await tasksApi.delete(id);
tasks.value = tasks.value.filter(t => t.id !== id); tasks.value = tasks.value.filter(t => t.id !== id);
} }
return { tasks, loading, fetchAll, create, updateStatut, remove }; return { tasks, loading, fetchAll, create, update, updateStatut, remove };
}); });

View File

@@ -18,9 +18,16 @@ export const useToolsStore = defineStore('tools', () => {
tools.value.push(created); tools.value.push(created);
return created; return created;
} }
async function update(id, t) {
const updated = await toolsApi.update(id, t);
const idx = tools.value.findIndex(x => x.id === id);
if (idx !== -1)
tools.value[idx] = updated;
return updated;
}
async function remove(id) { async function remove(id) {
await toolsApi.delete(id); await toolsApi.delete(id);
tools.value = tools.value.filter(t => t.id !== id); tools.value = tools.value.filter(t => t.id !== id);
} }
return { tools, loading, fetchAll, create, remove }; return { tools, loading, fetchAll, create, update, remove };
}); });

View File

@@ -0,0 +1,405 @@
<template>
<div class="p-4 max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-yellow">💡 Astuces</h1>
<button @click="openCreate" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
+ Ajouter
</button>
</div>
<div class="flex flex-wrap items-center gap-2 mb-4">
<select
v-model="filterCategorie"
class="bg-bg border border-bg-hard rounded px-3 py-1 text-text text-xs outline-none focus:border-yellow"
>
<option value="">Toutes catégories</option>
<option value="plante">Plante</option>
<option value="jardin">Jardin</option>
<option value="tache">Tâche</option>
<option value="general">Général</option>
<option value="ravageur">Ravageur</option>
<option value="maladie">Maladie</option>
</select>
<input
v-model="filterTag"
placeholder="Filtrer par tag..."
class="bg-bg border border-bg-hard rounded px-3 py-1 text-text text-xs outline-none focus:border-yellow w-44"
/>
<button
@click="filterMoisActuel = !filterMoisActuel"
:class="[
'px-3 py-1 rounded-full text-xs font-medium transition-colors border',
filterMoisActuel ? 'bg-green/20 text-green border-green/40' : 'border-bg-hard text-text-muted',
]"
>
📅 Ce mois
</button>
<button @click="refresh" class="text-xs text-text-muted hover:text-text underline ml-auto">Rafraîchir</button>
</div>
<div v-if="store.loading" class="text-text-muted text-sm">Chargement...</div>
<div v-else-if="!store.astuces.length" class="text-text-muted text-sm py-6">Aucune astuce pour ce filtre.</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div
v-for="a in store.astuces"
:key="a.id"
class="bg-bg-soft rounded-xl p-4 border border-bg-hard"
>
<div class="flex items-start justify-between gap-2 mb-2">
<h2 class="text-text font-semibold leading-tight">{{ a.titre }}</h2>
<div class="flex gap-2 shrink-0">
<button @click="openEdit(a)" class="text-yellow text-xs hover:underline">Édit.</button>
<button @click="removeAstuce(a.id)" class="text-red text-xs hover:underline">Suppr.</button>
</div>
</div>
<p class="text-text-muted text-sm whitespace-pre-line">{{ a.contenu }}</p>
<div v-if="parseMediaUrls(a.photos).length" class="mt-3 grid grid-cols-3 gap-2">
<img
v-for="(url, idx) in parseMediaUrls(a.photos)"
:key="`astuce-photo-${a.id}-${idx}`"
:src="url"
alt="photo astuce"
class="w-full h-20 object-cover rounded-md border border-bg-hard"
/>
</div>
<div v-if="parseMediaUrls(a.videos).length" class="mt-3 space-y-2">
<video
v-for="(url, idx) in parseMediaUrls(a.videos)"
:key="`astuce-video-${a.id}-${idx}`"
:src="url"
controls
muted
class="w-full rounded-md border border-bg-hard bg-black/40 max-h-52"
/>
</div>
<div class="mt-3 flex flex-wrap gap-1">
<span v-if="a.categorie" class="text-[11px] bg-yellow/15 text-yellow rounded-full px-2 py-0.5">{{ a.categorie }}</span>
<span v-for="t in parseTags(a.tags)" :key="`${a.id}-t-${t}`" class="text-[11px] bg-blue/15 text-blue rounded-full px-2 py-0.5">#{{ t }}</span>
<span v-if="parseMois(a.mois).length" class="text-[11px] bg-green/15 text-green rounded-full px-2 py-0.5">mois: {{ parseMois(a.mois).join(',') }}</span>
</div>
</div>
</div>
<div v-if="showForm" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-lg border border-bg-soft">
<h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier astuce' : 'Nouvelle astuce' }}</h2>
<form @submit.prevent="submitAstuce" class="flex flex-col gap-3">
<input
v-model="form.titre"
placeholder="Titre *"
required
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow"
/>
<textarea
v-model="form.contenu"
placeholder="Contenu *"
required
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow resize-none h-28"
/>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<select
v-model="form.categorie"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow"
>
<option value="">Catégorie</option>
<option value="plante">Plante</option>
<option value="jardin">Jardin</option>
<option value="tache">Tâche</option>
<option value="general">Général</option>
<option value="ravageur">Ravageur</option>
<option value="maladie">Maladie</option>
</select>
<input
v-model="form.source"
placeholder="Source"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow"
/>
</div>
<input
v-model="form.tagsInput"
placeholder="Tags (ex: tomate, semis, mildiou)"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow"
/>
<input
v-model="form.moisInput"
placeholder="Mois (ex: 3,4,5)"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow"
/>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<label class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm cursor-pointer text-center hover:border-yellow">
{{ uploadingPhotos ? 'Upload photos...' : 'Ajouter photo(s)' }}
<input
type="file"
accept="image/*"
multiple
class="hidden"
@change="uploadFiles($event, 'photo')"
/>
</label>
<label class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm cursor-pointer text-center hover:border-yellow">
{{ uploadingVideos ? 'Upload vidéos...' : 'Ajouter vidéo(s)' }}
<input
type="file"
accept="video/*"
multiple
class="hidden"
@change="uploadFiles($event, 'video')"
/>
</label>
</div>
<div v-if="form.photos.length" class="bg-bg border border-bg-soft rounded-lg p-2">
<div class="text-xs text-text-muted mb-1">Photos jointes</div>
<div class="grid grid-cols-3 gap-2">
<div v-for="(url, idx) in form.photos" :key="`form-photo-${idx}`" class="relative group">
<img :src="url" alt="photo astuce" class="w-full h-16 object-cover rounded border border-bg-hard" />
<button
type="button"
class="absolute top-1 right-1 hidden group-hover:block bg-red/80 text-white text-[10px] rounded px-1"
@click="removeMedia('photo', idx)"
>
</button>
</div>
</div>
</div>
<div v-if="form.videos.length" class="bg-bg border border-bg-soft rounded-lg p-2">
<div class="text-xs text-text-muted mb-1">Vidéos jointes</div>
<div class="space-y-2">
<div v-for="(url, idx) in form.videos" :key="`form-video-${idx}`" class="relative group">
<video :src="url" controls muted class="w-full max-h-36 rounded border border-bg-hard bg-black/40" />
<button
type="button"
class="absolute top-1 right-1 hidden group-hover:block bg-red/80 text-white text-[10px] rounded px-1"
@click="removeMedia('video', idx)"
>
</button>
</div>
</div>
</div>
<div class="flex gap-2 justify-end mt-1">
<button type="button" @click="closeForm" class="px-4 py-2 text-text-muted hover:text-text text-sm">Annuler</button>
<button type="submit" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
{{ editId ? 'Enregistrer' : 'Créer' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import axios from 'axios'
import { useAstucesStore } from '@/stores/astuces'
import type { Astuce } from '@/api/astuces'
const store = useAstucesStore()
const filterCategorie = ref('')
const filterTag = ref('')
const filterMoisActuel = ref(false)
const showForm = ref(false)
const editId = ref<number | null>(null)
const form = reactive({
titre: '',
contenu: '',
categorie: '',
source: '',
tagsInput: '',
moisInput: '',
photos: [] as string[],
videos: [] as string[],
})
const uploadingPhotos = ref(false)
const uploadingVideos = ref(false)
const currentMonth = computed(() => new Date().getMonth() + 1)
function parseTags(raw?: string): string[] {
if (!raw) return []
try {
const arr = JSON.parse(raw)
if (Array.isArray(arr)) return arr.map((x) => String(x).trim()).filter(Boolean)
} catch {
return raw.split(',').map((x) => x.trim()).filter(Boolean)
}
return []
}
function parseMois(raw?: string): number[] {
if (!raw) return []
try {
const arr = JSON.parse(raw)
if (Array.isArray(arr)) {
return arr
.map((x) => Number(x))
.filter((x) => Number.isInteger(x) && x >= 1 && x <= 12)
}
} catch {
return raw
.split(',')
.map((x) => Number(x.trim()))
.filter((x) => Number.isInteger(x) && x >= 1 && x <= 12)
}
return []
}
function parseMediaUrls(raw?: string): string[] {
if (!raw) return []
try {
const arr = JSON.parse(raw)
if (Array.isArray(arr)) return arr.map((x) => String(x).trim()).filter(Boolean)
} catch {
return raw
.split(',')
.map((x) => x.trim())
.filter(Boolean)
}
return []
}
function toJsonTags(input: string): string | undefined {
const tags = input
.split(',')
.map((x) => x.trim().toLowerCase())
.filter(Boolean)
return tags.length ? JSON.stringify(tags) : undefined
}
function toJsonMois(input: string): string | undefined {
const months = input
.split(',')
.map((x) => Number(x.trim()))
.filter((x) => Number.isInteger(x) && x >= 1 && x <= 12)
return months.length ? JSON.stringify(months) : undefined
}
function toJsonArray(values: string[]): string | undefined {
const clean = values.map((x) => x.trim()).filter(Boolean)
return clean.length ? JSON.stringify(clean) : undefined
}
async function uploadFiles(event: Event, kind: 'photo' | 'video') {
const files = (event.target as HTMLInputElement).files
if (!files?.length) return
if (kind === 'photo') uploadingPhotos.value = true
else uploadingVideos.value = true
try {
for (const file of Array.from(files)) {
const fd = new FormData()
fd.append('file', file)
const { data } = await axios.post<{ url: string }>('/api/upload', fd)
if (!data?.url) continue
if (kind === 'photo') form.photos.push(data.url)
else form.videos.push(data.url)
}
} finally {
if (kind === 'photo') uploadingPhotos.value = false
else uploadingVideos.value = false
;(event.target as HTMLInputElement).value = ''
}
}
function removeMedia(kind: 'photo' | 'video', idx: number) {
if (kind === 'photo') form.photos.splice(idx, 1)
else form.videos.splice(idx, 1)
}
async function refresh() {
await store.fetchAll({
categorie: filterCategorie.value || undefined,
tag: filterTag.value || undefined,
mois: filterMoisActuel.value ? currentMonth.value : undefined,
})
}
function openCreate() {
editId.value = null
Object.assign(form, {
titre: '',
contenu: '',
categorie: '',
source: '',
tagsInput: '',
moisInput: '',
photos: [],
videos: [],
})
showForm.value = true
}
function openEdit(a: Astuce) {
editId.value = a.id || null
Object.assign(form, {
titre: a.titre,
contenu: a.contenu,
categorie: a.categorie || '',
source: a.source || '',
tagsInput: parseTags(a.tags).join(', '),
moisInput: parseMois(a.mois).join(','),
photos: parseMediaUrls(a.photos),
videos: parseMediaUrls(a.videos),
})
showForm.value = true
}
function closeForm() {
showForm.value = false
}
async function submitAstuce() {
const payload = {
titre: form.titre.trim(),
contenu: form.contenu.trim(),
categorie: form.categorie || undefined,
source: form.source.trim() || undefined,
tags: toJsonTags(form.tagsInput),
mois: toJsonMois(form.moisInput),
photos: toJsonArray(form.photos),
videos: toJsonArray(form.videos),
}
if (editId.value) {
await store.update(editId.value, payload)
} else {
await store.create(payload)
}
closeForm()
}
async function removeAstuce(id?: number) {
if (!id) return
if (confirm('Supprimer cette astuce ?')) {
await store.remove(id)
}
}
watch([filterCategorie, filterTag, filterMoisActuel], refresh)
onMounted(refresh)
</script>

View File

@@ -0,0 +1,883 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { computed, onMounted, reactive, ref, watch } from 'vue';
import axios from 'axios';
import { useAstucesStore } from '@/stores/astuces';
const store = useAstucesStore();
const filterCategorie = ref('');
const filterTag = ref('');
const filterMoisActuel = ref(false);
const showForm = ref(false);
const editId = ref(null);
const form = reactive({
titre: '',
contenu: '',
categorie: '',
source: '',
tagsInput: '',
moisInput: '',
photos: [],
videos: [],
});
const uploadingPhotos = ref(false);
const uploadingVideos = ref(false);
const currentMonth = computed(() => new Date().getMonth() + 1);
function parseTags(raw) {
if (!raw)
return [];
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr))
return arr.map((x) => String(x).trim()).filter(Boolean);
}
catch {
return raw.split(',').map((x) => x.trim()).filter(Boolean);
}
return [];
}
function parseMois(raw) {
if (!raw)
return [];
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) {
return arr
.map((x) => Number(x))
.filter((x) => Number.isInteger(x) && x >= 1 && x <= 12);
}
}
catch {
return raw
.split(',')
.map((x) => Number(x.trim()))
.filter((x) => Number.isInteger(x) && x >= 1 && x <= 12);
}
return [];
}
function parseMediaUrls(raw) {
if (!raw)
return [];
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr))
return arr.map((x) => String(x).trim()).filter(Boolean);
}
catch {
return raw
.split(',')
.map((x) => x.trim())
.filter(Boolean);
}
return [];
}
function toJsonTags(input) {
const tags = input
.split(',')
.map((x) => x.trim().toLowerCase())
.filter(Boolean);
return tags.length ? JSON.stringify(tags) : undefined;
}
function toJsonMois(input) {
const months = input
.split(',')
.map((x) => Number(x.trim()))
.filter((x) => Number.isInteger(x) && x >= 1 && x <= 12);
return months.length ? JSON.stringify(months) : undefined;
}
function toJsonArray(values) {
const clean = values.map((x) => x.trim()).filter(Boolean);
return clean.length ? JSON.stringify(clean) : undefined;
}
async function uploadFiles(event, kind) {
const files = event.target.files;
if (!files?.length)
return;
if (kind === 'photo')
uploadingPhotos.value = true;
else
uploadingVideos.value = true;
try {
for (const file of Array.from(files)) {
const fd = new FormData();
fd.append('file', file);
const { data } = await axios.post('/api/upload', fd);
if (!data?.url)
continue;
if (kind === 'photo')
form.photos.push(data.url);
else
form.videos.push(data.url);
}
}
finally {
if (kind === 'photo')
uploadingPhotos.value = false;
else
uploadingVideos.value = false;
event.target.value = '';
}
}
function removeMedia(kind, idx) {
if (kind === 'photo')
form.photos.splice(idx, 1);
else
form.videos.splice(idx, 1);
}
async function refresh() {
await store.fetchAll({
categorie: filterCategorie.value || undefined,
tag: filterTag.value || undefined,
mois: filterMoisActuel.value ? currentMonth.value : undefined,
});
}
function openCreate() {
editId.value = null;
Object.assign(form, {
titre: '',
contenu: '',
categorie: '',
source: '',
tagsInput: '',
moisInput: '',
photos: [],
videos: [],
});
showForm.value = true;
}
function openEdit(a) {
editId.value = a.id || null;
Object.assign(form, {
titre: a.titre,
contenu: a.contenu,
categorie: a.categorie || '',
source: a.source || '',
tagsInput: parseTags(a.tags).join(', '),
moisInput: parseMois(a.mois).join(','),
photos: parseMediaUrls(a.photos),
videos: parseMediaUrls(a.videos),
});
showForm.value = true;
}
function closeForm() {
showForm.value = false;
}
async function submitAstuce() {
const payload = {
titre: form.titre.trim(),
contenu: form.contenu.trim(),
categorie: form.categorie || undefined,
source: form.source.trim() || undefined,
tags: toJsonTags(form.tagsInput),
mois: toJsonMois(form.moisInput),
photos: toJsonArray(form.photos),
videos: toJsonArray(form.videos),
};
if (editId.value) {
await store.update(editId.value, payload);
}
else {
await store.create(payload);
}
closeForm();
}
async function removeAstuce(id) {
if (!id)
return;
if (confirm('Supprimer cette astuce ?')) {
await store.remove(id);
}
}
watch([filterCategorie, filterTag, filterMoisActuel], refresh);
onMounted(refresh);
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 max-w-4xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-6" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
...{ class: "text-2xl font-bold text-yellow" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.openCreate) },
...{ class: "bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-wrap items-center gap-2 mb-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.filterCategorie),
...{ class: "bg-bg border border-bg-hard rounded px-3 py-1 text-text text-xs outline-none focus:border-yellow" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "plante",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "jardin",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "tache",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "general",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "ravageur",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "maladie",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
placeholder: "Filtrer par tag...",
...{ class: "bg-bg border border-bg-hard rounded px-3 py-1 text-text text-xs outline-none focus:border-yellow w-44" },
});
(__VLS_ctx.filterTag);
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.filterMoisActuel = !__VLS_ctx.filterMoisActuel;
} },
...{ class: ([
'px-3 py-1 rounded-full text-xs font-medium transition-colors border',
__VLS_ctx.filterMoisActuel ? 'bg-green/20 text-green border-green/40' : 'border-bg-hard text-text-muted',
]) },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.refresh) },
...{ class: "text-xs text-text-muted hover:text-text underline ml-auto" },
});
if (__VLS_ctx.store.loading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm" },
});
}
else if (!__VLS_ctx.store.astuces.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm py-6" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 md:grid-cols-2 gap-3" },
});
for (const [a] of __VLS_getVForSourceType((__VLS_ctx.store.astuces))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (a.id),
...{ class: "bg-bg-soft rounded-xl p-4 border border-bg-hard" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-start justify-between gap-2 mb-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-semibold leading-tight" },
});
(a.titre);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 shrink-0" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.openEdit(a);
} },
...{ class: "text-yellow text-xs hover:underline" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.removeAstuce(a.id);
} },
...{ class: "text-red text-xs hover:underline" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-sm whitespace-pre-line" },
});
(a.contenu);
if (__VLS_ctx.parseMediaUrls(a.photos).length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-3 grid grid-cols-3 gap-2" },
});
for (const [url, idx] of __VLS_getVForSourceType((__VLS_ctx.parseMediaUrls(a.photos)))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
key: (`astuce-photo-${a.id}-${idx}`),
src: (url),
alt: "photo astuce",
...{ class: "w-full h-20 object-cover rounded-md border border-bg-hard" },
});
}
}
if (__VLS_ctx.parseMediaUrls(a.videos).length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-3 space-y-2" },
});
for (const [url, idx] of __VLS_getVForSourceType((__VLS_ctx.parseMediaUrls(a.videos)))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.video)({
key: (`astuce-video-${a.id}-${idx}`),
src: (url),
controls: true,
muted: true,
...{ class: "w-full rounded-md border border-bg-hard bg-black/40 max-h-52" },
});
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-3 flex flex-wrap gap-1" },
});
if (a.categorie) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-[11px] bg-yellow/15 text-yellow rounded-full px-2 py-0.5" },
});
(a.categorie);
}
for (const [t] of __VLS_getVForSourceType((__VLS_ctx.parseTags(a.tags)))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
key: (`${a.id}-t-${t}`),
...{ class: "text-[11px] bg-blue/15 text-blue rounded-full px-2 py-0.5" },
});
(t);
}
if (__VLS_ctx.parseMois(a.mois).length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-[11px] bg-green/15 text-green rounded-full px-2 py-0.5" },
});
(__VLS_ctx.parseMois(a.mois).join(','));
}
}
if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (__VLS_ctx.closeForm) },
...{ class: "fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-hard rounded-xl p-6 w-full max-w-lg border border-bg-soft" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-bold text-lg mb-4" },
});
(__VLS_ctx.editId ? 'Modifier astuce' : 'Nouvelle astuce');
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.submitAstuce) },
...{ class: "flex flex-col gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
placeholder: "Titre *",
required: true,
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
(__VLS_ctx.form.titre);
__VLS_asFunctionalElement(__VLS_intrinsicElements.textarea)({
value: (__VLS_ctx.form.contenu),
placeholder: "Contenu *",
required: true,
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow resize-none h-28" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 sm:grid-cols-2 gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.form.categorie),
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "plante",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "jardin",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "tache",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "general",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "ravageur",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "maladie",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
placeholder: "Source",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
(__VLS_ctx.form.source);
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
placeholder: "Tags (ex: tomate, semis, mildiou)",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
(__VLS_ctx.form.tagsInput);
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
placeholder: "Mois (ex: 3,4,5)",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
(__VLS_ctx.form.moisInput);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 sm:grid-cols-2 gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm cursor-pointer text-center hover:border-yellow" },
});
(__VLS_ctx.uploadingPhotos ? 'Upload photos...' : 'Ajouter photo(s)');
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (...[$event]) => {
if (!(__VLS_ctx.showForm))
return;
__VLS_ctx.uploadFiles($event, 'photo');
} },
type: "file",
accept: "image/*",
multiple: true,
...{ class: "hidden" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm cursor-pointer text-center hover:border-yellow" },
});
(__VLS_ctx.uploadingVideos ? 'Upload vidéos...' : 'Ajouter vidéo(s)');
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (...[$event]) => {
if (!(__VLS_ctx.showForm))
return;
__VLS_ctx.uploadFiles($event, 'video');
} },
type: "file",
accept: "video/*",
multiple: true,
...{ class: "hidden" },
});
if (__VLS_ctx.form.photos.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg border border-bg-soft rounded-lg p-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-xs text-text-muted mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-3 gap-2" },
});
for (const [url, idx] of __VLS_getVForSourceType((__VLS_ctx.form.photos))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (`form-photo-${idx}`),
...{ class: "relative group" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (url),
alt: "photo astuce",
...{ class: "w-full h-16 object-cover rounded border border-bg-hard" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.showForm))
return;
if (!(__VLS_ctx.form.photos.length))
return;
__VLS_ctx.removeMedia('photo', idx);
} },
type: "button",
...{ class: "absolute top-1 right-1 hidden group-hover:block bg-red/80 text-white text-[10px] rounded px-1" },
});
}
}
if (__VLS_ctx.form.videos.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg border border-bg-soft rounded-lg p-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-xs text-text-muted mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "space-y-2" },
});
for (const [url, idx] of __VLS_getVForSourceType((__VLS_ctx.form.videos))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (`form-video-${idx}`),
...{ class: "relative group" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.video)({
src: (url),
controls: true,
muted: true,
...{ class: "w-full max-h-36 rounded border border-bg-hard bg-black/40" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.showForm))
return;
if (!(__VLS_ctx.form.videos.length))
return;
__VLS_ctx.removeMedia('video', idx);
} },
type: "button",
...{ class: "absolute top-1 right-1 hidden group-hover:block bg-red/80 text-white text-[10px] rounded px-1" },
});
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 justify-end mt-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.closeForm) },
type: "button",
...{ class: "px-4 py-2 text-text-muted hover:text-text text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
type: "submit",
...{ class: "bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
});
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer');
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-4xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-between']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-wrap']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['w-44']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['underline']} */ ;
/** @type {__VLS_StyleScopedClasses['ml-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['py-6']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['md:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-start']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-between']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['leading-tight']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['shrink-0']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['whitespace-pre-line']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-3']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-20']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-3']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/40']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-52']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-3']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-wrap']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-yellow/15']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-blue/15']} */ ;
/** @type {__VLS_StyleScopedClasses['text-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green/15']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-6']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-col']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['resize-none']} */ ;
/** @type {__VLS_StyleScopedClasses['h-28']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['group']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['top-1']} */ ;
/** @type {__VLS_StyleScopedClasses['right-1']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['group-hover:block']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-red/80']} */ ;
/** @type {__VLS_StyleScopedClasses['text-white']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[10px]']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-2']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['group']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-36']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/40']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['top-1']} */ ;
/** @type {__VLS_StyleScopedClasses['right-1']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['group-hover:block']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-red/80']} */ ;
/** @type {__VLS_StyleScopedClasses['text-white']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[10px]']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-1']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-end']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
store: store,
filterCategorie: filterCategorie,
filterTag: filterTag,
filterMoisActuel: filterMoisActuel,
showForm: showForm,
editId: editId,
form: form,
uploadingPhotos: uploadingPhotos,
uploadingVideos: uploadingVideos,
parseTags: parseTags,
parseMois: parseMois,
parseMediaUrls: parseMediaUrls,
uploadFiles: uploadFiles,
removeMedia: removeMedia,
refresh: refresh,
openCreate: openCreate,
openEdit: openEdit,
closeForm: closeForm,
submitAstuce: submitAstuce,
removeAstuce: removeAstuce,
};
},
});
export default (await import('vue')).defineComponent({
setup() {
return {};
},
});
; /* PartiallyEnd: #4569/main.vue */

View File

@@ -63,6 +63,12 @@
class="flex-1 bg-blue/20 text-blue hover:bg-blue/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors"> class="flex-1 bg-blue/20 text-blue hover:bg-blue/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors">
🔗 Associer à une plante 🔗 Associer à une plante
</button> </button>
<button
@click="markAsAdventice(lightbox!)"
class="bg-green/20 text-green hover:bg-green/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors"
>
🌾 Marquer adventice
</button>
<button @click="deleteMedia(lightbox!); lightbox = null" <button @click="deleteMedia(lightbox!); lightbox = null"
class="bg-red/20 text-red hover:bg-red/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors"> class="bg-red/20 text-red hover:bg-red/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors">
🗑 Supprimer 🗑 Supprimer
@@ -125,6 +131,7 @@ const plantsStore = usePlantsStore()
const filters = [ const filters = [
{ val: '', label: 'Toutes' }, { val: '', label: 'Toutes' },
{ val: 'plante', label: '🌱 Plantes' }, { val: 'plante', label: '🌱 Plantes' },
{ val: 'adventice', label: '🌾 Adventices' },
{ val: 'jardin', label: '🏡 Jardins' }, { val: 'jardin', label: '🏡 Jardins' },
{ val: 'plantation', label: '🥕 Plantations' }, { val: 'plantation', label: '🥕 Plantations' },
{ val: 'outil', label: '🔧 Outils' }, { val: 'outil', label: '🔧 Outils' },
@@ -137,6 +144,7 @@ const filtered = computed(() =>
function labelFor(type: string) { function labelFor(type: string) {
const map: Record<string, string> = { const map: Record<string, string> = {
adventice: '🌾 Adventice',
plante: '🌱 Plante', jardin: '🏡 Jardin', plante: '🌱 Plante', jardin: '🏡 Jardin',
plantation: '🥕 Plantation', outil: '🔧 Outil', bibliotheque: '📷' plantation: '🥕 Plantation', outil: '🔧 Outil', bibliotheque: '📷'
} }
@@ -168,6 +176,21 @@ async function confirmLink() {
linkPlantId.value = null linkPlantId.value = null
} }
async function markAsAdventice(m: Media) {
await axios.patch(`/api/media/${m.id}`, {
entity_type: 'adventice',
entity_id: 0,
})
const target = medias.value.find(x => x.id === m.id)
if (target) {
target.entity_type = 'adventice'
target.entity_id = 0
}
if (lightbox.value?.id === m.id) {
lightbox.value = { ...lightbox.value, entity_type: 'adventice', entity_id: 0 }
}
}
async function deleteMedia(m: Media) { async function deleteMedia(m: Media) {
if (!confirm('Supprimer cette photo ?')) return if (!confirm('Supprimer cette photo ?')) return
await axios.delete(`/api/media/${m.id}`) await axios.delete(`/api/media/${m.id}`)

View File

@@ -0,0 +1,554 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { computed, onMounted, ref } from 'vue';
import axios from 'axios';
import PhotoIdentifyModal from '@/components/PhotoIdentifyModal.vue';
import { usePlantsStore } from '@/stores/plants';
const medias = ref([]);
const loading = ref(false);
const lightbox = ref(null);
const showIdentify = ref(false);
const activeFilter = ref('');
const linkMedia = ref(null);
const linkPlantId = ref(null);
const plantsStore = usePlantsStore();
const filters = [
{ val: '', label: 'Toutes' },
{ val: 'plante', label: '🌱 Plantes' },
{ val: 'adventice', label: '🌾 Adventices' },
{ val: 'jardin', label: '🏡 Jardins' },
{ val: 'plantation', label: '🥕 Plantations' },
{ val: 'outil', label: '🔧 Outils' },
{ val: 'bibliotheque', label: '📷 Sans lien' },
];
const filtered = computed(() => activeFilter.value ? medias.value.filter(m => m.entity_type === activeFilter.value) : medias.value);
function labelFor(type) {
const map = {
adventice: '🌾 Adventice',
plante: '🌱 Plante', jardin: '🏡 Jardin',
plantation: '🥕 Plantation', outil: '🔧 Outil', bibliotheque: '📷'
};
return map[type] ?? '📷';
}
function plantName(id) {
return plantsStore.plants.find(p => p.id === id)?.nom_commun ?? '';
}
function openLightbox(m) { lightbox.value = m; }
function startLink(m) {
linkMedia.value = m;
linkPlantId.value = m.entity_type === 'plante' ? m.entity_id : null;
}
async function confirmLink() {
if (!linkMedia.value || !linkPlantId.value)
return;
await axios.patch(`/api/media/${linkMedia.value.id}`, {
entity_type: 'plante', entity_id: linkPlantId.value,
});
const m = medias.value.find(x => x.id === linkMedia.value.id);
if (m) {
m.entity_type = 'plante';
m.entity_id = linkPlantId.value;
}
if (lightbox.value?.id === linkMedia.value.id) {
lightbox.value = { ...lightbox.value, entity_type: 'plante', entity_id: linkPlantId.value };
}
linkMedia.value = null;
linkPlantId.value = null;
}
async function markAsAdventice(m) {
await axios.patch(`/api/media/${m.id}`, {
entity_type: 'adventice',
entity_id: 0,
});
const target = medias.value.find(x => x.id === m.id);
if (target) {
target.entity_type = 'adventice';
target.entity_id = 0;
}
if (lightbox.value?.id === m.id) {
lightbox.value = { ...lightbox.value, entity_type: 'adventice', entity_id: 0 };
}
}
async function deleteMedia(m) {
if (!confirm('Supprimer cette photo ?'))
return;
await axios.delete(`/api/media/${m.id}`);
medias.value = medias.value.filter(x => x.id !== m.id);
if (lightbox.value?.id === m.id)
lightbox.value = null;
}
async function fetchAll() {
loading.value = true;
try {
const { data } = await axios.get('/api/media/all');
medias.value = data;
}
finally {
loading.value = false;
}
}
function onIdentified() { fetchAll(); }
onMounted(() => {
fetchAll();
plantsStore.fetchAll();
});
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 max-w-4xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-6" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
...{ class: "text-2xl font-bold text-green" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.showIdentify = true;
} },
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 mb-4 flex-wrap" },
});
for (const [f] of __VLS_getVForSourceType((__VLS_ctx.filters))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.activeFilter = f.val;
} },
key: (f.val),
...{ class: (['px-3 py-1 rounded-full text-xs font-medium transition-colors',
__VLS_ctx.activeFilter === f.val ? 'bg-green text-bg' : 'bg-bg-soft text-text-muted hover:text-text']) },
});
(f.label);
}
if (__VLS_ctx.loading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm" },
});
}
else if (!__VLS_ctx.filtered.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm py-4" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-3 md:grid-cols-4 gap-2" },
});
for (const [m] of __VLS_getVForSourceType((__VLS_ctx.filtered))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.loading))
return;
if (!!(!__VLS_ctx.filtered.length))
return;
__VLS_ctx.openLightbox(m);
} },
key: (m.id),
...{ class: "aspect-square rounded-lg overflow-hidden bg-bg-hard relative group cursor-pointer" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (m.thumbnail_url || m.url),
alt: (m.titre || ''),
...{ class: "w-full h-full object-cover" },
});
if (m.identified_common) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "absolute bottom-0 left-0 right-0 bg-black/70 text-xs text-green px-1 py-0.5 truncate" },
});
(m.identified_common);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "absolute top-1 left-1 bg-black/60 text-text-muted text-xs px-1 rounded" },
});
(__VLS_ctx.labelFor(m.entity_type));
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.loading))
return;
if (!!(!__VLS_ctx.filtered.length))
return;
__VLS_ctx.deleteMedia(m);
} },
...{ class: "hidden group-hover:flex absolute top-1 right-1 bg-red/80 text-white text-xs rounded px-1" },
});
}
}
if (__VLS_ctx.lightbox) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.lightbox = null;
} },
...{ class: "fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "max-w-lg w-full bg-bg-hard rounded-xl overflow-hidden border border-bg-soft" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.lightbox.url),
...{ class: "w-full" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4" },
});
if (__VLS_ctx.lightbox.identified_species) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-center mb-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-green font-semibold text-base" },
});
(__VLS_ctx.lightbox.identified_common);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "italic text-text-muted text-sm" },
});
(__VLS_ctx.lightbox.identified_species);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-xs text-text-muted mt-1" },
});
(Math.round((__VLS_ctx.lightbox.identified_confidence || 0) * 100));
(__VLS_ctx.lightbox.identified_source);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-xs text-text-muted mb-3 text-center" },
});
(__VLS_ctx.labelFor(__VLS_ctx.lightbox.entity_type));
if (__VLS_ctx.lightbox.entity_type === 'plante' && __VLS_ctx.plantName(__VLS_ctx.lightbox.entity_id)) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-green font-medium" },
});
(__VLS_ctx.plantName(__VLS_ctx.lightbox.entity_id));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 flex-wrap" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.startLink(__VLS_ctx.lightbox);
} },
...{ class: "flex-1 bg-blue/20 text-blue hover:bg-blue/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.markAsAdventice(__VLS_ctx.lightbox);
} },
...{ class: "bg-green/20 text-green hover:bg-green/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.deleteMedia(__VLS_ctx.lightbox);
__VLS_ctx.lightbox = null;
} },
...{ class: "bg-red/20 text-red hover:bg-red/30 px-3 py-2 rounded-lg text-xs font-medium transition-colors" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.lightbox = null;
} },
...{ class: "mt-3 w-full text-text-muted hover:text-text text-sm" },
});
}
if (__VLS_ctx.linkMedia) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.linkMedia))
return;
__VLS_ctx.linkMedia = null;
} },
...{ class: "fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-hard rounded-xl p-6 w-full max-w-sm border border-bg-soft" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h3, __VLS_intrinsicElements.h3)({
...{ class: "text-text font-bold mb-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.linkPlantId),
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green mb-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: (null),
});
for (const [p] of __VLS_getVForSourceType((__VLS_ctx.plantsStore.plants))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
key: (p.id),
value: (p.id),
});
(p.nom_commun);
(p.variete ? ' — ' + p.variete : '');
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 justify-end" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.linkMedia))
return;
__VLS_ctx.linkMedia = null;
} },
...{ class: "px-4 py-2 text-text-muted hover:text-text text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.confirmLink) },
disabled: (!__VLS_ctx.linkPlantId),
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90 disabled:opacity-40" },
});
}
if (__VLS_ctx.showIdentify) {
/** @type {[typeof PhotoIdentifyModal, ]} */ ;
// @ts-ignore
const __VLS_0 = __VLS_asFunctionalComponent(PhotoIdentifyModal, new PhotoIdentifyModal({
...{ 'onClose': {} },
...{ 'onIdentified': {} },
}));
const __VLS_1 = __VLS_0({
...{ 'onClose': {} },
...{ 'onIdentified': {} },
}, ...__VLS_functionalComponentArgsRest(__VLS_0));
let __VLS_3;
let __VLS_4;
let __VLS_5;
const __VLS_6 = {
onClose: (...[$event]) => {
if (!(__VLS_ctx.showIdentify))
return;
__VLS_ctx.showIdentify = false;
}
};
const __VLS_7 = {
onIdentified: (__VLS_ctx.onIdentified)
};
var __VLS_2;
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-4xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-between']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-wrap']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['py-4']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['md:grid-cols-4']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['aspect-square']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['group']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['bottom-0']} */ ;
/** @type {__VLS_StyleScopedClasses['left-0']} */ ;
/** @type {__VLS_StyleScopedClasses['right-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/70']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['px-1']} */ ;
/** @type {__VLS_StyleScopedClasses['py-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['top-1']} */ ;
/** @type {__VLS_StyleScopedClasses['left-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['px-1']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['group-hover:flex']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['top-1']} */ ;
/** @type {__VLS_StyleScopedClasses['right-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-red/80']} */ ;
/** @type {__VLS_StyleScopedClasses['text-white']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-1']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/80']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-base']} */ ;
/** @type {__VLS_StyleScopedClasses['italic']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-wrap']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-blue/20']} */ ;
/** @type {__VLS_StyleScopedClasses['text-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-blue/30']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green/20']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-green/30']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-red/20']} */ ;
/** @type {__VLS_StyleScopedClasses['text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-red/30']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-3']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/70']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-6']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-end']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-40']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
PhotoIdentifyModal: PhotoIdentifyModal,
loading: loading,
lightbox: lightbox,
showIdentify: showIdentify,
activeFilter: activeFilter,
linkMedia: linkMedia,
linkPlantId: linkPlantId,
plantsStore: plantsStore,
filters: filters,
filtered: filtered,
labelFor: labelFor,
plantName: plantName,
openLightbox: openLightbox,
startLink: startLink,
confirmLink: confirmLink,
markAsAdventice: markAsAdventice,
deleteMedia: deleteMedia,
onIdentified: onIdentified,
};
},
});
export default (await import('vue')).defineComponent({
setup() {
return {};
},
});
; /* PartiallyEnd: #4569/main.vue */

View File

@@ -1,247 +1,429 @@
<template> <template>
<div class="p-4 max-w-4xl mx-auto"> <div class="p-4 max-w-6xl mx-auto">
<h1 class="text-2xl font-bold text-blue mb-4">📅 Calendrier</h1> <h1 class="text-2xl font-bold text-blue mb-4">🌦 Météo</h1>
<!-- Onglets --> <div class="flex flex-wrap items-center gap-2 mb-4">
<div class="flex gap-1 mb-6 bg-bg-soft rounded-lg p-1 w-fit"> <button
<button v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" class="px-3 py-1.5 rounded-md text-xs font-medium bg-bg-soft text-text hover:text-blue border border-bg-hard"
:class="['px-4 py-2 rounded-md text-sm font-medium transition-colors', @click="shiftWindow(-spanDays)"
activeTab === tab.id ? 'bg-blue text-bg' : 'text-text-muted hover:text-text']"> >
{{ tab.label }} Prev
</button> </button>
<button
class="px-3 py-1.5 rounded-md text-xs font-medium bg-blue/20 text-blue border border-blue/30"
@click="goToday"
>
Today
</button>
<button
class="px-3 py-1.5 rounded-md text-xs font-medium bg-bg-soft text-text hover:text-blue border border-bg-hard"
@click="shiftWindow(spanDays)"
>
Next
</button>
<span class="text-text-muted text-xs ml-1">
Fenêtre: {{ formatDate(rangeStart) }} {{ formatDate(rangeEnd) }}
</span>
</div> </div>
<!-- Sélecteur mois (onglets lunaire/dictons) --> <div
<div v-if="activeTab === 'lunaire' || activeTab === 'dictons'" class="flex items-center gap-3 mb-4"> v-if="stationCurrent"
<button @click="prevMonth" class="text-text-muted hover:text-text text-lg"></button> class="bg-bg-soft rounded-xl p-4 border border-bg-hard mb-4 flex flex-wrap gap-4 items-center"
<span class="text-text font-semibold">{{ monthLabel }}</span> >
<button @click="nextMonth" class="text-text-muted hover:text-text text-lg"></button> <div>
</div> <div class="text-text-muted text-xs mb-1">Température extérieure</div>
<div class="text-text text-2xl font-bold">{{ stationCurrent.temp_ext?.toFixed(1) ?? '—' }}°C</div>
<!-- === LUNAIRE === -->
<div v-if="activeTab === 'lunaire'">
<div v-if="loadingLunar" class="text-text-muted text-sm py-4">Calcul en cours (skyfield)...</div>
<div v-else-if="errorLunar" class="bg-red/10 border border-red rounded-lg p-4 text-red text-sm">
{{ errorLunar }}
</div> </div>
<div v-else-if="!lunarDays.length" class="text-text-muted text-sm py-4">Aucune donnée lunaire.</div> <div class="flex gap-4 text-sm">
<div v-else> <span v-if="stationCurrent.humidite != null" class="text-blue">💧{{ stationCurrent.humidite }}%</span>
<!-- Grille calendrier --> <span v-if="stationCurrent.vent_kmh != null" class="text-text">💨{{ stationCurrent.vent_kmh }} km/h {{ stationCurrent.vent_dir || '' }}</span>
<div class="grid grid-cols-7 gap-1 mb-2"> <span v-if="stationCurrent.pression != null" class="text-text">🧭{{ stationCurrent.pression }} hPa</span>
<div v-for="d in ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim']" :key="d" </div>
class="text-center text-text-muted text-xs py-1">{{ d }}</div> <div class="flex items-center gap-2">
</div> <img
<div class="grid grid-cols-7 gap-1"> v-if="currentOpenMeteo?.wmo != null"
<div v-for="_ in firstDayOffset" :key="'empty-'+_" class="h-16"></div> :src="weatherIcon(currentOpenMeteo.wmo)"
<div v-for="day in lunarDays" :key="day.date" class="w-6 h-6"
@click="selectedDay = day" :alt="currentOpenMeteo.label || 'Météo'"
:class="['h-16 bg-bg-soft rounded-lg p-1 cursor-pointer hover:border hover:border-blue transition-colors flex flex-col items-center justify-center gap-0.5', />
selectedDay?.date === day.date ? 'border border-blue' : 'border border-transparent']"> <div>
<span class="text-text-muted text-xs">{{ new Date(day.date+'T12:00:00').getDate() }}</span> <div class="text-text-muted text-xs mb-1">Condition actuelle</div>
<img :src="moonIcon(day.illumination, day.croissante_decroissante)" class="w-6 h-6 opacity-90" alt="phase" /> <div class="text-text text-sm">{{ currentOpenMeteo?.label || '—' }}</div>
<span class="text-xs leading-none" :class="typeColor(day.type_jour)">{{ typeEmoji(day.type_jour) }}</span>
</div>
</div> </div>
</div>
<div v-if="stationCurrent.date_heure" class="text-text-muted text-xs ml-auto">
Relevé {{ stationCurrent.date_heure.slice(11, 16) }}
</div>
</div>
<!-- Détail jour sélectionné --> <div v-if="loadingTableau" class="text-text-muted text-sm py-4">Chargement météo...</div>
<div v-if="selectedDay" class="mt-4 bg-bg-soft rounded-xl p-4 border border-bg-hard"> <div v-else-if="!tableauRows.length" class="text-text-muted text-sm py-4">Pas de données météo.</div>
<div class="flex items-center gap-3 mb-3">
<img :src="moonIcon(selectedDay.illumination, selectedDay.croissante_decroissante)" class="w-10 h-10" alt="phase" /> <div v-else class="grid grid-cols-1 xl:grid-cols-[minmax(0,1fr)_340px] gap-4 items-start">
<div> <div class="overflow-x-auto bg-bg-soft rounded-xl border border-bg-hard p-2">
<div class="text-text font-bold">{{ formatDate(selectedDay.date) }}</div> <table class="w-full text-sm border-collapse">
<div class="text-text-muted text-sm">{{ selectedDay.phase || 'Pas de phase particulière' }}</div> <thead>
</div> <tr class="text-text-muted text-xs">
<th class="text-left py-2 px-2">Date</th>
<th class="text-center py-2 px-2 text-blue" colspan="3">📡 Station locale</th>
<th class="text-center py-2 px-2 text-green border-l-2 border-bg-hard" colspan="4">🌐 Open-Meteo</th>
<th class="text-center py-2 px-2 text-yellow border-l-2 border-bg-hard" colspan="3">🌙 Lunaire</th>
</tr>
<tr class="text-text-muted text-xs border-b border-bg-hard">
<th class="text-left py-1 px-2"></th>
<th class="text-right py-1 px-1">T°min</th>
<th class="text-right py-1 px-1">T°max</th>
<th class="text-right py-1 px-1">💧mm</th>
<th class="text-right py-1 px-1 border-l-2 border-bg-hard">T°min</th>
<th class="text-right py-1 px-1">T°max</th>
<th class="text-right py-1 px-1">💧mm</th>
<th class="text-left py-1 px-2">État</th>
<th class="text-left py-1 px-2 border-l-2 border-bg-hard">Type lune</th>
<th class="text-left py-1 px-2">Mont./Desc.</th>
<th class="text-left py-1 px-2">Type jour</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in tableauRows"
:key="row.date"
@click="selectMeteoDate(row.date)"
:class="[
'border-b border-bg-hard transition-colors cursor-pointer',
row.type === 'passe' ? 'opacity-80' : '',
row.date === selectedMeteoDate ? 'bg-blue/10 border-blue' : '',
]"
>
<td class="py-2 px-2 text-text-muted text-xs whitespace-nowrap">
<span :class="row.type === 'aujourd_hui' ? 'text-green font-bold' : ''">
{{ formatDate(row.date) }}
</span>
</td>
<td class="text-right px-1 text-blue text-xs">{{ stationTMin(row) }}</td>
<td class="text-right px-1 text-orange text-xs">{{ stationTMax(row) }}</td>
<td class="text-right px-1 text-blue text-xs">{{ stationRain(row) }}</td>
<td class="text-right px-1 text-blue text-xs border-l-2 border-bg-hard">{{ omTMin(row) }}</td>
<td class="text-right px-1 text-orange text-xs">{{ omTMax(row) }}</td>
<td class="text-right px-1 text-blue text-xs">{{ omRain(row) }}</td>
<td class="px-2">
<div class="flex items-center gap-1">
<img
v-if="row.open_meteo?.wmo != null"
:src="weatherIcon(row.open_meteo.wmo)"
class="w-5 h-5"
:alt="row.open_meteo.label"
/>
<span class="text-text-muted text-xs">{{ row.open_meteo?.label || '—' }}</span>
</div>
</td>
<td class="px-2 text-xs border-l-2 border-bg-hard">
{{ lunarForDate(row.date)?.croissante_decroissante || '—' }}
</td>
<td class="px-2 text-xs">
{{ lunarForDate(row.date)?.montante_descendante || '—' }}
</td>
<td class="px-2 text-xs" :class="typeColor(lunarForDate(row.date)?.type_jour || '')">
{{ lunarForDate(row.date)?.type_jour || '—' }}
</td>
</tr>
</tbody>
</table>
</div>
<aside class="bg-bg-soft rounded-xl border border-bg-hard p-4">
<div v-if="!selectedMeteoRow" class="text-text-muted text-sm">Sélectionne un jour dans le tableau pour voir le détail.</div>
<div v-else class="space-y-3">
<div>
<h3 class="text-text font-semibold">{{ formatDateLong(selectedMeteoRow.date) }}</h3>
<p class="text-text-muted text-xs">
{{ selectedMeteoRow.type === 'passe' ? 'Historique' : selectedMeteoRow.type === 'aujourd_hui' ? 'Aujourd\'hui' : 'Prévision' }}
</p>
</div> </div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="bg-bg rounded-lg p-2"> <div class="pt-2 border-t border-bg-hard">
<div class="text-text-muted text-xs mb-1">Illumination</div> <div class="text-blue text-xs font-semibold mb-1">📡 Station locale</div>
<div class="text-text">{{ selectedDay.illumination }}%</div> <div class="text-xs text-text-muted space-y-1">
</div> <div v-if="selectedMeteoRow.station && 'temp_ext' in selectedMeteoRow.station && selectedMeteoRow.station.temp_ext != null">
<div class="bg-bg rounded-lg p-2"> T° actuelle: <span class="text-text">{{ selectedMeteoRow.station.temp_ext.toFixed(1) }}°</span>
<div class="text-text-muted text-xs mb-1">Tendance</div> </div>
<div class="text-text">{{ selectedDay.croissante_decroissante }}</div> <div>T° min: <span class="text-text">{{ stationTMin(selectedMeteoRow) }}</span></div>
</div> <div>T° max/actuelle: <span class="text-text">{{ stationTMax(selectedMeteoRow) }}</span></div>
<div class="bg-bg rounded-lg p-2"> <div>Pluie: <span class="text-text">{{ stationRain(selectedMeteoRow) }}</span></div>
<div class="text-text-muted text-xs mb-1">Lune</div> <div v-if="selectedMeteoRow.station && 'vent_kmh' in selectedMeteoRow.station && selectedMeteoRow.station.vent_kmh != null">
<div class="text-text">{{ selectedDay.montante_descendante }}</div> Vent max: <span class="text-text">{{ selectedMeteoRow.station.vent_kmh }} km/h</span>
</div> </div>
<div class="bg-bg rounded-lg p-2"> <div v-if="selectedMeteoRow.station && 'humidite' in selectedMeteoRow.station && selectedMeteoRow.station.humidite != null">
<div class="text-text-muted text-xs mb-1">Signe</div> Humidité: <span class="text-text">{{ selectedMeteoRow.station.humidite }}%</span>
<div class="text-text">{{ selectedDay.signe }}</div>
</div>
<div class="bg-bg rounded-lg p-2 col-span-2">
<div class="text-text-muted text-xs mb-1">Type de jour</div>
<div :class="['font-semibold', typeColor(selectedDay.type_jour)]">
{{ typeEmoji(selectedDay.type_jour) }} {{ selectedDay.type_jour }}
</div> </div>
</div> </div>
</div> </div>
<div v-if="selectedDay.perigee" class="mt-2 text-xs text-orange bg-orange/10 rounded px-2 py-1"> Périgée (lune proche)</div>
<div v-if="selectedDay.apogee" class="mt-2 text-xs text-blue bg-blue/10 rounded px-2 py-1">🌌 Apogée (lune éloignée)</div>
<div v-if="selectedDay.noeud_lunaire" class="mt-2 text-xs text-yellow bg-yellow/10 rounded px-2 py-1"> Nœud lunaire</div>
</div>
</div>
</div>
<!-- === MÉTÉO === --> <div class="pt-2 border-t border-bg-hard">
<div v-if="activeTab === 'meteo'"> <div class="text-green text-xs font-semibold mb-1">🌐 Open-Meteo</div>
<div v-if="loadingMeteo" class="text-text-muted text-sm py-4">Chargement météo...</div> <div class="text-xs text-text-muted space-y-1">
<div v-else-if="!meteoData.length" class="text-text-muted text-sm py-4">Données météo non disponibles.</div> <div>T° min: <span class="text-text">{{ omTMin(selectedMeteoRow) }}</span></div>
<div v-else class="grid grid-cols-2 sm:grid-cols-4 gap-3"> <div>T° max: <span class="text-text">{{ omTMax(selectedMeteoRow) }}</span></div>
<div v-for="day in meteoData" :key="day.date" <div>Pluie: <span class="text-text">{{ omRain(selectedMeteoRow) }}</span></div>
class="bg-bg-soft rounded-xl p-3 border border-bg-hard flex flex-col items-center gap-1"> <div>État: <span class="text-text">{{ selectedMeteoRow.open_meteo?.label || '—' }}</span></div>
<div class="text-text-muted text-xs">{{ formatDate(day.date) }}</div> <div v-if="selectedMeteoRow.open_meteo?.vent_kmh != null">Vent: <span class="text-text">{{ selectedMeteoRow.open_meteo.vent_kmh }} km/h</span></div>
<img :src="weatherIcon(day.code)" class="w-10 h-10" :alt="day.label" /> <div v-if="selectedMeteoRow.open_meteo?.sol_0cm != null">Sol 0cm: <span class="text-text">{{ selectedMeteoRow.open_meteo.sol_0cm }}°C</span></div>
<div class="text-text text-xs font-medium text-center">{{ day.label }}</div> <div v-if="selectedMeteoRow.open_meteo?.etp_mm != null">ETP: <span class="text-text">{{ selectedMeteoRow.open_meteo.etp_mm }} mm</span></div>
<div class="flex gap-2 text-xs mt-1"> </div>
<span class="text-orange">{{ day.t_max?.toFixed(0) }}°</span> </div>
<span class="text-blue">{{ day.t_min?.toFixed(0) }}°</span>
<div class="pt-2 border-t border-bg-hard">
<div class="text-yellow text-xs font-semibold mb-1">🌙 Lunaire</div>
<div v-if="selectedLunarDay" class="text-xs text-text-muted space-y-1">
<div>Type lune: <span class="text-text">{{ selectedLunarDay.croissante_decroissante }}</span></div>
<div>Montante/Descendante: <span class="text-text">{{ selectedLunarDay.montante_descendante }}</span></div>
<div>Type de jour: <span :class="['font-semibold', typeColor(selectedLunarDay.type_jour)]">{{ selectedLunarDay.type_jour }}</span></div>
<div>Signe: <span class="text-text">{{ selectedLunarDay.signe }}</span></div>
<div>Illumination: <span class="text-text">{{ selectedLunarDay.illumination }}%</span></div>
<div>Saint: <span class="text-text">{{ selectedSaint || '—' }}</span></div>
</div>
<div v-else class="text-xs text-text-muted">Donnée lunaire indisponible pour cette date.</div>
</div>
<div class="pt-2 border-t border-bg-hard">
<div class="text-orange text-xs font-semibold mb-1">📜 Dictons</div>
<div v-if="selectedDictons.length" class="space-y-2">
<p v-for="d in selectedDictons" :key="`detail-dicton-${d.id}`" class="text-xs text-text-muted italic">
"{{ d.texte }}"
</p>
</div>
<div v-else class="text-xs text-text-muted">Aucun dicton trouvé pour ce jour.</div>
</div> </div>
<div v-if="day.pluie_mm > 0" class="text-xs text-blue">💧 {{ day.pluie_mm }}mm</div>
</div> </div>
</div> </aside>
</div>
<!-- === TÂCHES === -->
<div v-if="activeTab === 'taches'">
<p class="text-text-muted text-sm py-4">Les tâches planifiées s'afficheront ici.</p>
</div>
<!-- === DICTONS === -->
<div v-if="activeTab === 'dictons'">
<div v-if="!dictons.length" class="text-text-muted text-sm py-4">Aucun dicton pour ce mois.</div>
<div v-for="d in dictons" :key="d.id" class="bg-bg-soft rounded-lg p-4 mb-2 border border-bg-hard">
<p class="text-text italic text-sm">"{{ d.texte }}"</p>
<p v-if="d.region" class="text-text-muted text-xs mt-1">— {{ d.region }}</p>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { lunarApi, type LunarDay, type Dicton } from '@/api/lunar' import { lunarApi, type Dicton, type LunarDay } from '@/api/lunar'
import { meteoApi, type MeteoDay } from '@/api/meteo' import { meteoApi, type StationCurrent, type TableauRow } from '@/api/meteo'
const activeTab = ref('lunaire') const spanDays = 15
const tabs = [
{ id: 'lunaire', label: '🌙 Lunaire' },
{ id: 'meteo', label: ' Météo' },
{ id: 'taches', label: ' Tâches' },
{ id: 'dictons', label: '📜 Dictons' },
]
const now = new Date() const tableauRows = ref<TableauRow[]>([])
const currentYear = ref(now.getFullYear()) const loadingTableau = ref(false)
const currentMonth = ref(now.getMonth() + 1) const stationCurrent = ref<StationCurrent | null>(null)
const lunarByDate = ref<Record<string, LunarDay>>({})
const selectedMeteoDate = ref('')
const monthLabel = computed(() => { const dictonsByMonth = ref<Record<number, Dicton[]>>({})
const d = new Date(currentYear.value, currentMonth.value - 1, 1)
return d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })
})
const monthStr = computed(() => `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}`) const centerDate = ref(todayIso())
function prevMonth() { const rangeStart = computed(() => shiftIso(centerDate.value, -spanDays))
if (currentMonth.value === 1) { currentMonth.value = 12; currentYear.value-- } const rangeEnd = computed(() => shiftIso(centerDate.value, spanDays))
else currentMonth.value--
} const saintsFallback: Record<string, string> = {
function nextMonth() { '04-23': 'Saint Georges',
if (currentMonth.value === 12) { currentMonth.value = 1; currentYear.value++ } '04-25': 'Saint Marc',
else currentMonth.value++ '05-11': 'Saint Mamert',
'05-12': 'Saint Pancrace',
'05-13': 'Saint Servais',
'05-14': 'Saint Boniface',
'05-19': 'Saint Yves',
'05-25': 'Saint Urbain',
} }
// Lunaire const selectedMeteoRow = computed(() => tableauRows.value.find((r) => r.date === selectedMeteoDate.value) || null)
const lunarDays = ref<LunarDay[]>([]) const selectedLunarDay = computed(() => lunarByDate.value[selectedMeteoDate.value] || null)
const loadingLunar = ref(false) const currentOpenMeteo = computed(() => {
const errorLunar = ref('') const today = tableauRows.value.find((r) => r.type === 'aujourd_hui')
const selectedDay = ref<LunarDay | null>(null) return today?.open_meteo || null
const firstDayOffset = computed(() => {
if (!lunarDays.value.length) return 0
const d = new Date(lunarDays.value[0].date + 'T12:00:00')
return (d.getDay() + 6) % 7 // Lundi=0
}) })
async function loadLunar() { const selectedSaint = computed(() => {
loadingLunar.value = true; errorLunar.value = ''; selectedDay.value = null if (!selectedMeteoDate.value) return ''
if (selectedLunarDay.value?.saint_du_jour) return selectedLunarDay.value.saint_du_jour
const mmdd = selectedMeteoDate.value.slice(5)
return saintsFallback[mmdd] || ''
})
const selectedDictons = computed(() => {
if (!selectedMeteoDate.value) return []
const month = monthFromIso(selectedMeteoDate.value)
const day = dayFromIso(selectedMeteoDate.value)
const rows = dictonsByMonth.value[month] || []
const exact = rows.filter((d) => d.jour === day)
if (exact.length) return exact
return rows.filter((d) => d.jour == null).slice(0, 3)
})
function todayIso(): string {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
function shiftIso(isoDate: string, days: number): string {
const d = new Date(`${isoDate}T12:00:00`)
d.setDate(d.getDate() + days)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
function shiftWindow(days: number) {
centerDate.value = shiftIso(centerDate.value, days)
}
function goToday() {
centerDate.value = todayIso()
}
function monthFromIso(isoDate: string): number {
return Number(isoDate.slice(5, 7))
}
function dayFromIso(isoDate: string): number {
return Number(isoDate.slice(8, 10))
}
function selectMeteoDate(isoDate: string) {
selectedMeteoDate.value = isoDate
void ensureDictonsMonth(monthFromIso(isoDate))
}
async function ensureDictonsMonth(month: number) {
if (!month || dictonsByMonth.value[month]) return
try { try {
lunarDays.value = await lunarApi.getMonth(monthStr.value) const data = await lunarApi.getDictons(month)
} catch (e: unknown) { dictonsByMonth.value = { ...dictonsByMonth.value, [month]: data }
const err = e as { response?: { data?: { detail?: string } } } } catch {
errorLunar.value = err?.response?.data?.detail || 'Erreur lors du chargement du calendrier lunaire.' dictonsByMonth.value = { ...dictonsByMonth.value, [month]: [] }
}
}
async function loadLunarForTableau() {
const months = Array.from(new Set(tableauRows.value.map((r) => r.date.slice(0, 7))))
const map: Record<string, LunarDay> = {}
for (const month of months) {
try {
const days = await lunarApi.getMonth(month)
for (const d of days) map[d.date] = d
} catch {
// Mois indisponible: on laisse les cellules lunaires vides
}
}
lunarByDate.value = map
}
async function loadTableau() {
loadingTableau.value = true
try {
const res = await meteoApi.getTableau({
center_date: centerDate.value,
span: spanDays,
})
tableauRows.value = res.rows || []
await loadLunarForTableau()
const selectedStillVisible = tableauRows.value.some((r) => r.date === selectedMeteoDate.value)
if (selectedStillVisible) return
const todayRow = tableauRows.value.find((r) => r.type === 'aujourd_hui')
if (todayRow) {
selectMeteoDate(todayRow.date)
} else if (tableauRows.value.length) {
selectMeteoDate(tableauRows.value[0].date)
}
} catch {
tableauRows.value = []
lunarByDate.value = {}
} finally { } finally {
loadingLunar.value = false loadingTableau.value = false
} }
} }
// Météo async function loadStationCurrent() {
const meteoData = ref<MeteoDay[]>([])
const loadingMeteo = ref(false)
async function loadMeteo() {
loadingMeteo.value = true
try { try {
const res = await meteoApi.getForecast(14) stationCurrent.value = await meteoApi.getStationCurrent()
meteoData.value = res.days || [] } catch {
} catch { meteoData.value = [] } stationCurrent.value = null
finally { loadingMeteo.value = false }
}
// Dictons
const dictons = ref<Dicton[]>([])
async function loadDictons() {
try { dictons.value = await lunarApi.getDictons(currentMonth.value) }
catch { dictons.value = [] }
}
// Helpers affichage
function moonIcon(illum: number, tendance: string): string {
const i = illum / 100
let name: string
if (i < 0.05) name = 'new_moon'
else if (tendance === 'Croissante') {
if (i < 0.35) name = 'waxing_crescent'
else if (i < 0.65) name = 'first_quarter'
else if (i < 0.95) name = 'waxing_gibbous'
else name = 'full_moon'
} else {
if (i > 0.95) name = 'full_moon'
else if (i > 0.65) name = 'waning_gibbous'
else if (i > 0.35) name = 'last_quarter'
else name = 'waning_crescent'
} }
return `/icons/moon/${name}.svg` }
function lunarForDate(isoDate: string): LunarDay | null {
return lunarByDate.value[isoDate] || null
}
function stationTMin(row: TableauRow): string {
if (row.station && 't_min' in row.station && row.station.t_min != null) return `${row.station.t_min.toFixed(1)}°`
return '—'
}
function stationTMax(row: TableauRow): string {
if (row.station && 't_max' in row.station && row.station.t_max != null) return `${row.station.t_max.toFixed(1)}°`
if (row.type === 'aujourd_hui' && row.station && 'temp_ext' in row.station && row.station.temp_ext != null) {
return `${row.station.temp_ext.toFixed(1)}° act.`
}
return '—'
}
function stationRain(row: TableauRow): string {
if (row.station && row.station.pluie_mm != null) return String(row.station.pluie_mm)
return '—'
}
function omTMin(row: TableauRow): string {
return row.open_meteo?.t_min != null ? `${row.open_meteo.t_min.toFixed(1)}°` : '—'
}
function omTMax(row: TableauRow): string {
return row.open_meteo?.t_max != null ? `${row.open_meteo.t_max.toFixed(1)}°` : '—'
}
function omRain(row: TableauRow): string {
return row.open_meteo?.pluie_mm != null ? String(row.open_meteo.pluie_mm) : '—'
} }
function weatherIcon(code: number): string { function weatherIcon(code: number): string {
// WMO code → fichier SVG disponible
const available = [0, 1, 2, 3, 45, 48, 51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 71, 73, 75, 77, 80, 81, 82, 85, 86, 95, 96, 99] const available = [0, 1, 2, 3, 45, 48, 51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 71, 73, 75, 77, 80, 81, 82, 85, 86, 95, 96, 99]
const closest = available.reduce((prev, curr) => const closest = available.reduce((prev, curr) =>
Math.abs(curr - code) < Math.abs(prev - code) ? curr : prev Math.abs(curr - code) < Math.abs(prev - code) ? curr : prev,
) )
return `/icons/weather/${closest}.svg` return `/icons/weather/${closest}.svg`
} }
function typeEmoji(type: string): string {
return ({ Racine: '🌱', Feuille: '🌿', Fleur: '🌸', Fruit: '🍅' } as Record<string, string>)[type] || ''
}
function typeColor(type: string): string { function typeColor(type: string): string {
return ({ Racine: 'text-yellow', Feuille: 'text-green', Fleur: 'text-orange', Fruit: 'text-red' } as Record<string, string>)[type] || 'text-text-muted' return ({ Racine: 'text-yellow', Feuille: 'text-green', Fleur: 'text-orange', Fruit: 'text-red' } as Record<string, string>)[type] || 'text-text-muted'
} }
function formatDate(dateStr: string): string { function formatDate(dateStr: string): string {
return new Date(dateStr + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }) return new Date(`${dateStr}T12:00:00`).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })
} }
watch(monthStr, () => { function formatDateLong(dateStr: string): string {
if (activeTab.value === 'lunaire') loadLunar() return new Date(`${dateStr}T12:00:00`).toLocaleDateString('fr-FR', {
if (activeTab.value === 'dictons') loadDictons() weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})
}
watch(centerDate, () => {
void loadTableau()
}) })
watch(activeTab, (tab) => { watch(selectedMeteoDate, (iso) => {
if (tab === 'lunaire' && !lunarDays.value.length) loadLunar() if (!iso) return
if (tab === 'meteo' && !meteoData.value.length) loadMeteo() void ensureDictonsMonth(monthFromIso(iso))
if (tab === 'dictons' && !dictons.value.length) loadDictons()
}) })
onMounted(() => { loadLunar(); loadMeteo() }) onMounted(() => {
void loadTableau()
void loadStationCurrent()
})
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="p-4 max-w-2xl mx-auto"> <div class="p-4 max-w-6xl mx-auto">
<h1 class="text-2xl font-bold text-green mb-6">Tableau de bord</h1> <h1 class="text-2xl font-bold text-green mb-6">Tableau de bord</h1>
<section class="mb-6"> <section class="mb-6">
@@ -24,19 +24,46 @@
</section> </section>
<section class="mb-6"> <section class="mb-6">
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">Météo (3 jours)</h2> <h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">Météo</h2>
<div class="flex gap-2 overflow-x-auto">
<div v-for="day in meteo3j" :key="day.date" <div v-if="stationCurrent || meteo7j.length" class="bg-bg-soft rounded-xl p-3 border border-bg-hard mb-3">
class="bg-bg-soft rounded-xl p-3 border border-bg-hard flex flex-col items-center gap-1 min-w-[90px]"> <div class="text-text-muted text-xs mb-1">Condition actuelle</div>
<div class="text-text-muted text-xs">{{ formatDate(day.date) }}</div> <div class="flex items-center gap-3">
<div class="text-2xl">{{ day.icone }}</div> <img
<div class="text-xs text-center text-text-muted">{{ day.label }}</div> v-if="meteoCurrent?.wmo != null"
<div class="flex gap-1 text-xs"> :src="weatherIcon(meteoCurrent.wmo)"
<span class="text-orange">{{ day.t_max?.toFixed(0) }}°</span> class="w-8 h-8"
<span class="text-blue">{{ day.t_min?.toFixed(0) }}°</span> :alt="meteoCurrent.label || 'Météo'"
/>
<div class="text-sm text-text">
<div>{{ meteoCurrent?.label || '—' }}</div>
<div class="text-text-muted text-xs">
{{ stationCurrent?.temp_ext != null ? `${stationCurrent.temp_ext.toFixed(1)}°C` : 'Temp. indisponible' }}
<span v-if="stationCurrent?.date_heure"> · relevé {{ stationCurrent.date_heure.slice(11, 16) }}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="meteo7j.length" class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-7 gap-2">
<div v-for="day in meteo7j" :key="day.date"
class="bg-bg-soft rounded-xl p-3 border border-bg-hard flex flex-col items-center gap-1 min-w-0">
<div class="text-text-muted text-xs">{{ formatDate(day.date || '') }}</div>
<img
v-if="day.wmo != null"
:src="weatherIcon(day.wmo)"
class="w-8 h-8"
:alt="day.label || 'Météo'"
/>
<div v-else class="text-2xl"></div>
<div class="text-[11px] text-center text-text-muted leading-tight min-h-[30px]">{{ day.label || '—' }}</div>
<div class="flex gap-1 text-xs">
<span class="text-orange">{{ day.t_max != null ? day.t_max.toFixed(0) : '—' }}°</span>
<span class="text-blue">{{ day.t_min != null ? day.t_min.toFixed(0) : '—' }}°</span>
</div>
</div>
</div>
<div v-else class="text-text-muted text-sm py-2">Prévisions indisponibles.</div>
</section> </section>
<section> <section>
@@ -60,22 +87,34 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useGardensStore } from '@/stores/gardens' import { useGardensStore } from '@/stores/gardens'
import { useTasksStore } from '@/stores/tasks' import { useTasksStore } from '@/stores/tasks'
import { meteoApi, type MeteoDay } from '@/api/meteo' import { meteoApi, type OpenMeteoDay, type StationCurrent } from '@/api/meteo'
const router = useRouter() const router = useRouter()
const gardensStore = useGardensStore() const gardensStore = useGardensStore()
const tasksStore = useTasksStore() const tasksStore = useTasksStore()
const pendingTasks = computed(() => tasksStore.tasks.filter(t => t.statut === 'a_faire').slice(0, 5)) const pendingTasks = computed(() => tasksStore.tasks.filter(t => t.statut === 'a_faire').slice(0, 5))
const meteo3j = ref<MeteoDay[]>([]) const meteo7j = ref<OpenMeteoDay[]>([])
const stationCurrent = ref<StationCurrent | null>(null)
const meteoCurrent = computed(() => meteo7j.value[0] || null)
function formatDate(s: string) { function formatDate(s: string) {
if (!s) return '—'
return new Date(s + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }) return new Date(s + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })
} }
function weatherIcon(code: number): string {
const available = [0, 1, 2, 3, 45, 48, 51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 71, 73, 75, 77, 80, 81, 82, 85, 86, 95, 96, 99]
const closest = available.reduce((prev, curr) =>
Math.abs(curr - code) < Math.abs(prev - code) ? curr : prev,
)
return `/icons/weather/${closest}.svg`
}
onMounted(async () => { onMounted(async () => {
gardensStore.fetchAll() gardensStore.fetchAll()
tasksStore.fetchAll() tasksStore.fetchAll()
try { const r = await meteoApi.getForecast(3); meteo3j.value = r.days.slice(0, 3) } catch {} try { stationCurrent.value = await meteoApi.getStationCurrent() } catch { stationCurrent.value = null }
try { const r = await meteoApi.getPrevisions(7); meteo7j.value = r.days.slice(0, 7) } catch { meteo7j.value = [] }
}) })
</script> </script>

View File

@@ -8,25 +8,42 @@ const router = useRouter();
const gardensStore = useGardensStore(); const gardensStore = useGardensStore();
const tasksStore = useTasksStore(); const tasksStore = useTasksStore();
const pendingTasks = computed(() => tasksStore.tasks.filter(t => t.statut === 'a_faire').slice(0, 5)); const pendingTasks = computed(() => tasksStore.tasks.filter(t => t.statut === 'a_faire').slice(0, 5));
const meteo3j = ref([]); const meteo7j = ref([]);
const stationCurrent = ref(null);
const meteoCurrent = computed(() => meteo7j.value[0] || null);
function formatDate(s) { function formatDate(s) {
if (!s)
return '—';
return new Date(s + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }); return new Date(s + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
} }
function weatherIcon(code) {
const available = [0, 1, 2, 3, 45, 48, 51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 71, 73, 75, 77, 80, 81, 82, 85, 86, 95, 96, 99];
const closest = available.reduce((prev, curr) => Math.abs(curr - code) < Math.abs(prev - code) ? curr : prev);
return `/icons/weather/${closest}.svg`;
}
onMounted(async () => { onMounted(async () => {
gardensStore.fetchAll(); gardensStore.fetchAll();
tasksStore.fetchAll(); tasksStore.fetchAll();
try { try {
const r = await meteoApi.getForecast(3); stationCurrent.value = await meteoApi.getStationCurrent();
meteo3j.value = r.days.slice(0, 3); }
catch {
stationCurrent.value = null;
}
try {
const r = await meteoApi.getPrevisions(7);
meteo7j.value = r.days.slice(0, 7);
}
catch {
meteo7j.value = [];
} }
catch { }
}); });
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {}; const __VLS_ctx = {};
let __VLS_components; let __VLS_components;
let __VLS_directives; let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 max-w-2xl mx-auto" }, ...{ class: "p-4 max-w-6xl mx-auto" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
...{ class: "text-2xl font-bold text-green mb-6" }, ...{ class: "text-2xl font-bold text-green mb-6" },
@@ -71,37 +88,83 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElemen
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text-muted text-xs uppercase tracking-widest mb-3" }, ...{ class: "text-text-muted text-xs uppercase tracking-widest mb-3" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ if (__VLS_ctx.stationCurrent || __VLS_ctx.meteo7j.length) {
...{ class: "flex gap-2 overflow-x-auto" },
});
for (const [day] of __VLS_getVForSourceType((__VLS_ctx.meteo3j))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (day.date), ...{ class: "bg-bg-soft rounded-xl p-3 border border-bg-hard mb-3" },
...{ class: "bg-bg-soft rounded-xl p-3 border border-bg-hard flex flex-col items-center gap-1 min-w-[90px]" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-3" },
});
if (__VLS_ctx.meteoCurrent?.wmo != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.weatherIcon(__VLS_ctx.meteoCurrent.wmo)),
...{ class: "w-8 h-8" },
alt: (__VLS_ctx.meteoCurrent.label || 'Météo'),
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-sm text-text" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
(__VLS_ctx.meteoCurrent?.label || '—');
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" }, ...{ class: "text-text-muted text-xs" },
}); });
(__VLS_ctx.formatDate(day.date)); (__VLS_ctx.stationCurrent?.temp_ext != null ? `${__VLS_ctx.stationCurrent.temp_ext.toFixed(1)}°C` : 'Temp. indisponible');
if (__VLS_ctx.stationCurrent?.date_heure) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.stationCurrent.date_heure.slice(11, 16));
}
}
if (__VLS_ctx.meteo7j.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-2xl" }, ...{ class: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-7 gap-2" },
}); });
(day.icone); for (const [day] of __VLS_getVForSourceType((__VLS_ctx.meteo7j))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (day.date),
...{ class: "bg-bg-soft rounded-xl p-3 border border-bg-hard flex flex-col items-center gap-1 min-w-0" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
});
(__VLS_ctx.formatDate(day.date || ''));
if (day.wmo != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.weatherIcon(day.wmo)),
...{ class: "w-8 h-8" },
alt: (day.label || 'Météo'),
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-2xl" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-[11px] text-center text-text-muted leading-tight min-h-[30px]" },
});
(day.label || '—');
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-orange" },
});
(day.t_max != null ? day.t_max.toFixed(0) : '—');
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-blue" },
});
(day.t_min != null ? day.t_min.toFixed(0) : '—');
}
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-xs text-center text-text-muted" }, ...{ class: "text-text-muted text-sm py-2" },
}); });
(day.label);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-orange" },
});
(day.t_max?.toFixed(0));
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-blue" },
});
(day.t_min?.toFixed(0));
} }
__VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({}); __VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
@@ -130,7 +193,7 @@ for (const [g] of __VLS_getVForSourceType((__VLS_ctx.gardensStore.gardens))) {
(g.type); (g.type);
} }
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-6xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ; /** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ; /** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
@@ -167,9 +230,29 @@ for (const [g] of __VLS_getVForSourceType((__VLS_ctx.gardensStore.gardens))) {
/** @type {__VLS_StyleScopedClasses['uppercase']} */ ; /** @type {__VLS_StyleScopedClasses['uppercase']} */ ;
/** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ; /** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ; /** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['w-8']} */ ;
/** @type {__VLS_StyleScopedClasses['h-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:grid-cols-7']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ; /** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-x-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ; /** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ; /** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ; /** @type {__VLS_StyleScopedClasses['p-3']} */ ;
@@ -179,19 +262,26 @@ for (const [g] of __VLS_getVForSourceType((__VLS_ctx.gardensStore.gardens))) {
/** @type {__VLS_StyleScopedClasses['flex-col']} */ ; /** @type {__VLS_StyleScopedClasses['flex-col']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ; /** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ; /** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['min-w-[90px]']} */ ; /** @type {__VLS_StyleScopedClasses['min-w-0']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['w-8']} */ ;
/** @type {__VLS_StyleScopedClasses['h-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ; /** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['leading-tight']} */ ;
/** @type {__VLS_StyleScopedClasses['min-h-[30px]']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ; /** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-orange']} */ ; /** @type {__VLS_StyleScopedClasses['text-orange']} */ ;
/** @type {__VLS_StyleScopedClasses['text-blue']} */ ; /** @type {__VLS_StyleScopedClasses['text-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['uppercase']} */ ; /** @type {__VLS_StyleScopedClasses['uppercase']} */ ;
/** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ; /** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ;
@@ -224,8 +314,11 @@ const __VLS_self = (await import('vue')).defineComponent({
gardensStore: gardensStore, gardensStore: gardensStore,
tasksStore: tasksStore, tasksStore: tasksStore,
pendingTasks: pendingTasks, pendingTasks: pendingTasks,
meteo3j: meteo3j, meteo7j: meteo7j,
stationCurrent: stationCurrent,
meteoCurrent: meteoCurrent,
formatDate: formatDate, formatDate: formatDate,
weatherIcon: weatherIcon,
}; };
}, },
}); });

View File

@@ -7,8 +7,38 @@
<p class="text-text-muted text-sm mb-6"> <p class="text-text-muted text-sm mb-6">
{{ garden.type }} · {{ garden.exposition ?? 'exposition non définie' }} {{ garden.type }} · {{ garden.exposition ?? 'exposition non définie' }}
<span v-if="garden.sol_type"> · Sol : {{ garden.sol_type }}</span> <span v-if="garden.sol_type"> · Sol : {{ garden.sol_type }}</span>
<span v-if="garden.surface_m2 != null"> · {{ garden.surface_m2 }} m²</span>
</p> </p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-6">
<div class="bg-bg-soft border border-bg-hard rounded-lg p-3 text-sm">
<div class="text-text-muted text-xs mb-1">Dimensions</div>
<div class="text-text">
<span v-if="garden.longueur_m != null && garden.largeur_m != null">
{{ garden.longueur_m }} m × {{ garden.largeur_m }} m
</span>
<span v-else>Non renseignées</span>
</div>
</div>
<div class="bg-bg-soft border border-bg-hard rounded-lg p-3 text-sm">
<div class="text-text-muted text-xs mb-1">Géolocalisation</div>
<div class="text-text">
<span v-if="garden.latitude != null && garden.longitude != null">
{{ garden.latitude }}, {{ garden.longitude }}
<span v-if="garden.altitude != null"> · {{ garden.altitude }} m</span>
</span>
<span v-else>Non renseignée</span>
</div>
<div v-if="garden.adresse" class="text-text-muted text-xs mt-1">{{ garden.adresse }}</div>
</div>
</div>
<div v-if="garden.photo_parcelle" class="mb-6">
<div class="text-text-muted text-xs uppercase tracking-widest mb-2">Photo parcelle</div>
<img :src="garden.photo_parcelle" alt="Photo parcelle"
class="w-full max-h-72 object-cover rounded-lg border border-bg-hard bg-bg-soft" />
</div>
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-3"> <h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">
Grille {{ garden.grille_largeur }}×{{ garden.grille_hauteur }} Grille {{ garden.grille_largeur }}×{{ garden.grille_hauteur }}
</h2> </h2>

View File

@@ -55,6 +55,70 @@ if (__VLS_ctx.garden) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({}); __VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.garden.sol_type); (__VLS_ctx.garden.sol_type);
} }
if (__VLS_ctx.garden.surface_m2 != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.garden.surface_m2);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 md:grid-cols-2 gap-3 mb-6" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-soft border border-bg-hard rounded-lg p-3 text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text" },
});
if (__VLS_ctx.garden.longueur_m != null && __VLS_ctx.garden.largeur_m != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.garden.longueur_m);
(__VLS_ctx.garden.largeur_m);
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-soft border border-bg-hard rounded-lg p-3 text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text" },
});
if (__VLS_ctx.garden.latitude != null && __VLS_ctx.garden.longitude != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.garden.latitude);
(__VLS_ctx.garden.longitude);
if (__VLS_ctx.garden.altitude != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.garden.altitude);
}
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
}
if (__VLS_ctx.garden.adresse) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mt-1" },
});
(__VLS_ctx.garden.adresse);
}
if (__VLS_ctx.garden.photo_parcelle) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mb-6" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs uppercase tracking-widest mb-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.garden.photo_parcelle),
alt: "Photo parcelle",
...{ class: "w-full max-h-72 object-cover rounded-lg border border-bg-hard bg-bg-soft" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text-muted text-xs uppercase tracking-widest mb-3" }, ...{ class: "text-text-muted text-xs uppercase tracking-widest mb-3" },
}); });
@@ -96,6 +160,47 @@ else {
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ; /** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['md:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['uppercase']} */ ;
/** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-72']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['uppercase']} */ ; /** @type {__VLS_StyleScopedClasses['uppercase']} */ ;

View File

@@ -14,6 +14,11 @@
<div class="text-text-muted text-xs mt-1"> <div class="text-text-muted text-xs mt-1">
{{ typeLabel(g.type) }} · {{ g.grille_largeur }}×{{ g.grille_hauteur }} cases {{ typeLabel(g.type) }} · {{ g.grille_largeur }}×{{ g.grille_hauteur }} cases
<span v-if="g.exposition"> · {{ g.exposition }}</span> <span v-if="g.exposition"> · {{ g.exposition }}</span>
<span v-if="g.carre_potager && g.carre_x_cm != null && g.carre_y_cm != null">
· Carré potager {{ g.carre_x_cm }}×{{ g.carre_y_cm }} cm
</span>
<span v-if="g.longueur_m != null && g.largeur_m != null"> · {{ g.longueur_m }}×{{ g.largeur_m }} m</span>
<span v-if="g.surface_m2 != null"> · {{ g.surface_m2 }} m²</span>
</div> </div>
</div> </div>
<button @click="startEdit(g)" class="text-yellow text-xs hover:underline px-2">Édit.</button> <button @click="startEdit(g)" class="text-yellow text-xs hover:underline px-2">Édit.</button>
@@ -26,15 +31,15 @@
<!-- Modal création / édition --> <!-- Modal création / édition -->
<div v-if="showForm" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm"> <div v-if="showForm" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft max-h-[90vh] overflow-y-auto"> <div class="bg-bg-hard rounded-xl p-6 w-full max-w-4xl border border-bg-soft max-h-[90vh] overflow-y-auto">
<h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier le jardin' : 'Nouveau jardin' }}</h2> <h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier le jardin' : 'Nouveau jardin' }}</h2>
<form @submit.prevent="submit" class="grid gap-3"> <form @submit.prevent="submit" class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div> <div>
<label class="text-text-muted text-xs block mb-1">Nom *</label> <label class="text-text-muted text-xs block mb-1">Nom *</label>
<input v-model="form.nom" required <input v-model="form.nom" required
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" /> class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
</div> </div>
<div> <div class="lg:col-span-2">
<label class="text-text-muted text-xs block mb-1">Description</label> <label class="text-text-muted text-xs block mb-1">Description</label>
<textarea v-model="form.description" rows="2" <textarea v-model="form.description" rows="2"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none resize-none" /> class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none resize-none" />
@@ -48,7 +53,26 @@
<option value="bac">Bac / Pot</option> <option value="bac">Bac / Pot</option>
</select> </select>
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="bg-bg rounded border border-bg-hard p-3 lg:col-span-2">
<label class="inline-flex items-center gap-2 text-sm text-text">
<input v-model="form.carre_potager" type="checkbox" class="accent-green" />
Carré potager
</label>
<p class="text-text-muted text-[11px] mt-1">Active les dimensions X/Y en centimètres pour un bac carré.</p>
</div>
<div v-if="form.carre_potager" class="grid grid-cols-1 sm:grid-cols-2 gap-3 lg:col-span-2">
<div>
<label class="text-text-muted text-xs block mb-1">Dimension X (cm)</label>
<input v-model.number="form.carre_x_cm" type="number" min="1" step="1"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Dimension Y (cm)</label>
<input v-model.number="form.carre_y_cm" type="number" min="1" step="1"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div> <div>
<label class="text-text-muted text-xs block mb-1">Largeur grille</label> <label class="text-text-muted text-xs block mb-1">Largeur grille</label>
<input v-model.number="form.grille_largeur" type="number" min="1" max="30" <input v-model.number="form.grille_largeur" type="number" min="1" max="30"
@@ -60,7 +84,55 @@
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" /> class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 lg:col-span-2">
<div>
<label class="text-text-muted text-xs block mb-1">Longueur (m)</label>
<input v-model.number="form.longueur_m" type="number" min="0" step="0.1"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Largeur (m)</label>
<input v-model.number="form.largeur_m" type="number" min="0" step="0.1"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Surface ()</label>
<input v-model.number="form.surface_m2" type="number" min="0" step="0.1"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
</div>
<div class="lg:col-span-2">
<label class="text-text-muted text-xs block mb-1">Photo parcelle (image)</label>
<input type="file" accept="image/*" @change="onPhotoSelected"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
<div v-if="photoPreview" class="mt-2">
<img :src="photoPreview" alt="Prévisualisation parcelle"
class="w-full max-h-44 object-cover rounded border border-bg-hard bg-bg-soft" />
</div>
</div>
<div class="lg:col-span-2">
<label class="text-text-muted text-xs block mb-1">Adresse / localisation</label>
<input v-model="form.adresse"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 lg:col-span-2">
<div>
<label class="text-text-muted text-xs block mb-1">Latitude</label>
<input v-model.number="form.latitude" type="number" step="0.000001"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Longitude</label>
<input v-model.number="form.longitude" type="number" step="0.000001"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Altitude (m)</label>
<input v-model.number="form.altitude" type="number" step="1"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" />
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 lg:col-span-2">
<div> <div>
<label class="text-text-muted text-xs block mb-1">Exposition</label> <label class="text-text-muted text-xs block mb-1">Exposition</label>
<select v-model="form.exposition" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm"> <select v-model="form.exposition" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
@@ -86,7 +158,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="flex gap-2 mt-2"> <div class="flex gap-2 mt-2 lg:col-span-2">
<button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold"> <button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold">
{{ editId ? 'Enregistrer' : 'Créer' }} {{ editId ? 'Enregistrer' : 'Créer' }}
</button> </button>
@@ -102,16 +174,28 @@
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useGardensStore } from '@/stores/gardens' import { useGardensStore } from '@/stores/gardens'
import type { Garden } from '@/api/gardens' import { gardensApi, type Garden } from '@/api/gardens'
const router = useRouter() const router = useRouter()
const store = useGardensStore() const store = useGardensStore()
const showForm = ref(false) const showForm = ref(false)
const editId = ref<number | null>(null) const editId = ref<number | null>(null)
const photoFile = ref<File | null>(null)
const photoPreview = ref('')
const form = reactive({ const form = reactive({
nom: '', description: '', type: 'plein_air', nom: '', description: '', type: 'plein_air',
grille_largeur: 6, grille_hauteur: 4, grille_largeur: 6, grille_hauteur: 4,
carre_potager: false,
carre_x_cm: undefined as number | undefined,
carre_y_cm: undefined as number | undefined,
longueur_m: undefined as number | undefined,
largeur_m: undefined as number | undefined,
surface_m2: undefined as number | undefined,
latitude: undefined as number | undefined,
longitude: undefined as number | undefined,
altitude: undefined as number | undefined,
adresse: '',
exposition: '', sol_type: '', exposition: '', sol_type: '',
}) })
@@ -121,7 +205,16 @@ function typeLabel(t: string) {
function openCreate() { function openCreate() {
editId.value = null editId.value = null
Object.assign(form, { nom: '', description: '', type: 'plein_air', grille_largeur: 6, grille_hauteur: 4, exposition: '', sol_type: '' }) Object.assign(form, {
nom: '', description: '', type: 'plein_air',
grille_largeur: 6, grille_hauteur: 4,
carre_potager: false, carre_x_cm: undefined, carre_y_cm: undefined,
longueur_m: undefined, largeur_m: undefined, surface_m2: undefined,
latitude: undefined, longitude: undefined, altitude: undefined, adresse: '',
exposition: '', sol_type: '',
})
photoFile.value = null
photoPreview.value = ''
showForm.value = true showForm.value = true
} }
@@ -130,18 +223,73 @@ function startEdit(g: Garden) {
Object.assign(form, { Object.assign(form, {
nom: g.nom, description: g.description || '', nom: g.nom, description: g.description || '',
type: g.type, grille_largeur: g.grille_largeur, grille_hauteur: g.grille_hauteur, type: g.type, grille_largeur: g.grille_largeur, grille_hauteur: g.grille_hauteur,
carre_potager: !!g.carre_potager, carre_x_cm: g.carre_x_cm, carre_y_cm: g.carre_y_cm,
longueur_m: g.longueur_m, largeur_m: g.largeur_m, surface_m2: g.surface_m2,
latitude: g.latitude, longitude: g.longitude, altitude: g.altitude, adresse: g.adresse || '',
exposition: g.exposition || '', sol_type: g.sol_type || '', exposition: g.exposition || '', sol_type: g.sol_type || '',
}) })
photoFile.value = null
photoPreview.value = g.photo_parcelle || ''
showForm.value = true showForm.value = true
} }
function closeForm() { showForm.value = false; editId.value = null } function closeForm() { showForm.value = false; editId.value = null }
function onPhotoSelected(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0] || null
photoFile.value = file
if (file) {
photoPreview.value = URL.createObjectURL(file)
}
}
async function submit() { async function submit() {
const autoLongueur =
form.carre_potager && form.carre_x_cm != null
? Number((form.carre_x_cm / 100).toFixed(2))
: form.longueur_m
const autoLargeur =
form.carre_potager && form.carre_y_cm != null
? Number((form.carre_y_cm / 100).toFixed(2))
: form.largeur_m
const inferredSurface =
form.surface_m2 ??
((autoLongueur != null && autoLargeur != null)
? Number((autoLongueur * autoLargeur).toFixed(2))
: undefined)
const payload: Partial<Garden> = {
nom: form.nom,
description: form.description || undefined,
type: form.type,
grille_largeur: form.grille_largeur,
grille_hauteur: form.grille_hauteur,
carre_potager: form.carre_potager,
carre_x_cm: form.carre_potager ? form.carre_x_cm : undefined,
carre_y_cm: form.carre_potager ? form.carre_y_cm : undefined,
longueur_m: autoLongueur,
largeur_m: autoLargeur,
surface_m2: inferredSurface,
latitude: form.latitude,
longitude: form.longitude,
altitude: form.altitude,
adresse: form.adresse || undefined,
exposition: form.exposition || undefined,
sol_type: form.sol_type || undefined,
}
let saved: Garden
if (editId.value) { if (editId.value) {
await store.update(editId.value, { ...form }) saved = await store.update(editId.value, payload)
} else { } else {
await store.create({ ...form }) saved = await store.create(payload)
}
if (photoFile.value && saved.id) {
await gardensApi.uploadPhoto(saved.id, photoFile.value)
await store.fetchAll()
} }
closeForm() closeForm()
} }

View File

@@ -2,16 +2,112 @@
import { onMounted, reactive, ref } from 'vue'; import { onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useGardensStore } from '@/stores/gardens'; import { useGardensStore } from '@/stores/gardens';
import { gardensApi } from '@/api/gardens';
const router = useRouter(); const router = useRouter();
const store = useGardensStore(); const store = useGardensStore();
const showForm = ref(false); const showForm = ref(false);
const form = reactive({ nom: '', type: 'plein_air', grille_largeur: 6, grille_hauteur: 4 }); const editId = ref(null);
onMounted(() => store.fetchAll()); const photoFile = ref(null);
async function submit() { const photoPreview = ref('');
await store.create({ ...form }); const form = reactive({
showForm.value = false; nom: '', description: '', type: 'plein_air',
Object.assign(form, { nom: '', type: 'plein_air', grille_largeur: 6, grille_hauteur: 4 }); grille_largeur: 6, grille_hauteur: 4,
carre_potager: false,
carre_x_cm: undefined,
carre_y_cm: undefined,
longueur_m: undefined,
largeur_m: undefined,
surface_m2: undefined,
latitude: undefined,
longitude: undefined,
altitude: undefined,
adresse: '',
exposition: '', sol_type: '',
});
function typeLabel(t) {
return { plein_air: 'Plein air', serre: 'Serre', tunnel: 'Tunnel', bac: 'Bac/Pot' }[t] ?? t;
} }
function openCreate() {
editId.value = null;
Object.assign(form, {
nom: '', description: '', type: 'plein_air',
grille_largeur: 6, grille_hauteur: 4,
carre_potager: false, carre_x_cm: undefined, carre_y_cm: undefined,
longueur_m: undefined, largeur_m: undefined, surface_m2: undefined,
latitude: undefined, longitude: undefined, altitude: undefined, adresse: '',
exposition: '', sol_type: '',
});
photoFile.value = null;
photoPreview.value = '';
showForm.value = true;
}
function startEdit(g) {
editId.value = g.id;
Object.assign(form, {
nom: g.nom, description: g.description || '',
type: g.type, grille_largeur: g.grille_largeur, grille_hauteur: g.grille_hauteur,
carre_potager: !!g.carre_potager, carre_x_cm: g.carre_x_cm, carre_y_cm: g.carre_y_cm,
longueur_m: g.longueur_m, largeur_m: g.largeur_m, surface_m2: g.surface_m2,
latitude: g.latitude, longitude: g.longitude, altitude: g.altitude, adresse: g.adresse || '',
exposition: g.exposition || '', sol_type: g.sol_type || '',
});
photoFile.value = null;
photoPreview.value = g.photo_parcelle || '';
showForm.value = true;
}
function closeForm() { showForm.value = false; editId.value = null; }
function onPhotoSelected(event) {
const input = event.target;
const file = input.files?.[0] || null;
photoFile.value = file;
if (file) {
photoPreview.value = URL.createObjectURL(file);
}
}
async function submit() {
const autoLongueur = form.carre_potager && form.carre_x_cm != null
? Number((form.carre_x_cm / 100).toFixed(2))
: form.longueur_m;
const autoLargeur = form.carre_potager && form.carre_y_cm != null
? Number((form.carre_y_cm / 100).toFixed(2))
: form.largeur_m;
const inferredSurface = form.surface_m2 ??
((autoLongueur != null && autoLargeur != null)
? Number((autoLongueur * autoLargeur).toFixed(2))
: undefined);
const payload = {
nom: form.nom,
description: form.description || undefined,
type: form.type,
grille_largeur: form.grille_largeur,
grille_hauteur: form.grille_hauteur,
carre_potager: form.carre_potager,
carre_x_cm: form.carre_potager ? form.carre_x_cm : undefined,
carre_y_cm: form.carre_potager ? form.carre_y_cm : undefined,
longueur_m: autoLongueur,
largeur_m: autoLargeur,
surface_m2: inferredSurface,
latitude: form.latitude,
longitude: form.longitude,
altitude: form.altitude,
adresse: form.adresse || undefined,
exposition: form.exposition || undefined,
sol_type: form.sol_type || undefined,
};
let saved;
if (editId.value) {
saved = await store.update(editId.value, payload);
}
else {
saved = await store.create(payload);
}
if (photoFile.value && saved.id) {
await gardensApi.uploadPhoto(saved.id, photoFile.value);
await store.fetchAll();
}
closeForm();
}
onMounted(() => store.fetchAll());
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {}; const __VLS_ctx = {};
let __VLS_components; let __VLS_components;
@@ -26,18 +122,86 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1
...{ class: "text-2xl font-bold text-green" }, ...{ class: "text-2xl font-bold text-green" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.openCreate) },
__VLS_ctx.showForm = !__VLS_ctx.showForm; ...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
} },
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90 transition-opacity" },
}); });
if (__VLS_ctx.showForm) { if (__VLS_ctx.store.loading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onSubmit: (__VLS_ctx.submit) }, ...{ class: "text-text-muted text-sm" },
...{ class: "bg-bg-soft rounded-lg p-4 mb-6 border border-green/30" }, });
}
for (const [g] of __VLS_getVForSourceType((__VLS_ctx.store.gardens))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (g.id),
...{ class: "bg-bg-soft rounded-lg p-4 mb-3 border border-bg-hard flex items-center gap-3 group" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid gap-3" }, ...{ onClick: (...[$event]) => {
__VLS_ctx.router.push(`/jardins/${g.id}`);
} },
...{ class: "flex-1 cursor-pointer" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text font-medium group-hover:text-green transition-colors" },
});
(g.nom);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mt-1" },
});
(__VLS_ctx.typeLabel(g.type));
(g.grille_largeur);
(g.grille_hauteur);
if (g.exposition) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(g.exposition);
}
if (g.carre_potager && g.carre_x_cm != null && g.carre_y_cm != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(g.carre_x_cm);
(g.carre_y_cm);
}
if (g.longueur_m != null && g.largeur_m != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(g.longueur_m);
(g.largeur_m);
}
if (g.surface_m2 != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(g.surface_m2);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.startEdit(g);
} },
...{ class: "text-yellow text-xs hover:underline px-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.store.remove(g.id);
} },
...{ class: "text-text-muted hover:text-red text-sm px-2" },
});
}
if (!__VLS_ctx.store.loading && !__VLS_ctx.store.gardens.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm text-center py-8" },
});
}
if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (__VLS_ctx.closeForm) },
...{ class: "fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-hard rounded-xl p-6 w-full max-w-4xl border border-bg-soft max-h-[90vh] overflow-y-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-bold text-lg mb-4" },
});
(__VLS_ctx.editId ? 'Modifier le jardin' : 'Nouveau jardin');
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.submit) },
...{ class: "grid grid-cols-1 lg:grid-cols-2 gap-3" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({}); __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
@@ -48,6 +212,17 @@ if (__VLS_ctx.showForm) {
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" }, ...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
}); });
(__VLS_ctx.form.nom); (__VLS_ctx.form.nom);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.textarea)({
value: (__VLS_ctx.form.description),
rows: "2",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none resize-none" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({}); __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" }, ...{ class: "text-text-muted text-xs block mb-1" },
@@ -65,8 +240,52 @@ if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "tunnel", value: "tunnel",
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "bac",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-2 gap-3" }, ...{ class: "bg-bg rounded border border-bg-hard p-3 lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "inline-flex items-center gap-2 text-sm text-text" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "checkbox",
...{ class: "accent-green" },
});
(__VLS_ctx.form.carre_potager);
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-[11px] mt-1" },
});
if (__VLS_ctx.form.carre_potager) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 sm:grid-cols-2 gap-3 lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "1",
step: "1",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.carre_x_cm);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "1",
step: "1",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.carre_y_cm);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 sm:grid-cols-2 gap-3" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({}); __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
@@ -75,7 +294,7 @@ if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number", type: "number",
min: "1", min: "1",
max: "20", max: "30",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" }, ...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
}); });
(__VLS_ctx.form.grille_largeur); (__VLS_ctx.form.grille_largeur);
@@ -86,65 +305,186 @@ if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number", type: "number",
min: "1", min: "1",
max: "20", max: "30",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" }, ...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
}); });
(__VLS_ctx.form.grille_hauteur); (__VLS_ctx.form.grille_hauteur);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 mt-4" }, ...{ class: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "0",
step: "0.1",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.longueur_m);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "0",
step: "0.1",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.largeur_m);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "0",
step: "0.1",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.surface_m2);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (__VLS_ctx.onPhotoSelected) },
type: "file",
accept: "image/*",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
});
if (__VLS_ctx.photoPreview) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.photoPreview),
alt: "Prévisualisation parcelle",
...{ class: "w-full max-h-44 object-cover rounded border border-bg-hard bg-bg-soft" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
});
(__VLS_ctx.form.adresse);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
step: "0.000001",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.latitude);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
step: "0.000001",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.longitude);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
step: "1",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
(__VLS_ctx.form.altitude);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 sm:grid-cols-2 gap-3 lg:col-span-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.form.exposition),
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "Nord",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "Est",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "Sud",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "Ouest",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "Sud-Est",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "Sud-Ouest",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.form.sol_type),
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "argileux",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "limoneux",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "sableux",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "calcaire",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "humifère",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "mixte",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 mt-2 lg:col-span-2" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
type: "submit", type: "submit",
...{ class: "bg-green text-bg px-4 py-2 rounded text-sm font-semibold" }, ...{ class: "bg-green text-bg px-4 py-2 rounded text-sm font-semibold" },
}); });
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer');
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.closeForm) },
if (!(__VLS_ctx.showForm))
return;
__VLS_ctx.showForm = false;
} },
type: "button", type: "button",
...{ class: "text-text-muted text-sm px-4 py-2 hover:text-text" }, ...{ class: "text-text-muted text-sm px-4 py-2 hover:text-text" },
}); });
} }
if (__VLS_ctx.store.loading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm" },
});
}
for (const [g] of __VLS_getVForSourceType((__VLS_ctx.store.gardens))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
__VLS_ctx.router.push(`/jardins/${g.id}`);
} },
key: (g.id),
...{ class: "bg-bg-soft rounded-lg p-4 mb-3 border border-bg-hard flex items-center gap-3 cursor-pointer hover:border-green transition-colors group" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text font-medium group-hover:text-green transition-colors" },
});
(g.nom);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mt-1" },
});
(g.type);
(g.grille_largeur);
(g.grille_hauteur);
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.store.remove(g.id);
} },
...{ class: "text-text-muted hover:text-red text-sm px-2 py-1 rounded hover:bg-bg transition-colors" },
});
}
if (!__VLS_ctx.store.loading && !__VLS_ctx.store.gardens.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm text-center py-8" },
});
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ; /** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
@@ -163,14 +503,63 @@ if (!__VLS_ctx.store.loading && !__VLS_ctx.store.gardens.length) {
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ; /** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ; /** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-opacity']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ; /** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ; /** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ; /** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ; /** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-green/30']} */ ; /** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['group']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['group-hover:text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['py-8']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-6']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-4xl']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-[90vh]']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-y-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ; /** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ; /** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
@@ -187,6 +576,69 @@ if (!__VLS_ctx.store.loading && !__VLS_ctx.store.gardens.length) {
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ; /** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ; /** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['resize-none']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['accent-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ; /** @type {__VLS_StyleScopedClasses['block']} */ ;
@@ -201,7 +653,8 @@ if (!__VLS_ctx.store.loading && !__VLS_ctx.store.gardens.length) {
/** @type {__VLS_StyleScopedClasses['text-text']} */ ; /** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ; /** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-2']} */ ; /** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ; /** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
@@ -229,9 +682,171 @@ if (!__VLS_ctx.store.loading && !__VLS_ctx.store.gardens.length) {
/** @type {__VLS_StyleScopedClasses['py-2']} */ ; /** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ; /** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-44']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['sm:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ; /** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-4']} */ ; /** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:col-span-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ; /** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ; /** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ; /** @type {__VLS_StyleScopedClasses['px-4']} */ ;
@@ -244,41 +859,6 @@ if (!__VLS_ctx.store.loading && !__VLS_ctx.store.gardens.length) {
/** @type {__VLS_StyleScopedClasses['px-4']} */ ; /** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ; /** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ; /** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['group']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['group-hover:text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['py-8']} */ ;
var __VLS_dollars; var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({ const __VLS_self = (await import('vue')).defineComponent({
setup() { setup() {
@@ -286,7 +866,14 @@ const __VLS_self = (await import('vue')).defineComponent({
router: router, router: router,
store: store, store: store,
showForm: showForm, showForm: showForm,
editId: editId,
photoPreview: photoPreview,
form: form, form: form,
typeLabel: typeLabel,
openCreate: openCreate,
startEdit: startEdit,
closeForm: closeForm,
onPhotoSelected: onPhotoSelected,
submit: submit, submit: submit,
}; };
}, },

View File

@@ -2,7 +2,7 @@
<div class="p-4 max-w-4xl mx-auto"> <div class="p-4 max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-yellow">🔧 Outils</h1> <h1 class="text-2xl font-bold text-yellow">🔧 Outils</h1>
<button @click="showForm = true" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">+ Ajouter</button> <button @click="openCreate" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">+ Ajouter</button>
</div> </div>
<div v-if="toolsStore.loading" class="text-text-muted text-sm">Chargement...</div> <div v-if="toolsStore.loading" class="text-text-muted text-sm">Chargement...</div>
@@ -19,11 +19,28 @@
</div> </div>
<span v-if="t.categorie" class="text-xs text-yellow bg-yellow/10 rounded-full px-2 py-0.5 w-fit">{{ t.categorie }}</span> <span v-if="t.categorie" class="text-xs text-yellow bg-yellow/10 rounded-full px-2 py-0.5 w-fit">{{ t.categorie }}</span>
<p v-if="t.description" class="text-text-muted text-xs">{{ t.description }}</p> <p v-if="t.description" class="text-text-muted text-xs">{{ t.description }}</p>
<p v-if="t.boutique_nom || t.prix_achat != null" class="text-text-muted text-xs">
<span v-if="t.boutique_nom">🛒 {{ t.boutique_nom }}</span>
<span v-if="t.prix_achat != null"> · 💶 {{ t.prix_achat }} </span>
</p>
<a v-if="t.boutique_url" :href="t.boutique_url" target="_blank" rel="noopener noreferrer"
class="text-blue text-xs hover:underline truncate">🔗 Boutique</a>
<a v-if="t.video_url" :href="t.video_url" target="_blank" rel="noopener noreferrer"
class="text-aqua text-xs hover:underline truncate">🎬 Vidéo</a>
<a v-if="t.notice_fichier_url" :href="t.notice_fichier_url" target="_blank" rel="noopener noreferrer"
class="text-aqua text-xs hover:underline truncate">📄 Notice</a>
<div v-if="t.photo_url || t.video_url" class="mt-auto pt-2 space-y-2">
<img v-if="t.photo_url" :src="t.photo_url" alt="photo outil"
class="w-full h-28 object-cover rounded border border-bg-hard bg-bg" />
<video v-if="t.video_url" :src="t.video_url" controls muted
class="w-full h-36 object-cover rounded border border-bg-hard bg-bg" />
</div>
</div> </div>
</div> </div>
<div v-if="showForm" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm"> <div v-if="showForm" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-sm border border-bg-soft"> <div class="bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft">
<h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier l\'outil' : 'Nouvel outil' }}</h2> <h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier l\'outil' : 'Nouvel outil' }}</h2>
<form @submit.prevent="submitTool" class="flex flex-col gap-3"> <form @submit.prevent="submitTool" class="flex flex-col gap-3">
<input v-model="form.nom" placeholder="Nom de l'outil *" required <input v-model="form.nom" placeholder="Nom de l'outil *" required
@@ -35,11 +52,41 @@
<option value="fourche">Fourche</option> <option value="fourche">Fourche</option>
<option value="griffe">Griffe/Grelinette</option> <option value="griffe">Griffe/Grelinette</option>
<option value="arrosage">Arrosage</option> <option value="arrosage">Arrosage</option>
<option value="taille">Taille</option> <option value="taille">Taille</option>
<option value="autre">Autre</option> <option value="autre">Autre</option>
</select> </select>
<div class="grid grid-cols-2 gap-3">
<input v-model="form.boutique_nom" placeholder="Nom boutique"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
<input v-model.number="form.prix_achat" type="number" min="0" step="0.01" placeholder="Prix achat (€)"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
</div>
<input v-model="form.boutique_url" type="url" placeholder="URL boutique (https://...)"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
<textarea v-model="form.description" placeholder="Description..." <textarea v-model="form.description" placeholder="Description..."
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow resize-none h-16" /> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow resize-none h-16" />
<div>
<label class="text-text-muted text-xs block mb-1">Photo de l'outil</label>
<input type="file" accept="image/*" @change="onPhotoSelected"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
<img v-if="photoPreview" :src="photoPreview" alt="Prévisualisation photo"
class="mt-2 w-full h-28 object-cover rounded border border-bg-hard bg-bg" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Vidéo de l'outil</label>
<input type="file" accept="video/*" @change="onVideoSelected"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
<video v-if="videoPreview" :src="videoPreview" controls muted
class="mt-2 w-full h-36 object-cover rounded border border-bg-hard bg-bg" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Notice (fichier texte)</label>
<input type="file" accept=".txt,.md,text/plain" @change="onNoticeSelected"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" />
<div v-if="noticeFileName || form.notice_fichier_url" class="text-text-muted text-xs mt-1 truncate">
{{ noticeFileName || fileNameFromUrl(form.notice_fichier_url || '') }}
</div>
</div>
<div class="flex gap-2 justify-end"> <div class="flex gap-2 justify-end">
<button type="button" @click="closeForm" class="px-4 py-2 text-text-muted hover:text-text text-sm">Annuler</button> <button type="button" @click="closeForm" class="px-4 py-2 text-text-muted hover:text-text text-sm">Annuler</button>
<button type="submit" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"> <button type="submit" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
@@ -54,29 +101,152 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref } from 'vue'
import axios from 'axios'
import { useToolsStore } from '@/stores/tools' import { useToolsStore } from '@/stores/tools'
import type { Tool } from '@/api/tools' import type { Tool } from '@/api/tools'
const toolsStore = useToolsStore() const toolsStore = useToolsStore()
const showForm = ref(false) const showForm = ref(false)
const editId = ref<number | null>(null) const editId = ref<number | null>(null)
const form = reactive({ nom: '', categorie: '', description: '' }) const photoFile = ref<File | null>(null)
const videoFile = ref<File | null>(null)
const noticeFile = ref<File | null>(null)
const photoPreview = ref('')
const videoPreview = ref('')
const noticeFileName = ref('')
const form = reactive({
nom: '',
categorie: '',
description: '',
boutique_nom: '',
boutique_url: '',
prix_achat: undefined as number | undefined,
photo_url: '',
video_url: '',
notice_fichier_url: '',
})
function startEdit(t: Tool) { function fileNameFromUrl(url: string) {
editId.value = t.id! if (!url) return ''
Object.assign(form, { nom: t.nom, categorie: t.categorie || '', description: t.description || '' }) return url.split('/').pop() || url
}
function resetForm() {
Object.assign(form, {
nom: '',
categorie: '',
description: '',
boutique_nom: '',
boutique_url: '',
prix_achat: undefined,
photo_url: '',
video_url: '',
notice_fichier_url: '',
})
}
function openCreate() {
editId.value = null
resetForm()
photoFile.value = null
videoFile.value = null
noticeFile.value = null
photoPreview.value = ''
videoPreview.value = ''
noticeFileName.value = ''
showForm.value = true showForm.value = true
} }
function closeForm() { showForm.value = false; editId.value = null } function onPhotoSelected(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0] || null
photoFile.value = file
if (file) photoPreview.value = URL.createObjectURL(file)
}
function onVideoSelected(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0] || null
videoFile.value = file
if (file) videoPreview.value = URL.createObjectURL(file)
}
function onNoticeSelected(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0] || null
noticeFile.value = file
noticeFileName.value = file?.name || ''
}
function startEdit(t: Tool) {
editId.value = t.id!
Object.assign(form, {
nom: t.nom,
categorie: t.categorie || '',
description: t.description || '',
boutique_nom: t.boutique_nom || '',
boutique_url: t.boutique_url || '',
prix_achat: t.prix_achat,
photo_url: t.photo_url || '',
video_url: t.video_url || '',
notice_fichier_url: t.notice_fichier_url || '',
})
photoFile.value = null
videoFile.value = null
noticeFile.value = null
photoPreview.value = t.photo_url || ''
videoPreview.value = t.video_url || ''
noticeFileName.value = fileNameFromUrl(t.notice_fichier_url || '')
showForm.value = true
}
function closeForm() {
showForm.value = false
editId.value = null
photoFile.value = null
videoFile.value = null
noticeFile.value = null
photoPreview.value = ''
videoPreview.value = ''
noticeFileName.value = ''
}
async function uploadFile(file: File): Promise<string> {
const fd = new FormData()
fd.append('file', file)
const { data } = await axios.post('/api/upload', fd)
return data.url as string
}
async function submitTool() { async function submitTool() {
if (editId.value) { let saved: Tool
await toolsStore.update(editId.value, { ...form }) const payload: Partial<Tool> = {
} else { nom: form.nom,
await toolsStore.create({ ...form }) categorie: form.categorie || undefined,
description: form.description || undefined,
boutique_nom: form.boutique_nom || undefined,
boutique_url: form.boutique_url || undefined,
prix_achat: form.prix_achat,
photo_url: form.photo_url || undefined,
video_url: form.video_url || undefined,
notice_fichier_url: form.notice_fichier_url || undefined,
} }
Object.assign(form, { nom: '', categorie: '', description: '' })
if (editId.value) {
saved = await toolsStore.update(editId.value, payload)
} else {
saved = await toolsStore.create(payload)
}
if (saved.id && (photoFile.value || videoFile.value || noticeFile.value)) {
const patch: Partial<Tool> = {}
if (photoFile.value) patch.photo_url = await uploadFile(photoFile.value)
if (videoFile.value) patch.video_url = await uploadFile(videoFile.value)
if (noticeFile.value) patch.notice_fichier_url = await uploadFile(noticeFile.value)
if (Object.keys(patch).length) await toolsStore.update(saved.id, patch)
}
resetForm()
closeForm() closeForm()
} }

View File

@@ -1,13 +1,145 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" /> /// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { onMounted, reactive, ref } from 'vue'; import { onMounted, reactive, ref } from 'vue';
import axios from 'axios';
import { useToolsStore } from '@/stores/tools'; import { useToolsStore } from '@/stores/tools';
const toolsStore = useToolsStore(); const toolsStore = useToolsStore();
const showForm = ref(false); const showForm = ref(false);
const form = reactive({ nom: '', categorie: '', description: '' }); const editId = ref(null);
async function submitTool() { const photoFile = ref(null);
await toolsStore.create({ ...form }); const videoFile = ref(null);
Object.assign(form, { nom: '', categorie: '', description: '' }); const noticeFile = ref(null);
const photoPreview = ref('');
const videoPreview = ref('');
const noticeFileName = ref('');
const form = reactive({
nom: '',
categorie: '',
description: '',
boutique_nom: '',
boutique_url: '',
prix_achat: undefined,
photo_url: '',
video_url: '',
notice_fichier_url: '',
});
function fileNameFromUrl(url) {
if (!url)
return '';
return url.split('/').pop() || url;
}
function resetForm() {
Object.assign(form, {
nom: '',
categorie: '',
description: '',
boutique_nom: '',
boutique_url: '',
prix_achat: undefined,
photo_url: '',
video_url: '',
notice_fichier_url: '',
});
}
function openCreate() {
editId.value = null;
resetForm();
photoFile.value = null;
videoFile.value = null;
noticeFile.value = null;
photoPreview.value = '';
videoPreview.value = '';
noticeFileName.value = '';
showForm.value = true;
}
function onPhotoSelected(event) {
const input = event.target;
const file = input.files?.[0] || null;
photoFile.value = file;
if (file)
photoPreview.value = URL.createObjectURL(file);
}
function onVideoSelected(event) {
const input = event.target;
const file = input.files?.[0] || null;
videoFile.value = file;
if (file)
videoPreview.value = URL.createObjectURL(file);
}
function onNoticeSelected(event) {
const input = event.target;
const file = input.files?.[0] || null;
noticeFile.value = file;
noticeFileName.value = file?.name || '';
}
function startEdit(t) {
editId.value = t.id;
Object.assign(form, {
nom: t.nom,
categorie: t.categorie || '',
description: t.description || '',
boutique_nom: t.boutique_nom || '',
boutique_url: t.boutique_url || '',
prix_achat: t.prix_achat,
photo_url: t.photo_url || '',
video_url: t.video_url || '',
notice_fichier_url: t.notice_fichier_url || '',
});
photoFile.value = null;
videoFile.value = null;
noticeFile.value = null;
photoPreview.value = t.photo_url || '';
videoPreview.value = t.video_url || '';
noticeFileName.value = fileNameFromUrl(t.notice_fichier_url || '');
showForm.value = true;
}
function closeForm() {
showForm.value = false; showForm.value = false;
editId.value = null;
photoFile.value = null;
videoFile.value = null;
noticeFile.value = null;
photoPreview.value = '';
videoPreview.value = '';
noticeFileName.value = '';
}
async function uploadFile(file) {
const fd = new FormData();
fd.append('file', file);
const { data } = await axios.post('/api/upload', fd);
return data.url;
}
async function submitTool() {
let saved;
const payload = {
nom: form.nom,
categorie: form.categorie || undefined,
description: form.description || undefined,
boutique_nom: form.boutique_nom || undefined,
boutique_url: form.boutique_url || undefined,
prix_achat: form.prix_achat,
photo_url: form.photo_url || undefined,
video_url: form.video_url || undefined,
notice_fichier_url: form.notice_fichier_url || undefined,
};
if (editId.value) {
saved = await toolsStore.update(editId.value, payload);
}
else {
saved = await toolsStore.create(payload);
}
if (saved.id && (photoFile.value || videoFile.value || noticeFile.value)) {
const patch = {};
if (photoFile.value)
patch.photo_url = await uploadFile(photoFile.value);
if (videoFile.value)
patch.video_url = await uploadFile(videoFile.value);
if (noticeFile.value)
patch.notice_fichier_url = await uploadFile(noticeFile.value);
if (Object.keys(patch).length)
await toolsStore.update(saved.id, patch);
}
resetForm();
closeForm();
} }
async function removeTool(id) { async function removeTool(id) {
if (confirm('Supprimer cet outil ?')) if (confirm('Supprimer cet outil ?'))
@@ -28,9 +160,7 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1
...{ class: "text-2xl font-bold text-yellow" }, ...{ class: "text-2xl font-bold text-yellow" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.openCreate) },
__VLS_ctx.showForm = true;
} },
...{ class: "bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" }, ...{ class: "bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
}); });
if (__VLS_ctx.toolsStore.loading) { if (__VLS_ctx.toolsStore.loading) {
@@ -58,6 +188,15 @@ for (const [t] of __VLS_getVForSourceType((__VLS_ctx.toolsStore.tools))) {
...{ class: "text-text font-semibold" }, ...{ class: "text-text font-semibold" },
}); });
(t.nom); (t.nom);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.startEdit(t);
} },
...{ class: "text-yellow text-xs hover:underline" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (...[$event]) => {
__VLS_ctx.removeTool(t.id); __VLS_ctx.removeTool(t.id);
@@ -76,22 +215,76 @@ for (const [t] of __VLS_getVForSourceType((__VLS_ctx.toolsStore.tools))) {
}); });
(t.description); (t.description);
} }
if (t.boutique_nom || t.prix_achat != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-xs" },
});
if (t.boutique_nom) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(t.boutique_nom);
}
if (t.prix_achat != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(t.prix_achat);
}
}
if (t.boutique_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.a, __VLS_intrinsicElements.a)({
href: (t.boutique_url),
target: "_blank",
rel: "noopener noreferrer",
...{ class: "text-blue text-xs hover:underline truncate" },
});
}
if (t.video_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.a, __VLS_intrinsicElements.a)({
href: (t.video_url),
target: "_blank",
rel: "noopener noreferrer",
...{ class: "text-aqua text-xs hover:underline truncate" },
});
}
if (t.notice_fichier_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.a, __VLS_intrinsicElements.a)({
href: (t.notice_fichier_url),
target: "_blank",
rel: "noopener noreferrer",
...{ class: "text-aqua text-xs hover:underline truncate" },
});
}
if (t.photo_url || t.video_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-auto pt-2 space-y-2" },
});
if (t.photo_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (t.photo_url),
alt: "photo outil",
...{ class: "w-full h-28 object-cover rounded border border-bg-hard bg-bg" },
});
}
if (t.video_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.video)({
src: (t.video_url),
controls: true,
muted: true,
...{ class: "w-full h-36 object-cover rounded border border-bg-hard bg-bg" },
});
}
}
} }
if (__VLS_ctx.showForm) { if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.closeForm) },
if (!(__VLS_ctx.showForm))
return;
__VLS_ctx.showForm = false;
} },
...{ class: "fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" }, ...{ class: "fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-hard rounded-xl p-6 w-full max-w-sm border border-bg-soft" }, ...{ class: "bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-bold text-lg mb-4" }, ...{ class: "text-text font-bold text-lg mb-4" },
}); });
(__VLS_ctx.editId ? 'Modifier l\'outil' : 'Nouvel outil');
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.submitTool) }, ...{ onSubmit: (__VLS_ctx.submitTool) },
...{ class: "flex flex-col gap-3" }, ...{ class: "flex flex-col gap-3" },
@@ -127,20 +320,89 @@ if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "autre", value: "autre",
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-2 gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
placeholder: "Nom boutique",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
(__VLS_ctx.form.boutique_nom);
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "0",
step: "0.01",
placeholder: "Prix achat (€)",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
(__VLS_ctx.form.prix_achat);
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "url",
placeholder: "URL boutique (https://...)",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
(__VLS_ctx.form.boutique_url);
__VLS_asFunctionalElement(__VLS_intrinsicElements.textarea)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.textarea)({
value: (__VLS_ctx.form.description), value: (__VLS_ctx.form.description),
placeholder: "Description...", placeholder: "Description...",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow resize-none h-16" }, ...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow resize-none h-16" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (__VLS_ctx.onPhotoSelected) },
type: "file",
accept: "image/*",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
if (__VLS_ctx.photoPreview) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (__VLS_ctx.photoPreview),
alt: "Prévisualisation photo",
...{ class: "mt-2 w-full h-28 object-cover rounded border border-bg-hard bg-bg" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (__VLS_ctx.onVideoSelected) },
type: "file",
accept: "video/*",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
if (__VLS_ctx.videoPreview) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.video)({
src: (__VLS_ctx.videoPreview),
controls: true,
muted: true,
...{ class: "mt-2 w-full h-36 object-cover rounded border border-bg-hard bg-bg" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onChange: (__VLS_ctx.onNoticeSelected) },
type: "file",
accept: ".txt,.md,text/plain",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-yellow" },
});
if (__VLS_ctx.noticeFileName || __VLS_ctx.form.notice_fichier_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mt-1 truncate" },
});
(__VLS_ctx.noticeFileName || __VLS_ctx.fileNameFromUrl(__VLS_ctx.form.notice_fichier_url || ''));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 justify-end" }, ...{ class: "flex gap-2 justify-end" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.closeForm) },
if (!(__VLS_ctx.showForm))
return;
__VLS_ctx.showForm = false;
} },
type: "button", type: "button",
...{ class: "px-4 py-2 text-text-muted hover:text-text text-sm" }, ...{ class: "px-4 py-2 text-text-muted hover:text-text text-sm" },
}); });
@@ -148,6 +410,7 @@ if (__VLS_ctx.showForm) {
type: "submit", type: "submit",
...{ class: "bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" }, ...{ class: "bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
}); });
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer');
} }
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-4xl']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-4xl']} */ ;
@@ -190,6 +453,11 @@ if (__VLS_ctx.showForm) {
/** @type {__VLS_StyleScopedClasses['justify-between']} */ ; /** @type {__VLS_StyleScopedClasses['justify-between']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ; /** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ; /** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-red']} */ ; /** @type {__VLS_StyleScopedClasses['text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ; /** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
@@ -202,6 +470,37 @@ if (__VLS_ctx.showForm) {
/** @type {__VLS_StyleScopedClasses['w-fit']} */ ; /** @type {__VLS_StyleScopedClasses['w-fit']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ; /** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['text-aqua']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['text-aqua']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-28']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-36']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ; /** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ; /** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ; /** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ;
@@ -214,7 +513,7 @@ if (__VLS_ctx.showForm) {
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ; /** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-6']} */ ; /** @type {__VLS_StyleScopedClasses['p-6']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ; /** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-sm']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-md']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ; /** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ; /** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ; /** @type {__VLS_StyleScopedClasses['text-text']} */ ;
@@ -246,6 +545,42 @@ if (__VLS_ctx.showForm) {
/** @type {__VLS_StyleScopedClasses['w-full']} */ ; /** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ; /** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ; /** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ; /** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ; /** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ; /** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
@@ -259,6 +594,71 @@ if (__VLS_ctx.showForm) {
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ; /** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['resize-none']} */ ; /** @type {__VLS_StyleScopedClasses['resize-none']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ; /** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-28']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['h-36']} */ ;
/** @type {__VLS_StyleScopedClasses['object-cover']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ; /** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-end']} */ ; /** @type {__VLS_StyleScopedClasses['justify-end']} */ ;
@@ -281,7 +681,18 @@ const __VLS_self = (await import('vue')).defineComponent({
return { return {
toolsStore: toolsStore, toolsStore: toolsStore,
showForm: showForm, showForm: showForm,
editId: editId,
photoPreview: photoPreview,
videoPreview: videoPreview,
noticeFileName: noticeFileName,
form: form, form: form,
fileNameFromUrl: fileNameFromUrl,
openCreate: openCreate,
onPhotoSelected: onPhotoSelected,
onVideoSelected: onVideoSelected,
onNoticeSelected: onNoticeSelected,
startEdit: startEdit,
closeForm: closeForm,
submitTool: submitTool, submitTool: submitTool,
removeTool: removeTool, removeTool: removeTool,
}; };

View File

@@ -1,30 +1,256 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" /> /// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { computed, onMounted, ref } from 'vue';
import { useTasksStore } from '@/stores/tasks';
const store = useTasksStore();
const today = new Date();
const weekStart = ref(getMonday(today));
function getMonday(d) {
const day = d.getDay();
const diff = (day === 0 ? -6 : 1 - day);
const m = new Date(d);
m.setDate(d.getDate() + diff);
m.setHours(0, 0, 0, 0);
return m;
}
function toIso(d) {
return d.toISOString().slice(0, 10);
}
const weekDays = computed(() => {
const todayIso = toIso(today);
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(weekStart.value);
d.setDate(d.getDate() + i);
return {
iso: toIso(d),
dayShort: d.toLocaleDateString('fr-FR', { weekday: 'short' }),
dayNum: d.getDate(),
isToday: toIso(d) === todayIso,
};
});
});
const weekLabel = computed(() => {
const start = weekDays.value[0];
const end = weekDays.value[6];
const s = new Date(start.iso + 'T12:00:00');
const e = new Date(end.iso + 'T12:00:00');
return `${s.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })} ${e.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' })}`;
});
const tasksByDay = computed(() => {
const map = {};
for (const t of store.tasks) {
if (!t.echeance)
continue;
const key = t.echeance.slice(0, 10);
if (!map[key])
map[key] = [];
map[key].push(t);
}
return map;
});
const unscheduled = computed(() => store.tasks.filter(t => !t.echeance && t.statut !== 'fait'));
function prevWeek() {
const d = new Date(weekStart.value);
d.setDate(d.getDate() - 7);
weekStart.value = d;
}
function nextWeek() {
const d = new Date(weekStart.value);
d.setDate(d.getDate() + 7);
weekStart.value = d;
}
function goToday() { weekStart.value = getMonday(today); }
const priorityClass = (p) => ({
haute: 'bg-red/20 text-red',
normale: 'bg-yellow/20 text-yellow',
basse: 'bg-bg-hard text-text-muted',
}[p] || 'bg-bg-hard text-text-muted');
const dotClass = (p) => ({
haute: 'bg-red', normale: 'bg-yellow', basse: 'bg-text-muted',
}[p] || 'bg-text-muted');
const statutClass = (s) => ({
a_faire: 'bg-blue/20 text-blue', en_cours: 'bg-green/20 text-green',
fait: 'bg-text-muted/20 text-text-muted',
}[s] || '');
onMounted(() => store.fetchAll());
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {}; const __VLS_ctx = {};
let __VLS_components; let __VLS_components;
let __VLS_directives; let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 max-w-2xl mx-auto" }, ...{ class: "p-4 max-w-3xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-6" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
...{ class: "text-2xl font-bold text-green mb-4" }, ...{ class: "text-2xl font-bold text-green" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-sm" }, ...{ class: "flex items-center gap-3" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.prevWeek) },
...{ class: "text-text-muted hover:text-text text-lg" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text text-sm font-medium" },
});
(__VLS_ctx.weekLabel);
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.nextWeek) },
...{ class: "text-text-muted hover:text-text text-lg" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.goToday) },
...{ class: "text-xs text-green border border-green/30 rounded px-2 py-0.5 hover:bg-green/10" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-7 gap-1 mb-2" },
});
for (const [d] of __VLS_getVForSourceType((__VLS_ctx.weekDays))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (d.iso),
...{ class: (['text-center text-xs py-1 rounded',
d.isToday ? 'text-green font-bold' : 'text-text-muted']) },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
(d.dayShort);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: (['text-sm font-semibold mt-0.5', d.isToday ? 'bg-green text-bg rounded-full w-6 h-6 flex items-center justify-center mx-auto' : '']) },
});
(d.dayNum);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-7 gap-1" },
});
for (const [d] of __VLS_getVForSourceType((__VLS_ctx.weekDays))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (d.iso),
...{ class: (['min-h-24 rounded-lg p-1 border transition-colors',
d.isToday ? 'border-green/40 bg-green/5' : 'border-bg-hard bg-bg-soft']) },
});
for (const [t] of __VLS_getVForSourceType((__VLS_ctx.tasksByDay[d.iso] || []))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (t.id),
...{ class: (['text-xs rounded px-1 py-0.5 mb-0.5 cursor-pointer hover:opacity-80 truncate',
__VLS_ctx.priorityClass(t.priorite)]) },
title: (t.titre),
});
(t.titre);
}
if (!(__VLS_ctx.tasksByDay[d.iso]?.length)) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs text-center pt-2 opacity-40" },
});
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-6" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text-muted text-xs uppercase tracking-widest mb-2" },
});
if (!__VLS_ctx.unscheduled.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs pl-2" },
});
}
for (const [t] of __VLS_getVForSourceType((__VLS_ctx.unscheduled))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (t.id),
...{ class: "bg-bg-soft rounded-lg p-2 mb-1 border border-bg-hard flex items-center gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (['text-xs w-2 h-2 rounded-full flex-shrink-0', __VLS_ctx.dotClass(t.priorite)]) },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text text-sm flex-1 truncate" },
});
(t.titre);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (['text-xs px-1.5 py-0.5 rounded', __VLS_ctx.statutClass(t.statut)]) },
});
(t.statut);
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-3xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ; /** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-between']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ; /** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ; /** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-green/30']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-green/10']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-7']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-7']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['opacity-40']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['uppercase']} */ ;
/** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['pl-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
var __VLS_dollars; var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({ const __VLS_self = (await import('vue')).defineComponent({
setup() { setup() {
return {}; return {
weekDays: weekDays,
weekLabel: weekLabel,
tasksByDay: tasksByDay,
unscheduled: unscheduled,
prevWeek: prevWeek,
nextWeek: nextWeek,
goToday: goToday,
priorityClass: priorityClass,
dotClass: dotClass,
statutClass: statutClass,
};
}, },
}); });
export default (await import('vue')).defineComponent({ export default (await import('vue')).defineComponent({

View File

@@ -35,6 +35,9 @@
<div class="text-text-muted text-xs mt-1 flex gap-3 flex-wrap"> <div class="text-text-muted text-xs mt-1 flex gap-3 flex-wrap">
<span>{{ p.quantite }} plant(s)</span> <span>{{ p.quantite }} plant(s)</span>
<span v-if="p.date_plantation">🌱 {{ fmtDate(p.date_plantation) }}</span> <span v-if="p.date_plantation">🌱 {{ fmtDate(p.date_plantation) }}</span>
<span v-if="p.boutique_nom">🛒 {{ p.boutique_nom }}</span>
<span v-if="p.tarif_achat != null">💶 {{ p.tarif_achat }} </span>
<span v-if="p.date_achat">🧾 {{ fmtDate(p.date_achat) }}</span>
<span v-if="p.notes">📝 {{ p.notes }}</span> <span v-if="p.notes">📝 {{ p.notes }}</span>
</div> </div>
</div> </div>
@@ -135,6 +138,30 @@
<option value="termine">Terminé</option> <option value="termine">Terminé</option>
</select> </select>
</div> </div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-text-muted text-xs block mb-1">Nom boutique</label>
<input v-model="cForm.boutique_nom" type="text" placeholder="Ex: Graines Bocquet"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Date achat</label>
<input v-model="cForm.date_achat" type="date"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-text-muted text-xs block mb-1">Tarif achat ()</label>
<input v-model.number="cForm.tarif_achat" type="number" min="0" step="0.01" placeholder="0.00"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
</div>
<div>
<label class="text-text-muted text-xs block mb-1">URL boutique</label>
<input v-model="cForm.boutique_url" type="url" placeholder="https://..."
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
</div>
</div>
<textarea v-model="cForm.notes" placeholder="Notes..." <textarea v-model="cForm.notes" placeholder="Notes..."
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green resize-none h-16" /> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green resize-none h-16" />
<div class="flex gap-2 justify-end"> <div class="flex gap-2 justify-end">
@@ -177,7 +204,9 @@ const statuts = [
const cForm = reactive({ const cForm = reactive({
garden_id: 0, variety_id: 0, quantite: 1, garden_id: 0, variety_id: 0, quantite: 1,
date_plantation: '', statut: 'prevu', notes: '' date_plantation: '', statut: 'prevu',
boutique_nom: '', boutique_url: '', tarif_achat: undefined as number | undefined, date_achat: '',
notes: ''
}) })
const rForm = reactive({ const rForm = reactive({
@@ -233,7 +262,12 @@ function startEdit(p: typeof store.plantings[0]) {
Object.assign(cForm, { Object.assign(cForm, {
garden_id: p.garden_id, variety_id: p.variety_id, garden_id: p.garden_id, variety_id: p.variety_id,
quantite: p.quantite, date_plantation: p.date_plantation?.slice(0, 10) || '', quantite: p.quantite, date_plantation: p.date_plantation?.slice(0, 10) || '',
statut: p.statut, notes: p.notes || '', statut: p.statut,
boutique_nom: p.boutique_nom || '',
boutique_url: p.boutique_url || '',
tarif_achat: p.tarif_achat,
date_achat: p.date_achat?.slice(0, 10) || '',
notes: p.notes || '',
}) })
showCreate.value = true showCreate.value = true
} }
@@ -247,7 +281,11 @@ async function createPlanting() {
await store.create({ ...cForm }) await store.create({ ...cForm })
} }
closeCreate() closeCreate()
Object.assign(cForm, { garden_id: 0, variety_id: 0, quantite: 1, date_plantation: '', statut: 'prevu', notes: '' }) Object.assign(cForm, {
garden_id: 0, variety_id: 0, quantite: 1, date_plantation: '', statut: 'prevu',
boutique_nom: '', boutique_url: '', tarif_achat: undefined, date_achat: '',
notes: '',
})
} }
onMounted(() => { onMounted(() => {

View File

@@ -8,6 +8,7 @@ const store = usePlantingsStore();
const gardensStore = useGardensStore(); const gardensStore = useGardensStore();
const plantsStore = usePlantsStore(); const plantsStore = usePlantsStore();
const showCreate = ref(false); const showCreate = ref(false);
const editId = ref(null);
const filterStatut = ref(''); const filterStatut = ref('');
const openRecoltes = ref(null); const openRecoltes = ref(null);
const recoltesList = ref([]); const recoltesList = ref([]);
@@ -21,7 +22,9 @@ const statuts = [
]; ];
const cForm = reactive({ const cForm = reactive({
garden_id: 0, variety_id: 0, quantite: 1, garden_id: 0, variety_id: 0, quantite: 1,
date_plantation: '', statut: 'prevu', notes: '' date_plantation: '', statut: 'prevu',
boutique_nom: '', boutique_url: '', tarif_achat: undefined, date_achat: '',
notes: ''
}); });
const rForm = reactive({ const rForm = reactive({
quantite: 1, unite: 'kg', date_recolte: new Date().toISOString().slice(0, 10) quantite: 1, unite: 'kg', date_recolte: new Date().toISOString().slice(0, 10)
@@ -71,10 +74,34 @@ async function deleteRecolte(id, plantingId) {
await recoltesApi.delete(id); await recoltesApi.delete(id);
recoltesList.value = recoltesList.value.filter(r => r.id !== id); recoltesList.value = recoltesList.value.filter(r => r.id !== id);
} }
function startEdit(p) {
editId.value = p.id;
Object.assign(cForm, {
garden_id: p.garden_id, variety_id: p.variety_id,
quantite: p.quantite, date_plantation: p.date_plantation?.slice(0, 10) || '',
statut: p.statut,
boutique_nom: p.boutique_nom || '',
boutique_url: p.boutique_url || '',
tarif_achat: p.tarif_achat,
date_achat: p.date_achat?.slice(0, 10) || '',
notes: p.notes || '',
});
showCreate.value = true;
}
function closeCreate() { showCreate.value = false; editId.value = null; }
async function createPlanting() { async function createPlanting() {
await store.create({ ...cForm }); if (editId.value) {
showCreate.value = false; await store.update(editId.value, { ...cForm });
Object.assign(cForm, { garden_id: 0, variety_id: 0, quantite: 1, date_plantation: '', statut: 'prevu', notes: '' }); }
else {
await store.create({ ...cForm });
}
closeCreate();
Object.assign(cForm, {
garden_id: 0, variety_id: 0, quantite: 1, date_plantation: '', statut: 'prevu',
boutique_nom: '', boutique_url: '', tarif_achat: undefined, date_achat: '',
notes: '',
});
} }
onMounted(() => { onMounted(() => {
store.fetchAll(); store.fetchAll();
@@ -159,6 +186,18 @@ for (const [p] of __VLS_getVForSourceType((__VLS_ctx.filtered))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({}); __VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.fmtDate(p.date_plantation)); (__VLS_ctx.fmtDate(p.date_plantation));
} }
if (p.boutique_nom) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.boutique_nom);
}
if (p.tarif_achat != null) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.tarif_achat);
}
if (p.date_achat) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(__VLS_ctx.fmtDate(p.date_achat));
}
if (p.notes) { if (p.notes) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({}); __VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.notes); (p.notes);
@@ -173,6 +212,12 @@ for (const [p] of __VLS_getVForSourceType((__VLS_ctx.filtered))) {
...{ class: (['text-xs px-2 py-1 rounded transition-colors', ...{ class: (['text-xs px-2 py-1 rounded transition-colors',
__VLS_ctx.openRecoltes === p.id ? 'bg-aqua/20 text-aqua' : 'bg-bg-hard text-text-muted hover:text-aqua']) }, __VLS_ctx.openRecoltes === p.id ? 'bg-aqua/20 text-aqua' : 'bg-bg-hard text-text-muted hover:text-aqua']) },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.startEdit(p);
} },
...{ class: "text-yellow text-xs hover:underline" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (...[$event]) => {
__VLS_ctx.store.remove(p.id); __VLS_ctx.store.remove(p.id);
@@ -280,11 +325,7 @@ for (const [p] of __VLS_getVForSourceType((__VLS_ctx.filtered))) {
} }
if (__VLS_ctx.showCreate) { if (__VLS_ctx.showCreate) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.closeCreate) },
if (!(__VLS_ctx.showCreate))
return;
__VLS_ctx.showCreate = false;
} },
...{ class: "fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" }, ...{ class: "fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
@@ -293,6 +334,7 @@ if (__VLS_ctx.showCreate) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-bold text-lg mb-4" }, ...{ class: "text-text font-bold text-lg mb-4" },
}); });
(__VLS_ctx.editId ? 'Modifier la plantation' : 'Nouvelle plantation');
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.createPlanting) }, ...{ onSubmit: (__VLS_ctx.createPlanting) },
...{ class: "flex flex-col gap-3" }, ...{ class: "flex flex-col gap-3" },
@@ -375,6 +417,53 @@ if (__VLS_ctx.showCreate) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "termine", value: "termine",
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-2 gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
value: (__VLS_ctx.cForm.boutique_nom),
type: "text",
placeholder: "Ex: Graines Bocquet",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "date",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" },
});
(__VLS_ctx.cForm.date_achat);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-2 gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "0",
step: "0.01",
placeholder: "0.00",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" },
});
(__VLS_ctx.cForm.tarif_achat);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "url",
placeholder: "https://...",
...{ class: "bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" },
});
(__VLS_ctx.cForm.boutique_url);
__VLS_asFunctionalElement(__VLS_intrinsicElements.textarea)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.textarea)({
value: (__VLS_ctx.cForm.notes), value: (__VLS_ctx.cForm.notes),
placeholder: "Notes...", placeholder: "Notes...",
@@ -384,11 +473,7 @@ if (__VLS_ctx.showCreate) {
...{ class: "flex gap-2 justify-end" }, ...{ class: "flex gap-2 justify-end" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.closeCreate) },
if (!(__VLS_ctx.showCreate))
return;
__VLS_ctx.showCreate = false;
} },
type: "button", type: "button",
...{ class: "px-4 py-2 text-text-muted hover:text-text text-sm" }, ...{ class: "px-4 py-2 text-text-muted hover:text-text text-sm" },
}); });
@@ -396,6 +481,7 @@ if (__VLS_ctx.showCreate) {
type: "submit", type: "submit",
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" }, ...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
}); });
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer');
} }
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-3xl']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-3xl']} */ ;
@@ -453,6 +539,9 @@ if (__VLS_ctx.showCreate) {
/** @type {__VLS_StyleScopedClasses['flex']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ; /** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ; /** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-red']} */ ; /** @type {__VLS_StyleScopedClasses['hover:text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
@@ -645,6 +734,72 @@ if (__VLS_ctx.showCreate) {
/** @type {__VLS_StyleScopedClasses['w-full']} */ ; /** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ; /** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ; /** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ; /** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ; /** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ; /** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
@@ -682,6 +837,7 @@ const __VLS_self = (await import('vue')).defineComponent({
gardensStore: gardensStore, gardensStore: gardensStore,
plantsStore: plantsStore, plantsStore: plantsStore,
showCreate: showCreate, showCreate: showCreate,
editId: editId,
filterStatut: filterStatut, filterStatut: filterStatut,
openRecoltes: openRecoltes, openRecoltes: openRecoltes,
recoltesList: recoltesList, recoltesList: recoltesList,
@@ -697,6 +853,8 @@ const __VLS_self = (await import('vue')).defineComponent({
toggleRecoltes: toggleRecoltes, toggleRecoltes: toggleRecoltes,
addRecolte: addRecolte, addRecolte: addRecolte,
deleteRecolte: deleteRecolte, deleteRecolte: deleteRecolte,
startEdit: startEdit,
closeCreate: closeCreate,
createPlanting: createPlanting, createPlanting: createPlanting,
}; };
}, },

View File

@@ -78,62 +78,107 @@
<!-- Modal formulaire création / édition --> <!-- Modal formulaire création / édition -->
<div v-if="showForm || editPlant" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm"> <div v-if="showForm || editPlant" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" @click.self="closeForm">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft max-h-[90vh] overflow-y-auto"> <div class="bg-bg-hard rounded-xl p-6 w-full max-w-4xl border border-bg-soft max-h-[90vh] overflow-y-auto">
<h2 class="text-text font-bold text-lg mb-4">{{ editPlant ? 'Modifier la plante' : 'Nouvelle plante' }}</h2> <h2 class="text-text font-bold text-lg mb-4">{{ editPlant ? 'Modifier la plante' : 'Nouvelle plante' }}</h2>
<form @submit.prevent="submitPlant" class="flex flex-col gap-3"> <form @submit.prevent="submitPlant" class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<input v-model="form.nom_commun" placeholder="Nom commun *" required <div>
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" /> <label class="text-text-muted text-xs block mb-1">Nom commun *</label>
<input v-model="form.nom_botanique" placeholder="Nom botanique" <input v-model="form.nom_commun" placeholder="Ex: Tomate" required
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" /> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
<input v-model="form.variete" placeholder="Variété" <p class="text-text-muted text-[11px] mt-1">Nom utilisé au jardin pour identifier rapidement la plante.</p>
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" /> </div>
<input v-model="form.famille" placeholder="Famille botanique" <div>
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" /> <label class="text-text-muted text-xs block mb-1">Nom botanique</label>
<select v-model="form.categorie" <input v-model="form.nom_botanique" placeholder="Ex: Solanum lycopersicum"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green"> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
<option value="">Catégorie</option> <p class="text-text-muted text-[11px] mt-1">Nom scientifique utile pour éviter les ambiguïtés.</p>
<option value="potager">Potager</option> </div>
<option value="fleur">Fleur</option> <div>
<option value="arbre">Arbre</option> <label class="text-text-muted text-xs block mb-1">Variété</label>
<option value="arbuste">Arbuste</option> <input v-model="form.variete" placeholder="Ex: Andine Cornue"
<option value="adventice">Adventice (mauvaise herbe)</option> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
</select> <p class="text-text-muted text-[11px] mt-1">Cultivar précis (optionnel).</p>
<select v-model="form.type_plante" </div>
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green"> <div>
<option value="">Type</option> <label class="text-text-muted text-xs block mb-1">Famille botanique</label>
<option value="legume">Légume</option> <input v-model="form.famille" placeholder="Ex: Solanacées"
<option value="fruit">Fruit</option> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
<option value="aromatique">Aromatique</option> <p class="text-text-muted text-[11px] mt-1">Permet d'organiser la rotation des cultures.</p>
<option value="fleur">Fleur</option> </div>
<option value="adventice">Adventice</option> <div>
</select> <label class="text-text-muted text-xs block mb-1">Catégorie</label>
<select v-model="form.besoin_eau" <select v-model="form.categorie"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green"> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green">
<option value="">Besoin en eau</option> <option value="">Catégorie</option>
<option value="faible">Faible</option> <option value="potager">Potager</option>
<option value="moyen">Moyen</option> <option value="fleur">Fleur</option>
<option value="élevé">Élevé</option> <option value="arbre">Arbre</option>
</select> <option value="arbuste">Arbuste</option>
<select v-model="form.besoin_soleil" <option value="adventice">Adventice (mauvaise herbe)</option>
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green"> </select>
<option value="">Ensoleillement</option> <p class="text-text-muted text-[11px] mt-1">Classe principale pour filtrer la bibliothèque de plantes.</p>
<option value="ombre">Ombre</option> </div>
<option value="mi-ombre">Mi-ombre</option> <div>
<option value="plein soleil">Plein soleil</option> <label class="text-text-muted text-xs block mb-1">Type de plante</label>
</select> <select v-model="form.type_plante"
<div class="flex gap-2"> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green">
<option value="">Type</option>
<option value="legume">Légume</option>
<option value="fruit">Fruit</option>
<option value="aromatique">Aromatique</option>
<option value="fleur">Fleur</option>
<option value="adventice">Adventice</option>
</select>
<p class="text-text-muted text-[11px] mt-1">Type d'usage de la plante (récolte, ornement, etc.).</p>
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Besoin en eau</label>
<select v-model="form.besoin_eau"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green">
<option value="">Besoin en eau</option>
<option value="faible">Faible</option>
<option value="moyen">Moyen</option>
<option value="élevé">Élevé</option>
</select>
<p class="text-text-muted text-[11px] mt-1">Aide à planifier l'arrosage.</p>
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Ensoleillement</label>
<select v-model="form.besoin_soleil"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green">
<option value="">Ensoleillement</option>
<option value="ombre">Ombre</option>
<option value="mi-ombre">Mi-ombre</option>
<option value="plein soleil">Plein soleil</option>
</select>
<p class="text-text-muted text-[11px] mt-1">Exposition lumineuse idéale.</p>
</div>
<div class="lg:col-span-2 flex gap-2">
<input v-model.number="form.espacement_cm" type="number" placeholder="Espacement (cm)" <input v-model.number="form.espacement_cm" type="number" placeholder="Espacement (cm)"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm flex-1 outline-none focus:border-green" /> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm flex-1 outline-none focus:border-green" />
<input v-model.number="form.temp_min_c" type="number" placeholder="T° min (°C)" <input v-model.number="form.temp_min_c" type="number" placeholder="T° min (°C)"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm flex-1 outline-none focus:border-green" /> class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm flex-1 outline-none focus:border-green" />
</div> </div>
<input v-model="form.plantation_mois" placeholder="Mois plantation (ex: 3,4,5)" <p class="lg:col-span-2 text-text-muted text-[11px] -mt-2">Espacement recommandé en cm et température minimale supportée (en °C).</p>
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" /> <div>
<input v-model="form.recolte_mois" placeholder="Mois récolte (ex: 7,8,9)" <label class="text-text-muted text-xs block mb-1">Mois de plantation</label>
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" /> <input v-model="form.plantation_mois" placeholder="Ex: 3,4,5"
<textarea v-model="form.notes" placeholder="Notes..." class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green resize-none h-20" /> <p class="text-text-muted text-[11px] mt-1">Liste des mois conseillés, séparés par des virgules.</p>
<div class="flex gap-2 justify-end"> </div>
<div>
<label class="text-text-muted text-xs block mb-1">Mois de récolte</label>
<input v-model="form.recolte_mois" placeholder="Ex: 7,8,9"
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green" />
<p class="text-text-muted text-[11px] mt-1">Période habituelle de récolte.</p>
</div>
<div class="lg:col-span-2">
<label class="text-text-muted text-xs block mb-1">Notes</label>
<textarea v-model="form.notes" placeholder="Observations, maladies, astuces..."
class="bg-bg border border-bg-soft rounded-lg px-3 py-2 text-text text-sm w-full outline-none focus:border-green resize-none h-20" />
<p class="text-text-muted text-[11px] mt-1">Commentaires libres visibles dans le détail de la plante.</p>
</div>
<div class="lg:col-span-2 flex gap-2 justify-end">
<button type="button" @click="closeForm" <button type="button" @click="closeForm"
class="px-4 py-2 text-text-muted hover:text-text text-sm">Annuler</button> class="px-4 py-2 text-text-muted hover:text-text text-sm">Annuler</button>
<button type="submit" <button type="submit"

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,106 @@
<template> <template>
<div class="p-4 max-w-2xl mx-auto"> <div class="p-4 max-w-3xl mx-auto">
<h1 class="text-2xl font-bold text-green mb-4">Réglages</h1> <h1 class="text-2xl font-bold text-green mb-4">Réglages</h1>
<p class="text-text-muted text-sm">Paramètres et export/import prochaine étape.</p>
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
<h2 class="text-text font-semibold mb-2">Général</h2>
<p class="text-text-muted text-sm mb-3">Options globales de l'application.</p>
<label class="inline-flex items-center gap-2 text-sm text-text">
<input v-model="debugMode" type="checkbox" class="accent-green" />
Activer le mode debug (affichage CPU / RAM / disque en header)
</label>
<div class="mt-3 flex items-center gap-2">
<button
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
:disabled="saving"
@click="saveSettings"
>
{{ saving ? 'Enregistrement...' : 'Enregistrer' }}
</button>
<span v-if="savedMsg" class="text-xs text-aqua">{{ savedMsg }}</span>
</div>
</section>
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
<h2 class="text-text font-semibold mb-2">Maintenance météo</h2>
<p class="text-text-muted text-sm mb-3">Déclenche un rafraîchissement immédiat des jobs météo backend.</p>
<button
class="bg-blue text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
:disabled="refreshingMeteo"
@click="refreshMeteo"
>
{{ refreshingMeteo ? 'Rafraîchissement...' : 'Rafraîchir maintenant' }}
</button>
</section>
<section class="bg-bg-soft border border-bg-hard rounded-xl p-4">
<h2 class="text-text font-semibold mb-2">Idées utiles (prochaine étape)</h2>
<ul class="text-text-muted text-sm space-y-1">
<li>• Sauvegarde/restauration JSON de la base métier</li>
<li>• Rotation/nettoyage des médias anciens</li>
<li>• Choix des unités météo (°C, mm, km/h)</li>
<li>• Paramètres de seuils alertes (gel, pluie, vent)</li>
</ul>
</section>
</div> </div>
</template> </template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { settingsApi } from '@/api/settings'
import { meteoApi } from '@/api/meteo'
const debugMode = ref(false)
const saving = ref(false)
const savedMsg = ref('')
const refreshingMeteo = ref(false)
function toBool(value: unknown): boolean {
if (typeof value === 'boolean') return value
const s = String(value ?? '').toLowerCase().trim()
return s === '1' || s === 'true' || s === 'yes' || s === 'on'
}
function notifyDebugChanged(enabled: boolean) {
localStorage.setItem('debug_mode', enabled ? '1' : '0')
window.dispatchEvent(new CustomEvent('settings-updated', { detail: { debug_mode: enabled } }))
}
async function loadSettings() {
try {
const data = await settingsApi.get()
debugMode.value = toBool(data.debug_mode)
notifyDebugChanged(debugMode.value)
} catch {
// Laisse la valeur locale si l'API n'est pas disponible.
}
}
async function saveSettings() {
saving.value = true
savedMsg.value = ''
try {
await settingsApi.update({ debug_mode: debugMode.value ? '1' : '0' })
notifyDebugChanged(debugMode.value)
savedMsg.value = 'Enregistré'
window.setTimeout(() => { savedMsg.value = '' }, 1800)
} finally {
saving.value = false
}
}
async function refreshMeteo() {
refreshingMeteo.value = true
try {
await meteoApi.refresh()
} finally {
refreshingMeteo.value = false
}
}
onMounted(() => {
void loadSettings()
})
</script>

View File

@@ -1,30 +1,207 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" /> /// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { onMounted, ref } from 'vue';
import { settingsApi } from '@/api/settings';
import { meteoApi } from '@/api/meteo';
const debugMode = ref(false);
const saving = ref(false);
const savedMsg = ref('');
const refreshingMeteo = ref(false);
function toBool(value) {
if (typeof value === 'boolean')
return value;
const s = String(value ?? '').toLowerCase().trim();
return s === '1' || s === 'true' || s === 'yes' || s === 'on';
}
function notifyDebugChanged(enabled) {
localStorage.setItem('debug_mode', enabled ? '1' : '0');
window.dispatchEvent(new CustomEvent('settings-updated', { detail: { debug_mode: enabled } }));
}
async function loadSettings() {
try {
const data = await settingsApi.get();
debugMode.value = toBool(data.debug_mode);
notifyDebugChanged(debugMode.value);
}
catch {
// Laisse la valeur locale si l'API n'est pas disponible.
}
}
async function saveSettings() {
saving.value = true;
savedMsg.value = '';
try {
await settingsApi.update({ debug_mode: debugMode.value ? '1' : '0' });
notifyDebugChanged(debugMode.value);
savedMsg.value = 'Enregistré';
window.setTimeout(() => { savedMsg.value = ''; }, 1800);
}
finally {
saving.value = false;
}
}
async function refreshMeteo() {
refreshingMeteo.value = true;
try {
await meteoApi.refresh();
}
finally {
refreshingMeteo.value = false;
}
}
onMounted(() => {
void loadSettings();
});
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {}; const __VLS_ctx = {};
let __VLS_components; let __VLS_components;
let __VLS_directives; let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 max-w-2xl mx-auto" }, ...{ class: "p-4 max-w-3xl mx-auto" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
...{ class: "text-2xl font-bold text-green mb-4" }, ...{ class: "text-2xl font-bold text-green mb-4" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({
...{ class: "text-text-muted text-sm" }, ...{ class: "bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-semibold mb-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-sm mb-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "inline-flex items-center gap-2 text-sm text-text" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "checkbox",
...{ class: "accent-green" },
});
(__VLS_ctx.debugMode);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-3 flex items-center gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.saveSettings) },
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
disabled: (__VLS_ctx.saving),
});
(__VLS_ctx.saving ? 'Enregistrement...' : 'Enregistrer');
if (__VLS_ctx.savedMsg) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-xs text-aqua" },
});
(__VLS_ctx.savedMsg);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({
...{ class: "bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-semibold mb-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-sm mb-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.refreshMeteo) },
...{ class: "bg-blue text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
disabled: (__VLS_ctx.refreshingMeteo),
});
(__VLS_ctx.refreshingMeteo ? 'Rafraîchissement...' : 'Rafraîchir maintenant');
__VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElements.section)({
...{ class: "bg-bg-soft border border-bg-hard rounded-xl p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-semibold mb-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.ul, __VLS_intrinsicElements.ul)({
...{ class: "text-text-muted text-sm space-y-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.li, __VLS_intrinsicElements.li)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.li, __VLS_intrinsicElements.li)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.li, __VLS_intrinsicElements.li)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.li, __VLS_intrinsicElements.li)({});
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-3xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ; /** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ; /** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ; /** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ; /** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['accent-green']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-3']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-aqua']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-1']} */ ;
var __VLS_dollars; var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({ const __VLS_self = (await import('vue')).defineComponent({
setup() { setup() {
return {}; return {
debugMode: debugMode,
saving: saving,
savedMsg: savedMsg,
refreshingMeteo: refreshingMeteo,
saveSettings: saveSettings,
refreshMeteo: refreshMeteo,
};
}, },
}); });
export default (await import('vue')).defineComponent({ export default (await import('vue')).defineComponent({

View File

@@ -19,6 +19,9 @@
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="text-text text-sm">{{ t.titre }}</div> <div class="text-text text-sm">{{ t.titre }}</div>
<div v-if="t.echeance" class="text-text-muted text-xs">📅 {{ fmtDate(t.echeance) }}</div> <div v-if="t.echeance" class="text-text-muted text-xs">📅 {{ fmtDate(t.echeance) }}</div>
<div v-if="t.frequence_jours != null && t.frequence_jours > 0" class="text-text-muted text-xs">
🔁 Tous les {{ t.frequence_jours }} jours
</div>
</div> </div>
<div class="flex gap-1 items-center shrink-0"> <div class="flex gap-1 items-center shrink-0">
<button v-if="t.statut === 'a_faire'" class="text-xs text-blue hover:underline" <button v-if="t.statut === 'a_faire'" class="text-xs text-blue hover:underline"
@@ -69,6 +72,25 @@
<input v-model="form.echeance" type="date" <input v-model="form.echeance" type="date"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" /> class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" />
</div> </div>
<div class="bg-bg rounded border border-bg-hard p-3">
<label class="inline-flex items-center gap-2 text-sm text-text">
<input v-model="form.repetition" type="checkbox" class="accent-green" />
Répétition
</label>
<p class="text-text-muted text-[11px] mt-1">Active une tâche récurrente.</p>
</div>
<div v-if="form.repetition">
<label class="text-text-muted text-xs block mb-1">Fréquence (jours)</label>
<input
v-model.number="form.frequence_jours"
type="number"
min="1"
step="1"
required
placeholder="Ex: 7"
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none"
/>
</div>
<div class="flex gap-2 mt-2"> <div class="flex gap-2 mt-2">
<button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold"> <button type="submit" class="bg-green text-bg px-4 py-2 rounded text-sm font-semibold">
{{ editId ? 'Enregistrer' : 'Créer' }} {{ editId ? 'Enregistrer' : 'Créer' }}
@@ -89,7 +111,15 @@ import type { Task } from '@/api/tasks'
const store = useTasksStore() const store = useTasksStore()
const showForm = ref(false) const showForm = ref(false)
const editId = ref<number | null>(null) const editId = ref<number | null>(null)
const form = reactive({ titre: '', description: '', priorite: 'normale', statut: 'a_faire', echeance: '' }) const form = reactive({
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
repetition: false,
frequence_jours: undefined as number | undefined,
})
const groupes: [string, string][] = [ const groupes: [string, string][] = [
['a_faire', 'À faire'], ['a_faire', 'À faire'],
@@ -105,7 +135,15 @@ function fmtDate(s: string) {
function openCreate() { function openCreate() {
editId.value = null editId.value = null
Object.assign(form, { titre: '', description: '', priorite: 'normale', statut: 'a_faire', echeance: '' }) Object.assign(form, {
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
repetition: false,
frequence_jours: undefined,
})
showForm.value = true showForm.value = true
} }
@@ -115,6 +153,8 @@ function startEdit(t: Task) {
titre: t.titre, description: (t as any).description || '', titre: t.titre, description: (t as any).description || '',
priorite: t.priorite, statut: t.statut, priorite: t.priorite, statut: t.statut,
echeance: t.echeance ? t.echeance.slice(0, 10) : '', echeance: t.echeance ? t.echeance.slice(0, 10) : '',
repetition: Boolean((t as any).recurrence || (t as any).frequence_jours),
frequence_jours: (t as any).frequence_jours ?? undefined,
}) })
showForm.value = true showForm.value = true
} }
@@ -124,10 +164,19 @@ function closeForm() { showForm.value = false; editId.value = null }
onMounted(() => store.fetchAll()) onMounted(() => store.fetchAll())
async function submit() { async function submit() {
const payload = {
titre: form.titre,
description: form.description,
priorite: form.priorite,
statut: form.statut,
echeance: form.echeance || undefined,
recurrence: form.repetition ? 'jours' : null,
frequence_jours: form.repetition ? (form.frequence_jours ?? 7) : null,
}
if (editId.value) { if (editId.value) {
await store.update(editId.value, { ...form }) await store.update(editId.value, payload)
} else { } else {
await store.create({ ...form }) await store.create(payload)
} }
closeForm() closeForm()
} }

View File

@@ -3,18 +3,68 @@ import { onMounted, reactive, ref } from 'vue';
import { useTasksStore } from '@/stores/tasks'; import { useTasksStore } from '@/stores/tasks';
const store = useTasksStore(); const store = useTasksStore();
const showForm = ref(false); const showForm = ref(false);
const form = reactive({ titre: '', priorite: 'normale', statut: 'a_faire', echeance: '' }); const editId = ref(null);
const form = reactive({
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
repetition: false,
frequence_jours: undefined,
});
const groupes = [ const groupes = [
['a_faire', 'À faire'], ['a_faire', 'À faire'],
['en_cours', 'En cours'], ['en_cours', 'En cours'],
['fait', 'Terminé'], ['fait', 'Terminé'],
]; ];
const byStatut = (s) => store.tasks.filter(t => t.statut === s); const byStatut = (s) => store.tasks.filter(t => t.statut === s);
function fmtDate(s) {
return new Date(s + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
}
function openCreate() {
editId.value = null;
Object.assign(form, {
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
repetition: false,
frequence_jours: undefined,
});
showForm.value = true;
}
function startEdit(t) {
editId.value = t.id;
Object.assign(form, {
titre: t.titre, description: t.description || '',
priorite: t.priorite, statut: t.statut,
echeance: t.echeance ? t.echeance.slice(0, 10) : '',
repetition: Boolean(t.recurrence || t.frequence_jours),
frequence_jours: t.frequence_jours ?? undefined,
});
showForm.value = true;
}
function closeForm() { showForm.value = false; editId.value = null; }
onMounted(() => store.fetchAll()); onMounted(() => store.fetchAll());
async function submit() { async function submit() {
await store.create({ ...form }); const payload = {
showForm.value = false; titre: form.titre,
Object.assign(form, { titre: '', priorite: 'normale', statut: 'a_faire', echeance: '' }); description: form.description,
priorite: form.priorite,
statut: form.statut,
echeance: form.echeance || undefined,
recurrence: form.repetition ? 'jours' : null,
frequence_jours: form.repetition ? (form.frequence_jours ?? 7) : null,
};
if (editId.value) {
await store.update(editId.value, payload);
}
else {
await store.create(payload);
}
closeForm();
} }
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */ debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {}; const __VLS_ctx = {};
@@ -30,74 +80,9 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1
...{ class: "text-2xl font-bold text-green" }, ...{ class: "text-2xl font-bold text-green" },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (__VLS_ctx.openCreate) },
__VLS_ctx.showForm = !__VLS_ctx.showForm;
} },
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" }, ...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
}); });
if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.submit) },
...{ class: "bg-bg-soft rounded-lg p-4 mb-6 border border-green/30" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
required: true,
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
});
(__VLS_ctx.form.titre);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-2 gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.form.priorite),
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "basse",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "normale",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "haute",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "date",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
});
(__VLS_ctx.form.echeance);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 mt-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
type: "submit",
...{ class: "bg-green text-bg px-4 py-2 rounded text-sm font-semibold" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.showForm))
return;
__VLS_ctx.showForm = false;
} },
type: "button",
...{ class: "text-text-muted text-sm px-4 py-2 hover:text-text" },
});
}
for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) { for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (groupe), key: (groupe),
@@ -124,12 +109,27 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
'text-text-muted': t.priorite === 'basse' 'text-text-muted': t.priorite === 'basse'
}) }, }) },
}); });
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text text-sm flex-1" }, ...{ class: "flex-1 min-w-0" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text text-sm" },
}); });
(t.titre); (t.titre);
if (t.echeance) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
});
(__VLS_ctx.fmtDate(t.echeance));
}
if (t.frequence_jours != null && t.frequence_jours > 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
});
(t.frequence_jours);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-1 items-center" }, ...{ class: "flex gap-1 items-center shrink-0" },
}); });
if (t.statut === 'a_faire') { if (t.statut === 'a_faire') {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
@@ -151,14 +151,143 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
...{ class: "text-xs text-green hover:underline" }, ...{ class: "text-xs text-green hover:underline" },
}); });
} }
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.startEdit(t);
} },
...{ class: "text-xs text-yellow hover:underline ml-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => { ...{ onClick: (...[$event]) => {
__VLS_ctx.store.remove(t.id); __VLS_ctx.store.remove(t.id);
} }, } },
...{ class: "text-xs text-text-muted hover:text-red ml-2" }, ...{ class: "text-xs text-text-muted hover:text-red ml-1" },
}); });
} }
} }
if (__VLS_ctx.showForm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (__VLS_ctx.closeForm) },
...{ class: "fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg-hard rounded-xl p-6 w-full max-w-md border border-bg-soft" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-text font-bold text-lg mb-4" },
});
(__VLS_ctx.editId ? 'Modifier la tâche' : 'Nouvelle tâche');
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.submit) },
...{ class: "grid gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
required: true,
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
});
(__VLS_ctx.form.titre);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.textarea)({
value: (__VLS_ctx.form.description),
rows: "2",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none resize-none" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-2 gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.form.priorite),
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "basse",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "normale",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "haute",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.form.statut),
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "a_faire",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "en_cours",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "fait",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "date",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
});
(__VLS_ctx.form.echeance);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg rounded border border-bg-hard p-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "inline-flex items-center gap-2 text-sm text-text" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "checkbox",
...{ class: "accent-green" },
});
(__VLS_ctx.form.repetition);
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-[11px] mt-1" },
});
if (__VLS_ctx.form.repetition) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-text-muted text-xs block mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
type: "number",
min: "1",
step: "1",
required: true,
placeholder: "Ex: 7",
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm focus:border-green outline-none" },
});
(__VLS_ctx.form.frequence_jours);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 mt-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
type: "submit",
...{ class: "bg-green text-bg px-4 py-2 rounded text-sm font-semibold" },
});
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer');
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.closeForm) },
type: "button",
...{ class: "text-text-muted text-sm px-4 py-2 hover:text-text" },
});
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ; /** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ; /** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
@@ -177,12 +306,70 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ; /** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ; /** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['uppercase']} */ ;
/** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['pl-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ; /** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ; /** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ; /** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ; /** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ; /** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-green/30']} */ ; /** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['min-w-0']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['shrink-0']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['ml-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['ml-1']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/60']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['p-6']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-md']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ; /** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ; /** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ; /** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
@@ -200,6 +387,22 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ; /** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ; /** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['resize-none']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ; /** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-2']} */ ; /** @type {__VLS_StyleScopedClasses['grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ; /** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
@@ -229,11 +432,53 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
/** @type {__VLS_StyleScopedClasses['py-2']} */ ; /** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ; /** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ; /** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['accent-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['block']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ; /** @type {__VLS_StyleScopedClasses['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ; /** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ; /** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ; /** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-4']} */ ; /** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-green']} */ ; /** @type {__VLS_StyleScopedClasses['bg-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ; /** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ; /** @type {__VLS_StyleScopedClasses['px-4']} */ ;
@@ -246,50 +491,20 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
/** @type {__VLS_StyleScopedClasses['px-4']} */ ; /** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ; /** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ; /** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['uppercase']} */ ;
/** @type {__VLS_StyleScopedClasses['tracking-widest']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['pl-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-red']} */ ;
/** @type {__VLS_StyleScopedClasses['ml-2']} */ ;
var __VLS_dollars; var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({ const __VLS_self = (await import('vue')).defineComponent({
setup() { setup() {
return { return {
store: store, store: store,
showForm: showForm, showForm: showForm,
editId: editId,
form: form, form: form,
groupes: groupes, groupes: groupes,
byStatut: byStatut, byStatut: byStatut,
fmtDate: fmtDate,
openCreate: openCreate,
startEdit: startEdit,
closeForm: closeForm,
submit: submit, submit: submit,
}; };
}, },

View File

@@ -0,0 +1,286 @@
#!/usr/bin/env python3
"""Backfill historique Open-Meteo vers la table SQLite meteoopenmeteo.
Script autonome (hors webapp) :
- appelle l'API Open-Meteo Archive par tranches de dates
- reconstruit les champs journaliers utilises par l'app
- fait un UPSERT dans la table `meteoopenmeteo`
"""
from __future__ import annotations
import argparse
import json
import os
import sqlite3
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from typing import Any
import httpx
WMO_LABELS = {
0: "Ensoleillé",
1: "Principalement ensoleillé",
2: "Partiellement nuageux",
3: "Couvert",
45: "Brouillard",
48: "Brouillard givrant",
51: "Bruine légère",
53: "Bruine modérée",
55: "Bruine dense",
61: "Pluie légère",
63: "Pluie modérée",
65: "Pluie forte",
71: "Neige légère",
73: "Neige modérée",
75: "Neige forte",
77: "Grains de neige",
80: "Averses légères",
81: "Averses modérées",
82: "Averses violentes",
85: "Averses de neige",
86: "Averses de neige fortes",
95: "Orage",
96: "Orage avec grêle",
99: "Orage violent",
}
DAILY_FIELDS = [
"temperature_2m_max",
"temperature_2m_min",
"precipitation_sum",
"wind_speed_10m_max",
"weather_code",
"relative_humidity_2m_max",
"et0_fao_evapotranspiration",
]
HOURLY_FIELDS = [
"soil_temperature_0cm",
]
def _to_float(value: Any) -> float | None:
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _value_at(values: list[Any], index: int, default: Any = None) -> Any:
if index < 0 or index >= len(values):
return default
return values[index]
def _assert_db_writable(db_path: Path) -> None:
if not db_path.exists():
raise FileNotFoundError(f"Base introuvable: {db_path}")
if not db_path.is_file():
raise RuntimeError(f"Chemin de base invalide (pas un fichier): {db_path}")
if not os.access(db_path, os.R_OK):
raise PermissionError(f"Pas de lecture sur la base: {db_path}")
if not os.access(db_path, os.W_OK):
raise PermissionError(
f"Pas d'ecriture sur la base: {db_path}. "
"Lance le script avec un utilisateur qui a les droits."
)
def _parse_iso_date(value: str) -> date:
try:
return date.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"Date invalide '{value}' (attendu YYYY-MM-DD)") from exc
def _date_chunks(start: date, end: date, chunk_days: int) -> list[tuple[date, date]]:
chunks: list[tuple[date, date]] = []
cur = start
while cur <= end:
chunk_end = min(cur + timedelta(days=chunk_days - 1), end)
chunks.append((cur, chunk_end))
cur = chunk_end + timedelta(days=1)
return chunks
def _daily_soil_average(raw: dict[str, Any]) -> dict[str, float]:
hourly = raw.get("hourly", {})
times = hourly.get("time", []) or []
soils = hourly.get("soil_temperature_0cm", []) or []
by_day: dict[str, list[float]] = {}
for idx, ts in enumerate(times):
soil = _to_float(_value_at(soils, idx))
if soil is None or not isinstance(ts, str) or len(ts) < 10:
continue
day = ts[:10]
by_day.setdefault(day, []).append(soil)
return {
day: round(sum(vals) / len(vals), 2)
for day, vals in by_day.items()
if vals
}
def _fetch_archive_chunk(
*,
lat: float,
lon: float,
start_date: date,
end_date: date,
timezone_name: str,
timeout: int,
) -> list[dict[str, Any]]:
url = "https://archive-api.open-meteo.com/v1/archive"
params: list[tuple[str, Any]] = [
("latitude", lat),
("longitude", lon),
("start_date", start_date.isoformat()),
("end_date", end_date.isoformat()),
("timezone", timezone_name),
]
for field in DAILY_FIELDS:
params.append(("daily", field))
for field in HOURLY_FIELDS:
params.append(("hourly", field))
r = httpx.get(url, params=params, timeout=timeout)
r.raise_for_status()
raw = r.json()
daily = raw.get("daily", {})
dates = daily.get("time", []) or []
soil_by_day = _daily_soil_average(raw)
now_iso = datetime.now(timezone.utc).isoformat()
rows: list[dict[str, Any]] = []
for i, iso in enumerate(dates):
raw_code = _value_at(daily.get("weather_code", []), i, 0)
code = int(raw_code) if raw_code is not None else 0
rows.append(
{
"date": iso,
"t_min": _to_float(_value_at(daily.get("temperature_2m_min", []), i)),
"t_max": _to_float(_value_at(daily.get("temperature_2m_max", []), i)),
"pluie_mm": _to_float(_value_at(daily.get("precipitation_sum", []), i, 0.0)) or 0.0,
"vent_kmh": _to_float(_value_at(daily.get("wind_speed_10m_max", []), i, 0.0)) or 0.0,
"wmo": code,
"label": WMO_LABELS.get(code, f"Code {code}"),
"humidite_moy": _to_float(_value_at(daily.get("relative_humidity_2m_max", []), i)),
"sol_0cm": soil_by_day.get(iso),
"etp_mm": _to_float(_value_at(daily.get("et0_fao_evapotranspiration", []), i)),
"fetched_at": now_iso,
}
)
return rows
def _upsert_row(conn: sqlite3.Connection, row: dict[str, Any]) -> None:
conn.execute(
"""
INSERT INTO meteoopenmeteo (
date, t_min, t_max, pluie_mm, vent_kmh, wmo, label,
humidite_moy, sol_0cm, etp_mm, fetched_at
) VALUES (
:date, :t_min, :t_max, :pluie_mm, :vent_kmh, :wmo, :label,
:humidite_moy, :sol_0cm, :etp_mm, :fetched_at
)
ON CONFLICT(date) DO UPDATE SET
t_min=excluded.t_min,
t_max=excluded.t_max,
pluie_mm=excluded.pluie_mm,
vent_kmh=excluded.vent_kmh,
wmo=excluded.wmo,
label=excluded.label,
humidite_moy=excluded.humidite_moy,
sol_0cm=excluded.sol_0cm,
etp_mm=excluded.etp_mm,
fetched_at=excluded.fetched_at
""",
row,
)
def main() -> int:
parser = argparse.ArgumentParser(
description="Backfill historique Open-Meteo dans la table meteoopenmeteo."
)
parser.add_argument("--db", default="data/jardin.db", help="Chemin SQLite (defaut: data/jardin.db)")
parser.add_argument("--lat", type=float, default=45.14, help="Latitude")
parser.add_argument("--lon", type=float, default=4.12, help="Longitude")
parser.add_argument("--start-date", default="2026-01-01", help="Date debut YYYY-MM-DD")
parser.add_argument("--end-date", default=date.today().isoformat(), help="Date fin YYYY-MM-DD")
parser.add_argument("--chunk-days", type=int, default=31, help="Taille des tranches en jours")
parser.add_argument("--timezone", default="Europe/Paris", help="Timezone Open-Meteo")
parser.add_argument("--timeout", type=int, default=25, help="Timeout HTTP en secondes")
parser.add_argument("--dry-run", action="store_true", help="N ecrit pas en base")
args = parser.parse_args()
if args.chunk_days < 1:
raise ValueError("--chunk-days doit etre >= 1")
db_path = Path(args.db).expanduser().resolve()
if not args.dry_run:
_assert_db_writable(db_path)
start = _parse_iso_date(args.start_date)
end = _parse_iso_date(args.end_date)
today = date.today()
if end > today:
end = today
if end < start:
raise ValueError(f"Plage invalide: {start.isoformat()} > {end.isoformat()}")
chunks = _date_chunks(start, end, args.chunk_days)
all_rows: list[dict[str, Any]] = []
for idx, (chunk_start, chunk_end) in enumerate(chunks, start=1):
print(f"[{idx}/{len(chunks)}] Open-Meteo {chunk_start.isoformat()} -> {chunk_end.isoformat()}")
rows = _fetch_archive_chunk(
lat=args.lat,
lon=args.lon,
start_date=chunk_start,
end_date=chunk_end,
timezone_name=args.timezone,
timeout=args.timeout,
)
all_rows.extend(rows)
summary = {
"db": str(db_path),
"lat": args.lat,
"lon": args.lon,
"start_date": start.isoformat(),
"end_date": end.isoformat(),
"chunk_count": len(chunks),
"rows_fetched": len(all_rows),
"dry_run": args.dry_run,
}
if args.dry_run:
print(json.dumps(summary, ensure_ascii=False, indent=2))
return 0
conn: sqlite3.Connection | None = None
try:
conn = sqlite3.connect(str(db_path))
with conn:
for row in all_rows:
_upsert_row(conn, row)
finally:
if conn is not None:
conn.close()
print(json.dumps(summary, ensure_ascii=False, indent=2))
print("\nOK: meteoopenmeteo mise a jour.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""Mise a jour ponctuelle de la table meteostation depuis la station locale.
Script autonome (hors webapp) qui lit:
- le flux RSS courant de la station
- la page HTML de la station (donnees enrichies)
- le fichier NOAA mensuel pour une date cible
Puis ecrit dans la base SQLite:
- 1 ligne type="current" (heure observee arrondie a l'heure)
- 1 ligne type="veille" (date cible a T00:00), sauf si --current-only
"""
from __future__ import annotations
import argparse
import json
import os
import sqlite3
from datetime import datetime, timedelta
from email.utils import parsedate_to_datetime
from pathlib import Path
from typing import Any
from local_station_weather import (
fetch_text,
parse_current_from_rss,
parse_daily_summary_from_rss,
parse_station_page,
parse_yesterday_from_noaa,
)
def _first_not_none(*values: Any) -> Any:
for v in values:
if v is not None:
return v
return None
def _to_kmh(value_m_s: float | None) -> float | None:
if value_m_s is None:
return None
return round(value_m_s * 3.6, 1)
def _deg_to_dir(deg: int | float | None) -> str | None:
if deg is None:
return None
dirs = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"]
idx = int((float(deg) + 22.5) // 45) % 8
return dirs[idx]
def _parse_observed_hour(observed_at: str | None) -> str:
if observed_at:
try:
dt = parsedate_to_datetime(observed_at)
if dt.tzinfo:
dt = dt.astimezone()
dt = dt.replace(minute=0, second=0, microsecond=0)
return dt.strftime("%Y-%m-%dT%H:00")
except Exception:
pass
now = datetime.now().replace(minute=0, second=0, microsecond=0)
return now.strftime("%Y-%m-%dT%H:00")
def _target_date(date_arg: str | None) -> datetime:
if date_arg:
return datetime.strptime(date_arg, "%Y-%m-%d")
return datetime.now() - timedelta(days=1)
def _build_current_row(base_url: str) -> dict[str, Any]:
base = base_url.rstrip("/") + "/"
rss_url = f"{base}rss.xml"
station_page_url = base
rss_xml = fetch_text(rss_url)
current = parse_current_from_rss(rss_xml)
daily = parse_daily_summary_from_rss(rss_xml)
station_html = fetch_text(station_page_url)
station_extra = parse_station_page(station_html)
ext = station_extra.get("current_extended", {})
stats_today = station_extra.get("stats_today", {})
wind_deg = _first_not_none(current.get("wind_dir_deg"), ext.get("wind_dir_deg"))
vent_dir = _first_not_none(_deg_to_dir(wind_deg), ext.get("wind_dir_text"))
wind_m_s = _first_not_none(current.get("wind_speed_m_s"), ext.get("wind_speed_m_s"))
return {
"date_heure": _parse_observed_hour(current.get("observed_at")),
"type": "current",
"temp_ext": _first_not_none(current.get("temperature_ext_c"), ext.get("temperature_ext_c")),
"t_min": _first_not_none(daily.get("temp_ext_min_c"), stats_today.get("temp_ext_min_c")),
"t_max": _first_not_none(daily.get("temp_ext_max_c"), stats_today.get("temp_ext_max_c")),
"temp_int": _first_not_none(current.get("temperature_int_c"), ext.get("temperature_int_c")),
"humidite": _first_not_none(current.get("humidity_ext_pct"), ext.get("humidity_ext_pct")),
"pression": _first_not_none(current.get("pressure_mbar"), ext.get("pressure_mbar")),
"pluie_mm": _first_not_none(current.get("rain_mm"), ext.get("rain_today_mm")),
"vent_kmh": _to_kmh(wind_m_s),
"vent_dir": vent_dir,
"uv": ext.get("uv_index"),
"solaire": ext.get("solar_radiation_w_m2"),
}
def _build_day_row(base_url: str, target: datetime) -> dict[str, Any] | None:
base = base_url.rstrip("/") + "/"
noaa_url = f"{base}NOAA/NOAA-{target.year}-{target.month:02d}.txt"
noaa_text = fetch_text(noaa_url)
day_data = parse_yesterday_from_noaa(noaa_text, target.day)
if "error" in day_data:
return None
return {
"date_heure": target.strftime("%Y-%m-%dT00:00"),
"type": "veille",
"temp_ext": day_data.get("temp_mean_c"),
"t_min": day_data.get("temp_min_c"),
"t_max": day_data.get("temp_max_c"),
"temp_int": None,
"humidite": None,
"pression": None,
"pluie_mm": day_data.get("rain_mm"),
"vent_kmh": _to_kmh(day_data.get("wind_max_m_s")),
"vent_dir": _deg_to_dir(day_data.get("wind_dom_dir_deg")),
"uv": None,
"solaire": None,
}
def _upsert_row(conn: sqlite3.Connection, row: dict[str, Any]) -> None:
conn.execute(
"""
INSERT INTO meteostation (
date_heure, type, temp_ext, t_min, t_max, temp_int, humidite, pression,
pluie_mm, vent_kmh, vent_dir, uv, solaire
) VALUES (
:date_heure, :type, :temp_ext, :t_min, :t_max, :temp_int, :humidite, :pression,
:pluie_mm, :vent_kmh, :vent_dir, :uv, :solaire
)
ON CONFLICT(date_heure) DO UPDATE SET
type=excluded.type,
temp_ext=excluded.temp_ext,
t_min=excluded.t_min,
t_max=excluded.t_max,
temp_int=excluded.temp_int,
humidite=excluded.humidite,
pression=excluded.pression,
pluie_mm=excluded.pluie_mm,
vent_kmh=excluded.vent_kmh,
vent_dir=excluded.vent_dir,
uv=excluded.uv,
solaire=excluded.solaire
""",
row,
)
def _assert_db_writable(db_path: Path) -> None:
if not db_path.exists():
raise FileNotFoundError(f"Base introuvable: {db_path}")
if not db_path.is_file():
raise RuntimeError(f"Chemin de base invalide (pas un fichier): {db_path}")
if not os.access(db_path, os.R_OK):
raise PermissionError(f"Pas de lecture sur la base: {db_path}")
if not os.access(db_path, os.W_OK):
raise PermissionError(
f"Pas d'ecriture sur la base: {db_path}. "
"Lance le script avec un utilisateur qui a les droits (ou dans le conteneur backend)."
)
def main() -> int:
parser = argparse.ArgumentParser(description="Met a jour la table meteostation depuis la station locale.")
parser.add_argument("--base", default="http://10.0.0.8:8081/", help="URL de base de la station locale")
parser.add_argument("--db", default="data/jardin.db", help="Chemin SQLite (defaut: data/jardin.db)")
parser.add_argument("--date", help="Date NOAA cible (YYYY-MM-DD). Defaut: veille")
parser.add_argument("--current-only", action="store_true", help="Met a jour uniquement la ligne current")
parser.add_argument("--dry-run", action="store_true", help="N ecrit pas en base, affiche seulement le payload")
args = parser.parse_args()
db_path = Path(args.db).expanduser().resolve()
if not args.dry_run:
_assert_db_writable(db_path)
target = _target_date(args.date)
current_row = _build_current_row(args.base)
day_row = None if args.current_only else _build_day_row(args.base, target)
payload = {"current": current_row, "day_data": day_row, "target_date": target.strftime("%Y-%m-%d")}
print(json.dumps(payload, ensure_ascii=False, indent=2))
if args.dry_run:
return 0
conn: sqlite3.Connection | None = None
try:
conn = sqlite3.connect(str(db_path))
with conn:
_upsert_row(conn, current_row)
if day_row is not None:
_upsert_row(conn, day_row)
finally:
if conn is not None:
conn.close()
print(f"\nOK: base mise a jour -> {db_path}")
print(f"- current: {current_row['date_heure']}")
if day_row is not None:
print(f"- veille: {day_row['date_heure']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())