333 lines
13 KiB
TypeScript
333 lines
13 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { profileJson } from '@/lib/profile-json';
|
||
import { createOperationKey } from '@/lib/operation-key';
|
||
type Resource = {
|
||
id: string;
|
||
version: number;
|
||
deleted: boolean;
|
||
caption: string | null;
|
||
digitally_edited: boolean | null;
|
||
edit_description: string | null;
|
||
scan: string;
|
||
};
|
||
export default function OfferPrivateResources({
|
||
id,
|
||
etag,
|
||
editable,
|
||
property,
|
||
disabled,
|
||
onChanged,
|
||
}: {
|
||
id: number;
|
||
etag: string;
|
||
editable: boolean;
|
||
property: boolean;
|
||
disabled: boolean;
|
||
onChanged: (etag: string) => void;
|
||
}) {
|
||
const [resources, setResources] = useState<{
|
||
images: Resource[];
|
||
documents: Resource[];
|
||
}>({ images: [], documents: [] });
|
||
const [internal, setInternal] = useState(false);
|
||
const [file, setFile] = useState<File | null>(null);
|
||
const [kind, setKind] = useState('images');
|
||
const [caption, setCaption] = useState('');
|
||
const [edited, setEdited] = useState(false);
|
||
const [editDescription, setEditDescription] = useState('');
|
||
const [message, setMessage] = useState('');
|
||
const [busy, setBusy] = useState(false);
|
||
const [editing, setEditing] = useState<Resource | null>(null);
|
||
const base = `/minhas-oportunidades/ofertas/${id}/recursos`;
|
||
async function refresh() {
|
||
const result = await profileJson(base, {}, 'GET');
|
||
setResources(result.resources);
|
||
setInternal(result.internalProofAccess);
|
||
onChanged(result.etag);
|
||
}
|
||
useEffect(() => {
|
||
let active = true;
|
||
profileJson(base, {}, 'GET')
|
||
.then((result) => {
|
||
if (active) {
|
||
setResources(result.resources);
|
||
setInternal(result.internalProofAccess);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
if (active) setMessage(error.message);
|
||
});
|
||
return () => {
|
||
active = false;
|
||
};
|
||
}, [base]);
|
||
async function upload() {
|
||
if ((!file && !editing) || busy || disabled) return;
|
||
setBusy(true);
|
||
setMessage('');
|
||
const form = new FormData();
|
||
if (file) form.append('file', file);
|
||
form.append('etag', etag);
|
||
if (editing) form.append('version', String(editing.version));
|
||
else form.append('creation_key', createOperationKey());
|
||
form.append(
|
||
'reason',
|
||
'Envio privado de suporte da oferta pelo promotor.',
|
||
);
|
||
form.append(
|
||
'payload',
|
||
JSON.stringify(
|
||
kind === 'images'
|
||
? {
|
||
visibility: 'internal',
|
||
caption,
|
||
alt_text: caption,
|
||
digitally_edited: edited,
|
||
edit_description: edited ? editDescription : null,
|
||
}
|
||
: {
|
||
visibility: 'internal',
|
||
classification: 'restricted',
|
||
category: 'real_estate_proof',
|
||
},
|
||
),
|
||
);
|
||
try {
|
||
await profileJson(
|
||
editing
|
||
? `${base}/images/${editing.id}/guardar`
|
||
: `${base}/${kind}`,
|
||
form,
|
||
);
|
||
await refresh();
|
||
setFile(null);
|
||
setEditing(null);
|
||
setMessage(
|
||
'Original guardado em privado. A análise de segurança pode ainda estar pendente.',
|
||
);
|
||
} catch (error) {
|
||
setMessage(
|
||
error instanceof Error
|
||
? error.message
|
||
: 'Não foi possível guardar.',
|
||
);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
async function lifecycle(resource: Resource, resourceKind: string) {
|
||
setBusy(true);
|
||
try {
|
||
await profileJson(`${base}/${resourceKind}/${resource.id}`, {
|
||
etag,
|
||
version: resource.version,
|
||
action: resource.deleted ? 'restore' : 'delete',
|
||
confirm: true,
|
||
reason: 'Alteração explícita dos suportes privados da oferta.',
|
||
});
|
||
await refresh();
|
||
} catch (error) {
|
||
setMessage(
|
||
error instanceof Error
|
||
? error.message
|
||
: 'Não foi possível alterar.',
|
||
);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
return (
|
||
<section className="space-y-4 rounded-lg border p-4">
|
||
<h2 className="font-semibold">Fotografias e suportes privados</h2>
|
||
<p>
|
||
Até seis fotografias. JPEG, PNG ou WebP, até 10 MB e pelo menos
|
||
400 × 400 píxeis. Os comprovativos são reservados à validação
|
||
interna; o envio não certifica titularidade.
|
||
</p>
|
||
{disabled && <p>Guarde os dados antes de alterar os ficheiros.</p>}
|
||
{editable && (
|
||
<fieldset
|
||
disabled={busy || disabled}
|
||
className="grid gap-3 sm:grid-cols-2"
|
||
>
|
||
<label>
|
||
Tipo
|
||
<select
|
||
className="w-full rounded border p-2"
|
||
value={kind}
|
||
disabled={editing !== null}
|
||
onChange={(e) => setKind(e.target.value)}
|
||
>
|
||
<option value="images">Fotografia</option>
|
||
{property && (
|
||
<option value="documents">
|
||
Comprovativo imobiliário
|
||
</option>
|
||
)}
|
||
</select>
|
||
</label>
|
||
<label>
|
||
Ficheiro
|
||
<input
|
||
type="file"
|
||
className="block w-full min-w-0"
|
||
accept={
|
||
kind === 'images'
|
||
? 'image/jpeg,image/png,image/webp'
|
||
: '.pdf,.docx,.xlsx,.png,.jpg,.jpeg'
|
||
}
|
||
onChange={(e) =>
|
||
setFile(e.target.files?.[0] ?? null)
|
||
}
|
||
/>
|
||
</label>
|
||
{kind === 'images' && (
|
||
<>
|
||
<label>
|
||
Legenda / divisão
|
||
<input
|
||
className="w-full rounded border p-2"
|
||
maxLength={250}
|
||
value={caption}
|
||
onChange={(e) => setCaption(e.target.value)}
|
||
/>
|
||
</label>
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
checked={edited}
|
||
onChange={(e) =>
|
||
setEdited(e.target.checked)
|
||
}
|
||
/>{' '}
|
||
Imagem editada digitalmente
|
||
</label>
|
||
{edited && (
|
||
<label>
|
||
Alterações efectuadas
|
||
<textarea
|
||
className="w-full rounded border p-2"
|
||
maxLength={1000}
|
||
value={editDescription}
|
||
onChange={(e) =>
|
||
setEditDescription(e.target.value)
|
||
}
|
||
/>
|
||
</label>
|
||
)}
|
||
</>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="rounded border p-2"
|
||
disabled={
|
||
(!file && !editing) ||
|
||
(!editing &&
|
||
kind === 'images' &&
|
||
resources.images.filter((item) => !item.deleted)
|
||
.length >= 6)
|
||
}
|
||
onClick={() => void upload()}
|
||
>
|
||
{editing
|
||
? 'Guardar alterações da fotografia'
|
||
: 'Guardar ficheiro privado'}
|
||
</button>
|
||
</fieldset>
|
||
)}
|
||
{editing && (
|
||
<button
|
||
type="button"
|
||
className="underline"
|
||
disabled={busy}
|
||
onClick={() => {
|
||
setEditing(null);
|
||
setFile(null);
|
||
}}
|
||
>
|
||
Cancelar alteração da fotografia
|
||
</button>
|
||
)}
|
||
{(['images', 'documents'] as const).map((resourceKind) => (
|
||
<ul key={resourceKind} className="space-y-2">
|
||
{resources[resourceKind].map((resource) => (
|
||
<li
|
||
key={resource.id}
|
||
className="flex flex-wrap gap-3 break-words"
|
||
>
|
||
<span>
|
||
{resource.caption ||
|
||
(resourceKind === 'images'
|
||
? 'Fotografia'
|
||
: 'Comprovativo')}{' '}
|
||
· versão {resource.version} ·{' '}
|
||
{resource.deleted
|
||
? 'Retirado'
|
||
: resource.scan === 'clean'
|
||
? 'Análise concluída'
|
||
: 'Análise de segurança pendente'}
|
||
</span>
|
||
{!resource.deleted &&
|
||
resource.scan === 'clean' &&
|
||
(resourceKind === 'images' || internal) && (
|
||
<a
|
||
className="underline"
|
||
href={`${base}/${resourceKind}/${resource.id}`}
|
||
>
|
||
Consultar original
|
||
</a>
|
||
)}
|
||
{editable &&
|
||
resourceKind === 'images' &&
|
||
!resource.deleted && (
|
||
<button
|
||
type="button"
|
||
className="underline"
|
||
disabled={busy || disabled}
|
||
onClick={() => {
|
||
setEditing(resource);
|
||
setKind('images');
|
||
setCaption(resource.caption ?? '');
|
||
setEdited(
|
||
resource.digitally_edited ??
|
||
false,
|
||
);
|
||
setEditDescription(
|
||
resource.edit_description ?? '',
|
||
);
|
||
setFile(null);
|
||
}}
|
||
>
|
||
Alterar fotografia
|
||
</button>
|
||
)}
|
||
{editable && (
|
||
<button
|
||
className="underline"
|
||
disabled={busy || disabled}
|
||
onClick={() =>
|
||
void lifecycle(resource, resourceKind)
|
||
}
|
||
>
|
||
{resource.deleted ? 'Restaurar' : 'Retirar'}
|
||
</button>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
))}
|
||
<button
|
||
type="button"
|
||
className="underline"
|
||
disabled={busy || disabled}
|
||
onClick={() =>
|
||
void refresh().catch((error) => setMessage(error.message))
|
||
}
|
||
>
|
||
Actualizar estado dos ficheiros
|
||
</button>
|
||
<p role="status">{message}</p>
|
||
</section>
|
||
);
|
||
}
|