Skip to main content

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:

  1. Quick Print — Select multiple chemicals, choose label type, configure, preview, print/download
  2. Four label types: GHS, Barcode, Secondary Container (new), NFPA 704
  3. Templates — Save label configurations as reusable presets
  4. Print History — Track recent print jobs with reprint capability

Phase Summary

PhaseFeatureStatus
1Database & Backend (tables, models, API)🔲 Not Started
2Frontend — Label Center Page (Quick Print)🔲 Not Started
3Secondary Container Label Type🔲 Not Started
4Templates & Print History🔲 Not Started
5Integration (sidebar, routing, feature flag)🔲 Not Started

Phase 1: Database & Backend

1A. Database Tables

chemiq_label_templates — Saved label configurations

ColumnTypeNullableNotes
template_idUUIDPKgen_random_uuid()
company_idUUID FKNOT NULLcore_data_companies
nameVARCHAR(100)NOT NULLe.g., "Warehouse Shelf — Small Barcode"
label_typeVARCHAR(30)NOT NULLbarcode, ghs, nfpa, secondary_container
configurationJSONBNOT NULLSize, field toggles per label type (see 1D)
is_defaultBOOLEANNOT NULLfalse — one default per type per company
created_by_user_idUUID FKNULLcore_data_users
created_atTIMESTAMPNOT NULLnow()
updated_atTIMESTAMPNULL

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
}
}
FieldDefaultRequired?
BarcodealwaysYes — core purpose of this label
product_nameonNo
manufactureronNo
cas_numberoffNo
locationonNo
internal_skuoffNo

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
}
}
FieldDefaultRequired?
Product namealwaysYes — OSHA HCS
Signal word (DANGER/WARNING)alwaysYes — OSHA HCS
pictogramsonNo (but strongly recommended)
hazard_statementsonNo
precautionary_statementsonNo
cas_numberonNo
supplier_infoonNo (manufacturer name)
manufacturer_addressoffNo
emergency_phoneoffNo

NFPA 704 Template:

{
"size": "medium",
"fields": {
"product_name": true,
"manufacturer": false,
"special_hazards": true
}
}
FieldDefaultRequired?
NFPA Diamond (Health/Fire/Reactivity)alwaysYes — core of NFPA 704
Special Hazards (W, OX, SA)onNo
product_nameonNo
manufactureroffNo

Secondary Container Template:

{
"size": "medium",
"fields": {
"pictograms": true,
"cas_number": true,
"sds_reference": true,
"manufacturer": false
}
}
FieldDefaultRequired?
Product namealwaysYes — OSHA HCS
Signal word (DANGER/WARNING)alwaysYes — OSHA HCS
pictogramsonNo (but recommended)
cas_numberonNo
sds_referenceonNo ("Refer to SDS for full information")
manufactureroffNo

chemiq_print_jobs — Print history / audit trail

ColumnTypeNullableNotes
job_idUUIDPKgen_random_uuid()
company_idUUID FKNOT NULLcore_data_companies
user_idUUID FKNOT NULLcore_data_users
label_typeVARCHAR(30)NOT NULLbarcode, ghs, nfpa, secondary_container
chemical_idsJSONBNOT NULLArray of chemical_id strings
chemical_namesJSONBNOT NULLArray of product names (for history display)
template_idUUID FKNULLchemiq_label_templates
label_sizeVARCHAR(20)NOT NULLe.g., small, medium, large
copiesINTEGERNOT NULL1-100
configurationJSONBNULLFull config snapshot for reprint
created_atTIMESTAMPNOT NULLnow()

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 model
  • ChemIQPrintJob(Base) — SQLAlchemy model

Schemas: tellus-ehs-hazcom-service/app/schemas/chemiq/labels.py

  • LabelTemplateCreate, LabelTemplateUpdate, LabelTemplateResponse
  • PrintJobCreate, PrintJobResponse, PrintJobListResponse
  • BatchPrepareRequest, 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

MethodPathDescription
POST/labels/batch-prepareFetch chemical + SDS hazard data for multiple chemicals
POST/labels/print-jobsRecord a print job (audit trail)
GET/labels/print-jobsList print history (paginated, company-scoped)
GET/labels/print-jobs/{job_id}Get single print job detail
GET/labels/templatesList templates (company-scoped)
POST/labels/templatesCreate 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_templates table with constraints and indexes
  • Create chemiq_print_jobs table with indexes
  • Insert LABELS feature into core_config_module_features for 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:

  • MainLayoutModuleLayout with 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:

  • ChemicalSelector component uses existing listChemicals API 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:
CardAccent ColorDescription
GHS LabelRedOSHA-compliant hazard communication label
BarcodeBlueContainer tracking with UPC/barcode
Secondary ContainerAmberSimplified workplace transfer label
NFPA 704PurpleEmergency 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:

  • LabelConfigPanel renders two sections: Print Settings and Field Toggles
  • Print Settings (all types): Label size selector (3 options), copies (1-100)
Label TypeSize Options
BarcodeSmall 50×25mm, Medium 70×35mm, Large 100×50mm
GHSSmall 100×75mm, Medium/A6 148×105mm, Large/A5 210×148mm
Secondary ContainerSmall 76×51mm, Medium 102×76mm, Large 152×102mm
NFPA 704Small 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
  • BatchLabelPreview shows 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-jobs to 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:

FunctionSource 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

FileActionPhase
service/app/db/models/chemiq/labels.pyCreate — Label template + print job models1
service/app/schemas/chemiq/labels.pyCreate — Pydantic request/response schemas1
service/app/services/chemiq/label_service.pyCreate — Service layer (CRUD + batch prepare)1
service/app/api/v1/chemiq/labels.pyCreate — API router (8 endpoints)1
service/app/api/v1/chemiq/__init__.pyEdit — Register labels router1
service/alembic/versions/..._add_chemiq_label_tables.pyCreate — Migration (2 tables + feature flag)1
ui/src/pages/chemiq/labels/index.tsxCreate — Label Center page2
ui/src/pages/chemiq/labels/components/QuickPrintTab.tsxCreate — Quick print workflow2
ui/src/pages/chemiq/labels/components/ChemicalSelector.tsxCreate — Multi-select chemical picker2
ui/src/pages/chemiq/labels/components/LabelTypeSelector.tsxCreate — Label type card picker2
ui/src/pages/chemiq/labels/components/LabelConfigPanel.tsxCreate — Config form per label type2
ui/src/pages/chemiq/labels/components/BatchLabelPreview.tsxCreate — Preview grid2
ui/src/services/api/chemiq.api.tsEdit — Add 8 label API functions2
ui/src/pages/chemiq/labels/components/SecondaryContainerPreview.tsxCreate — New label renderer3
ui/src/pages/chemiq/labels/utils/ghsPictograms.tsCreate — Shared GHS pictogram drawing3
ui/src/pages/chemiq/inventory/components/GHSLabelPreview.tsxEdit — Import from shared utility3
ui/src/pages/chemiq/labels/components/TemplatesTab.tsxCreate — Templates CRUD UI4
ui/src/pages/chemiq/labels/components/PrintHistoryTab.tsxCreate — Print history UI4
ui/src/components/sidebar/SidebarChemIQ.tsxEdit — Add Labels menu entry5
ui/src/App.tsxEdit — Add /chemiq/labels route5

13 new files, 5 modified files


Implementation Order

  1. Phase 1 (Backend): Migration → Models → Schemas → Service → API → Register router
  2. Phase 2 (Frontend core): Label Center page → Quick Print with 3 existing label types → API service functions
  3. Phase 3 (New label type): Extract GHS pictogram utility → Secondary Container preview
  4. Phase 4 (Templates + History): Templates tab → Print History tab
  5. Phase 5 (Integration): Sidebar entry → Route → Feature flag → Verify

Testing & Verification

#TestExpected Result
1Run migrationalembic upgrade head succeeds, both tables created
2Test API via SwaggerAll 8 endpoints return correct responses at /api/docs
3Feature flagLABELS feature appears in /api/v1/menu/ response
4Sidebar"Labels" entry visible under ChemIQ with Tag icon
5Quick Print — single chemicalSelect 1 chemical → GHS → medium → Print → PDF opens
6Quick Print — batchSelect 10 chemicals → Barcode → Print → 10 labels in PDF
7Secondary ContainerNew label type renders with pictograms, signal word, SDS reference
8SDS-required types disabledAll chemicals have sds_missing → GHS/NFPA/Secondary cards disabled
9Save templateSave config → reload page → template appears in Templates tab
10Default templateSet default → select label type in Quick Print → config auto-loads
11Print historyPrint job → appears in History tab → Reprint loads config
12Existing modalsChemical Details page Print dropdown still works (unchanged)
13TypeScriptnpx tsc --noEmit — zero new errors
14ResponsiveLabel Center page works on desktop and mobile

Future Enhancements

  • Label printing insights dashboard — Aggregate chemiq_print_jobs data 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. The chemiq_print_jobs table already captures chemical_ids, label_type, user_id, and created_at needed for this. Note: Current granularity is at the chemical/product level (chemical_ids from chemiq_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 a chemiq_containers table 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)