add achats import and hardware analyse
This commit is contained in:
150
frontend/pages/achats/index.vue
Normal file
150
frontend/pages/achats/index.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<main class="container">
|
||||
<h1>{{ t('nav.achats') }}</h1>
|
||||
<p v-if="message">{{ message }}</p>
|
||||
|
||||
<section class="card" style="margin-bottom: 16px;">
|
||||
<h2>{{ t('achats.mode') }}</h2>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<label>
|
||||
{{ t('achats.boutique') }}
|
||||
<select v-model="boutique">
|
||||
<option value="amazon">Amazon</option>
|
||||
<option value="aliexpress">AliExpress</option>
|
||||
<option value="generic">{{ t('achats.generic') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{{ t('achats.format') }}
|
||||
<select v-model="mode">
|
||||
<option value="file">{{ t('achats.fileMode') }}</option>
|
||||
<option value="json">{{ t('achats.jsonMode') }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div v-if="mode === 'file'" style="display: grid; gap: 8px;">
|
||||
<input type="file" @change="onFileChange" />
|
||||
<button class="card" type="button" :disabled="uploading" @click="uploadFile">
|
||||
{{ uploading ? t('form.saving') : t('actions.upload') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else style="display: grid; gap: 8px;">
|
||||
<textarea
|
||||
v-model="jsonText"
|
||||
rows="8"
|
||||
:placeholder="t('achats.jsonPlaceholder')"
|
||||
/>
|
||||
<button class="card" type="button" :disabled="uploading" @click="uploadJson">
|
||||
{{ uploading ? t('form.saving') : t('actions.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="result" class="card">
|
||||
<h2>{{ t('achats.summary') }}</h2>
|
||||
<p>{{ t('achats.imported') }}: {{ result.importes }}</p>
|
||||
<p>{{ t('achats.created') }}: {{ result.crees }}</p>
|
||||
<p>{{ t('achats.duplicates') }}: {{ result.doublons }}</p>
|
||||
<div v-if="result.erreurs?.length" style="margin-top: 8px;">
|
||||
<strong>{{ t('achats.errors') }}</strong>
|
||||
<ul>
|
||||
<li v-for="(err, idx) in result.erreurs" :key="idx">
|
||||
<span v-if="err.ligne">#{{ err.ligne }} </span>{{ err.message }}
|
||||
<span v-if="err.nom">({{ err.nom }})</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type ImportErreur = {
|
||||
ligne?: number
|
||||
message: string
|
||||
nom?: string
|
||||
}
|
||||
|
||||
type ImportResultat = {
|
||||
importes: number
|
||||
crees: number
|
||||
doublons: number
|
||||
erreurs: ImportErreur[]
|
||||
}
|
||||
|
||||
const { apiBase, getErrorMessage } = useApi()
|
||||
const { t } = useI18n()
|
||||
|
||||
const boutique = ref("amazon")
|
||||
const mode = ref<"file" | "json">("file")
|
||||
const uploading = ref(false)
|
||||
const message = ref("")
|
||||
const result = ref<ImportResultat | null>(null)
|
||||
const file = ref<File | null>(null)
|
||||
const jsonText = ref("")
|
||||
|
||||
const onFileChange = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
file.value = input.files?.[0] ?? null
|
||||
}
|
||||
|
||||
const uploadFile = async () => {
|
||||
message.value = ""
|
||||
result.value = null
|
||||
if (!file.value) {
|
||||
message.value = t("messages.noFiles")
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("boutique", boutique.value)
|
||||
formData.append("fichier", file.value)
|
||||
result.value = await $fetch(`${apiBase}/imports/achats`, {
|
||||
method: "POST",
|
||||
body: formData
|
||||
})
|
||||
message.value = t("messages.uploadDone")
|
||||
} catch (err) {
|
||||
message.value = getErrorMessage(err, t("messages.uploadError"))
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const uploadJson = async () => {
|
||||
message.value = ""
|
||||
result.value = null
|
||||
if (!jsonText.value.trim()) {
|
||||
message.value = t("errors.invalidJson")
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText.value)
|
||||
let payload = parsed
|
||||
if (Array.isArray(parsed)) {
|
||||
payload = { boutique: boutique.value, achats: parsed }
|
||||
} else if (parsed && typeof parsed === "object") {
|
||||
if (!parsed.boutique) {
|
||||
payload = { ...parsed, boutique: boutique.value }
|
||||
}
|
||||
}
|
||||
result.value = await $fetch(`${apiBase}/imports/achats`, {
|
||||
method: "POST",
|
||||
body: payload
|
||||
})
|
||||
message.value = t("messages.uploadDone")
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
message.value = t("errors.invalidJson")
|
||||
} else {
|
||||
message.value = getErrorMessage(err, t("messages.uploadError"))
|
||||
}
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
92
frontend/pages/analyse.vue
Normal file
92
frontend/pages/analyse.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<main class="container">
|
||||
<h1>{{ t('nav.analyse') }}</h1>
|
||||
<p v-if="message">{{ message }}</p>
|
||||
|
||||
<section class="card" style="margin-bottom: 16px;">
|
||||
<h2>{{ t('analyse.title') }}</h2>
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<label>
|
||||
{{ t('analyse.type') }}
|
||||
<select v-model="type">
|
||||
<option value="auto">{{ t('analyse.auto') }}</option>
|
||||
<option value="lspci">lspci</option>
|
||||
<option value="lsusb">lsusb</option>
|
||||
</select>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="texte"
|
||||
rows="10"
|
||||
:placeholder="t('analyse.placeholder')"
|
||||
/>
|
||||
<button class="card" type="button" :disabled="loading" @click="runAnalyse">
|
||||
{{ loading ? t('form.saving') : t('analyse.run') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="result" class="card">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h2>{{ t('analyse.result') }}</h2>
|
||||
<button class="card" type="button" @click="copyJson">{{ t('actions.copy') }}</button>
|
||||
</div>
|
||||
<p>{{ t('analyse.count') }}: {{ result.count }}</p>
|
||||
<pre>{{ formatted }}</pre>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type AnalyseResponse = {
|
||||
type: string
|
||||
count: number
|
||||
devices: unknown[]
|
||||
}
|
||||
|
||||
const { apiBase, getErrorMessage } = useApi()
|
||||
const { t } = useI18n()
|
||||
|
||||
const type = ref("auto")
|
||||
const texte = ref("")
|
||||
const loading = ref(false)
|
||||
const message = ref("")
|
||||
const result = ref<AnalyseResponse | null>(null)
|
||||
|
||||
const formatted = computed(() => (result.value ? JSON.stringify(result.value, null, 2) : ""))
|
||||
|
||||
const runAnalyse = async () => {
|
||||
message.value = ""
|
||||
result.value = null
|
||||
if (!texte.value.trim()) {
|
||||
message.value = t("analyse.empty")
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
type: type.value === "auto" ? "" : type.value,
|
||||
texte: texte.value
|
||||
}
|
||||
result.value = await $fetch(`${apiBase}/analyse-hardware`, {
|
||||
method: "POST",
|
||||
body: payload
|
||||
})
|
||||
} catch (err) {
|
||||
message.value = getErrorMessage(err, t("messages.loadError"))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const copyJson = async () => {
|
||||
if (!formatted.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(formatted.value)
|
||||
message.value = t("messages.copied")
|
||||
} catch {
|
||||
message.value = t("errors.copy")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
<I18nStub />
|
||||
|
||||
<ThemeToggle />
|
||||
|
||||
<section class="card">
|
||||
<div style="display: grid; gap: 8px; margin-bottom: 12px;">
|
||||
<label for="timezone">{{ t('settings.timezone') }}</label>
|
||||
|
||||
Reference in New Issue
Block a user