// Inventory — SPEC's core view. One row per home; the record here is the
// single source of truth the public website (v3) will eventually render
// from. Self-fetching (Dashboard pattern): remounts and refetches on nav.
//
// Form shape is tenant-configurable (migration 0003): built-in fields can be
// hidden, and managers add custom fields ("Garage Door: Left/Right") that
// live as defs in property_field_defs + values in properties.custom.

// Controlled vocabulary — mirrors server/routes/properties.js and the CHECK
// constraint in migration 0002. Label + color defined once, rendered
// everywhere (list badge, form select, filter chips).
const PROPERTY_STATUSES = [
    { id: 'planned',            label: 'Planned',            color: '#6b7280' },
    { id: 'under_construction', label: 'Under Construction', color: '#f59e0b' },
    { id: 'move_in_ready',      label: 'Move-In Ready',      color: '#22c55e' },
    { id: 'sold',               label: 'Sold',               color: '#3b82f6' },
];
const statusMeta = (id) => PROPERTY_STATUSES.find(s => s.id === id) || PROPERTY_STATUSES[0];

// Built-ins the tenant may hide — mirrors HIDEABLE_BUILTINS on the server
// (address/status/published are load-bearing and never hidden).
const HIDEABLE_LABELS = {
    city: 'City', state: 'State', zip: 'ZIP', price: 'Price',
    beds: 'Beds', baths: 'Baths', sqft: 'Sqft', description: 'Description',
};

const fmtPrice = (v) => v == null ? '—'
    : '$' + parseFloat(v).toLocaleString('en-US', { maximumFractionDigits: 0 });

const PropertiesView = ({ currentUser }) => {
    const [rows, setRows]           = React.useState([]);
    const [total, setTotal]         = React.useState(0);
    const [loading, setLoading]     = React.useState(true);
    const [loadError, setLoadError] = React.useState(false);
    const [search, setSearch]       = React.useState('');
    const [statusFilter, setStatusFilter] = React.useState(null); // null = all
    const [editing, setEditing]     = React.useState(null);       // null | 'new' | property row
    // Form shape (defs + hidden built-ins) — one fetch, shared by the table
    // and both modals below.
    const [fieldDefs, setFieldDefs]     = React.useState([]);
    const [hiddenFields, setHiddenFields] = React.useState([]);
    const [managingFields, setManagingFields] = React.useState(false);

    const isManager = currentUser.role === 'admin' || currentUser.role === 'manager';
    const hidden = (f) => hiddenFields.includes(f);

    const load = async () => {
        try {
            const params = { paged: 'true', limit: 100 };
            if (search.trim()) params.search = search.trim();
            if (statusFilter) params.status = statusFilter;
            const page = await api.getProperties(params);
            setRows(page.rows);
            setTotal(page.total);
            setLoadError(false);
        } catch (err) {
            console.error('Inventory load failed:', err);
            setLoadError(true);
        } finally {
            setLoading(false);
        }
    };
    const loadShape = async () => {
        try {
            const shape = await api.getPropertyFields();
            setFieldDefs(shape.fields);
            setHiddenFields(shape.hidden_fields);
        } catch (err) {
            console.error('Field defs load failed:', err); // form falls back to built-ins only
        }
    };
    React.useEffect(() => { loadShape(); }, []);
    React.useEffect(() => { load(); }, [statusFilter]);
    // Debounced search — same 250ms budget as the accounts list.
    React.useEffect(() => {
        const t = setTimeout(load, 250);
        return () => clearTimeout(t);
    }, [search]);

    // Hidden built-ins drop their table column too — one config, one truth.
    const showBedsBaths = !hidden('beds') || !hidden('baths');

    return (
        <div className="view-content">
            <div className="accounts-toolbar">
                <input
                    type="text"
                    className="search-box accounts-search"
                    placeholder="Search address, city, ZIP…"
                    value={search}
                    onChange={(e) => setSearch(e.target.value)}
                />
                {PROPERTY_STATUSES.map(s => {
                    const active = statusFilter === s.id;
                    return (
                        <button key={s.id}
                            className={`tag-pill tag-filter-chip ${active ? 'active' : ''}`}
                            style={active
                                ? { background: s.color, color: '#fff', border: `1px solid ${s.color}` }
                                : { background: s.color + '22', color: s.color, border: `1px solid ${s.color}44` }}
                            onClick={() => setStatusFilter(active ? null : s.id)}
                            title={active ? 'Show every status' : `Show only ${s.label}`}>
                            {s.label}
                        </button>
                    );
                })}
                {isManager && (
                    <button className="btn btn-secondary" onClick={() => setManagingFields(true)}
                            title="Add custom fields and choose which built-in fields the form shows">
                        <i className="fas fa-sliders-h"></i> Fields
                    </button>
                )}
                <button className="btn btn-primary" onClick={() => setEditing('new')}>
                    <i className="fas fa-plus"></i> New Property
                </button>
            </div>

            {loadError && !loading && (
                <LoadErrorBanner what="the inventory" hasData={rows.length > 0} onRetry={load} />
            )}

            {loading ? (
                <div className="loading-state"><i className="fas fa-spinner fa-spin"></i> Loading…</div>
            ) : rows.length === 0 ? (
                <div className="empty-state" style={{ marginTop: '3rem' }}>
                    <i className="fas fa-home empty-state-icon"></i>
                    <p className="empty-state-message">
                        {search || statusFilter ? 'No properties match.' : 'No properties yet — add the first home.'}
                    </p>
                </div>
            ) : (<>
                {total > rows.length && (
                    <div className="accounts-count-note">
                        Showing {rows.length} of {total.toLocaleString()} — search to narrow
                    </div>
                )}
                <table className="data-table accounts-table">
                    <thead>
                        <tr>
                            <th>Address</th>
                            <th>Status</th>
                            {!hidden('price') && <th>Price</th>}
                            {showBedsBaths && <th>Beds / Baths</th>}
                            {!hidden('sqft') && <th>Sqft</th>}
                            <th>Photos</th>
                            <th>Website</th>
                        </tr>
                    </thead>
                    <tbody>
                        {rows.map(p => {
                            const st = statusMeta(p.status);
                            return (
                                <tr key={p.id} className="row-clickable" onClick={() => setEditing(p)}>
                                    <td>
                                        <span className="accounts-table-name">{p.address}</span>
                                        {(p.city || p.zip) && !hidden('city') && (
                                            <span style={{ marginLeft: '0.5rem', fontSize: '0.8125rem', color: 'var(--text-3, #9ca3af)' }}>
                                                {[p.city, p.state, p.zip].filter(Boolean).join(', ')}
                                            </span>
                                        )}
                                    </td>
                                    <td>
                                        <span className="badge" style={{ color: st.color, borderColor: st.color + '55', background: st.color + '14', border: `1px solid ${st.color}55` }}>
                                            {st.label}
                                        </span>
                                    </td>
                                    {!hidden('price') && <td>{fmtPrice(p.price)}</td>}
                                    {showBedsBaths && <td>{p.beds ?? '—'} / {p.baths != null ? parseFloat(p.baths) : '—'}</td>}
                                    {!hidden('sqft') && <td>{p.sqft ? parseInt(p.sqft).toLocaleString() : '—'}</td>}
                                    <td>{p.attachment_count > 0 ? <><i className="fas fa-image" style={{ marginRight: '0.25rem', color: 'var(--text-3, #9ca3af)' }}></i>{p.attachment_count}</> : '—'}</td>
                                    <td>
                                        {p.published
                                            ? <span className="badge" style={{ color: '#22c55e', background: '#22c55e14', border: '1px solid #22c55e55' }}><i className="fas fa-globe" style={{ marginRight: '0.25rem' }}></i>Published</span>
                                            : <span style={{ fontSize: '0.8125rem', color: 'var(--text-3, #9ca3af)' }}>Hidden</span>}
                                    </td>
                                </tr>
                            );
                        })}
                    </tbody>
                </table>
            </>)}

            {editing && (
                <PropertyModal
                    property={editing === 'new' ? null : editing}
                    fieldDefs={fieldDefs}
                    hiddenFields={hiddenFields}
                    isManager={isManager}
                    isAdmin={currentUser.role === 'admin'}
                    onSaved={() => { setEditing(null); load(); }}
                    onClose={() => setEditing(null)}
                />
            )}

            {managingFields && (
                <ManageFieldsModal
                    fieldDefs={fieldDefs}
                    hiddenFields={hiddenFields}
                    onChanged={loadShape}
                    onClose={() => { setManagingFields(false); loadShape(); }}
                />
            )}
        </div>
    );
};

// One custom-field input, rendered by type. Values live as strings in form
// state (like every other input) except booleans, which are real booleans.
const CustomFieldInput = ({ def, value, onChange }) => {
    if (def.type === 'boolean') {
        return (
            <div className="form-group">
                <label className="form-label">{def.label}</label>
                <select className="form-input" value={value === true ? 'yes' : value === false ? 'no' : ''}
                        onChange={(e) => onChange(e.target.value === '' ? null : e.target.value === 'yes')}>
                    <option value="">—</option>
                    <option value="yes">Yes</option>
                    <option value="no">No</option>
                </select>
            </div>
        );
    }
    if (def.type === 'dropdown') {
        return (
            <div className="form-group">
                <label className="form-label">{def.label}</label>
                <select className="form-input" value={value ?? ''} onChange={(e) => onChange(e.target.value || null)}>
                    <option value="">—</option>
                    {(def.options || []).map(o => <option key={o} value={o}>{o}</option>)}
                </select>
            </div>
        );
    }
    if (def.type === 'number') {
        return (
            <div className="form-group">
                <label className="form-label">{def.label}</label>
                <input className="form-input" type="number" value={value ?? ''}
                       onChange={(e) => onChange(e.target.value)} />
            </div>
        );
    }
    // Text custom fields get the value-suggestion typeahead — "Plan Name"
    // and friends repeat across homes, so surface what's already been typed.
    return (
        <div className="form-group">
            <label className="form-label">{def.label}</label>
            <SuggestInput field={def.key} value={value ?? ''} maxLength={1000}
                          onValueChange={onChange} />
        </div>
    );
};

// Create/edit modal. Photos and documents ride the shared AttachmentsPanel
// (entity_type 'property') — available once the record exists, because an
// attachment needs an id to hang off.
const PropertyModal = ({ property, fieldDefs, hiddenFields, isManager, isAdmin, onSaved, onClose }) => {
    const isNew = !property;
    const show = (f) => !hiddenFields.includes(f);
    const [form, setForm] = React.useState({
        address: property?.address || '',
        city:    property?.city    || '',
        state:   property?.state   || '',
        zip:     property?.zip     || '',
        price:   property?.price   != null ? String(parseFloat(property.price)) : '',
        beds:    property?.beds    != null ? String(property.beds) : '',
        baths:   property?.baths   != null ? String(parseFloat(property.baths)) : '',
        sqft:    property?.sqft    != null ? String(property.sqft) : '',
        status:  property?.status  || 'planned',
        description: property?.description || '',
    });
    const [custom, setCustom] = React.useState({ ...(property?.custom || {}) });
    const [published, setPublished] = React.useState(property?.published || false);
    const [saving, setSaving] = React.useState(false);
    const [error, setError]   = React.useState('');
    // A publish flip is written to the server the moment it's clicked, so once
    // one has happened NO close path may leave the list showing the old state.
    const [publishFlipped, setPublishFlipped] = React.useState(false);
    // Snapshot of the form as opened — only the first render's value is kept,
    // so this is the "what did the user change?" baseline.
    const opened = React.useRef(null);
    if (opened.current === null) opened.current = JSON.stringify({ form, custom });
    const isDirty = () => JSON.stringify({ form, custom }) !== opened.current;
    const set = (k) => (e) => setForm(p => ({ ...p, [k]: e.target.value }));
    const setCustomField = (key) => (v) => setCustom(p => ({ ...p, [key]: v }));

    // Cancel/X/Escape/overlay all land here. Publishing is not part of the
    // form, so "cancel" cannot revert it — the honest thing is to close AND
    // resync the list so the row matches the website.
    const closeModal = () => { publishFlipped ? onSaved() : onClose(); };

    const submit = async (e) => {
        e.preventDefault();
        setSaving(true); setError('');
        // Empty string → null so clearing a field actually clears it.
        // Custom number fields stay strings in the input; coerce on the way out.
        const customOut = {};
        for (const def of fieldDefs) {
            const v = custom[def.key];
            if (v === undefined || v === null || v === '') continue;
            customOut[def.key] = def.type === 'number' ? Number(v) : v;
        }
        const payload = {
            address: form.address, city: form.city, state: form.state, zip: form.zip,
            price: form.price === '' ? null : Number(form.price),
            beds:  form.beds  === '' ? null : Number(form.beds),
            baths: form.baths === '' ? null : Number(form.baths),
            sqft:  form.sqft  === '' ? null : Number(form.sqft),
            status: form.status,
            description: form.description,
            custom: customOut,
        };
        try {
            if (isNew) await api.createProperty(payload);
            else       await api.updateProperty(property.id, payload);
            onSaved();
        } catch (err) { setError(err.message); setSaving(false); }
    };

    // Publish flip saves immediately — it's a switch, not a form field. It is
    // also an OUTWARD-FACING act (the public site reads the feed at request
    // time, so "published" means live to strangers within a page load), which
    // is why it gets a confirm and why the dirty-form case is called out: the
    // flip publishes the SAVED record, not what's on screen.
    const togglePublished = async () => {
        const goingLive = !published;
        const dirty = isDirty();
        const ok = await confirmAction({
            title: goingLive ? 'Publish to the public website' : 'Unpublish from the website',
            message: goingLive
                ? `"${property.address}" goes live on the public website immediately — visitors can see it as soon as you confirm.`
                  + (dirty ? '\n\nYou have unsaved edits on this form. Publishing puts the SAVED version live, not what you see here — Cancel out of this and Save first if those edits should go public.' : '')
                : `"${property.address}" comes off the public website immediately. The record, its photos and its history all stay here.`,
            confirmLabel: goingLive ? 'Publish it' : 'Unpublish it',
            danger: !goingLive,
        });
        if (!ok) return;
        try {
            const updated = await api.updateProperty(property.id, { published: !published });
            setPublished(updated.published);
            setPublishFlipped(true);
        } catch (err) { setError(err.message); }
    };

    const handleDelete = async () => {
        if (!await confirmAction(`Delete "${property.address}"? Photos and documents go with it. This cannot be undone.`)) return;
        try { await api.deleteProperty(property.id); onSaved(); }
        catch (err) { setError(err.message); }
    };

    return (
        <Modal isOpen={true} onClose={closeModal} title={isNew ? 'New Property' : property.address}>
            <form onSubmit={submit}>
                {error && <div className="api-error">{error}</div>}
                <div className="form-grid">
                    <div className="form-group full-width">
                        <label className="form-label">Street address *</label>
                        <input className="form-input" value={form.address} onChange={set('address')} maxLength={500} required autoFocus={isNew} />
                    </div>
                    {/* City/state/ZIP repeat across a builder's inventory —
                        value-suggestion typeahead keeps spellings consistent. */}
                    {show('city') && (
                        <div className="form-group">
                            <label className="form-label">City</label>
                            <SuggestInput field="city" value={form.city} maxLength={100}
                                          onValueChange={(v) => setForm(p => ({ ...p, city: v }))} />
                        </div>
                    )}
                    {show('state') && (
                        <div className="form-group">
                            <label className="form-label">State</label>
                            <SuggestInput field="state" value={form.state} maxLength={100}
                                          onValueChange={(v) => setForm(p => ({ ...p, state: v }))} />
                        </div>
                    )}
                    {show('zip') && (
                        <div className="form-group">
                            <label className="form-label">ZIP</label>
                            <SuggestInput field="zip" value={form.zip} maxLength={20}
                                          onValueChange={(v) => setForm(p => ({ ...p, zip: v }))} />
                        </div>
                    )}
                    <div className="form-group">
                        <label className="form-label">Status</label>
                        <select className="form-input" value={form.status} onChange={set('status')}>
                            {PROPERTY_STATUSES.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
                        </select>
                    </div>
                    {show('price') && (
                        <div className="form-group">
                            <label className="form-label">Price ($)</label>
                            <input className="form-input" type="number" min="0" step="any" value={form.price} onChange={set('price')} />
                        </div>
                    )}
                    {show('beds') && (
                        <div className="form-group">
                            <label className="form-label">Beds</label>
                            <input className="form-input" type="number" min="0" step="1" value={form.beds} onChange={set('beds')} />
                        </div>
                    )}
                    {show('baths') && (
                        <div className="form-group">
                            <label className="form-label">Baths</label>
                            <input className="form-input" type="number" min="0" step="0.5" value={form.baths} onChange={set('baths')} />
                        </div>
                    )}
                    {show('sqft') && (
                        <div className="form-group">
                            <label className="form-label">Sqft</label>
                            <input className="form-input" type="number" min="0" step="1" value={form.sqft} onChange={set('sqft')} />
                        </div>
                    )}
                    {fieldDefs.map(def => (
                        <CustomFieldInput key={def.key} def={def}
                            value={custom[def.key]} onChange={setCustomField(def.key)} />
                    ))}
                    {show('description') && (
                        <div className="form-group full-width">
                            <label className="form-label">Description</label>
                            <textarea className="form-input" rows={4} value={form.description} onChange={set('description')}
                                      maxLength={10000} style={{ resize: 'vertical' }}
                                      placeholder="What a buyer (and later, the website) should know about this home…" />
                        </div>
                    )}
                </div>

                {!isNew && <RealtorsPanel propertyId={property.id} />}

                {!isNew && (
                    <div style={{ margin: '1rem 0' }}>
                        {/* One title, owned by the panel (it carries the count and the
                            collapse toggle) — no duplicate header above it. */}
                        <AttachmentsPanel entityType="property" entityId={property.id}
                                          title="Photos & Documents" />
                    </div>
                )}

                {!isNew && isManager && (
                    <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', margin: '1rem 0' }}>
                        <button type="button"
                            className={`btn btn-small ${published ? 'btn-secondary' : 'btn-primary'}`}
                            onClick={togglePublished}
                            title={published ? 'Take this home off the public website' : 'Show this home on the public website'}>
                            <i className={`fas ${published ? 'fa-eye-slash' : 'fa-globe'}`}></i>
                            {published ? ' Unpublish' : ' Publish to website'}
                        </button>
                        <span style={{ fontSize: '0.75rem', color: 'var(--text-3, #9ca3af)' }}>
                            {published
                                ? 'Live on the public website right now — this switch saves immediately, Cancel does not undo it.'
                                : 'Hidden from the public website. Publishing takes effect immediately.'}
                        </span>
                    </div>
                )}

                <div className="btn-group">
                    {!isNew && isAdmin && (
                        <button type="button" className="btn btn-danger" onClick={handleDelete} style={{ marginRight: 'auto' }}>
                            <i className="fas fa-trash"></i> Delete
                        </button>
                    )}
                    {/* Truthful label: after a publish flip there is nothing left to
                        cancel — that change is already live. */}
                    <button type="button" className="btn btn-secondary" onClick={closeModal} disabled={saving}>
                        {publishFlipped ? 'Close' : 'Cancel'}
                    </button>
                    <button type="submit" className="btn btn-primary" disabled={saving}>
                        {saving ? <><i className="fas fa-spinner fa-spin"></i> Saving…</> : <><i className="fas fa-check"></i> {isNew ? 'Add Property' : 'Save'}</>}
                    </button>
                </div>
            </form>
        </Modal>
    );
};

// Realtors on a home (migration 0004) — self-fetching like AttachmentsPanel,
// and like it, only rendered once the record exists (a link needs an id to
// hang off). Contact search is the ContactPicker typeahead; the optional role
// ("Listing agent") is typed before picking. Available to every user — the
// inventory is the team's shared list.
const RealtorsPanel = ({ propertyId }) => {
    const [links, setLinks] = React.useState([]);
    const [role, setRole]   = React.useState('');
    const [error, setError] = React.useState('');

    const load = () => api.getPropertyContacts(propertyId).then(setLinks).catch(() => setLinks([]));
    React.useEffect(() => { load(); }, [propertyId]);

    const add = async (contact) => {
        setError('');
        try {
            await api.linkPropertyContact(propertyId, { contact_id: contact.id, role: role.trim() || null });
            setRole('');
            await load();
        } catch (err) { setError(err.message); }
    };
    const remove = async (link) => {
        setError('');
        try { await api.unlinkPropertyContact(propertyId, link.id); await load(); }
        catch (err) { setError(err.message); }
    };

    return (
        <div style={{ margin: '1rem 0' }}>
            <div className="detail-section-header">
                <div className="detail-section-title"><i className="fas fa-user-tie" style={{ marginRight: '0.5rem', color: '#6b7280' }}></i>Realtors & Contacts</div>
            </div>
            {error && <div className="api-error">{error}</div>}
            {links.map(l => (
                <div key={l.id} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.375rem', fontSize: '0.875rem' }}>
                    <span style={{ fontWeight: 500 }}>{[l.first_name, l.last_name].filter(Boolean).join(' ')}</span>
                    {l.role && <span className="badge">{l.role}</span>}
                    {l.account_name && <span style={{ color: 'var(--text-3, #9ca3af)' }}>{l.account_name}</span>}
                    {(l.email || l.phone) && (
                        <span style={{ color: 'var(--text-3, #9ca3af)', fontSize: '0.8125rem' }}>{l.email || l.phone}</span>
                    )}
                    <button type="button" className="btn-icon-sm" style={{ marginLeft: 'auto' }}
                            title="Unlink from this property" onClick={() => remove(l)}>
                        <i className="fas fa-times"></i>
                    </button>
                </div>
            ))}
            <div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.5rem' }}>
                <div style={{ flex: 2 }}>
                    <ContactPicker onPick={add} excludeIds={links.map(l => l.contact_id)}
                                   placeholder="Link a contact — type a name…" />
                </div>
                <input className="form-input" style={{ flex: 1 }} maxLength={100}
                       placeholder="Role (e.g. Listing agent)" value={role}
                       onChange={(e) => setRole(e.target.value)} />
            </div>
        </div>
    );
};

// Manage the form's shape (manager+): add/rename/delete custom fields and
// choose which built-in fields show. Lives in this file, not components/ —
// it's inventory-specific and shares the vocab constants above.
const ManageFieldsModal = ({ fieldDefs, hiddenFields, onChanged, onClose }) => {
    const [newLabel, setNewLabel]     = React.useState('');
    const [newType, setNewType]       = React.useState('text');
    const [newOptions, setNewOptions] = React.useState(''); // comma-separated
    const [busy, setBusy]   = React.useState(false);
    const [error, setError] = React.useState('');

    const run = async (fn) => {
        setBusy(true); setError('');
        try { await fn(); await onChanged(); }
        catch (err) { setError(err.message); }
        finally { setBusy(false); }
    };

    const addField = () => run(async () => {
        await api.createPropertyField({
            label: newLabel,
            type: newType,
            options: newType === 'dropdown'
                ? newOptions.split(',').map(s => s.trim()).filter(Boolean)
                : [],
        });
        setNewLabel(''); setNewOptions(''); setNewType('text');
    });

    const renameField = (def, label) => {
        if (!label.trim() || label.trim() === def.label) return;
        run(() => api.updatePropertyField(def.id, { label }));
    };

    const deleteField = async (def) => {
        if (!await confirmAction(`Remove the "${def.label}" field from the form? Saved values are kept and come back if you re-add a field with the same name.`)) return;
        run(() => api.deletePropertyField(def.id));
    };

    const toggleBuiltin = (key) => run(() => api.setInventoryVisibility(
        hiddenFields.includes(key)
            ? hiddenFields.filter(f => f !== key)
            : [...hiddenFields, key]
    ));

    const typeLabel = { text: 'Text', number: 'Number', dropdown: 'Dropdown', boolean: 'Yes / No' };

    return (
        <Modal isOpen={true} onClose={onClose} title="Inventory Fields">
            {error && <div className="api-error">{error}</div>}

            <div className="detail-section-header"><div className="detail-section-title">Custom fields</div></div>
            {fieldDefs.length === 0 && (
                <p style={{ fontSize: '0.875rem', color: 'var(--text-3, #9ca3af)' }}>
                    No custom fields yet — add whatever your spreadsheet had ("Garage Door", "Lot #", …).
                </p>
            )}
            {fieldDefs.map(def => (
                <div key={def.id} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
                    <input className="form-input" defaultValue={def.label} maxLength={100}
                           style={{ flex: 1 }} disabled={busy}
                           onBlur={(e) => renameField(def, e.target.value)}
                           title="Rename — saved values stay attached" />
                    <span className="badge" style={{ whiteSpace: 'nowrap' }}>
                        {typeLabel[def.type]}{def.type === 'dropdown' ? `: ${(def.options || []).join(' / ')}` : ''}
                    </span>
                    <button type="button" className="btn btn-small btn-secondary" disabled={busy}
                            onClick={() => deleteField(def)} title="Remove this field">
                        <i className="fas fa-trash"></i>
                    </button>
                </div>
            ))}

            <div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem', flexWrap: 'wrap' }}>
                <input className="form-input" placeholder="New field name…" value={newLabel} maxLength={100}
                       style={{ flex: 2, minWidth: '10rem' }}
                       onChange={(e) => setNewLabel(e.target.value)} />
                <select className="form-input" value={newType} style={{ flex: 1, minWidth: '7rem' }}
                        onChange={(e) => setNewType(e.target.value)}>
                    {Object.entries(typeLabel).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
                </select>
                <button type="button" className="btn btn-primary" disabled={busy || !newLabel.trim()} onClick={addField}>
                    <i className="fas fa-plus"></i> Add
                </button>
                {newType === 'dropdown' && (
                    <input className="form-input" style={{ flexBasis: '100%' }}
                           placeholder="Options, comma-separated (e.g. Left, Right)"
                           value={newOptions} onChange={(e) => setNewOptions(e.target.value)} />
                )}
            </div>

            <div className="detail-section-header" style={{ marginTop: '1.25rem' }}>
                <div className="detail-section-title">Built-in fields</div>
            </div>
            <p style={{ fontSize: '0.8125rem', color: 'var(--text-3, #9ca3af)', marginTop: 0 }}>
                Uncheck a field to hide it from the form and the list. Address and status always show.
            </p>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(9rem, 1fr))', gap: '0.375rem' }}>
                {Object.entries(HIDEABLE_LABELS).map(([key, label]) => (
                    <label key={key} style={{ display: 'flex', alignItems: 'center', gap: '0.375rem', fontSize: '0.875rem', cursor: 'pointer' }}>
                        <input type="checkbox" checked={!hiddenFields.includes(key)} disabled={busy}
                               onChange={() => toggleBuiltin(key)} />
                        {label}
                    </label>
                ))}
            </div>

            <div className="btn-group" style={{ marginTop: '1rem' }}>
                <button type="button" className="btn btn-secondary" onClick={onClose}>Done</button>
            </div>
        </Modal>
    );
};
