Files
ODOO4projects.com/odoo-community-developers/admin/index.html
T
webmaster ada9605c90 feat(freelancer-directory): Country is an ISO 3166-1 dropdown on both forms
Country was free text, so one country could be entered as Germany / DE /
Deutschland / germany and split the directory filter four ways. Both the
application and the admin form now submit from a native <select> of the
249 ISO 3166-1 alpha-2 codes: the option shows the English name, the value
is the code. No free-text fallback and no "other" escape hatch. Required,
with an empty "— choose —" first option. The directory filter shows the
English name for the stored code, so one country is one option.

Non-trivial, non-UI: the submitted value format lives in one constant per
page, COUNTRY_VALUE_FORMAT ("code" | "name") — switching to full country
names is a one-line change if the n8n workflow expects names, not codes.
The webhook field name stays exactly "Country".
2026-11-03 12:41:02 +00:00

843 lines
35 KiB
HTML

<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Freelancer records — admin</title>
<!-- DRAFT / INTERNAL: not indexed, not linked, not in sitemap.xml / llms.txt.
Reachable only by this URL. All five draft conditions hold
(noindex · not in sitemap · not in llms.txt · no inbound links · no announcement
surface). Do NOT link to this page from anywhere — not from the directory, not
from the footer. Remove this robots meta only if it ever becomes public. -->
<meta name="robots" content="noindex, nofollow" />
<link rel="canonical" href="https://odoo4projects.com/odoo-community-developers/admin/" />
<link rel="icon" href="/images/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap"
media="print"
onload="this.media='all'"
/>
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Marcellus&display=swap" /></noscript>
<link rel="stylesheet" href="/style.css" />
<style>
/* ---- Freelancer records — admin (page-scoped). Internal page, never linked. ---- */
.adm-bar {
background: var(--midnight-2);
border-bottom: 1px solid var(--line-brass);
padding: 0.85rem 0;
}
.adm-bar .container {
display: flex;
align-items: baseline;
gap: 1rem;
flex-wrap: wrap;
}
.adm-brand {
font-family: Marcellus, Georgia, serif;
font-size: 1.15rem;
letter-spacing: 0.04em;
color: #fff;
}
.adm-brand span {
color: var(--brass);
}
.adm-tag {
font-size: 0.78rem;
letter-spacing: 0.05em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.65);
}
.adm-wrap {
padding: 2.5rem 0 4rem;
}
.adm-wrap h1 {
font-family: Marcellus, Georgia, serif;
font-weight: 400;
font-size: 1.9rem;
color: var(--midnight);
margin: 0 0 0.6rem;
}
.adm-note {
max-width: 720px;
margin: 0 0 1.75rem;
color: var(--ink-soft);
line-height: 1.7;
}
.adm-note code {
font-size: 0.85em;
color: var(--midnight);
}
.form-card {
max-width: 860px;
background: var(--ivory);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
padding: 1.5rem;
box-shadow: var(--shadow-rest);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.form-field.full,
.form-grid > .full {
grid-column: 1 / -1;
}
.form-field label {
font-size: 0.8rem;
font-weight: 600;
color: var(--midnight);
}
.form-field .hint {
font-size: 0.78rem;
color: var(--ink-soft);
}
.form-field input,
.form-field textarea,
.form-field select {
font-family: Inter, sans-serif;
font-size: 0.9rem;
padding: 0.6rem 0.7rem;
border-radius: var(--radius);
border: 1px solid var(--line);
background: #fff;
color: var(--ink);
width: 100%;
}
.form-check {
grid-column: 1 / -1;
display: flex;
align-items: flex-start;
gap: 0.6rem;
font-size: 0.85rem;
color: var(--ink-soft);
}
.form-check input {
width: auto;
margin-top: 0.2rem;
flex-shrink: 0;
}
.form-actions {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
margin-top: 0.25rem;
}
.form-status {
font-size: 0.85rem;
color: var(--ink-soft);
}
.form-status[data-state="ok"] {
color: var(--success, #3f7d4f);
}
.form-status[data-state="err"] {
color: #b3452f;
}
.adm-loaded {
grid-column: 1 / -1;
margin: 0;
font-size: 0.85rem;
line-height: 1.6;
color: var(--ink-soft);
}
.adm-loaded[data-state="ok"] {
color: var(--success, #3f7d4f);
}
.adm-loaded[data-state="err"] {
color: #b3452f;
}
.adm-sep {
grid-column: 1 / -1;
width: 100%;
border: 0;
border-top: 1px solid var(--line);
margin: 0.35rem 0;
}
/* Page-local quiet button: the shared .btn-outline is cream-on-transparent (built
for the dark sections) and would be near-invisible on this page's ivory card. */
.btn-quiet {
background: transparent;
color: var(--ink);
border-color: rgba(23, 28, 38, 0.22);
}
.btn-quiet:hover {
color: var(--brass-deep);
border-color: var(--brass-deep);
background: rgba(201, 162, 39, 0.08);
}
.btn:focus-visible {
outline: 3px solid var(--brass-deep);
outline-offset: 2px;
border-radius: var(--radius);
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="adm-bar">
<div class="container">
<span class="adm-brand">ODOO4<span>projects</span></span>
<span class="adm-tag">Freelancer records — internal</span>
</div>
</div>
<main class="adm-wrap">
<div class="container">
<h1>Add or correct a freelancer record</h1>
<p class="adm-note">
Internal page — not linked from the directory. Add a new record, or load an existing one by
its id, <strong>confirm it is the right person</strong>, then correct what is wrong. A save
posts to the one <code>odoo_experts</code> webhook and is applied after email confirmation.
Fields you leave alone keep the record's current value.
</p>
<form class="form-card" id="adminForm" novalidate>
<div class="form-grid">
<div class="form-field">
<label for="adId">Record id <span class="hint">(leave blank to create a new record)</span></label>
<input id="adId" name="id" type="number" min="1" />
</div>
<div class="form-field">
<label for="adLoadBtn">Confirm before you edit</label>
<button type="button" class="btn btn-quiet" id="adLoadBtn">Load this record</button>
</div>
<p class="adm-loaded" id="adLoadInfo" role="status">Enter a record id and load it — you will see who it is before anything is written.</p>
<hr class="adm-sep" />
<div class="form-field">
<label for="adName">Name</label>
<input id="adName" name="Name" type="text" />
</div>
<div class="form-field">
<label for="adEmail">Email</label>
<input id="adEmail" name="email" type="email" />
</div>
<div class="form-field">
<label for="adClients">Projects on ODOO4projects</label>
<input id="adClients" name="odoo4projects_clients" type="number" min="0" />
<span class="hint">A number we record ourselves — not the freelancer's, not a rating.</span>
</div>
<div class="form-field">
<label for="adFocus">Focus</label>
<input id="adFocus" name="Focus" type="text" />
</div>
<div class="form-field full">
<label for="adDescription">Description</label>
<textarea id="adDescription" name="description" rows="4"></textarea>
</div>
<div class="form-field">
<label for="adCountry">Country</label>
<select id="adCountry" name="Country" required>
<!-- COUNTRY-OPTIONS-START -->
<option value="">— choose —</option>
<option value="AD">Andorra</option>
<option value="AE">United Arab Emirates</option>
<option value="AF">Afghanistan</option>
<option value="AG">Antigua and Barbuda</option>
<option value="AI">Anguilla</option>
<option value="AL">Albania</option>
<option value="AM">Armenia</option>
<option value="AO">Angola</option>
<option value="AQ">Antarctica</option>
<option value="AR">Argentina</option>
<option value="AS">American Samoa</option>
<option value="AT">Austria</option>
<option value="AU">Australia</option>
<option value="AW">Aruba</option>
<option value="AX">Aland</option>
<option value="AZ">Azerbaijan</option>
<option value="BA">Bosnia and Herzegovina</option>
<option value="BB">Barbados</option>
<option value="BD">Bangladesh</option>
<option value="BE">Belgium</option>
<option value="BF">Burkina Faso</option>
<option value="BG">Bulgaria</option>
<option value="BH">Bahrain</option>
<option value="BI">Burundi</option>
<option value="BJ">Benin</option>
<option value="BL">Saint Barthelemy</option>
<option value="BM">Bermuda</option>
<option value="BN">Brunei</option>
<option value="BO">Bolivia</option>
<option value="BQ">Bonaire</option>
<option value="BR">Brazil</option>
<option value="BS">Bahamas</option>
<option value="BT">Bhutan</option>
<option value="BV">Bouvet Island</option>
<option value="BW">Botswana</option>
<option value="BY">Belarus</option>
<option value="BZ">Belize</option>
<option value="CA">Canada</option>
<option value="CC">Cocos (Keeling) Islands</option>
<option value="CD">Democratic Republic of the Congo</option>
<option value="CF">Central African Republic</option>
<option value="CG">Republic of the Congo</option>
<option value="CH">Switzerland</option>
<option value="CI">Ivory Coast</option>
<option value="CK">Cook Islands</option>
<option value="CL">Chile</option>
<option value="CM">Cameroon</option>
<option value="CN">China</option>
<option value="CO">Colombia</option>
<option value="CR">Costa Rica</option>
<option value="CU">Cuba</option>
<option value="CV">Cape Verde</option>
<option value="CW">Curacao</option>
<option value="CX">Christmas Island</option>
<option value="CY">Cyprus</option>
<option value="CZ">Czech Republic</option>
<option value="DE">Germany</option>
<option value="DJ">Djibouti</option>
<option value="DK">Denmark</option>
<option value="DM">Dominica</option>
<option value="DO">Dominican Republic</option>
<option value="DZ">Algeria</option>
<option value="EC">Ecuador</option>
<option value="EE">Estonia</option>
<option value="EG">Egypt</option>
<option value="EH">Western Sahara</option>
<option value="ER">Eritrea</option>
<option value="ES">Spain</option>
<option value="ET">Ethiopia</option>
<option value="FI">Finland</option>
<option value="FJ">Fiji</option>
<option value="FK">Falkland Islands</option>
<option value="FM">Micronesia</option>
<option value="FO">Faroe Islands</option>
<option value="FR">France</option>
<option value="GA">Gabon</option>
<option value="GB">United Kingdom</option>
<option value="GD">Grenada</option>
<option value="GE">Georgia</option>
<option value="GF">French Guiana</option>
<option value="GG">Guernsey</option>
<option value="GH">Ghana</option>
<option value="GI">Gibraltar</option>
<option value="GL">Greenland</option>
<option value="GM">Gambia</option>
<option value="GN">Guinea</option>
<option value="GP">Guadeloupe</option>
<option value="GQ">Equatorial Guinea</option>
<option value="GR">Greece</option>
<option value="GS">South Georgia and the South Sandwich Islands</option>
<option value="GT">Guatemala</option>
<option value="GU">Guam</option>
<option value="GW">Guinea-Bissau</option>
<option value="GY">Guyana</option>
<option value="HK">Hong Kong</option>
<option value="HM">Heard Island and McDonald Islands</option>
<option value="HN">Honduras</option>
<option value="HR">Croatia</option>
<option value="HT">Haiti</option>
<option value="HU">Hungary</option>
<option value="ID">Indonesia</option>
<option value="IE">Ireland</option>
<option value="IL">Israel</option>
<option value="IM">Isle of Man</option>
<option value="IN">India</option>
<option value="IO">British Indian Ocean Territory</option>
<option value="IQ">Iraq</option>
<option value="IR">Iran</option>
<option value="IS">Iceland</option>
<option value="IT">Italy</option>
<option value="JE">Jersey</option>
<option value="JM">Jamaica</option>
<option value="JO">Jordan</option>
<option value="JP">Japan</option>
<option value="KE">Kenya</option>
<option value="KG">Kyrgyzstan</option>
<option value="KH">Cambodia</option>
<option value="KI">Kiribati</option>
<option value="KM">Comoros</option>
<option value="KN">Saint Kitts and Nevis</option>
<option value="KP">North Korea</option>
<option value="KR">South Korea</option>
<option value="KW">Kuwait</option>
<option value="KY">Cayman Islands</option>
<option value="KZ">Kazakhstan</option>
<option value="LA">Laos</option>
<option value="LB">Lebanon</option>
<option value="LC">Saint Lucia</option>
<option value="LI">Liechtenstein</option>
<option value="LK">Sri Lanka</option>
<option value="LR">Liberia</option>
<option value="LS">Lesotho</option>
<option value="LT">Lithuania</option>
<option value="LU">Luxembourg</option>
<option value="LV">Latvia</option>
<option value="LY">Libya</option>
<option value="MA">Morocco</option>
<option value="MC">Monaco</option>
<option value="MD">Moldova</option>
<option value="ME">Montenegro</option>
<option value="MF">Saint Martin</option>
<option value="MG">Madagascar</option>
<option value="MH">Marshall Islands</option>
<option value="MK">North Macedonia</option>
<option value="ML">Mali</option>
<option value="MM">Myanmar (Burma)</option>
<option value="MN">Mongolia</option>
<option value="MO">Macao</option>
<option value="MP">Northern Mariana Islands</option>
<option value="MQ">Martinique</option>
<option value="MR">Mauritania</option>
<option value="MS">Montserrat</option>
<option value="MT">Malta</option>
<option value="MU">Mauritius</option>
<option value="MV">Maldives</option>
<option value="MW">Malawi</option>
<option value="MX">Mexico</option>
<option value="MY">Malaysia</option>
<option value="MZ">Mozambique</option>
<option value="NA">Namibia</option>
<option value="NC">New Caledonia</option>
<option value="NE">Niger</option>
<option value="NF">Norfolk Island</option>
<option value="NG">Nigeria</option>
<option value="NI">Nicaragua</option>
<option value="NL">Netherlands</option>
<option value="NO">Norway</option>
<option value="NP">Nepal</option>
<option value="NR">Nauru</option>
<option value="NU">Niue</option>
<option value="NZ">New Zealand</option>
<option value="OM">Oman</option>
<option value="PA">Panama</option>
<option value="PE">Peru</option>
<option value="PF">French Polynesia</option>
<option value="PG">Papua New Guinea</option>
<option value="PH">Philippines</option>
<option value="PK">Pakistan</option>
<option value="PL">Poland</option>
<option value="PM">Saint Pierre and Miquelon</option>
<option value="PN">Pitcairn Islands</option>
<option value="PR">Puerto Rico</option>
<option value="PS">Palestine</option>
<option value="PT">Portugal</option>
<option value="PW">Palau</option>
<option value="PY">Paraguay</option>
<option value="QA">Qatar</option>
<option value="RE">Reunion</option>
<option value="RO">Romania</option>
<option value="RS">Serbia</option>
<option value="RU">Russia</option>
<option value="RW">Rwanda</option>
<option value="SA">Saudi Arabia</option>
<option value="SB">Solomon Islands</option>
<option value="SC">Seychelles</option>
<option value="SD">Sudan</option>
<option value="SE">Sweden</option>
<option value="SG">Singapore</option>
<option value="SH">Saint Helena</option>
<option value="SI">Slovenia</option>
<option value="SJ">Svalbard and Jan Mayen</option>
<option value="SK">Slovakia</option>
<option value="SL">Sierra Leone</option>
<option value="SM">San Marino</option>
<option value="SN">Senegal</option>
<option value="SO">Somalia</option>
<option value="SR">Suriname</option>
<option value="SS">South Sudan</option>
<option value="ST">Sao Tome and Principe</option>
<option value="SV">El Salvador</option>
<option value="SX">Sint Maarten</option>
<option value="SY">Syria</option>
<option value="SZ">Eswatini</option>
<option value="TC">Turks and Caicos Islands</option>
<option value="TD">Chad</option>
<option value="TF">French Southern Territories</option>
<option value="TG">Togo</option>
<option value="TH">Thailand</option>
<option value="TJ">Tajikistan</option>
<option value="TK">Tokelau</option>
<option value="TL">East Timor</option>
<option value="TM">Turkmenistan</option>
<option value="TN">Tunisia</option>
<option value="TO">Tonga</option>
<option value="TR">Turkey</option>
<option value="TT">Trinidad and Tobago</option>
<option value="TV">Tuvalu</option>
<option value="TW">Taiwan</option>
<option value="TZ">Tanzania</option>
<option value="UA">Ukraine</option>
<option value="UG">Uganda</option>
<option value="UM">U.S. Minor Outlying Islands</option>
<option value="US">United States</option>
<option value="UY">Uruguay</option>
<option value="UZ">Uzbekistan</option>
<option value="VA">Vatican City</option>
<option value="VC">Saint Vincent and the Grenadines</option>
<option value="VE">Venezuela</option>
<option value="VG">British Virgin Islands</option>
<option value="VI">U.S. Virgin Islands</option>
<option value="VN">Vietnam</option>
<option value="VU">Vanuatu</option>
<option value="WF">Wallis and Futuna</option>
<option value="WS">Samoa</option>
<option value="YE">Yemen</option>
<option value="YT">Mayotte</option>
<option value="ZA">South Africa</option>
<option value="ZM">Zambia</option>
<option value="ZW">Zimbabwe</option>
<!-- COUNTRY-OPTIONS-END -->
</select>
<span class="hint">Stored as the ISO code (e.g. DE).</span>
</div>
<div class="form-field">
<label for="adVersions">Odoo versions</label>
<input id="adVersions" name="odoo_versions" type="text" />
<span class="hint">Separate with commas.</span>
</div>
<div class="form-field">
<label for="adLanguages">Languages</label>
<input id="adLanguages" name="languages" type="text" />
<span class="hint">Separate with commas.</span>
</div>
<div class="form-field">
<label for="adSpecialities">Specialities</label>
<input id="adSpecialities" name="specialities" type="text" />
<span class="hint">Separate with commas.</span>
</div>
<div class="form-field">
<label for="adGithub">GitHub URL</label>
<input id="adGithub" name="github" type="text" inputmode="url" />
</div>
<div class="form-field">
<label for="adOca">OCA profile URL</label>
<input id="adOca" name="oca_profile" type="text" inputmode="url" />
</div>
<div class="form-field">
<label for="adLinkedin">LinkedIn URL</label>
<input id="adLinkedin" name="Linkedin" type="text" inputmode="url" />
</div>
<div class="form-field">
<label for="adUpwork">Upwork URL</label>
<input id="adUpwork" name="upwork" type="text" inputmode="url" />
</div>
<div class="form-field">
<label for="adFiverr">Fiverr URL</label>
<input id="adFiverr" name="fiverr" type="text" inputmode="url" />
</div>
<div class="form-field">
<label for="adImage">Image URL</label>
<input id="adImage" name="Image" type="text" inputmode="url" />
</div>
<label class="form-check">
<input type="checkbox" name="OCA_member" value="1" />
<span>OCA member.</span>
</label>
<label class="form-check">
<input type="checkbox" name="is_individual" value="1" />
<span>Works as an individual.</span>
</label>
<div class="form-actions">
<button type="submit" class="btn btn-brass">Save this record</button>
<span class="form-status" id="adminStatus" role="status"></span>
</div>
</div>
</form>
</div>
</main>
<script>
/* ============================================================================
Freelancer records — admin page script.
ONE webhook, for everything. The freelancer platform is a SEPARATE system from
the CRM: there is no /crm, no /crm_odoo and nothing from the CRM skill here.
Write contract (same URL, id decides):
POST with an id → UPDATE that record
POST without an id → CREATE a new record
Email confirmation applies to both paths.
READ-MODIFY-WRITE (the reason this page loads the record first): a boolean the
owner did not touch must keep the record's CURRENT value. Loading the record
syncs the checkboxes to it, so an untouched box sends what the record already
holds instead of flipping it to false. An edit is refused until the record has
been loaded and confirmed — a bare numeric id can no longer edit the wrong person.
============================================================================ */
var ODOO_EXPERTS_WEBHOOK =
"https://n8n.odoo4projects.com/webhook/ODOO4projects/odoo_experts";
var MULTI_FIELDS = ["odoo_versions"]; // comma-separated → array
// `languages` and `specialities` are COMMA-SEPARATED STRINGS on the wire
// ("English,German"), NOT arrays. `odoo_versions` stays an array.
var CSV_STRING_FIELDS = ["languages", "specialities"];
var BOOL_FIELDS = ["OCA_member", "is_individual"];
var NUM_FIELDS = ["odoo4projects_clients"];
// Country VALUE FORMAT — what the Country select sends to the webhook.
// "code" → the ISO 3166-1 alpha-2 code, e.g. "DE" (default; stable, exact-matchable)
// "name" → the full English name, e.g. "Germany" (switch to this line if the
// workflow expects names instead of codes)
// The select always DISPLAYS the English name; this only changes the SUBMITTED value.
var COUNTRY_VALUE_FORMAT = "code";
var TEXT_FIELDS = [
"Name", "email", "Focus", "description", "Country",
"Linkedin", "upwork", "fiverr", "github", "oca_profile", "Image"
];
var loadedRecord = null;
function fieldElements(form, name) {
return Array.prototype.slice.call(
form.querySelectorAll('[name="' + name.replace(/"/g, '\\"') + '"]')
);
}
/* The Country value to submit: the ISO code by default, the English name when
COUNTRY_VALUE_FORMAT is "name". Empty until the owner picks a country. */
function countryValue(form) {
var sel = form.querySelector('select[name="Country"]');
if (!sel || !sel.value) return "";
if (COUNTRY_VALUE_FORMAT === "name") {
var opt = sel.options[sel.selectedIndex];
return opt ? opt.textContent.trim() : "";
}
return sel.value.trim();
}
function setStatus(el, message, state) {
if (!el) return;
el.textContent = message;
if (state) el.setAttribute("data-state", state);
else el.removeAttribute("data-state");
}
/* The webhook casing is inconsistent; read a field's value by its known names. */
function recordField(rec, name) {
if (rec[name] !== undefined) return rec[name];
if (name === "Country" && rec.country !== undefined) return rec.country;
return undefined;
}
function joinMulti(value) {
if (value == null) return "";
if (Array.isArray(value)) return value.join(", ");
return String(value);
}
/* Copy a loaded record into the form, so an untouched field carries the record's
current value through to the write. */
function populate(rec) {
var form = document.getElementById("adminForm");
TEXT_FIELDS.forEach(function (name) {
var el = form.querySelector('[name="' + name + '"]');
if (!el) return;
var v = recordField(rec, name);
el.value = v == null ? "" : String(v);
});
MULTI_FIELDS.forEach(function (name) {
var el = form.querySelector('[name="' + name + '"]');
if (el) el.value = joinMulti(recordField(rec, name));
});
var clients = form.querySelector('[name="odoo4projects_clients"]');
if (clients) {
var n = rec.odoo4projects_clients;
clients.value = n == null ? "" : String(n);
}
BOOL_FIELDS.forEach(function (name) {
var box = form.querySelector('[name="' + name + '"]');
if (!box) return;
var val = rec[name] === true;
box.checked = val;
box.dataset.loaded = val ? "true" : "false";
});
}
function loadRecord() {
var form = document.getElementById("adminForm");
var info = document.getElementById("adLoadInfo");
var idRaw = form.querySelector('[name="id"]').value.trim();
loadedRecord = null;
if (!idRaw) {
setStatus(info, "Enter a record id first — or leave it blank to create a new record.", "err");
return;
}
setStatus(info, "Loading record " + idRaw + "…", null);
fetch(ODOO_EXPERTS_WEBHOOK, { method: "GET" })
.then(function (r) {
return r.json();
})
.then(function (payload) {
var list = Array.isArray(payload) ? payload : (payload && payload.data) || [];
var rec = null;
list.forEach(function (r) {
if (String(r.id) === String(idRaw)) rec = r;
});
if (!rec) {
setStatus(info, "No record with id " + idRaw + ". Nothing was loaded.", "err");
return;
}
loadedRecord = rec;
populate(rec);
var who = rec.Name ? rec.Name : "(no name)";
setStatus(
info,
"Loaded: " + who + " — id " + rec.id + ". Check this is the right person, then edit and save. Fields you leave alone keep their current value.",
"ok"
);
})
.catch(function () {
setStatus(info, "Could not read the record list. Try again.", "err");
});
}
function buildPayload(form, withId) {
var payload = {};
var names = [];
Array.prototype.forEach.call(form.querySelectorAll("[name]"), function (el) {
if (names.indexOf(el.name) === -1) names.push(el.name);
});
names.forEach(function (name) {
var els = fieldElements(form, name);
if (!els.length) return;
if (name === "id") {
if (!withId) return;
var raw = els[0].value.trim();
if (raw) payload.id = Number(raw);
return;
}
if (name === "Country") {
var countryVal = countryValue(form);
if (countryVal !== "") payload[name] = countryVal;
return;
}
if (MULTI_FIELDS.indexOf(name) !== -1) {
var text = els[0].value || "";
var list = text
.split(",")
.map(function (s) {
return s.trim();
})
.filter(Boolean);
if (list.length) payload[name] = list; // an empty box must not wipe the field
return;
}
if (CSV_STRING_FIELDS.indexOf(name) !== -1) {
// Normalise to the wire shape: trim each entry, drop blanks, re-join.
var norm = (els[0].value || "")
.split(",")
.map(function (s) {
return s.trim();
})
.filter(Boolean)
.join(",");
if (norm) payload[name] = norm; // an empty box must not wipe the field
return;
}
if (BOOL_FIELDS.indexOf(name) !== -1) {
var box = els[0];
if (withId && box.dataset.loaded !== undefined) {
// READ-MODIFY-WRITE: an untouched box keeps the record's current value.
var original = box.dataset.loaded === "true";
var now = !!box.checked;
payload[name] = now === original ? original : now;
} else {
payload[name] = !!box.checked;
}
return;
}
if (NUM_FIELDS.indexOf(name) !== -1) {
var n = els[0].value.trim();
if (n !== "") payload[name] = Number(n);
return;
}
var value = els[0].value.trim();
if (value !== "") payload[name] = value;
});
return payload;
}
function postToWebhook(payload) {
return fetch(ODOO_EXPERTS_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
}
(function init() {
var form = document.getElementById("adminForm");
if (!form) return;
var info = document.getElementById("adLoadInfo");
var status = document.getElementById("adminStatus");
document.getElementById("adLoadBtn").addEventListener("click", loadRecord);
// Changing the id after a load invalidates the confirmation.
form.querySelector('[name="id"]').addEventListener("input", function () {
if (loadedRecord && String(loadedRecord.id) !== this.value.trim()) {
loadedRecord = null;
setStatus(info, "The id changed — load the record again to confirm who you are editing.", "err");
}
});
form.addEventListener("submit", function (event) {
event.preventDefault();
if (!form.reportValidity()) return;
var idRaw = form.querySelector('[name="id"]').value.trim();
var hasId = !!idRaw;
if (hasId && (!loadedRecord || String(loadedRecord.id) !== idRaw)) {
setStatus(
status,
"Load the record first (Load this record) and confirm it is the right person. Nothing was saved.",
"err"
);
return;
}
var submit = form.querySelector('button[type="submit"]');
submit.disabled = true;
setStatus(status, "Saving…", null);
postToWebhook(buildPayload(form, true))
.then(function () {
setStatus(
status,
hasId
? "Saved. Check your inbox to confirm the change before it is applied."
: "New record sent. Check your inbox to confirm it before it goes live.",
"ok"
);
})
.catch(function () {
setStatus(status, "That did not save. Please try again.", "err");
})
.then(function () {
submit.disabled = false;
});
});
})();
</script>
</body>
</html>