Add impacted-people total and letter deletion
- Home header shows a running sum of impacted people across all matching records (respects the current search filter), next to the record count - Letters can be deleted from the table (trash button next to Download), which unlinks the pastor and removes the upload doc so a new file can be uploaded in its place Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
348bb3480a
commit
efeee14752
|
|
@ -3,7 +3,7 @@
|
|||
import React, { useRef, useState, useTransition } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { createPastor, updatePastor, uploadLetter } from './actions'
|
||||
import { createPastor, deleteLetter, updatePastor, uploadLetter } from './actions'
|
||||
|
||||
export type PastorRow = {
|
||||
id: number
|
||||
|
|
@ -136,26 +136,7 @@ export function PastorsTable({ rows }: { rows: PastorRow[] }) {
|
|||
{
|
||||
key: 'letter',
|
||||
label: 'Carta',
|
||||
cell: (p) =>
|
||||
p.letterUrl ? (
|
||||
<a
|
||||
href={p.letterUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
download={p.letterName ?? undefined}
|
||||
className="btn btn-xs btn-outline btn-success"
|
||||
>
|
||||
Descargar
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-outline"
|
||||
onClick={() => setUploading(p)}
|
||||
>
|
||||
Subir
|
||||
</button>
|
||||
),
|
||||
cell: (p) => <LetterActions pastor={p} onUpload={() => setUploading(p)} />,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -282,6 +263,67 @@ export function PastorsTable({ rows }: { rows: PastorRow[] }) {
|
|||
)
|
||||
}
|
||||
|
||||
function LetterActions({ pastor, onUpload }: { pastor: PastorRow; onUpload: () => void }) {
|
||||
const router = useRouter()
|
||||
const [pending, startTransition] = useTransition()
|
||||
|
||||
if (!pastor.letterUrl) {
|
||||
return (
|
||||
<button type="button" className="btn btn-xs btn-outline" onClick={onUpload}>
|
||||
Subir
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
if (!window.confirm('¿Eliminar la carta? Podrás subir una nueva después.')) return
|
||||
startTransition(async () => {
|
||||
await deleteLetter(pastor.id)
|
||||
router.refresh()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
href={pastor.letterUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
download={pastor.letterName ?? undefined}
|
||||
className="btn btn-xs btn-outline btn-success"
|
||||
>
|
||||
Descargar
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-square btn-ghost text-error"
|
||||
onClick={onDelete}
|
||||
disabled={pending}
|
||||
aria-label="Eliminar carta"
|
||||
title="Eliminar carta"
|
||||
>
|
||||
{pending ? (
|
||||
<span className="loading loading-spinner loading-xs" />
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-4 h-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: () => void }) {
|
||||
const router = useRouter()
|
||||
const isCreate = pastor === null
|
||||
|
|
|
|||
|
|
@ -80,6 +80,31 @@ export async function updatePastor(id: number, data: PastorUpdateInput) {
|
|||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina la carta asociada a un pastor: desvincula el campo `letter` y borra
|
||||
* el documento de subida, de modo que se pueda subir uno nuevo en su lugar.
|
||||
*/
|
||||
export async function deleteLetter(pastorId: number) {
|
||||
const payload = await getPayload({ config: await config })
|
||||
|
||||
const pastor = await payload.findByID({ collection: 'pastors', id: pastorId, depth: 0 })
|
||||
const letterId =
|
||||
pastor.letter && typeof pastor.letter === 'object' ? pastor.letter.id : pastor.letter
|
||||
|
||||
await payload.update({ collection: 'pastors', id: pastorId, data: { letter: null } })
|
||||
|
||||
if (letterId) {
|
||||
try {
|
||||
await payload.delete({ collection: 'pastor-letters', id: letterId })
|
||||
} catch {
|
||||
// The upload doc may already be gone; unlinking the pastor is enough.
|
||||
}
|
||||
}
|
||||
|
||||
revalidatePath('/')
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sube una carta (PDF o Word) y la asocia a un pastor. El archivo se guarda en
|
||||
* la colección de subidas `pastor-letters`, que valida los tipos de archivo
|
||||
|
|
|
|||
|
|
@ -61,6 +61,17 @@ export default async function HomePage({
|
|||
limit: PAGE_SIZE,
|
||||
})
|
||||
|
||||
// Running total of impacted people across ALL matching records (not just the
|
||||
// current page). Only the number field is selected to keep this light.
|
||||
const impactedAgg = await payload.find({
|
||||
collection: 'pastors',
|
||||
where,
|
||||
pagination: false,
|
||||
depth: 0,
|
||||
select: { impactedPeople: true },
|
||||
})
|
||||
const totalImpacted = impactedAgg.docs.reduce((sum, d) => sum + (d.impactedPeople ?? 0), 0)
|
||||
|
||||
const rows: PastorRow[] = result.docs.map((p) => {
|
||||
const letter = p.letter && typeof p.letter === 'object' ? p.letter : null
|
||||
return {
|
||||
|
|
@ -90,6 +101,11 @@ export default async function HomePage({
|
|||
<h1 className="text-3xl font-bold">Pastores</h1>
|
||||
<p className="text-base-content/60 text-sm mt-1">
|
||||
{result.totalDocs.toLocaleString('es')} registros
|
||||
<span className="mx-2 opacity-40">·</span>
|
||||
<span className="font-medium text-base-content/80">
|
||||
{totalImpacted.toLocaleString('es')}
|
||||
</span>{' '}
|
||||
personas impactadas
|
||||
</p>
|
||||
</div>
|
||||
<form method="get" className="join">
|
||||
|
|
|
|||
Loading…
Reference in New Issue