feat(web): create companies independently from physical inventory hierarchy
This commit is contained in:
@@ -18,7 +18,7 @@ import type {
|
||||
InventoryStructureParent,
|
||||
} from '../lib/inventoryStructureApi';
|
||||
|
||||
const KINDS: Array<{ kind: InventoryStructureKind; label: string; help: string; step: number }> = [
|
||||
const STRUCTURE_KINDS: Array<{ kind: InventoryStructureKind; label: string; help: string; step: number }> = [
|
||||
{ kind: 'AREA', label: 'Área', help: 'Nivel territorial raíz.', step: 1 },
|
||||
{ kind: 'YACIMIENTO', label: 'Yacimiento', help: 'Debe pertenecer a un Área.', step: 2 },
|
||||
{ kind: 'INSTALACION', label: 'Instalación', help: 'Debe pertenecer a un Yacimiento.', step: 3 },
|
||||
@@ -32,7 +32,8 @@ const childKindByParentType: Record<string, InventoryStructureKind | undefined>
|
||||
};
|
||||
|
||||
function kindLabel(kind: InventoryStructureKind): string {
|
||||
return KINDS.find((item) => item.kind === kind)?.label ?? kind;
|
||||
if (kind === 'EMPRESA') return 'Empresa';
|
||||
return STRUCTURE_KINDS.find((item) => item.kind === kind)?.label ?? kind;
|
||||
}
|
||||
|
||||
export function InventoryCreatePage() {
|
||||
@@ -72,7 +73,7 @@ export function InventoryCreatePage() {
|
||||
}, [contextParentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (kind === 'AREA') {
|
||||
if (kind === 'AREA' || kind === 'EMPRESA') {
|
||||
setParents([]);
|
||||
setParentId('');
|
||||
return;
|
||||
@@ -130,7 +131,7 @@ export function InventoryCreatePage() {
|
||||
|
||||
const save = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
const requiresParent = kind !== 'AREA';
|
||||
const requiresParent = kind !== 'AREA' && kind !== 'EMPRESA';
|
||||
const requiresFamily = kind === 'INSTALACION' || kind === 'SUBINSTALACION';
|
||||
if (requiresParent && !parentId) {
|
||||
setError(`Seleccioná el ${kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación'} padre.`);
|
||||
@@ -160,10 +161,13 @@ export function InventoryCreatePage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <LoadingBlock label="Preparando alta de Inventario…" />;
|
||||
if (loading) return <LoadingBlock label="Preparando alta…" />;
|
||||
|
||||
const isIndependentCompany = kind === 'EMPRESA';
|
||||
const parentLabel = kind === 'YACIMIENTO' ? 'Área' : kind === 'INSTALACION' ? 'Yacimiento' : 'Instalación';
|
||||
const currentStep = KINDS.find((item) => item.kind === kind)?.step ?? 1;
|
||||
const currentStep = STRUCTURE_KINDS.find((item) => item.kind === kind)?.step ?? 1;
|
||||
const identificationStep = isIndependentCompany ? '2' : kind === 'AREA' ? '2' : kind === 'YACIMIENTO' ? '3' : '4';
|
||||
const requiresParent = kind !== 'AREA' && kind !== 'EMPRESA';
|
||||
|
||||
return <section className="narrow-section asset-detail-page">
|
||||
<nav className="breadcrumb asset-detail-breadcrumb" aria-label="Ruta del inventario">
|
||||
@@ -172,9 +176,9 @@ export function InventoryCreatePage() {
|
||||
|
||||
<div className="page-heading asset-editor-heading">
|
||||
<div>
|
||||
<span className="eyebrow">INVENTARIO</span>
|
||||
<h1>Agregar a la estructura</h1>
|
||||
<p>La estructura oficial es Área → Yacimiento → Instalación → Subinstalación. Elegí el nivel y el sistema te guía con los vínculos válidos.</p>
|
||||
<span className="eyebrow">CONFIGURACIÓN E INVENTARIO</span>
|
||||
<h1>Agregar registro</h1>
|
||||
<p>Empresa es un maestro independiente. La estructura física es Área → Yacimiento → Instalación → Subinstalación.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,9 +186,18 @@ export function InventoryCreatePage() {
|
||||
|
||||
<div className="panel" style={{ marginBottom: 18 }}>
|
||||
<div className="form-section">
|
||||
<div><h2>1. ¿Qué querés crear?</h2><p className="section-copy">Sólo se pueden crear los cuatro niveles estructurales definidos para DH.</p></div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 10 }}>
|
||||
{KINDS.map((item) => <button
|
||||
<div><h2>1. ¿Qué querés crear?</h2><p className="section-copy">Creá una Empresa independiente o agregá un nivel a la estructura física.</p></div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', gap: 10 }}>
|
||||
<button
|
||||
type="button"
|
||||
className={`button ${kind === 'EMPRESA' ? 'primary' : 'secondary'}`}
|
||||
onClick={() => chooseKind('EMPRESA')}
|
||||
style={{ minHeight: 76, display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center', gap: 3 }}
|
||||
>
|
||||
<strong>Empresa</strong>
|
||||
<small>Maestro independiente.</small>
|
||||
</button>
|
||||
{STRUCTURE_KINDS.map((item) => <button
|
||||
key={item.kind}
|
||||
type="button"
|
||||
className={`button ${kind === item.kind ? 'primary' : 'secondary'}`}
|
||||
@@ -195,15 +208,18 @@ export function InventoryCreatePage() {
|
||||
<small>{item.help}</small>
|
||||
</button>)}
|
||||
</div>
|
||||
<div className="temporal-notice" style={{ marginTop: 12 }}>
|
||||
{isIndependentCompany ? <div className="temporal-notice" style={{ marginTop: 12 }}>
|
||||
<Icon name="users" />
|
||||
<p><strong>Empresa:</strong> no forma parte de la jerarquía física. Su vínculo con un Área se administra como relación temporal de operación/concesión.</p>
|
||||
</div> : <div className="temporal-notice" style={{ marginTop: 12 }}>
|
||||
<Icon name="layers" />
|
||||
<p><strong>Ruta:</strong> {KINDS.slice(0, currentStep).map((item) => item.label).join(' → ')}</p>
|
||||
</div>
|
||||
<p><strong>Ruta física:</strong> {STRUCTURE_KINDS.slice(0, currentStep).map((item) => item.label).join(' → ')}</p>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="panel form-panel" onSubmit={save}>
|
||||
{kind !== 'AREA' && <div className="form-section">
|
||||
{requiresParent && <div className="form-section">
|
||||
<div><h2>2. Ubicación en la estructura</h2><p className="section-copy">Primero elegí el {parentLabel} al que pertenece este registro.</p></div>
|
||||
<label className="field">
|
||||
<span>Buscar {parentLabel.toLowerCase()}</span>
|
||||
@@ -220,24 +236,24 @@ export function InventoryCreatePage() {
|
||||
</div>}
|
||||
|
||||
{(kind === 'INSTALACION' || kind === 'SUBINSTALACION') && <div className="form-section">
|
||||
<div><h2>3. Familia técnica</h2><p className="section-copy">La familia no crea otro nivel. Sirve para aplicar exactamente los Hallazgos del Excel que corresponden.</p></div>
|
||||
{kind === 'SUBINSTALACION' && !selectedParent?.inventoryFamily ? <Alert>La Instalación seleccionada todavía no tiene una familia técnica F3.1. Revisala antes de crear una Subinstalación.</Alert> : <label className="field">
|
||||
<span>Familia de {kindLabel(kind).toLowerCase()} <em>obligatorio</em></span>
|
||||
<div><h2>3. Clasificación técnica</h2><p className="section-copy">La clasificación no crea otro nivel: define el tipo de Instalación/Subinstalación y los Hallazgos aplicables.</p></div>
|
||||
{kind === 'SUBINSTALACION' && !selectedParent?.inventoryFamily ? <Alert>La Instalación seleccionada todavía no tiene una clasificación técnica. Revisala antes de crear una Subinstalación.</Alert> : <label className="field">
|
||||
<span>Tipo de {kindLabel(kind).toLowerCase()} <em>obligatorio</em></span>
|
||||
<select value={familyId} onChange={(event) => setFamilyId(event.target.value)} required>
|
||||
<option value="">Seleccionar familia…</option>
|
||||
<option value="">Seleccionar tipo…</option>
|
||||
{families.map((family) => <option key={family.id} value={family.id}>{family.name}</option>)}
|
||||
</select>
|
||||
{kind === 'SUBINSTALACION' && selectedParent?.inventoryFamily && <small>Se muestran sólo las subinstalaciones válidas para {selectedParent.inventoryFamily.name}.</small>}
|
||||
{kind === 'SUBINSTALACION' && selectedParent?.inventoryFamily && <small>Se muestran sólo las Subinstalaciones válidas para {selectedParent.inventoryFamily.name}.</small>}
|
||||
</label>}
|
||||
|
||||
{selectedFamily && <div className="context-create-banner" style={{ alignItems: 'flex-start' }}>
|
||||
<Icon name="alert" />
|
||||
<div style={{ flex: 1 }}>
|
||||
<strong>Hallazgos asociados automáticamente</strong>
|
||||
<span>{familyFindings ? `${familyFindings.count} controles del Excel para ${selectedFamily.name}` : 'Cargando catálogo asociado…'}</span>
|
||||
<span>{familyFindings ? `${familyFindings.count} Hallazgos aplicables para ${selectedFamily.name}` : 'Cargando catálogo asociado…'}</span>
|
||||
{familyFindings && familyFindings.items.length > 0 && <ul style={{ margin: '8px 0 0', paddingLeft: 18 }}>
|
||||
{familyFindings.items.slice(0, 7).map((item) => <li key={item.id}>{item.title}</li>)}
|
||||
{familyFindings.items.length > 7 && <li><strong>+ {familyFindings.items.length - 7} hallazgos más</strong></li>}
|
||||
{familyFindings.items.length > 7 && <li><strong>+ {familyFindings.items.length - 7} Hallazgos más</strong></li>}
|
||||
</ul>}
|
||||
</div>
|
||||
</div>}
|
||||
@@ -249,18 +265,18 @@ export function InventoryCreatePage() {
|
||||
</div>}
|
||||
|
||||
<div className="form-section">
|
||||
<div><h2>{kind === 'AREA' ? '2' : kind === 'YACIMIENTO' ? '3' : '4'}. Identificación</h2><p className="section-copy">Usá el nombre real de campo. El código DH puede generarse automáticamente.</p></div>
|
||||
<div><h2>{identificationStep}. Identificación</h2><p className="section-copy">{isIndependentCompany ? 'Registrá la denominación de la Empresa.' : 'Usá el nombre real de campo.'} El código DH puede generarse automáticamente.</p></div>
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Nombre <em>obligatorio</em></span><input value={name} onChange={(event) => setName(event.target.value)} required maxLength={200} placeholder={`Nombre de ${kindLabel(kind).toLowerCase()}`} /></label>
|
||||
<label className="field"><span>Código DH <em>opcional</em></span><input value={code} onChange={(event) => setCode(event.target.value.toUpperCase())} maxLength={120} pattern="[A-Z0-9][A-Z0-9._/-]*" placeholder="Dejar vacío para generar automáticamente" /><small>Si no lo informás, DH genera un código único.</small></label>
|
||||
<label className="field"><span>Nombre habitual / sobrenombre <em>opcional</em></span><input value={commonName} onChange={(event) => setCommonName(event.target.value)} maxLength={200} placeholder="Nombre usado por los inspectores en campo" /></label>
|
||||
<label className="field"><span>Nombre habitual / sobrenombre <em>opcional</em></span><input value={commonName} onChange={(event) => setCommonName(event.target.value)} maxLength={200} placeholder={isIndependentCompany ? 'Nombre habitual o abreviado' : 'Nombre usado por los inspectores en campo'} /></label>
|
||||
</div>
|
||||
<label className="field"><span>Descripción <em>opcional</em></span><textarea value={description} onChange={(event) => setDescription(event.target.value)} rows={2} maxLength={4000} /></label>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<Link className="button secondary" to="/inventarios">Cancelar</Link>
|
||||
<button className="button primary" disabled={saving || !name.trim() || (kind !== 'AREA' && !parentId) || ((kind === 'INSTALACION' || kind === 'SUBINSTALACION') && !familyId)}>
|
||||
<button className="button primary" disabled={saving || !name.trim() || (requiresParent && !parentId) || ((kind === 'INSTALACION' || kind === 'SUBINSTALACION') && !familyId)}>
|
||||
<Icon name="check" />{saving ? 'Creando…' : `Crear ${kindLabel(kind)}`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user