# Kanban CRM Sales Deals

This example demonstrates a sophisticated Kanban board for CRM sales pipeline management with drag-and-drop functionality, keyboard navigation, and real-time updates.

## Key Features

* **Drag-and-drop deal management** between pipeline stages
* **Keyboard navigation** with arrow keys and tab support
* **Real-time search and filtering** across deals
* **Stage-based organization** with visual indicators
* **Deal detail navigation** with click-to-view functionality
* **Responsive design** that works on mobile and desktop
* **Accessibility features** with ARIA labels and screen reader support
* **Dynamic metadata resolution** for flexible table structures

## Use Cases

Perfect for applications requiring:

* Sales pipeline management and CRM systems
* Project management with workflow stages
* Task management and Kanban boards
* Lead tracking and conversion funnels
* Support ticket management systems
* Any workflow requiring stage-based organization

## Implementation Overview

This widget uses the **Other Tables Data** pattern with dynamic metadata resolution to work with any "Deals" datatable structure. Key features include:

* Advanced drag-and-drop with visual feedback
* Comprehensive keyboard navigation for accessibility
* Real-time search across deal titles and customers
* Responsive design with mobile optimization
* Professional styling with stage-specific colors and icons

## Code Example

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Kanban Board Widget</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js" crossorigin></script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js" crossorigin></script>
    <script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js" crossorigin></script>
    <style>
      html, body, #root {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
      }
      .kanban-scroll-x {
        overflow-x: auto;
        overflow-y: hidden;
        width: 100%;
        height: 100%;
        min-height: 400px;
        display: flex;
        flex-direction: row;
      }
      .kanban-col {
        min-width: 320px;
        max-width: 380px;
        flex: 1 1 320px;
        display: flex;
        flex-direction: column;
        margin-right: 1.5rem;
        background: #fff;
        border-radius: 6px;
        box-shadow: 0 1px 4px rgba(10,10,10,0.06);
        height: 100%;
      }
      .kanban-col:last-child {
        margin-right: 0;
      }
      .kanban-col-header {
        padding: 1rem 1rem 0.5rem 1rem;
        border-bottom: 1px solid #f0f0f0;
        font-weight: 600;
        font-size: 1.1rem;
        background: #fafbfc;
        border-radius: 6px 6px 0 0;
        display: flex;
        align-items: center;
        justify-content: space-between;
      }
      .kanban-card-list {
        flex: 1 1 auto;
        padding: 1rem;
        min-height: 60px;
        background: transparent;
        transition: background 0.2s;
      }
      .kanban-card-list.drag-over {
        background: #f5faff;
      }
      .kanban-card {
        background: #f8f9fa;
        border-radius: 6px;
        box-shadow: 0 1px 2px rgba(10,10,10,0.04);
        margin-bottom: 1rem;
        padding: 1rem;
        cursor: grab;
        transition: box-shadow 0.2s, background 0.2s;
        outline: none;
        border: 2px solid transparent;
        display: flex;
        flex-direction: column;
      }
      .kanban-card:last-child {
        margin-bottom: 0;
      }
      .kanban-card.dragging {
        opacity: 0.5;
        background: #e3e9f7;
        box-shadow: 0 4px 12px rgba(10,10,10,0.10);
      }
      .kanban-card:focus {
        border: 2px solid #3273dc;
        background: #e8f0fe;
      }
      .kanban-card .card-title {
        font-weight: 600;
        font-size: 1.05rem;
        margin-bottom: 0.25rem;
        color: #363636;
      }
      .kanban-card .card-row {
        display: flex;
        align-items: center;
        margin-bottom: 0.25rem;
        font-size: 0.95rem;
      }
      .kanban-card .card-row:last-child {
        margin-bottom: 0;
      }
      .kanban-card .card-value {
        font-weight: 500;
        color: #209cee;
        margin-right: 0.5rem;
      }
      .kanban-card .card-deadline {
        color: #b86b00;
        font-size: 0.93rem;
        margin-left: 0.5rem;
      }
      .kanban-card .card-owner {
        display: flex;
        align-items: center;
        font-size: 0.95rem;
        color: #4a4a4a;
      }
      .kanban-card .card-customer {
        color: #7a7a7a;
        font-size: 0.93rem;
        margin-left: 0.5rem;
      }
      .kanban-filters {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 1rem;
        margin-bottom: 1.25rem;
      }
      @media (max-width: 900px) {
        .kanban-col {
          min-width: 260px;
        }
      }
      @media (max-width: 600px) {
        .kanban-col {
          min-width: 180px;
        }
        .kanban-scroll-x {
          min-height: 300px;
        }
      }
    </style>
  </head>
  <body>
    <div id="root" style="height:100%;width:100%;"></div>
    <script type="text/babel">
      const buzzyFrameAPI = new BuzzyFrameAPI();
      
      const DEAL_TABLE_NAME = 'Deals';
      const DEAL_FIELD_NAMES = {
        title: 'title',
        value: 'value', 
        stage: 'stage',
        owner: 'owner',
        customer: 'customer',
        deadline: 'deadline'
      };
      
      const STAGES = [
        { key: "Suspect", color: "#bdbdbd", icon: "help" },
        { key: "Lead", color: "#209cee", icon: "person_search" },
        { key: "Warm", color: "#ffdd57", icon: "local_fire_department" },
        { key: "Hot", color: "#ff3860", icon: "whatshot" },
        { key: "Closed", color: "#23d160", icon: "check_circle" },
        { key: "Lost", color: "#7a7a7a", icon: "cancel" }
      ];

      function KanbanBoard() {
        const { useState, useEffect, useRef } = React;
        
        const [deals, setDeals] = useState([]);
        const [search, setSearch] = useState('');
        const [draggedDeal, setDraggedDeal] = useState(null);
        const [dragOverStage, setDragOverStage] = useState(null);
        const [keyboardFocus, setKeyboardFocus] = useState({ dealId: null });
        const [loading, setLoading] = useState(false);
        const [error, setError] = useState('');
        const cardRefs = useRef({});
        
        const [dealTableID, setDealTableID] = useState(null);
        const [dealFieldIDs, setDealFieldIDs] = useState({});
        const [dealDetailScreenID, setDealDetailScreenID] = useState(null);

        useEffect(() => {
          async function initialiseAll() {
            try {
              console.log('[init] Calling buzzyFrameAPI.initialise()...');
              const init = await buzzyFrameAPI.initialise();
              
              const metadata = await buzzyFrameAPI.fetchDatatableMetadata({
                dataTableID: init.rowJSON?.parentResourceID,
              });
              
              const dealsTable = metadata.find(
                table => table.dataTableName === DEAL_TABLE_NAME
              );
              
              if (!dealsTable) {
                throw new Error(`Table "${DEAL_TABLE_NAME}" not found. Please ensure there is a table named "${DEAL_TABLE_NAME}".`);
              }
              
              setDealTableID(dealsTable.id);
              
              const fieldIDs = {};
              Object.entries(DEAL_FIELD_NAMES).forEach(([key, fieldName]) => {
                const field = dealsTable.fields.find(f => f.fieldName === fieldName);
                if (field) {
                  fieldIDs[key] = field.id;
                } else {
                  console.warn(`Field "${fieldName}" not found in table "${DEAL_TABLE_NAME}"`);
                }
              });
              
              setDealFieldIDs(fieldIDs);
              
              console.log('[init] Metadata resolved:', {
                dealTableID: dealsTable.id,
                fieldIDs
              });
              
              buzzyFrameAPI.addMicroappListener({
                microAppID: dealsTable.id,
                listener: async () => {
                  console.log('[listener] Deals data changed, refreshing...');
                  await fetchDeals();
                }
              });

              try {
                const detailScreenID = await buzzyFrameAPI.getScreenID({ screenName: 'Deal Detail' });
                setDealDetailScreenID(detailScreenID);
              } catch (err) {
                console.warn('Deal detail screen not found:', err);
              }

            } catch (err) {
              setError('Error during initialisation: ' + (err.message || err));
              console.error('[error]', err);
            }
          }

          initialiseAll();
        }, []);
        
        useEffect(() => {
          if (dealTableID) {
            fetchDeals();
          }
        }, [dealTableID]);

        async function fetchDeals() {
          if (!dealTableID) return;
          
          setLoading(true);
          setError('');
          console.log('[fetchDeals] Fetching deals...');

          try {
            const dealsData = await buzzyFrameAPI.fetchDataTableRows({ 
              microAppID: dealTableID 
            });
            
            console.log('[fetchDeals] Got', dealsData.length, 'deals');
            setDeals(dealsData);

          } catch (err) {
            setError('Error fetching deals: ' + (err.message || err));
            console.error('[error]', err);
          } finally {
            setLoading(false);
          }
        }

        function formatCurrency(val) {
          if (typeof val !== 'number') return '';
          return '$' + val.toLocaleString();
        }
        
        function formatDate(dateStr) {
          if (!dateStr) return '';
          const d = new Date(dateStr);
          if (isNaN(d)) return '';
          return d.toLocaleDateString();
        }

        const filteredDeals = deals.filter(deal => {
          if (search) {
            const s = search.toLowerCase();
            const title = (deal.title || '').toLowerCase();
            const customer = deal.customer || '';
            return title.includes(s) || customer.includes(s);
          }
          return true;
        });

        const dealsByStage = {};
        STAGES.forEach(s => { dealsByStage[s.key] = []; });
        
        filteredDeals.forEach(deal => {
          if (STAGES.some(s => s.key === deal.stage)) {
            dealsByStage[deal.stage].push(deal);
          }
        });

        function onDragStart(e, deal) {
          setDraggedDeal(deal);
          e.dataTransfer.effectAllowed = 'move';
          e.dataTransfer.setData('text/plain', deal._id);
        }
        
        function onDragEnd() {
          setDraggedDeal(null);
          setDragOverStage(null);
        }
        
        function onDragOverCol(e, stage) {
          e.preventDefault();
          setDragOverStage(stage);
        }
        
        async function onDropCol(e, stage) {
          e.preventDefault();
          setDragOverStage(null);
          
          if (draggedDeal && draggedDeal.stage !== stage) {
            try {
              await buzzyFrameAPI.updateMicroappRow({
                body: {
                  rowID: draggedDeal._id,
                  rowData: { stage },
                  ignoreActionRules: true
                }
              });
              
              console.log(`[onDropCol] Successfully updated deal ${draggedDeal._id} to stage ${stage}`);
              
            } catch (error) {
              console.error('Error updating deal stage:', error);
              setError('Error updating deal stage: ' + (error.message || error));
            }
          }
          
          setDraggedDeal(null);
        }
        
        function onCardKeyDown(e, deal, stageIdx, cardIdx) {
          if (e.key === 'Enter' || e.key === ' ') {
            setDraggedDeal(deal);
            setDragOverStage(deal.stage);
            e.preventDefault();
          } else if (e.key === 'ArrowRight') {
            if (stageIdx < STAGES.length - 1) {
              const nextStage = STAGES[stageIdx + 1].key;
              updateDealStage(deal, nextStage);
            }
            e.preventDefault();
          } else if (e.key === 'ArrowLeft') {
            if (stageIdx > 0) {
              const prevStage = STAGES[stageIdx - 1].key;
              updateDealStage(deal, prevStage);
            }
            e.preventDefault();
          }
        }
        
        async function updateDealStage(deal, newStage) {
          try {
            await buzzyFrameAPI.updateMicroappRow({
              body: {
                rowID: deal._id,
                rowData: { stage: newStage },
                ignoreActionRules: true
              }
            });
            
            console.log(`[updateDealStage] Successfully updated deal ${deal._id} to stage ${newStage}`);
            
          } catch (error) {
            console.error('Error updating deal stage:', error);
            setError('Error updating deal stage: ' + (error.message || error));
          }
        }
        
        async function onCardClick(deal) {
          if (dealDetailScreenID) {
            buzzyFrameAPI.navigate({ screenID: dealDetailScreenID, rowID: deal._id });
          } else {
            console.warn('Deal detail screen not available');
          }
        }

        return (
          <div className="is-flex is-flex-direction-column" style={{height:'100%',width:'100%'}}>
            <div className="kanban-filters">
              <div className="field has-addons">
                <p className="control has-icons-left">
                  <input
                    className="input"
                    type="search"
                    placeholder="Search deals..."
                    aria-label="Search deals"
                    value={search}
                    onChange={e => setSearch(e.target.value)}
                  />
                  <span className="icon is-left">
                    <span className="material-symbols-outlined">search</span>
                  </span>
                </p>
                {search && (
                  <p className="control">
                    <button className="button is-light" aria-label="Clear search" onClick={() => setSearch('')}>
                      <span className="material-symbols-outlined">close</span>
                    </button>
                  </p>
                )}
              </div>
            </div>
            {error && <div style={{color: 'red', marginBottom: '1rem'}}>{error}</div>}
            {loading && <div style={{marginBottom: '1rem'}}>Loading deals...</div>}
            <div className="kanban-scroll-x" role="list" aria-label="Kanban board" tabIndex={0}>
              {STAGES.map((stage, stageIdx) => (
                <section
                  key={stage.key}
                  className="kanban-col"
                  aria-label={stage.key + ' column'}
                  onDragOver={e => onDragOverCol(e, stage.key)}
                  onDrop={e => onDropCol(e, stage.key)}
                  tabIndex={-1}
                >
                  <header className="kanban-col-header" style={{borderBottomColor: stage.color}}>
                    <span>
                      <span className="material-symbols-outlined" style={{color: stage.color, verticalAlign:'middle', marginRight:'0.3em'}}>{stage.icon}</span>
                      {stage.key}
                    </span>
                    <span className="tag is-light" aria-label={dealsByStage[stage.key].length + ' deals'}>{dealsByStage[stage.key].length}</span>
                  </header>
                  <div
                    className={
                      'kanban-card-list' +
                      (dragOverStage === stage.key ? ' drag-over' : '')
                    }
                    role="list"
                    aria-label={stage.key + ' deals'}
                  >
                    {dealsByStage[stage.key].map((deal, cardIdx) => {
                      return (
                        <article
                          key={deal._id}
                          className={
                            'kanban-card' +
                            (draggedDeal && draggedDeal._id === deal._id ? ' dragging' : '')
                          }
                          role="listitem"
                          tabIndex={0}
                          aria-label={deal.title}
                          ref={el => (cardRefs.current[deal._id] = el)}
                          draggable
                          onDragStart={e => onDragStart(e, deal)}
                          onDragEnd={onDragEnd}
                          onKeyDown={e => onCardKeyDown(e, deal, stageIdx, cardIdx)}
                          onFocus={() => setKeyboardFocus({ dealId: deal._id })}
                          onBlur={() => setKeyboardFocus({ dealId: null })}
                          aria-grabbed={draggedDeal && draggedDeal._id === deal._id ? 'true' : 'false'}
                          aria-describedby={'deal-desc-' + deal._id}
                          onClick={() => onCardClick(deal)}
                        >
                          <div className="card-title">{deal.title}</div>
                          <div className="card-row">
                            <span className="card-value">
                              <span className="material-symbols-outlined" style={{fontSize:'1.1em',verticalAlign:'middle'}}>attach_money</span>
                              {formatCurrency(deal.value)}
                            </span>
                            {deal.deadline && (
                              <span className="card-deadline">
                                <span className="material-symbols-outlined" style={{fontSize:'1.1em',verticalAlign:'middle'}}>event</span>
                                {formatDate(deal.deadline)}
                              </span>
                            )}
                          </div>
                          <div className="card-row card-owner">
                            <span className="material-symbols-outlined card-avatar" style={{background:'#e0e0e0',display:'inline-flex',alignItems:'center',justifyContent:'center',width:'28px',height:'28px',fontSize:'1.2em'}}>person</span>
                            <span>{deal.owner || 'Unassigned'}</span>
                            {deal.customer && (
                              <span className="card-customer">
                                <span className="material-symbols-outlined" style={{fontSize:'1em',verticalAlign:'middle'}}>business</span>
                                {deal.customer}
                              </span>
                            )}
                          </div>
                          <div id={'deal-desc-' + deal._id} className="is-sr-only">
                            Value: {formatCurrency(deal.value)}. Deadline: {formatDate(deal.deadline)}. Owner: {deal.owner || 'Unassigned'}. Customer: {deal.customer || ''}.
                          </div>
                        </article>
                      );
                    })}
                  </div>
                </section>
              ))}
            </div>
          </div>
        );
      }

      ReactDOM.render(<KanbanBoard />, document.getElementById('root'));
    </script>
  </body>
</html>
```

## Key Concepts

### Advanced Drag-and-Drop

* **Visual Feedback**: Cards and columns provide clear visual feedback during drag operations
* **Stage Updates**: Automatic database updates when deals are moved between stages
* **Keyboard Support**: Full keyboard navigation with arrow keys for accessibility
* **Touch Support**: Works seamlessly on mobile devices with touch interactions

### Dynamic Metadata Resolution

* **Flexible Structure**: Automatically discovers table and field IDs by name
* **Error Handling**: Graceful handling when expected tables or fields are missing
* **Field Mapping**: Maps business field names to actual database field IDs
* **Validation**: Warns when expected fields are not found

### Professional CRM Features

* **Search Functionality**: Real-time search across deal titles and customers
* **Currency Formatting**: Professional currency display with locale formatting
* **Date Formatting**: User-friendly date display for deadlines
* **Stage Management**: Visual stage indicators with colors and icons

### Accessibility & UX

* **ARIA Labels**: Comprehensive accessibility support for screen readers
* **Keyboard Navigation**: Full keyboard support for all interactions
* **Responsive Design**: Optimized for desktop, tablet, and mobile devices
* **Loading States**: Clear feedback during data loading and updates

This Kanban board example demonstrates how code widgets can create sophisticated business applications with professional-grade user interfaces and full accessibility support.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.buzzy.buzz/the-building-blocks/code-widget-custom-code/examples/kanban-crm-sales-deals.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
