refactor : merge Inventory_frontend submodule into frontend/ directory
Merges the full git history of Inventory_frontend into the monorepo under frontend/. Removes the submodule in favor of a unified repo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
264
frontend/app/pages/comments.vue
Normal file
264
frontend/app/pages/comments.vue
Normal file
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<main class="container mx-auto px-6 py-10 space-y-8">
|
||||
<header>
|
||||
<h1 class="text-3xl font-semibold text-base-content">
|
||||
Commentaires
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/50">
|
||||
Liste de tous les commentaires et tickets ouverts sur les fiches.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="card border border-base-200 bg-base-100 shadow-sm">
|
||||
<div class="card-body space-y-4">
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:rows="comments"
|
||||
:loading="loadingList"
|
||||
:sort="table.sort.value"
|
||||
:pagination="paginationState"
|
||||
:column-filters="table.columnFilters.value"
|
||||
:show-per-page="true"
|
||||
empty-message="Aucun commentaire trouvé."
|
||||
no-results-message="Aucun commentaire trouvé."
|
||||
@sort="table.handleSort"
|
||||
@update:current-page="table.handlePageChange"
|
||||
@update:per-page="table.handlePerPageChange"
|
||||
@update:column-filters="table.handleColumnFiltersChange"
|
||||
>
|
||||
<template #toolbar>
|
||||
<div class="flex items-center gap-2">
|
||||
<label
|
||||
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
||||
for="comment-status"
|
||||
>
|
||||
Statut
|
||||
</label>
|
||||
<select
|
||||
id="comment-status"
|
||||
v-model="statusFilter"
|
||||
class="select select-bordered select-sm"
|
||||
@change="table.handleFilterChange"
|
||||
>
|
||||
<option value="open">Ouverts</option>
|
||||
<option value="resolved">Résolus</option>
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label
|
||||
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
||||
for="comment-entity-type"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
<select
|
||||
id="comment-entity-type"
|
||||
v-model="entityTypeFilter"
|
||||
class="select select-bordered select-sm"
|
||||
@change="table.handleFilterChange"
|
||||
>
|
||||
<option value="">Tous</option>
|
||||
<option value="machine">Machine</option>
|
||||
<option value="piece">Pièce</option>
|
||||
<option value="composant">Composant</option>
|
||||
<option value="product">Produit</option>
|
||||
<option value="piece_category">Catégorie pièce</option>
|
||||
<option value="component_category">Catégorie composant</option>
|
||||
<option value="product_category">Catégorie produit</option>
|
||||
<option value="machine_skeleton">Squelette machine</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #cell-content="{ row }">
|
||||
<div class="tooltip tooltip-top max-w-xs" :data-tip="row.content">
|
||||
<span class="line-clamp-2 text-sm text-left">{{ row.content }}</span>
|
||||
</div>
|
||||
<CommentDocumentList :documents="getDocuments(row)" />
|
||||
</template>
|
||||
|
||||
<template #cell-entityType="{ row }">
|
||||
<span class="badge badge-outline badge-sm">
|
||||
{{ entityTypeLabel(row.entityType) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-entity="{ row }">
|
||||
<NuxtLink
|
||||
v-if="getEntityRoute(row)"
|
||||
:to="getEntityRoute(row)!"
|
||||
class="link link-primary text-sm font-medium"
|
||||
>
|
||||
{{ row.entityName || row.entityId }}
|
||||
</NuxtLink>
|
||||
<span v-else class="text-sm">
|
||||
{{ row.entityName || row.entityId }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-createdAt="{ row }">
|
||||
<span class="whitespace-nowrap">{{ formatCommentDate(row.createdAt) }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-status="{ row }">
|
||||
<span
|
||||
class="badge badge-sm"
|
||||
:class="row.status === 'open' ? 'badge-warning' : 'badge-success'"
|
||||
>
|
||||
{{ row.status === 'open' ? 'Ouvert' : 'Résolu' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="canEdit" #cell-actions="{ row }">
|
||||
<button
|
||||
v-if="row.status === 'open'"
|
||||
type="button"
|
||||
class="btn btn-success btn-xs gap-1"
|
||||
:disabled="loading"
|
||||
@click="handleResolve(row.id)"
|
||||
>
|
||||
<IconLucideCheck class="w-3 h-3" />
|
||||
Résoudre
|
||||
</button>
|
||||
<span v-else class="text-xs text-base-content/50">
|
||||
{{ row.resolvedByName }}
|
||||
</span>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, type Ref } from 'vue'
|
||||
import DataTable from '~/components/common/DataTable.vue'
|
||||
import { useComments, type Comment, type CommentDocument } from '~/composables/useComments'
|
||||
import CommentDocumentList from '~/components/CommentDocumentList.vue'
|
||||
import { usePermissions } from '~/composables/usePermissions'
|
||||
import { useDataTable } from '~/composables/useDataTable'
|
||||
import IconLucideCheck from '~icons/lucide/check'
|
||||
|
||||
const { canEdit } = usePermissions()
|
||||
const {
|
||||
loading,
|
||||
fetchAllComments,
|
||||
resolveComment,
|
||||
} = useComments()
|
||||
|
||||
const comments = ref<Comment[]>([])
|
||||
const total = ref(0)
|
||||
const loadingList = ref(true)
|
||||
|
||||
const getDocuments = (comment: Comment): CommentDocument[] =>
|
||||
comment.documents?.filter((d): d is CommentDocument => typeof d === 'object' && d !== null && 'id' in d) ?? []
|
||||
|
||||
const table = useDataTable(
|
||||
{ fetchData: loadComments },
|
||||
{
|
||||
defaultSort: 'createdAt',
|
||||
defaultDirection: 'desc',
|
||||
defaultPerPage: 20,
|
||||
persistToUrl: true,
|
||||
extraParams: {
|
||||
status: { default: 'open' },
|
||||
entityType: { default: '' },
|
||||
},
|
||||
columnFilterKeys: ['entity'],
|
||||
},
|
||||
)
|
||||
|
||||
const statusFilter = table.filters.status as Ref<string>
|
||||
const entityTypeFilter = table.filters.entityType as Ref<string>
|
||||
|
||||
const commentsOnPage = computed(() => comments.value.length)
|
||||
const paginationState = table.pagination(total, commentsOnPage)
|
||||
|
||||
const columns = computed(() => {
|
||||
const cols = [
|
||||
{ key: 'content', label: 'Contenu', class: 'max-w-xs' },
|
||||
{ key: 'entityType', label: 'Type' },
|
||||
{ key: 'entity', label: 'Item', filterable: true, filterPlaceholder: 'Rechercher…' },
|
||||
{ key: 'authorName', label: 'Auteur', sortable: true, sortKey: 'authorName' },
|
||||
{ key: 'createdAt', label: 'Date', sortable: true, sortKey: 'createdAt' },
|
||||
{ key: 'status', label: 'Statut', sortable: true, sortKey: 'status' },
|
||||
]
|
||||
if (canEdit.value) {
|
||||
cols.push({ key: 'actions', label: 'Actions' })
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const ENTITY_TYPE_LABELS: Record<string, string> = {
|
||||
machine: 'Machine',
|
||||
piece: 'Pièce',
|
||||
composant: 'Composant',
|
||||
product: 'Produit',
|
||||
piece_category: 'Cat. pièce',
|
||||
component_category: 'Cat. composant',
|
||||
product_category: 'Cat. produit',
|
||||
machine_skeleton: 'Squelette',
|
||||
}
|
||||
|
||||
const entityTypeLabel = (type: string): string =>
|
||||
ENTITY_TYPE_LABELS[type] ?? type
|
||||
|
||||
const formatCommentDate = (dateStr: string): string => {
|
||||
const date = new Date(dateStr)
|
||||
if (Number.isNaN(date.getTime())) return '—'
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
async function loadComments() {
|
||||
loadingList.value = true
|
||||
const result = await fetchAllComments({
|
||||
status: statusFilter.value || undefined,
|
||||
entityType: entityTypeFilter.value || undefined,
|
||||
entityName: table.columnFilters.value.entity || undefined,
|
||||
page: table.currentPage.value,
|
||||
itemsPerPage: table.itemsPerPage.value,
|
||||
orderBy: table.sortField.value,
|
||||
orderDir: table.sortDirection.value,
|
||||
})
|
||||
if (result.success) {
|
||||
comments.value = result.data ?? []
|
||||
total.value = result.total ?? 0
|
||||
}
|
||||
loadingList.value = false
|
||||
}
|
||||
|
||||
const handleResolve = async (commentId: string) => {
|
||||
const result = await resolveComment(commentId)
|
||||
if (result.success) {
|
||||
await loadComments()
|
||||
}
|
||||
}
|
||||
|
||||
const ENTITY_ROUTE_MAP: Record<string, (id: string) => string> = {
|
||||
machine: (id: string) => `/machine/${id}`,
|
||||
piece: (id: string) => `/piece/${id}`,
|
||||
composant: (id: string) => `/component/${id}`,
|
||||
product: (id: string) => `/product/${id}`,
|
||||
piece_category: (id: string) => `/piece-category/${id}/edit`,
|
||||
component_category: (id: string) => `/component-category/${id}/edit`,
|
||||
product_category: (id: string) => `/product-category/${id}/edit`,
|
||||
machine_skeleton: (id: string) => `/type/${id}`,
|
||||
}
|
||||
|
||||
const getEntityRoute = (comment: Comment): string | null => {
|
||||
const builder = ENTITY_ROUTE_MAP[comment.entityType]
|
||||
return builder ? builder(comment.entityId) : null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadComments()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user