Amirali YaghoutiSenior Software Engineer

webapp Case study

CRM Operator PWA

Sales and support staff were working out of a desktop admin that assumed they were sitting down. The people who actually needed it were standing at a counter with a customer in front of them. Customers, meanwhile, had an account page that showed none of what the CRM knew about them. One modular WordPress CRM now serves both sides: an operator workspace built for the counter and a customer hub inside the WooCommerce account area.

The business problem

Sales and support work lived in separate places: conversations, order screens, notes, customer history and manual follow-up. Nobody could tell at a glance who owned a customer, what had already happened, or what needed attention next, and management could only review team activity by asking people for reports. A CRM that only works at a desk is a CRM that gets updated later, from memory, or not at all; the in-person side of the business needed order entry, customer lookup and follow-up handling to happen during the conversation itself, and the WordPress admin is not usable that way on a phone. On the customer side, stock WooCommerce gives My Account an order list, an address form and little else. The store tracked loyalty points, wallet balances and customer tiers inside the CRM, and none of that data had a screen the customer could see.

What I delivered

  • a2-crm-plugin, a modular WordPress CRM focused on in-person store operations, with operator inboxes, assignment flow, unread state, handoff and role-aware access, so a follow-up belongs to someone specific rather than to everyone.
  • Customer profile views that put behaviour, conversation history, order context and follow-up status in one place, plus lead, deal and task workflows for sales follow-up and management visibility.
  • In-person order forms with editable payment and source dropdowns that can be managed from inside the modal, so a new payment method does not require a developer, and a manual product insertion action for the cases the catalogue does not cover, which in a jewellery shop is more common than it sounds.
  • A customer-hub module rendered by a [customer_hub] shortcode with tabs for dashboard, orders, loyalty, wallet, recently viewed, profile and addresses, served by a REST API under the customer-hub/v1 namespace: /me/summary, /me/orders, /me/loyalty, /me/wallet, /me/recently-viewed, plus read and write routes for profile and addresses.
  • A points engine where paid orders earn points per fixed amount unit, and tiers (bronze, silver, gold, VIP) resolve from lifetime points at 0, 500, 1500 and 5000, backed by dedicated points and wallet ledger tables instead of balances stuffed into user meta. Recently-viewed tracking is capped at 20 products per customer with a 30-day expiry.
  • Internal reporting dashboards built on the same data, so the numbers management sees come from what operators actually recorded, alongside audit logs, CSV exports and backup/export flows.
  • Integration-ready modules for SMS and VoIP-style call context, kept behind provider adapters, with configuration in a gitignored config file and credentials preferred in environment variables or wp-config constants.

Technical approach

  • I built it as modules rather than one large plugin file. A CRM keeps gaining features, and the ones that arrive later must not force me to touch the ones that already work.
  • It runs on WordPress's own database layer and capability system rather than introducing a parallel auth model, so access control is the one the site already has. Custom tables exist where core has no model; core hooks are used where it does.
  • Vanilla JavaScript on the front end. A CRM used from a counter on variable connections is not the place for a framework payload.
  • I store the editable dropdown lists as data rather than defining them in code, which keeps ordinary operational change out of the deployment path.
  • Points apply on woocommerce_order_status_changed when an order reaches a paid status, and reverse automatically on cancelled, refunded, failed or trash. Every points write passes an idempotency check (a2_ch_points_entry_exists) so a replayed status change cannot double-credit a customer.
  • The hub records product views on template_redirect for logged-in users only, and a cleanup routine trims them so the table cannot grow unbounded.
  • Hub tabs are configuration in a settings option. I removed tickets, downloads and wishlist from the available list because their REST routes did not exist yet, and a permanently blank tab is worse than a missing one.
  • I split management reporting from the daily operator screens, so each group gets what it needs without cluttering the other's workflow, and I added audit logs and exports because an internal tool has to be explainable long after release.
  • I keep credentials out of the repository entirely: the sample config is committed, the real one ignored.

Result and evidence

Store staff record orders and follow-ups at the counter, in the conversation, rather than reconstructing them afterwards, and the team works from one structured layer for customer visibility, sales follow-up, reporting and workflow control instead of memory and scattered messages. The customer hub is shipped and running in production as part of the same plugin. I have not run a formal before-and-after measurement on it, and I will not dress that up; the evidence is that it has survived ongoing maintenance releases in daily use. The plugin is in maintenance and keeps iterating on in-person workflows.

Commercial value

A CRM is worth nothing unless staff keep it updated. Making it usable at the point of the interaction decides that, far more than any feature it has. On the other side of the counter, a customer who can check points, balance and tier in the account area has a reason to log in before buying, and the team no longer has to relay that information by hand. Together they turn WordPress from a public storefront into the working operating system of the commerce team.

implementation-brief.readme

Readable implementation brief

implementation_brief {
  project: "CRM Operator PWA"
  plugin: "a2-crm-plugin, modular WordPress CRM"
  stack: "PHP (WordPress + WooCommerce), vanilla JS, MySQL"
  focus: "in-person store operations at the counter"
  operator_side: "inbox, assignment, unread state, customer profile,
                  lead/deal/task workflows, in-person order form,
                  manual product insertion, reporting, audit log"
  operator_config: "payment and source dropdowns editable
                    from the modal, stored as data"
  customer_hub: "[customer_hub] shortcode; customer-hub/v1 ->
                 /me/summary /me/orders /me/loyalty /me/wallet
                 /me/recently-viewed /me/profile /me/addresses"
  loyalty: "tiers bronze/silver/gold/VIP at 0/500/1500/5000
            lifetime points; points and wallet ledger tables"
  hooks: "woocommerce_order_status_changed -> points apply/reverse;
          template_redirect -> view tracking"
  auth: "WordPress capabilities; no parallel auth model"
  secrets: "config.php gitignored; env or wp-config constants"
  status: "in production and maintenance; no formal
           before/after metrics recorded"
}

What this project shows

A plugin that is in maintenance and still shaped by daily use says more than a feature list does. It is the kind of internal system that sits close to revenue work: not a set of pages, but a tool sales and support staff open every day.

Letting operators manage their own dropdown values is a small decision that removes a standing stream of developer requests. I look for those. The hub shows the same restraint from the other direction: I cut features without a working backend from the tab list rather than shipping empty screens, and I built every write that touches customer value to be reversible and traceable.