CAPSTONE 1 OF 3

Enterprise HR Dashboard

A complete production application combining Phases 1–9 — BorderContainer layout, dgrid employee list, Dialog CRUD forms, Observable store, pub/sub architecture, i18n, and a build profile

BorderContainer dgrid Observable Store Pub/Sub i18n EN/FR Build Profile
What this capstone covers: Every concept from Phases 1–9 appears in this app. Read each section alongside its referenced phase for the deep-dive explanation. This file focuses on how the pieces connect — the architecture decisions behind the code.

C1.1 — App Overview & Architecture

🗂�?
5-Region Layout

BorderContainer with top navbar, left filter panel, center tab area, right detail panel, bottom status bar

📋
Live Employee Grid

dgrid OnDemandGrid with row selection, inline salary editing, department badge renderer, bulk delete

�?
CRUD Dialogs

Add/Edit employee Dialog with ValidationTextBox form, full validation, server-round-trip simulation

🔌
Zero Coupling

6 independent modules communicate only via dojo/topic — any module can be replaced without touching others

�?
i18n EN/FR

All strings in NLS bundles, dates and numbers locale-formatted, runtime locale switcher in header

Production Build

3-layer build profile reduces 180+ requests to 3, with Closure optimization and inlined templates

Topic Map

// Who publishes → topic → who subscribes FilterPanel filter/changed EmployeeList EmployeeList employee/selected DetailPanel, StatusBar EditForm employee/saved EmployeeList, DetailPanel, StatusBar EditForm employee/deleted EmployeeList, DetailPanel, StatusBar boot.js app/ready all modules request/notify auth/sessionExpired LoginOverlay

C1.2 — Module Structure Phase 1 – AMD

hrapp/
├── boot.js                �? AMD entry: wires layout + subscribes top-level topics
├── topics.js              �? All topic name constants (no magic strings)
├── nls/
│   ├── messages.js        �? EN root bundle
│   └── fr/messages.js     �? FR overrides
├── api/
│   └── EmployeeAPI.js     �? dojo/request wrapper for REST calls
├── stores/
│   └── EmployeeStore.js   �? Observable(TrackableMemory) singleton
├── layout/
│   ├── AppShell.js        �? BorderContainer + all regions
│   ├── HeaderBar.js       �? Top region: logo + locale switcher
│   └── StatusBar.js       �? Bottom region: subscribes to employee/* topics
├── views/
│   ├── EmployeeList.js    �? dgrid OnDemandGrid + selector + bulk delete
│   ├── DetailPanel.js     �? Right panel: subscribes to employee/selected
│   └── ReportsView.js     �? Reports tab placeholder
├── dialogs/
│   └── EmployeeDialog.js  �? Add/Edit dialog with Form + ValidationTextBox
├── widgets/
│   └── FilterPanel.js     �? Left sidebar: dept/status filter selects
└── profiles/
    └── hrapp.profile.js   �? Build profile

C1.3 — Store & Data Layer Phase 3 – Observable

// hrapp/stores/EmployeeStore.js
// Singleton store — shared by all modules via AMD require()
define([
  "dojo/store/Memory",
  "dojo/store/Observable",
  "dojo/_base/declare"
], function(Memory, Observable, declare) {

  var _store = null;

  return {
    // Lazy init — create once, return same instance always
    get: function() {
      if (!_store) {
        _store = Observable(new Memory({
          idProperty: "id",
          data: [
            { id:1, name:"Alice Chen",   dept:"Engineering", salary:95000, active:true,  hireDate:"2019-03-15" },
            { id:2, name:"Bob Smith",    dept:"Marketing",   salary:72000, active:true,  hireDate:"2020-07-01" },
            { id:3, name:"Carol Davis",  dept:"Engineering", salary:105000,active:false, hireDate:"2017-11-20" },
            { id:4, name:"Dave Wilson",  dept:"HR",          salary:68000, active:true,  hireDate:"2021-02-08" },
            { id:5, name:"Eve Martinez", dept:"Engineering", salary:88000, active:true,  hireDate:"2022-05-14" },
            { id:6, name:"Frank Lee",    dept:"Marketing",   salary:76000, active:true,  hireDate:"2020-09-30" },
            { id:7, name:"Grace Kim",    dept:"Finance",     salary:92000, active:false, hireDate:"2018-06-12" },
            { id:8, name:"Henry Brown",  dept:"Engineering", salary:112000,active:true,  hireDate:"2016-01-22" }
          ]
        }));
      }
      return _store;
    },

    // Convenience query methods
    getAll:        function()     { return this.get().query({}); },
    getActive:     function()     { return this.get().query({ active: true }); },
    getByDept:     function(dept) { return this.get().query({ dept: dept }); },
    getEmployee:   function(id)   { return this.get().get(id); },

    // Mutations — store is Observable so all observe() listeners update automatically
    save: function(emp) {
      return emp.id ? this.get().put(emp) : this.get().add(emp);
    },
    remove: function(id) { return this.get().remove(id); }
  };
});

C1.4 — Layout Shell Phase 4 – BorderContainer

// hrapp/layout/AppShell.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dijit/layout/BorderContainer",
  "dijit/layout/TabContainer",
  "dijit/layout/ContentPane",
  "hrapp/layout/HeaderBar",
  "hrapp/layout/StatusBar",
  "hrapp/widgets/FilterPanel",
  "hrapp/views/EmployeeList",
  "hrapp/views/DetailPanel",
  "hrapp/views/ReportsView"
], function(
  declare, lang, _Widget,
  BorderContainer, TabContainer, ContentPane,
  HeaderBar, StatusBar, FilterPanel,
  EmployeeList, DetailPanel, ReportsView
) {
  return declare([_Widget], {

    buildRendering: function() {
      // html, body { height:100%; overflow:hidden; margin:0; }
      this.domNode = document.body;
    },

    postCreate: function() {
      this.inherited(arguments);
      var self = this;

      // ── Outer shell ──────────────────────────────────────────
      var shell = new BorderContainer({
        design: "headline",
        style:  "width:100%; height:100vh;"
      });

      // ── Header (top) ─────────────────────────────────────────
      var header = new HeaderBar({ region: "top", style: "height:56px;" });

      // ── Left sidebar ─────────────────────────────────────────
      var sidebar = new ContentPane({
        region:   "left",
        style:    "width:240px;",
        splitter: true,
        minSize:  160,
        content:  new FilterPanel()
      });

      // ── Center: tabs ─────────────────────────────────────────
      var tabs = new TabContainer({
        region:      "center",
        tabPosition: "top"
      });

      var empPane = new ContentPane({
        title:   "Employees",
        content: new EmployeeList()
      });

      var rptPane = new ContentPane({
        title:   "Reports",
        content: new ReportsView()
      });

      tabs.addChild(empPane);
      tabs.addChild(rptPane);

      // ── Right: detail panel ───────────────────────────────────
      var detailPane = new ContentPane({
        region: "right",
        style:  "width:300px;",
        splitter: true
      });
      var detail = new DetailPanel();
      detail.placeAt(detailPane.containerNode);

      // ── Bottom: status bar ────────────────────────────────────
      var status = new StatusBar({
        region: "bottom",
        style:  "height:28px;"
      });

      shell.addChild(header);
      shell.addChild(sidebar);
      shell.addChild(tabs);
      shell.addChild(detailPane);
      shell.addChild(status);

      shell.placeAt(document.body);
      shell.startup();
    }
  });
});

C1.5 — Employee List Phase 7 – dgrid Phase 6 – Pub/Sub

// hrapp/views/EmployeeList.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dgrid/OnDemandGrid",
  "dgrid/editor",
  "dgrid/selector",
  "dgrid/Selection",
  "dstore/Memory",
  "dstore/Trackable",
  "dojo/topic",
  "dojo/dom-construct",
  "dojo/dom-attr",
  "dojo/on",
  "dojo/date/locale",
  "dojo/number",
  "dojo/i18n!hrapp/nls/messages",
  "hrapp/topics",
  "hrapp/stores/EmployeeStore"
], function(
  declare, lang, _Widget,
  OnDemandGrid, editor, selector, Selection,
  Memory, Trackable,
  topic, domConstruct, domAttr, on,
  dateLocale, numberFmt,
  strings, T, EmployeeStore
) {

  var TrackableMemory = Memory.createSubclass([Trackable]);
  var SelectableGrid  = declare([OnDemandGrid, Selection]);

  return declare([_Widget], {

    postCreate: function() {
      this.inherited(arguments);
      var self = this;
      var store = EmployeeStore.get();

      // ── Toolbar ───────────────────────────────────────────────
      var toolbar = domConstruct.create("div", { className:"emp-toolbar" }, this.domNode);

      var addBtn = domConstruct.create("button", {
        textContent: strings.addEmployee, className: "btn-primary"
      }, toolbar);

      this._deleteBtn = domConstruct.create("button", {
        textContent: strings.deleteSelected, disabled: true, className: "btn-danger"
      }, toolbar);

      // ── Grid ─────────────────────────────────────────────────
      var gridNode = domConstruct.create("div", { style:"height:calc(100% - 48px)" }, this.domNode);

      this._grid = new SelectableGrid({
        collection:    store,
        selectionMode: "multiple",
        sort:          [{ property: "name", descending: false }],

        columns: [
          selector({ label: "" }),
          {
            field: "name", label: strings.name, sortable: true,
            renderCell: function(obj) {
              var w = domConstruct.create("div", { style:"display:flex;align-items:center;gap:8px" });
              var ini = obj.name.split(" ").map(function(x){ return x[0]; }).join("");
              domConstruct.create("span", {
                textContent: ini,
                style: "width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#22d3ee);" +
                       "color:#fff;font-size:.72rem;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;"
              }, w);
              domConstruct.create("span", { textContent: obj.name }, w);
              return w;
            }
          },
          { field:"dept", label:strings.department, sortable:true,
            renderCell: function(obj) {
              var colors = { Engineering:"cyan", Marketing:"purple", HR:"pink", Finance:"green" };
              return domConstruct.create("span", {
                textContent: obj.dept,
                className:   "dept-badge dept-" + (colors[obj.dept] || "default")
              });
            }
          },
          editor({ field:"salary", label:strings.salary,
            formatter: function(v){ return numberFmt.format(v, { type:"currency", currency:"USD" }); },
            editor:"number", autoSave:false, editOn:"dblclick", sortable:true
          }),
          { field:"hireDate", label:strings.hireDate, sortable:true,
            formatter: function(v){ return v ? dateLocale.format(new Date(v), { datePattern:"medium" }) : "—"; }
          },
          { field:"active", label:strings.status, sortable:false,
            renderCell: function(obj) {
              return domConstruct.create("span", {
                textContent: obj.active ? strings.active : strings.inactive,
                style: "color:" + (obj.active ? "#34d399" : "#fbbf24")
              });
            }
          },
          { label: strings.actions, sortable: false,
            renderCell: function(obj) {
              var wrap = domConstruct.create("span");
              var editBtn = domConstruct.create("button",
                { textContent:strings.edit, "data-id":obj.id, className:"grid-btn" }, wrap);
              var delBtn  = domConstruct.create("button",
                { textContent:strings.delete, "data-id":obj.id, className:"grid-btn grid-btn--red" }, wrap);

              on(editBtn, "click", function(e) {
                e.stopPropagation();
                topic.publish(T.EMPLOYEE_EDIT_REQUESTED, { id: +this.dataset.id });
              });
              on(delBtn, "click", function(e) {
                e.stopPropagation();
                if (confirm(strings.confirmDelete.replace("${name}", obj.name))) {
                  EmployeeStore.remove(+this.dataset.id);
                  topic.publish(T.EMPLOYEE_DELETED, { id: +this.dataset.id });
                }
              }.bind(delBtn));

              return wrap;
            }
          }
        ]
      }, gridNode);

      this._grid.startup();

      // ── Row selection → publish ───────────────────────────────
      this._grid.on("dgrid-select", function(e) {
        if (e.rows.length === 1) {
          topic.publish(T.EMPLOYEE_SELECTED, { employee: e.rows[0].data });
        }
        self._updateBulkDelete();
      });
      this._grid.on("dgrid-deselect", function() { self._updateBulkDelete(); });

      // ── Toolbar events ────────────────────────────────────────
      on(addBtn, "click", function() {
        topic.publish(T.EMPLOYEE_ADD_REQUESTED);
      });

      on(this._deleteBtn, "click", lang.hitch(this, function() {
        var ids = Object.keys(this._grid.selection).filter(lang.hitch(this, function(k) {
          return this._grid.selection[k];
        }));
        if (!ids.length) return;
        if (!confirm(strings.confirmBulkDelete.replace("${count}", ids.length))) return;
        ids.forEach(function(id) {
          EmployeeStore.remove(+id);
          topic.publish(T.EMPLOYEE_DELETED, { id: +id });
        });
        this._grid.clearSelection();
      }));

      // ── Subscribe to filter and save events ───────────────────
      this.own(
        topic.subscribe(T.FILTER_CHANGED, lang.hitch(this, "_applyFilter")),
        topic.subscribe(T.EMPLOYEE_SAVED, lang.hitch(this, function() {
          this._grid.refresh();
        }))
      );
    },

    _applyFilter: function(data) {
      var store = EmployeeStore.get();
      var col = store;
      if (data.dept)   col = col.filter({ dept: data.dept });
      if (data.active !== undefined && data.active !== "") {
        col = col.filter({ active: data.active === "true" });
      }
      if (data.text) {
        var t = data.text.toLowerCase();
        col = col.filter(function(e) {
          return e.name.toLowerCase().indexOf(t) > -1;
        });
      }
      this._grid.set("collection", col);
    },

    _updateBulkDelete: function() {
      var count = Object.values(this._grid.selection).filter(Boolean).length;
      this._deleteBtn.disabled = count === 0;
      this._deleteBtn.textContent = count
        ? strings.deleteSelected + " (" + count + ")"
        : strings.deleteSelected;
    },

    startup: function() {
      if (this._started) return;
      this.inherited(arguments);
      this._grid.startup();
    },

    destroy: function(p) {
      if (this._grid) this._grid.destroyRecursive();
      this.inherited(arguments);
    }
  });
});

C1.6 — Detail Panel Phase 6 – Pub/Sub Phase 2 – Templated

// hrapp/views/DetailPanel.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dijit/_TemplatedMixin",
  "dojo/topic",
  "dojo/dom-class",
  "dojo/date/locale",
  "dojo/number",
  "dojo/i18n!hrapp/nls/messages",
  "hrapp/topics",
  "dojo/text!hrapp/views/templates/DetailPanel.html"
], function(declare, lang, _Widget, _TemplatedMixin,
            topic, domClass, dateLocale, numberFmt,
            strings, T, template) {

  return declare([_Widget, _TemplatedMixin], {
    templateString: template,

    postCreate: function() {
      this.inherited(arguments);
      this.own(
        topic.subscribe(T.EMPLOYEE_SELECTED, lang.hitch(this, "_showEmployee")),
        topic.subscribe(T.EMPLOYEE_SAVED,    lang.hitch(this, "_onSaved")),
        topic.subscribe(T.EMPLOYEE_DELETED,  lang.hitch(this, "_onDeleted"))
      );
      domClass.add(this.domNode, "detail-panel-empty");
    },

    _showEmployee: function(data) {
      var emp = data.employee;
      this._currentId = emp.id;

      domClass.remove(this.domNode, "detail-panel-empty");
      this.nameNode.textContent  = emp.name;
      this.deptNode.textContent  = emp.dept;
      this.salaryNode.textContent = numberFmt.format(emp.salary, { type:"currency", currency:"USD" });
      this.hireDateNode.textContent = emp.hireDate
        ? dateLocale.format(new Date(emp.hireDate), { datePattern: "long" })
        : "—";
      this.statusNode.textContent = emp.active ? strings.active : strings.inactive;
      domClass.toggle(this.statusNode, "status--active",   emp.active);
      domClass.toggle(this.statusNode, "status--inactive", !emp.active);

      // Edit button wires to edit request topic
      this.editBtn.dataset.id = emp.id;
    },

    _onSaved: function(data) {
      if (this._currentId && data.employee.id === this._currentId) {
        this._showEmployee({ employee: data.employee });
      }
    },

    _onDeleted: function(data) {
      if (this._currentId && data.id === this._currentId) {
        this._currentId = null;
        domClass.add(this.domNode, "detail-panel-empty");
        this.nameNode.textContent = strings.selectEmployee;
      }
    }
  });
});

// hrapp/views/templates/DetailPanel.html
// <div class="detail-panel">
//   <div class="detail-empty-msg">${i18n.selectEmployee}</div>
//   <div class="detail-content">
//     <h2 data-dojo-attach-point="nameNode"></h2>
//     <dl>
//       <dt>${i18n.department}</dt>
//       <dd data-dojo-attach-point="deptNode"></dd>
//       <dt>${i18n.salary}</dt>
//       <dd data-dojo-attach-point="salaryNode"></dd>
//       <dt>${i18n.hireDate}</dt>
//       <dd data-dojo-attach-point="hireDateNode"></dd>
//       <dt>${i18n.status}</dt>
//       <dd data-dojo-attach-point="statusNode"></dd>
//     </dl>
//     <button data-dojo-attach-point="editBtn"
//             data-dojo-attach-event="onclick:_onEditClick">
//       ${i18n.edit}
//     </button>
//   </div>
// </div>

C1.7 — Add/Edit Dialog Phase 4 – Dialog Phase 5 – Deferred

// hrapp/dialogs/EmployeeDialog.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/Dialog",
  "dijit/form/ValidationTextBox",
  "dijit/form/Select",
  "dijit/form/NumberTextBox",
  "dijit/form/DateTextBox",
  "dijit/form/CheckBox",
  "dijit/form/Button",
  "dojo/dom-construct",
  "dojo/topic",
  "dojo/i18n!hrapp/nls/messages",
  "hrapp/topics",
  "hrapp/stores/EmployeeStore"
], function(declare, lang,
            Dialog, ValidationTextBox, Select, NumberTextBox, DateTextBox, CheckBox, Button,
            domConstruct, topic, strings, T, EmployeeStore) {

  // Returns a promise that resolves with the saved employee, or nothing on cancel
  return {
    open: function(employee) {
      var isNew = !employee;
      var formNode = domConstruct.create("div", { style:"padding:16px;min-width:380px" });

      // ── Form fields ────────────────────────────────────────────
      var nameField = new ValidationTextBox({
        name:"name", label:strings.fullName, required:true, trim:true,
        value: employee ? employee.name : "",
        style:"width:100%;margin-bottom:12px"
      }).placeAt(formNode); nameField.startup();

      var deptField = new Select({
        name:"dept", label:strings.department,
        options: [
          { label:"Engineering", value:"Engineering" },
          { label:"Marketing",   value:"Marketing" },
          { label:"HR",          value:"HR" },
          { label:"Finance",     value:"Finance" }
        ],
        value: employee ? employee.dept : "Engineering",
        style:"width:100%;margin-bottom:12px"
      }).placeAt(formNode); deptField.startup();

      var salaryField = new NumberTextBox({
        name:"salary", label:strings.salary,
        constraints: { min:0, max:500000, places:0 },
        value: employee ? employee.salary : 0,
        style:"width:100%;margin-bottom:12px"
      }).placeAt(formNode); salaryField.startup();

      var hireDateField = new DateTextBox({
        name:"hireDate", label:strings.hireDate,
        value: employee && employee.hireDate ? new Date(employee.hireDate) : new Date(),
        style:"width:100%;margin-bottom:12px"
      }).placeAt(formNode); hireDateField.startup();

      var activeField = new CheckBox({
        name:"active", label:strings.active,
        checked: employee ? employee.active : true
      }).placeAt(formNode); activeField.startup();

      // ── Buttons ────────────────────────────────────────────────
      var btnRow = domConstruct.create("div",
        { style:"display:flex;gap:8px;justify-content:flex-end;margin-top:16px;" }, formNode);

      var cancelBtn = new Button({ label:strings.cancel }).placeAt(btnRow); cancelBtn.startup();
      var saveBtn   = new Button({ label:strings.save, "class":"btn-primary" }).placeAt(btnRow); saveBtn.startup();

      // ── Dialog ─────────────────────────────────────────────────
      var dlg = new Dialog({
        title:   isNew ? strings.addEmployee : strings.editEmployee,
        content: formNode,
        onHide:  function() { dlg.destroyRecursive(); }
      });

      cancelBtn.on("click", function() { dlg.hide(); });

      saveBtn.on("click", function() {
        if (!nameField.isValid() || !salaryField.isValid()) {
          if (!nameField.isValid()) nameField.focus();
          else salaryField.focus();
          return;
        }

        var hd = hireDateField.get("value");
        var data = {
          id:       employee ? employee.id : Date.now(),
          name:     nameField.get("value"),
          dept:     deptField.get("value"),
          salary:   salaryField.get("value"),
          hireDate: hd ? hd.toISOString().slice(0,10) : "",
          active:   activeField.get("checked")
        };

        // Save to store (Observable notifies grid automatically)
        EmployeeStore.save(data);

        // Publish — Detail panel, StatusBar, etc. all react
        topic.publish(isNew ? T.EMPLOYEE_ADDED : T.EMPLOYEE_SAVED, { employee: data });

        dlg.hide();
      });

      dlg.show();

      // Subscribe to edit/add requests from anywhere in the app
      // (This module exports the open() function — boot.js subscribes)
    }
  };
});

// In boot.js — wire the dialog to topic requests
// topic.subscribe(T.EMPLOYEE_ADD_REQUESTED, function() {
//   EmployeeDialog.open(null);
// });
// topic.subscribe(T.EMPLOYEE_EDIT_REQUESTED, function(data) {
//   var emp = EmployeeStore.getEmployee(data.id);
//   EmployeeDialog.open(emp);
// });

C1.8 — Filter Panel Phase 6 – Pub/Sub

// hrapp/widgets/FilterPanel.js
define([
  "dojo/_base/declare",
  "dojo/_base/lang",
  "dijit/_Widget",
  "dijit/form/Select",
  "dijit/form/ValidationTextBox",
  "dojo/topic",
  "dojo/dom-construct",
  "dojo/i18n!hrapp/nls/messages",
  "hrapp/topics"
], function(declare, lang, _Widget,
            Select, ValidationTextBox,
            topic, domConstruct, strings, T) {

  return declare([_Widget], {
    _filters: { dept:"", active:"", text:"" },

    postCreate: function() {
      this.inherited(arguments);
      var self = this;

      domConstruct.create("h4", { textContent: strings.filters, className:"sidebar-heading" }, this.domNode);

      // ── Department filter ──────────────────────────────────────
      var deptSelect = new Select({
        label: strings.department,
        options: [
          { label:strings.allDepts, value:"" },
          { label:"Engineering",    value:"Engineering" },
          { label:"Marketing",      value:"Marketing" },
          { label:"HR",             value:"HR" },
          { label:"Finance",        value:"Finance" }
        ],
        onChange: function(v) { self._filters.dept = v; self._publish(); }
      }).placeAt(this.domNode);
      deptSelect.startup();

      // ── Status filter ──────────────────────────────────────────
      var statusSelect = new Select({
        label: strings.status,
        options: [
          { label:strings.allStatuses, value:"" },
          { label:strings.active,      value:"true" },
          { label:strings.inactive,    value:"false" }
        ],
        onChange: function(v) { self._filters.active = v; self._publish(); }
      }).placeAt(this.domNode);
      statusSelect.startup();

      // ── Text search ────────────────────────────────────────────
      var searchField = new ValidationTextBox({
        label:       strings.search,
        placeHolder: strings.searchPlaceholder,
        intermediateChanges: true,
        onChange: function(v) { self._filters.text = v; self._publish(); }
      }).placeAt(this.domNode);
      searchField.startup();
    },

    _publish: function() {
      topic.publish(T.FILTER_CHANGED, lang.mixin({}, this._filters));
    }
  });
});

C1.9 — i18n (EN/FR) Phase 8 – i18n

// hrapp/nls/messages.js — ROOT BUNDLE
define({
  root: {
    appTitle:"Employee Manager", name:"Name", department:"Department",
    salary:"Salary", hireDate:"Hire Date", status:"Status", actions:"Actions",
    addEmployee:"Add Employee", editEmployee:"Edit Employee",
    deleteSelected:"Delete Selected", edit:"Edit", delete:"Delete",
    save:"Save", cancel:"Cancel", filters:"Filters",
    allDepts:"All Departments", allStatuses:"All Statuses",
    active:"Active", inactive:"Inactive",
    search:"Search", searchPlaceholder:"Name or department...",
    selectEmployee:"Select an employee to view details",
    confirmDelete:"Delete ${name}? This cannot be undone.",
    confirmBulkDelete:"Delete ${count} employee(s)? This cannot be undone.",
    fullName:"Full Name", errorSave:"Failed to save employee."
  },
  fr: true
});

// hrapp/nls/fr/messages.js — FRENCH OVERRIDES
define({
  appTitle:"Gestionnaire d'Employés", name:"Nom", department:"Département",
  salary:"Salaire", hireDate:"Date d'embauche", status:"Statut", actions:"Actions",
  addEmployee:"Ajouter un employé", editEmployee:"Modifier l'employé",
  deleteSelected:"Supprimer la sélection", edit:"Modifier", delete:"Supprimer",
  save:"Enregistrer", cancel:"Annuler", filters:"Filtres",
  allDepts:"Tous les départements", allStatuses:"Tous les statuts",
  active:"Actif", inactive:"Inactif",
  search:"Rechercher", searchPlaceholder:"Nom ou département...",
  selectEmployee:"Sélectionnez un employé pour voir les détails",
  confirmDelete:"Supprimer ${name} ? Cette action est irréversible.",
  confirmBulkDelete:"Supprimer ${count} employé(s) ? Cette action est irréversible.",
  fullName:"Nom complet", errorSave:"Échec de la sauvegarde de l'employé."
});

C1.10 — Build Profile Phase 9 – Build

// hrapp/profiles/hrapp.profile.js
var profile = (function() {
  return {
    releaseDir:    "dist",
    action:        "release",
    layerOptimize: "closure",
    optimize:      "closure",
    cssOptimize:   "comments",
    internStrings:  true,
    localeList:     "en-us,fr",

    packages: [
      { name:"dojo",    location:"dojo" },
      { name:"dijit",   location:"dijit" },
      { name:"dgrid",   location:"dgrid" },
      { name:"dstore",  location:"dstore" },
      { name:"put-selector", location:"put-selector" },
      { name:"xstyle",       location:"xstyle" },
      { name:"hrapp",   location:"hrapp" }
    ],

    layers: {
      "dojo/dojo": {
        include: [
          "dojo/_base/declare","dojo/_base/lang","dojo/_base/array",
          "dojo/dom","dojo/dom-class","dojo/dom-style","dojo/dom-construct","dojo/dom-attr",
          "dojo/on","dojo/query","dojo/topic","dojo/when","dojo/Deferred","dojo/promise/all",
          "dojo/request","dojo/store/Memory","dojo/store/Observable","dojo/Stateful","dojo/i18n",
          "dojo/date/locale","dojo/number",
          "dojo/i18n!hrapp/nls/messages"
        ],
        boot: true, customBase: false
      },
      "hrapp/dijit-layer": {
        include: [
          "dijit/_Widget","dijit/_TemplatedMixin","dijit/_WidgetsInTemplateMixin",
          "dijit/form/ValidationTextBox","dijit/form/Select","dijit/form/NumberTextBox",
          "dijit/form/DateTextBox","dijit/form/CheckBox","dijit/form/Button","dijit/form/Form",
          "dijit/Dialog","dijit/layout/BorderContainer","dijit/layout/TabContainer",
          "dijit/layout/ContentPane","dijit/Tooltip",
          "dgrid/OnDemandGrid","dgrid/editor","dgrid/selector","dgrid/Selection",
          "dstore/Memory","dstore/Trackable"
        ],
        exclude: ["dojo/dojo"]
      },
      "hrapp/main": {
        include: [
          "hrapp/boot","hrapp/topics","hrapp/stores/EmployeeStore","hrapp/api/EmployeeAPI",
          "hrapp/layout/AppShell","hrapp/layout/HeaderBar","hrapp/layout/StatusBar",
          "hrapp/views/EmployeeList","hrapp/views/DetailPanel","hrapp/views/ReportsView",
          "hrapp/dialogs/EmployeeDialog","hrapp/widgets/FilterPanel"
        ],
        exclude: ["dojo/dojo","hrapp/dijit-layer"]
      }
    }
  };
})();

// Build: node util/buildscripts/build.js profile=hrapp/profiles/hrapp action=release

C1.11 — App Bootstrap & Wiring Phase 1 – AMD

// hrapp/topics.js — all topic constants
define({
  EMPLOYEE_SELECTED:      "employee/selected",
  EMPLOYEE_SAVED:         "employee/saved",
  EMPLOYEE_ADDED:         "employee/added",
  EMPLOYEE_DELETED:       "employee/deleted",
  EMPLOYEE_ADD_REQUESTED: "employee/addRequested",
  EMPLOYEE_EDIT_REQUESTED:"employee/editRequested",
  FILTER_CHANGED:         "filter/changed",
  APP_READY:              "app/ready",
  AUTH_EXPIRED:           "auth/sessionExpired"
});

// hrapp/boot.js — single entry point
define([
  "dojo/topic",
  "dojo/request/notify",
  "hrapp/topics",
  "hrapp/layout/AppShell",
  "hrapp/dialogs/EmployeeDialog",
  "dojo/domReady!"
], function(topic, notify, T, AppShell, EmployeeDialog) {

  // ── Global request hooks (spinner, auth handling) ─────────────
  var pendingCount = 0;
  notify("start", function() {
    pendingCount++;
    document.getElementById("globalSpinner").style.display = "block";
  });
  notify("stop", function() {
    document.getElementById("globalSpinner").style.display = "none";
  });
  notify("error", function(err) {
    if (err.response && err.response.status === 401) {
      topic.publish(T.AUTH_EXPIRED);
    }
  });

  // ── Wire dialog to topic requests ─────────────────────────────
  topic.subscribe(T.EMPLOYEE_ADD_REQUESTED, function() {
    EmployeeDialog.open(null);
  });

  topic.subscribe(T.EMPLOYEE_EDIT_REQUESTED, function(data) {
    require(["hrapp/stores/EmployeeStore"], function(store) {
      EmployeeDialog.open(store.getEmployee(data.id));
    });
  });

  // ── Mount the application ────────────────────────────────────
  new AppShell().startup();

  topic.publish(T.APP_READY, { timestamp: Date.now() });
});

// index.html
// <script>
//   var dojoConfig = {
//     async:true, parseOnLoad:false, locale:"en-us",
//     packages:[{ name:"hrapp", location:"/js/hrapp" }]
//   };
// </script>
// <script src="/js/dojo/dojo.js"></script>
// <script>require(["hrapp/boot"]);</script>

Capstone 1 — What This App Demonstrates

PhaseFeature in This App
Phase 1 — AMD6 packages, lazy dialog loading, boot entry point
Phase 2 — declareAll 6 modules are proper declare widgets with lifecycle
Phase 3 — StoreSingleton Observable store shared across all modules
Phase 4 — DijitBorderContainer + TabContainer + Dialog + all form widgets
Phase 5 — Asyncrequest/notify for global spinner + auth handling
Phase 6 — Pub/Sub6 topics, zero direct module references
Phase 7 — dgridOnDemandGrid + selector + editor + custom renderers
Phase 8 — i18nEN/FR NLS bundles, locale date/number formatting
Phase 9 — Build3-layer build profile, Closure optimization, internStrings

Capstone 2 — Real-Time Data Monitor

Focuses on Deferred-driven polling, dojox/charting live line chart, and alert system via pub/sub.

Continue to Capstone 2 →