before gemiin

This commit is contained in:
2026-02-22 22:18:32 +01:00
parent fb33540bb0
commit 9db5cbf236
147 changed files with 7948 additions and 531 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,18 +9,19 @@
"lint": "vue-tsc --noEmit"
},
"dependencies": {
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"axios": "^1.7.9",
"pinia": "^2.3.0",
"axios": "^1.7.9"
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.7",
"typescript": "^5.7.3",
"vue-tsc": "^2.2.0",
"tailwindcss": "^3.4.17",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.1",
"autoprefixer": "^10.4.20"
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"vite": "^6.0.7",
"vite-plugin-pwa": "^1.2.0",
"vue-tsc": "^2.2.0"
}
}

View File

@@ -38,7 +38,7 @@
</aside>
<!-- Main content -->
<main class="pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full" style="font-size: var(--ui-font-size, 14px)">
<main class="pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full bg-bg" style="font-size: var(--ui-font-size, 14px)">
<RouterView />
</main>
</div>

View File

@@ -5,6 +5,7 @@ import AppHeader from '@/components/AppHeader.vue';
import AppDrawer from '@/components/AppDrawer.vue';
import { meteoApi } from '@/api/meteo';
import { settingsApi } from '@/api/settings';
import { applyUiSizesToRoot } from '@/utils/uiSizeDefaults';
const drawerOpen = ref(false);
const debugMode = ref(localStorage.getItem('debug_mode') === '1');
const debugStats = ref(null);
@@ -71,11 +72,15 @@ function startDebugPolling() {
void fetchDebugStats();
}, 10000);
}
function applyUiSizesFromSettings(data) {
applyUiSizesToRoot(data);
}
async function loadDebugModeFromApi() {
try {
const data = await settingsApi.get();
debugMode.value = toBool(data.debug_mode);
localStorage.setItem('debug_mode', debugMode.value ? '1' : '0');
applyUiSizesFromSettings(data);
}
catch {
// On garde la valeur locale.
@@ -235,10 +240,12 @@ for (const [l] of __VLS_getVForSourceType((__VLS_ctx.links))) {
}, ...__VLS_functionalComponentArgsRest(__VLS_19));
__VLS_21.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-base leading-none" },
...{ style: (`font-size: var(--ui-menu-icon-size, 18px); line-height: 1`) },
});
(l.icon);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ style: (`font-size: var(--ui-menu-font-size, 13px)`) },
});
(l.label);
var __VLS_21;
}
@@ -246,7 +253,8 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.d
...{ class: "px-4 py-4 border-t border-bg-soft text-text-muted text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.main, __VLS_intrinsicElements.main)({
...{ class: "pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full" },
...{ class: "pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full bg-bg" },
...{ style: {} },
});
const __VLS_22 = {}.RouterView;
/** @type {[typeof __VLS_components.RouterView, ]} */ ;
@@ -313,8 +321,6 @@ const __VLS_24 = __VLS_23({}, ...__VLS_functionalComponentArgsRest(__VLS_23));
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['group']} */ ;
/** @type {__VLS_StyleScopedClasses['text-base']} */ ;
/** @type {__VLS_StyleScopedClasses['leading-none']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-4']} */ ;
/** @type {__VLS_StyleScopedClasses['border-t']} */ ;
@@ -326,6 +332,7 @@ const __VLS_24 = __VLS_23({}, ...__VLS_functionalComponentArgsRest(__VLS_23));
/** @type {__VLS_StyleScopedClasses['lg:pl-60']} */ ;
/** @type {__VLS_StyleScopedClasses['min-h-screen']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {

View File

@@ -3,4 +3,19 @@ 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),
downloadBackup: () => client.get('/api/settings/backup/download', { responseType: 'blob' }).then(r => {
let filename = 'jardin_backup.zip';
const contentDisposition = r.headers?.['content-disposition'];
if (typeof contentDisposition === 'string') {
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
const classicMatch = contentDisposition.match(/filename="?([^\";]+)"?/i);
if (utf8Match?.[1]) {
filename = decodeURIComponent(utf8Match[1]);
}
else if (classicMatch?.[1]) {
filename = classicMatch[1];
}
}
return { blob: r.data, filename };
}),
};

View File

@@ -30,4 +30,19 @@ export const settingsApi = {
client.put<{ ok: boolean }>('/api/settings', settings).then(r => r.data),
getDebugSystemStats: () =>
client.get<DebugSystemStats>('/api/settings/debug/system').then(r => r.data),
downloadBackup: () =>
client.get('/api/settings/backup/download', { responseType: 'blob' }).then(r => {
let filename = 'jardin_backup.zip'
const contentDisposition = r.headers?.['content-disposition']
if (typeof contentDisposition === 'string') {
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
const classicMatch = contentDisposition.match(/filename="?([^\";]+)"?/i)
if (utf8Match?.[1]) {
filename = decodeURIComponent(utf8Match[1])
} else if (classicMatch?.[1]) {
filename = classicMatch[1]
}
}
return { blob: r.data as Blob, filename }
}),
}

View File

@@ -5,6 +5,8 @@ export interface Task {
titre: string
description?: string
garden_id?: number
planting_id?: number
outil_id?: number
priorite: string
echeance?: string
recurrence?: string | null
@@ -14,7 +16,7 @@ export interface Task {
}
export const tasksApi = {
list: (params?: { statut?: string; garden_id?: number }) =>
list: (params?: { statut?: string; garden_id?: number; planting_id?: number }) =>
client.get<Task[]>('/api/tasks', { params }).then(r => r.data),
get: (id: number) => client.get<Task>(`/api/tasks/${id}`).then(r => r.data),
create: (t: Partial<Task>) => client.post<Task>('/api/tasks', t).then(r => r.data),

View File

@@ -7,6 +7,7 @@ export interface Tool {
categorie?: string
photo_url?: string
video_url?: string
notice_texte?: string
notice_fichier_url?: string
boutique_nom?: string
boutique_url?: string

View File

@@ -75,7 +75,7 @@
>
<option :value="null"> Associer à une plante existante (optionnel)</option>
<option v-for="p in plants" :key="p.id" :value="p.id">
{{ p.nom_commun }}
{{ formatPlantLabel(p) }}
</option>
</select>
@@ -111,6 +111,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { formatPlantLabel } from '@/utils/plants'
interface IdentifyResult {
species: string
@@ -122,6 +123,7 @@ interface IdentifyResult {
interface Plant {
id: number
nom_commun: string
variete?: string
}
const emit = defineEmits<{

View File

@@ -1,6 +1,7 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { ref, onMounted } from 'vue';
import axios from 'axios';
import { formatPlantLabel } from '@/utils/plants';
const emit = defineEmits();
const previewUrl = ref(null);
const imageFile = ref(null);
@@ -186,7 +187,7 @@ else {
key: (p.id),
value: (p.id),
});
(p.nom_commun);
(__VLS_ctx.formatPlantLabel(p));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2" },
@@ -345,6 +346,7 @@ var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
formatPlantLabel: formatPlantLabel,
previewUrl: previewUrl,
loading: loading,
saving: saving,

View File

@@ -6,7 +6,7 @@ export const useTasksStore = defineStore('tasks', () => {
const tasks = ref<Task[]>([])
const loading = ref(false)
async function fetchAll(params?: { statut?: string; garden_id?: number }) {
async function fetchAll(params?: { statut?: string; garden_id?: number; planting_id?: number }) {
loading.value = true
tasks.value = await tasksApi.list(params)
loading.value = false

View File

@@ -0,0 +1,6 @@
export function formatPlantLabel(plant) {
if (plant.variete && plant.variete.trim()) {
return `${plant.nom_commun}${plant.variete.trim()}`;
}
return plant.nom_commun;
}

View File

@@ -0,0 +1,12 @@
export interface PlantLabelData {
id?: number
nom_commun: string
variete?: string | null
}
export function formatPlantLabel(plant: PlantLabelData): string {
if (plant.variete && plant.variete.trim()) {
return `${plant.nom_commun}${plant.variete.trim()}`
}
return plant.nom_commun
}

View File

@@ -0,0 +1,14 @@
export const UI_SIZE_DEFAULTS = {
ui_font_size: 14,
ui_menu_font_size: 13,
ui_menu_icon_size: 18,
ui_thumb_size: 96,
};
export function applyUiSizesToRoot(data) {
const root = document.documentElement;
for (const [key, def] of Object.entries(UI_SIZE_DEFAULTS)) {
const val = Number(data[key]) || def;
const prop = '--' + key.replace(/_/g, '-');
root.style.setProperty(prop, `${val}px`);
}
}

View File

@@ -64,10 +64,13 @@
🔗 Associer à une plante
</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"
@click="toggleAdventice(lightbox!)"
:class="[
'px-3 py-2 rounded-lg text-xs font-medium transition-colors',
isAdventice(lightbox!) ? 'bg-red/20 text-red hover:bg-red/30' : 'bg-green/20 text-green hover:bg-green/30'
]"
>
🌾 Marquer adventice
{{ isAdventice(lightbox!) ? '🪓 Retirer adventice' : '🌾 Marquer adventice' }}
</button>
<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">
@@ -87,7 +90,7 @@
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">
<option :value="null">-- Choisir une plante --</option>
<option v-for="p in plantsStore.plants" :key="p.id" :value="p.id">
{{ p.nom_commun }}{{ p.variete ? ' ' + p.variete : '' }}
{{ formatPlantLabel(p) }}
</option>
</select>
<div class="flex gap-2 justify-end">
@@ -110,6 +113,7 @@ import { computed, onMounted, ref } from 'vue'
import axios from 'axios'
import PhotoIdentifyModal from '@/components/PhotoIdentifyModal.vue'
import { usePlantsStore } from '@/stores/plants'
import { formatPlantLabel } from '@/utils/plants'
interface Media {
id: number; entity_type: string; entity_id: number
@@ -152,7 +156,8 @@ function labelFor(type: string) {
}
function plantName(id: number) {
return plantsStore.plants.find(p => p.id === id)?.nom_commun ?? ''
const plant = plantsStore.plants.find(p => p.id === id)
return plant ? formatPlantLabel(plant) : ''
}
function openLightbox(m: Media) { lightbox.value = m }
@@ -191,6 +196,29 @@ async function markAsAdventice(m: Media) {
}
}
function isAdventice(m: Media) {
return m.entity_type === 'adventice'
}
async function toggleAdventice(m: Media) {
if (isAdventice(m)) {
await axios.patch(`/api/media/${m.id}`, {
entity_type: 'bibliotheque',
entity_id: 0,
})
const target = medias.value.find(x => x.id === m.id)
if (target) {
target.entity_type = 'bibliotheque'
target.entity_id = 0
}
if (lightbox.value?.id === m.id) {
lightbox.value = { ...lightbox.value, entity_type: 'bibliotheque', entity_id: 0 }
}
return
}
await markAsAdventice(m)
}
async function deleteMedia(m: Media) {
if (!confirm('Supprimer cette photo ?')) return
await axios.delete(`/api/media/${m.id}`)

View File

@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue';
import axios from 'axios';
import PhotoIdentifyModal from '@/components/PhotoIdentifyModal.vue';
import { usePlantsStore } from '@/stores/plants';
import { formatPlantLabel } from '@/utils/plants';
const medias = ref([]);
const loading = ref(false);
const lightbox = ref(null);
@@ -30,7 +31,8 @@ function labelFor(type) {
return map[type] ?? '📷';
}
function plantName(id) {
return plantsStore.plants.find(p => p.id === id)?.nom_commun ?? '';
const plant = plantsStore.plants.find(p => p.id === id);
return plant ? formatPlantLabel(plant) : '';
}
function openLightbox(m) { lightbox.value = m; }
function startLink(m) {
@@ -68,6 +70,27 @@ async function markAsAdventice(m) {
lightbox.value = { ...lightbox.value, entity_type: 'adventice', entity_id: 0 };
}
}
function isAdventice(m) {
return m.entity_type === 'adventice';
}
async function toggleAdventice(m) {
if (isAdventice(m)) {
await axios.patch(`/api/media/${m.id}`, {
entity_type: 'bibliotheque',
entity_id: 0,
});
const target = medias.value.find(x => x.id === m.id);
if (target) {
target.entity_type = 'bibliotheque';
target.entity_id = 0;
}
if (lightbox.value?.id === m.id) {
lightbox.value = { ...lightbox.value, entity_type: 'bibliotheque', entity_id: 0 };
}
return;
}
await markAsAdventice(m);
}
async function deleteMedia(m) {
if (!confirm('Supprimer cette photo ?'))
return;
@@ -240,10 +263,14 @@ if (__VLS_ctx.lightbox) {
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
return;
__VLS_ctx.markAsAdventice(__VLS_ctx.lightbox);
__VLS_ctx.toggleAdventice(__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" },
...{ class: ([
'px-3 py-2 rounded-lg text-xs font-medium transition-colors',
__VLS_ctx.isAdventice(__VLS_ctx.lightbox) ? 'bg-red/20 text-red hover:bg-red/30' : 'bg-green/20 text-green hover:bg-green/30'
]) },
});
(__VLS_ctx.isAdventice(__VLS_ctx.lightbox) ? '🪓 Retirer adventice' : '🌾 Marquer adventice');
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.lightbox))
@@ -289,8 +316,7 @@ if (__VLS_ctx.linkMedia) {
key: (p.id),
value: (p.id),
});
(p.nom_commun);
(p.variete ? ' — ' + p.variete : '');
(__VLS_ctx.formatPlantLabel(p));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 justify-end" },
@@ -451,15 +477,6 @@ if (__VLS_ctx.showIdentify) {
/** @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']} */ ;
@@ -526,6 +543,7 @@ const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
PhotoIdentifyModal: PhotoIdentifyModal,
formatPlantLabel: formatPlantLabel,
loading: loading,
lightbox: lightbox,
showIdentify: showIdentify,
@@ -540,7 +558,8 @@ const __VLS_self = (await import('vue')).defineComponent({
openLightbox: openLightbox,
startLink: startLink,
confirmLink: confirmLink,
markAsAdventice: markAsAdventice,
isAdventice: isAdventice,
toggleAdventice: toggleAdventice,
deleteMedia: deleteMedia,
onIdentified: onIdentified,
};

View File

@@ -1,5 +1,5 @@
<template>
<div class="p-4 max-w-3xl mx-auto">
<div class="p-4 max-w-5xl mx-auto">
<button class="text-text-muted text-sm mb-4 hover:text-text" @click="router.back()"> Retour</button>
<div v-if="garden">

View File

@@ -32,7 +32,7 @@ const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 max-w-3xl mx-auto" },
...{ class: "p-4 max-w-5xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
@@ -147,7 +147,7 @@ else {
});
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-3xl']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-5xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;

View File

@@ -1,5 +1,5 @@
<template>
<div class="p-4 max-w-2xl mx-auto">
<div class="p-4 max-w-5xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-green">🪴 Jardins</h1>
<button class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"

View File

@@ -113,7 +113,7 @@ const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 max-w-2xl mx-auto" },
...{ class: "p-4 max-w-5xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-6" },
@@ -486,7 +486,7 @@ if (__VLS_ctx.showForm) {
});
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-5xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;

View File

@@ -27,8 +27,9 @@
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>
<p v-if="t.notice_texte" class="text-text-muted text-xs whitespace-pre-line">{{ t.notice_texte }}</p>
<a v-else-if="t.notice_fichier_url" :href="t.notice_fichier_url" target="_blank" rel="noopener noreferrer"
class="text-aqua text-xs hover:underline truncate">📄 Notice (fichier)</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"
@@ -79,14 +80,8 @@
<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>
<textarea v-model="form.notice_texte" placeholder="Notice (texte libre)..."
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-24" />
<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="submit" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
@@ -110,10 +105,8 @@ const showForm = ref(false)
const editId = ref<number | null>(null)
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: '',
@@ -123,14 +116,10 @@ const form = reactive({
prix_achat: undefined as number | undefined,
photo_url: '',
video_url: '',
notice_texte: '',
notice_fichier_url: '',
})
function fileNameFromUrl(url: string) {
if (!url) return ''
return url.split('/').pop() || url
}
function resetForm() {
Object.assign(form, {
nom: '',
@@ -141,6 +130,7 @@ function resetForm() {
prix_achat: undefined,
photo_url: '',
video_url: '',
notice_texte: '',
notice_fichier_url: '',
})
}
@@ -150,10 +140,8 @@ function openCreate() {
resetForm()
photoFile.value = null
videoFile.value = null
noticeFile.value = null
photoPreview.value = ''
videoPreview.value = ''
noticeFileName.value = ''
showForm.value = true
}
@@ -171,13 +159,6 @@ function onVideoSelected(event: Event) {
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, {
@@ -189,14 +170,13 @@ function startEdit(t: Tool) {
prix_achat: t.prix_achat,
photo_url: t.photo_url || '',
video_url: t.video_url || '',
notice_texte: t.notice_texte || '',
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
}
@@ -205,10 +185,8 @@ function closeForm() {
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> {
@@ -229,6 +207,7 @@ async function submitTool() {
prix_achat: form.prix_achat,
photo_url: form.photo_url || undefined,
video_url: form.video_url || undefined,
notice_texte: form.notice_texte || undefined,
notice_fichier_url: form.notice_fichier_url || undefined,
}
@@ -238,11 +217,10 @@ async function submitTool() {
saved = await toolsStore.create(payload)
}
if (saved.id && (photoFile.value || videoFile.value || noticeFile.value)) {
if (saved.id && (photoFile.value || videoFile.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)
}

View File

@@ -7,10 +7,8 @@ const showForm = ref(false);
const editId = ref(null);
const photoFile = ref(null);
const videoFile = ref(null);
const noticeFile = ref(null);
const photoPreview = ref('');
const videoPreview = ref('');
const noticeFileName = ref('');
const form = reactive({
nom: '',
categorie: '',
@@ -20,13 +18,9 @@ const form = reactive({
prix_achat: undefined,
photo_url: '',
video_url: '',
notice_texte: '',
notice_fichier_url: '',
});
function fileNameFromUrl(url) {
if (!url)
return '';
return url.split('/').pop() || url;
}
function resetForm() {
Object.assign(form, {
nom: '',
@@ -37,6 +31,7 @@ function resetForm() {
prix_achat: undefined,
photo_url: '',
video_url: '',
notice_texte: '',
notice_fichier_url: '',
});
}
@@ -45,10 +40,8 @@ function openCreate() {
resetForm();
photoFile.value = null;
videoFile.value = null;
noticeFile.value = null;
photoPreview.value = '';
videoPreview.value = '';
noticeFileName.value = '';
showForm.value = true;
}
function onPhotoSelected(event) {
@@ -65,12 +58,6 @@ function onVideoSelected(event) {
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, {
@@ -82,14 +69,13 @@ function startEdit(t) {
prix_achat: t.prix_achat,
photo_url: t.photo_url || '',
video_url: t.video_url || '',
notice_texte: t.notice_texte || '',
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() {
@@ -97,10 +83,8 @@ function closeForm() {
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();
@@ -119,6 +103,7 @@ async function submitTool() {
prix_achat: form.prix_achat,
photo_url: form.photo_url || undefined,
video_url: form.video_url || undefined,
notice_texte: form.notice_texte || undefined,
notice_fichier_url: form.notice_fichier_url || undefined,
};
if (editId.value) {
@@ -127,14 +112,12 @@ async function submitTool() {
else {
saved = await toolsStore.create(payload);
}
if (saved.id && (photoFile.value || videoFile.value || noticeFile.value)) {
if (saved.id && (photoFile.value || videoFile.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);
}
@@ -244,7 +227,13 @@ for (const [t] of __VLS_getVForSourceType((__VLS_ctx.toolsStore.tools))) {
...{ class: "text-aqua text-xs hover:underline truncate" },
});
}
if (t.notice_fichier_url) {
if (t.notice_texte) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-xs whitespace-pre-line" },
});
(t.notice_texte);
}
else if (t.notice_fichier_url) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.a, __VLS_intrinsicElements.a)({
href: (t.notice_fichier_url),
target: "_blank",
@@ -382,22 +371,11 @@ if (__VLS_ctx.showForm) {
...{ 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.textarea)({
value: (__VLS_ctx.form.notice_texte),
placeholder: "Notice (texte libre)...",
...{ 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-24" },
});
__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)({
...{ class: "flex gap-2 justify-end" },
});
@@ -480,6 +458,9 @@ if (__VLS_ctx.showForm) {
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['whitespace-pre-line']} */ ;
/** @type {__VLS_StyleScopedClasses['text-aqua']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
@@ -640,10 +621,6 @@ if (__VLS_ctx.showForm) {
/** @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']} */ ;
@@ -655,10 +632,8 @@ if (__VLS_ctx.showForm) {
/** @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['resize-none']} */ ;
/** @type {__VLS_StyleScopedClasses['h-24']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-end']} */ ;
@@ -684,13 +659,10 @@ const __VLS_self = (await import('vue')).defineComponent({
editId: editId,
photoPreview: photoPreview,
videoPreview: videoPreview,
noticeFileName: noticeFileName,
form: form,
fileNameFromUrl: fileNameFromUrl,
openCreate: openCreate,
onPhotoSelected: onPhotoSelected,
onVideoSelected: onVideoSelected,
onNoticeSelected: onNoticeSelected,
startEdit: startEdit,
closeForm: closeForm,
submitTool: submitTool,

View File

@@ -2,32 +2,46 @@
<div class="p-4 max-w-3xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-green">📆 Planning</h1>
<!-- Navigateur semaine -->
<div class="flex items-center gap-3">
<button @click="prevWeek" class="text-text-muted hover:text-text text-lg"></button>
<span class="text-text text-sm font-medium">{{ weekLabel }}</span>
<button @click="nextWeek" class="text-text-muted hover:text-text text-lg"></button>
<button @click="goToday" class="text-xs text-green border border-green/30 rounded px-2 py-0.5 hover:bg-green/10">Auj.</button>
<!-- Navigateur 4 semaines -->
<div class="flex items-center gap-2">
<button @click="prevPeriod"
class="text-xs text-text-muted border border-bg-hard rounded px-2 py-1 hover:text-text hover:border-text-muted">
Prev
</button>
<button @click="goToday"
class="text-xs text-green border border-green/30 rounded px-2 py-1 hover:bg-green/10">
Today
</button>
<button @click="nextPeriod"
class="text-xs text-text-muted border border-bg-hard rounded px-2 py-1 hover:text-text hover:border-text-muted">
Next
</button>
</div>
</div>
<div class="text-text text-sm font-medium mb-3">{{ periodLabel }}</div>
<!-- Grille semaine -->
<!-- En-tête jours -->
<div class="grid grid-cols-7 gap-1 mb-2">
<div v-for="d in weekDays" :key="d.iso"
:class="['text-center text-xs py-1 rounded',
d.isToday ? 'text-green font-bold' : 'text-text-muted']">
<div>{{ d.dayShort }}</div>
<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 }}
</div>
<div v-for="dayName in dayHeaders" :key="dayName" class="text-center text-xs py-1 rounded text-text-muted">
{{ dayName }}
</div>
</div>
<!-- Tâches par jour -->
<!-- Grille 4 semaines -->
<div class="grid grid-cols-7 gap-1">
<div v-for="d in weekDays" :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']">
<div v-for="d in periodDays" :key="d.iso"
@click="selectDay(d.iso)"
:class="['min-h-24 rounded-lg p-1 border transition-colors cursor-pointer',
d.isToday ? 'border-green/40 bg-green/5' : 'border-bg-hard bg-bg-soft',
selectedIso === d.iso ? 'ring-1 ring-yellow/60 border-yellow/40' : '']">
<div class="text-[11px] text-text-muted mb-1">
<span :class="d.isToday ? 'text-green font-bold' : ''">{{ d.dayNum }}</span>
<span v-if="d.showMonth" class="ml-1">{{ d.monthShort }}</span>
</div>
<div v-if="todoTasksByDay[d.iso]?.length" class="flex items-center gap-1 flex-wrap mb-1">
<span v-for="(t, i) in todoTasksByDay[d.iso].slice(0, 10)" :key="`${d.iso}-${t.id ?? i}`"
:class="['w-1.5 h-1.5 rounded-full', dotClass(t.priorite)]"></span>
</div>
<div v-for="t in tasksByDay[d.iso] || []" :key="t.id"
:class="['text-xs rounded px-1 py-0.5 mb-0.5 cursor-pointer hover:opacity-80 truncate',
priorityClass(t.priorite)]"
@@ -39,6 +53,21 @@
</div>
</div>
<!-- Détail jour sélectionné -->
<div class="mt-4 bg-bg-soft rounded-lg p-3 border border-bg-hard">
<div class="text-text text-sm font-semibold">{{ selectedLabel }}</div>
<div class="text-text-muted text-xs mt-0.5">{{ selectedTasks.length }} tâche(s) planifiée(s)</div>
<div v-if="!selectedTasks.length" class="text-text-muted text-xs mt-2">Aucune tâche planifiée ce jour.</div>
<div v-else class="mt-2 space-y-1">
<div v-for="t in selectedTasks" :key="t.id"
class="bg-bg rounded px-2 py-1 border border-bg-hard flex items-center gap-2">
<span :class="['w-2 h-2 rounded-full shrink-0', dotClass(t.priorite)]"></span>
<span class="text-text text-xs flex-1 truncate">{{ t.titre }}</span>
<span :class="['text-[10px] px-1.5 py-0.5 rounded shrink-0', statutClass(t.statut)]">{{ t.statut }}</span>
</div>
</div>
</div>
<!-- Tâches sans date -->
<div class="mt-6">
<h2 class="text-text-muted text-xs uppercase tracking-widest mb-2">Sans date</h2>
@@ -61,6 +90,8 @@ const store = useTasksStore()
const today = new Date()
const weekStart = ref(getMonday(today))
const selectedIso = ref(toIso(today))
const dayHeaders = ['lun', 'mar', 'mer', 'jeu', 'ven', 'sam', 'dim']
function getMonday(d: Date) {
const day = d.getDay()
@@ -75,23 +106,26 @@ function toIso(d: Date) {
return d.toISOString().slice(0, 10)
}
const weekDays = computed(() => {
const periodDays = computed(() => {
const todayIso = toIso(today)
return Array.from({ length: 7 }, (_, i) => {
return Array.from({ length: 28 }, (_, i) => {
const d = new Date(weekStart.value)
d.setDate(d.getDate() + i)
const dayNum = d.getDate()
const monthShort = d.toLocaleDateString('fr-FR', { month: 'short' })
return {
iso: toIso(d),
dayShort: d.toLocaleDateString('fr-FR', { weekday: 'short' }),
dayNum: d.getDate(),
dayNum,
monthShort,
showMonth: dayNum === 1 || i === 0,
isToday: toIso(d) === todayIso,
}
})
})
const weekLabel = computed(() => {
const start = weekDays.value[0]
const end = weekDays.value[6]
const periodLabel = computed(() => {
const start = periodDays.value[0]
const end = periodDays.value[27]
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' })}`
@@ -100,6 +134,7 @@ const weekLabel = computed(() => {
const tasksByDay = computed(() => {
const map: Record<string, typeof store.tasks> = {}
for (const t of store.tasks) {
if (t.statut === 'template') continue
if (!t.echeance) continue
const key = t.echeance.slice(0, 10)
if (!map[key]) map[key] = []
@@ -108,19 +143,47 @@ const tasksByDay = computed(() => {
return map
})
const unscheduled = computed(() => store.tasks.filter(t => !t.echeance && t.statut !== 'fait'))
const todoTasksByDay = computed(() => {
const map: Record<string, typeof store.tasks> = {}
for (const [iso, tasks] of Object.entries(tasksByDay.value)) {
map[iso] = tasks.filter(t => t.statut !== 'fait')
}
return map
})
function prevWeek() {
const selectedTasks = computed(() => tasksByDay.value[selectedIso.value] || [])
const selectedLabel = computed(() => {
if (!selectedIso.value) return 'Détail du jour'
const d = new Date(selectedIso.value + 'T12:00:00')
return d.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
})
})
const unscheduled = computed(() => store.tasks.filter(t => !t.echeance && t.statut !== 'fait' && t.statut !== 'template'))
function prevPeriod() {
const d = new Date(weekStart.value)
d.setDate(d.getDate() - 7)
d.setDate(d.getDate() - 28)
weekStart.value = d
selectedIso.value = toIso(d)
}
function nextWeek() {
function nextPeriod() {
const d = new Date(weekStart.value)
d.setDate(d.getDate() + 7)
d.setDate(d.getDate() + 28)
weekStart.value = d
selectedIso.value = toIso(d)
}
function goToday() {
weekStart.value = getMonday(today)
selectedIso.value = toIso(today)
}
function selectDay(iso: string) {
selectedIso.value = iso
}
function goToday() { weekStart.value = getMonday(today) }
const priorityClass = (p: string) => ({
haute: 'bg-red/20 text-red',

View File

@@ -4,6 +4,8 @@ import { useTasksStore } from '@/stores/tasks';
const store = useTasksStore();
const today = new Date();
const weekStart = ref(getMonday(today));
const selectedIso = ref(toIso(today));
const dayHeaders = ['lun', 'mar', 'mer', 'jeu', 'ven', 'sam', 'dim'];
function getMonday(d) {
const day = d.getDay();
const diff = (day === 0 ? -6 : 1 - day);
@@ -15,22 +17,25 @@ function getMonday(d) {
function toIso(d) {
return d.toISOString().slice(0, 10);
}
const weekDays = computed(() => {
const periodDays = computed(() => {
const todayIso = toIso(today);
return Array.from({ length: 7 }, (_, i) => {
return Array.from({ length: 28 }, (_, i) => {
const d = new Date(weekStart.value);
d.setDate(d.getDate() + i);
const dayNum = d.getDate();
const monthShort = d.toLocaleDateString('fr-FR', { month: 'short' });
return {
iso: toIso(d),
dayShort: d.toLocaleDateString('fr-FR', { weekday: 'short' }),
dayNum: d.getDate(),
dayNum,
monthShort,
showMonth: dayNum === 1 || i === 0,
isToday: toIso(d) === todayIso,
};
});
});
const weekLabel = computed(() => {
const start = weekDays.value[0];
const end = weekDays.value[6];
const periodLabel = computed(() => {
const start = periodDays.value[0];
const end = periodDays.value[27];
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' })}`;
@@ -38,6 +43,8 @@ const weekLabel = computed(() => {
const tasksByDay = computed(() => {
const map = {};
for (const t of store.tasks) {
if (t.statut === 'template')
continue;
if (!t.echeance)
continue;
const key = t.echeance.slice(0, 10);
@@ -47,18 +54,45 @@ const tasksByDay = computed(() => {
}
return map;
});
const unscheduled = computed(() => store.tasks.filter(t => !t.echeance && t.statut !== 'fait'));
function prevWeek() {
const todoTasksByDay = computed(() => {
const map = {};
for (const [iso, tasks] of Object.entries(tasksByDay.value)) {
map[iso] = tasks.filter(t => t.statut !== 'fait');
}
return map;
});
const selectedTasks = computed(() => tasksByDay.value[selectedIso.value] || []);
const selectedLabel = computed(() => {
if (!selectedIso.value)
return 'Détail du jour';
const d = new Date(selectedIso.value + 'T12:00:00');
return d.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
});
});
const unscheduled = computed(() => store.tasks.filter(t => !t.echeance && t.statut !== 'fait' && t.statut !== 'template'));
function prevPeriod() {
const d = new Date(weekStart.value);
d.setDate(d.getDate() - 7);
d.setDate(d.getDate() - 28);
weekStart.value = d;
selectedIso.value = toIso(d);
}
function nextWeek() {
function nextPeriod() {
const d = new Date(weekStart.value);
d.setDate(d.getDate() + 7);
d.setDate(d.getDate() + 28);
weekStart.value = d;
selectedIso.value = toIso(d);
}
function goToday() {
weekStart.value = getMonday(today);
selectedIso.value = toIso(today);
}
function selectDay(iso) {
selectedIso.value = iso;
}
function goToday() { weekStart.value = getMonday(today); }
const priorityClass = (p) => ({
haute: 'bg-red/20 text-red',
normale: 'bg-yellow/20 text-yellow',
@@ -86,49 +120,71 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1
...{ class: "text-2xl font-bold text-green" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-3" },
...{ class: "flex items-center gap-2" },
});
__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" },
...{ onClick: (__VLS_ctx.prevPeriod) },
...{ class: "text-xs text-text-muted border border-bg-hard rounded px-2 py-1 hover:text-text hover:border-text-muted" },
});
__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" },
...{ class: "text-xs text-green border border-green/30 rounded px-2 py-1 hover:bg-green/10" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.nextPeriod) },
...{ class: "text-xs text-text-muted border border-bg-hard rounded px-2 py-1 hover:text-text hover:border-text-muted" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text text-sm font-medium mb-3" },
});
(__VLS_ctx.periodLabel);
__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))) {
for (const [dayName] of __VLS_getVForSourceType((__VLS_ctx.dayHeaders))) {
__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']) },
key: (dayName),
...{ class: "text-center text-xs py-1 rounded 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);
(dayName);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-7 gap-1" },
});
for (const [d] of __VLS_getVForSourceType((__VLS_ctx.weekDays))) {
for (const [d] of __VLS_getVForSourceType((__VLS_ctx.periodDays))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
__VLS_ctx.selectDay(d.iso);
} },
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']) },
...{ class: (['min-h-24 rounded-lg p-1 border transition-colors cursor-pointer',
d.isToday ? 'border-green/40 bg-green/5' : 'border-bg-hard bg-bg-soft',
__VLS_ctx.selectedIso === d.iso ? 'ring-1 ring-yellow/60 border-yellow/40' : '']) },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-[11px] text-text-muted mb-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (d.isToday ? 'text-green font-bold' : '') },
});
(d.dayNum);
if (d.showMonth) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "ml-1" },
});
(d.monthShort);
}
if (__VLS_ctx.todoTasksByDay[d.iso]?.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-1 flex-wrap mb-1" },
});
for (const [t, i] of __VLS_getVForSourceType((__VLS_ctx.todoTasksByDay[d.iso].slice(0, 10)))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
key: (`${d.iso}-${t.id ?? i}`),
...{ class: (['w-1.5 h-1.5 rounded-full', __VLS_ctx.dotClass(t.priorite)]) },
});
}
}
for (const [t] of __VLS_getVForSourceType((__VLS_ctx.tasksByDay[d.iso] || []))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (t.id),
@@ -144,6 +200,44 @@ for (const [d] of __VLS_getVForSourceType((__VLS_ctx.weekDays))) {
});
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-4 bg-bg-soft rounded-lg p-3 border border-bg-hard" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text text-sm font-semibold" },
});
(__VLS_ctx.selectedLabel);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mt-0.5" },
});
(__VLS_ctx.selectedTasks.length);
if (!__VLS_ctx.selectedTasks.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mt-2" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-2 space-y-1" },
});
for (const [t] of __VLS_getVForSourceType((__VLS_ctx.selectedTasks))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (t.id),
...{ class: "bg-bg rounded px-2 py-1 border border-bg-hard flex items-center gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (['w-2 h-2 rounded-full shrink-0', __VLS_ctx.dotClass(t.priorite)]) },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text text-xs flex-1 truncate" },
});
(t.titre);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (['text-[10px] px-1.5 py-0.5 rounded shrink-0', __VLS_ctx.statutClass(t.statut)]) },
});
(t.statut);
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-6" },
});
@@ -184,36 +278,93 @@ for (const [t] of __VLS_getVForSourceType((__VLS_ctx.unscheduled))) {
/** @type {__VLS_StyleScopedClasses['text-green']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @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['hover:border-text-muted']} */ ;
/** @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['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-green/10']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:border-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-7']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-7']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-[11px]']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['ml-1']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-wrap']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-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-4']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['p-3']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-2']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-1']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-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-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['truncate']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-6']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
@@ -240,13 +391,19 @@ var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
weekDays: weekDays,
weekLabel: weekLabel,
selectedIso: selectedIso,
dayHeaders: dayHeaders,
periodDays: periodDays,
periodLabel: periodLabel,
tasksByDay: tasksByDay,
todoTasksByDay: todoTasksByDay,
selectedTasks: selectedTasks,
selectedLabel: selectedLabel,
unscheduled: unscheduled,
prevWeek: prevWeek,
nextWeek: nextWeek,
prevPeriod: prevPeriod,
nextPeriod: nextPeriod,
goToday: goToday,
selectDay: selectDay,
priorityClass: priorityClass,
dotClass: dotClass,
statutClass: statutClass,

View File

@@ -1,5 +1,5 @@
<template>
<div class="p-4 max-w-3xl mx-auto">
<div class="p-4 max-w-5xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-green">🌱 Plantations</h1>
<button @click="showCreate = true"
@@ -47,6 +47,12 @@
openRecoltes === p.id ? 'bg-aqua/20 text-aqua' : 'bg-bg-hard text-text-muted hover:text-aqua']">
🍅 Récoltes
</button>
<button
class="text-xs px-2 py-1 rounded bg-blue/20 text-blue hover:bg-blue/30 transition-colors"
@click="openTaskFromTemplate(p)"
>
Tâche
</button>
<button @click="startEdit(p)" class="text-yellow text-xs hover:underline">Édit.</button>
<button @click="store.remove(p.id!)" class="text-text-muted hover:text-red text-sm ml-1"></button>
</div>
@@ -93,6 +99,62 @@
</div>
</div>
<!-- Modal ajout tâche depuis template -->
<div
v-if="showTaskTemplateModal && taskTarget"
class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
@click.self="closeTaskTemplateModal"
>
<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-1">Ajouter une tâche</h2>
<p class="text-text-muted text-xs mb-4">
Plantation: {{ plantName(taskTarget.variety_id) }} {{ gardenName(taskTarget.garden_id) }}
</p>
<form @submit.prevent="createTaskFromTemplate" class="grid gap-3">
<div>
<label class="text-text-muted text-xs block mb-1">Template *</label>
<select
v-model.number="taskTemplateForm.template_id"
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"
>
<option value="">Choisir un template</option>
<option v-for="t in templates" :key="t.id" :value="t.id">
{{ t.titre }}
</option>
</select>
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Échéance (optionnel)</label>
<input
v-model="taskTemplateForm.echeance"
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>
<label class="text-text-muted text-xs block mb-1">Description complémentaire (optionnel)</label>
<textarea
v-model="taskTemplateForm.extra_description"
rows="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 resize-none"
/>
</div>
<div class="flex gap-2 justify-end">
<button type="button" @click="closeTaskTemplateModal" class="px-4 py-2 text-text-muted hover:text-text text-sm">
Annuler
</button>
<button
type="submit"
class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
>
Créer la tâche
</button>
</div>
</form>
</div>
</div>
<!-- Modal création / édition plantation -->
<div v-if="showCreate" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4"
@click.self="closeCreate">
@@ -113,7 +175,7 @@
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="">Choisir une plante</option>
<option v-for="p in plantsStore.plants" :key="p.id" :value="p.id">
{{ p.nom_commun }}{{ p.variete ? ' ' + p.variete : '' }}
{{ formatPlantLabel(p) }}
</option>
</select>
</div>
@@ -181,7 +243,10 @@ import { computed, onMounted, reactive, ref } from 'vue'
import { usePlantingsStore } from '@/stores/plantings'
import { useGardensStore } from '@/stores/gardens'
import { usePlantsStore } from '@/stores/plants'
import type { Planting } from '@/api/plantings'
import { recoltesApi, type Recolte } from '@/api/recoltes'
import { tasksApi, type Task } from '@/api/tasks'
import { formatPlantLabel } from '@/utils/plants'
const store = usePlantingsStore()
const gardensStore = useGardensStore()
@@ -193,6 +258,9 @@ const filterStatut = ref('')
const openRecoltes = ref<number | null>(null)
const recoltesList = ref<Recolte[]>([])
const loadingRecoltes = ref(false)
const templates = ref<Task[]>([])
const showTaskTemplateModal = ref(false)
const taskTarget = ref<Planting | null>(null)
const statuts = [
{ val: '', label: 'Toutes' },
@@ -213,13 +281,19 @@ const rForm = reactive({
quantite: 1, unite: 'kg', date_recolte: new Date().toISOString().slice(0, 10)
})
const taskTemplateForm = reactive({
template_id: 0,
echeance: '',
extra_description: '',
})
const filtered = computed(() =>
filterStatut.value ? store.plantings.filter(p => p.statut === filterStatut.value) : store.plantings
)
function plantName(id: number) {
const p = plantsStore.plants.find(x => x.id === id)
return p ? (p.variete ? `${p.nom_commun} (${p.variete})` : p.nom_commun) : `Plante #${id}`
return p ? formatPlantLabel(p) : `Plante #${id}`
}
function gardenName(id: number) {
@@ -274,6 +348,44 @@ function startEdit(p: typeof store.plantings[0]) {
function closeCreate() { showCreate.value = false; editId.value = null }
async function loadTemplates() {
templates.value = await tasksApi.list({ statut: 'template' })
}
async function openTaskFromTemplate(planting: Planting) {
if (!templates.value.length) {
await loadTemplates()
}
taskTarget.value = planting
Object.assign(taskTemplateForm, { template_id: 0, echeance: '', extra_description: '' })
showTaskTemplateModal.value = true
}
function closeTaskTemplateModal() {
showTaskTemplateModal.value = false
taskTarget.value = null
}
async function createTaskFromTemplate() {
if (!taskTarget.value || !taskTarget.value.id || !taskTemplateForm.template_id) return
const tpl = templates.value.find(t => t.id === taskTemplateForm.template_id)
if (!tpl) return
const extra = taskTemplateForm.extra_description.trim()
const description = [tpl.description || '', extra].filter(Boolean).join('\n\n')
await tasksApi.create({
titre: tpl.titre,
description: description || undefined,
garden_id: taskTarget.value.garden_id,
planting_id: taskTarget.value.id,
priorite: tpl.priorite || 'normale',
echeance: taskTemplateForm.echeance || undefined,
recurrence: tpl.recurrence ?? null,
frequence_jours: tpl.frequence_jours ?? null,
statut: 'a_faire',
})
closeTaskTemplateModal()
}
async function createPlanting() {
if (editId.value) {
await store.update(editId.value, { ...cForm })
@@ -292,5 +404,6 @@ onMounted(() => {
store.fetchAll()
gardensStore.fetchAll()
plantsStore.fetchAll()
loadTemplates()
})
</script>

View File

@@ -4,6 +4,8 @@ import { usePlantingsStore } from '@/stores/plantings';
import { useGardensStore } from '@/stores/gardens';
import { usePlantsStore } from '@/stores/plants';
import { recoltesApi } from '@/api/recoltes';
import { tasksApi } from '@/api/tasks';
import { formatPlantLabel } from '@/utils/plants';
const store = usePlantingsStore();
const gardensStore = useGardensStore();
const plantsStore = usePlantsStore();
@@ -13,6 +15,9 @@ const filterStatut = ref('');
const openRecoltes = ref(null);
const recoltesList = ref([]);
const loadingRecoltes = ref(false);
const templates = ref([]);
const showTaskTemplateModal = ref(false);
const taskTarget = ref(null);
const statuts = [
{ val: '', label: 'Toutes' },
{ val: 'prevu', label: '📋 Prévu' },
@@ -29,10 +34,15 @@ const cForm = reactive({
const rForm = reactive({
quantite: 1, unite: 'kg', date_recolte: new Date().toISOString().slice(0, 10)
});
const taskTemplateForm = reactive({
template_id: 0,
echeance: '',
extra_description: '',
});
const filtered = computed(() => filterStatut.value ? store.plantings.filter(p => p.statut === filterStatut.value) : store.plantings);
function plantName(id) {
const p = plantsStore.plants.find(x => x.id === id);
return p ? (p.variete ? `${p.nom_commun} (${p.variete})` : p.nom_commun) : `Plante #${id}`;
return p ? formatPlantLabel(p) : `Plante #${id}`;
}
function gardenName(id) {
return gardensStore.gardens.find(g => g.id === id)?.nom ?? `Jardin #${id}`;
@@ -89,6 +99,42 @@ function startEdit(p) {
showCreate.value = true;
}
function closeCreate() { showCreate.value = false; editId.value = null; }
async function loadTemplates() {
templates.value = await tasksApi.list({ statut: 'template' });
}
async function openTaskFromTemplate(planting) {
if (!templates.value.length) {
await loadTemplates();
}
taskTarget.value = planting;
Object.assign(taskTemplateForm, { template_id: 0, echeance: '', extra_description: '' });
showTaskTemplateModal.value = true;
}
function closeTaskTemplateModal() {
showTaskTemplateModal.value = false;
taskTarget.value = null;
}
async function createTaskFromTemplate() {
if (!taskTarget.value || !taskTarget.value.id || !taskTemplateForm.template_id)
return;
const tpl = templates.value.find(t => t.id === taskTemplateForm.template_id);
if (!tpl)
return;
const extra = taskTemplateForm.extra_description.trim();
const description = [tpl.description || '', extra].filter(Boolean).join('\n\n');
await tasksApi.create({
titre: tpl.titre,
description: description || undefined,
garden_id: taskTarget.value.garden_id,
planting_id: taskTarget.value.id,
priorite: tpl.priorite || 'normale',
echeance: taskTemplateForm.echeance || undefined,
recurrence: tpl.recurrence ?? null,
frequence_jours: tpl.frequence_jours ?? null,
statut: 'a_faire',
});
closeTaskTemplateModal();
}
async function createPlanting() {
if (editId.value) {
await store.update(editId.value, { ...cForm });
@@ -107,13 +153,14 @@ onMounted(() => {
store.fetchAll();
gardensStore.fetchAll();
plantsStore.fetchAll();
loadTemplates();
});
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-3xl mx-auto" },
...{ class: "p-4 max-w-5xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-6" },
@@ -212,6 +259,12 @@ for (const [p] of __VLS_getVForSourceType((__VLS_ctx.filtered))) {
...{ 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_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.openTaskFromTemplate(p);
} },
...{ class: "text-xs px-2 py-1 rounded bg-blue/20 text-blue hover:bg-blue/30 transition-colors" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.startEdit(p);
@@ -323,6 +376,76 @@ for (const [p] of __VLS_getVForSourceType((__VLS_ctx.filtered))) {
}
}
}
if (__VLS_ctx.showTaskTemplateModal && __VLS_ctx.taskTarget) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (__VLS_ctx.closeTaskTemplateModal) },
...{ 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-1" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-xs mb-4" },
});
(__VLS_ctx.plantName(__VLS_ctx.taskTarget.variety_id));
(__VLS_ctx.gardenName(__VLS_ctx.taskTarget.garden_id));
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.createTaskFromTemplate) },
...{ 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.select, __VLS_intrinsicElements.select)({
value: (__VLS_ctx.taskTemplateForm.template_id),
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-green" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
value: "",
});
for (const [t] of __VLS_getVForSourceType((__VLS_ctx.templates))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.option, __VLS_intrinsicElements.option)({
key: (t.id),
value: (t.id),
});
(t.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.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.taskTemplateForm.echeance);
__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.taskTemplateForm.extra_description),
rows: "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 resize-none" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-2 justify-end" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.closeTaskTemplateModal) },
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-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
});
}
if (__VLS_ctx.showCreate) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (__VLS_ctx.closeCreate) },
@@ -375,8 +498,7 @@ if (__VLS_ctx.showCreate) {
key: (p.id),
value: (p.id),
});
(p.nom_commun);
(p.variete ? ' — ' + p.variete : '');
(__VLS_ctx.formatPlantLabel(p));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-2 gap-3" },
@@ -484,7 +606,7 @@ if (__VLS_ctx.showCreate) {
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer');
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-3xl']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-5xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
@@ -539,6 +661,14 @@ if (__VLS_ctx.showCreate) {
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-blue/20']} */ ;
/** @type {__VLS_StyleScopedClasses['text-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-blue/30']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-yellow']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:underline']} */ ;
@@ -652,6 +782,92 @@ if (__VLS_ctx.showCreate) {
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['font-bold']} */ ;
/** @type {__VLS_StyleScopedClasses['text-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @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['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['resize-none']} */ ;
/** @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['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['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-col']} */ ;
@@ -833,6 +1049,7 @@ var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
formatPlantLabel: formatPlantLabel,
store: store,
gardensStore: gardensStore,
plantsStore: plantsStore,
@@ -842,9 +1059,13 @@ const __VLS_self = (await import('vue')).defineComponent({
openRecoltes: openRecoltes,
recoltesList: recoltesList,
loadingRecoltes: loadingRecoltes,
templates: templates,
showTaskTemplateModal: showTaskTemplateModal,
taskTarget: taskTarget,
statuts: statuts,
cForm: cForm,
rForm: rForm,
taskTemplateForm: taskTemplateForm,
filtered: filtered,
plantName: plantName,
gardenName: gardenName,
@@ -855,6 +1076,9 @@ const __VLS_self = (await import('vue')).defineComponent({
deleteRecolte: deleteRecolte,
startEdit: startEdit,
closeCreate: closeCreate,
openTaskFromTemplate: openTaskFromTemplate,
closeTaskTemplateModal: closeTaskTemplateModal,
createTaskFromTemplate: createTaskFromTemplate,
createPlanting: createPlanting,
};
},

View File

@@ -1,5 +1,5 @@
<template>
<div class="p-4 max-w-4xl mx-auto">
<div class="p-4 max-w-6xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-green">🌱 Plantes</h1>
<button @click="showForm = true" class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">+ Ajouter</button>
@@ -18,61 +18,63 @@
<!-- Liste -->
<div v-if="plantsStore.loading" class="text-text-muted text-sm">Chargement...</div>
<div v-else-if="!filteredPlants.length" class="text-text-muted text-sm py-4">Aucune plante.</div>
<div v-for="p in filteredPlants" :key="p.id"
class="bg-bg-soft rounded-lg mb-2 border border-bg-hard overflow-hidden">
<!-- En-tête cliquable -->
<div class="p-4 flex items-start justify-between gap-4 cursor-pointer"
@click="toggleDetail(p.id!)">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<span class="text-text font-semibold">{{ p.nom_commun }}</span>
<span v-if="p.variete" class="text-text-muted text-xs"> {{ p.variete }}</span>
<span v-if="p.categorie" :class="['text-xs px-2 py-0.5 rounded-full font-medium', catClass(p.categorie)]">{{ catLabel(p.categorie) }}</span>
</div>
<div class="text-text-muted text-xs flex gap-3 flex-wrap">
<span v-if="p.famille">🌿 {{ p.famille }}</span>
<span v-if="p.espacement_cm"> {{ p.espacement_cm }}cm</span>
<span v-if="p.besoin_eau">💧 {{ p.besoin_eau }}</span>
<span v-if="p.plantation_mois">🌱 Plantation: mois {{ p.plantation_mois }}</span>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-text-muted text-xs">{{ openId === p.id ? '▲' : '▼' }}</span>
<button @click.stop="startEdit(p)" class="text-yellow text-xs hover:underline">Édit.</button>
<button @click.stop="removePlant(p.id!)" class="text-red text-xs hover:underline">Suppr.</button>
</div>
</div>
<!-- Panneau détail -->
<div v-if="openId === p.id" class="border-t border-bg-hard px-4 pb-4 pt-3">
<!-- Notes -->
<p v-if="p.notes" class="text-text-muted text-sm mb-3 italic">{{ p.notes }}</p>
<!-- Galerie photos -->
<div class="mb-2 flex items-center justify-between">
<span class="text-text-muted text-xs font-medium uppercase tracking-wide">Photos</span>
<button @click="openUpload(p)" class="text-green text-xs hover:underline">+ Ajouter une photo</button>
</div>
<div v-if="loadingPhotos" class="text-text-muted text-xs">Chargement...</div>
<div v-else-if="!plantPhotos.length" class="text-text-muted text-xs mb-3">Aucune photo pour cette plante.</div>
<div v-else class="grid grid-cols-4 gap-2 mb-3">
<div v-for="m in plantPhotos" :key="m.id"
class="aspect-square rounded overflow-hidden bg-bg-hard relative group cursor-pointer"
@click="lightbox = m">
<img :src="m.thumbnail_url || m.url" class="w-full h-full object-cover" />
<div v-if="m.identified_common"
class="absolute bottom-0 left-0 right-0 bg-black/70 text-xs text-green px-1 py-0.5 truncate">
{{ m.identified_common }}
<div v-else class="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3">
<div v-for="p in filteredPlants" :key="p.id"
class="bg-bg-soft rounded-lg border border-bg-hard overflow-hidden">
<!-- En-tête cliquable -->
<div class="p-4 flex items-start justify-between gap-4 cursor-pointer"
@click="toggleDetail(p.id!)">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<span class="text-text font-semibold">{{ p.nom_commun }}</span>
<span v-if="p.variete" class="text-text-muted text-xs"> {{ p.variete }}</span>
<span v-if="p.categorie" :class="['text-xs px-2 py-0.5 rounded-full font-medium', catClass(p.categorie)]">{{ catLabel(p.categorie) }}</span>
</div>
<button @click.stop="deletePhoto(m)" class="hidden group-hover:flex absolute top-1 right-1 bg-red/80 text-white text-xs rounded px-1"></button>
<div class="text-text-muted text-xs flex gap-3 flex-wrap">
<span v-if="p.famille">🌿 {{ p.famille }}</span>
<span v-if="p.espacement_cm"> {{ p.espacement_cm }}cm</span>
<span v-if="p.besoin_eau">💧 {{ p.besoin_eau }}</span>
<span v-if="p.plantation_mois">🌱 Plantation: mois {{ p.plantation_mois }}</span>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-text-muted text-xs">{{ openId === p.id ? '▲' : '▼' }}</span>
<button @click.stop="startEdit(p)" class="text-yellow text-xs hover:underline">Édit.</button>
<button @click.stop="removePlant(p.id!)" class="text-red text-xs hover:underline">Suppr.</button>
</div>
</div>
<!-- Lier une photo existante de la bibliothèque -->
<button @click="openLinkPhoto(p)" class="text-blue text-xs hover:underline">
🔗 Lier une photo existante de la bibliothèque
</button>
<!-- Panneau détail -->
<div v-if="openId === p.id" class="border-t border-bg-hard px-4 pb-4 pt-3">
<!-- Notes -->
<p v-if="p.notes" class="text-text-muted text-sm mb-3 italic">{{ p.notes }}</p>
<!-- Galerie photos -->
<div class="mb-2 flex items-center justify-between">
<span class="text-text-muted text-xs font-medium uppercase tracking-wide">Photos</span>
<button @click="openUpload(p)" class="text-green text-xs hover:underline">+ Ajouter une photo</button>
</div>
<div v-if="loadingPhotos" class="text-text-muted text-xs">Chargement...</div>
<div v-else-if="!plantPhotos.length" class="text-text-muted text-xs mb-3">Aucune photo pour cette plante.</div>
<div v-else class="grid grid-cols-4 gap-2 mb-3">
<div v-for="m in plantPhotos" :key="m.id"
class="aspect-square rounded overflow-hidden bg-bg-hard relative group cursor-pointer"
@click="lightbox = m">
<img :src="m.thumbnail_url || m.url" class="w-full h-full object-cover" />
<div v-if="m.identified_common"
class="absolute bottom-0 left-0 right-0 bg-black/70 text-xs text-green px-1 py-0.5 truncate">
{{ m.identified_common }}
</div>
<button @click.stop="deletePhoto(m)" class="hidden group-hover:flex absolute top-1 right-1 bg-red/80 text-white text-xs rounded px-1"></button>
</div>
</div>
<!-- Lier une photo existante de la bibliothèque -->
<button @click="openLinkPhoto(p)" class="text-blue text-xs hover:underline">
🔗 Lier une photo existante de la bibliothèque
</button>
</div>
</div>
</div>
@@ -193,7 +195,7 @@
<!-- Modal upload photo pour une plante -->
<div v-if="uploadTarget" class="fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4" @click.self="uploadTarget = null">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-sm border border-bg-soft">
<h3 class="text-text font-bold mb-4">Photo pour "{{ uploadTarget.nom_commun }}"</h3>
<h3 class="text-text font-bold mb-4">Photo pour "{{ formatPlantLabel(uploadTarget) }}"</h3>
<label class="block border-2 border-dashed border-bg-soft rounded-lg p-6 text-center cursor-pointer hover:border-green transition-colors">
<input type="file" accept="image/*" class="hidden" @change="uploadPhoto" />
<div class="text-text-muted text-sm">📷 Choisir une image</div>
@@ -205,7 +207,7 @@
<!-- Modal lier photo existante -->
<div v-if="linkTarget" class="fixed inset-0 bg-black/70 z-50 flex items-center justify-center p-4" @click.self="linkTarget = null">
<div class="bg-bg-hard rounded-xl p-6 w-full max-w-2xl border border-bg-soft max-h-[80vh] flex flex-col">
<h3 class="text-text font-bold mb-3">Lier une photo à "{{ linkTarget.nom_commun }}"</h3>
<h3 class="text-text font-bold mb-3">Lier une photo à "{{ formatPlantLabel(linkTarget) }}"</h3>
<p class="text-text-muted text-xs mb-3">Sélectionne une photo de la bibliothèque (non liée à une plante)</p>
<div v-if="!unlinkPhotos.length" class="text-text-muted text-sm py-4 text-center">Aucune photo disponible.</div>
<div v-else class="grid grid-cols-4 gap-2 overflow-y-auto flex-1">
@@ -247,6 +249,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
import axios from 'axios'
import { usePlantsStore } from '@/stores/plants'
import type { Plant } from '@/api/plants'
import { formatPlantLabel } from '@/utils/plants'
const plantsStore = usePlantsStore()
const showForm = ref(false)
@@ -285,9 +288,16 @@ const form = reactive({
plantation_mois: '', recolte_mois: '', notes: '',
})
const filteredPlants = computed(() =>
selectedCat.value ? plantsStore.plants.filter(p => p.categorie === selectedCat.value) : plantsStore.plants
)
const filteredPlants = computed(() => {
const source = selectedCat.value
? plantsStore.plants.filter(p => p.categorie === selectedCat.value)
: plantsStore.plants
return [...source].sort((a, b) => {
const byName = (a.nom_commun || '').localeCompare(b.nom_commun || '', 'fr', { sensitivity: 'base' })
if (byName !== 0) return byName
return (a.variete || '').localeCompare(b.variete || '', 'fr', { sensitivity: 'base' })
})
})
const catClass = (cat: string) => ({
potager: 'bg-green/20 text-green',

View File

@@ -2,6 +2,7 @@
import { computed, onMounted, reactive, ref } from 'vue';
import axios from 'axios';
import { usePlantsStore } from '@/stores/plants';
import { formatPlantLabel } from '@/utils/plants';
const plantsStore = usePlantsStore();
const showForm = ref(false);
const editPlant = ref(null);
@@ -29,7 +30,17 @@ const form = reactive({
temp_min_c: undefined,
plantation_mois: '', recolte_mois: '', notes: '',
});
const filteredPlants = computed(() => selectedCat.value ? plantsStore.plants.filter(p => p.categorie === selectedCat.value) : plantsStore.plants);
const filteredPlants = computed(() => {
const source = selectedCat.value
? plantsStore.plants.filter(p => p.categorie === selectedCat.value)
: plantsStore.plants;
return [...source].sort((a, b) => {
const byName = (a.nom_commun || '').localeCompare(b.nom_commun || '', 'fr', { sensitivity: 'base' });
if (byName !== 0)
return byName;
return (a.variete || '').localeCompare(b.variete || '', 'fr', { sensitivity: 'base' });
});
});
const catClass = (cat) => ({
potager: 'bg-green/20 text-green',
fleur: 'bg-orange/20 text-orange',
@@ -148,7 +159,7 @@ 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" },
...{ class: "p-4 max-w-6xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-6" },
@@ -186,161 +197,194 @@ else if (!__VLS_ctx.filteredPlants.length) {
...{ class: "text-text-muted text-sm py-4" },
});
}
for (const [p] of __VLS_getVForSourceType((__VLS_ctx.filteredPlants))) {
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (p.id),
...{ class: "bg-bg-soft rounded-lg mb-2 border border-bg-hard overflow-hidden" },
...{ class: "grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
__VLS_ctx.toggleDetail(p.id);
} },
...{ class: "p-4 flex items-start justify-between gap-4 cursor-pointer" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex-1 min-w-0" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-2 mb-1 flex-wrap" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text font-semibold" },
});
(p.nom_commun);
if (p.variete) {
for (const [p] of __VLS_getVForSourceType((__VLS_ctx.filteredPlants))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (p.id),
...{ class: "bg-bg-soft rounded-lg border border-bg-hard overflow-hidden" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.plantsStore.loading))
return;
if (!!(!__VLS_ctx.filteredPlants.length))
return;
__VLS_ctx.toggleDetail(p.id);
} },
...{ class: "p-4 flex items-start justify-between gap-4 cursor-pointer" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex-1 min-w-0" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-2 mb-1 flex-wrap" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text font-semibold" },
});
(p.nom_commun);
if (p.variete) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text-muted text-xs" },
});
(p.variete);
}
if (p.categorie) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (['text-xs px-2 py-0.5 rounded-full font-medium', __VLS_ctx.catClass(p.categorie)]) },
});
(__VLS_ctx.catLabel(p.categorie));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs flex gap-3 flex-wrap" },
});
if (p.famille) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.famille);
}
if (p.espacement_cm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.espacement_cm);
}
if (p.besoin_eau) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.besoin_eau);
}
if (p.plantation_mois) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.plantation_mois);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-2 shrink-0" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text-muted text-xs" },
});
(p.variete);
}
if (p.categorie) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (['text-xs px-2 py-0.5 rounded-full font-medium', __VLS_ctx.catClass(p.categorie)]) },
});
(__VLS_ctx.catLabel(p.categorie));
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs flex gap-3 flex-wrap" },
});
if (p.famille) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.famille);
}
if (p.espacement_cm) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.espacement_cm);
}
if (p.besoin_eau) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.besoin_eau);
}
if (p.plantation_mois) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(p.plantation_mois);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-2 shrink-0" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text-muted text-xs" },
});
(__VLS_ctx.openId === p.id ? '▲' : '▼');
__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)({
...{ onClick: (...[$event]) => {
__VLS_ctx.removePlant(p.id);
} },
...{ class: "text-red text-xs hover:underline" },
});
if (__VLS_ctx.openId === p.id) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "border-t border-bg-hard px-4 pb-4 pt-3" },
});
if (p.notes) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-sm mb-3 italic" },
});
(p.notes);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mb-2 flex items-center justify-between" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text-muted text-xs font-medium uppercase tracking-wide" },
(__VLS_ctx.openId === p.id ? '▲' : '▼');
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.plantsStore.loading))
return;
if (!!(!__VLS_ctx.filteredPlants.length))
return;
__VLS_ctx.startEdit(p);
} },
...{ class: "text-yellow text-xs hover:underline" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.openId === p.id))
if (!!(__VLS_ctx.plantsStore.loading))
return;
__VLS_ctx.openUpload(p);
if (!!(!__VLS_ctx.filteredPlants.length))
return;
__VLS_ctx.removePlant(p.id);
} },
...{ class: "text-green text-xs hover:underline" },
...{ class: "text-red text-xs hover:underline" },
});
if (__VLS_ctx.loadingPhotos) {
if (__VLS_ctx.openId === p.id) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
...{ class: "border-t border-bg-hard px-4 pb-4 pt-3" },
});
}
else if (!__VLS_ctx.plantPhotos.length) {
if (p.notes) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-sm mb-3 italic" },
});
(p.notes);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mb-3" },
...{ class: "mb-2 flex items-center justify-between" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-4 gap-2 mb-3" },
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text-muted text-xs font-medium uppercase tracking-wide" },
});
for (const [m] of __VLS_getVForSourceType((__VLS_ctx.plantPhotos))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.plantsStore.loading))
return;
if (!!(!__VLS_ctx.filteredPlants.length))
return;
if (!(__VLS_ctx.openId === p.id))
return;
__VLS_ctx.openUpload(p);
} },
...{ class: "text-green text-xs hover:underline" },
});
if (__VLS_ctx.loadingPhotos) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.openId === p.id))
return;
if (!!(__VLS_ctx.loadingPhotos))
return;
if (!!(!__VLS_ctx.plantPhotos.length))
return;
__VLS_ctx.lightbox = m;
} },
key: (m.id),
...{ class: "aspect-square rounded overflow-hidden bg-bg-hard relative group cursor-pointer" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (m.thumbnail_url || m.url),
...{ 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.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.openId === p.id))
return;
if (!!(__VLS_ctx.loadingPhotos))
return;
if (!!(!__VLS_ctx.plantPhotos.length))
return;
__VLS_ctx.deletePhoto(m);
} },
...{ class: "hidden group-hover:flex absolute top-1 right-1 bg-red/80 text-white text-xs rounded px-1" },
...{ class: "text-text-muted text-xs" },
});
}
else if (!__VLS_ctx.plantPhotos.length) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs mb-3" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-4 gap-2 mb-3" },
});
for (const [m] of __VLS_getVForSourceType((__VLS_ctx.plantPhotos))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.plantsStore.loading))
return;
if (!!(!__VLS_ctx.filteredPlants.length))
return;
if (!(__VLS_ctx.openId === p.id))
return;
if (!!(__VLS_ctx.loadingPhotos))
return;
if (!!(!__VLS_ctx.plantPhotos.length))
return;
__VLS_ctx.lightbox = m;
} },
key: (m.id),
...{ class: "aspect-square rounded overflow-hidden bg-bg-hard relative group cursor-pointer" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.img)({
src: (m.thumbnail_url || m.url),
...{ 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.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.plantsStore.loading))
return;
if (!!(!__VLS_ctx.filteredPlants.length))
return;
if (!(__VLS_ctx.openId === p.id))
return;
if (!!(__VLS_ctx.loadingPhotos))
return;
if (!!(!__VLS_ctx.plantPhotos.length))
return;
__VLS_ctx.deletePhoto(m);
} },
...{ class: "hidden group-hover:flex absolute top-1 right-1 bg-red/80 text-white text-xs rounded px-1" },
});
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.plantsStore.loading))
return;
if (!!(!__VLS_ctx.filteredPlants.length))
return;
if (!(__VLS_ctx.openId === p.id))
return;
__VLS_ctx.openLinkPhoto(p);
} },
...{ class: "text-blue text-xs hover:underline" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.openId === p.id))
return;
__VLS_ctx.openLinkPhoto(p);
} },
...{ class: "text-blue text-xs hover:underline" },
});
}
}
if (__VLS_ctx.showForm || __VLS_ctx.editPlant) {
@@ -597,7 +641,7 @@ if (__VLS_ctx.uploadTarget) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.h3, __VLS_intrinsicElements.h3)({
...{ class: "text-text font-bold mb-4" },
});
(__VLS_ctx.uploadTarget.nom_commun);
(__VLS_ctx.formatPlantLabel(__VLS_ctx.uploadTarget));
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "block border-2 border-dashed border-bg-soft rounded-lg p-6 text-center cursor-pointer hover:border-green transition-colors" },
});
@@ -634,7 +678,7 @@ if (__VLS_ctx.linkTarget) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.h3, __VLS_intrinsicElements.h3)({
...{ class: "text-text font-bold mb-3" },
});
(__VLS_ctx.linkTarget.nom_commun);
(__VLS_ctx.formatPlantLabel(__VLS_ctx.linkTarget));
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-xs mb-3" },
});
@@ -733,7 +777,7 @@ if (__VLS_ctx.lightbox) {
});
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-4xl']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-6xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
@@ -759,9 +803,13 @@ if (__VLS_ctx.lightbox) {
/** @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-1']} */ ;
/** @type {__VLS_StyleScopedClasses['lg:grid-cols-2']} */ ;
/** @type {__VLS_StyleScopedClasses['2xl:grid-cols-3']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg-soft']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
@@ -1274,6 +1322,7 @@ var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
formatPlantLabel: formatPlantLabel,
plantsStore: plantsStore,
showForm: showForm,
editPlant: editPlant,

View File

@@ -2,10 +2,80 @@
import { onMounted, ref } from 'vue';
import { settingsApi } from '@/api/settings';
import { meteoApi } from '@/api/meteo';
import { UI_SIZE_DEFAULTS, applyUiSizesToRoot } from '@/utils/uiSizeDefaults';
const debugMode = ref(false);
const saving = ref(false);
const savedMsg = ref('');
const refreshingMeteo = ref(false);
const downloadingBackup = ref(false);
const backupMsg = ref('');
const apiBaseUrl = detectApiBaseUrl();
// --- UI Size settings ---
const uiSizeSettings = [
{ key: 'ui_font_size', label: 'Taille texte', min: 12, max: 20, step: 1, unit: 'px' },
{ key: 'ui_menu_font_size', label: 'Texte menu latéral', min: 11, max: 18, step: 1, unit: 'px' },
{ key: 'ui_menu_icon_size', label: 'Icônes menu', min: 14, max: 28, step: 1, unit: 'px' },
{ key: 'ui_thumb_size', label: 'Miniatures images/vidéo', min: 60, max: 200, step: 4, unit: 'px' },
];
const uiSizes = ref({ ...UI_SIZE_DEFAULTS });
const savingUi = ref(false);
const uiSavedMsg = ref('');
function applyUiSizes() {
applyUiSizesToRoot(uiSizes.value);
window.dispatchEvent(new CustomEvent('ui-sizes-updated', { detail: { ...uiSizes.value } }));
}
async function saveUiSettings() {
savingUi.value = true;
uiSavedMsg.value = '';
try {
const payload = {};
for (const [k, v] of Object.entries(uiSizes.value))
payload[k] = String(v);
await settingsApi.update(payload);
applyUiSizes();
uiSavedMsg.value = 'Enregistré';
setTimeout(() => { uiSavedMsg.value = ''; }, 1800);
}
catch {
uiSavedMsg.value = 'Erreur lors de l\'enregistrement.';
setTimeout(() => { uiSavedMsg.value = ''; }, 2200);
}
finally {
savingUi.value = false;
}
}
function resetUiSettings() {
uiSizes.value = { ...UI_SIZE_DEFAULTS };
applyUiSizes();
}
function detectApiBaseUrl() {
const envBase = String(import.meta.env?.VITE_API_URL || '').trim();
if (envBase) {
if (envBase.startsWith('http://') || envBase.startsWith('https://')) {
return envBase.replace(/\/$/, '');
}
if (envBase.startsWith('/')) {
return window.location.origin;
}
}
if (window.location.port === '8060') {
return window.location.origin;
}
return `${window.location.protocol}//${window.location.hostname}:8060`;
}
function openInNewTab(path) {
const url = `${apiBaseUrl}${path}`;
window.open(url, '_blank', 'noopener,noreferrer');
}
function openApiDocs() {
openInNewTab('/docs');
}
function openApiRedoc() {
openInNewTab('/redoc');
}
function openApiHealth() {
openInNewTab('/api/health');
}
function toBool(value) {
if (typeof value === 'boolean')
return value;
@@ -21,6 +91,12 @@ async function loadSettings() {
const data = await settingsApi.get();
debugMode.value = toBool(data.debug_mode);
notifyDebugChanged(debugMode.value);
for (const s of uiSizeSettings) {
const v = data[s.key];
if (v != null)
uiSizes.value[s.key] = Number(v) || UI_SIZE_DEFAULTS[s.key];
}
applyUiSizes();
}
catch {
// Laisse la valeur locale si l'API n'est pas disponible.
@@ -48,6 +124,29 @@ async function refreshMeteo() {
refreshingMeteo.value = false;
}
}
async function downloadBackup() {
downloadingBackup.value = true;
backupMsg.value = '';
try {
const { blob, filename } = await settingsApi.downloadBackup();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
backupMsg.value = 'Téléchargement lancé.';
}
catch {
backupMsg.value = 'Erreur lors de la sauvegarde.';
}
finally {
downloadingBackup.value = false;
window.setTimeout(() => { backupMsg.value = ''; }, 2200);
}
}
onMounted(() => {
void loadSettings();
});
@@ -67,6 +166,61 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.section, __VLS_intrinsicElemen
__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-4" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid grid-cols-1 gap-4" },
});
for (const [s] of __VLS_getVForSourceType((__VLS_ctx.uiSizeSettings))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
key: (s.key),
...{ class: "flex items-center gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.label, __VLS_intrinsicElements.label)({
...{ class: "text-sm text-text w-44 shrink-0" },
});
(s.label);
__VLS_asFunctionalElement(__VLS_intrinsicElements.input)({
...{ onInput: (__VLS_ctx.applyUiSizes) },
type: "range",
min: (s.min),
max: (s.max),
step: (s.step),
...{ class: "flex-1 accent-green" },
});
(__VLS_ctx.uiSizes[s.key]);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text-muted text-xs w-12 text-right" },
});
(__VLS_ctx.uiSizes[s.key]);
(s.unit);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mt-4 flex items-center gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.saveUiSettings) },
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
disabled: (__VLS_ctx.savingUi),
});
(__VLS_ctx.savingUi ? 'Enregistrement...' : 'Enregistrer');
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.resetUiSettings) },
...{ class: "text-text-muted text-xs hover:text-text px-2" },
});
if (__VLS_ctx.uiSavedMsg) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-xs text-aqua" },
});
(__VLS_ctx.uiSavedMsg);
}
__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" },
});
@@ -108,19 +262,61 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElement
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 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-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-xs mb-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-text" },
});
(__VLS_ctx.apiBaseUrl);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-wrap items-center gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.openApiDocs) },
...{ class: "bg-blue text-bg px-3 py-2 rounded-lg text-xs font-semibold hover:opacity-90" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.openApiRedoc) },
...{ class: "bg-aqua text-bg px-3 py-2 rounded-lg text-xs font-semibold hover:opacity-90" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.openApiHealth) },
...{ class: "bg-bg border border-bg-hard text-text px-3 py-2 rounded-lg text-xs font-semibold hover:border-text-muted" },
});
__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.p, __VLS_intrinsicElements.p)({
...{ class: "text-text-muted text-sm mb-3" },
});
__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)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-2" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.downloadBackup) },
...{ class: "bg-aqua text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
disabled: (__VLS_ctx.downloadingBackup),
});
(__VLS_ctx.downloadingBackup ? 'Préparation du ZIP...' : 'Télécharger la sauvegarde (.zip)');
if (__VLS_ctx.backupMsg) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-xs text-aqua" },
});
(__VLS_ctx.backupMsg);
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-3xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
@@ -139,6 +335,52 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.li, __VLS_intrinsicElements.li
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['grid']} */ ;
/** @type {__VLS_StyleScopedClasses['grid-cols-1']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-4']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['w-44']} */ ;
/** @type {__VLS_StyleScopedClasses['shrink-0']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['accent-green']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['w-12']} */ ;
/** @type {__VLS_StyleScopedClasses['text-right']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-4']} */ ;
/** @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-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @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['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
@@ -185,12 +427,71 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.li, __VLS_intrinsicElements.li
/** @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['space-y-1']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-wrap']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-blue']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-aqua']} */ ;
/** @type {__VLS_StyleScopedClasses['text-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:opacity-90']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['border-bg-hard']} */ ;
/** @type {__VLS_StyleScopedClasses['text-text']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:border-text-muted']} */ ;
/** @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['mb-3']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-aqua']} */ ;
/** @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']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
@@ -199,8 +500,22 @@ const __VLS_self = (await import('vue')).defineComponent({
saving: saving,
savedMsg: savedMsg,
refreshingMeteo: refreshingMeteo,
downloadingBackup: downloadingBackup,
backupMsg: backupMsg,
apiBaseUrl: apiBaseUrl,
uiSizeSettings: uiSizeSettings,
uiSizes: uiSizes,
savingUi: savingUi,
uiSavedMsg: uiSavedMsg,
applyUiSizes: applyUiSizes,
saveUiSettings: saveUiSettings,
resetUiSettings: resetUiSettings,
openApiDocs: openApiDocs,
openApiRedoc: openApiRedoc,
openApiHealth: openApiHealth,
saveSettings: saveSettings,
refreshMeteo: refreshMeteo,
downloadBackup: downloadBackup,
};
},
});

View File

@@ -1,9 +1,9 @@
<template>
<div class="p-4 max-w-2xl mx-auto">
<div class="p-4 max-w-5xl mx-auto">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-green"> Tâches</h1>
<button class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
@click="openCreate">+ Nouvelle</button>
@click="openCreateTemplate">+ Nouveau template</button>
</div>
<div v-for="[groupe, label] in groupes" :key="groupe" class="mb-6">
@@ -18,17 +18,25 @@
}"></span>
<div class="flex-1 min-w-0">
<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.description" class="text-text-muted text-xs">{{ t.description }}</div>
<div v-if="t.echeance && t.statut !== 'template'" 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 v-if="t.planting_id && t.statut !== 'template'" class="text-text-muted text-xs">🌱 Plantation #{{ t.planting_id }}</div>
</div>
<div class="flex gap-1 items-center shrink-0">
<button v-if="t.statut === 'a_faire'" class="text-xs text-blue hover:underline"
@click="store.updateStatut(t.id!, 'en_cours')"> En cours</button>
<button v-if="t.statut === 'en_cours'" class="text-xs text-green hover:underline"
@click="store.updateStatut(t.id!, 'fait')"> Fait</button>
<button @click="startEdit(t)" class="text-xs text-yellow hover:underline ml-2">Édit.</button>
<button
v-if="t.statut === 'template'"
@click="startEdit(t)"
class="text-xs text-yellow hover:underline ml-2"
>
Édit.
</button>
<button class="text-xs text-text-muted hover:text-red ml-1" @click="store.remove(t.id!)"></button>
</div>
</div>
@@ -37,7 +45,7 @@
<!-- 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 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 la tâche' : 'Nouvelle tâche' }}</h2>
<h2 class="text-text font-bold text-lg mb-4">{{ editId ? 'Modifier la tâche' : 'Nouveau template' }}</h2>
<form @submit.prevent="submit" class="grid gap-3">
<div>
<label class="text-text-muted text-xs block mb-1">Titre *</label>
@@ -59,25 +67,17 @@
</select>
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Statut</label>
<select v-model="form.statut" class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text text-sm">
<option value="a_faire">À faire</option>
<option value="en_cours">En cours</option>
<option value="fait">Terminé</option>
</select>
<label class="text-text-muted text-xs block mb-1">Type</label>
<input value="Template" readonly
class="w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text-muted text-sm" />
</div>
</div>
<div>
<label class="text-text-muted text-xs block mb-1">Échéance</label>
<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" />
</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>
<p class="text-text-muted text-[11px] mt-1">Fréquence proposée quand la tâche est ajoutée depuis une plantation.</p>
</div>
<div v-if="form.repetition">
<label class="text-text-muted text-xs block mb-1">Fréquence (jours)</label>
@@ -93,7 +93,7 @@
</div>
<div class="flex gap-2 mt-2">
<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 le template' }}
</button>
<button type="button" class="text-text-muted text-sm px-4 py-2 hover:text-text" @click="closeForm">Annuler</button>
</div>
@@ -115,8 +115,7 @@ const form = reactive({
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
statut: 'template',
repetition: false,
frequence_jours: undefined as number | undefined,
})
@@ -125,6 +124,7 @@ const groupes: [string, string][] = [
['a_faire', 'À faire'],
['en_cours', 'En cours'],
['fait', 'Terminé'],
['template', 'Templates'],
]
const byStatut = (s: string) => store.tasks.filter(t => t.statut === s)
@@ -133,33 +133,40 @@ function fmtDate(s: string) {
return new Date(s + 'T12:00:00').toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })
}
function openCreate() {
editId.value = null
function resetForm() {
Object.assign(form, {
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
statut: 'template',
repetition: false,
frequence_jours: undefined,
})
}
function openCreateTemplate() {
editId.value = null
resetForm()
showForm.value = true
}
function startEdit(t: Task) {
editId.value = t.id!
Object.assign(form, {
titre: t.titre, description: (t as any).description || '',
priorite: t.priorite, statut: t.statut,
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,
titre: t.titre,
description: t.description || '',
priorite: t.priorite,
statut: t.statut || 'template',
repetition: Boolean(t.recurrence || t.frequence_jours),
frequence_jours: t.frequence_jours ?? undefined,
})
showForm.value = true
}
function closeForm() { showForm.value = false; editId.value = null }
function closeForm() {
showForm.value = false
editId.value = null
}
onMounted(() => store.fetchAll())
@@ -168,10 +175,11 @@ async function submit() {
titre: form.titre,
description: form.description,
priorite: form.priorite,
statut: form.statut,
echeance: form.echeance || undefined,
statut: 'template',
recurrence: form.repetition ? 'jours' : null,
frequence_jours: form.repetition ? (form.frequence_jours ?? 7) : null,
echeance: undefined,
planting_id: undefined,
}
if (editId.value) {
await store.update(editId.value, payload)
@@ -179,5 +187,6 @@ async function submit() {
await store.create(payload)
}
closeForm()
resetForm()
}
</script>

View File

@@ -8,8 +8,7 @@ const form = reactive({
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
statut: 'template',
repetition: false,
frequence_jours: undefined,
});
@@ -17,46 +16,54 @@ const groupes = [
['a_faire', 'À faire'],
['en_cours', 'En cours'],
['fait', 'Terminé'],
['template', 'Templates'],
];
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;
function resetForm() {
Object.assign(form, {
titre: '',
description: '',
priorite: 'normale',
statut: 'a_faire',
echeance: '',
statut: 'template',
repetition: false,
frequence_jours: undefined,
});
}
function openCreateTemplate() {
editId.value = null;
resetForm();
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) : '',
titre: t.titre,
description: t.description || '',
priorite: t.priorite,
statut: t.statut || 'template',
repetition: Boolean(t.recurrence || t.frequence_jours),
frequence_jours: t.frequence_jours ?? undefined,
});
showForm.value = true;
}
function closeForm() { showForm.value = false; editId.value = null; }
function closeForm() {
showForm.value = false;
editId.value = null;
}
onMounted(() => store.fetchAll());
async function submit() {
const payload = {
titre: form.titre,
description: form.description,
priorite: form.priorite,
statut: form.statut,
echeance: form.echeance || undefined,
statut: 'template',
recurrence: form.repetition ? 'jours' : null,
frequence_jours: form.repetition ? (form.frequence_jours ?? 7) : null,
echeance: undefined,
planting_id: undefined,
};
if (editId.value) {
await store.update(editId.value, payload);
@@ -65,13 +72,14 @@ async function submit() {
await store.create(payload);
}
closeForm();
resetForm();
}
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-2xl mx-auto" },
...{ class: "p-4 max-w-5xl mx-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center justify-between mb-6" },
@@ -80,7 +88,7 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1
...{ class: "text-2xl font-bold text-green" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.openCreate) },
...{ onClick: (__VLS_ctx.openCreateTemplate) },
...{ class: "bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90" },
});
for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
@@ -116,7 +124,13 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
...{ class: "text-text text-sm" },
});
(t.titre);
if (t.echeance) {
if (t.description) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
});
(t.description);
}
if (t.echeance && t.statut !== 'template') {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
});
@@ -128,6 +142,12 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
});
(t.frequence_jours);
}
if (t.planting_id && t.statut !== 'template') {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "text-text-muted text-xs" },
});
(t.planting_id);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-1 items-center shrink-0" },
});
@@ -151,12 +171,16 @@ for (const [[groupe, label]] of __VLS_getVForSourceType((__VLS_ctx.groupes))) {
...{ 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" },
});
if (t.statut === 'template') {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(t.statut === 'template'))
return;
__VLS_ctx.startEdit(t);
} },
...{ class: "text-xs text-yellow hover:underline ml-2" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
__VLS_ctx.store.remove(t.id);
@@ -176,7 +200,7 @@ if (__VLS_ctx.showForm) {
__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_ctx.editId ? 'Modifier la tâche' : 'Nouveau template');
__VLS_asFunctionalElement(__VLS_intrinsicElements.form, __VLS_intrinsicElements.form)({
...{ onSubmit: (__VLS_ctx.submit) },
...{ class: "grid gap-3" },
@@ -223,28 +247,11 @@ if (__VLS_ctx.showForm) {
__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" },
value: "Template",
readonly: true,
...{ class: "w-full bg-bg border border-bg-hard rounded px-3 py-2 text-text-muted text-sm" },
});
(__VLS_ctx.form.echeance);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-bg rounded border border-bg-hard p-3" },
});
@@ -281,7 +288,7 @@ if (__VLS_ctx.showForm) {
type: "submit",
...{ class: "bg-green text-bg px-4 py-2 rounded text-sm font-semibold" },
});
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer');
(__VLS_ctx.editId ? 'Enregistrer' : 'Créer le template');
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.closeForm) },
type: "button",
@@ -289,7 +296,7 @@ if (__VLS_ctx.showForm) {
});
}
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-5xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
@@ -333,6 +340,10 @@ if (__VLS_ctx.showForm) {
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @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['text-text-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
@@ -430,23 +441,8 @@ if (__VLS_ctx.showForm) {
/** @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['focus:border-green']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-bg']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
@@ -502,7 +498,7 @@ const __VLS_self = (await import('vue')).defineComponent({
groupes: groupes,
byStatut: byStatut,
fmtDate: fmtDate,
openCreate: openCreate,
openCreateTemplate: openCreateTemplate,
startEdit: startEdit,
closeForm: closeForm,
submit: submit,