chore: harden config, strengthen frontend typing, fix UTF-8 mojibake
Security config (3.1): - config/session.ts: cookie.secure now follows app.inProduction instead of being hardcoded to false - config/shield.ts: enable CSP with baseline directives compatible with Vite dev server and Inertia (self + @viteDevUrl/@viteHmrUrl) Frontend typing (3.2): - Replace 'any' props with concrete types (Computer, Component, ComponentType, ComputerState) across computers/*.vue and components/*.vue, using the existing inertia/pages/types/UITypes.ts definitions - Fix UITypes.ts: export ComponentType/ComputerState, switch from interface to type (interfaces are never assignable to Inertia's ComponentProps index signature), drop the stale componentTypeId/componentStateId fields that didn't match the actual Lucid schema (real field is typeId, no component state exists) - Add an asPageProp() boundary cast in the two controllers: Lucid's serialize() returns a loosely typed ModelObject that isn't structurally assignable to the concrete page prop shapes (and the inertia/** tree is excluded from the backend tsconfig, so real types can't be imported directly across that boundary) UTF-8 mojibake fixes (3.3): - Repair broken accented characters (├®/├á/├┤ -> é/à/ô, Ôé¼ -> €) left over from a bad encoding during the GWT->Inertia migration, across ComponentEditor.vue, ComputerComponentsTable.vue, SimpleGallery.vue, TableComponent.vue, isAdminMode.ts and the custom directives Misc cleanup (3.5): - computer.ts: replace console.warn with the app logger (includes computerId) when .cost is accessed without preloading components
This commit is contained in:
1 parent
3579c9dfec
commit
94f89be605
19 files changed
+103
-48
No files matched your search
@@ -5,16 +5,34 @@ import { updateComponentValidator } from '#validators/Components/update'
|
|||||||
import Component from '#models/component'
|
import Component from '#models/component'
|
||||||
import ComponentType from '#models/component_type'
|
import ComponentType from '#models/component_type'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lucid's `serialize()` returns a loosely typed `ModelObject`
|
||||||
|
* (`{ [key: string]: any }`). It is not structurally assignable to the
|
||||||
|
* concrete prop shapes declared by the Vue pages (see
|
||||||
|
* `inertia/pages/types/UITypes.ts`), which live in a separate TypeScript
|
||||||
|
* project (the `inertia/**` tree is excluded from the backend tsconfig).
|
||||||
|
* Since the controller fully controls which columns/relations are
|
||||||
|
* preloaded and serialized, this cast documents that the runtime shape
|
||||||
|
* matches what the page expects at this boundary.
|
||||||
|
*/
|
||||||
|
function asPageProp<T>(value: unknown): T {
|
||||||
|
return value as T
|
||||||
|
}
|
||||||
|
|
||||||
export default class ComponentsController {
|
export default class ComponentsController {
|
||||||
async index({ inertia, auth }: HttpContext) {
|
async index({ inertia, auth }: HttpContext) {
|
||||||
const user = await auth.authenticate()
|
const user = await auth.authenticate()
|
||||||
const components = await Component.query().preload('type').where('userId', user.id)
|
const components = await Component.query().preload('type').where('userId', user.id)
|
||||||
return inertia.render('components/index', { components: components.map((c) => c.serialize()) })
|
return inertia.render('components/index', {
|
||||||
|
components: asPageProp(components.map((c) => c.serialize())),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async create({ inertia }: HttpContext) {
|
async create({ inertia }: HttpContext) {
|
||||||
const types = await ComponentType.all()
|
const types = await ComponentType.all()
|
||||||
return inertia.render('components/create', { types: types.map((t) => t.serialize()) })
|
return inertia.render('components/create', {
|
||||||
|
types: asPageProp(types.map((t) => t.serialize())),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async store({ request, response, auth }: HttpContext) {
|
async store({ request, response, auth }: HttpContext) {
|
||||||
@@ -39,7 +57,7 @@ export default class ComponentsController {
|
|||||||
.where('userId', user.id)
|
.where('userId', user.id)
|
||||||
.andWhere('id', componentId)
|
.andWhere('id', componentId)
|
||||||
.firstOrFail()
|
.firstOrFail()
|
||||||
return inertia.render('components/show', { component: component.serialize() })
|
return inertia.render('components/show', { component: asPageProp(component.serialize()) })
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit({ params, auth, inertia }: HttpContext) {
|
async edit({ params, auth, inertia }: HttpContext) {
|
||||||
@@ -52,8 +70,8 @@ export default class ComponentsController {
|
|||||||
.andWhere('id', componentId)
|
.andWhere('id', componentId)
|
||||||
.firstOrFail()
|
.firstOrFail()
|
||||||
return inertia.render('components/edit', {
|
return inertia.render('components/edit', {
|
||||||
component: component.serialize(),
|
component: asPageProp(component.serialize()),
|
||||||
types: types.map((t) => t.serialize()),
|
types: asPageProp(types.map((t) => t.serialize())),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ import { handlePictures } from '#services/picture_service'
|
|||||||
import type { Image } from '#types/UItypes'
|
import type { Image } from '#types/UItypes'
|
||||||
import { updateComputerValidator } from '#validators/Computers/update'
|
import { updateComputerValidator } from '#validators/Computers/update'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lucid's `serialize()` returns a loosely typed `ModelObject`
|
||||||
|
* (`{ [key: string]: any }`). It is not structurally assignable to the
|
||||||
|
* concrete prop shapes declared by the Vue pages (see
|
||||||
|
* `inertia/pages/types/UITypes.ts`), which live in a separate TypeScript
|
||||||
|
* project (the `inertia/**` tree is excluded from the backend tsconfig).
|
||||||
|
* Since the controller fully controls which columns/relations are
|
||||||
|
* preloaded and serialized, this cast documents that the runtime shape
|
||||||
|
* matches what the page expects at this boundary.
|
||||||
|
*/
|
||||||
|
function asPageProp<T>(value: unknown): T {
|
||||||
|
return value as T
|
||||||
|
}
|
||||||
|
|
||||||
export default class ComputersController {
|
export default class ComputersController {
|
||||||
async index({ auth, inertia }: HttpContext) {
|
async index({ auth, inertia }: HttpContext) {
|
||||||
const user = await auth.authenticate()
|
const user = await auth.authenticate()
|
||||||
@@ -20,7 +34,9 @@ export default class ComputersController {
|
|||||||
picturesQuery.where('order', 1)
|
picturesQuery.where('order', 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
return inertia.render('computers/index', { computers: computers.map((c) => c.serialize()) })
|
return inertia.render('computers/index', {
|
||||||
|
computers: asPageProp(computers.map((c) => c.serialize())),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async create({ auth, inertia }: HttpContext) {
|
async create({ auth, inertia }: HttpContext) {
|
||||||
@@ -30,7 +46,10 @@ export default class ComputersController {
|
|||||||
.where('userId', user.id)
|
.where('userId', user.id)
|
||||||
.whereNull('computerId')
|
.whereNull('computerId')
|
||||||
.preload('type')
|
.preload('type')
|
||||||
return inertia.render('computers/create', { components: components.map((c) => c.serialize()), states: states.map((s) => s.serialize()) })
|
return inertia.render('computers/create', {
|
||||||
|
components: asPageProp(components.map((c) => c.serialize())),
|
||||||
|
states: asPageProp(states.map((s) => s.serialize())),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async store({ auth, request, response }: HttpContext) {
|
async store({ auth, request, response }: HttpContext) {
|
||||||
@@ -61,7 +80,7 @@ export default class ComputersController {
|
|||||||
.where('user_id', user.id)
|
.where('user_id', user.id)
|
||||||
.andWhere('id', computerId)
|
.andWhere('id', computerId)
|
||||||
.firstOrFail()
|
.firstOrFail()
|
||||||
return inertia.render('computers/show', { computer: computer.serialize() })
|
return inertia.render('computers/show', { computer: asPageProp(computer.serialize()) })
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit({ params, inertia, auth }: HttpContext) {
|
async edit({ params, inertia, auth }: HttpContext) {
|
||||||
@@ -87,7 +106,11 @@ export default class ComputersController {
|
|||||||
'id',
|
'id',
|
||||||
computer.components.map((c) => c.id)
|
computer.components.map((c) => c.id)
|
||||||
)
|
)
|
||||||
return inertia.render('computers/edit', { computer: computer.serialize(), availableComponents: availableComponents.map((c) => c.serialize()), states: states.map((s) => s.serialize()) })
|
return inertia.render('computers/edit', {
|
||||||
|
computer: asPageProp(computer.serialize()),
|
||||||
|
availableComponents: asPageProp(availableComponents.map((c) => c.serialize())),
|
||||||
|
states: asPageProp(states.map((s) => s.serialize())),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async update({ auth, params, request, response }: HttpContext) {
|
async update({ auth, params, request, response }: HttpContext) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { BaseModel, belongsTo, column, computed, hasMany } from '@adonisjs/lucid/orm'
|
import { BaseModel, belongsTo, column, computed, hasMany } from '@adonisjs/lucid/orm'
|
||||||
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
|
import type { BelongsTo, HasMany } from '@adonisjs/lucid/types/relations'
|
||||||
|
import logger from '@adonisjs/core/services/logger'
|
||||||
import ComputerState from '#models/computer_state'
|
import ComputerState from '#models/computer_state'
|
||||||
import ComputerPicture from '#models/computer_picture'
|
import ComputerPicture from '#models/computer_picture'
|
||||||
import Component from '#models/component'
|
import Component from '#models/component'
|
||||||
@@ -52,7 +53,10 @@ export default class Computer extends BaseModel {
|
|||||||
@computed()
|
@computed()
|
||||||
get cost(): number {
|
get cost(): number {
|
||||||
if (!this.$preloaded.components) {
|
if (!this.$preloaded.components) {
|
||||||
console.warn('Components not preloaded, cost may be incorrect')
|
logger.warn(
|
||||||
|
{ computerId: this.id },
|
||||||
|
'Computer.cost accessed without preloading "components"; returning 0'
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return this.components.reduce((total, component) => total + Number(component.price), 0)
|
return this.components.reduce((total, component) => total + Number(component.price), 0)
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import env from '#start/env'
|
import env from '#start/env'
|
||||||
|
import app from '@adonisjs/core/services/app'
|
||||||
import { defineConfig, stores } from '@adonisjs/session'
|
import { defineConfig, stores } from '@adonisjs/session'
|
||||||
|
|
||||||
const sessionConfig = defineConfig({
|
const sessionConfig = defineConfig({
|
||||||
@@ -42,7 +43,7 @@ const sessionConfig = defineConfig({
|
|||||||
/**
|
/**
|
||||||
* Send cookies only over HTTPS in production.
|
* Send cookies only over HTTPS in production.
|
||||||
*/
|
*/
|
||||||
secure: false,
|
secure: app.inProduction,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cross-site policy for cookie sending.
|
* Cross-site policy for cookie sending.
|
||||||
|
|||||||
+9
-2
@@ -9,12 +9,19 @@ const shieldConfig = defineConfig({
|
|||||||
/**
|
/**
|
||||||
* Enable the Content-Security-Policy header.
|
* Enable the Content-Security-Policy header.
|
||||||
*/
|
*/
|
||||||
enabled: false,
|
enabled: true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-resource CSP directives.
|
* Per-resource CSP directives.
|
||||||
*/
|
*/
|
||||||
directives: {},
|
directives: {
|
||||||
|
defaultSrc: [`'self'`, '@viteDevUrl'],
|
||||||
|
connectSrc: [`'self'`, '@viteHmrUrl'],
|
||||||
|
scriptSrc: [`'self'`, '@viteDevUrl'],
|
||||||
|
styleSrc: [`'self'`, '@viteDevUrl', `'unsafe-inline'`],
|
||||||
|
imgSrc: [`'self'`, 'data:'],
|
||||||
|
fontSrc: [`'self'`, 'data:'],
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report violations without blocking resources.
|
* Report violations without blocking resources.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
// Cr├®er une r├®f├®rence pour isAdminActive
|
// Créer une référence pour isAdminActive
|
||||||
const isAdminActive = ref(JSON.parse(localStorage.getItem('isAdminActive') || 'false'))
|
const isAdminActive = ref(JSON.parse(localStorage.getItem('isAdminActive') || 'false'))
|
||||||
|
|
||||||
// Surveiller les changements de isAdminActive et mettre à jour localStorage
|
// Surveiller les changements de isAdminActive et mettre à jour localStorage
|
||||||
watch(isAdminActive, (newValue) => {
|
watch(isAdminActive, (newValue) => {
|
||||||
localStorage.setItem('isAdminActive', JSON.stringify(newValue))
|
localStorage.setItem('isAdminActive', JSON.stringify(newValue))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createApp, DirectiveBinding, h, ObjectDirective } from 'vue'
|
import { createApp, DirectiveBinding, h, ObjectDirective } from 'vue'
|
||||||
import DeleteConfirmationModal from '~/pages/widgets/DeleteConfirmationModal.vue'
|
import DeleteConfirmationModal from '~/pages/widgets/DeleteConfirmationModal.vue'
|
||||||
|
|
||||||
// Interface pour ├®tendre HTMLElement avec _handleClick
|
// Interface pour étendre HTMLElement avec _handleClick
|
||||||
interface HTMLElementWithHandleClick extends HTMLElement {
|
interface HTMLElementWithHandleClick extends HTMLElement {
|
||||||
_handleClick?: (event: Event) => void
|
_handleClick?: (event: Event) => void
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,8 @@ interface HTMLElementWithHandleClick extends HTMLElement {
|
|||||||
const deleteConfirmDirective: ObjectDirective<HTMLElementWithHandleClick> = {
|
const deleteConfirmDirective: ObjectDirective<HTMLElementWithHandleClick> = {
|
||||||
mounted(el: HTMLElementWithHandleClick, binding: DirectiveBinding) {
|
mounted(el: HTMLElementWithHandleClick, binding: DirectiveBinding) {
|
||||||
const handleClick = (event: Event) => {
|
const handleClick = (event: Event) => {
|
||||||
event.stopPropagation() // Emp├¬cher la propagation de l'├®v├®nement
|
event.stopPropagation() // Empêcher la propagation de l'événement
|
||||||
event.preventDefault() // Emp├¬cher l'action par d├®faut
|
event.preventDefault() // Empêcher l'action par défaut
|
||||||
|
|
||||||
const modalApp = createApp({
|
const modalApp = createApp({
|
||||||
data() {
|
data() {
|
||||||
@@ -20,7 +20,7 @@ const deleteConfirmDirective: ObjectDirective<HTMLElementWithHandleClick> = {
|
|||||||
confirm() {
|
confirm() {
|
||||||
this.show = false
|
this.show = false
|
||||||
document.body.removeChild(modalContainer)
|
document.body.removeChild(modalContainer)
|
||||||
binding.value() // Ex├®cuter la fonction de suppression
|
binding.value() // Exécuter la fonction de suppression
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
this.show = false
|
this.show = false
|
||||||
@@ -42,7 +42,7 @@ const deleteConfirmDirective: ObjectDirective<HTMLElementWithHandleClick> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
el.addEventListener('click', handleClick)
|
el.addEventListener('click', handleClick)
|
||||||
el._handleClick = handleClick // Stocker la r├®f├®rence pour la suppression ult├®rieure
|
el._handleClick = handleClick // Stocker la référence pour la suppression ultérieure
|
||||||
},
|
},
|
||||||
unmounted(el: HTMLElementWithHandleClick) {
|
unmounted(el: HTMLElementWithHandleClick) {
|
||||||
if (el._handleClick) {
|
if (el._handleClick) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const formatPrice = (value: string): string => {
|
|||||||
return Number.isNaN(numberValue) ? '' : numberValue.toFixed(2).replace('.', ',')
|
return Number.isNaN(numberValue) ? '' : numberValue.toFixed(2).replace('.', ',')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface pour ├®tendre HTMLInputElement avec _handleBlur
|
// Interface pour étendre HTMLInputElement avec _handleBlur
|
||||||
interface HTMLInputElementWithHandleBlur extends HTMLInputElement {
|
interface HTMLInputElementWithHandleBlur extends HTMLInputElement {
|
||||||
_handleBlur?: () => void
|
_handleBlur?: () => void
|
||||||
}
|
}
|
||||||
@@ -15,10 +15,10 @@ const priceDirective: ObjectDirective<HTMLInputElementWithHandleBlur> = {
|
|||||||
mounted(el: HTMLInputElementWithHandleBlur) {
|
mounted(el: HTMLInputElementWithHandleBlur) {
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
el.value = formatPrice(el.value)
|
el.value = formatPrice(el.value)
|
||||||
el.dispatchEvent(new Event('input')) // Pour mettre à jour le v-model
|
el.dispatchEvent(new Event('input')) // Pour mettre à jour le v-model
|
||||||
}
|
}
|
||||||
el.addEventListener('blur', handleBlur)
|
el.addEventListener('blur', handleBlur)
|
||||||
el._handleBlur = handleBlur // Stocker la r├®f├®rence pour la suppression ult├®rieure
|
el._handleBlur = handleBlur // Stocker la référence pour la suppression ultérieure
|
||||||
},
|
},
|
||||||
unmounted(el: HTMLInputElementWithHandleBlur) {
|
unmounted(el: HTMLInputElementWithHandleBlur) {
|
||||||
if (el._handleBlur) {
|
if (el._handleBlur) {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const validationErrorDirective: ObjectDirective<HTMLElement, string | null> = {
|
|||||||
const vnode = createVNode(ValidationError, { message: errorMessage })
|
const vnode = createVNode(ValidationError, { message: errorMessage })
|
||||||
render(vnode, container)
|
render(vnode, container)
|
||||||
|
|
||||||
// Force reflow pour d├®clencher la transition
|
// Force reflow pour déclencher la transition
|
||||||
void container.offsetWidth
|
void container.offsetWidth
|
||||||
container.classList.remove('w-0', 'opacity-0')
|
container.classList.remove('w-0', 'opacity-0')
|
||||||
container.classList.add('w-full', 'opacity-100')
|
container.classList.add('w-full', 'opacity-100')
|
||||||
@@ -77,7 +77,7 @@ const validationErrorDirective: ObjectDirective<HTMLElement, string | null> = {
|
|||||||
const vnode = createVNode(ValidationError, { message: errorMessage })
|
const vnode = createVNode(ValidationError, { message: errorMessage })
|
||||||
render(vnode, newContainer)
|
render(vnode, newContainer)
|
||||||
|
|
||||||
// Force reflow pour d├®clencher la transition
|
// Force reflow pour déclencher la transition
|
||||||
void newContainer.offsetWidth
|
void newContainer.offsetWidth
|
||||||
newContainer.classList.remove('w-0', 'opacity-0')
|
newContainer.classList.remove('w-0', 'opacity-0')
|
||||||
newContainer.classList.add('w-100', 'opacity-100')
|
newContainer.classList.add('w-100', 'opacity-100')
|
||||||
@@ -92,7 +92,7 @@ const validationErrorDirective: ObjectDirective<HTMLElement, string | null> = {
|
|||||||
container.classList.remove('transition-all', 'duration-2500', 'ease-in-out') // Supprimez les classes de transition pour la disparition
|
container.classList.remove('transition-all', 'duration-2500', 'ease-in-out') // Supprimez les classes de transition pour la disparition
|
||||||
container.classList.add('w-0', 'opacity-0')
|
container.classList.add('w-0', 'opacity-0')
|
||||||
|
|
||||||
// Supprimez le conteneur imm├®diatement sans d├®lai
|
// Supprimez le conteneur immédiatement sans délai
|
||||||
render(null, container)
|
render(null, container)
|
||||||
container.remove()
|
container.remove()
|
||||||
;(el as any)._validationErrorContainer = null
|
;(el as any)._validationErrorContainer = null
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { reactive } from 'vue'
|
|||||||
import { Link } from '@adonisjs/inertia/vue'
|
import { Link } from '@adonisjs/inertia/vue'
|
||||||
import { useForm } from '@inertiajs/vue3'
|
import { useForm } from '@inertiajs/vue3'
|
||||||
import CustomSelect from '~/pages/widgets/CustomSelect.vue'
|
import CustomSelect from '~/pages/widgets/CustomSelect.vue'
|
||||||
|
import type { ComponentType } from '~/pages/types/UITypes'
|
||||||
|
|
||||||
defineProps<{ types: any[]; errors?: Record<string, string> }>()
|
defineProps<{ types: ComponentType[]; errors?: Record<string, string> }>()
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
price: 0,
|
price: 0,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@adonisjs/inertia/vue'
|
import { Link } from '@adonisjs/inertia/vue'
|
||||||
|
import type { Component } from '~/pages/types/UITypes'
|
||||||
|
|
||||||
const props = defineProps<{ component: any }>()
|
const props = defineProps<{ component: Component }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { Link } from '@adonisjs/inertia/vue'
|
import { Link } from '@adonisjs/inertia/vue'
|
||||||
import { router, usePage } from '@inertiajs/vue3'
|
import { router, usePage } from '@inertiajs/vue3'
|
||||||
|
import type { Component, Computer, ComputerState } from '~/pages/types/UITypes'
|
||||||
|
|
||||||
const { props } = usePage<{
|
const { props } = usePage<{
|
||||||
computer: any
|
computer: Computer
|
||||||
availableComponents: any[]
|
availableComponents: Component[]
|
||||||
states: any[]
|
states: ComputerState[]
|
||||||
errors?: Record<string, string>
|
errors?: Record<string, string>
|
||||||
}>()
|
}>()
|
||||||
const { computer, states } = props
|
const { computer, states } = props
|
||||||
@@ -19,7 +20,7 @@ const form = reactive({
|
|||||||
cpuScore: computer.cpuScore,
|
cpuScore: computer.cpuScore,
|
||||||
globalScore: computer.globalScore,
|
globalScore: computer.globalScore,
|
||||||
stateId: computer.state.id,
|
stateId: computer.state.id,
|
||||||
componentsId: computer.components.map((c: any) => c.id),
|
componentsId: computer.components.map((c) => c.id),
|
||||||
})
|
})
|
||||||
|
|
||||||
function deleteComputer(id: number) {
|
function deleteComputer(id: number) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@adonisjs/inertia/vue'
|
import { Link } from '@adonisjs/inertia/vue'
|
||||||
|
import type { Computer } from '~/pages/types/UITypes'
|
||||||
|
|
||||||
defineProps<{ computers: any[] }>()
|
defineProps<{ computers: Computer[] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@adonisjs/inertia/vue'
|
import { Link } from '@adonisjs/inertia/vue'
|
||||||
|
import type { Computer } from '~/pages/types/UITypes'
|
||||||
|
|
||||||
const { computer } = defineProps<{ computer: any }>()
|
const { computer } = defineProps<{ computer: Computer }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
interface ComponentType {
|
export type ComponentType = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
unique: boolean
|
unique: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComputerState {
|
export type ComputerState = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,7 @@ export type Component = {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
price: string
|
price: string
|
||||||
componentTypeId: number
|
|
||||||
computerId: number | null
|
computerId: number | null
|
||||||
userId: number
|
|
||||||
componentStateId: number
|
|
||||||
purchaseDate: string
|
purchaseDate: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const componentTypeOrder = [
|
|||||||
'Other',
|
'Other',
|
||||||
]
|
]
|
||||||
|
|
||||||
// Fusionner les composants disponibles et initialement s├®lectionn├®s
|
// Fusionner les composants disponibles et initialement sélectionnés
|
||||||
const allComponents = computed(() => {
|
const allComponents = computed(() => {
|
||||||
const mergedComponents = [...props.availableComponents]
|
const mergedComponents = [...props.availableComponents]
|
||||||
if (props.initialSelectedComponents) {
|
if (props.initialSelectedComponents) {
|
||||||
@@ -134,7 +134,7 @@ function updateComponents(event: Event, type: string) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Utilisation de cases à cocher pour les composants non uniques avec grille responsive -->
|
<!-- Utilisation de cases à cocher pour les composants non uniques avec grille responsive -->
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
<div v-for="component in components" :key="component.id" class="flex items-center mt-1">
|
<div v-for="component in components" :key="component.id" class="flex items-center mt-1">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const componentTypeOrder = [
|
|||||||
'Other',
|
'Other',
|
||||||
]
|
]
|
||||||
|
|
||||||
// Trier les composants selon l'ordre sp├®cifi├®
|
// Trier les composants selon l'ordre spécifié
|
||||||
const sortedComponents = computed(() => {
|
const sortedComponents = computed(() => {
|
||||||
return components.slice().sort((a, b) => {
|
return components.slice().sort((a, b) => {
|
||||||
return componentTypeOrder.indexOf(a.type.name) - componentTypeOrder.indexOf(b.type.name)
|
return componentTypeOrder.indexOf(a.type.name) - componentTypeOrder.indexOf(b.type.name)
|
||||||
@@ -67,7 +67,7 @@ function getIconPath(typeName: string): string {
|
|||||||
'WiFi-Card': wiFiCard,
|
'WiFi-Card': wiFiCard,
|
||||||
'Case': pcCase,
|
'Case': pcCase,
|
||||||
}
|
}
|
||||||
return typeToIconMap[typeName] || defaultIcon // Utilisez une ic├┤ne par d├®faut si n├®cessaire
|
return typeToIconMap[typeName] || defaultIcon // Utilisez une icône par défaut si nécessaire
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ function getIconPath(typeName: string): string {
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td v-if="isAdminActive" class="px-4 py-2 border-b border-gray-400 text-right">
|
<td v-if="isAdminActive" class="px-4 py-2 border-b border-gray-400 text-right">
|
||||||
Ôé¼{{ component.price }}
|
€{{ component.price }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2 border-b border-gray-400 flex justify-center items-center">
|
<td class="px-4 py-2 border-b border-gray-400 flex justify-center items-center">
|
||||||
<Link
|
<Link
|
||||||
@@ -125,7 +125,7 @@ function getIconPath(typeName: string): string {
|
|||||||
<tr class="bg-gray-800 text-white">
|
<tr class="bg-gray-800 text-white">
|
||||||
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap" colspan="3">Total</td>
|
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap" colspan="3">Total</td>
|
||||||
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap text-right">
|
<td class="px-4 py-2 border-b border-gray-400 whitespace-nowrap text-right">
|
||||||
Ôé¼{{ cost.toFixed(2) }}
|
€{{ cost.toFixed(2) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ function handleDragEnter(index: number) {
|
|||||||
const sourceArray =
|
const sourceArray =
|
||||||
dragIndex.value < serverImages.value.length ? serverImages.value : newImages.value
|
dragIndex.value < serverImages.value.length ? serverImages.value : newImages.value
|
||||||
const draggedItem = sourceArray.splice(dragIndex.value, 1)[0]
|
const draggedItem = sourceArray.splice(dragIndex.value, 1)[0]
|
||||||
sourceArray.splice(index, 0, draggedItem as any) // Utilisation de 'as any' pour r├®soudre le probl├¿me de typage
|
sourceArray.splice(index, 0, draggedItem as any) // Utilisation de 'as any' pour résoudre le problème de typage
|
||||||
dragIndex.value = index
|
dragIndex.value = index
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function getIconPath(typeName: string): string {
|
|||||||
'WiFi-Card': wiFiCard,
|
'WiFi-Card': wiFiCard,
|
||||||
'Case': pcCase,
|
'Case': pcCase,
|
||||||
}
|
}
|
||||||
return typeToIconMap[typeName] || defaultIcon // Utilisez une ic├┤ne par d├®faut si n├®cessaire
|
return typeToIconMap[typeName] || defaultIcon // Utilisez une icône par défaut si nécessaire
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToDetail(id: number) {
|
function goToDetail(id: number) {
|
||||||
@@ -105,7 +105,7 @@ function deleteComponent(id: number) {
|
|||||||
{{ component.name }}
|
{{ component.name }}
|
||||||
</td>
|
</td>
|
||||||
<td v-if="isAdminActive" class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
|
<td v-if="isAdminActive" class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
|
||||||
Ôé¼{{ component.price }}
|
€{{ component.price }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
|
<td class="px-6 py-4 border-b border-gray-200 whitespace-nowrap">
|
||||||
{{ component.purchaseDate }}
|
{{ component.purchaseDate }}
|
||||||
|
|||||||
Reference in new issue
Block a user