Files
Investidor-V2/resources/js/components/offer-private-resources.tsx
T

333 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}