ChemIQ Label Center — Implementation Plan
Document Version: 1.0 Created: 2026-02-20 Status: Draft
Overview
Label printing is a primary EHS workflow — safety coordinators regularly print GHS labels, barcode labels, and NFPA diamonds for chemical containers. Currently this is buried 3 clicks deep inside the Chemical Details page (Sidebar → Inventory → click chemical → Print dropdown), limited to one chemical at a time. This plan elevates label printing to a dedicated sidebar entry under ChemIQ with batch printing, saved templates, and a new Secondary Container label type.
Existing per-chemical print modals remain unchanged as convenience shortcuts.
Problem Statement
- Label printing is buried deep in the chemical detail page — not discoverable
- Can only print labels for one chemical at a time
- No saved templates for frequently used label configurations
- No print history or audit trail
- Missing Secondary Container label type (OSHA requirement for transferred chemicals)
Solution
A new Label Center page at /chemiq/labels with:
- Quick Print — Select multiple chemicals, choose label type, configure, preview, print/download
- Four label types: GHS, Barcode, Secondary Container (new), NFPA 704
- Templates — Save label configurations as reusable presets
- Print History — Track recent print jobs with reprint capability
Phase Summary
| Phase | Feature | Status |
|---|---|---|
| 1 | Database & Backend (tables, models, API) | 🔲 Not Started |
| 2 | Frontend — Label Center Page (Quick Print) | 🔲 Not Started |
| 3 | Secondary Container Label Type | 🔲 Not Started |
| 4 | Templates & Print History | 🔲 Not Started |
| 5 | Integration (sidebar, routing, feature flag) | 🔲 Not Started |
Phase 1: Database & Backend
1A. Database Tables
chemiq_label_templates — Saved label configurations
| Column | Type | Nullable | Notes |
|---|---|---|---|
template_id | UUID | PK | gen_random_uuid() |
company_id | UUID FK | NOT NULL | → core_data_companies |
name | VARCHAR(100) | NOT NULL | e.g., "Warehouse Shelf — Small Barcode" |
label_type | VARCHAR(30) | NOT NULL | barcode, ghs, nfpa, secondary_container |
configuration | JSONB | NOT NULL | Size, field toggles per label type (see 1D) |
is_default | BOOLEAN | NOT NULL | false — one default per type per company |
created_by_user_id | UUID FK | NULL | → core_data_users |
created_at | TIMESTAMP | NOT NULL | now() |
updated_at | TIMESTAMP | NULL |
Unique constraint: (company_id, name)
Index: (company_id, label_type)
1D. Template Configuration Schema (JSONB)
Each label type defines required fields (always shown, OSHA-mandated) and optional fields (toggleable by the user). Templates save these toggle states along with print settings.
Barcode Template:
{
"size": "medium",
"fields": {
"product_name": true,
"manufacturer": true,
"cas_number": false,
"location": true,
"internal_sku": false
}
}
| Field | Default | Required? |
|---|---|---|
| Barcode | always | Yes — core purpose of this label |
product_name | on | No |
manufacturer | on | No |
cas_number | off | No |
location | on | No |
internal_sku | off | No |
GHS Template:
{
"size": "medium",
"fields": {
"pictograms": true,
"hazard_statements": true,
"precautionary_statements": true,
"manufacturer_address": false,
"emergency_phone": false,
"cas_number": true,
"supplier_info": true
}
}
| Field | Default | Required? |
|---|---|---|
| Product name | always | Yes — OSHA HCS |
| Signal word (DANGER/WARNING) | always | Yes — OSHA HCS |
pictograms | on | No (but strongly recommended) |
hazard_statements | on | No |
precautionary_statements | on | No |
cas_number | on | No |
supplier_info | on | No (manufacturer name) |
manufacturer_address | off | No |
emergency_phone | off | No |
NFPA 704 Template:
{
"size": "medium",
"fields": {
"product_name": true,
"manufacturer": false,
"special_hazards": true
}
}
| Field | Default | Required? |
|---|---|---|
| NFPA Diamond (Health/Fire/Reactivity) | always | Yes — core of NFPA 704 |
| Special Hazards (W, OX, SA) | on | No |
product_name | on | No |
manufacturer | off | No |
Secondary Container Template:
{
"size": "medium",
"fields": {
"pictograms": true,
"cas_number": true,
"sds_reference": true,
"manufacturer": false
}
}
| Field | Default | Required? |
|---|---|---|
| Product name | always | Yes — OSHA HCS |
| Signal word (DANGER/WARNING) | always | Yes — OSHA HCS |
pictograms | on | No (but recommended) |
cas_number | on | No |
sds_reference | on | No ("Refer to SDS for full information") |
manufacturer | off | No |
chemiq_print_jobs — Print history / audit trail
| Column | Type | Nullable | Notes |
|---|---|---|---|
job_id | UUID | PK | gen_random_uuid() |
company_id | UUID FK | NOT NULL | → core_data_companies |
user_id | UUID FK | NOT NULL | → core_data_users |
label_type | VARCHAR(30) | NOT NULL | barcode, ghs, nfpa, secondary_container |
chemical_ids | JSONB | NOT NULL | Array of chemical_id strings |
chemical_names | JSONB | NOT NULL | Array of product names (for history display) |
template_id | UUID FK | NULL | → chemiq_label_templates |
label_size | VARCHAR(20) | NOT NULL | e.g., small, medium, large |
copies | INTEGER | NOT NULL | 1-100 |
configuration | JSONB | NULL | Full config snapshot for reprint |
created_at | TIMESTAMP | NOT NULL | now() |
Index: (company_id, created_at DESC) for history listing
1B. Backend Files
Model: tellus-ehs-hazcom-service/app/db/models/chemiq/labels.py
ChemIQLabelTemplate(Base)— SQLAlchemy modelChemIQPrintJob(Base)— SQLAlchemy model
Schemas: tellus-ehs-hazcom-service/app/schemas/chemiq/labels.py
LabelTemplateCreate,LabelTemplateUpdate,LabelTemplateResponsePrintJobCreate,PrintJobResponse,PrintJobListResponseBatchPrepareRequest,BatchPrepareResponse(chemical + SDS data for batch label generation)
Service: tellus-ehs-hazcom-service/app/services/chemiq/label_service.py
LabelService— CRUD for templates, print job recording, batch data preparation
API Router: tellus-ehs-hazcom-service/app/api/v1/chemiq/labels.py
1C. API Endpoints
| Method | Path | Description |
|---|---|---|
POST | /labels/batch-prepare | Fetch chemical + SDS hazard data for multiple chemicals |
POST | /labels/print-jobs | Record a print job (audit trail) |
GET | /labels/print-jobs | List print history (paginated, company-scoped) |
GET | /labels/print-jobs/{job_id} | Get single print job detail |
GET | /labels/templates | List templates (company-scoped) |
POST | /labels/templates | Create template |
PUT | /labels/templates/{template_id} | Update template |
DELETE | /labels/templates/{template_id} | Delete template |
Register router in app/api/v1/chemiq/__init__.py:
from .labels import router as labels_router
router.include_router(labels_router, prefix="/labels", tags=["ChemIQ - Labels"])
1D. Batch Prepare Endpoint (Key Design)
POST /labels/batch-prepare avoids N+1 API calls from the frontend when batch printing:
Request:
{
"chemical_ids": ["uuid1", "uuid2", "uuid3"],
"include_sds_hazard": true
}
Response:
{
"chemicals": [
{
"chemical_id": "uuid1",
"product_name": "Acetone",
"manufacturer": "Fisher Scientific",
"cas_number": "67-64-1",
"barcode_upc": "012345678905",
"location_name": "Lab A-1",
"site_name": "Main Campus",
"sds_hazard": {
"signal_word": "DANGER",
"pictograms": ["GHS02", "GHS07"],
"hazard_statements": ["H225", "H319", "H336"],
"precautionary_statements": ["P210", "P261", "P305+P351+P338"],
"manufacturer_address": "1 Fisher Ln, Fair Lawn, NJ",
"emergency_phone": "1-800-555-1234"
}
}
]
}
1E. Migration
File: alembic/versions/2026_02_XX_XXXX_add_chemiq_label_tables.py
- Create
chemiq_label_templatestable with constraints and indexes - Create
chemiq_print_jobstable with indexes - Insert
LABELSfeature intocore_config_module_featuresfor the ChemIQ module:
INSERT INTO core_config_module_features (module_id, code, name, description, display_order, is_active)
SELECT module_id, 'LABELS', 'Label Center', 'Print GHS, barcode, NFPA, and secondary container labels', 60, true
FROM core_config_modules WHERE code = 'CHEMIQ';
Phase 2: Frontend — Label Center Page
2A. New Page Structure
src/pages/chemiq/labels/
├── index.tsx # Main Label Center page (3 sections)
├── components/
│ ├── QuickPrintTab.tsx # Section 1: Multi-step quick print workflow
│ ├── ChemicalSelector.tsx # Searchable multi-select chemical picker
│ ├── LabelTypeSelector.tsx # Card-based 4-type picker
│ ├── LabelConfigPanel.tsx # Dynamic config form per label type
│ ├── BatchLabelPreview.tsx # Preview grid of selected chemicals' labels
│ ├── TemplatesTab.tsx # Section 2: Saved templates CRUD
│ └── PrintHistoryTab.tsx # Section 3: Print history with reprint
2B. Label Center Page (index.tsx)
Follows the pattern from src/pages/chemiq/compliance/index.tsx:
MainLayout→ModuleLayoutwith left nav sections
const sections: NavItem[] = [
{ id: 'quick-print', label: 'Quick Print', description: 'Print labels for chemicals', icon: Printer },
{ id: 'templates', label: 'Templates', description: 'Saved label configurations', icon: LayoutTemplate },
{ id: 'history', label: 'Print History', description: 'Recent print jobs', icon: History },
];
Header: title="Label Center", description="Print GHS, barcode, NFPA, and secondary container labels", gradient: from-fuchsia-600 via-pink-500 to-rose-400 (ChemIQ magenta).
2C. Quick Print Workflow (QuickPrintTab.tsx)
Step 1 — Select Chemicals:
ChemicalSelectorcomponent uses existinglistChemicalsAPI with search/filter- Multi-select with checkboxes — shows product name, manufacturer, site
- Selected chemicals displayed as removable chips above the list
- Limit: 50 chemicals per batch
Step 2 — Choose Label Type:
LabelTypeSelector— 4 large tappable cards in 2×2 grid:
| Card | Accent Color | Description |
|---|---|---|
| GHS Label | Red | OSHA-compliant hazard communication label |
| Barcode | Blue | Container tracking with UPC/barcode |
| Secondary Container | Amber | Simplified workplace transfer label |
| NFPA 704 | Purple | Emergency responder hazard diamond |
- GHS / NFPA / Secondary Container cards are disabled if ALL selected chemicals have
sds_missing: true - Tooltip on disabled cards: "SDS data required — attach SDS to chemicals first"
Step 3 — Configure & Preview:
LabelConfigPanelrenders two sections: Print Settings and Field Toggles- Print Settings (all types): Label size selector (3 options), copies (1-100)
| Label Type | Size Options |
|---|---|
| Barcode | Small 50×25mm, Medium 70×35mm, Large 100×50mm |
| GHS | Small 100×75mm, Medium/A6 148×105mm, Large/A5 210×148mm |
| Secondary Container | Small 76×51mm, Medium 102×76mm, Large 152×102mm |
| NFPA 704 | Small 2"×2", Medium 4"×4", Wall 8"×8" |
- Field Toggles (per label type — see Section 1D for full schema):
- Barcode: product name, manufacturer, CAS number, location, internal SKU
- GHS: pictograms, hazard statements, precautionary statements, CAS number, supplier info, manufacturer address, emergency phone
- NFPA: product name, manufacturer, special hazards
- Secondary Container: pictograms, CAS number, SDS reference text, manufacturer
- Required fields (product name, signal word, barcode, NFPA diamond) are shown as always-on, non-toggleable
- If a default template exists for the selected label type, its field toggles auto-load
BatchLabelPreviewshows scrollable grid of label previews reflecting current toggle state (max 6 visible, "+N more" badge)- "Save as Template" button → name input → saves size + field toggles to backend
Step 4 — Print / Download:
- "Print All" → generates multi-page PDF (one label per page × copies per chemical) → opens print dialog
- "Download PDF" → saves PDF file
- Both actions call
POST /labels/print-jobsto record the audit trail
2D. API Service Functions
Add to src/services/api/chemiq.api.ts:
// Label Center
batchPrepareLabels(token, userId, companyId, chemicalIds, includeSdsHazard)
createPrintJob(token, userId, companyId, data)
listPrintJobs(token, userId, companyId, page, pageSize)
getPrintJob(token, userId, companyId, jobId)
listLabelTemplates(token, userId, companyId)
createLabelTemplate(token, userId, companyId, data)
updateLabelTemplate(token, userId, companyId, templateId, data)
deleteLabelTemplate(token, userId, companyId, templateId)
Re-export from src/services/api/index.ts.
2E. Reuse Existing Label Generators
The Quick Print workflow calls existing generator functions for 3 of 4 label types:
| Function | Source File |
|---|---|
generateLabelDataURL() | src/pages/chemiq/inventory/components/BarcodeLabelPreview.tsx |
generateGHSLabelDataURL() | src/pages/chemiq/inventory/components/GHSLabelPreview.tsx |
generateNFPALabelDataURL() | src/pages/chemiq/inventory/components/NFPADiamondPreview.tsx |
These accept ChemicalInventory + SDSDocumentDetail and return PNG data URLs. The batch prepare endpoint returns equivalent data that maps to these interfaces.
New: generateSecondaryContainerDataURL() — see Phase 3.
Phase 3: Secondary Container Label Type
3A. OSHA Secondary Container Label
Secondary container labels are required when chemicals are transferred from their original container to a secondary/workplace container. They are simplified versions of the full GHS label.
Required elements (per OSHA HCS 2012):
- Product name/identifier
- Signal word (DANGER / WARNING)
- GHS pictograms
- Reference to the full SDS
3B. New Preview Component
File: src/pages/chemiq/labels/components/SecondaryContainerPreview.tsx
Exports: generateSecondaryContainerDataURL(chemical, sdsHazard, labelSize)
Label layout:
┌──────────────────────────────────┐
│ PRODUCT NAME (bold, large) │
│ Manufacturer │
│ │
│ ⚠ DANGER / WARNING │
│ │
│ [GHS02] [GHS07] [GHS05] │
│ (pictogram icons) │
│ │
│ "Refer to SDS for full hazard │
│ and handling information" │
│ │
│ CAS: 67-64-1 │
└──────────────────────────────────┘
- Canvas rendering (same pattern as
GHSLabelPreview) - Amber border (distinguishes from full GHS red border)
- Sizes: Small (76×51mm / 3"×2"), Medium (102×76mm / 4"×3"), Large (152×102mm / 6"×4")
3C. Extract Shared GHS Pictogram Utility
New file: src/pages/chemiq/labels/utils/ghsPictograms.ts
Move drawGHSPictogram() from GHSLabelPreview.tsx to this shared utility. Both GHSLabelPreview.tsx and SecondaryContainerPreview.tsx import from here.
Update import in GHSLabelPreview.tsx to avoid breaking existing per-chemical print modals.
Phase 4: Templates & Print History
4A. Templates Tab (TemplatesTab.tsx)
- Lists saved templates grouped by label type (4 collapsible sections)
- Each template card shows: name, label type icon, size, field toggle summary (e.g., "5 of 7 fields enabled")
- Actions: Edit (opens inline editor for name, size, field toggles), Set as default, Delete
- "Default" template auto-loads size + field toggles in Quick Print when that label type is selected
- Only one default allowed per label type per company (toggling default on one template removes it from another)
- Empty state: "No saved templates yet. Use Quick Print → Save as Template to create one."
4B. Print History Tab (PrintHistoryTab.tsx)
- Table listing recent print jobs (paginated, 20 per page)
- Columns: Date, Label Type, Chemicals (count + truncated names), Size, Copies, User
- "Reprint" action → loads saved configuration into Quick Print tab with chemicals pre-selected
- Filter by label type and date range
Phase 5: Integration
5A. Sidebar Entry
File: src/components/sidebar/SidebarChemIQ.tsx
Add after "Export" entry (position 6 in the menu array):
{ label: 'Labels', icon: Tag, path: '/chemiq/labels', permission: 'chemiq:inventory:*', feature: 'LABELS' },
Import Tag from lucide-react.
5B. Route
File: src/App.tsx
Add under ChemIQ routes:
<Route path="/chemiq/labels" element={<ProtectedRoute><LabelCenterPage /></ProtectedRoute>} />
Import LabelCenterPage from src/pages/chemiq/labels/index.tsx.
5C. Feature Flag
The migration (Phase 1E) inserts the LABELS feature code into core_config_module_features for the ChemIQ module. The sidebar entry uses feature: 'LABELS' to gate visibility based on the company's plan.
Files Summary
| File | Action | Phase |
|---|---|---|
service/app/db/models/chemiq/labels.py | Create — Label template + print job models | 1 |
service/app/schemas/chemiq/labels.py | Create — Pydantic request/response schemas | 1 |
service/app/services/chemiq/label_service.py | Create — Service layer (CRUD + batch prepare) | 1 |
service/app/api/v1/chemiq/labels.py | Create — API router (8 endpoints) | 1 |
service/app/api/v1/chemiq/__init__.py | Edit — Register labels router | 1 |
service/alembic/versions/..._add_chemiq_label_tables.py | Create — Migration (2 tables + feature flag) | 1 |
ui/src/pages/chemiq/labels/index.tsx | Create — Label Center page | 2 |
ui/src/pages/chemiq/labels/components/QuickPrintTab.tsx | Create — Quick print workflow | 2 |
ui/src/pages/chemiq/labels/components/ChemicalSelector.tsx | Create — Multi-select chemical picker | 2 |
ui/src/pages/chemiq/labels/components/LabelTypeSelector.tsx | Create — Label type card picker | 2 |
ui/src/pages/chemiq/labels/components/LabelConfigPanel.tsx | Create — Config form per label type | 2 |
ui/src/pages/chemiq/labels/components/BatchLabelPreview.tsx | Create — Preview grid | 2 |
ui/src/services/api/chemiq.api.ts | Edit — Add 8 label API functions | 2 |
ui/src/pages/chemiq/labels/components/SecondaryContainerPreview.tsx | Create — New label renderer | 3 |
ui/src/pages/chemiq/labels/utils/ghsPictograms.ts | Create — Shared GHS pictogram drawing | 3 |
ui/src/pages/chemiq/inventory/components/GHSLabelPreview.tsx | Edit — Import from shared utility | 3 |
ui/src/pages/chemiq/labels/components/TemplatesTab.tsx | Create — Templates CRUD UI | 4 |
ui/src/pages/chemiq/labels/components/PrintHistoryTab.tsx | Create — Print history UI | 4 |
ui/src/components/sidebar/SidebarChemIQ.tsx | Edit — Add Labels menu entry | 5 |
ui/src/App.tsx | Edit — Add /chemiq/labels route | 5 |
13 new files, 5 modified files
Implementation Order
- Phase 1 (Backend): Migration → Models → Schemas → Service → API → Register router
- Phase 2 (Frontend core): Label Center page → Quick Print with 3 existing label types → API service functions
- Phase 3 (New label type): Extract GHS pictogram utility → Secondary Container preview
- Phase 4 (Templates + History): Templates tab → Print History tab
- Phase 5 (Integration): Sidebar entry → Route → Feature flag → Verify
Testing & Verification
| # | Test | Expected Result |
|---|---|---|
| 1 | Run migration | alembic upgrade head succeeds, both tables created |
| 2 | Test API via Swagger | All 8 endpoints return correct responses at /api/docs |
| 3 | Feature flag | LABELS feature appears in /api/v1/menu/ response |
| 4 | Sidebar | "Labels" entry visible under ChemIQ with Tag icon |
| 5 | Quick Print — single chemical | Select 1 chemical → GHS → medium → Print → PDF opens |
| 6 | Quick Print — batch | Select 10 chemicals → Barcode → Print → 10 labels in PDF |
| 7 | Secondary Container | New label type renders with pictograms, signal word, SDS reference |
| 8 | SDS-required types disabled | All chemicals have sds_missing → GHS/NFPA/Secondary cards disabled |
| 9 | Save template | Save config → reload page → template appears in Templates tab |
| 10 | Default template | Set default → select label type in Quick Print → config auto-loads |
| 11 | Print history | Print job → appears in History tab → Reprint loads config |
| 12 | Existing modals | Chemical Details page Print dropdown still works (unchanged) |
| 13 | TypeScript | npx tsc --noEmit — zero new errors |
| 14 | Responsive | Label Center page works on desktop and mobile |
Future Enhancements
- Label printing insights dashboard — Aggregate
chemiq_print_jobsdata to show: which chemicals have/haven't had labels printed, label type distribution (GHS vs barcode vs NFPA vs secondary container), printing trends over time, per-user printing activity. Thechemiq_print_jobstable already captureschemical_ids,label_type,user_id, andcreated_atneeded for this. Note: Current granularity is at the chemical/product level (chemical_idsfromchemiq_inventory), not individual physical containers. If container-level label tracking is needed (e.g., "Container #3 of Acetone in Lab A was labeled on Feb 15"), a future container-level data model would be required — potentially achemiq_containerstable linking physical containers to inventory records. - Label designer — Custom label layout editor (drag-and-drop fields)
- QR code labels — Link to digital SDS via QR code
- Network printer integration — Direct print to label printers (Zebra, Dymo)
- Scheduled printing — Auto-print labels when new chemicals are added
- Label compliance audit — Track which containers have/don't have labels (depends on container-level data model above)