Files
iti-technology/apps/private/src/app/pages/admin/contacts.ts
T
Lorenzo SanesiandClaude Opus 5.5 b83ce039fb
Deploy stack su Proxmox / deploy (push) Failing after 3s
Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA),
apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/;
infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e
workflow Gitea Actions per il deploy via SSH a ogni push su master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
2026-10-20 09:30:48 +02:00

89 lines
3.6 KiB
TypeScript

import type { ContactRequestDto, ContactRequestStatus, Paginated } from '@iti/shared'
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'
import { rxResource } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router'
import { ApiService } from '../../core/api.service'
import { formatDateTime } from '../../core/format'
import { CONTACT_STATUS } from '../../core/labels'
import { Badge } from '../../shared/badge'
import { type FilterOption, FilterPills, SearchBox } from '../../shared/list-filters'
import { Pager, pageFromQuery } from '../../shared/pager'
import { Empty, Loading, LoadError } from '../../shared/state'
import { ADMIN_PAGE_SIZE, adminTitle, pageParams } from './admin-common'
const STATUSES = Object.keys(CONTACT_STATUS) as ContactRequestStatus[]
const FILTERS: FilterOption[] = [{ value: null, label: 'Tutte' }, ...STATUSES.map((s) => ({ value: s, label: CONTACT_STATUS[s].label }))]
/** Messaggi dal form contatti del sito. */
@Component({
selector: 'app-admin-contacts',
imports: [RouterLink, Badge, FilterPills, SearchBox, Pager, Loading, Empty, LoadError],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>Contatti</h1>
<div class="toolbar">
<app-filter-pills param="stato" [options]="filters" [current]="status()" label="Filtra per stato" />
<app-search-box [value]="q()" label="Cerca per nome, email o oggetto" />
</div>
@if (res.isLoading()) {
<app-loading label="Caricamento dei messaggi…" />
} @else if (res.error()) {
<app-load-error [error]="res.error()" (retry)="res.reload()" />
} @else if (res.value(); as data) {
@if (data.items.length) {
<ul class="item-list">
@for (c of data.items; track c.id) {
<li>
<a class="item" [routerLink]="['/admin/contatti', c.id]">
<span class="item__main">
<span class="item__title">{{ c.subject || 'Senza oggetto' }}</span>
<span class="meta">{{ c.name }} · {{ c.email }} · {{ dateTime(c.createdAt) }}</span>
<span class="excerpt">{{ excerpt(c.message) }}</span>
</span>
<app-badge [status]="labels[c.status]" />
</a>
</li>
}
</ul>
<app-pager [page]="page()" [total]="data.total" [pageSize]="pageSize" />
} @else {
<app-empty message="Nessun messaggio trovato." />
}
}
`,
styles: `
.excerpt {
color: var(--color-ink-muted);
overflow-wrap: anywhere;
}
`,
})
export default class AdminContacts {
private readonly api = inject(ApiService)
readonly pagina = input<string>()
readonly stato = input<string>()
readonly q = input<string>()
protected readonly filters = FILTERS
protected readonly pageSize = ADMIN_PAGE_SIZE
protected readonly page = computed(() => pageFromQuery(this.pagina()))
protected readonly status = computed(() => ((STATUSES as string[]).includes(this.stato() ?? '') ? (this.stato() as ContactRequestStatus) : null))
protected readonly res = rxResource({
params: () => ({ page: this.page(), status: this.status(), q: this.q() }),
stream: ({ params }) =>
this.api.get<Paginated<ContactRequestDto>>('/admin/contact-requests', { query: { ...pageParams(params.page), status: params.status, q: params.q } }),
})
protected readonly labels = CONTACT_STATUS
protected readonly dateTime = formatDateTime
protected excerpt(text: string): string {
const flat = text.replace(/\s+/g, ' ').trim()
return flat.length > 140 ? `${flat.slice(0, 139)}…` : flat
}
constructor() {
adminTitle()('Contatti')
}
}