837 lines
53 KiB
HTML
837 lines
53 KiB
HTML
<template>
|
||
<!-- LWS-safe download anchor — rendered in template (not document.body) -->
|
||
<a class="hidden-download-anchor" style="display:none;" aria-hidden="true"></a>
|
||
|
||
<div class={hostClass} onclick={handleBodyClick}>
|
||
|
||
<!-- ============== Toasts ============== -->
|
||
<template lwc:if={toasts.length}>
|
||
<div class="toast-container">
|
||
<template for:each={toasts} for:item="t">
|
||
<div key={t.id} class={t.cssClass}>
|
||
<span class="toast-icon">{t.iconLabel}</span>
|
||
<div class="toast-body">
|
||
<div class="toast-title">{t.title}</div>
|
||
<div class="toast-message">{t.message}</div>
|
||
</div>
|
||
<button class="toast-close" data-id={t.id} onclick={handleToastClose} aria-label="Dismiss">×</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============== Refresh-in-progress banner ============== -->
|
||
<template lwc:if={refreshBannerVisible}>
|
||
<div class={refreshBannerClass}>
|
||
<span class="dot-blink"></span>
|
||
<span class="banner-text">{refreshCurrentObjectLabel}</span>
|
||
<div class="banner-progress-track">
|
||
<div class="banner-progress-bar" style={refreshProgressStyle}></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============== SIDEBAR ============== -->
|
||
<aside class="sidebar">
|
||
<div class="brand">
|
||
<div class="brand-logo">
|
||
<img src={syncLogoUrl} alt="Sync Made Easy" class="brand-logo-img" />
|
||
</div>
|
||
<div class="brand-text">
|
||
<div class="t1"><b>Sync </b> <span class="italic">made easy</span></div>
|
||
<div class="t2">Your Shopify sync with Salesforce made easy</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="store-selector" onclick={toggleStoreMenu}>
|
||
<div class="store-icon">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4H6zM3 6h18M16 10a4 4 0 01-8 0"></path>
|
||
</svg>
|
||
</div>
|
||
<div class="meta">
|
||
<div class="l1">{currentStoreLabel}</div>
|
||
<div class="l2">{storeNamesPreview}</div>
|
||
</div>
|
||
<div class="caret">▾</div>
|
||
|
||
<div class={storeMenuClass}>
|
||
<template for:each={storeOptions} for:item="s">
|
||
<div key={s.id} class={s.rowClass} data-store-id={s.id} onclick={handleStoreSelect}>
|
||
<span class="store-dot"></span>
|
||
<div class="store-meta">
|
||
<div class="store-name">{s.name}</div>
|
||
</div>
|
||
<template lwc:if={s.selected}><span class="store-status">✓</span></template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav-label">OBJECTS</div>
|
||
<div class="obj-list">
|
||
<template for:each={sidebarItems} for:item="obj">
|
||
<div key={obj.id} class={obj.rowClass} data-id={obj.id} onclick={handleObjectClick}>
|
||
<div class="obj-text">
|
||
<span class="obj-icon">
|
||
<lightning-icon icon-name={obj.icon} size="x-small" variant="inverse"></lightning-icon>
|
||
</span>
|
||
<div class="obj-name">{obj.name}</div>
|
||
</div>
|
||
<div class="counts">
|
||
<template lwc:if={obj.showShopify}>
|
||
<span class="count-pill">
|
||
<span class="logo-stack" title="Shopify">
|
||
<span class="logo logo-shopify">
|
||
<svg viewBox="0 0 32 32" width="18" height="18" aria-hidden="true">
|
||
<path fill="#95BF47" d="M22.5 7.6c-.1-.7-.7-.8-1-.8l-1.5-.1-1-1c-.5-.5-.7-.4-.8-.4l-.7.2c-.4-1.1-1-2.2-2.3-2.2h-.2C14.7 3 14.4 3 14 3c-1.5 0-3 1.5-3.7 4.5l-3 .9c-1 .3-1 .3-1.1 1.3L4.5 26l13.3 2.5L25 27l-2.5-19.4zM15.5 5.8c-.3 0-.5.1-.8.2v-.3c0-1 .1-1.8.4-2.4.5.1.9.4 1.2 1-.4.5-.7 1-.8 1.5zm-1.9-1.6c-.5.6-.9 1.6-1 3v.2l-2.2.7c.4-1.4 1.3-3.6 3.2-3.9zm.5 7.5l-.6 1.7c-.3-.2-.7-.4-1.5-.4-1.6 0-1.7 1-1.7 1.3 0 1.5 3.4 2.1 3.4 5.2 0 2.5-1.5 4-3.5 4-2.3 0-3.5-1.5-3.5-1.5l.7-2.4s1.2 1.1 2.2 1.1c.6 0 .9-.5.9-.9 0-2-2.8-2-2.8-4.9 0-2.4 1.6-4.7 4.7-4.7 1.3 0 1.7.4 1.7.4z"></path>
|
||
</svg>
|
||
</span>
|
||
<span class="logo-name">Shopify</span>
|
||
</span>
|
||
<span class="count-val">{obj.shopify}</span>
|
||
</span>
|
||
</template>
|
||
<span class="count-pill">
|
||
<span class="logo-stack" title="Salesforce">
|
||
<span class="logo logo-salesforce">
|
||
<svg viewBox="0 0 32 22" width="20" height="14" aria-hidden="true">
|
||
<path fill="#00A1E0" d="M13.5 2.3a5 5 0 014 2 6 6 0 014-1.5c3.4 0 6 2.6 6 6a6 6 0 01-.2 1.5c1.4.7 2.5 2 2.5 3.7 0 2-1.5 3.6-3.4 3.8a4 4 0 01-3.4 2 4 4 0 01-2-.5 5 5 0 01-4.5 2.8 5 5 0 01-4-1.9 4 4 0 01-2 .4 4 4 0 01-4-3.4c-.6 0-1.1.2-1.6.2-1.8 0-3.3-1.5-3.3-3.3 0-1.3.7-2.4 1.8-3-.3-.6-.5-1.3-.5-2 0-2.8 2.4-5 5.2-5 1.5 0 2.9.6 3.8 1.6.4-1.5 1.7-3.4 4-3.4z"></path>
|
||
</svg>
|
||
</span>
|
||
<span class="logo-name">Salesforce</span>
|
||
</span>
|
||
<span class="count-val">{obj.sf}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Sync Health panel — title is fixed, stores list scrolls
|
||
internally if there are more than ~3 connected stores. -->
|
||
<div class="sync-health">
|
||
<div class="sync-health-title">SYNC HEALTH</div>
|
||
<div class="sync-health-stores">
|
||
<template for:each={storeHealthList} for:item="s">
|
||
<div key={s.id} class="sync-health-store">
|
||
<div class="sync-health-row">
|
||
<span class={s.statusClass} aria-hidden="true"></span>
|
||
<span class="sync-health-label">{s.name}</span>
|
||
<span class="sync-health-value">{s.statusLabel}</span>
|
||
</div>
|
||
<div class="sync-health-last">Last order: {s.lastOrderLabel}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-footer">
|
||
Made with <span class="footer-heart">❤</span> by <a href="https://syncmadeasy.com/" target="_blank" rel="noopener">Sync made easy</a>
|
||
</div>
|
||
|
||
</aside>
|
||
|
||
<!-- ============== MAIN ============== -->
|
||
<section class="main">
|
||
|
||
<div class="topbar">
|
||
<!-- Filters now live in the top bar (replaced the breadcrumb) -->
|
||
<div class="topbar-filters">
|
||
<span class="filter-label">
|
||
<svg class="filter-funnel" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true">
|
||
<path d="M2 3h12a0.5 0.5 0 0 1 0.4 0.8L10 9.2v4l-4 1.8V9.2L1.6 3.8A0.5 0.5 0 0 1 2 3z" fill="currentColor"></path>
|
||
</svg>
|
||
<span class="filter-label-text">Filter by</span>
|
||
</span>
|
||
|
||
<!-- Date Range -->
|
||
<div class="menu-wrap">
|
||
<div class="select" onclick={toggleDateRangeMenu}>
|
||
<span>{filterRangeValue}</span> <span class="caret">▾</span>
|
||
</div>
|
||
<div class={dateRangeMenuClass} onclick={stopMenuPropagation}>
|
||
<template for:each={dateRangeOptions} for:item="opt">
|
||
<div key={opt.key} class={opt.cssClass}
|
||
data-label={opt.label}
|
||
onclick={handleDateRangeSelect}>
|
||
{opt.label}
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sort By -->
|
||
<div class="menu-wrap" style="color: var(--brand-gradient) !important; ">
|
||
<div class="select sort-by" onclick={toggleSortByMenu}>
|
||
<span style="color: var(--brand-gradient);">{sortByLabel}</span> <span class="caret">▾</span>
|
||
</div>
|
||
<div class={sortByMenuClass} onclick={stopMenuPropagation}>
|
||
<template for:each={sortByOptions} for:item="opt">
|
||
<div key={opt.key} class={opt.cssClass}
|
||
data-field={opt.field} data-dir={opt.dir}
|
||
onclick={handleSortBySelect}>
|
||
{opt.label}
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="topbar-actions">
|
||
<button class="icon-btn" title={themeButtonTitle} onclick={toggleTheme}>
|
||
<span class="theme-icon">{themeIconLabel}</span>
|
||
</button>
|
||
|
||
<div class="menu-wrap">
|
||
<button class="btn" onclick={toggleExportMenu}>⬇ Export ▾</button>
|
||
<div class={exportMenuClass}>
|
||
<div class="menu-item" data-fmt="excel" onclick={handleExport}>
|
||
<span class="menu-icon">📊</span> Excel (.xls)
|
||
</div>
|
||
<div class="menu-item" data-fmt="pdf" onclick={handleExport}>
|
||
<span class="menu-icon">📄</span> PDF
|
||
</div>
|
||
<div class="menu-item" data-fmt="word" onclick={handleExport}>
|
||
<span class="menu-icon">📝</span> Word (.doc)
|
||
</div>
|
||
<div class="menu-sep"></div>
|
||
<div class="menu-item" data-fmt="csv" onclick={handleExport}>
|
||
<span class="menu-icon">🧾</span> CSV
|
||
</div>
|
||
<div class="menu-item" data-fmt="json" onclick={handleExport}>
|
||
<span class="menu-icon">🧩</span> JSON
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn" onclick={openShareModal}>↗ Share</button>
|
||
|
||
<!-- Refresh: tooltip shows next-refresh countdown; respects 4hr cooldown.
|
||
Force Refresh is offered only inside the cooldown modal — keeps the
|
||
topbar clean and signals Force is an escape hatch, not a primary action. -->
|
||
<button class="btn" onclick={handleRefresh} title={refreshButtonTitle}>⟳ Refresh</button>
|
||
|
||
<button class="btn primary" onclick={handleSchedule}>+ Schedule Report</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<div class="section-title">{activeObject.name} Overview</div>
|
||
<div class="kpi-row">
|
||
<template for:each={kpis} for:item="k">
|
||
<div key={k.id} class={k.cssClass}>
|
||
<div class="label">{k.label}</div>
|
||
<div class="value">{k.value}</div>
|
||
<div class="sub">{k.sub}</div>
|
||
<div class={k.deltaClass}>{k.delta}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="section-title">{activeObject.name} Dashboards</div>
|
||
<div class={dashRowClass}>
|
||
<template for:each={dashboards} for:item="d">
|
||
<div key={d.id} class="panel dash-panel">
|
||
<div class="panel-head">
|
||
<div class="title">{d.title}</div>
|
||
<div class="chip-group">
|
||
<button class={d.btnLineClass} data-dash-id={d.id} data-type="line" onclick={handleChartType} title="Line">📈</button>
|
||
<button class={d.btnBarClass} data-dash-id={d.id} data-type="bar" onclick={handleChartType} title="Bar">📊</button>
|
||
<button class={d.btnDonutClass} data-dash-id={d.id} data-type="donut" onclick={handleChartType} title="Donut">◐</button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-body">
|
||
|
||
<template lwc:if={d.isLine}>
|
||
<svg viewBox="0 0 540 300" preserveAspectRatio="xMidYMid meet" class="svg-line">
|
||
<defs>
|
||
<linearGradient id="lineGrad" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stop-color="#95BE45" stop-opacity="0.30"></stop>
|
||
<stop offset="100%" stop-color="#95BE45" stop-opacity="0"></stop>
|
||
</linearGradient>
|
||
</defs>
|
||
<template for:each={d.lineSvg.yTicks} for:item="y">
|
||
<g key={y.key}>
|
||
<line x1="44" x2={d.lineSvg.gridX2} y1={y.y} y2={y.y} stroke="#eef0ee" stroke-width="1"></line>
|
||
<text x="8" y={y.y} font-size="13" fill="#6b7280" dominant-baseline="middle">{y.label}</text>
|
||
</g>
|
||
</template>
|
||
<path d={d.lineSvg.areaPath} fill="url(#lineGrad)" stroke="none"></path>
|
||
<path d={d.lineSvg.linePath} fill="none" stroke="#95BE45" stroke-width="3"></path>
|
||
<template for:each={d.lineSvg.points} for:item="pt">
|
||
<g key={pt.key}>
|
||
<circle cx={pt.cx} cy={pt.cy} r="5" fill="#95BE45"></circle>
|
||
<circle cx={pt.cx} cy={pt.cy} r="14" fill="transparent" class="hover-target">
|
||
<title>{pt.tooltip}</title>
|
||
</circle>
|
||
</g>
|
||
</template>
|
||
<template for:each={d.lineSvg.xLabels} for:item="x">
|
||
<text key={x.key} x={x.x} y={d.lineSvg.labelY} font-size="13" fill="#6b7280" text-anchor="middle">{x.label}</text>
|
||
</template>
|
||
</svg>
|
||
</template>
|
||
|
||
<template lwc:elseif={d.isBar}>
|
||
<svg viewBox="0 0 540 300" preserveAspectRatio="xMidYMid meet" class="svg-bar">
|
||
<template for:each={d.barSvg.yTicks} for:item="y">
|
||
<g key={y.key}>
|
||
<line x1="44" x2={d.barSvg.gridX2} y1={y.y} y2={y.y} stroke="#eef0ee" stroke-width="1"></line>
|
||
<text x="8" y={y.y} font-size="13" fill="#6b7280" dominant-baseline="middle">{y.label}</text>
|
||
</g>
|
||
</template>
|
||
<template for:each={d.barSvg.bars} for:item="b">
|
||
<g key={b.key}>
|
||
<rect x={b.x} y={b.y} width={b.width} height={b.height} fill={b.fill} rx="4">
|
||
<title>{b.tooltip}</title>
|
||
</rect>
|
||
<text x={b.labelX} y={b.labelY} font-size="12" fill="#6b7280" text-anchor="middle">{b.label}</text>
|
||
</g>
|
||
</template>
|
||
</svg>
|
||
</template>
|
||
|
||
<template lwc:elseif={d.isDonut}>
|
||
<div class="donut-wrap">
|
||
<svg viewBox="0 0 160 160" class="svg-donut">
|
||
<circle cx="80" cy="80" r="60" fill="none" stroke="#eef0ee" stroke-width="22"></circle>
|
||
<template for:each={d.donutSvg.segments} for:item="s">
|
||
<circle key={s.key}
|
||
cx="80" cy="80" r="60"
|
||
fill="none"
|
||
stroke={s.color}
|
||
stroke-width="22"
|
||
stroke-dasharray={s.dasharray}
|
||
stroke-dashoffset={s.dashoffset}
|
||
transform="rotate(-90 80 80)">
|
||
<title>{s.tooltip}</title>
|
||
</circle>
|
||
</template>
|
||
</svg>
|
||
<div class="legend">
|
||
<template for:each={d.legend} for:item="l">
|
||
<div key={l.key} class="legend-row">
|
||
<span class="sw" style={l.swatchStyle}></span>
|
||
<span class="legend-name">{l.label}</span>
|
||
<span class="legend-value">{l.value}</span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="section-title report-title-row">
|
||
<span class="records-title">
|
||
{activeObject.name} Record Details
|
||
<span class="records-info" onclick={openFilterInfo} aria-label="Show query filter">ⓘ</span>
|
||
</span>
|
||
<div class="report-actions">
|
||
<div class="menu-wrap">
|
||
<button class="btn small" onclick={toggleFieldMenu}>⚙ Select fields</button>
|
||
<div class={fieldMenuClass} onclick={stopMenuPropagation}>
|
||
<div class="menu-title">Select fields to show — drag column headers to reorder</div>
|
||
<div class="field-search">
|
||
<input type="text" placeholder="Filter fields…" value={fieldFilterText} onkeyup={handleFieldSearch} oninput={handleFieldSearch} />
|
||
</div>
|
||
<div class="field-list">
|
||
<template for:each={fieldOptions} for:item="f">
|
||
<div key={f.key} class={f.wrapperClass}>
|
||
<span class="check-box" data-api={f.api} onclick={toggleField}>{f.visibleMark}</span>
|
||
<div class="field-meta">
|
||
<div class="field-name">{f.label}</div>
|
||
<div class="field-api">{f.api}</div>
|
||
</div>
|
||
<div class="field-reorder">
|
||
<button class="reorder-btn" data-api={f.api} onclick={moveFieldUp} disabled={f.disableMoveUp} title="Move up">▲</button>
|
||
<button class="reorder-btn" data-api={f.api} onclick={moveFieldDown} disabled={f.disableMoveDown} title="Move down">▼</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel report-panel">
|
||
<div class="table-scroll">
|
||
<table class={reportTableClass}>
|
||
<thead>
|
||
<tr>
|
||
<template for:each={reportColumns} for:item="c">
|
||
<th key={c.key} class={c.headerClass} data-field={c.api}
|
||
draggable={c.draggable}
|
||
ondragstart={handleColDragStart}
|
||
ondragover={handleColDragOver}
|
||
ondrop={handleColDrop}
|
||
ondragend={handleColDragEnd}
|
||
onclick={handleSort}
|
||
title={c.headerTitle}>
|
||
<template lwc:if={c.showPin}>
|
||
<span class={c.pinClass} title={c.pinTitle} data-field={c.api} onclick={togglePin}>📌</span>
|
||
</template>
|
||
<template lwc:if={c.showGrip}>
|
||
<span class="col-grip" aria-hidden="true">⋮⋮</span>
|
||
</template>
|
||
<span>{c.label}</span>
|
||
<span class="sort-ind">{c.sortIndicator}</span>
|
||
</th>
|
||
</template>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template for:each={reportVisibleRows} for:item="row">
|
||
<tr key={row.key}>
|
||
<template for:each={row.cells} for:item="cell">
|
||
<td key={cell.key}>
|
||
<template lwc:if={cell.isLink}>
|
||
<span class="record-link"
|
||
data-row-index={cell.rowIndex}
|
||
data-field-api={cell.key}
|
||
onclick={openRecordDetail}>{cell.value}</span>
|
||
</template>
|
||
<template lwc:else>{cell.value}</template>
|
||
</td>
|
||
</template>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="panel-footer">
|
||
<span>{reportRangeLabel}</span>
|
||
<div class="pager">
|
||
<template lwc:if={showPrevButton}>
|
||
<span class="nav" onclick={handlePagePrev}>‹ Previous</span>
|
||
</template>
|
||
<template for:each={pageButtons} for:item="p">
|
||
<span key={p.key} class={p.cssClass} data-page={p.num} onclick={handlePageGo}>{p.num}</span>
|
||
</template>
|
||
<template lwc:if={showNextButton}>
|
||
<span class="nav" onclick={handlePageNext}>Next ›</span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="footer-copy">
|
||
Copyright 2026 Product of <a href="https://hicglobalsolutions.com" target="_blank" rel="noopener">HIC Global Solutions</a> All Rights Reserved
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<!-- ============== Refresh cooldown modal ============== -->
|
||
<template lwc:if={showCooldownModal}>
|
||
<div class="modal-backdrop">
|
||
<div class="modal-card">
|
||
<div class="modal-title">Refresh on cooldown</div>
|
||
<div class="modal-body">
|
||
<p>This dashboard refreshes automatically every <strong>4 hours</strong> to protect Salesforce and Shopify API limits.</p>
|
||
<p>Next automatic refresh in:</p>
|
||
<div class="cooldown">{cooldownRemainingLabel}</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-force" onclick={handleForceRefresh}
|
||
title="Force a refresh now (bypasses 4hr cooldown). The next 4hr countdown will start from this point.">
|
||
⚡ Force refresh now
|
||
</button>
|
||
<button class="btn primary" onclick={closeCooldownModal}>Got it</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============== Schedule Report modal ============== -->
|
||
<template lwc:if={showScheduleModal}>
|
||
<div class="modal-backdrop">
|
||
<div class="modal-card schedule-card">
|
||
<div class="modal-title">Schedule Report</div>
|
||
<div class="modal-body scrollbar-hidden">
|
||
|
||
<!-- Existing schedules (queried from CronTrigger — survives page reloads) -->
|
||
<template lwc:if={schedJobsLoading}>
|
||
<div class="sched-existing-loading">Loading existing schedules…</div>
|
||
</template>
|
||
<template lwc:elseif={schedHasExistingJobs}>
|
||
<div class="sched-existing">
|
||
<div class="sched-existing-title">EXISTING SCHEDULES</div>
|
||
<template for:each={schedExistingJobs} for:item="j">
|
||
<div key={j.id} class="sched-existing-row">
|
||
<div class="sched-existing-meta">
|
||
<div class="sched-existing-name" title={j.jobName}>{j.reportName}</div>
|
||
<div class="sched-existing-sub">Next run: {j.nextRunLabel} · {j.state}</div>
|
||
</div>
|
||
<button class="btn small sched-delete-btn"
|
||
data-job-id={j.id}
|
||
onclick={handleDeleteSchedule}
|
||
disabled={j.deleting}
|
||
title="Delete this scheduled report">
|
||
<template lwc:if={j.deleting}>Deleting…</template>
|
||
<template lwc:else>🗑 Delete</template>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="sched-form">
|
||
|
||
<label class="sched-row">
|
||
<span class="sched-label">Object</span>
|
||
<input type="text" class="sched-input sched-input-readonly"
|
||
value={activeObject.name}
|
||
readonly
|
||
tabindex="-1" />
|
||
<span class="sched-hint">The dashboard you're scheduling — switch tabs in the sidebar to change.</span>
|
||
</label>
|
||
|
||
<label class="sched-row">
|
||
<span class="sched-label">Report</span>
|
||
<template lwc:if={schedReportsLoading}>
|
||
<input type="text" class="sched-input sched-input-readonly"
|
||
value="Loading reports…" readonly tabindex="-1" />
|
||
</template>
|
||
<template lwc:elseif={schedReportsEmpty}>
|
||
<input type="text" class="sched-input sched-input-readonly"
|
||
value="No reports available" readonly tabindex="-1" />
|
||
</template>
|
||
<template lwc:else>
|
||
<select class="sched-input" onchange={onSchedReportNameChange}>
|
||
<template for:each={schedReportOptions} for:item="o">
|
||
<option key={o.value} value={o.value} selected={o.selected}>{o.label}</option>
|
||
</template>
|
||
</select>
|
||
</template>
|
||
<template lwc:if={schedReportsError}>
|
||
<span class="sched-error">{schedReportsError}</span>
|
||
</template>
|
||
<span class="sched-hint">Sync Made Easy reports for this object — pick the one to run & email.</span>
|
||
</label>
|
||
|
||
<label class="sched-row">
|
||
<span class="sched-label">Frequency</span>
|
||
<select class="sched-input" value={schedFrequency} onchange={onSchedFrequencyChange}>
|
||
<template for:each={schedFrequencyOptions} for:item="o">
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
</template>
|
||
</select>
|
||
</label>
|
||
|
||
<template lwc:if={schedShowWeeklyDay}>
|
||
<label class="sched-row">
|
||
<span class="sched-label">Day of Week</span>
|
||
<select class="sched-input" value={schedDayOfWeek} onchange={onSchedDayOfWeekChange}>
|
||
<template for:each={schedDayOfWeekOptions} for:item="o">
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
</template>
|
||
</select>
|
||
</label>
|
||
</template>
|
||
|
||
<template lwc:if={schedShowYearlyMonth}>
|
||
<label class="sched-row">
|
||
<span class="sched-label">Month</span>
|
||
<select class="sched-input" value={schedMonth} onchange={onSchedMonthChange}>
|
||
<template for:each={schedMonthOptions} for:item="o">
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
</template>
|
||
</select>
|
||
</label>
|
||
</template>
|
||
|
||
<template lwc:if={schedShowMonthlyDay}>
|
||
<label class="sched-row">
|
||
<span class="sched-label">Day of Month</span>
|
||
<select class="sched-input" value={schedDayOfMonth} onchange={onSchedDayOfMonthChange}>
|
||
<template for:each={schedDayOfMonthOptions} for:item="o">
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
</template>
|
||
</select>
|
||
</label>
|
||
</template>
|
||
|
||
<label class="sched-row">
|
||
<span class="sched-label">Time (24h)</span>
|
||
<input type="time" class="sched-input"
|
||
value={schedTime} onchange={onSchedTimeChange} />
|
||
</label>
|
||
|
||
<label class="sched-row">
|
||
<span class="sched-label">Format</span>
|
||
<select class="sched-input" value={schedFormat} onchange={onSchedFormatChange}>
|
||
<template for:each={schedFormatOptions} for:item="o">
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
</template>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="sched-row">
|
||
<span class="sched-label">Recipients <span class="sched-label-hint">(comma or semicolon separated, max 5)</span></span>
|
||
<textarea class="sched-input sched-textarea"
|
||
rows="2"
|
||
placeholder="alice@syncmadeasy.com, bob@syncmadeasy.com; carol@syncmadeasy.com"
|
||
oninput={onSchedRecipientsChange}></textarea>
|
||
<div class="rcpt-summary">{schedRecipientSummary}</div>
|
||
<template lwc:if={schedRecipientPills.length}>
|
||
<div class="rcpt-pills">
|
||
<template for:each={schedRecipientPills} for:item="r">
|
||
<span key={r.key} class={r.cssClass} title={r.email}>
|
||
<span class="rcpt-icon">{r.icon}</span> {r.email}
|
||
</span>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</label>
|
||
|
||
<template lwc:if={schedError}>
|
||
<div class="sched-error">{schedError}</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn" onclick={closeScheduleModal} disabled={schedSubmitting}>Cancel</button>
|
||
<button class="btn primary" onclick={submitSchedule} disabled={schedSubmitting}>
|
||
<template lwc:if={schedSubmitting}>Scheduling…</template>
|
||
<template lwc:else>Schedule</template>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============== Share modal ============== -->
|
||
<template lwc:if={showShareModal}>
|
||
<div class="modal-backdrop">
|
||
<div class="modal-card share-card">
|
||
<div class="modal-title">{shareTitle}</div>
|
||
|
||
<template lwc:if={shareStep1}>
|
||
<div class="share-grid">
|
||
<div class="share-tile" data-fmt="pdf" onclick={selectShareFormat}>
|
||
<span class="share-tile-icon">📄</span>
|
||
<span class="share-tile-label">PDF</span>
|
||
</div>
|
||
<div class="share-tile" data-fmt="excel" onclick={selectShareFormat}>
|
||
<span class="share-tile-icon">📊</span>
|
||
<span class="share-tile-label">Excel</span>
|
||
</div>
|
||
<div class="share-tile" data-fmt="csv" onclick={selectShareFormat}>
|
||
<span class="share-tile-icon">🧾</span>
|
||
<span class="share-tile-label">CSV</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template lwc:elseif={shareStep2}>
|
||
<div class="share-format-pill">Sharing as <strong>{shareFormatLabel}</strong></div>
|
||
<div class="share-grid">
|
||
<div class="share-tile" data-dest="WhatsApp" onclick={selectShareDestination}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true">
|
||
<path fill="#25D366" d="M4.868 43.303l2.694-9.835C5.9 30.59 5.026 27.324 5.027 23.979 5.032 13.514 13.548 5 24.014 5c5.079.002 9.845 1.979 13.43 5.566 3.584 3.588 5.558 8.356 5.556 13.428C42.994 34.467 34.478 43 24.014 43c-3.174 0-6.293-.86-9.007-2.473L4.868 43.303z"/>
|
||
<path fill="#fff" d="M24.014 7C14.65 7 7.032 14.618 7.032 23.983c0 3.27.87 6.451 2.52 9.231l.382.645-1.62 5.914 6.074-1.594.624.37A16.89 16.89 0 0024.014 41c9.365 0 16.983-7.617 16.983-16.983 0-4.535-1.766-8.796-4.97-12.001C32.822 8.81 28.561 7.044 24.014 7z"/>
|
||
<path fill="#25D366" d="M19.268 15.916c-.36-.8-.739-.816-1.082-.83-.28-.012-.601-.011-.921-.011s-.841.12-1.281.601c-.441.48-1.682 1.642-1.682 4.003s1.722 4.644 1.962 4.964c.24.32 3.322 5.324 8.204 7.252 4.06 1.603 4.882 1.283 5.763 1.203.881-.08 2.843-1.162 3.243-2.284.401-1.122.401-2.084.281-2.284-.12-.2-.441-.32-.921-.561-.48-.24-2.843-1.402-3.283-1.562-.441-.16-.761-.24-1.082.24-.32.48-1.241 1.562-1.521 1.882-.281.32-.561.36-1.041.12-.48-.24-2.027-.747-3.862-2.382-1.428-1.273-2.392-2.845-2.672-3.325-.28-.48-.03-.74.21-.98.217-.216.48-.561.721-.841.24-.28.32-.48.48-.801.16-.32.08-.601-.04-.841-.12-.24-1.062-2.604-1.476-3.561z"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">WhatsApp</span>
|
||
</div>
|
||
<div class="share-tile" data-dest="Outlook" onclick={selectShareDestination}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true">
|
||
<path fill="#1976D2" d="M28 13h14.533C43.343 13 44 13.657 44 14.467v19.066C44 34.343 43.343 35 42.533 35H28V13z"/>
|
||
<path fill="#2196F3" d="M28 6H42.533C43.343 6 44 6.657 44 7.467V14H28V6z"/>
|
||
<path fill="#1E88E5" d="M28 34h16v5.533C44 40.343 43.343 41 42.533 41H28V34z"/>
|
||
<path fill="#1565C0" d="M28 13v22L4 32V16l24-3z"/>
|
||
<path fill="#fff" d="M15 19.5c-3.584 0-6 2.692-6 5.5s2.416 5.5 6 5.5 6-2.692 6-5.5-2.416-5.5-6-5.5zm0 8.5c-1.88 0-3.5-1.343-3.5-3s1.62-3 3.5-3 3.5 1.343 3.5 3-1.62 3-3.5 3z"/>
|
||
<path fill="#E3F2FD" d="M38 15.5h-8v3h8v-3zm0 5h-8v3h8v-3zm0 5h-8v3h8v-3z"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Outlook</span>
|
||
</div>
|
||
<div class="share-tile" data-dest="Gmail" onclick={selectShareDestination}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true">
|
||
<path fill="#4caf50" d="M45 16.2l-5 2.75-8 4.75V40h11c1.1 0 2-.9 2-2V16.2z"/>
|
||
<path fill="#1e88e5" d="M3 16.2l5 2.75 8 4.75V40H5c-1.1 0-2-.9-2-2V16.2z"/>
|
||
<path fill="#e53935" d="M35 11.2L24 19.45 13 11.2 8 14.2V24l8 4.75L24 24l8 4.75 8-4.75V14.2z"/>
|
||
<path fill="#c62828" d="M3 7.38v8.82l5-2.75L13 11.2V8c0-1.1.9-2 2-2h1.45L24 12l7.55-6H33c1.1 0 2 .9 2 2v3.2l5 2.25 3-1.84V7.38C43 5.51 40.98 4 38.5 4h-29C7.02 4 5 5.51 3 7.38z"/>
|
||
<path fill="#fbc02d" d="M45 16.2l-10-5V40h8c1.1 0 2-.9 2-2V16.2z"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Gmail</span>
|
||
</div>
|
||
<div class="share-tile" data-dest="Slack" onclick={selectShareDestination}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true">
|
||
<path fill="#e01e5a" d="M13 28a3 3 0 110-6h3v3a3 3 0 01-3 3zm1.5 0a3 3 0 016 0v7.5a3 3 0 01-6 0V28z"/>
|
||
<path fill="#36c5f0" d="M20 13a3 3 0 116 0v3h-3a3 3 0 01-3-3zm0 1.5a3 3 0 010 6h-7.5a3 3 0 010-6H20z"/>
|
||
<path fill="#2eb67d" d="M35 20a3 3 0 11 0 6h-3v-3a3 3 0 013-3zm-1.5 0a3 3 0 01-6 0v-7.5a3 3 0 016 0V20z"/>
|
||
<path fill="#ecb22e" d="M28 35a3 3 0 11-6 0v-3h3a3 3 0 013 3zm0-1.5a3 3 0 010-6h7.5a3 3 0 010 6H28z"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Slack</span>
|
||
</div>
|
||
<div class="share-tile" data-dest="Teams" onclick={selectShareDestination}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true">
|
||
<path fill="#5059c9" d="M44 22v10c0 3.314-2.686 6-6 6s-6-2.686-6-6V20h10c1.105 0 2 .895 2 2z"/>
|
||
<circle fill="#5059c9" cx="38" cy="10" r="4"/>
|
||
<circle fill="#7b83eb" cx="26" cy="10" r="5"/>
|
||
<path fill="#7b83eb" d="M34 20v13c0 4.971-4.029 9-9 9s-9-4.029-9-9V20h18z"/>
|
||
<path fill="#4b53bc" d="M28 20v13.5c0 1.933-1.567 3.5-3.5 3.5H12.031C12.011 36.672 12 36.338 12 36V20H2v-4c0-1.105.895-2 2-2h24c1.105 0 2 .895 2 2v4z" opacity=".8"/>
|
||
<path fill="#fff" d="M19 17h-3v11h-3V17H10v-3h9v3z"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Teams</span>
|
||
</div>
|
||
<div class="share-tile" data-dest="More" onclick={selectShareDestination}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 32 32" width="40" height="40" aria-hidden="true">
|
||
<rect width="32" height="32" rx="6" fill="#475569"/>
|
||
<circle cx="9.5" cy="16" r="2" fill="#fff"/>
|
||
<circle cx="16" cy="16" r="2" fill="#fff"/>
|
||
<circle cx="22.5" cy="16" r="2" fill="#fff"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">More</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="modal-actions">
|
||
<template lwc:if={shareStep2}>
|
||
<button class="btn" onclick={backToShareStep1}>← Back</button>
|
||
</template>
|
||
<button class="btn" onclick={closeShareModal}>Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============== More share options sub-modal ============== -->
|
||
<template lwc:if={showMoreShareModal}>
|
||
<div class="modal-backdrop">
|
||
<div class="modal-card share-card">
|
||
<div class="modal-title">More share options</div>
|
||
<div class="share-format-pill">Sharing as <strong>{shareFormatLabel}</strong></div>
|
||
<p class="more-share-hint">
|
||
Save the file to your downloads, then open it with any app on your device.
|
||
</p>
|
||
<div class="share-grid">
|
||
<div class="share-tile" data-channel="copy" onclick={handleMoreShareOption}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 32 32" width="40" height="40" aria-hidden="true">
|
||
<rect width="32" height="32" rx="6" fill="#0ea5e9"/>
|
||
<rect x="8" y="6" width="14" height="18" rx="2" fill="#fff"/>
|
||
<rect x="10" y="9" width="10" height="12" rx="1" fill="#0ea5e9"/>
|
||
<rect x="6" y="9" width="14" height="18" rx="2" fill="#fff" opacity="0.85"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Copy Download Link</span>
|
||
</div>
|
||
<div class="share-tile" data-channel="saveFile" onclick={handleMoreShareOption}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 32 32" width="40" height="40" aria-hidden="true">
|
||
<rect width="32" height="32" rx="6" fill="#95BE45"/>
|
||
<path d="M16 6v12m-5-5l5 5 5-5" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<path d="M6 22v3a2 2 0 002 2h16a2 2 0 002-2v-3" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Save File</span>
|
||
</div>
|
||
<!-- <div class="share-tile" data-channel="email" onclick={handleMoreShareOption}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 32 32" width="40" height="40" aria-hidden="true">
|
||
<rect width="32" height="32" rx="6" fill="#6366f1"/>
|
||
<rect x="5" y="9" width="22" height="14" rx="2" fill="#fff"/>
|
||
<path d="M5 11l11 7 11-7" fill="none" stroke="#6366f1" stroke-width="1.5" stroke-linejoin="round"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Email</span>
|
||
</div> -->
|
||
<div class="share-tile" data-channel="print" onclick={handleMoreShareOption}>
|
||
<span class="share-tile-icon">
|
||
<svg viewBox="0 0 32 32" width="40" height="40" aria-hidden="true">
|
||
<rect width="32" height="32" rx="6" fill="#64748b"/>
|
||
<rect x="8" y="5" width="16" height="8" rx="1" fill="#fff"/>
|
||
<rect x="5" y="12" width="22" height="10" rx="2" fill="#fff"/>
|
||
<rect x="9" y="19" width="14" height="8" rx="1" fill="#fff" stroke="#64748b" stroke-width="0.8"/>
|
||
<circle cx="22" cy="16" r="1" fill="#64748b"/>
|
||
</svg>
|
||
</span>
|
||
<span class="share-tile-label">Print</span>
|
||
</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn" onclick={backFromMoreShare}>← Back</button>
|
||
<button class="btn" onclick={closeMoreShareModal}>Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============== Filter info modal (click ⓘ on Records header) ============== -->
|
||
<template lwc:if={showFilterInfoModal}>
|
||
<div class="modal-backdrop">
|
||
<div class="modal-card filter-info-card">
|
||
<div class="modal-title">Active filter on this report</div>
|
||
<div class="modal-body">
|
||
<ul class="filter-info-list">
|
||
<template for:each={activeObjectFilterLines} for:item="line">
|
||
<li key={line.key} class="filter-info-row">{line.value}</li>
|
||
</template>
|
||
</ul>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn primary" onclick={closeFilterInfo}>Got it</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ============== Record Detail modal ============== -->
|
||
<template lwc:if={showRecordDetailModal}>
|
||
<div class="modal-backdrop">
|
||
<div class="modal-card record-detail-card">
|
||
<div class="modal-title">{recordDetailTitle}</div>
|
||
<div class="modal-body record-detail-body">
|
||
<template lwc:if={recordDetailLoading}>
|
||
<div class="record-detail-loading">Loading related record…</div>
|
||
</template>
|
||
<template lwc:else>
|
||
<div class="record-detail-field record-detail-id-row">
|
||
<span class="record-detail-label">Record ID</span>
|
||
<a class="record-detail-value record-id-link"
|
||
href={recordDetailUrl}
|
||
target="_blank"
|
||
rel="noopener"
|
||
title="Open this record in Salesforce">{recordDetailId} ↗</a>
|
||
</div>
|
||
<template for:each={recordDetailFields} for:item="f">
|
||
<div key={f.key} class="record-detail-field">
|
||
<span class="record-detail-label">{f.label}</span>
|
||
<span class="record-detail-value">{f.value}</span>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn" onclick={closeRecordDetail}>Close</button>
|
||
<button class="btn primary" onclick={openFullRecordPage} disabled={recordDetailLoading}>Open Full Record ↗</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
</div>
|
||
</template> |