update frontend ui, i18n, filters, and deps
This commit is contained in:
61
frontend/components/CategorieForm.vue
Normal file
61
frontend/components/CategorieForm.vue
Normal 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>
|
||||
27
frontend/components/ConfirmDialog.vue
Normal file
27
frontend/components/ConfirmDialog.vue
Normal 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>
|
||||
63
frontend/components/EmplacementForm.vue
Normal file
63
frontend/components/EmplacementForm.vue
Normal 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>
|
||||
58
frontend/components/EmplacementPicker.vue
Normal file
58
frontend/components/EmplacementPicker.vue
Normal 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>
|
||||
94
frontend/components/FileUploader.vue
Normal file
94
frontend/components/FileUploader.vue
Normal 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>
|
||||
10
frontend/components/I18nStub.vue
Normal file
10
frontend/components/I18nStub.vue
Normal 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>
|
||||
66
frontend/components/ObjetForm.vue
Normal file
66
frontend/components/ObjetForm.vue
Normal 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>
|
||||
94
frontend/components/TreeList.vue
Normal file
94
frontend/components/TreeList.vue
Normal 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>
|
||||
Reference in New Issue
Block a user