Add place field and expand pastors table
Collection:
- New `place` text field on Pastors
Frontend table:
- Show church name, place, state, and impacted people columns
- Column-visibility toggle ("Columnas" dropdown) to show/hide any column
- Replace "Editar" text button with an edit icon
- Create/edit modal now covers church, place, and impacted people too
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
db9eb8a304
commit
348bb3480a
|
|
@ -8,69 +8,75 @@ import { createPastor, updatePastor, uploadLetter } from './actions'
|
|||
export type PastorRow = {
|
||||
id: number
|
||||
name: string
|
||||
churchName: string | null
|
||||
email: string | null
|
||||
telephone: string[]
|
||||
place: string | null
|
||||
city: string | null
|
||||
state: string | null
|
||||
country: string | null
|
||||
countryName: string | null
|
||||
countryFlag: string
|
||||
impactedPeople: number | null
|
||||
inTelegram: boolean
|
||||
notes: string | null
|
||||
letterUrl: string | null
|
||||
letterName: string | null
|
||||
}
|
||||
|
||||
const dash = <span className="opacity-40">—</span>
|
||||
|
||||
type ColumnKey =
|
||||
| 'name'
|
||||
| 'churchName'
|
||||
| 'email'
|
||||
| 'telephone'
|
||||
| 'place'
|
||||
| 'city'
|
||||
| 'state'
|
||||
| 'country'
|
||||
| 'impactedPeople'
|
||||
| 'inTelegram'
|
||||
| 'letter'
|
||||
|
||||
export function PastorsTable({ rows }: { rows: PastorRow[] }) {
|
||||
const [editing, setEditing] = useState<PastorRow | null>(null)
|
||||
const [uploading, setUploading] = useState<PastorRow | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-end mb-4">
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={() => setCreating(true)}>
|
||||
+ Crear pastor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="card bg-base-100 shadow-sm">
|
||||
<div className="card-body items-center text-center">
|
||||
<h3 className="card-title">No se encontraron pastores</h3>
|
||||
<p className="text-base-content/60">Intenta con otro término de búsqueda.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-box border border-base-300 bg-base-100">
|
||||
<table className="table table-zebra table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Correo</th>
|
||||
<th>Teléfonos</th>
|
||||
<th>Ciudad</th>
|
||||
<th>País</th>
|
||||
<th className="text-center">Telegram</th>
|
||||
<th>Carta</th>
|
||||
<th className="text-right">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((p) => (
|
||||
<tr key={p.id}>
|
||||
<td className="font-medium whitespace-nowrap">{p.name}</td>
|
||||
<td>
|
||||
{p.email ? (
|
||||
const columns: {
|
||||
key: ColumnKey
|
||||
label: string
|
||||
align?: 'center' | 'right'
|
||||
cell: (p: PastorRow) => React.ReactNode
|
||||
}[] = [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Nombre',
|
||||
cell: (p) => <span className="font-medium whitespace-nowrap">{p.name}</span>,
|
||||
},
|
||||
{
|
||||
key: 'churchName',
|
||||
label: 'Iglesia',
|
||||
cell: (p) => <span className="whitespace-nowrap">{p.churchName || dash}</span>,
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'Correo',
|
||||
cell: (p) =>
|
||||
p.email ? (
|
||||
<a href={`mailto:${p.email}`} className="link link-hover">
|
||||
{p.email}
|
||||
</a>
|
||||
) : (
|
||||
<span className="opacity-40">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{p.telephone.length ? (
|
||||
dash
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'telephone',
|
||||
label: 'Teléfonos',
|
||||
cell: (p) =>
|
||||
p.telephone.length ? (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{p.telephone.map((t, i) => (
|
||||
<a key={i} href={`tel:${t}`} className="link link-hover whitespace-nowrap">
|
||||
|
|
@ -79,28 +85,59 @@ export function PastorsTable({ rows }: { rows: PastorRow[] }) {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="opacity-40">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap">{p.city || <span className="opacity-40">—</span>}</td>
|
||||
<td className="whitespace-nowrap">
|
||||
{p.countryName ? (
|
||||
<span>
|
||||
dash
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'place',
|
||||
label: 'Lugar',
|
||||
cell: (p) => <span className="whitespace-nowrap">{p.place || dash}</span>,
|
||||
},
|
||||
{
|
||||
key: 'city',
|
||||
label: 'Ciudad',
|
||||
cell: (p) => <span className="whitespace-nowrap">{p.city || dash}</span>,
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: 'Estado',
|
||||
cell: (p) => <span className="whitespace-nowrap">{p.state || dash}</span>,
|
||||
},
|
||||
{
|
||||
key: 'country',
|
||||
label: 'País',
|
||||
cell: (p) =>
|
||||
p.countryName ? (
|
||||
<span className="whitespace-nowrap">
|
||||
{p.countryFlag} {p.countryName}
|
||||
</span>
|
||||
) : (
|
||||
<span className="opacity-40">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{p.inTelegram ? (
|
||||
dash
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'impactedPeople',
|
||||
label: 'Personas impactadas',
|
||||
align: 'right',
|
||||
cell: (p) =>
|
||||
p.impactedPeople === null ? dash : p.impactedPeople.toLocaleString('es'),
|
||||
},
|
||||
{
|
||||
key: 'inTelegram',
|
||||
label: 'Telegram',
|
||||
align: 'center',
|
||||
cell: (p) =>
|
||||
p.inTelegram ? (
|
||||
<span className="badge badge-success badge-sm">Sí</span>
|
||||
) : (
|
||||
<span className="badge badge-ghost badge-sm">No</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{p.letterUrl ? (
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'letter',
|
||||
label: 'Carta',
|
||||
cell: (p) =>
|
||||
p.letterUrl ? (
|
||||
<a
|
||||
href={p.letterUrl}
|
||||
target="_blank"
|
||||
|
|
@ -118,15 +155,109 @@ export function PastorsTable({ rows }: { rows: PastorRow[] }) {
|
|||
>
|
||||
Subir
|
||||
</button>
|
||||
)}
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// Column visibility — all shown by default; toggled via the "Columnas" menu.
|
||||
const [visible, setVisible] = useState<Record<ColumnKey, boolean>>(
|
||||
() => Object.fromEntries(columns.map((c) => [c.key, true])) as Record<ColumnKey, boolean>,
|
||||
)
|
||||
const shown = columns.filter((c) => visible[c.key])
|
||||
const alignClass = (a?: 'center' | 'right') =>
|
||||
a === 'center' ? 'text-center' : a === 'right' ? 'text-right' : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-end gap-2 mb-4">
|
||||
<div className="dropdown dropdown-end">
|
||||
<button type="button" tabIndex={0} className="btn btn-sm btn-outline">
|
||||
Columnas
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-3.5 h-3.5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.06l3.71-3.83a.75.75 0 111.08 1.04l-4.25 4.39a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content menu bg-base-100 rounded-box z-10 w-56 p-2 shadow-lg border border-base-300 max-h-96 overflow-y-auto flex-nowrap"
|
||||
>
|
||||
{columns.map((c) => (
|
||||
<li key={c.key}>
|
||||
<label className="label cursor-pointer justify-start gap-3 py-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-sm"
|
||||
checked={visible[c.key]}
|
||||
onChange={(e) =>
|
||||
setVisible((prev) => ({ ...prev, [c.key]: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
<span className="label-text">{c.label}</span>
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={() => setCreating(true)}>
|
||||
+ Crear pastor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="card bg-base-100 shadow-sm">
|
||||
<div className="card-body items-center text-center">
|
||||
<h3 className="card-title">No se encontraron pastores</h3>
|
||||
<p className="text-base-content/60">Intenta con otro término de búsqueda.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto rounded-box border border-base-300 bg-base-100">
|
||||
<table className="table table-zebra table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
{shown.map((c) => (
|
||||
<th key={c.key} className={alignClass(c.align)}>
|
||||
{c.label}
|
||||
</th>
|
||||
))}
|
||||
<th className="text-right">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((p) => (
|
||||
<tr key={p.id}>
|
||||
{shown.map((c) => (
|
||||
<td key={c.key} className={alignClass(c.align)}>
|
||||
{c.cell(p)}
|
||||
</td>
|
||||
))}
|
||||
<td className="text-right">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-ghost"
|
||||
className="btn btn-xs btn-square btn-ghost"
|
||||
onClick={() => setEditing(p)}
|
||||
aria-label={`Editar ${p.name}`}
|
||||
title="Editar"
|
||||
>
|
||||
Editar
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-4 h-4"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -158,13 +289,18 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
|||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [name, setName] = useState(pastor?.name ?? '')
|
||||
const [churchName, setChurchName] = useState(pastor?.churchName ?? '')
|
||||
const [email, setEmail] = useState(pastor?.email ?? '')
|
||||
const [phones, setPhones] = useState<string[]>(
|
||||
pastor?.telephone.length ? pastor.telephone : [''],
|
||||
)
|
||||
const [place, setPlace] = useState(pastor?.place ?? '')
|
||||
const [city, setCity] = useState(pastor?.city ?? '')
|
||||
const [state, setState] = useState(pastor?.state ?? '')
|
||||
const [country, setCountry] = useState(pastor?.country ?? '')
|
||||
const [impactedPeople, setImpactedPeople] = useState(
|
||||
pastor?.impactedPeople != null ? String(pastor.impactedPeople) : '',
|
||||
)
|
||||
const [inTelegram, setInTelegram] = useState(pastor?.inTelegram ?? false)
|
||||
const [notes, setNotes] = useState(pastor?.notes ?? '')
|
||||
|
||||
|
|
@ -177,7 +313,19 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
|||
e.preventDefault()
|
||||
setError(null)
|
||||
startTransition(async () => {
|
||||
const input = { name, email, telephone: phones, city, state, country, inTelegram, notes }
|
||||
const input = {
|
||||
name,
|
||||
churchName,
|
||||
email,
|
||||
telephone: phones,
|
||||
place,
|
||||
city,
|
||||
state,
|
||||
country,
|
||||
impactedPeople: impactedPeople.trim() === '' ? null : Number(impactedPeople),
|
||||
inTelegram,
|
||||
notes,
|
||||
}
|
||||
const res = isCreate ? await createPastor(input) : await updatePastor(pastor.id, input)
|
||||
if (res?.ok) {
|
||||
router.refresh()
|
||||
|
|
@ -213,6 +361,17 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
|||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="f-church" className="text-sm font-medium sm:text-right sm:pr-1">
|
||||
Iglesia
|
||||
</label>
|
||||
<input
|
||||
id="f-church"
|
||||
className="input input-bordered w-full"
|
||||
value={churchName}
|
||||
placeholder="Nombre de la iglesia"
|
||||
onChange={(e) => setChurchName(e.target.value)}
|
||||
/>
|
||||
|
||||
<label htmlFor="f-email" className="text-sm font-medium sm:text-right sm:pr-1">
|
||||
Correo
|
||||
</label>
|
||||
|
|
@ -252,6 +411,16 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<label htmlFor="f-place" className="text-sm font-medium sm:text-right sm:pr-1">
|
||||
Lugar
|
||||
</label>
|
||||
<input
|
||||
id="f-place"
|
||||
className="input input-bordered w-full"
|
||||
value={place}
|
||||
onChange={(e) => setPlace(e.target.value)}
|
||||
/>
|
||||
|
||||
<label htmlFor="f-city" className="text-sm font-medium sm:text-right sm:pr-1">
|
||||
Ciudad
|
||||
</label>
|
||||
|
|
@ -287,6 +456,18 @@ function PastorModal({ pastor, onClose }: { pastor: PastorRow | null; onClose: (
|
|||
<span className="text-xs text-base-content/50">Código ISO de 2 letras</span>
|
||||
</div>
|
||||
|
||||
<label htmlFor="f-impacted" className="text-sm font-medium sm:text-right sm:pr-1">
|
||||
Personas impactadas
|
||||
</label>
|
||||
<input
|
||||
id="f-impacted"
|
||||
type="number"
|
||||
min={0}
|
||||
className="input input-bordered w-40"
|
||||
value={impactedPeople}
|
||||
onChange={(e) => setImpactedPeople(e.target.value)}
|
||||
/>
|
||||
|
||||
<span className="text-sm font-medium sm:text-right sm:pr-1">En Telegram</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ import config from '@/payload.config'
|
|||
|
||||
export type PastorUpdateInput = {
|
||||
name: string
|
||||
churchName?: string | null
|
||||
email?: string | null
|
||||
telephone?: string[]
|
||||
place?: string | null
|
||||
city?: string | null
|
||||
state?: string | null
|
||||
country?: string | null
|
||||
impactedPeople?: number | null
|
||||
inTelegram?: boolean
|
||||
notes?: string | null
|
||||
}
|
||||
|
|
@ -30,9 +33,15 @@ export async function createPastor(data: PastorUpdateInput) {
|
|||
name: data.name.trim(),
|
||||
email: data.email || undefined,
|
||||
telephone: (data.telephone ?? []).map((t) => t.trim()).filter(Boolean),
|
||||
churchName: data.churchName || undefined,
|
||||
place: data.place || undefined,
|
||||
city: data.city || undefined,
|
||||
state: data.state || undefined,
|
||||
country: data.country ? data.country.trim().toUpperCase() : undefined,
|
||||
impactedPeople:
|
||||
data.impactedPeople === null || data.impactedPeople === undefined
|
||||
? undefined
|
||||
: Number(data.impactedPeople),
|
||||
inTelegram: Boolean(data.inTelegram),
|
||||
notes: data.notes || undefined,
|
||||
},
|
||||
|
|
@ -53,9 +62,15 @@ export async function updatePastor(id: number, data: PastorUpdateInput) {
|
|||
name: data.name,
|
||||
email: data.email || undefined,
|
||||
telephone: (data.telephone ?? []).map((t) => t.trim()).filter(Boolean),
|
||||
churchName: data.churchName || undefined,
|
||||
place: data.place || undefined,
|
||||
city: data.city || undefined,
|
||||
state: data.state || undefined,
|
||||
country: data.country ? data.country.trim().toUpperCase() : undefined,
|
||||
impactedPeople:
|
||||
data.impactedPeople === null || data.impactedPeople === undefined
|
||||
? undefined
|
||||
: Number(data.impactedPeople),
|
||||
inTelegram: Boolean(data.inTelegram),
|
||||
notes: data.notes || undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -66,13 +66,16 @@ export default async function HomePage({
|
|||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
churchName: p.churchName ?? null,
|
||||
email: p.email ?? null,
|
||||
telephone: Array.isArray(p.telephone) ? p.telephone.filter(Boolean) : [],
|
||||
place: p.place ?? null,
|
||||
city: p.city ?? null,
|
||||
state: p.state ?? null,
|
||||
country: p.country ?? null,
|
||||
countryName: localizedCountry(p.country),
|
||||
countryFlag: flag(p.country),
|
||||
impactedPeople: p.impactedPeople ?? null,
|
||||
inTelegram: Boolean(p.inTelegram),
|
||||
notes: p.notes ?? null,
|
||||
letterUrl: letter?.url ?? null,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ export const Pastors: CollectionConfig = {
|
|||
label: 'Church Name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'place',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
type: 'text',
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ export interface Pastor {
|
|||
id: number;
|
||||
name: string;
|
||||
churchName?: string | null;
|
||||
place?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
/**
|
||||
|
|
@ -428,6 +429,7 @@ export interface TagsSelect<T extends boolean = true> {
|
|||
export interface PastorsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
churchName?: T;
|
||||
place?: T;
|
||||
city?: T;
|
||||
state?: T;
|
||||
country?: T;
|
||||
|
|
|
|||
Loading…
Reference in New Issue