updates on the ui changes and the ai bot implementation

This commit is contained in:
2026-08-19 11:19:51 +05:30
parent 5284fc75bc
commit bdb21766f2
27 changed files with 2607 additions and 278 deletions

View File

@@ -0,0 +1,192 @@
import { createAdminCustomer } from 'pages/api/doormileApi';
// ==============================|| 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/customers — the same resource the Customers page
// reads, so a created customer shows up there immediately.
//
// Note for anyone reading express-console-api.md: that document lists
// /admin/customers as GET + PATCH only, and describes a customer there as
// something created by a booking (POST /admin/expressbooking documents
// `customer_phone` as "creates a Guest customer if unknown"). This call was
// pointed here deliberately anyway. If the route turns out not to exist the
// server answers 404/405 and the assistant shows that verbatim alongside the
// endpoint name, so the failure is loud rather than silent.
//
// The previous target was POST /admin/tenantcustomers, which succeeded but
// wrote to a different store with its own id sequence — a customer created
// that way came back as id 2 while the Customers list ran to id 44, and was
// invisible on that page. That is the bug this change fixes.
//
// The payload already mirrors this resource's own GET shape (firstname,
// lastname, phone, email, address, doorno, landmark, suburb, city, state,
// postcode, latitude, longitude), so nothing else had to move.
export const executeCreateCustomer = async (payload) => {
const started = Date.now();
const call = {
name: 'createAdminCustomer',
target: 'POST /admin/customers',
stats: Object.keys(payload).join(', ')
};
try {
const res = await createAdminCustomer(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`;
const status = err.response?.status;
const serverMessage = err.response?.data?.message || err.response?.data?.error;
let message;
if (status === 404 || status === 405) {
// express-console-api.md documents /admin/customers as GET + PATCH only.
// If the server agrees, say so plainly rather than implying the operator
// typed something wrong.
message =
`POST /admin/customers returned ${status} — that route does not exist on the server. ` +
'A customer there is created by a booking, or via POST /admin/tenantcustomers.';
} else if (status) {
message = `POST /admin/customers 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}` : ''}`
}
]
};
}
};