<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Federico</title>
    <description>The latest articles on DEV Community by Federico (@fedesw5402).</description>
    <link>https://dev.to/fedesw5402</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3962406%2Faf1960db-2e3a-4bb2-a99c-a4ad2b30bb38.png</url>
      <title>DEV Community: Federico</title>
      <link>https://dev.to/fedesw5402</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fedesw5402"/>
    <language>en</language>
    <item>
      <title>From Google Sheets to WhatsApp and Monday.com: Building a WordPress Plugin That Connects Elementor Forms to an Automated Webhook Pipeline</title>
      <dc:creator>Federico</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:22:19 +0000</pubDate>
      <link>https://dev.to/fedesw5402/from-google-sheets-to-whatsapp-and-mondaycom-building-a-wordpress-plugin-that-connects-elementor-26ed</link>
      <guid>https://dev.to/fedesw5402/from-google-sheets-to-whatsapp-and-mondaycom-building-a-wordpress-plugin-that-connects-elementor-26ed</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Our company uses &lt;strong&gt;Monday.com&lt;/strong&gt; as our CRM. We also rely heavily on &lt;strong&gt;Elementor Pro&lt;/strong&gt; forms on our WordPress site to collect leads, registrations, and inquiries. The issue? Data lived in two separate worlds — form submissions landed in Google Sheets, and there was no automated way to push them into our CRM or notify our team in real time.&lt;/p&gt;

&lt;p&gt;Operators had to manually copy data from Google Sheets into Monday.com and then separately reach out to leads via phone or WhatsApp. It was slow, error-prone, and wasted hours every week.&lt;/p&gt;

&lt;p&gt;We needed a bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Infrastructure
&lt;/h2&gt;

&lt;p&gt;The solution relies on four components working together:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5l0niw11h4h115pi387d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5l0niw11h4h115pi387d.png" alt="WordPress Plugin Settings" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WordPress + Elementor Pro&lt;/strong&gt; — Forms are built visually with Elementor. The plugin hooks into Elementor's submission lifecycle to capture data before it hits any other handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom WordPress Plugin&lt;/strong&gt; — Developed from scratch. It intercepts form submissions, prepares the payload, and fires a POST request to an n8n webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n (self-hosted via Docker)&lt;/strong&gt; — Deployed on a private server. It receives the webhook, processes the data, and routes it through different branches based on the form configuration. n8n acts as the orchestration layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenWA (self-hosted via Docker)&lt;/strong&gt; — An open-source WhatsApp API gateway deployed as a separate Docker container. n8n talks to OpenWA's HTTP API to send templated messages automatically to the phone number submitted in the form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7eda0zt91hsnehrfvww8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7eda0zt91hsnehrfvww8.png" alt="n8n Workflow" width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every piece of the pipeline is &lt;strong&gt;self-hosted&lt;/strong&gt;. No third-party SaaS for the automation layer, no reliance on external WhatsApp business APIs — everything runs on our own infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Plugin Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Google Sheets Only
&lt;/h3&gt;

&lt;p&gt;The plugin started simple. When an Elementor form was submitted, the plugin would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hook into &lt;code&gt;elementor_pro/forms/new_record&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;Extract the form fields and their values&lt;/li&gt;
&lt;li&gt;Append a row to a Google Sheet via the Google Sheets API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This worked fine for a while. But soon the team needed more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Adding WhatsApp Notifications
&lt;/h3&gt;

&lt;p&gt;We added an &lt;strong&gt;n8n webhook&lt;/strong&gt; step. Now, on submission, the plugin fires a POST request to an n8n workflow endpoint. The workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receives the form data (name, email, phone, message, etc.)&lt;/li&gt;
&lt;li&gt;Calls OpenWA's REST API with the recipient's phone number and a pre-built message template&lt;/li&gt;
&lt;li&gt;OpenWA delivers the message via WhatsApp automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The beauty of this is that any Elementor form with a phone field can trigger a WhatsApp notification — no extra configuration needed beyond setting up the workflow once in n8n.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified example of the plugin's webhook call&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;elementor_pro/forms/new_record&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;$form_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$record&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;$webhook_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;n8n_webhook_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;wp_remote_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$webhook_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;$record&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_form_settings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fields&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;$form_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whatsapp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monday&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// configured per form&lt;/span&gt;
        &lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 3: Adding Monday.com Integration
&lt;/h3&gt;

&lt;p&gt;This was the most complex part. The requirement was: when a form is submitted, create or update an item in a specific Monday.com board, mapping form fields to board columns.&lt;/p&gt;

&lt;p&gt;The challenge? Monday.com boards can have dynamic columns, and the target board might not have the exact columns we needed. So the plugin's n8n workflow implements a &lt;strong&gt;self-healing column mapping&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check if the group exists&lt;/strong&gt; — If not, create a new group on the board&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check if the required columns exist&lt;/strong&gt; — Iterate over the form fields and, for each one, verify a matching column exists on the Monday.com board&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create missing columns&lt;/strong&gt; — If a column doesn't exist, create it with the appropriate type (text, date, phone, email, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map form fields to columns&lt;/strong&gt; — Build a column values payload matching the form data to the board columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the item&lt;/strong&gt; — Insert the item with all mapped values&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process was completely automated with n8n nodes using monday pre-built blocks&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Kept the Excel-Like Logic in the Plugin
&lt;/h2&gt;

&lt;p&gt;You might wonder: &lt;em&gt;shouldn't the column mapping logic live in n8n instead of the WordPress plugin?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ideally, yes. But there's a practical reason it stayed in the plugin. The original Google Sheets implementation already had a robust field-to-column mapping system baked in. When we added Monday.com, it was faster and safer to reuse that same mapping logic in the plugin rather than duplicating it in n8n.&lt;/p&gt;

&lt;p&gt;The plugin sends the already-structured payload to n8n, which acts more as a &lt;strong&gt;router and action executor&lt;/strong&gt; — it doesn't need to understand the form structure. This keeps the n8n workflows simpler and more generic: they just receive a well-defined JSON object and act on it.&lt;/p&gt;

&lt;p&gt;Could the logic be moved to n8n? Absolutely. But unless there's a compelling reason (like supporting multiple sites with different plugins), the current approach works well and keeps complexity centralized in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Flow
&lt;/h2&gt;

&lt;p&gt;Here's the complete lifecycle of a form submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User submits Elementor form
        │
        ▼
WordPress plugin hooks into elementor_pro/forms/new_record
        │
        ├──► Google Sheets (append row — original behavior)
        │
        └──► POST to n8n webhook
                  │
                  ├──► Branch: WhatsApp
                  │         └──► OpenWA API → WhatsApp message to lead
                  │
                  └──► Branch: Monday.com
                            ├──► Check/create group
                            ├──► Check/create columns
                            └──► Create item with mapped values
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each form in Elementor can be configured to enable WhatsApp notifications, Monday.com sync, or both — all from a simple settings panel in the plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted automation is liberating.&lt;/strong&gt; Running n8n and OpenWA on our own Docker infrastructure means no per-message fees, no rate limits imposed by a third party, and full control over the data pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bridge technologies are the real heroes.&lt;/strong&gt; WordPress doesn't need to know about Monday.com's GraphQL API. OpenWA doesn't need to know about Elementor forms. n8n sits in the middle and translates between all of them, and the plugin just speaks one language: HTTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start simple, then expand.&lt;/strong&gt; The Google Sheets integration was built in a day and delivered immediate value. WhatsApp and Monday.com came later, each adding a layer of automation that compounded the ROI. If we had tried to build the whole thing at once, we'd probably still be designing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Move the column mapping logic to n8n&lt;/strong&gt; for multi-site support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add two-way sync&lt;/strong&gt; — when an item updates in Monday.com, notify the WordPress user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template-based messages&lt;/strong&gt; — let admins edit WhatsApp message templates from the WordPress admin panel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling dashboard&lt;/strong&gt; — log failed webhook calls and retry with a queue system&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built with WordPress, Elementor Pro, n8n, OpenWA, and Monday.com — all glued together by a custom plugin and a lot of JSON.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>automation</category>
    </item>
    <item>
      <title>Building a Modular Panel-Based Management Software with React 19, TypeScript, and Dockview 5</title>
      <dc:creator>Federico</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:01:16 +0000</pubDate>
      <link>https://dev.to/fedesw5402/building-a-modular-panel-based-management-software-with-react-19-typescript-and-dockview-5-2h1l</link>
      <guid>https://dev.to/fedesw5402/building-a-modular-panel-based-management-software-with-react-19-typescript-and-dockview-5-2h1l</guid>
      <description>&lt;h1&gt;
  
  
  Building a Modular Panel-Based Management Software with React 19, TypeScript, and Dockview
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This article documents my experience designing and developing the frontend of a management software for a manufacturing company in the leather processing industry. The company had been using a legacy application developed over 20 years ago with outdated technologies — a slow, insecure Windows XP-era tool with a dated interface that had become increasingly difficult to maintain.&lt;/p&gt;

&lt;p&gt;The goal was clear: replace the old system with a modern, web-based solution without disrupting the established operational workflows that operators had been using for decades.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project at a Glance
&lt;/h2&gt;

&lt;p&gt;The client needed a fast, modern web application accessible from any browser. The key requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modular panel interface&lt;/strong&gt;: Operators needed to work with multiple modules simultaneously, just like a desktop environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern UI&lt;/strong&gt;: Clean, intuitive, and responsive with dark/light theme support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Low latency for loading and saving data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Two-factor authentication and granular permission management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i18n&lt;/strong&gt;: Full support for Italian and English&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard shortcuts&lt;/strong&gt;: Speed up frequent operations like create, edit, save, and cancel&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;The frontend was built entirely with modern tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI Framework&lt;/td&gt;
&lt;td&gt;React 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript 5.9 (strict mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build Tool&lt;/td&gt;
&lt;td&gt;Vite 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Router&lt;/td&gt;
&lt;td&gt;react-router v7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server State&lt;/td&gt;
&lt;td&gt;TanStack React Query 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client State&lt;/td&gt;
&lt;td&gt;Zustand 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP Client&lt;/td&gt;
&lt;td&gt;Axios 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI Library&lt;/td&gt;
&lt;td&gt;Material-UI (MUI) 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Panels&lt;/td&gt;
&lt;td&gt;Dockview 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms&lt;/td&gt;
&lt;td&gt;react-hook-form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tables&lt;/td&gt;
&lt;td&gt;material-react-table (MRT)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;i18n&lt;/td&gt;
&lt;td&gt;i18next + react-i18next&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notifications&lt;/td&gt;
&lt;td&gt;notistack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dates&lt;/td&gt;
&lt;td&gt;dayjs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The backend (handled by a colleague) was built with Symfony (PHP), MySQL, and RESTful APIs with JWT cookie-based authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: The Modular Panel System
&lt;/h2&gt;

&lt;p&gt;The core of the application is a &lt;strong&gt;Dockview&lt;/strong&gt; container that hosts all open panels — think of it as Visual Studio Code's tab system but for a business management app. Each panel corresponds to a specific business entity (contacts, batches, orders, etc.) and is built from three reusable generic components.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three Generics
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;GenericPanel&lt;/strong&gt; — The container component providing panel context and layout structure (list on top, form on bottom).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenericList&lt;/strong&gt; — A wrapper around material-react-table offering selection, sorting, and filtering out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenericForm&lt;/strong&gt; — The most complex component: it orchestrates the full CRUD lifecycle including button states, permissions, keyboard shortcuts, and confirmation dialogs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This abstraction was a game-changer for productivity. Instead of writing CRUD logic from scratch for each of the &lt;strong&gt;50+ panels&lt;/strong&gt;, I defined these three generics once, and each specific panel only declares the variable parts: table columns, form fields, API endpoint, and translations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── apps/
│   └── default/          # Entry point (main.tsx, provider hierarchy)
├── features/
│   ├── auth/             # Login, OTP, logout
│   ├── authz/            # Permissions (resource-action based)
│   ├── panels/           # Dockview panels (the heart of the app)
│   ├── password-reset/
│   ├── profile/
│   ├── routing/          # Router and guards
│   ├── search/           # Global search
│   ├── settings/
│   └── user/             # User and role management
└── shared/
    ├── api/              # Axios layer, useApi, query keys
    ├── ui/               # Reusable UI components
    ├── themes/           # MUI light/dark themes
    ├── helpers/          # Utilities (language detection)
    └── interfaces/       # Shared TypeScript interfaces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Factory Pattern for API Hooks
&lt;/h2&gt;

&lt;p&gt;One of the most impactful patterns I implemented was a &lt;strong&gt;Factory Method&lt;/strong&gt; for generating CRUD API hooks. The &lt;code&gt;createPanelApiFactory&lt;/code&gt; function automatically generates five TanStack Query hooks for any REST endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batchApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createPanelApi&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IBatch&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/batch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BATCH&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single declaration produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useGetList()&lt;/code&gt; → &lt;code&gt;GET /batch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useGetDetail(id)&lt;/code&gt; → &lt;code&gt;GET /batch/:id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usePost()&lt;/code&gt; → &lt;code&gt;POST /batch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usePut()&lt;/code&gt; → &lt;code&gt;PUT /batch/:id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useDelete()&lt;/code&gt; → &lt;code&gt;DELETE /batch/:id&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each hook handles caching, automatic cache invalidation after mutations, and error handling. The factory completely abstracts HTTP complexity so the developer can focus on business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolated State Management with Zustand
&lt;/h2&gt;

&lt;p&gt;A tricky architectural challenge: users can open &lt;strong&gt;multiple panels of the same type simultaneously&lt;/strong&gt;. Each instance must maintain its own independent state.&lt;/p&gt;

&lt;p&gt;The solution combines React Context with a Zustand factory. Every panel gets wrapped in its own React context, inside which a dynamic Zustand store is instantiated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Panel "Batches" #1 — context #1 → Zustand store #1 — { selectedId: 5, formEnabled: false }
Panel "Batches" #2 — context #2 → Zustand store #2 — { selectedId: 12, formEnabled: true }
Panel "Contacts" — context #3 → Zustand store #3 — { selectedId: 3, formEnabled: false }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees complete state isolation: modifying a form on one panel never interferes with another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Panel Communication
&lt;/h2&gt;

&lt;p&gt;Here's my favorite feature. Imagine an operator is in the &lt;strong&gt;Create Order&lt;/strong&gt; form and needs to select a contact that doesn't exist yet. They press Enter on the contact select field, and the system opens the &lt;strong&gt;Create Contact&lt;/strong&gt; panel automatically. Once they save the new contact, the ID flows back to the original order form — without the user having to re-select anything.&lt;/p&gt;

&lt;p&gt;This was implemented using &lt;strong&gt;React Query's cache as a communication bus&lt;/strong&gt;. The creating panel writes the newly created entity ID to a special query key (&lt;code&gt;LAST_CREATED&lt;/code&gt;), and the receiving panel subscribes to changes on that key via a &lt;code&gt;useSubscribePanel&lt;/code&gt; hook. No direct coupling between panels — the architecture stays modular and decoupled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keyboard Shortcuts
&lt;/h2&gt;

&lt;p&gt;To match the operators' muscle memory from the old system, I implemented keyboard shortcuts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;F4&lt;/td&gt;
&lt;td&gt;Enable editing on selected item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F9&lt;/td&gt;
&lt;td&gt;Create new item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F10&lt;/td&gt;
&lt;td&gt;Save current form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Esc&lt;/td&gt;
&lt;td&gt;Cancel operation / close floating panel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Shortcuts only fire on the focused panel to avoid conflicts when multiple panels are open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission System — Three Layers of Security
&lt;/h2&gt;

&lt;p&gt;The authorization system operates on three levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Component level&lt;/strong&gt; — Buttons and sections are conditionally rendered based on permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook level&lt;/strong&gt; — Permissions are verified before executing any operation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine level&lt;/strong&gt; — A centralized policy checker enforces rules globally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users belong to groups (Admin, Staff), and each group has specific permissions on business resources (contacts, production, orders, etc.). The menu and routes are filtered automatically — operators only see what they're allowed to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;After three months of development, the frontend reached:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;50+ functional panels&lt;/strong&gt; across 10 business areas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;35,000+ lines of TypeScript&lt;/strong&gt; code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 reusable generic components&lt;/strong&gt; (GenericPanel, GenericList, GenericForm)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full CRUD&lt;/strong&gt; on every entity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-panel communication&lt;/strong&gt; via React Query cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard shortcuts&lt;/strong&gt; for all CRUD operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Italian/English i18n&lt;/strong&gt; with real-time switching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Light/dark theme&lt;/strong&gt; persisted in user preferences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granular permission system&lt;/strong&gt; with three-level verification&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges Faced
&lt;/h2&gt;

&lt;p&gt;No project is without its struggles. Here are the main ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unclear requirements.&lt;/strong&gt; The client provided the legacy software without a structured analysis. Many features were discovered and implemented day by day. I often had to infer expected behavior by observing the old system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend coordination.&lt;/strong&gt; APIs frequently returned data in different formats than agreed upon, or threw unexpected errors that required last-minute fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domain complexity.&lt;/strong&gt; The leather processing industry has highly specific production workflows. Understanding batch management, tanning stages, product composition, and leather logistics was essential to building an interface that truly matches how operators work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical challenges.&lt;/strong&gt; Implementing isolated panel state, cross-panel data passing, concurrent cache synchronization, and multi-panel keyboard shortcuts — each problem pushed me to find creative solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The modular architecture makes it easy to extend. Future possibilities include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated tests&lt;/strong&gt; — Unit, integration, and E2E tests for robustness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance optimizations&lt;/strong&gt; — Virtual scrolling, lazy loading, query optimization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom layouts&lt;/strong&gt; — Let users save their own panel configurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI assistant&lt;/strong&gt; — A chatbot that can perform CRUD operations via natural language&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Working on this project was an incredible learning experience. Having full autonomy over the frontend architecture meant every decision — from library choices to design patterns — had a direct impact on the final product. The result is a production-ready application used by real operators every day, replacing a two-decade-old system with a modern, performant, and maintainable solution.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is based on my graduate thesis project for the Web Developer Full Stack program at ITS Digital Academy Mario Volpato.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>erp</category>
    </item>
  </channel>
</rss>
