update frontend ui, i18n, filters, and deps

This commit is contained in:
2026-01-22 06:18:04 +01:00
parent 88624f3bed
commit 2b659920c2
52 changed files with 13874 additions and 13 deletions

View File

@@ -0,0 +1,61 @@
<template>
<section class="card" style="margin-bottom: 16px;">
<h2>{{ mode === 'edit' ? t('form.editCategorie') : t('form.createCategorie') }}</h2>
<div style="display: grid; gap: 8px;">
<input v-model="localForm.nom" :placeholder="t('form.nom')" />
<input v-model="localForm.parent_id" :placeholder="t('form.parentIdOpt')" />
<input v-model="localForm.slug" :placeholder="t('form.slug')" />
<input v-model="localForm.icone" :placeholder="t('form.icone')" />
<div style="display: flex; gap: 8px;">
<button class="card" type="button" :disabled="saving" @click="emitSave">
{{ saving ? t('form.saving') : t('form.save') }}
</button>
<button class="card" type="button" @click="emitCancel">{{ t('form.cancel') }}</button>
</div>
<p v-if="message">{{ message }}</p>
</div>
</section>
</template>
<script setup lang="ts">
const { t } = useI18n()
type CategorieFormPayload = {
nom: string
parent_id: string
slug: string
icone: string
}
const props = defineProps<{
modelValue: CategorieFormPayload
saving: boolean
mode: 'create' | 'edit'
message?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: CategorieFormPayload): void
(e: 'save'): void
(e: 'cancel'): void
}>()
const localForm = reactive({ ...props.modelValue })
watch(
() => props.modelValue,
(value) => {
Object.assign(localForm, value)
},
{ deep: true }
)
watch(
localForm,
() => emit('update:modelValue', { ...localForm }),
{ deep: true }
)
const emitSave = () => emit('save')
const emitCancel = () => emit('cancel')
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div v-if="open" class="modal-overlay">
<div class="modal-card">
<h3>{{ title || t('confirm.title') }}</h3>
<p>{{ message || t('confirm.message') }}</p>
<div class="modal-actions">
<button class="card" type="button" @click="cancel">{{ t('actions.cancel') }}</button>
<button class="card" type="button" @click="confirm">{{ t('actions.confirm') }}</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const props = defineProps({
open: { type: Boolean, default: false },
title: { type: String, default: '' },
message: { type: String, default: '' }
})
const emit = defineEmits(['confirm', 'cancel'])
const confirm = () => emit('confirm')
const cancel = () => emit('cancel')
</script>

View File

@@ -0,0 +1,63 @@
<template>
<section class="card" style="margin-bottom: 16px;">
<h2>{{ mode === 'edit' ? t('form.editEmplacement') : t('form.createEmplacement') }}</h2>
<div style="display: grid; gap: 8px;">
<input v-model="localForm.nom" :placeholder="t('form.nom')" />
<input v-model="localForm.parent_id" :placeholder="t('form.parentIdOpt')" />
<input v-model="localForm.piece" :placeholder="t('form.piece')" />
<input v-model="localForm.meuble" :placeholder="t('form.meuble')" />
<input v-model="localForm.numero_boite" :placeholder="t('form.numeroBoite')" />
<div style="display: flex; gap: 8px;">
<button class="card" type="button" :disabled="saving" @click="emitSave">
{{ saving ? t('form.saving') : t('form.save') }}
</button>
<button class="card" type="button" @click="emitCancel">{{ t('form.cancel') }}</button>
</div>
<p v-if="message">{{ message }}</p>
</div>
</section>
</template>
<script setup lang="ts">
const { t } = useI18n()
type EmplacementFormPayload = {
nom: string
parent_id: string
piece: string
meuble: string
numero_boite: string
}
const props = defineProps<{
modelValue: EmplacementFormPayload
saving: boolean
mode: 'create' | 'edit'
message?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: EmplacementFormPayload): void
(e: 'save'): void
(e: 'cancel'): void
}>()
const localForm = reactive({ ...props.modelValue })
watch(
() => props.modelValue,
(value) => {
Object.assign(localForm, value)
},
{ deep: true }
)
watch(
localForm,
() => emit('update:modelValue', { ...localForm }),
{ deep: true }
)
const emitSave = () => emit('save')
const emitCancel = () => emit('cancel')
</script>

View File

@@ -0,0 +1,58 @@
<template>
<label style="display: grid; gap: 6px;">
<span>{{ t('labels.emplacement') }}</span>
<select v-model="selectedId">
<option value="">{{ t('labels.chooseEmplacement') }}</option>
<option v-for="opt in options" :key="opt.id" :value="opt.id">
{{ opt.label }}
</option>
</select>
</label>
</template>
<script setup lang="ts">
const { t } = useI18n()
type Emplacement = {
id: string
nom: string
parent_id?: string | null
}
type Option = { id: string; label: string }
const props = defineProps<{
items: Emplacement[]
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const selectedId = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const options = computed<Option[]>(() => {
const map = new Map(props.items.map((item) => [item.id, item]))
const cache = new Map<string, number>()
const depthOf = (item: Emplacement): number => {
if (!item.parent_id) return 0
if (cache.has(item.id)) return cache.get(item.id) || 0
const parent = map.get(item.parent_id)
const depth = parent ? depthOf(parent) + 1 : 0
cache.set(item.id, depth)
return depth
}
return props.items
.map((item) => ({
id: item.id,
label: `${' '.repeat(depthOf(item) * 2)}${item.nom}`
}))
.sort((a, b) => a.label.localeCompare(b.label))
})
</script>

View File

@@ -0,0 +1,94 @@
<template>
<div>
<div
class="card"
:style="dropStyle"
@dragenter.prevent="onDragEnter"
@dragover.prevent
@dragleave.prevent="onDragLeave"
@drop.prevent="onDrop"
>
<p>{{ label || t('fileUploader.label') }}</p>
<input :disabled="disabled" type="file" multiple @change="onFilesSelected" />
<button class="card" type="button" :disabled="disabled" @click="emitUpload">
{{ buttonText || t('actions.upload') }}
</button>
</div>
<div v-if="previews.length" class="grid" style="margin-top: 12px;">
<div v-for="preview in previews" :key="preview.name" class="card">
<img v-if="preview.url" :src="preview.url" :alt="preview.name" style="max-width: 100%;" />
<p>{{ preview.name }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const props = defineProps({
disabled: { type: Boolean, default: false },
buttonText: { type: String, default: '' },
label: { type: String, default: '' }
})
const emit = defineEmits<{
(e: 'upload', files: FileList): void
}>()
const files = ref<FileList | null>(null)
const isDragging = ref(false)
const previews = ref<{ name: string; url?: string }[]>([])
const dropStyle = computed(() => ({
border: isDragging.value ? '2px dashed #c46b2d' : '1px dashed #d9c9b2',
padding: '16px',
cursor: props.disabled ? 'not-allowed' : 'pointer',
opacity: props.disabled ? '0.6' : '1'
}))
const onFilesSelected = (event: Event) => {
const target = event.target as HTMLInputElement
files.value = target.files
buildPreviews(target.files)
}
const emitUpload = () => {
if (!files.value || files.value.length === 0) {
return
}
emit('upload', files.value)
}
const onDragEnter = () => {
if (props.disabled) return
isDragging.value = true
}
const onDragLeave = () => {
isDragging.value = false
}
const onDrop = (event: DragEvent) => {
if (props.disabled) return
isDragging.value = false
if (event.dataTransfer?.files) {
files.value = event.dataTransfer.files
buildPreviews(event.dataTransfer.files)
}
}
const buildPreviews = (fileList: FileList | null) => {
previews.value = []
if (!fileList) return
Array.from(fileList).forEach((file) => {
if (file.type.startsWith('image/')) {
const url = URL.createObjectURL(file)
previews.value.push({ name: file.name, url })
} else {
previews.value.push({ name: file.name })
}
})
}
</script>

View File

@@ -0,0 +1,10 @@
<template>
<div class="card" style="margin-bottom: 16px;">
<h3>{{ t('i18n.title') }}</h3>
<p>{{ t('i18n.description') }}</p>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
</script>

View File

@@ -0,0 +1,66 @@
<template>
<section class="card" style="margin-bottom: 16px;">
<h2>{{ mode === 'edit' ? t('form.editObjet') : t('form.createObjet') }}</h2>
<div style="display: grid; gap: 8px;">
<input v-model="localForm.nom" :placeholder="t('form.nom')" />
<textarea v-model="localForm.description" rows="3" :placeholder="t('form.description')" />
<input v-model.number="localForm.quantite" type="number" :placeholder="t('form.quantite')" />
<select v-model="localForm.statut">
<option value="en_stock">en_stock</option>
<option value="pret">pret</option>
<option value="hors_service">hors_service</option>
<option value="archive">archive</option>
</select>
<div style="display: flex; gap: 8px;">
<button class="card" type="button" :disabled="saving" @click="emitSave">
{{ saving ? t('form.saving') : t('form.save') }}
</button>
<button class="card" type="button" @click="emitCancel">{{ t('form.cancel') }}</button>
</div>
<p v-if="message">{{ message }}</p>
</div>
</section>
</template>
<script setup lang="ts">
const { t } = useI18n()
type ObjetFormPayload = {
nom: string
description: string
quantite: number
statut: string
}
const props = defineProps<{
modelValue: ObjetFormPayload
saving: boolean
mode: 'create' | 'edit'
message?: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: ObjetFormPayload): void
(e: 'save'): void
(e: 'cancel'): void
}>()
const localForm = reactive({ ...props.modelValue })
watch(
() => props.modelValue,
(value) => {
Object.assign(localForm, value)
},
{ deep: true }
)
watch(
localForm,
() => emit('update:modelValue', { ...localForm }),
{ deep: true }
)
const emitSave = () => emit('save')
const emitCancel = () => emit('cancel')
</script>

View File

@@ -0,0 +1,94 @@
<template>
<ul class="tree-list">
<li v-for="node in flatNodes" :key="node.id" :style="{ paddingLeft: `${node.depth * 12}px` }">
<button
v-if="node.hasChildren"
class="card"
type="button"
style="margin-right: 6px;"
@click="toggle(node.id)"
>
{{ isCollapsed(node.id) ? '+' : '-' }}
</button>
<span>{{ node.nom }}</span>
</li>
</ul>
</template>
<script setup lang="ts">
type TreeItem = {
id: string
nom: string
parent_id?: string | null
}
type FlatNode = TreeItem & {
depth: number
hasChildren: boolean
}
const props = defineProps<{ items: TreeItem[] }>()
const collapsed = ref<Set<string>>(new Set())
const childrenMap = computed(() => {
const map = new Map<string | null, TreeItem[]>()
props.items.forEach((item) => {
const key = item.parent_id || null
const list = map.get(key) || []
list.push(item)
map.set(key, list)
})
return map
})
const hasChildren = (id: string) => {
const list = childrenMap.value.get(id)
return !!(list && list.length)
}
const buildFlat = (parentId: string | null, depth: number, acc: FlatNode[]) => {
const children = childrenMap.value.get(parentId) || []
children
.slice()
.sort((a, b) => a.nom.localeCompare(b.nom))
.forEach((child) => {
acc.push({
...child,
depth,
hasChildren: hasChildren(child.id)
})
if (!collapsed.value.has(child.id)) {
buildFlat(child.id, depth + 1, acc)
}
})
}
const flatNodes = computed(() => {
const acc: FlatNode[] = []
buildFlat(null, 0, acc)
return acc
})
const toggle = (id: string) => {
const next = new Set(collapsed.value)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
collapsed.value = next
}
const isCollapsed = (id: string) => collapsed.value.has(id)
</script>
<style scoped>
.tree-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 6px;
}
</style>