maj via codex
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 :
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
175
avancement.md
175
avancement.md
@@ -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.
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
75
backend/tests/test_astuces_filters.py
Normal file
75
backend/tests/test_astuces_filters.py
Normal 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"
|
||||||
87
backend/tests/test_meteo_service.py
Normal file
87
backend/tests/test_meteo_service.py
Normal 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"]
|
||||||
@@ -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
105
codex.md
Normal 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
BIN
data/jardin.db
Normal file → Executable file
Binary file not shown.
Binary file not shown.
BIN
data/uploads/384670c3-e3bc-4b30-af94-6a7f0fd0a0a9.webp
Normal file
BIN
data/uploads/384670c3-e3bc-4b30-af94-6a7f0fd0a0a9.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
BIN
data/uploads/a3c70da5-8081-42c0-ba14-05b936780fb6.webp
Normal file
BIN
data/uploads/a3c70da5-8081-42c0-ba14-05b936780fb6.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
data/uploads/garden_70757b2d-a485-44f1-881d-322640ef3f9d.webp
Normal file
BIN
data/uploads/garden_70757b2d-a485-44f1-881d-322640ef3f9d.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
data/uploads/garden_d249cc32-f10d-4e1f-a99c-e1b2deecfab1.webp
Normal file
BIN
data/uploads/garden_d249cc32-f10d-4e1f-a99c-e1b2deecfab1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
data/uploads/garden_e3dc6e2e-9fd1-4088-9c53-4edd7ceff418.webp
Normal file
BIN
data/uploads/garden_e3dc6e2e-9fd1-4088-9c53-4edd7ceff418.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
134
docs/api_utilisation_reseau_local_openclaw.md
Normal file
134
docs/api_utilisation_reseau_local_openclaw.md
Normal 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`
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
8
frontend/src/api/astuces.js
Normal file
8
frontend/src/api/astuces.js
Normal 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}`),
|
||||||
|
};
|
||||||
31
frontend/src/api/astuces.ts
Normal file
31
frontend/src/api/astuces.ts
Normal 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}`),
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
frontend/src/api/settings.js
Normal file
6
frontend/src/api/settings.js
Normal 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),
|
||||||
|
};
|
||||||
33
frontend/src/api/settings.ts
Normal file
33
frontend/src/api/settings.ts
Normal 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),
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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']} */ ;
|
||||||
|
|||||||
240
frontend/src/components/PhotoGallery.vue.js
Normal file
240
frontend/src/components/PhotoGallery.vue.js
Normal 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 */
|
||||||
369
frontend/src/components/PhotoIdentifyModal.vue.js
Normal file
369
frontend/src/components/PhotoIdentifyModal.vue.js
Normal 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 */
|
||||||
@@ -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' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
33
frontend/src/stores/astuces.js
Normal file
33
frontend/src/stores/astuces.js
Normal 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 };
|
||||||
|
});
|
||||||
37
frontend/src/stores/astuces.ts
Normal file
37
frontend/src/stores/astuces.ts
Normal 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 }
|
||||||
|
})
|
||||||
@@ -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 };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 };
|
||||||
});
|
});
|
||||||
|
|||||||
405
frontend/src/views/AstucesView.vue
Normal file
405
frontend/src/views/AstucesView.vue
Normal 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>
|
||||||
883
frontend/src/views/AstucesView.vue.js
Normal file
883
frontend/src/views/AstucesView.vue.js
Normal 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 */
|
||||||
@@ -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}`)
|
||||||
|
|||||||
554
frontend/src/views/BibliothequeView.vue.js
Normal file
554
frontend/src/views/BibliothequeView.vue.js
Normal 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 */
|
||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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']} */ ;
|
||||||
|
|||||||
@@ -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 (m²)</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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
286
station_meteo/update_openmeteo_history_db.py
Normal file
286
station_meteo/update_openmeteo_history_db.py
Normal 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())
|
||||||
220
station_meteo/update_station_db.py
Normal file
220
station_meteo/update_station_db.py
Normal 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())
|
||||||
Reference in New Issue
Block a user