@@ -223,6 +223,10 @@ export class CipWebviewManager {
223223 report : CipReportDefinition ,
224224 ) : Promise < void > {
225225 switch ( message . command ) {
226+ case 'loadSites' : {
227+ await this . loadSites ( panel ) ;
228+ break ;
229+ }
226230 case 'executeQuery' : {
227231 await this . executeQuery ( ( message . params ?? { } ) as Record < string , string > , panel , report ) ;
228232 break ;
@@ -650,6 +654,27 @@ export class CipWebviewManager {
650654 return `${ header } \n${ dataRows } ` ;
651655 }
652656
657+ /**
658+ * Fetch the list of site IDs from ccdw_dim_site and send to the webview.
659+ */
660+ private async loadSites ( panel : vscode . WebviewPanel ) : Promise < void > {
661+ const ctx = this . requireConnectedClient ( ) ;
662+ if ( ! ctx ) {
663+ panel . webview . postMessage ( { command : 'sitesLoaded' , sites : [ ] } ) ;
664+ return ;
665+ }
666+ try {
667+ const result = await ctx . client . query (
668+ `SELECT DISTINCT nsite_id FROM ccdw_dim_site WHERE nsite_id IS NOT NULL ORDER BY nsite_id` ,
669+ { fetchSize : 500 } ,
670+ ) ;
671+ const sites = result . rows . map ( ( r ) => String ( r . nsite_id ?? '' ) ) . filter ( Boolean ) ;
672+ panel . webview . postMessage ( { command : 'sitesLoaded' , sites} ) ;
673+ } catch {
674+ panel . webview . postMessage ( { command : 'sitesLoaded' , sites : [ ] } ) ;
675+ }
676+ }
677+
653678 /**
654679 * Execute the CIP report query and send results back to webview.
655680 */
@@ -745,59 +770,93 @@ export class CipWebviewManager {
745770 }
746771
747772 private getReportDashboardContent ( webview : vscode . Webview , report : CipReportDefinition ) : string {
748- const parameterFields = report . parameters
749- . map ( ( param ) => {
750- const nameAttr = CipWebviewManager . escapeAttr ( param . name ) ;
751- const descAttr = CipWebviewManager . escapeAttr ( param . description ) ;
752- const descText = CipWebviewManager . escapeHtml ( param . description ) ;
753- const labelText = param . name
754- . replace ( / ( [ A - Z ] ) / g, ' $1' )
755- . trim ( )
756- . split ( ' ' )
757- . map ( ( w ) => w . charAt ( 0 ) . toUpperCase ( ) + w . slice ( 1 ) )
758- . join ( ' ' )
759- . replace ( / \b I d \b / g, 'ID' ) ;
760- const nameText = CipWebviewManager . escapeHtml ( labelText ) ;
761- const required = param . required ? 'required' : '' ;
762-
763- let inputHtml = '' ;
764- // Dates get a `.field--date` modifier so CSS can break them onto their own
765- // grid row — keeps from/to ranges visually paired instead of wrapping the
766- // second date to a lonely third row.
767- let fieldModifier = '' ;
768- if ( param . type === 'string' ) {
769- fieldModifier = ' full' ;
770- inputHtml = `<input type="text" id="${ nameAttr } " name="${ nameAttr } " ${ required } class="input" placeholder="${ descAttr } " />` ;
771- } else if ( param . type === 'date' ) {
772- fieldModifier = ' field--date' ;
773- inputHtml = `<input type="date" id="${ nameAttr } " name="${ nameAttr } " ${ required } class="input" />` ;
774- } else if ( param . type === 'boolean' ) {
775- inputHtml = `
776- <select id="${ nameAttr } " name="${ nameAttr } " ${ required } class="select">
777- <option value="">— Select —</option>
778- <option value="true">True</option>
779- <option value="false">False</option>
780- </select>
781- ` ;
782- } else if ( param . type === 'number' ) {
783- const min = param . min !== undefined ? String ( param . min ) : '' ;
784- const max = param . max !== undefined ? String ( param . max ) : '' ;
785- inputHtml = `<input type="number" id="${ nameAttr } " name="${ nameAttr } " min="${ min } " max="${ max } " ${ required } class="input" placeholder="${ descAttr } " />` ;
786- } else {
787- inputHtml = `<input type="text" id="${ nameAttr } " name="${ nameAttr } " ${ required } class="input" placeholder="${ descAttr } " />` ;
788- }
789-
790- return `
791- <div class="field${ fieldModifier } ">
792- <label class="label" for="${ nameAttr } ">
793- ${ nameText } ${ param . required ? ' <span class="required">*</span>' : '' }
794- </label>
795- <span class="hint">${ descText } </span>
796- ${ inputHtml }
773+ const hasDateRange =
774+ report . parameters . some ( ( p ) => p . name === 'from' && p . type === 'date' ) &&
775+ report . parameters . some ( ( p ) => p . name === 'to' && p . type === 'date' ) ;
776+
777+ const dateRangeWidget = hasDateRange
778+ ? `
779+ <div class="field full date-range-field">
780+ <label class="label">Date Range <span class="required">*</span></label>
781+ <div class="date-range-presets">
782+ <button type="button" class="btn btn-secondary date-preset-btn" data-preset="last-week">Last Week</button>
783+ <button type="button" class="btn btn-secondary date-preset-btn" data-preset="last-month">Last Month</button>
784+ <button type="button" class="btn btn-secondary date-preset-btn" data-preset="last-6-months">Last 6 Months</button>
785+ <button type="button" class="btn btn-secondary date-preset-btn" data-preset="custom">Custom</button>
786+ </div>
787+ <div class="date-range-custom date-range-custom--hidden" id="dateRangeCustom">
788+ <div class="date-range-custom__fields">
789+ <div class="field">
790+ <label class="label" for="from">From <span class="required">*</span></label>
791+ <input type="date" id="from" name="from" class="input" />
792+ </div>
793+ <div class="field">
794+ <label class="label" for="to">To <span class="required">*</span></label>
795+ <input type="date" id="to" name="to" class="input" />
796+ </div>
797+ </div>
797798 </div>
798- ` ;
799- } )
800- . join ( '' ) ;
799+ <input type="hidden" id="fromHidden" name="from" />
800+ <input type="hidden" id="toHidden" name="to" />
801+ </div>
802+ `
803+ : '' ;
804+
805+ const parameterFields =
806+ report . parameters
807+ . filter ( ( p ) => ! ( hasDateRange && ( p . name === 'from' || p . name === 'to' ) ) )
808+ . map ( ( param ) => {
809+ const nameAttr = CipWebviewManager . escapeAttr ( param . name ) ;
810+ const descAttr = CipWebviewManager . escapeAttr ( param . description ) ;
811+ const descText = CipWebviewManager . escapeHtml ( param . description ) ;
812+ const labelText = param . name
813+ . replace ( / ( [ A - Z ] ) / g, ' $1' )
814+ . trim ( )
815+ . split ( ' ' )
816+ . map ( ( w ) => w . charAt ( 0 ) . toUpperCase ( ) + w . slice ( 1 ) )
817+ . join ( ' ' )
818+ . replace ( / \b I d \b / g, 'ID' ) ;
819+ const nameText = CipWebviewManager . escapeHtml ( labelText ) ;
820+ const required = param . required ? 'required' : '' ;
821+
822+ let inputHtml = '' ;
823+ let fieldModifier = '' ;
824+ if ( param . type === 'string' && param . name === 'siteId' ) {
825+ fieldModifier = ' full' ;
826+ inputHtml = `<select id="${ nameAttr } " name="${ nameAttr } " ${ required } class="select site-id-select"><option value="" disabled selected>— Configure connection to load sites —</option></select>` ;
827+ } else if ( param . type === 'string' ) {
828+ fieldModifier = ' full' ;
829+ inputHtml = `<input type="text" id="${ nameAttr } " name="${ nameAttr } " ${ required } class="input" placeholder="${ descAttr } " />` ;
830+ } else if ( param . type === 'date' ) {
831+ fieldModifier = ' field--date' ;
832+ inputHtml = `<input type="date" id="${ nameAttr } " name="${ nameAttr } " ${ required } class="input" />` ;
833+ } else if ( param . type === 'boolean' ) {
834+ inputHtml = `
835+ <select id="${ nameAttr } " name="${ nameAttr } " ${ required } class="select">
836+ <option value="">— Select —</option>
837+ <option value="true">True</option>
838+ <option value="false">False</option>
839+ </select>
840+ ` ;
841+ } else if ( param . type === 'number' ) {
842+ const min = param . min !== undefined ? String ( param . min ) : '' ;
843+ const max = param . max !== undefined ? String ( param . max ) : '' ;
844+ inputHtml = `<input type="number" id="${ nameAttr } " name="${ nameAttr } " min="${ min } " max="${ max } " ${ required } class="input" placeholder="${ descAttr } " />` ;
845+ } else {
846+ inputHtml = `<input type="text" id="${ nameAttr } " name="${ nameAttr } " ${ required } class="input" placeholder="${ descAttr } " />` ;
847+ }
848+
849+ return `
850+ <div class="field${ fieldModifier } ">
851+ <label class="label" for="${ nameAttr } ">
852+ ${ nameText } ${ param . required ? ' <span class="required">*</span>' : '' }
853+ </label>
854+ <span class="hint">${ descText } </span>
855+ ${ inputHtml }
856+ </div>
857+ ` ;
858+ } )
859+ . join ( '' ) + dateRangeWidget ;
801860
802861 const displayName = report . name
803862 . split ( '-' )
0 commit comments