updates on the ui changes and changed into doormile

This commit is contained in:
2026-08-24 18:10:47 +05:30
parent dcf9770ada
commit aa42d9ee81
409 changed files with 49950 additions and 66532 deletions

View File

@@ -0,0 +1,203 @@
import { createTenantCustomer } from '@/api/doormile/endpoints';
// ==============================|| Doormile AI — write actions ||============================== //
//
// The FIRST write capability in the assistant. Read the rules before adding
// another one.
//
// The contract (assistant/CLAUDE.md §4): an intent never mutates. It returns a
// PROPOSAL — the exact payload it would submit — and nothing reaches the API
// until the operator presses Create in the panel. Parsing, validation and
// execution are separated here so the proposal can be built, shown and
// discarded without any possibility of a request going out.
//
// Customer creation was chosen as the first write deliberately: it needs two
// required fields where an order needs fourteen, it has no CityGate pincode
// gate, no geocoding and no delivery slot, and a wrong record is an edit
// rather than a rider dispatched to the wrong address.
// "create a customer", "add new client" — an explicit verb + noun. Deliberately
// narrow: nothing here should fire on a question that merely mentions customers.
export const CREATE_CUSTOMER_TRIGGER = /\b(?:create|add|register|new)\s+(?:a\s+|an\s+|the\s+)?(?:new\s+)?(?:customer|client)\b/i;
// Words that are part of the instruction rather than the person's name.
const FILLER =
/\b(?:create|add|register|new|a|an|the|customer|client|named|called|with|phone|number|mobile|contact|no|email|id|please)\b/gi;
// Pulls what it can out of free text. Anything it can't find stays undefined
// and is asked for — never guessed.
export const parseCustomerDraft = (text) => {
const raw = String(text || '');
const email = (raw.match(/[\w.+-]+@[\w-]+\.[\w.]{2,}/) || [])[0];
// Exactly ten digits, standalone. A longer run is not a phone number and
// must not be silently truncated into one.
const phone = (raw.match(/(?<!\d)(\d{10})(?!\d)/) || [])[1];
// Whatever is left after removing the instruction, the phone and the email
// is the person's name.
let nameArea = raw;
if (email) nameArea = nameArea.replace(email, ' ');
if (phone) nameArea = nameArea.replace(phone, ' ');
const nameWords = nameArea
.replace(FILLER, ' ')
.replace(/[^A-Za-z .'-]/g, ' ')
.split(/\s+/)
.filter((w) => w.length > 1);
return {
firstname: nameWords[0],
lastname: nameWords.slice(1).join(' ') || undefined,
phone,
email
};
};
// Mirrors createCustomer.js's own checks — a name, and a phone of exactly ten
// digits. If the form would refuse it, the assistant refuses it too, rather
// than letting the server decide.
//
// No tenant check: a customer record carries no tenantid, and the documented
// POST body doesn't take one. The page's own `isStaffLogin && !tid` guard
// exists because its dropdown sends a speculative tenantid; the assistant
// doesn't send one at all.
export const validateCustomerDraft = (draft) => {
const missing = [];
if (!draft.firstname) missing.push('the customer’s name');
if (!draft.phone || !/^\d{10}$/.test(String(draft.phone))) missing.push('a 10-digit mobile number');
return { ok: missing.length === 0, missing };
};
// The exact body that will be POSTed.
//
// Documented body for POST /admin/tenantcustomers is
// { firstname, lastname, phone, email } — and a customer record carries NO
// tenantid, which is why the tenant field was removed. A real
// GET /admin/customers response does carry address, doorno, landmark, suburb,
// city, state, postcode, latitude and longitude, so those are sent on a
// best-effort basis: empty strings are dropped rather than sent as noise, and
// if the server ignores the rest nothing breaks.
const clean = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null && v !== ''));
export const buildCustomerPayload = (draft) =>
clean({
firstname: draft.firstname,
lastname: draft.lastname || '',
phone: draft.phone,
email: draft.email || '',
address: draft.address,
doorno: draft.doorno,
landmark: draft.landmark,
suburb: draft.suburb,
city: draft.city,
state: draft.state,
postcode: draft.postcode,
latitude: draft.latitude,
longitude: draft.longitude
});
// The ONLY function in the assistant that mutates anything. Called exclusively
// from the panel's confirm handler — never from an intent's run().
//
// ---- Which endpoint, and why -----------------------------------------------
//
// Writes to POST /admin/tenantcustomers. This is now settled by evidence, not
// by reading the docs:
//
// POST /admin/customers → 405 Method Not Allowed (confirmed live)
//
// 405 is the unambiguous answer: the route exists, and POST is not among its
// methods. express-console-api.md lists /admin/customers as GET + PATCH only,
// and the server agrees. It was pointed there briefly on explicit instruction;
// the live 405 settled it.
//
// The consequence, which the assistant states in its success message rather
// than leaving the operator to discover: a customer created here does NOT
// appear on the Customers page, because that page reads GET /admin/customers.
// On that resource a customer comes into existence as a side effect of a
// booking — POST /admin/expressbooking documents `customer_phone` as "creates a
// Guest customer if unknown". A B2C customer is, by design, someone who has
// ordered.
//
// To make created customers visible on that page, one of these has to happen:
// • the Customers page reads /admin/tenantcustomers (tried once, reverted —
// it changes what that page means, and its edit dialog would then PATCH a
// different store by id), or
// • the backend adds POST /admin/customers.
//
// The payload keeps the address fields. The documented body is
// { firstname, lastname, phone, email }; the rest are sent best-effort and
// ignored if unsupported.
export const executeCreateCustomer = async (payload) => {
const started = Date.now();
const call = {
name: 'createTenantCustomer',
target: 'POST /admin/tenantcustomers',
stats: Object.keys(payload).join(', ')
};
try {
const res = await createTenantCustomer(payload);
const duration = `${Date.now() - started}ms`;
// doormileApi mutations return the full envelope, so a `success: false`
// arrives as a resolved promise, not a rejection.
if (res && res.success === false) {
return {
ok: false,
message: res.message || 'The server rejected the customer.',
sourceCalls: [{ ...call, duration, status: 'error', errorMessage: res.message || 'Rejected' }]
};
}
const created = res?.data || res;
return {
ok: true,
id: created?.appcustomerid ?? created?.customerid ?? created?.id,
created,
message: 'Customer created.',
sourceCalls: [{ ...call, duration, status: 'complete', stats: `created id ${created?.appcustomerid ?? created?.id ?? '—'}` }]
};
} catch (err) {
// An HTTP failure used to throw straight past this function, and the panel
// printed a generic "could not be created" with the status thrown away —
// which is the one detail needed to tell "the route does not exist" apart
// from "the body was wrong". Report the status, the server's own message,
// and name the endpoint.
const duration = `${Date.now() - started}ms`;
// doormileAxios rejects with the response BODY, not the axios error, so
// `err.response` is undefined here — the status arrives as `err.httpStatus`.
const status = err.httpStatus ?? err.response?.status;
const serverMessage = err.message || err.error;
let message;
if (status === 405) {
// 405 is "the route exists but not this method" — a different fact from
// 404, and worth stating precisely so nobody re-tries the same call.
message = 'POST /admin/tenantcustomers returned 405 — this endpoint does not accept a create. Nothing was saved.';
} else if (status === 404) {
message = 'POST /admin/tenantcustomers returned 404 — that route is not on the server. Nothing was saved.';
} else if (status === 400 || status === 422) {
message = `The server rejected the details${serverMessage ? ` — ${serverMessage}` : ''}. Nothing was saved.`;
} else if (status) {
message = `POST /admin/tenantcustomers returned ${status}${serverMessage ? ` — ${serverMessage}` : ''}. Nothing was saved.`;
} else {
message = `${err.message || 'The request failed'} — the server could not be reached. Nothing was saved.`;
}
return {
ok: false,
status,
message,
sourceCalls: [
{
...call,
duration,
status: 'error',
errorMessage: `${status || 'network'}${serverMessage ? ` · ${serverMessage}` : ''}`
}
]
};
}
};