wahyuun
10 months ago
40 changed files with 3076 additions and 212 deletions
@ -0,0 +1,270 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcCore from '@grapecity/wijmo'; |
||||||
|
import { Grid } from './components/Grid'; |
||||||
|
import { RadialGauge } from './components/RadialGauge'; |
||||||
|
import { LinearGauge } from './components/LinearGauge'; |
||||||
|
import { BarChart } from './components/BarChart'; |
||||||
|
import { ColumnChart } from './components/ColumnChart'; |
||||||
|
import { LineChart } from './components/LineChart'; |
||||||
|
import { BubbleChart } from './components/BubbleChart'; |
||||||
|
import { BulletGraph } from './components/BulletGraph'; |
||||||
|
import { Blank } from './components/Blank'; |
||||||
|
import { Tile } from './components/Tile'; |
||||||
|
// Wijmo and Material Design Lite |
||||||
|
import '@grapecity/wijmo.styles/themes/material/wijmo.theme.material.indigo-amber.css'; |
||||||
|
// Styles |
||||||
|
import './index.css'; |
||||||
|
|
||||||
|
import './license' |
||||||
|
|
||||||
|
export default class App extends React.Component { |
||||||
|
constructor() { |
||||||
|
super(...arguments); |
||||||
|
// Color palette |
||||||
|
this.palette = ['#8e99f3', '#ffca28', '#5c6bc0', '#bbdefb']; |
||||||
|
// Icons assets |
||||||
|
this.icons = { |
||||||
|
grid: [ |
||||||
|
<path d="M57,5H7A2,2,0,0,0,5,7v7H59V7A2,2,0,0,0,57,5Zm1,19V23H46V15H45v8H33V15H32v8H19V15H18v8H6v1H18v7H6v1H18v8H6v1H18v8H6v1H18v8h1V50H32v8h1V50H45v8h1V50H58V49H46V41H58V40H46V32H58V31H46V24ZM19,24H32v7H19Zm0,8H32v8H19Zm0,17V41H32v8Zm26,0H33V41H45Zm0-9H33V32H45Zm0-9H33V24H45Z" fill={this.palette[3]}/>, |
||||||
|
<path d="M57,5H7A2,2,0,0,0,5,7V57a2,2,0,0,0,2,2H57a2,2,0,0,0,2-2V7A2,2,0,0,0,57,5ZM7,6H57a1,1,0,0,1,1,1v7H6V7A1,1,0,0,1,7,6ZM57,58H7a1,1,0,0,1-1-1V15H58V57A1,1,0,0,1,57,58Z" fill={this.palette[0]}/>, |
||||||
|
], |
||||||
|
barChart: [ |
||||||
|
<rect x="12" y="34" width="40" height="7" fill={this.palette[1]}/>, |
||||||
|
<rect x="12" y="46" width="33" height="7" fill={this.palette[2]}/>, |
||||||
|
<rect x="12" y="11" width="32" height="7" fill={this.palette[3]}/>, |
||||||
|
<path d="M36,22v7H12V22ZM10,14H8v1h2Zm0,11H8v1h2Zm0,12H8v1h2Zm0,12H8v1h2Zm49,9H7a.94.94,0,0,1-1-1V5H5V57a2,2,0,0,0,2,2H59Z" fill={this.palette[0]}/>, |
||||||
|
], |
||||||
|
columnChart: [ |
||||||
|
<rect x="23" y="12.02" width="7" height="40" fill={this.palette[1]}/>, |
||||||
|
<rect x="11" y="19.02" width="7" height="33" fill={this.palette[2]}/>, |
||||||
|
<rect x="46" y="20.02" width="7" height="32" fill={this.palette[3]}/>, |
||||||
|
<path d="M41,52H34V26h7Zm8,2v2h1V54ZM37,54v2h1V54ZM26,54v2h1V54ZM14,54v2h1V54ZM5,5V57a2,2,0,0,0,2,2H58V58H7a1,1,0,0,1-1-1V5Z" fill={this.palette[0]}/>, |
||||||
|
], |
||||||
|
bubbleChart: [ |
||||||
|
<path d="M59,58H7a.94.94,0,0,1-1-1V5H5V57a2,2,0,0,0,2,2H59Z" fill={this.palette[0]}/>, |
||||||
|
<path d="M36,23a2,2,0,1,1,2,2A2,2,0,0,1,36,23ZM13.63,29.07a2,2,0,1,0-2-2A2,2,0,0,0,13.63,29.07Zm9,12a2,2,0,1,0-2-2A2,2,0,0,0,22.63,41.07Zm24-5a2,2,0,1,0-2-2A2,2,0,0,0,46.63,36.07Zm-2.5,17a1.5,1.5,0,1,0-1.5-1.5A1.5,1.5,0,0,0,44.13,53.07Z" fill={this.palette[2]}/>, |
||||||
|
<path d="M19,12a4,4,0,1,1,4,4A4,4,0,0,1,19,12Zm6.63,16.07a3,3,0,1,0-3-3A3,3,0,0,0,25.63,28.07Zm11.5,8a3.5,3.5,0,1,0-3.5-3.5A3.5,3.5,0,0,0,37.13,36.07Zm-1,10a2.5,2.5,0,1,0-2.5-2.5A2.5,2.5,0,0,0,36.13,46.07Zm14,0a2.5,2.5,0,1,0-2.5-2.5A2.5,2.5,0,0,0,50.13,46.07Z" fill={this.palette[3]}/>, |
||||||
|
], |
||||||
|
lineChart: [ |
||||||
|
<polygon points="51 20.41 49.59 19 32.5 36.97 27.5 31.97 11 48.48 12.5 49.98 27.5 34.97 32.5 39.97 51 20.41" fill={this.palette[3]}/>, |
||||||
|
<path d="M6,5V57a1,1,0,0,0,1,1H58v1H7a2,2,0,0,1-2-2V5Z" fill={this.palette[0]}/>, |
||||||
|
<polygon points="34.92 30.42 27.5 23 11 39.51 12.5 41.01 27.5 26 33.42 31.92 34.92 30.42" fill={this.palette[2]}/>, |
||||||
|
<polygon points="40.58 36.08 39.08 37.58 45.97 44.47 47.38 42.88 40.58 36.08" fill={this.palette[2]}/>, |
||||||
|
], |
||||||
|
radialGauge: [ |
||||||
|
<circle cx="32" cy="32" r="4" fill={this.palette[1]}/>, |
||||||
|
<path d="M32,6A26,26,0,1,1,6,32,26,26,0,0,1,32,6m0-1A27,27,0,1,0,59,32,27,27,0,0,0,32,5ZM43.37,20.63a1.49,1.49,0,0,0-2.12,0l-6.84,6.84a6.51,6.51,0,0,1,2.12,2.12l6.84-6.84A1.49,1.49,0,0,0,43.37,20.63Z" fill={this.palette[0]}/>, |
||||||
|
<path d="M34,11a2,2,0,1,1-2-2A2,2,0,0,1,34,11ZM17,15a2,2,0,1,0,2,2A2,2,0,0,0,17,15Zm30,0a2,2,0,1,0,2,2A2,2,0,0,0,47,15ZM11,28a2,2,0,1,0,2,2A2,2,0,0,0,11,28Zm42,.91a2,2,0,1,0,2,2A2,2,0,0,0,53,28.91ZM32,40.76A25.87,25.87,0,0,0,14.09,48a23.95,23.95,0,0,0,35.83,0A25.88,25.88,0,0,0,32,40.76Z" fill={this.palette[3]}/>, |
||||||
|
], |
||||||
|
linearGauge: [ |
||||||
|
<path d="M29.5,19A4.5,4.5,0,1,1,34,23.5,4.49,4.49,0,0,1,29.5,19Zm-1.15-2H11a2,2,0,0,0-1.9,2.65,2,2,0,0,0,2,1.35H28.21A5.72,5.72,0,0,1,28,19.5c0-.08,0-.15,0-.23s0-.18,0-.27A6,6,0,0,1,28.35,17ZM54.9,18.35a2,2,0,0,0-2-1.35H39.65a5.89,5.89,0,0,1,0,4H53A2,2,0,0,0,54.9,18.35Z" fill={this.palette[2]}/>, |
||||||
|
<path d="M53,36H29.05a6,6,0,0,0,.29-1.85,6.13,6.13,0,0,0-.4-2.15h24a2,2,0,0,1,2,1.35A2,2,0,0,1,53,36ZM17.74,32H11a2,2,0,0,0-1.9,2.65,2,2,0,0,0,2,1.35h6.55a6.28,6.28,0,0,1-.29-1.85A6.13,6.13,0,0,1,17.74,32Zm5.6,6.65a4.5,4.5,0,1,0-4.5-4.5A4.5,4.5,0,0,0,23.34,38.65Z" fill={this.palette[3]}/>, |
||||||
|
<path d="M38.34,49.15A6.28,6.28,0,0,0,38.63,51H11.08a2,2,0,0,1-2-1.35A2,2,0,0,1,11,47H38.74A6.13,6.13,0,0,0,38.34,49.15Zm16.56-.8a2,2,0,0,0-2-1.35h-3a6.13,6.13,0,0,1,.4,2.15A6,6,0,0,1,50.05,51h3A2,2,0,0,0,54.9,48.35Zm-10.56,5.3a4.5,4.5,0,1,0-4.5-4.5A4.5,4.5,0,0,0,44.34,53.65Z" fill={this.palette[1]}/>, |
||||||
|
], |
||||||
|
bulletGraph: [ |
||||||
|
<rect x="41" y="17" width="14" height="11" fill={this.palette[2]}/>, |
||||||
|
<rect x="40.89" y="33.96" width="14" height="11" fill={this.palette[2]}/>, |
||||||
|
<polygon points="25 17 25 19 34 19 34 26 25 26 25 28 39 28 39 17 25 17" fill={this.palette[1]}/>, |
||||||
|
<polygon points="25 36 29 36 29 43 25 43 25 45 39 45 39 34 25 34 25 36" fill={this.palette[1]}/>, |
||||||
|
<rect x="9" y="26" width="14" height="2" fill={this.palette[3]}/>, |
||||||
|
<rect x="9" y="17" width="14" height="2" fill={this.palette[3]}/>, |
||||||
|
<rect x="9" y="34" width="14" height="2" fill={this.palette[3]}/>, |
||||||
|
<rect x="9" y="43" width="14" height="2" fill={this.palette[3]}/>, |
||||||
|
<path d="M49,53v2h1V53ZM37,53v2h1V53ZM26,53v2h1V53ZM14,53v2h1V53ZM31,24H9V21H31ZM26,41H9V38H26ZM60,58H6V57H60Z" fill={this.palette[0]}/>, |
||||||
|
], |
||||||
|
blank: [ |
||||||
|
<path d="M40,5V6H33V5ZM24,6h7V5H24ZM15,6h7V5H15Zm7,53V58H15v1Zm9-1H24v1h7ZM6,49V42H5v7ZM5,22H6V15H5ZM51,6h6a1,1,0,0,1,1,1v6h1V7a2,2,0,0,0-2-2H51ZM6,31V24H5v7Zm0,9V33H5v7ZM58,15v7h1V15ZM6,13V7A1,1,0,0,1,7,6h6V5H7A2,2,0,0,0,5,7v6ZM58,51v6a1,1,0,0,1-1,1H51v1h6a2,2,0,0,0,2-2V51ZM13,58H7a1,1,0,0,1-1-1V51H5v6a2,2,0,0,0,2,2h6ZM58,24v7h1V24Zm1,18H58v7h1ZM49,58H42v1h7ZM42,5V6h7V5ZM40,58H33v1h7ZM58,33v7h1V33Z" fill={this.palette[0]}/>, |
||||||
|
], |
||||||
|
}; |
||||||
|
// Tile names and types |
||||||
|
this.tileCatalog = [ |
||||||
|
{ name: 'Grid', tile: Grid, icon: this.icons.grid }, |
||||||
|
{ name: 'Radial Gauge', tile: RadialGauge, icon: this.icons.radialGauge }, |
||||||
|
{ name: 'Linear Gauge', tile: LinearGauge, icon: this.icons.linearGauge }, |
||||||
|
{ name: 'Bar Chart', tile: BarChart, icon: this.icons.barChart }, |
||||||
|
{ name: 'Column Chart', tile: ColumnChart, icon: this.icons.columnChart }, |
||||||
|
{ name: 'Line Chart', tile: LineChart, icon: this.icons.lineChart }, |
||||||
|
{ name: 'Bubble Chart', tile: BubbleChart, icon: this.icons.bubbleChart }, |
||||||
|
{ name: 'Bullet Graph', tile: BulletGraph, icon: this.icons.bulletGraph }, |
||||||
|
{ name: 'Blank', tile: Blank, icon: this.icons.blank }, |
||||||
|
]; |
||||||
|
this.key = 0; |
||||||
|
this.state = { |
||||||
|
isWideMenu: false, |
||||||
|
tileCatalog: new wjcCore.CollectionView(this.tileCatalog), |
||||||
|
tiles: this.getTiles(), |
||||||
|
key: this.key, |
||||||
|
data: this.getData(), |
||||||
|
}; |
||||||
|
} |
||||||
|
// tiles currently in use |
||||||
|
getTiles() { |
||||||
|
return [ |
||||||
|
{ name: this.tileCatalog[1].name, key: this.key += 1 }, |
||||||
|
{ name: this.tileCatalog[2].name, key: this.key += 1 }, |
||||||
|
{ name: this.tileCatalog[5].name, key: this.key += 1 }, |
||||||
|
{ name: this.tileCatalog[7].name, key: this.key += 1 }, |
||||||
|
{ name: this.tileCatalog[0].name, key: this.key += 1 }, |
||||||
|
]; |
||||||
|
} |
||||||
|
// generate some data to show in the tiles |
||||||
|
getData() { |
||||||
|
let data = []; |
||||||
|
const today = new Date(); |
||||||
|
for (let i = 0; i < 12; i++) { |
||||||
|
const sales = 100 + Math.random() * 800 + i * 50; |
||||||
|
const expenses = 50 + Math.random() * 300 + i * 5; |
||||||
|
data.push({ |
||||||
|
id: i, |
||||||
|
date: wjcCore.DateTime.addMonths(today, 12 - i), |
||||||
|
sales: sales, |
||||||
|
expenses: expenses, |
||||||
|
profit: sales - expenses, |
||||||
|
}); |
||||||
|
} |
||||||
|
return data; |
||||||
|
} |
||||||
|
// gets a tile content by name |
||||||
|
getTileContent(name) { |
||||||
|
const { data, tileCatalog } = this.state; |
||||||
|
const arr = tileCatalog.items; |
||||||
|
for (let i = 0; i < arr.length; i++) { |
||||||
|
if (arr[i].name == name) { |
||||||
|
return React.createElement(arr[i].tile, { |
||||||
|
data: new wjcCore.CollectionView(data), |
||||||
|
palette: this.palette |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
throw '*** tile not found: ' + name; |
||||||
|
} |
||||||
|
// adds a tile to the dashboard |
||||||
|
addTile(name) { |
||||||
|
const { tiles, key: stateKey } = this.state; |
||||||
|
const key = stateKey + 1; |
||||||
|
this.setState({ tiles: [{ name, key }, ...tiles], key }); |
||||||
|
} |
||||||
|
// removes a tile from the dashboard |
||||||
|
removeTile(tileIndex) { |
||||||
|
const tiles = this.state.tiles.filter((item, index) => index != tileIndex); |
||||||
|
this.setState({ tiles: tiles }); |
||||||
|
} |
||||||
|
// initialize component after it has been mounted |
||||||
|
componentDidMount() { |
||||||
|
// enable tile drag/drop |
||||||
|
const panel = document.querySelector('.dashboard'); |
||||||
|
this.enableItemReorder(panel); |
||||||
|
} |
||||||
|
// allow users to re-order elements within a panel element |
||||||
|
// we work with the DOM elements and update the state when done. |
||||||
|
enableItemReorder(panel) { |
||||||
|
let dragSource = null; |
||||||
|
let dropTarget = null; |
||||||
|
// add drag/drop event listeners |
||||||
|
panel.addEventListener('dragstart', (e) => { |
||||||
|
const target = wjcCore.closest(e.target, '.tile'); |
||||||
|
if (target && target.parentElement == panel) { |
||||||
|
dragSource = target; |
||||||
|
wjcCore.addClass(dragSource, 'drag-source'); |
||||||
|
const dt = e.dataTransfer; |
||||||
|
dt.effectAllowed = 'move'; |
||||||
|
dt.setData('text', dragSource.innerHTML); |
||||||
|
} |
||||||
|
}); |
||||||
|
panel.addEventListener('dragover', (e) => { |
||||||
|
if (dragSource) { |
||||||
|
let tile = wjcCore.closest(e.target, '.tile'); |
||||||
|
if (tile == dragSource) { |
||||||
|
tile = null; |
||||||
|
} |
||||||
|
if (dragSource && tile && tile != dragSource) { |
||||||
|
e.preventDefault(); |
||||||
|
e.dataTransfer.dropEffect = 'move'; |
||||||
|
} |
||||||
|
if (dropTarget != tile) { |
||||||
|
wjcCore.removeClass(dropTarget, 'drag-over'); |
||||||
|
dropTarget = tile; |
||||||
|
wjcCore.addClass(dropTarget, 'drag-over'); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
panel.addEventListener('drop', (e) => { |
||||||
|
if (dragSource && dropTarget) { |
||||||
|
// finish drag/drop |
||||||
|
e.stopPropagation(); |
||||||
|
e.stopImmediatePropagation(); |
||||||
|
e.preventDefault(); |
||||||
|
// re-order HTML elements (optional here, we're updating the state later) |
||||||
|
const srcIndex = getIndex(dragSource); |
||||||
|
const dstIndex = getIndex(dropTarget); |
||||||
|
const refChild = srcIndex > dstIndex ? dropTarget : dropTarget.nextElementSibling; |
||||||
|
dragSource.parentElement.insertBefore(dragSource, refChild); |
||||||
|
// focus and view on the tile that was dragged |
||||||
|
dragSource.focus(); |
||||||
|
// update state |
||||||
|
let tiles = this.state.tiles.slice(); |
||||||
|
tiles.splice(srcIndex, 1); |
||||||
|
tiles.splice(dstIndex, 0, this.state.tiles[srcIndex]); |
||||||
|
this.setState({ tiles: tiles }); |
||||||
|
} |
||||||
|
}); |
||||||
|
panel.addEventListener('dragend', () => { |
||||||
|
wjcCore.removeClass(dragSource, 'drag-source'); |
||||||
|
wjcCore.removeClass(dropTarget, 'drag-over'); |
||||||
|
dragSource = dropTarget = null; |
||||||
|
}); |
||||||
|
function getIndex(e) { |
||||||
|
const p = e.parentElement; |
||||||
|
for (let i = 0; i < p.children.length; i++) { |
||||||
|
if (p.children[i] == e) |
||||||
|
return i; |
||||||
|
} |
||||||
|
return -1; |
||||||
|
} |
||||||
|
} |
||||||
|
// render the dashboard |
||||||
|
render() { |
||||||
|
const { tiles, isWideMenu } = this.state; |
||||||
|
// animated toggle menu |
||||||
|
const renderMenuToggle = (<div className={'menu-toggle' + (this.state.isWideMenu ? ' menu--open' : '')} onClick={() => this.setState({ isWideMenu: !isWideMenu })}> |
||||||
|
<svg width="30" height="20" viewBox="0 0 30 20" fill={this.palette[2]}> |
||||||
|
<rect x="10" y="5" width="11" height="1"/> |
||||||
|
<rect x="10" y="15" width="11" height="1"/> |
||||||
|
<polygon points="29.48 10.27 24.23 5.03 23.52 5.73 27.79 10 10 10 10 11 27.79 11 23.52 15.27 24.23 15.97 29.48 10.73 29.7 10.5 29.48 10.27"/> |
||||||
|
</svg> |
||||||
|
</div>); |
||||||
|
// menu items |
||||||
|
const renderMenuItems = (<React.Fragment> |
||||||
|
{this.tileCatalog.map((item) => (<div key={`Menu ${item.name}`} className="menu-item" title={item.name} onClick={() => this.addTile(item.name)}> |
||||||
|
<svg width="64" height="64" viewBox="0 0 64 64"> |
||||||
|
{item.icon.map((entity, key) => (<React.Fragment key={`Menu Item ${key}`}>{entity}</React.Fragment>))} |
||||||
|
</svg> |
||||||
|
<div className="menu-item-name">{item.name}</div> |
||||||
|
</div>))} |
||||||
|
</React.Fragment>); |
||||||
|
// displayed when the dashboard is empty |
||||||
|
const renderBlankTile = (<div className="blank"> |
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill={this.palette[0]}> |
||||||
|
<path d="M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H16L12,22L8,18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M4,4V16H8.83L12,19.17L15.17,16H20V4H4M11,6H13V9H16V11H13V14H11V11H8V9H11V6Z"/> |
||||||
|
</svg> |
||||||
|
<div>Click on an item on the menu bar to add the new tile to the dashboard.</div> |
||||||
|
</div>); |
||||||
|
// list of tiles |
||||||
|
const renderTiles = (<React.Fragment> |
||||||
|
{tiles.map((item, index) => (<Tile header={item.name} content={this.getTileContent(item.name)} onRemove={this.removeTile.bind(this)} index={index} key={item.key}/>))} |
||||||
|
</React.Fragment>); |
||||||
|
const renderDashboard = tiles.length ? renderTiles : renderBlankTile; |
||||||
|
return (<div className="container"> |
||||||
|
<div className={`menu ${isWideMenu ? 'menu--open' : ''}`}> |
||||||
|
{renderMenuToggle} |
||||||
|
{renderMenuItems} |
||||||
|
</div> |
||||||
|
<div className="hr"/> |
||||||
|
<div className="content"> |
||||||
|
<div className="dashboard">{renderDashboard}</div> |
||||||
|
</div> |
||||||
|
</div>); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcChart from '@grapecity/wijmo.chart'; |
||||||
|
import * as wjChart from '@grapecity/wijmo.react.chart'; |
||||||
|
export const BarChart = ({ data, palette }) => (<wjChart.FlexChart chartType={wjcChart.ChartType.Bar} itemsSource={data} palette={palette} bindingX="date"> |
||||||
|
<wjChart.FlexChartAxis wjProperty="axisX" format="MMM-yy"/> |
||||||
|
<wjChart.FlexChartSeries name="Sales" binding="sales"/> |
||||||
|
<wjChart.FlexChartSeries name="Expenses" binding="expenses"/> |
||||||
|
<wjChart.FlexChartSeries name="Profit" binding="profit" chartType={wjcChart.ChartType.LineSymbols}/> |
||||||
|
</wjChart.FlexChart>); |
@ -0,0 +1,3 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
export const Blank = () => <div className="blank-tile">This is an empty tile.</div>; |
@ -0,0 +1,9 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcChart from '@grapecity/wijmo.chart'; |
||||||
|
import * as wjChart from '@grapecity/wijmo.react.chart'; |
||||||
|
export const BubbleChart = ({ data, palette }) => (<wjChart.FlexChart chartType={wjcChart.ChartType.Bubble} itemsSource={data} palette={palette} bindingX="date"> |
||||||
|
<wjChart.FlexChartAxis wjProperty="axisX" format="MMM yy"/> |
||||||
|
<wjChart.FlexChartSeries name="Sales/Profit" binding="sales,profit"/> |
||||||
|
</wjChart.FlexChart>); |
@ -0,0 +1,22 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcCore from '@grapecity/wijmo'; |
||||||
|
import * as wjGauge from '@grapecity/wijmo.react.gauge'; |
||||||
|
export const BulletGraph = ({ data }) => (<div style={{ width: '100%', padding: '0 1rem', overflow: 'hidden' }}> |
||||||
|
<table className="table"> |
||||||
|
<tbody> |
||||||
|
{data.items.map((item, index) => (<tr key={index}> |
||||||
|
<td>{wjcCore.Globalize.format(item.date, 'MMM yyyy')}</td> |
||||||
|
<td> |
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="orange" style={{ display: item.profit <= 400 ? 'block' : 'none' }}> |
||||||
|
<path d="M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16"/> |
||||||
|
</svg> |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
<wjGauge.BulletGraph hasShadow={false} value={item.profit} min={0} bad={400} target={600} good={600} max={1000}/> |
||||||
|
</td> |
||||||
|
</tr>))} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div>); |
@ -0,0 +1,11 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcChart from '@grapecity/wijmo.chart'; |
||||||
|
import * as wjChart from '@grapecity/wijmo.react.chart'; |
||||||
|
export const ColumnChart = ({ data, palette }) => (<wjChart.FlexChart chartType={wjcChart.ChartType.Column} itemsSource={data} palette={palette} bindingX="date" axisX={{ format: 'MMM-yy' }}> |
||||||
|
<wjChart.FlexChartAxis wjProperty="axisX" format="MMM yy"/> |
||||||
|
<wjChart.FlexChartSeries name="Sales" binding="sales"/> |
||||||
|
<wjChart.FlexChartSeries name="Expenses" binding="expenses"/> |
||||||
|
<wjChart.FlexChartSeries name="Profit" binding="profit" chartType={wjcChart.ChartType.LineSymbols}/> |
||||||
|
</wjChart.FlexChart>); |
@ -0,0 +1,12 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcGrid from '@grapecity/wijmo.grid'; |
||||||
|
import * as wjGrid from '@grapecity/wijmo.react.grid'; |
||||||
|
export const Grid = ({ data, palette }) => (<wjGrid.FlexGrid isReadOnly={true} headersVisibility={wjcGrid.HeadersVisibility.Column} selectionMode={wjcGrid.SelectionMode.ListBox} itemsSource={data}> |
||||||
|
<wjGrid.FlexGridColumn header="ID" binding="id" width={50}/> |
||||||
|
<wjGrid.FlexGridColumn header="Date" width="*" binding="date" format="MMM yyyy"/> |
||||||
|
<wjGrid.FlexGridColumn header="Sales" binding="sales" format="c"/> |
||||||
|
<wjGrid.FlexGridColumn header="Expenses" binding="expenses" format="c"/> |
||||||
|
<wjGrid.FlexGridColumn header="Profit" binding="profit" format="c"/> |
||||||
|
</wjGrid.FlexGrid>); |
@ -0,0 +1,10 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcChart from '@grapecity/wijmo.chart'; |
||||||
|
import * as wjChart from '@grapecity/wijmo.react.chart'; |
||||||
|
export const LineChart = ({ data, palette }) => (<wjChart.FlexChart chartType={wjcChart.ChartType.Line} itemsSource={data} palette={palette} bindingX="date"> |
||||||
|
<wjChart.FlexChartAxis wjProperty="axisX" format="MMM yy"/> |
||||||
|
<wjChart.FlexChartSeries name="Sales" binding="sales"/> |
||||||
|
<wjChart.FlexChartSeries name="Profit" binding="profit" chartType={wjcChart.ChartType.LineSymbols}/> |
||||||
|
</wjChart.FlexChart>); |
@ -0,0 +1,26 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcCore from '@grapecity/wijmo'; |
||||||
|
import * as wjGauge from '@grapecity/wijmo.react.gauge'; |
||||||
|
export const LinearGauge = ({ data, palette }) => { |
||||||
|
const lastItem = data.items[data.items.length - 1]; |
||||||
|
return (<div style={{ width: '100%' }}> |
||||||
|
<div className="flex-row"> |
||||||
|
<h4>Sales: {wjcCore.Globalize.format(lastItem.sales, 'c')}</h4> |
||||||
|
<wjGauge.LinearGauge min={0} max={1000} thickness={0.15} thumbSize={9} value={lastItem.sales} pointer={{ color: palette[0] }}/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex-row"> |
||||||
|
<h4>Expenses: {wjcCore.Globalize.format(lastItem.expenses, 'c')}</h4> |
||||||
|
<wjGauge.LinearGauge min={0} max={1000} thickness={0.15} thumbSize={9} value={lastItem.expenses} pointer={{ color: palette[1] }}/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex-row"> |
||||||
|
<h4>Profit: {wjcCore.Globalize.format(lastItem.profit, 'c')}</h4> |
||||||
|
<wjGauge.LinearGauge min={0} max={1000} thickness={0.15} thumbSize={9} value={lastItem.profit} pointer={{ color: palette[2] }}/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h3>KPIs for {wjcCore.Globalize.format(lastItem.date, 'MMMM yyyy')}</h3> |
||||||
|
</div>); |
||||||
|
}; |
@ -0,0 +1,12 @@ |
|||||||
|
// React |
||||||
|
import * as React from 'react'; |
||||||
|
// Wijmo |
||||||
|
import * as wjcCore from '@grapecity/wijmo'; |
||||||
|
import * as wjGauge from '@grapecity/wijmo.react.gauge'; |
||||||
|
export const RadialGauge = ({ data, palette }) => { |
||||||
|
const lastItem = data.items[data.items.length - 1]; |
||||||
|
return (<React.Fragment> |
||||||
|
<wjGauge.RadialGauge min={0} max={1000} format="c0" value={lastItem.profit}/> |
||||||
|
<h3>Profit for {wjcCore.Globalize.format(lastItem.date, 'MMMM yyyy')}</h3> |
||||||
|
</React.Fragment>); |
||||||
|
}; |
@ -0,0 +1,14 @@ |
|||||||
|
import * as React from 'react'; |
||||||
|
export const Tile = ({ header, content, index, onRemove }) => (<div className="tile" draggable={true}> |
||||||
|
<div className="tile-container"> |
||||||
|
<div className="tile-header">{header}</div> |
||||||
|
<div className="buttons"> |
||||||
|
<div className="button" title="Close Tile" onClick={() => onRemove(index)}> |
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24"> |
||||||
|
<path d="M12.71,12l4.64-4.65a.49.49,0,1,0-.7-.7L12,11.29,7.35,6.65a.49.49,0,0,0-.7.7L11.29,12,6.65,16.65a.48.48,0,0,0,0,.7.48.48,0,0,0,.7,0L12,12.71l4.65,4.64a.48.48,0,0,0,.7,0,.48.48,0,0,0,0-.7Z"/> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="tile-content">{content}</div> |
||||||
|
</div>); |
@ -0,0 +1,394 @@ |
|||||||
|
/* app */ |
||||||
|
*, *::before, *::after { |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
/* customize the browser's scrollbar: */ |
||||||
|
*::-webkit-scrollbar { |
||||||
|
width: 6px; |
||||||
|
height: 6px; |
||||||
|
} |
||||||
|
*::-webkit-scrollbar-track { |
||||||
|
border-radius: 0.25rem; |
||||||
|
background: rgba(0, 0, 0, 0.1); |
||||||
|
} |
||||||
|
*::-webkit-scrollbar-thumb { |
||||||
|
border-radius: 0.25rem; |
||||||
|
background: rgba(0, 0, 0, 0.2); |
||||||
|
} |
||||||
|
*::-webkit-scrollbar-thumb:hover { |
||||||
|
background: rgba(0, 0, 0, 0.4); |
||||||
|
} |
||||||
|
*::-webkit-scrollbar-thumb:active { |
||||||
|
background: rgba(0, 0, 0, 0.9); |
||||||
|
} |
||||||
|
|
||||||
|
html, |
||||||
|
body { |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
background-color: #f7faff; |
||||||
|
font-size: 0.875rem; |
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', sans-serif; |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
h3 { |
||||||
|
font-size: 0.875rem; |
||||||
|
font-weight: 500; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
h4 { |
||||||
|
font-size: 0.875rem; |
||||||
|
font-size: 0.75rem; |
||||||
|
font-weight: 400; |
||||||
|
min-width: 7rem; |
||||||
|
} |
||||||
|
|
||||||
|
.flex-row { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
padding: 0 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.button { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
#app { |
||||||
|
flex: 1 1 auto; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
height: 100%; |
||||||
|
width: 100%; |
||||||
|
max-width: 100%; |
||||||
|
background: #f2f6fe; |
||||||
|
} |
||||||
|
|
||||||
|
.hr { |
||||||
|
width: 1px; |
||||||
|
height: 100%; |
||||||
|
position: relative; |
||||||
|
background: #e4ecfb; |
||||||
|
z-index: 1; |
||||||
|
} |
||||||
|
.hr::before { |
||||||
|
content: ''; |
||||||
|
display: block; |
||||||
|
width: 1px; |
||||||
|
height: 100%; |
||||||
|
background-color: #f6f9fe; |
||||||
|
} |
||||||
|
|
||||||
|
.blank { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
justify-content: center; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.blank svg { |
||||||
|
width: 3rem; |
||||||
|
height: 3rem; |
||||||
|
margin-bottom: 1rem; |
||||||
|
} |
||||||
|
|
||||||
|
.content { |
||||||
|
flex: 1 1 auto; |
||||||
|
overflow: auto; |
||||||
|
background-color: #f7faff; |
||||||
|
width: 100%; |
||||||
|
padding: 2rem; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
@media only screen and (max-width: 811px) { |
||||||
|
.content { |
||||||
|
padding: 0.25rem; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.menu { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
font-size: 0.85rem; |
||||||
|
justify-content: flex-start; |
||||||
|
flex: 0 0 auto; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
@media only screen and (max-width: 840px), (max-height: 840px) { |
||||||
|
.menu { |
||||||
|
overflow: auto; |
||||||
|
font: 0.75rem; |
||||||
|
} |
||||||
|
} |
||||||
|
.menu.menu--open .menu-item-name { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
|
||||||
|
.menu-toggle { |
||||||
|
transition: all 500ms; |
||||||
|
padding: 1rem; |
||||||
|
text-align: center; |
||||||
|
border-bottom: 1px solid #e6ecf1; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.menu-toggle svg { |
||||||
|
transition: all 250ms ease-out 50ms; |
||||||
|
transform-origin: center center; |
||||||
|
transform: scaleX(1); |
||||||
|
} |
||||||
|
.menu.menu--open .menu-toggle svg { |
||||||
|
transform: scaleX(-1); |
||||||
|
} |
||||||
|
|
||||||
|
.menu-item { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
padding: 0.5rem 1rem; |
||||||
|
cursor: pointer; |
||||||
|
user-select: none; |
||||||
|
position: relative; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
.menu-item:hover { |
||||||
|
background: #f7faff; |
||||||
|
} |
||||||
|
.menu-item:hover:before { |
||||||
|
content: ''; |
||||||
|
display: inline-block; |
||||||
|
width: 1rem; |
||||||
|
height: 1rem; |
||||||
|
bottom: 2rem; |
||||||
|
left: 2.5rem; |
||||||
|
position: absolute; |
||||||
|
background: linear-gradient(#fff, #fff), linear-gradient(#fff, #fff), #0085c7; |
||||||
|
background-position: center; |
||||||
|
background-size: 50% 2px, 2px 50%; /*thickness = 2px, length = 50% (25px)*/ |
||||||
|
background-repeat: no-repeat; |
||||||
|
border-radius: 50%; |
||||||
|
box-shadow: 0 1px 2px rgba(55, 63, 66, 0.07), 0 2px 4px rgba(55, 63, 66, 0.07), 0 4px 8px rgba(55, 63, 66, 0.07), |
||||||
|
0 8px 16px rgba(55, 63, 66, 0.07), 0 16px 24px rgba(55, 63, 66, 0.07), 0 24px 32px rgba(55, 63, 66, 0.07); |
||||||
|
} |
||||||
|
.menu-item-name { |
||||||
|
margin: 0 0.5rem 0 1rem; |
||||||
|
white-space: nowrap; |
||||||
|
display: none; |
||||||
|
} |
||||||
|
@media only screen and (max-width: 840px), (max-height: 840px) { |
||||||
|
.menu-toggle { |
||||||
|
padding: 1rem 0; |
||||||
|
} |
||||||
|
.menu-item { |
||||||
|
padding: 0.5rem; |
||||||
|
} |
||||||
|
.menu-item:hover:before { |
||||||
|
bottom: 1rem; |
||||||
|
left: 1rem; |
||||||
|
} |
||||||
|
.menu-item svg { |
||||||
|
width: 32px; |
||||||
|
height: 32px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* dashboard and tiles */ |
||||||
|
.dashboard { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
justify-content: space-between; |
||||||
|
margin: auto; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.table { |
||||||
|
margin: 0; |
||||||
|
table-layout: fixed; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.table td { |
||||||
|
padding: 0.15rem 0.5rem; |
||||||
|
font-size: 0.75rem; |
||||||
|
white-space: nowrap; |
||||||
|
width: 1.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
.table td:first-child { |
||||||
|
width: 4rem; |
||||||
|
} |
||||||
|
|
||||||
|
.table td:last-child { |
||||||
|
width: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.tile { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
flex: 0 0 auto; |
||||||
|
width: calc(25% - 1rem); |
||||||
|
margin: 0.5rem; |
||||||
|
height: 50vh; |
||||||
|
max-height: calc(50% - 1rem); |
||||||
|
overflow: hidden; |
||||||
|
background: white; |
||||||
|
page-break-inside: avoid; /* important when printing the dashboard */ |
||||||
|
transition: all 250ms; |
||||||
|
border-radius: 0.5rem; |
||||||
|
box-sizing: border-box; |
||||||
|
box-shadow: 0 1px 2px rgba(55, 63, 66, 0.07), 0 2px 4px rgba(55, 63, 66, 0.07), 0 4px 8px rgba(55, 63, 66, 0.07), |
||||||
|
0 8px 16px rgba(55, 63, 66, 0.07), 0 16px 24px rgba(55, 63, 66, 0.07), 0 24px 32px rgba(55, 63, 66, 0.07); |
||||||
|
} |
||||||
|
@media only screen and (max-width: 1599px) { |
||||||
|
.tile { |
||||||
|
width: calc(33.33% - 1rem); |
||||||
|
} |
||||||
|
} |
||||||
|
@media only screen and (max-width: 1079px) { |
||||||
|
.tile { |
||||||
|
width: calc(50% - 1rem); |
||||||
|
} |
||||||
|
} |
||||||
|
@media only screen and (max-width: 1023px) { |
||||||
|
.tile { |
||||||
|
width: calc(100% - 1rem); |
||||||
|
} |
||||||
|
} |
||||||
|
@media only screen and (max-height: 800px) { |
||||||
|
.tile { |
||||||
|
max-height: 400px; |
||||||
|
} |
||||||
|
} |
||||||
|
.tile:last-child { |
||||||
|
flex: 1 1 auto; |
||||||
|
} |
||||||
|
.tile:hover { |
||||||
|
border-color: #adb7bd; |
||||||
|
} |
||||||
|
.tile.drag-over { |
||||||
|
border: 2px dashed #000; |
||||||
|
} |
||||||
|
|
||||||
|
.tile .buttons { |
||||||
|
transition: all 250ms; |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
@media (hover: none) and (pointer: coarse) { |
||||||
|
.tile .buttons { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
.tile:hover .buttons { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
.tile .buttons > span { |
||||||
|
padding: 0 0.5rem; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.tile.drag-over { |
||||||
|
border: 2px dashed #000; |
||||||
|
background-color: rgba(0, 0, 0, 0.1); |
||||||
|
transition: all 250ms; |
||||||
|
} |
||||||
|
.tile.drag-source { |
||||||
|
opacity: 0.4; |
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); |
||||||
|
background-color: rgba(145, 200, 248, 0.75); |
||||||
|
transform: scale(0.9); |
||||||
|
transition: all 250ms; |
||||||
|
} |
||||||
|
|
||||||
|
.tile .tile-container { |
||||||
|
border-bottom: 1px solid #e0e0e0; |
||||||
|
padding: 0.75rem 1rem; |
||||||
|
display: flex; |
||||||
|
cursor: move; |
||||||
|
} |
||||||
|
|
||||||
|
.tile .tile-header { |
||||||
|
flex-grow: 1; |
||||||
|
font-size: 1rem; |
||||||
|
font-weight: 400; |
||||||
|
padding: 0.125rem; |
||||||
|
opacity: 0.75; |
||||||
|
} |
||||||
|
|
||||||
|
.tile .tile-content { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
flex-direction: column; |
||||||
|
flex: 1 1 auto; |
||||||
|
overflow: auto; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.tile .blank-tile { |
||||||
|
height: 100vh; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
/* tile content */ |
||||||
|
|
||||||
|
.wj-flexgrid { |
||||||
|
border: none; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.wj-flexgrid .wj-cell { |
||||||
|
border-right: none; |
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1); |
||||||
|
padding: 0.35rem 1rem; |
||||||
|
font-size: 0.8125rem; |
||||||
|
} |
||||||
|
|
||||||
|
.wj-flexchart { |
||||||
|
background: transparent; |
||||||
|
height: calc(100%); |
||||||
|
width: 100%; |
||||||
|
border: none; |
||||||
|
padding: 1rem; |
||||||
|
margin: 0; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.wj-radialgauge { |
||||||
|
width: 60%; |
||||||
|
max-width: 300px; |
||||||
|
padding: 1rem; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.wj-radialgauge .wj-value { |
||||||
|
font-size: 0.75rem; |
||||||
|
font-weight: 500; |
||||||
|
} |
||||||
|
|
||||||
|
.wj-lineargauge { |
||||||
|
max-height: 1rem; |
||||||
|
width: 100%; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.wj-gauge .wj-face path { |
||||||
|
stroke: none; |
||||||
|
} |
||||||
|
|
||||||
|
.wj-ranges { |
||||||
|
opacity: 0.15; |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
import './license'; |
||||||
|
// Wijmo and Material Design Lite |
||||||
|
import '@grapecity/wijmo.styles/themes/material/wijmo.theme.material.indigo-amber.css'; |
||||||
|
// Styles |
||||||
|
import './index.css'; |
||||||
|
//React |
||||||
|
import * as React from 'react'; |
||||||
|
import * as ReactDOM from 'react-dom'; |
||||||
|
import { App } from './App'; |
||||||
|
ReactDOM.render(React.createElement(App), document.getElementById('app')); |
@ -0,0 +1,2 @@ |
|||||||
|
import { setLicenseKey } from '@grapecity/wijmo'; |
||||||
|
setLicenseKey('GrapeCity,427351286422477#B0JoIIklkIs4nIzYXMyAjMiojIyVmdiwSZzxWYmpjIyNHZisnOiwmbBJye0ICRiwiI34TQvAVW7ZWbqNjeWZzRh9Ucl5UTuZVaCR7ZpB5UH9EVC3kaqJWZ0pnasJ7Q9I4bB3GR0F6aIt6NZ96MFN4aotEZrUXe4kHUlllerlGb9dDSPhEcFFmclJXd8syNEtENzMTOBNVYpFje6lDUlBlbkdmcNNGOrAHR9pXNSl6NpVkbCNWUxlkZ7o7QT3icHNGavFXdapWQDZ5KsN5N7dDcyRUW85ESFBlb4JUZq3GWlBldXBzU0hjW9wkb8cncopnSOBjNkJFdyNmSqB7YVlnNzpnb9BDSMllTSFDaNFjca54dtBXatlEdS5mVNV6dxIEN7RUNxYGSvU7KIdndPpENvlXW6hzbCh7RsZ7YLJzboNnU7ZERTRkVBNlN7RWSGhDVrdmZqZ4cq94aFpkbWZDMGBHdVZ5YwUTTIdlN0RnVvMDdFJzbQt6bolDM5NkdrsUO536LrNWSotmI0IyUiwiIDRTR4MjM9QjI0ICSiwSMyIjM9UTMwEjM0IicfJye35XX3JSSwIjUiojIDJCLi86bpNnblRHeFBCI4VWZoNFelxmRg2Wbql6ViojIOJyes4nI5kkTRJiOiMkIsIibvl6cuVGd8VEIgIXZ7VWaWRncvBXZSBybtpWaXJiOi8kI1xSfis4N8gkI0IyQiwiIu3Waz9WZ4hXRgAydvJVa4xWdNBybtpWaXJiOi8kI1xSfiQjR6QkI0IyQiwiIu3Waz9WZ4hXRgACUBx4TgAybtpWaXJiOi8kI1xSfiMzQwIkI0IyQiwiIlJ7bDBybtpWaXJiOi8kI1xSfiUFO7EkI0IyQiwiIu3Waz9WZ4hXRgACdyFGaDxWYpNmbh9WaGBybtpWaXJiOi8kI1tlOiQmcQJCLiETMwAzNwACOwcDMwIDMyIiOiQncDJCLi46bj9idlRWe4l6YlBXYydmLqwibj9SbvNmL9RXajVGchJ7ZuoCLt36YukHdpNWZwFmcn9iKsI7au26YukHdpNWZwFmcn9iKsAnau26YukHdpNWZwFmcn9iKiojIz5GRiwiI9RXaDVGchJ7RiojIh94QiwiI7cDNyIDN6gjMxUzMdI6N'); |
@ -0,0 +1,370 @@ |
|||||||
|
import * as wjcCore from '@grapecity/wijmo'; |
||||||
|
/** |
||||||
|
* Object used to hold the data that is being dragged during drag and drop operations. |
||||||
|
* |
||||||
|
* It may hold one or more data items of different types. For more information about |
||||||
|
* drag and drop operations and data transfer objects, see |
||||||
|
* <a href="https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer">HTML Drag and Drop API</a>. |
||||||
|
* |
||||||
|
* This object is created automatically by the @see:DragDropTouch singleton and is |
||||||
|
* accessible through the @see:dataTransfer property of all drag events. |
||||||
|
*/ |
||||||
|
export class DataTransfer { |
||||||
|
constructor() { |
||||||
|
this._dropEffect = 'move'; |
||||||
|
this._effectAllowed = 'all'; |
||||||
|
this._data = {}; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Gets or sets the type of drag-and-drop operation currently selected. |
||||||
|
* The value must be 'none', 'copy', 'link', or 'move'. |
||||||
|
*/ |
||||||
|
get dropEffect() { |
||||||
|
return this._dropEffect; |
||||||
|
} |
||||||
|
set dropEffect(value) { |
||||||
|
this._dropEffect = wjcCore.asString(value); |
||||||
|
} |
||||||
|
/** |
||||||
|
* Gets or sets the types of operations that are possible. |
||||||
|
* Must be one of 'none', 'copy', 'copyLink', 'copyMove', 'link', |
||||||
|
* 'linkMove', 'move', 'all' or 'uninitialized'. |
||||||
|
*/ |
||||||
|
get effectAllowed() { |
||||||
|
return this._effectAllowed; |
||||||
|
} |
||||||
|
set effectAllowed(value) { |
||||||
|
this._effectAllowed = wjcCore.asString(value); |
||||||
|
} |
||||||
|
/** |
||||||
|
* Gets an array of strings giving the formats that were set in the @see:dragstart event. |
||||||
|
*/ |
||||||
|
get types() { |
||||||
|
return Object.keys(this._data); |
||||||
|
} |
||||||
|
/** |
||||||
|
* Removes the data associated with a given type. |
||||||
|
* |
||||||
|
* The type argument is optional. If the type is empty or not specified, the data |
||||||
|
* associated with all types is removed. If data for the specified type does not exist, |
||||||
|
* or the data transfer contains no data, this method will have no effect. |
||||||
|
* |
||||||
|
* @param type Type of data to remove. |
||||||
|
*/ |
||||||
|
clearData(type) { |
||||||
|
if (type != null) { |
||||||
|
delete this._data[type]; |
||||||
|
} |
||||||
|
else { |
||||||
|
this._data = null; |
||||||
|
} |
||||||
|
} |
||||||
|
/** |
||||||
|
* Retrieves the data for a given type, or an empty string if data for that type does |
||||||
|
* not exist or the data transfer contains no data. |
||||||
|
* |
||||||
|
* @param type Type of data to retrieve. |
||||||
|
*/ |
||||||
|
getData(type) { |
||||||
|
return this._data[type] || ''; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Set the data for a given type. |
||||||
|
* |
||||||
|
* For a list of recommended drag types, please see |
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Recommended_Drag_Types.
|
||||||
|
* |
||||||
|
* @param type Type of data to add. |
||||||
|
* @param value Data to add. |
||||||
|
*/ |
||||||
|
setData(type, value) { |
||||||
|
this._data[type] = value; |
||||||
|
} |
||||||
|
/** |
||||||
|
* Set the image to be used for dragging if a custom one is desired. |
||||||
|
* |
||||||
|
* @param img An image element to use as the drag feedback image. |
||||||
|
* @param offsetX The horizontal offset within the image. |
||||||
|
* @param offsetY The vertical offset within the image. |
||||||
|
*/ |
||||||
|
setDragImage(img, offsetX, offsetY) { |
||||||
|
var ddt = DragDropTouch._instance; |
||||||
|
ddt._imgCustom = img; |
||||||
|
ddt._imgOffset = new wjcCore.Point(offsetX, offsetY); |
||||||
|
} |
||||||
|
} |
||||||
|
/** |
||||||
|
* Defines a class that adds support for touch-based HTML5 drag/drop operations. |
||||||
|
* |
||||||
|
* The @see:DragDropTouch class listens to touch events and raises the |
||||||
|
* appropriate HTML5 drag/drop events as if the events had been caused |
||||||
|
* by mouse actions. |
||||||
|
* |
||||||
|
* The purpose of this class is to enable using existing, standard HTML5 |
||||||
|
* drag/drop code on mobile devices running IOS or Android. |
||||||
|
* |
||||||
|
* To use, include the DragDropTouch.js file on the page. The class will |
||||||
|
* automatically start monitoring touch events and will raise the HTML5 |
||||||
|
* drag drop events (dragstart, dragenter, dragleave, drop, dragend) which |
||||||
|
* should be handled by the application. |
||||||
|
* |
||||||
|
* For details and examples on HTML drag and drop, see |
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations.
|
||||||
|
*/ |
||||||
|
export class DragDropTouch { |
||||||
|
/** |
||||||
|
* Initializes the single instance of the @see:DragDropTouch class. |
||||||
|
*/ |
||||||
|
constructor() { |
||||||
|
this._lastClick = 0; |
||||||
|
// enforce singleton pattern
|
||||||
|
wjcCore.assert(!DragDropTouch._instance, 'DragDropTouch instance already created.'); |
||||||
|
// listen to touch events
|
||||||
|
if ('ontouchstart' in document) { |
||||||
|
var d = document, ts = this._touchstart.bind(this), tm = this._touchmove.bind(this), te = this._touchend.bind(this); |
||||||
|
d.addEventListener('touchstart', ts); |
||||||
|
d.addEventListener('touchmove', tm); |
||||||
|
d.addEventListener('touchend', te); |
||||||
|
d.addEventListener('touchcancel', te); |
||||||
|
} |
||||||
|
} |
||||||
|
/** |
||||||
|
* Gets a reference to the @see:DragDropTouch singleton. |
||||||
|
*/ |
||||||
|
static getInstance() { |
||||||
|
return DragDropTouch._instance; |
||||||
|
} |
||||||
|
// ** event handlers
|
||||||
|
_touchstart(e) { |
||||||
|
if (this._shouldHandle(e)) { |
||||||
|
// raise double-click and prevent zooming
|
||||||
|
if (Date.now() - this._lastClick < DragDropTouch._DBLCLICK) { |
||||||
|
if (this._dispatchEvent(e, 'dblclick', e.target)) { |
||||||
|
e.preventDefault(); |
||||||
|
this._reset(); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
// clear all variables
|
||||||
|
this._reset(); |
||||||
|
// get nearest draggable element
|
||||||
|
var src = wjcCore.closest(e.target, '[draggable]'); |
||||||
|
if (src) { |
||||||
|
// give caller a chance to handle the hover/move events
|
||||||
|
if (!this._dispatchEvent(e, 'mousemove', e.target) && |
||||||
|
!this._dispatchEvent(e, 'mousedown', e.target)) { |
||||||
|
// get ready to start dragging
|
||||||
|
this._dragSource = src; |
||||||
|
this._ptDown = this._getPoint(e); |
||||||
|
this._lastTouch = e; |
||||||
|
e.preventDefault(); |
||||||
|
// show context menu if the user hasn't started dragging after a while
|
||||||
|
setTimeout(() => { |
||||||
|
if (this._dragSource == src && this._img == null) { |
||||||
|
if (this._dispatchEvent(e, 'contextmenu', src)) { |
||||||
|
this._reset(); |
||||||
|
} |
||||||
|
} |
||||||
|
}, DragDropTouch._CTXMENU); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
_touchmove(e) { |
||||||
|
if (this._shouldHandle(e)) { |
||||||
|
// see if target wants to handle move
|
||||||
|
var target = this._getTarget(e); |
||||||
|
if (this._dispatchEvent(e, 'mousemove', target)) { |
||||||
|
this._lastTouch = e; |
||||||
|
e.preventDefault(); |
||||||
|
return; |
||||||
|
} |
||||||
|
// start dragging
|
||||||
|
if (this._dragSource && !this._img) { |
||||||
|
var delta = this._getDelta(e); |
||||||
|
if (delta > DragDropTouch._THRESHOLD) { |
||||||
|
this._dispatchEvent(e, 'dragstart', this._dragSource); |
||||||
|
this._createImage(e); |
||||||
|
this._dispatchEvent(e, 'dragenter', target); |
||||||
|
} |
||||||
|
} |
||||||
|
// continue dragging
|
||||||
|
if (this._img) { |
||||||
|
this._lastTouch = e; |
||||||
|
e.preventDefault(); // prevent scrolling
|
||||||
|
if (target != this._lastTarget) { |
||||||
|
this._dispatchEvent(this._lastTouch, 'dragleave', this._lastTarget); |
||||||
|
this._dispatchEvent(e, 'dragenter', target); |
||||||
|
this._lastTarget = target; |
||||||
|
} |
||||||
|
this._moveImage(e); |
||||||
|
this._dispatchEvent(e, 'dragover', target); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
_touchend(e) { |
||||||
|
if (this._shouldHandle(e)) { |
||||||
|
// see if target wants to handle up
|
||||||
|
if (this._dispatchEvent(this._lastTouch, 'mouseup', e.target)) { |
||||||
|
e.preventDefault(); |
||||||
|
return; |
||||||
|
} |
||||||
|
// user clicked the element but didn't drag, so clear the source and simulate a click
|
||||||
|
if (!this._img) { |
||||||
|
this._dragSource = null; |
||||||
|
this._dispatchEvent(this._lastTouch, 'click', e.target); |
||||||
|
this._lastClick = Date.now(); |
||||||
|
} |
||||||
|
// finish dragging
|
||||||
|
this._destroyImage(); |
||||||
|
if (this._dragSource) { |
||||||
|
if (e.type.indexOf('cancel') < 0) { |
||||||
|
this._dispatchEvent(this._lastTouch, 'drop', this._lastTarget); |
||||||
|
} |
||||||
|
this._dispatchEvent(this._lastTouch, 'dragend', this._dragSource); |
||||||
|
this._reset(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// ** utilities
|
||||||
|
// ignore events that have been handled or that involve more than one touch
|
||||||
|
_shouldHandle(e) { |
||||||
|
return e && |
||||||
|
!e.defaultPrevented && |
||||||
|
e.touches && e.touches.length < 2; |
||||||
|
} |
||||||
|
// clear all members
|
||||||
|
_reset() { |
||||||
|
this._destroyImage(); |
||||||
|
this._dragSource = null; |
||||||
|
this._lastTouch = null; |
||||||
|
this._lastTarget = null; |
||||||
|
this._ptDown = null; |
||||||
|
this._dataTransfer = new DataTransfer(); |
||||||
|
} |
||||||
|
// get point for a touch event
|
||||||
|
_getPoint(e, page) { |
||||||
|
if (e && e.touches) { |
||||||
|
e = e.touches[0]; |
||||||
|
} |
||||||
|
wjcCore.assert(e && ('clientX' in e), 'invalid event?'); |
||||||
|
if (page == true) { |
||||||
|
return new wjcCore.Point(e.pageX, e.pageY); |
||||||
|
} |
||||||
|
else { |
||||||
|
return new wjcCore.Point(e.clientX, e.clientY); |
||||||
|
} |
||||||
|
} |
||||||
|
// get distance between the current touch event and the first one
|
||||||
|
_getDelta(e) { |
||||||
|
var p = this._getPoint(e); |
||||||
|
return Math.abs(p.x - this._ptDown.x) + Math.abs(p.y - this._ptDown.y); |
||||||
|
} |
||||||
|
// get the element at a given touch event
|
||||||
|
_getTarget(e) { |
||||||
|
var pt = this._getPoint(e), el = document.elementFromPoint(pt.x, pt.y); |
||||||
|
while (el && getComputedStyle(el).pointerEvents == 'none') { |
||||||
|
el = el.parentElement; |
||||||
|
} |
||||||
|
return el; |
||||||
|
} |
||||||
|
// create drag image from source element
|
||||||
|
_createImage(e) { |
||||||
|
// just in case...
|
||||||
|
if (this._img) { |
||||||
|
this._destroyImage(); |
||||||
|
} |
||||||
|
// create drag image from custom element or drag source
|
||||||
|
var src = this._imgCustom || this._dragSource; |
||||||
|
this._img = src.cloneNode(true); |
||||||
|
this._copyStyle(src, this._img); |
||||||
|
this._img.style.top = this._img.style.left = '-9999px'; |
||||||
|
// if creating from drag source, apply offset and opacity
|
||||||
|
if (!this._imgCustom) { |
||||||
|
var rc = src.getBoundingClientRect(), pt = this._getPoint(e); |
||||||
|
this._imgOffset = new wjcCore.Point(pt.x - rc.left, pt.y - rc.top); |
||||||
|
this._img.style.opacity = DragDropTouch._OPACITY.toString(); |
||||||
|
} |
||||||
|
// add image to document
|
||||||
|
this._moveImage(e); |
||||||
|
document.body.appendChild(this._img); |
||||||
|
} |
||||||
|
// dispose of drag image element
|
||||||
|
_destroyImage() { |
||||||
|
if (this._img && this._img.parentElement) { |
||||||
|
this._img.parentElement.removeChild(this._img); |
||||||
|
} |
||||||
|
this._img = null; |
||||||
|
this._imgCustom = null; |
||||||
|
} |
||||||
|
// move the drag image element
|
||||||
|
_moveImage(e) { |
||||||
|
requestAnimationFrame(() => { |
||||||
|
var pt = this._getPoint(e, true); |
||||||
|
wjcCore.setCss(this._img, { |
||||||
|
position: 'absolute', |
||||||
|
pointerEvents: 'none', |
||||||
|
zIndex: 999999, |
||||||
|
left: Math.round(pt.x - this._imgOffset.x), |
||||||
|
top: Math.round(pt.y - this._imgOffset.y) |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
// copy properties from an object to another
|
||||||
|
_copyProps(dst, src, props) { |
||||||
|
for (var i = 0; i < props.length; i++) { |
||||||
|
var p = props[i]; |
||||||
|
dst[p] = src[p]; |
||||||
|
} |
||||||
|
} |
||||||
|
_copyStyle(src, dst) { |
||||||
|
// remove potentially troublesome attributes
|
||||||
|
DragDropTouch._rmvAtts.forEach(function (att) { |
||||||
|
dst.removeAttribute(att); |
||||||
|
}); |
||||||
|
// copy canvas content
|
||||||
|
if (src instanceof HTMLCanvasElement) { |
||||||
|
var cSrc = src, cDst = dst; |
||||||
|
cDst.width = cSrc.width; |
||||||
|
cDst.height = cSrc.height; |
||||||
|
cDst.getContext('2d').drawImage(cSrc, 0, 0); |
||||||
|
} |
||||||
|
// copy style
|
||||||
|
var cs = getComputedStyle(src); |
||||||
|
for (var i = 0; i < cs.length; i++) { |
||||||
|
var key = cs[i]; |
||||||
|
dst.style[key] = cs[key]; |
||||||
|
} |
||||||
|
dst.style.pointerEvents = 'none'; |
||||||
|
// and repeat for all children
|
||||||
|
for (var i = 0; i < src.children.length; i++) { |
||||||
|
this._copyStyle(src.children[i], dst.children[i]); |
||||||
|
} |
||||||
|
} |
||||||
|
_dispatchEvent(e, type, target) { |
||||||
|
if (e && target) { |
||||||
|
var evt = document.createEvent('Event'), t = e.touches ? e.touches[0] : e; |
||||||
|
evt.initEvent(type, true, true); |
||||||
|
evt.button = 0; |
||||||
|
evt.which = evt.buttons = 1; |
||||||
|
this._copyProps(evt, e, DragDropTouch._kbdProps); |
||||||
|
this._copyProps(evt, t, DragDropTouch._ptProps); |
||||||
|
evt.dataTransfer = this._dataTransfer; |
||||||
|
target.dispatchEvent(evt); |
||||||
|
return evt.defaultPrevented; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
/*private*/ DragDropTouch._instance = new DragDropTouch(); // singleton
|
||||||
|
// constants
|
||||||
|
DragDropTouch._THRESHOLD = 5; // pixels to move before drag starts
|
||||||
|
DragDropTouch._OPACITY = 0.5; // drag image opacity
|
||||||
|
DragDropTouch._DBLCLICK = 500; // max ms between clicks in a double click
|
||||||
|
DragDropTouch._CTXMENU = 900; // ms to hold before raising 'contextmenu' event
|
||||||
|
// copy styles/attributes from drag source to drag image element
|
||||||
|
DragDropTouch._rmvAtts = 'id,class,style,draggable'.split(','); |
||||||
|
// synthesize and dispatch an event
|
||||||
|
// returns true if the event has been handled (e.preventDefault == true)
|
||||||
|
DragDropTouch._kbdProps = 'altKey,ctrlKey,metaKey,shiftKey'.split(','); |
||||||
|
DragDropTouch._ptProps = 'pageX,pageY,clientX,clientY,screenX,screenY'.split(','); |
@ -0,0 +1,296 @@ |
|||||||
|
import React, { useState } from 'react'; |
||||||
|
import { Row, Col, Button, Modal } from 'antd'; |
||||||
|
import { RightOutlined, LeftOutlined } from '@ant-design/icons'; |
||||||
|
import { Widgets } from 'react-awesome-query-builder'; |
||||||
|
|
||||||
|
const DashboardDyna = () => { |
||||||
|
const [templates, setTemplates] = useState([]); |
||||||
|
const [widgets, setWidgets] = useState([]); |
||||||
|
const [templateColSpan, setTemplateColSpan] = useState(5); |
||||||
|
const [widgetColSpan, setWidgetColSpan] = useState(5); |
||||||
|
const [dashboardColSpan, setDashboardColSpan] = useState(19); |
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false); |
||||||
|
const [templateIndex, setTemplateIndex] = useState(0); |
||||||
|
const [columnIndex, setColumnIndex] = useState(0); |
||||||
|
|
||||||
|
const addWidget = (widget) => { |
||||||
|
// Make a copy of the current widgets state
|
||||||
|
const newWidgets = [...widgets]; |
||||||
|
if (!newWidgets[templateIndex]) { |
||||||
|
newWidgets[templateIndex] = [] |
||||||
|
} |
||||||
|
|
||||||
|
// Make sure the templateIndex and columnIndex are within bounds
|
||||||
|
// if (templateIndex <= newWidgets.length && columnIndex <= newWidgets[templateIndex].length) {
|
||||||
|
// Make a copy of the nested array at templateIndex
|
||||||
|
const nestedArray = [...newWidgets[templateIndex]]; |
||||||
|
|
||||||
|
// Update the value at columnIndex
|
||||||
|
nestedArray[columnIndex] = widget; |
||||||
|
|
||||||
|
// Update the nested array in the newWidgets array
|
||||||
|
newWidgets[templateIndex] = nestedArray; |
||||||
|
|
||||||
|
// Update the state with the newWidgets array
|
||||||
|
setWidgets(newWidgets); |
||||||
|
// }
|
||||||
|
const lastIndex = templates.length - 1; |
||||||
|
if (lastIndex >= 0) { |
||||||
|
let id = templates[templateIndex].id; |
||||||
|
let updatedTemplate = checkTemplate(id, newWidgets, true); |
||||||
|
let updatedTemplates = [...templates]; |
||||||
|
updatedTemplates[templateIndex] = updatedTemplate; |
||||||
|
setTemplates(updatedTemplates); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
const addTemplate = (template) => { |
||||||
|
const lastIndex = templates.length - 1; |
||||||
|
setTemplates([...templates, checkTemplate(template)]); |
||||||
|
}; |
||||||
|
|
||||||
|
const clearTemplates = () => { |
||||||
|
setTemplates([]); |
||||||
|
clearWidgets() |
||||||
|
}; |
||||||
|
|
||||||
|
const clearWidgets = () => { |
||||||
|
setWidgets([]); |
||||||
|
}; |
||||||
|
|
||||||
|
const showModal = (template = 0, column = 0) => { |
||||||
|
setTemplateIndex(template); |
||||||
|
setColumnIndex(column); |
||||||
|
setIsModalOpen(true); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOk = () => { |
||||||
|
setIsModalOpen(false); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleCancel = () => { |
||||||
|
setIsModalOpen(false); |
||||||
|
}; |
||||||
|
|
||||||
|
const checkTemplate = (template, widgets = null, add_widget = false) => { |
||||||
|
let component, widgetCount, id = template; |
||||||
|
let templateIndex = templates.length; |
||||||
|
if (add_widget) { |
||||||
|
templateIndex-- |
||||||
|
} |
||||||
|
switch (template) { |
||||||
|
case 0: |
||||||
|
component = ( |
||||||
|
<Row style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={24} onClick={() => showModal(templateIndex, 0)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][0] : null} |
||||||
|
</div> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
); |
||||||
|
widgetCount = 1; |
||||||
|
break; |
||||||
|
case 1: |
||||||
|
component = ( |
||||||
|
<Row style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, height: '25vh' }}> |
||||||
|
<Col span={5} onClick={() => showModal(templateIndex, 0)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][0] : null} |
||||||
|
</div> </Col> |
||||||
|
<Col span={5} onClick={() => showModal(templateIndex, 1)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][1] : null} |
||||||
|
</div> </Col> |
||||||
|
<Col span={5} onClick={() => showModal(templateIndex, 2)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][2] : null} |
||||||
|
</div> </Col> |
||||||
|
<Col span={6} onClick={() => showModal(templateIndex, 3)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][3] : null} |
||||||
|
</div> </Col> |
||||||
|
</Row> |
||||||
|
); |
||||||
|
widgetCount = 4; |
||||||
|
break; |
||||||
|
case 2: |
||||||
|
component = ( |
||||||
|
<Row style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, height: '25vh' }}> |
||||||
|
<Col span={5} onClick={() => showModal(templateIndex, 0)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][0] : null} |
||||||
|
</div> </Col> |
||||||
|
<Col span={18} onClick={() => showModal(templateIndex, 1)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][1] : null} |
||||||
|
</div> </Col> |
||||||
|
</Row> |
||||||
|
); |
||||||
|
widgetCount = 2; |
||||||
|
break; |
||||||
|
case 3: |
||||||
|
component = ( |
||||||
|
<Row style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, height: '25vh' }}> |
||||||
|
<Col span={17} onClick={() => showModal(templateIndex, 0)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][0] : null} |
||||||
|
</div> </Col> |
||||||
|
<Col span={6} onClick={() => showModal(templateIndex, 1)}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10, height: '25vh' }}> |
||||||
|
{widgets ? widgets[templateIndex][1] : null} |
||||||
|
</div> </Col> |
||||||
|
</Row> |
||||||
|
); |
||||||
|
widgetCount = 2; |
||||||
|
break; |
||||||
|
default: |
||||||
|
break; |
||||||
|
} |
||||||
|
return { component, widgetCount, id }; |
||||||
|
}; |
||||||
|
|
||||||
|
const toggleTemplateColumn = () => { |
||||||
|
if (templateColSpan === 1) { |
||||||
|
setTemplateColSpan(5); |
||||||
|
setDashboardColSpan(dashboardColSpan - 4); |
||||||
|
} else { |
||||||
|
setTemplateColSpan(1); |
||||||
|
setDashboardColSpan(dashboardColSpan + 4); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const toggleWidgetColumn = () => { |
||||||
|
if (widgetColSpan === 1) { |
||||||
|
setWidgetColSpan(5); |
||||||
|
setDashboardColSpan(dashboardColSpan - 4); |
||||||
|
} else { |
||||||
|
setWidgetColSpan(1); |
||||||
|
setDashboardColSpan(dashboardColSpan + 4); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div style={{ marginLeft: -25, marginRight: -25 }}> |
||||||
|
<Row> |
||||||
|
<Col span={templateColSpan}> |
||||||
|
<div style={{ backgroundColor: '#F8F8F8', margin: 2, paddingLeft: 20, paddingRight: 20, paddingTop: 10, height: '90vh' }}> |
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', marginBottom: 10 }}> |
||||||
|
<div style={{ flex: 20, display: 'flex', flexDirection: 'column' }}> |
||||||
|
{templateColSpan === 1 ? |
||||||
|
<Button icon={<RightOutlined />} onClick={toggleTemplateColumn}></Button> |
||||||
|
: |
||||||
|
<> |
||||||
|
<div style={{ color: '#444444', fontSize: 14, fontWeight: 'bold', marginBottom: 10 }}>Template</div> |
||||||
|
|
||||||
|
<Row onClick={() => addTemplate(0)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={24}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row onClick={() => addTemplate(1)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={6}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row onClick={() => addTemplate(2)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={18}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row onClick={() => addTemplate(3)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={17}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={6}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Button icon={<LeftOutlined />} onClick={toggleTemplateColumn}></Button> |
||||||
|
</> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Col> |
||||||
|
<Col span={dashboardColSpan}> |
||||||
|
<div style={{ backgroundColor: '#F8F8F8', margin: 2, paddingLeft: 20, paddingRight: 20, paddingTop: 10, height: '90vh' }}> |
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', marginBottom: 10 }}> |
||||||
|
<div style={{ flex: 20, display: 'flex', flexDirection: 'column' }}> |
||||||
|
<div style={{ color: '#444444', fontSize: 14, fontWeight: 'bold', marginBottom: 10 }}>Dashboard</div> |
||||||
|
{templates.length > 0 ? templates.map((template) => { |
||||||
|
return template.component; |
||||||
|
}) : null} |
||||||
|
<Button onClick={() => clearTemplates()}>Clear</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Modal title="Widgets" open={isModalOpen} onOk={handleOk} onCancel={handleCancel}> |
||||||
|
<div style={{ backgroundColor: '#F8F8F8', margin: 2, paddingLeft: 20, paddingRight: 20, paddingTop: 10 }}> |
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', marginBottom: 10 }}> |
||||||
|
<div style={{ flex: 20, display: 'flex', flexDirection: 'column' }}> |
||||||
|
{widgetColSpan === 1 ? |
||||||
|
<Button icon={<RightOutlined />} onClick={toggleWidgetColumn}></Button> |
||||||
|
: |
||||||
|
<> |
||||||
|
<div style={{ color: '#444444', fontSize: 14, fontWeight: 'bold', marginBottom: 10 }}>Widget</div> |
||||||
|
|
||||||
|
<Row onClick={() => addWidget(0)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={18}> |
||||||
|
<div style={{ padding: 0 }}>Pie Chart Project By Role</div> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row onClick={() => addWidget(1)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={18}> |
||||||
|
<div style={{ padding: 0 }}>Pie Chart Project By Role</div> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row onClick={() => addWidget(2)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={18}> |
||||||
|
<div style={{ padding: 0 }}>Pie Chart Project By Role</div> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row onClick={() => addWidget(3)} style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}> |
||||||
|
<Col span={5}> |
||||||
|
<div style={{ backgroundColor: '#CCCCCC', padding: 10 }} /> |
||||||
|
</Col> |
||||||
|
<Col span={18}> |
||||||
|
<div style={{ padding: 0 }}>Pie Chart Project By Role</div> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
</> |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Modal> |
||||||
|
</div> |
||||||
|
|
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default DashboardDyna; |
@ -0,0 +1,190 @@ |
|||||||
|
import React, { useEffect, useState } from 'react' |
||||||
|
import { |
||||||
|
Modal, ModalHeader, ModalBody, ModalFooter, |
||||||
|
Button, Form, FormGroup, Label, Input, Col, Row |
||||||
|
} from 'reactstrap'; |
||||||
|
import { Select, DatePicker } from 'antd'; |
||||||
|
import 'antd/dist/antd.css'; |
||||||
|
import { useTranslation } from 'react-i18next'; |
||||||
|
import "rc-color-picker/assets/index.css"; |
||||||
|
import moment from 'moment'; |
||||||
|
import { formatNumber } from "../../../const/CustomFunc"; |
||||||
|
const { Option } = Select |
||||||
|
const DialogForm = ({ openDialog, closeDialog, toggleDialog, typeDialog, dataEdit }) => { |
||||||
|
const [id, setId] = useState(0) |
||||||
|
const [code, setCode] = useState('') |
||||||
|
const [amount, setAmount] = useState(0) |
||||||
|
const [description, setDescription] = useState('') |
||||||
|
const [exp, setExp] = useState('') |
||||||
|
const [type, setType] = useState('') |
||||||
|
const [allocation, setAllocation] = useState(0) |
||||||
|
const { t } = useTranslation() |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
console.log('dataEdit', dataEdit); |
||||||
|
if (typeDialog === "Edit") { |
||||||
|
setId(dataEdit.id) |
||||||
|
setCode(dataEdit.code) |
||||||
|
setAmount(dataEdit.amount) |
||||||
|
setDescription(dataEdit.description) |
||||||
|
setExp(dataEdit.exp) |
||||||
|
setType(dataEdit.type) |
||||||
|
} else { |
||||||
|
handleClear() |
||||||
|
} |
||||||
|
}, [dataEdit, openDialog]) |
||||||
|
|
||||||
|
const validation = () => { |
||||||
|
if (!code || code === "") { |
||||||
|
alert("Code cannot be empty!"); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
const handleSave = () => { |
||||||
|
let data = ''; |
||||||
|
const err = validation(); |
||||||
|
|
||||||
|
if (!err) { |
||||||
|
if (typeDialog === "Save") { |
||||||
|
data = { |
||||||
|
code, |
||||||
|
amount, |
||||||
|
description, |
||||||
|
exp, |
||||||
|
type, |
||||||
|
allocation |
||||||
|
} |
||||||
|
console.log('data', data); |
||||||
|
closeDialog('save', data); |
||||||
|
} else { |
||||||
|
data = { |
||||||
|
id, |
||||||
|
code, |
||||||
|
amount, |
||||||
|
description, |
||||||
|
exp, |
||||||
|
type, |
||||||
|
allocation |
||||||
|
} |
||||||
|
console.log('data', data); |
||||||
|
closeDialog('edit', data); |
||||||
|
} |
||||||
|
handleClear() |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
const handleCancel = () => { |
||||||
|
closeDialog('cancel', 'none') |
||||||
|
handleClear() |
||||||
|
} |
||||||
|
const handleClear = () => { |
||||||
|
setId(0) |
||||||
|
setCode('') |
||||||
|
setAmount('') |
||||||
|
setDescription('') |
||||||
|
setExp('') |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const renderForm = () => { |
||||||
|
return ( |
||||||
|
<Form> |
||||||
|
<Row> |
||||||
|
<Col md={12}> |
||||||
|
<span style={{ color: "red" }}>*</span> Wajib diisi. |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('code')} {t('discount')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
value={code} |
||||||
|
onChange={(e) => setCode(e.target.value)} |
||||||
|
placeholder={t('inputCode')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('amount')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
value={amount} |
||||||
|
onChange={(e) => setAmount(e.target.value)} |
||||||
|
placeholder={t('inputAmount')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize" style={{ fontWeight: "bold" }}> |
||||||
|
{t('exp')}<span style={{ color: "red" }}>*</span> |
||||||
|
</Label> |
||||||
|
<DatePicker |
||||||
|
format={"DD-MM-YYYY HH:mm"} |
||||||
|
style={{ width: "100%" }} |
||||||
|
value={exp ? moment(exp) : exp} |
||||||
|
onChange={(date, dateString) => setExp(date)} |
||||||
|
showTime={{ defaultValue: moment('00:00:00', 'HH:mm:ss') }} |
||||||
|
/> |
||||||
|
|
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('allocation')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="number" |
||||||
|
value={allocation} |
||||||
|
onChange={(e) => setAllocation(e.target.value)} |
||||||
|
placeholder={t('input')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('type')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
value={type} |
||||||
|
onChange={(e) => setType(e.target.value)} |
||||||
|
placeholder={t('inputType')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col md={12}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('description')}</Label> |
||||||
|
<Input row="4" type="textarea" value={description} onChange={(e) => setDescription(e.target.value)} placeholder={t('inputDescription')} /> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
</Form> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Modal size="lg" isOpen={openDialog} toggle={toggleDialog}> |
||||||
|
<ModalHeader className="capitalize" toggle={closeDialog}>{typeDialog == "Save" ? `Add` : "Edit"} {t('Demo')}</ModalHeader> |
||||||
|
<ModalBody> |
||||||
|
{renderForm()} |
||||||
|
</ModalBody> |
||||||
|
<ModalFooter> |
||||||
|
<Button color="primary" onClick={() => handleSave()}>{typeDialog}</Button>{' '} |
||||||
|
<Button className="capitalize" color="secondary" onClick={() => handleCancel()}>{t('cancel')}</Button> |
||||||
|
</ModalFooter> |
||||||
|
</Modal> |
||||||
|
</> |
||||||
|
) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
export default DialogForm; |
@ -0,0 +1,372 @@ |
|||||||
|
import * as XLSX from 'xlsx'; |
||||||
|
import DialogForm from './DialogForm'; |
||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import SweetAlert from 'react-bootstrap-sweetalert'; |
||||||
|
import axios from "../../../const/interceptorApi" |
||||||
|
import { Card, CardBody, CardHeader, Col, Row, Input, Table } from 'reactstrap'; |
||||||
|
import { REFERRAL_CODE_EDIT, REFERRAL_CODE_SEARCH, REFERRAL_CODE_ADD, REFERRAL_CODE_DELETE } from '../../../const/ApiConst'; |
||||||
|
import { NotificationContainer, NotificationManager } from 'react-notifications'; |
||||||
|
import { Spin, Button, Tooltip } from 'antd'; |
||||||
|
import { useTranslation } from 'react-i18next'; |
||||||
|
import moment from 'moment'; |
||||||
|
import toRupiah from '@develoka/angka-rupiah-js'; |
||||||
|
|
||||||
|
const ProjectType = ({ params, ...props }) => { |
||||||
|
let role_id = 0, user_id = 0, isLogin = false, token = '', company_id = 0, all_project = null, role_name = '', hierarchy = [], user_name = ''; |
||||||
|
if (props && props.role_id && props.user_id) { |
||||||
|
role_id = props.role_id; |
||||||
|
user_id = props.user_id; |
||||||
|
token = props.token; |
||||||
|
isLogin = props.isLogin; |
||||||
|
company_id = props.company_id; |
||||||
|
all_project = props.all_project; |
||||||
|
role_name = props.role_name; |
||||||
|
isLogin = props.isLogin; |
||||||
|
hierarchy = props.hierarchy; |
||||||
|
user_name = props.user_name; |
||||||
|
} |
||||||
|
|
||||||
|
const HEADER = { |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/json", |
||||||
|
"Authorization": `Bearer ${token}` |
||||||
|
} |
||||||
|
} |
||||||
|
const pageName = params.name; |
||||||
|
|
||||||
|
const [alertDelete, setAlertDelete] = useState(false) |
||||||
|
const [allDataMenu, setAllDataMenu] = useState([]) |
||||||
|
const [clickOpenModal, setClickOpenModal] = useState(false) |
||||||
|
const [currentPage, setCurrentPage] = useState(1) |
||||||
|
const [dataEdit, setDataEdit] = useState([]) |
||||||
|
const [dataExport, setDataExport] = useState([]) |
||||||
|
const [dataTable, setDatatable] = useState([]) |
||||||
|
const [idDelete, setIdDelete] = useState(0) |
||||||
|
const [openDialog, setOpenDialog] = useState(false) |
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(10) |
||||||
|
const [search, setSearch] = useState("") |
||||||
|
const [totalPage, setTotalPage] = useState(0) |
||||||
|
const [typeDialog, setTypeDialog] = useState('Save') |
||||||
|
const [dataDemo, setDataDemo] = useState([]) |
||||||
|
const [listCompany, setListCompany] = useState([]) |
||||||
|
const [loading, setLoading] = useState(true); |
||||||
|
const { t } = useTranslation() |
||||||
|
const column = [ |
||||||
|
{ name: t('code') }, |
||||||
|
{ name: t('type') }, |
||||||
|
{ name: t('amount') }, |
||||||
|
{ name: t('exp') }, |
||||||
|
{ name: t('allocation') }, |
||||||
|
{ name: t('description') }, |
||||||
|
].filter(column => column && column.name); |
||||||
|
useEffect(() => { |
||||||
|
getReferralCode(); |
||||||
|
}, [currentPage, rowsPerPage, search]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const cekData = dataExport || [] |
||||||
|
if (cekData.length > 0) { |
||||||
|
exportExcel() |
||||||
|
} |
||||||
|
}, [dataExport]) |
||||||
|
|
||||||
|
const getReferralCode = async () => { |
||||||
|
let start = 0; |
||||||
|
if (currentPage !== 1 && currentPage > 1) { |
||||||
|
start = currentPage * rowsPerPage - rowsPerPage; |
||||||
|
} |
||||||
|
const payload = { |
||||||
|
group_column: { |
||||||
|
"operator": "AND", |
||||||
|
"group_operator": "OR", |
||||||
|
"where": [ |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"logic_operator": "~*", |
||||||
|
"value": search, |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
columns: [], |
||||||
|
"orders": { |
||||||
|
"ascending": true, |
||||||
|
"columns": [ |
||||||
|
'id' |
||||||
|
] |
||||||
|
}, |
||||||
|
"paging": { |
||||||
|
"length": rowsPerPage, |
||||||
|
"start": start |
||||||
|
}, |
||||||
|
'joins': [] |
||||||
|
} |
||||||
|
|
||||||
|
const result = await axios |
||||||
|
.post(REFERRAL_CODE_SEARCH, payload, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
|
||||||
|
if (result && result.data && result.data.code == 200) { |
||||||
|
setDatatable(result.data.data); |
||||||
|
setTotalPage(result.data.totalRecord); |
||||||
|
setLoading(false) |
||||||
|
} else { |
||||||
|
NotificationManager.error('Gagal Mengambil Data!!', 'Failed'); |
||||||
|
setLoading(false) |
||||||
|
} |
||||||
|
} |
||||||
|
const handleExportExcel = async () => { |
||||||
|
let start = 0; |
||||||
|
|
||||||
|
if (currentPage !== 1 && currentPage > 1) { |
||||||
|
start = (currentPage * rowsPerPage) - rowsPerPage |
||||||
|
} |
||||||
|
|
||||||
|
const payload = { |
||||||
|
group_column: { |
||||||
|
"operator": "AND", |
||||||
|
"group_operator": "OR", |
||||||
|
"where": [ |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"logic_operator": "~*", |
||||||
|
"value": search, |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
"columns": [], |
||||||
|
"orders": { |
||||||
|
"ascending": true, |
||||||
|
"columns": [ |
||||||
|
'id' |
||||||
|
] |
||||||
|
}, |
||||||
|
"paging": { |
||||||
|
"length": rowsPerPage, |
||||||
|
"start": start |
||||||
|
}, |
||||||
|
'joins': [] |
||||||
|
} |
||||||
|
|
||||||
|
const result = await axios |
||||||
|
.post(REFERRAL_CODE_SEARCH, payload, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
if (result && result.data && result.data.code == 200) { |
||||||
|
let resData = result.data.data; |
||||||
|
const excelData = []; |
||||||
|
resData.map((val, index) => { |
||||||
|
let dataRow = {}; |
||||||
|
dataRow["Kode"] = val.code; |
||||||
|
dataRow["Jumlah"] = val.amount; |
||||||
|
dataRow["Kuota"] = val.allocation; |
||||||
|
dataRow["Kadaluarsa"] = val.exp; |
||||||
|
dataRow["Deskripsi"] = val.deskription; |
||||||
|
excelData.push(dataRow) |
||||||
|
}) |
||||||
|
await setDataExport(excelData) |
||||||
|
} else { |
||||||
|
NotificationManager.error('Gagal Export Data!!', 'Failed'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const exportExcel = () => { |
||||||
|
const dataExcel = dataExport || []; |
||||||
|
const fileName = `Data ${pageName}.xlsx`; |
||||||
|
const ws = XLSX.utils.json_to_sheet(dataExcel); |
||||||
|
const wb = XLSX.utils.book_new(); |
||||||
|
XLSX.utils.book_append_sheet(wb, ws, `Data ${pageName}`); |
||||||
|
XLSX.writeFile(wb, fileName); |
||||||
|
setDataExport([]) |
||||||
|
} |
||||||
|
|
||||||
|
const handleSearch = e => { |
||||||
|
const value = e.target.value |
||||||
|
setSearch(value); |
||||||
|
setCurrentPage(1) |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOpenDialog = (type) => { |
||||||
|
setOpenDialog(true) |
||||||
|
setTypeDialog(type) |
||||||
|
} |
||||||
|
|
||||||
|
const handleEdit = (data) => { |
||||||
|
setDataEdit(data) |
||||||
|
handleOpenDialog('Edit'); |
||||||
|
} |
||||||
|
|
||||||
|
const handleDelete = async (id) => { |
||||||
|
await setAlertDelete(true) |
||||||
|
await setIdDelete(id) |
||||||
|
} |
||||||
|
|
||||||
|
const handleCloseDialog = (type, data) => { |
||||||
|
if (type === "save") { |
||||||
|
saveReferralCode(data); |
||||||
|
} else if (type === "edit") { |
||||||
|
editReferralCode(data); |
||||||
|
} |
||||||
|
setDataEdit([]) |
||||||
|
setOpenDialog(false) |
||||||
|
} |
||||||
|
|
||||||
|
const saveReferralCode = async (data) => { |
||||||
|
const formData = data |
||||||
|
const result = await axios.post(REFERRAL_CODE_ADD, formData, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
if (result && result.data && result.data.code === 200) { |
||||||
|
getReferralCode() |
||||||
|
NotificationManager.success(`Data berhasil ditambahkan`, 'Success!!'); |
||||||
|
} else { |
||||||
|
NotificationManager.error(`Data gagal ditambahkan`, 'Failed!!'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const editReferralCode = async (data) => { |
||||||
|
let urlEdit = REFERRAL_CODE_EDIT(data.id) |
||||||
|
const formData = data |
||||||
|
const result = await axios.put(urlEdit, formData, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
if (result && result.data && result.data.code === 200) { |
||||||
|
getReferralCode(); |
||||||
|
NotificationManager.success(`Data berhasil diubah`, 'Success!!'); |
||||||
|
} else { |
||||||
|
NotificationManager.error(`Data gagal diubah`, `Failed!!`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const toggleAddDialog = () => { |
||||||
|
setOpenDialog(!openDialog) |
||||||
|
} |
||||||
|
|
||||||
|
const onConfirmDelete = async () => { |
||||||
|
let url = REFERRAL_CODE_DELETE(idDelete); |
||||||
|
const result = await axios.delete(url, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
|
||||||
|
if (result && result.data && result.data.code === 200) { |
||||||
|
getReferralCode() |
||||||
|
setIdDelete(0) |
||||||
|
setAlertDelete(false) |
||||||
|
NotificationManager.success(`Data berhasil dihapus!`, 'Success!!'); |
||||||
|
} else { |
||||||
|
setIdDelete(0) |
||||||
|
setAlertDelete(false) |
||||||
|
NotificationManager.error(`Data gagal dihapus!}`, 'Failed!!'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const cancelDelete = () => { |
||||||
|
setAlertDelete(false) |
||||||
|
setIdDelete(0) |
||||||
|
} |
||||||
|
|
||||||
|
const onShowSizeChange = (current, pageSize) => { |
||||||
|
setRowsPerPage(pageSize) |
||||||
|
} |
||||||
|
|
||||||
|
const onPagination = (current, pageSize) => { |
||||||
|
setCurrentPage(current) |
||||||
|
} |
||||||
|
|
||||||
|
const dataNotAvailable = () => { |
||||||
|
if (dataTable.length === 0) { |
||||||
|
return ( |
||||||
|
<tr> |
||||||
|
<td align="center" colSpan="7">{t('noData')}</td> |
||||||
|
</tr> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<NotificationContainer /> |
||||||
|
<SweetAlert |
||||||
|
show={alertDelete} |
||||||
|
warning |
||||||
|
showCancel |
||||||
|
confirmBtnText="Delete" |
||||||
|
confirmBtnBsStyle="danger" |
||||||
|
title={t('deleteConfirm')} |
||||||
|
onConfirm={onConfirmDelete} |
||||||
|
onCancel={cancelDelete} |
||||||
|
focusCancelBtn |
||||||
|
> |
||||||
|
{t('deleteMsg')} |
||||||
|
</SweetAlert> |
||||||
|
<DialogForm |
||||||
|
openDialog={openDialog} |
||||||
|
closeDialog={handleCloseDialog} |
||||||
|
toggleDialog={() => toggleAddDialog} |
||||||
|
typeDialog={typeDialog} |
||||||
|
dataEdit={dataEdit} |
||||||
|
clickOpenModal={clickOpenModal} |
||||||
|
dataParent={allDataMenu} |
||||||
|
/> |
||||||
|
<Card> |
||||||
|
<CardHeader style={{ display: "flex", justifyContent: "space-between" }}> |
||||||
|
<h4 className="capitalize">{pageName}</h4> |
||||||
|
<Row> |
||||||
|
<Col> |
||||||
|
<Input onChange={handleSearch} value={search} type="text" name="search" id="search" placeholder={t('search')} /> |
||||||
|
</Col> |
||||||
|
<Col> |
||||||
|
<Tooltip title={t('demoAdd')}> |
||||||
|
<Button style={{ background: "#4caf50", color: "#fff" }} onClick={() => handleOpenDialog('Save')}><i className="fa fa-plus"></i></Button> |
||||||
|
</Tooltip> |
||||||
|
<Tooltip title={t('exportExcel')}> |
||||||
|
<Button style={{ marginLeft: "5px" }} onClick={() => handleExportExcel()}><i className="fa fa-print"></i></Button> |
||||||
|
</Tooltip> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
</CardHeader> |
||||||
|
<CardBody> |
||||||
|
<Spin tip="Loading..." spinning={loading}> |
||||||
|
<Table responsive striped hover> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Aksi</th> |
||||||
|
{column.map((i, index) => { |
||||||
|
return ( |
||||||
|
<th key={index} scope="row">{i.name}</th> |
||||||
|
) |
||||||
|
})} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{dataNotAvailable()} |
||||||
|
{dataTable.map((n, index) => { |
||||||
|
return ( |
||||||
|
<tr key={n.id}> |
||||||
|
<td className='nowrap'> |
||||||
|
<Tooltip title={t('delete')}> |
||||||
|
<i id="TooltipDelete" className="fa fa-trash" style={{ color: 'red', marginRight: 10, cursor: "pointer" }} onClick={() => handleDelete(n.id)}></i> |
||||||
|
</Tooltip> |
||||||
|
<Tooltip title={t('edit')}> |
||||||
|
<i id="TooltipEdit" className="fa fa-edit" style={{ color: 'green', cursor: "pointer" }} onClick={() => handleEdit(n)}></i> |
||||||
|
</Tooltip> |
||||||
|
</td> |
||||||
|
<td>{n.code}</td> |
||||||
|
<td>{n.type}</td> |
||||||
|
<td>{toRupiah(n.amount)}</td> |
||||||
|
<td>{n?.exp ? moment(n.exp).format('DD-MM-YYYY HH:mm') : "-"}</td> |
||||||
|
<td>{n.allocation ? n.allocation : "-"}</td> |
||||||
|
<td>{n.description ? n.description : "-"}</td> |
||||||
|
</tr> |
||||||
|
) |
||||||
|
})} |
||||||
|
</tbody> |
||||||
|
</Table> |
||||||
|
</Spin> |
||||||
|
</CardBody> |
||||||
|
</Card> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default ProjectType; |
@ -0,0 +1,201 @@ |
|||||||
|
import React, { useEffect, useState } from 'react' |
||||||
|
import { |
||||||
|
Modal, ModalHeader, ModalBody, ModalFooter, |
||||||
|
Button, Form, FormGroup, Label, Input, Col, Row |
||||||
|
} from 'reactstrap'; |
||||||
|
import { Select } from 'antd'; |
||||||
|
import 'antd/dist/antd.css'; |
||||||
|
import { useTranslation } from 'react-i18next'; |
||||||
|
import "rc-color-picker/assets/index.css"; |
||||||
|
const { Option } = Select |
||||||
|
const DialogForm = ({ openDialog, closeDialog, toggleDialog, typeDialog, dataEdit }) => { |
||||||
|
const [id, setId] = useState(0) |
||||||
|
const [name, setName] = useState('') |
||||||
|
const [email, setEmail] = useState('') |
||||||
|
const [message, setMessage] = useState('') |
||||||
|
const [phoneNumber, setPhoneNumber] = useState('') |
||||||
|
const [role, setRole] = useState(null) |
||||||
|
const [status, setStatus] = useState('') |
||||||
|
const { t } = useTranslation() |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (typeDialog === "Edit") { |
||||||
|
setId(dataEdit.id) |
||||||
|
setName(dataEdit.name) |
||||||
|
setEmail(dataEdit.email) |
||||||
|
setMessage(dataEdit.message) |
||||||
|
setPhoneNumber(dataEdit.number_phone) |
||||||
|
setStatus(dataEdit.status) |
||||||
|
setRole(dataEdit.role) |
||||||
|
} else { |
||||||
|
setId(0) |
||||||
|
setName('') |
||||||
|
setEmail('') |
||||||
|
setMessage('') |
||||||
|
setStatus('') |
||||||
|
setRole('') |
||||||
|
setPhoneNumber('') |
||||||
|
} |
||||||
|
}, [dataEdit, openDialog]) |
||||||
|
|
||||||
|
const validation = () => { |
||||||
|
if (!name || name === "") { |
||||||
|
alert("Name cannot be empty!"); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
const handleSave = () => { |
||||||
|
let data = ''; |
||||||
|
const err = validation(); |
||||||
|
|
||||||
|
if (!err) { |
||||||
|
if (typeDialog === "Save") { |
||||||
|
data = { |
||||||
|
name, |
||||||
|
email, |
||||||
|
message, |
||||||
|
number_phone: phoneNumber, |
||||||
|
role, |
||||||
|
status |
||||||
|
} |
||||||
|
closeDialog('save', data); |
||||||
|
} else { |
||||||
|
data = { |
||||||
|
id, |
||||||
|
name, |
||||||
|
email, |
||||||
|
message, |
||||||
|
number_phone: phoneNumber, |
||||||
|
role, |
||||||
|
status |
||||||
|
} |
||||||
|
closeDialog('edit', data); |
||||||
|
} |
||||||
|
setId(0) |
||||||
|
setName('') |
||||||
|
} |
||||||
|
} |
||||||
|
const handleCancel = () => { |
||||||
|
closeDialog('cancel', 'none') |
||||||
|
setId(0) |
||||||
|
setName('') |
||||||
|
} |
||||||
|
const onChangeStatus = (val) => { |
||||||
|
setStatus(val) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const renderForm = () => { |
||||||
|
return ( |
||||||
|
<Form> |
||||||
|
<Row> |
||||||
|
<Col md={12}> |
||||||
|
<span style={{ color: "red" }}>*</span> Wajib diisi. |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('name')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
value={name} |
||||||
|
onChange={(e) => setName(e.target.value)} |
||||||
|
placeholder={t('inputName')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('email')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
value={email} |
||||||
|
onChange={(e) => setEmail(e.target.value)} |
||||||
|
placeholder={t('inputEmail')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('phoneNumber')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
value={phoneNumber} |
||||||
|
onChange={(e) => setName(e.target.value)} |
||||||
|
placeholder={t('inputNoPhone')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
<Col md={6}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('role')}<span style={{ color: "red" }}>*</span></Label> |
||||||
|
<Input |
||||||
|
type="text" |
||||||
|
value={role} |
||||||
|
onChange={(e) => setRole(e.target.value)} |
||||||
|
placeholder={t('inputRole')} |
||||||
|
/> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col md={12}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize"> |
||||||
|
{t('status')}<span style={{ color: "red" }}>*</span> |
||||||
|
</Label> |
||||||
|
<Select |
||||||
|
showSearch |
||||||
|
filterOption={(inputValue, option) => |
||||||
|
option.children.toLowerCase().includes(inputValue.toLowerCase()) |
||||||
|
} |
||||||
|
value={status} |
||||||
|
defaultValue={status} |
||||||
|
onChange={onChangeStatus} |
||||||
|
style={{ width: "100%" }} |
||||||
|
> |
||||||
|
<Option value="New Contact">New Contact</Option> |
||||||
|
<Option value="Presentation">Presentation</Option> |
||||||
|
<Option value="Live Demo">Live Demo</Option> |
||||||
|
<Option value="Trial">Trial</Option> |
||||||
|
<Option value="Go-Live">Go-Live</Option> |
||||||
|
<Option value="Postpone">Postpone</Option> |
||||||
|
|
||||||
|
</Select> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
<Row> |
||||||
|
<Col md={12}> |
||||||
|
<FormGroup> |
||||||
|
<Label className="capitalize">{t('message')}</Label> |
||||||
|
<Input row="4" type="textarea" value={message} onChange={(e) => setMessage(e.target.value)} placeholder={t('inputMessage')} /> |
||||||
|
</FormGroup> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
</Form> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Modal size="lg" isOpen={openDialog} toggle={toggleDialog}> |
||||||
|
<ModalHeader className="capitalize" toggle={closeDialog}>{typeDialog == "Save" ? `Add` : "Edit"} {t('Demo')}</ModalHeader> |
||||||
|
<ModalBody> |
||||||
|
{renderForm()} |
||||||
|
</ModalBody> |
||||||
|
<ModalFooter> |
||||||
|
<Button color="primary" onClick={() => handleSave()}>{typeDialog}</Button>{' '} |
||||||
|
<Button className="capitalize" color="secondary" onClick={() => handleCancel()}>{t('cancel')}</Button> |
||||||
|
</ModalFooter> |
||||||
|
</Modal> |
||||||
|
</> |
||||||
|
) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
export default DialogForm; |
@ -0,0 +1,374 @@ |
|||||||
|
import * as XLSX from 'xlsx'; |
||||||
|
import DialogForm from './DialogForm'; |
||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import SweetAlert from 'react-bootstrap-sweetalert'; |
||||||
|
import axios from "../../../const/interceptorApi" |
||||||
|
import { Card, CardBody, CardHeader, Col, Row, Input, Table } from 'reactstrap'; |
||||||
|
import { SALES_CONTACT_EDIT, SALES_CONTACT_SEARCH, SALES_CONTACT_LIST, SALES_CONTACT_GET_ID, SALES_CONTACT_ADD, SALES_CONTACT_DELETE } from '../../../const/ApiConst'; |
||||||
|
import { NotificationContainer, NotificationManager } from 'react-notifications'; |
||||||
|
import { Spin, Button, Tooltip } from 'antd'; |
||||||
|
import { useTranslation } from 'react-i18next'; |
||||||
|
|
||||||
|
|
||||||
|
const ProjectType = ({ params, ...props }) => { |
||||||
|
let role_id = 0, user_id = 0, isLogin = false, token = '', company_id = 0, all_project = null, role_name = '', hierarchy = [], user_name = ''; |
||||||
|
if (props && props.role_id && props.user_id) { |
||||||
|
role_id = props.role_id; |
||||||
|
user_id = props.user_id; |
||||||
|
token = props.token; |
||||||
|
isLogin = props.isLogin; |
||||||
|
company_id = props.company_id; |
||||||
|
all_project = props.all_project; |
||||||
|
role_name = props.role_name; |
||||||
|
isLogin = props.isLogin; |
||||||
|
hierarchy = props.hierarchy; |
||||||
|
user_name = props.user_name; |
||||||
|
} |
||||||
|
|
||||||
|
const HEADER = { |
||||||
|
headers: { |
||||||
|
"Content-Type": "application/json", |
||||||
|
"Authorization": `Bearer ${token}` |
||||||
|
} |
||||||
|
} |
||||||
|
const pageName = params.name; |
||||||
|
|
||||||
|
const [alertDelete, setAlertDelete] = useState(false) |
||||||
|
const [allDataMenu, setAllDataMenu] = useState([]) |
||||||
|
const [clickOpenModal, setClickOpenModal] = useState(false) |
||||||
|
const [currentPage, setCurrentPage] = useState(1) |
||||||
|
const [dataEdit, setDataEdit] = useState([]) |
||||||
|
const [dataExport, setDataExport] = useState([]) |
||||||
|
const [dataTable, setDatatable] = useState([]) |
||||||
|
const [idDelete, setIdDelete] = useState(0) |
||||||
|
const [openDialog, setOpenDialog] = useState(false) |
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(10) |
||||||
|
const [search, setSearch] = useState("") |
||||||
|
const [totalPage, setTotalPage] = useState(0) |
||||||
|
const [typeDialog, setTypeDialog] = useState('Save') |
||||||
|
const [dataDemo, setDataDemo] = useState([]) |
||||||
|
const [listCompany, setListCompany] = useState([]) |
||||||
|
const [loading, setLoading] = useState(true); |
||||||
|
const { t } = useTranslation() |
||||||
|
const column = [ |
||||||
|
{ name: t('name') }, |
||||||
|
{ name: t('company') }, |
||||||
|
{ name: t('email') }, |
||||||
|
{ name: t('roles') }, |
||||||
|
{ name: t('phoneNumber') }, |
||||||
|
{ name: t('status') }, |
||||||
|
{ name: t('description') }, |
||||||
|
].filter(column => column && column.name); |
||||||
|
useEffect(() => { |
||||||
|
getDataContactSales(); |
||||||
|
}, [currentPage, rowsPerPage, search]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const cekData = dataExport || [] |
||||||
|
if (cekData.length > 0) { |
||||||
|
exportExcel() |
||||||
|
} |
||||||
|
}, [dataExport]) |
||||||
|
|
||||||
|
const getDataContactSales = async () => { |
||||||
|
let start = 0; |
||||||
|
if (currentPage !== 1 && currentPage > 1) { |
||||||
|
start = currentPage * rowsPerPage - rowsPerPage; |
||||||
|
} |
||||||
|
const payload = { |
||||||
|
group_column: { |
||||||
|
"operator": "AND", |
||||||
|
"group_operator": "OR", |
||||||
|
"where": [ |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"logic_operator": "~*", |
||||||
|
"value": search, |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
columns: [], |
||||||
|
"orders": { |
||||||
|
"ascending": true, |
||||||
|
"columns": [ |
||||||
|
'id' |
||||||
|
] |
||||||
|
}, |
||||||
|
"paging": { |
||||||
|
"length": rowsPerPage, |
||||||
|
"start": start |
||||||
|
}, |
||||||
|
'joins': [] |
||||||
|
} |
||||||
|
|
||||||
|
const result = await axios |
||||||
|
.post(SALES_CONTACT_SEARCH, payload, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
|
||||||
|
if (result && result.data && result.data.code == 200) { |
||||||
|
setDatatable(result.data.data); |
||||||
|
setTotalPage(result.data.totalRecord); |
||||||
|
setLoading(false) |
||||||
|
} else { |
||||||
|
NotificationManager.error('Gagal Mengambil Data!!', 'Failed'); |
||||||
|
setLoading(false) |
||||||
|
} |
||||||
|
} |
||||||
|
const handleExportExcel = async () => { |
||||||
|
let start = 0; |
||||||
|
|
||||||
|
if (currentPage !== 1 && currentPage > 1) { |
||||||
|
start = (currentPage * rowsPerPage) - rowsPerPage |
||||||
|
} |
||||||
|
|
||||||
|
const payload = { |
||||||
|
group_column: { |
||||||
|
"operator": "AND", |
||||||
|
"group_operator": "OR", |
||||||
|
"where": [ |
||||||
|
{ |
||||||
|
"name": "name", |
||||||
|
"logic_operator": "~*", |
||||||
|
"value": search, |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
"columns": [], |
||||||
|
"orders": { |
||||||
|
"ascending": true, |
||||||
|
"columns": [ |
||||||
|
'id' |
||||||
|
] |
||||||
|
}, |
||||||
|
"paging": { |
||||||
|
"length": rowsPerPage, |
||||||
|
"start": start |
||||||
|
}, |
||||||
|
'joins': [] |
||||||
|
} |
||||||
|
|
||||||
|
const result = await axios |
||||||
|
.post(SALES_CONTACT_SEARCH, payload, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
if (result && result.data && result.data.code == 200) { |
||||||
|
let resData = result.data.data; |
||||||
|
const excelData = []; |
||||||
|
resData.map((val, index) => { |
||||||
|
let dataRow = {}; |
||||||
|
dataRow["Nama"] = val.name; |
||||||
|
dataRow["Email"] = val.email; |
||||||
|
dataRow["Role"] = val.role; |
||||||
|
dataRow["No Telepon"] = val.number_phone; |
||||||
|
dataRow["Status"] = val.message; |
||||||
|
dataRow["Message"] = val.message; |
||||||
|
excelData.push(dataRow) |
||||||
|
}) |
||||||
|
await setDataExport(excelData) |
||||||
|
} else { |
||||||
|
NotificationManager.error('Gagal Export Data!!', 'Failed'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const exportExcel = () => { |
||||||
|
const dataExcel = dataExport || []; |
||||||
|
const fileName = `Data ${pageName}.xlsx`; |
||||||
|
const ws = XLSX.utils.json_to_sheet(dataExcel); |
||||||
|
const wb = XLSX.utils.book_new(); |
||||||
|
XLSX.utils.book_append_sheet(wb, ws, `Data ${pageName}`); |
||||||
|
XLSX.writeFile(wb, fileName); |
||||||
|
setDataExport([]) |
||||||
|
} |
||||||
|
|
||||||
|
const handleSearch = e => { |
||||||
|
const value = e.target.value |
||||||
|
setSearch(value); |
||||||
|
setCurrentPage(1) |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOpenDialog = (type) => { |
||||||
|
setOpenDialog(true) |
||||||
|
setTypeDialog(type) |
||||||
|
} |
||||||
|
|
||||||
|
const handleEdit = (data) => { |
||||||
|
setDataEdit(data) |
||||||
|
handleOpenDialog('Edit'); |
||||||
|
} |
||||||
|
|
||||||
|
const handleDelete = async (id) => { |
||||||
|
await setAlertDelete(true) |
||||||
|
await setIdDelete(id) |
||||||
|
} |
||||||
|
|
||||||
|
const handleCloseDialog = (type, data) => { |
||||||
|
if (type === "save") { |
||||||
|
saveContactSales(data); |
||||||
|
} else if (type === "edit") { |
||||||
|
editContactSales(data); |
||||||
|
} |
||||||
|
setDataEdit([]) |
||||||
|
setOpenDialog(false) |
||||||
|
} |
||||||
|
|
||||||
|
const saveContactSales = async (data) => { |
||||||
|
const formData = data |
||||||
|
const result = await axios.post(SALES_CONTACT_ADD, formData, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
if (result && result.data && result.data.code === 200) { |
||||||
|
getDataContactSales() |
||||||
|
NotificationManager.success(`Data berhasil ditambahkan`, 'Success!!'); |
||||||
|
} else { |
||||||
|
NotificationManager.error(`Data gagal ditambahkan`, 'Failed!!'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const editContactSales = async (data) => { |
||||||
|
let urlEdit = SALES_CONTACT_EDIT(data.id) |
||||||
|
const formData = data |
||||||
|
const result = await axios.put(urlEdit, formData, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
if (result && result.data && result.data.code === 200) { |
||||||
|
getDataContactSales(); |
||||||
|
NotificationManager.success(`Data berhasil diubah`, 'Success!!'); |
||||||
|
} else { |
||||||
|
NotificationManager.error(`Data gagal diubah`, `Failed!!`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const toggleAddDialog = () => { |
||||||
|
setOpenDialog(!openDialog) |
||||||
|
} |
||||||
|
|
||||||
|
const onConfirmDelete = async () => { |
||||||
|
let url = SALES_CONTACT_DELETE(idDelete); |
||||||
|
const result = await axios.delete(url, HEADER) |
||||||
|
.then(res => res) |
||||||
|
.catch((error) => error.response); |
||||||
|
|
||||||
|
if (result && result.data && result.data.code === 200) { |
||||||
|
getDataContactSales() |
||||||
|
setIdDelete(0) |
||||||
|
setAlertDelete(false) |
||||||
|
NotificationManager.success(`Data berhasil dihapus!`, 'Success!!'); |
||||||
|
} else { |
||||||
|
setIdDelete(0) |
||||||
|
setAlertDelete(false) |
||||||
|
NotificationManager.error(`Data gagal dihapus!}`, 'Failed!!'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const cancelDelete = () => { |
||||||
|
setAlertDelete(false) |
||||||
|
setIdDelete(0) |
||||||
|
} |
||||||
|
|
||||||
|
const onShowSizeChange = (current, pageSize) => { |
||||||
|
setRowsPerPage(pageSize) |
||||||
|
} |
||||||
|
|
||||||
|
const onPagination = (current, pageSize) => { |
||||||
|
setCurrentPage(current) |
||||||
|
} |
||||||
|
|
||||||
|
const dataNotAvailable = () => { |
||||||
|
if (dataTable.length === 0) { |
||||||
|
return ( |
||||||
|
<tr> |
||||||
|
<td align="center" colSpan="8">{t('noData')}</td> |
||||||
|
</tr> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<NotificationContainer /> |
||||||
|
<SweetAlert |
||||||
|
show={alertDelete} |
||||||
|
warning |
||||||
|
showCancel |
||||||
|
confirmBtnText="Delete" |
||||||
|
confirmBtnBsStyle="danger" |
||||||
|
title={t('deleteConfirm')} |
||||||
|
onConfirm={onConfirmDelete} |
||||||
|
onCancel={cancelDelete} |
||||||
|
focusCancelBtn |
||||||
|
> |
||||||
|
{t('deleteMsg')} |
||||||
|
</SweetAlert> |
||||||
|
<DialogForm |
||||||
|
openDialog={openDialog} |
||||||
|
closeDialog={handleCloseDialog} |
||||||
|
toggleDialog={() => toggleAddDialog} |
||||||
|
typeDialog={typeDialog} |
||||||
|
dataEdit={dataEdit} |
||||||
|
clickOpenModal={clickOpenModal} |
||||||
|
dataParent={allDataMenu} |
||||||
|
/> |
||||||
|
<Card> |
||||||
|
<CardHeader style={{ display: "flex", justifyContent: "space-between" }}> |
||||||
|
<h4 className="capitalize">{pageName}</h4> |
||||||
|
<Row> |
||||||
|
<Col> |
||||||
|
<Input onChange={handleSearch} value={search} type="text" name="search" id="search" placeholder={t('searchDemo')} /> |
||||||
|
</Col> |
||||||
|
<Col> |
||||||
|
<Tooltip title={t('demoAdd')}> |
||||||
|
<Button style={{ background: "#4caf50", color: "#fff" }} onClick={() => handleOpenDialog('Save')}><i className="fa fa-plus"></i></Button> |
||||||
|
</Tooltip> |
||||||
|
<Tooltip title={t('exportExcel')}> |
||||||
|
<Button style={{ marginLeft: "5px" }} onClick={() => handleExportExcel()}><i className="fa fa-print"></i></Button> |
||||||
|
</Tooltip> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
</CardHeader> |
||||||
|
<CardBody> |
||||||
|
<Spin tip="Loading..." spinning={loading}> |
||||||
|
<Table responsive striped hover> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Aksi</th> |
||||||
|
{column.map((i, index) => { |
||||||
|
return ( |
||||||
|
<th key={index} scope="row">{i.name}</th> |
||||||
|
) |
||||||
|
})} |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{dataNotAvailable()} |
||||||
|
{dataTable.map((n, index) => { |
||||||
|
return ( |
||||||
|
<tr key={n.id}> |
||||||
|
<td className='nowrap'> |
||||||
|
<Tooltip title={t('delete')}> |
||||||
|
<i id="TooltipDelete" className="fa fa-trash" style={{ color: 'red', marginRight: 10, cursor: "pointer" }} onClick={() => handleDelete(n.id)}></i> |
||||||
|
</Tooltip> |
||||||
|
<Tooltip title={t('edit')}> |
||||||
|
<i id="TooltipEdit" className="fa fa-edit" style={{ color: 'green', cursor: "pointer" }} onClick={() => handleEdit(n)}></i> |
||||||
|
</Tooltip> |
||||||
|
</td> |
||||||
|
<td>{n.name}</td> |
||||||
|
<td>{n.company_name}</td> |
||||||
|
<td>{n.email}</td> |
||||||
|
<td>{n.role}</td> |
||||||
|
<td>{n.number_phone}</td> |
||||||
|
<td>{n.status}</td> |
||||||
|
<td>{n.message}</td> |
||||||
|
</tr> |
||||||
|
) |
||||||
|
})} |
||||||
|
</tbody> |
||||||
|
</Table> |
||||||
|
</Spin> |
||||||
|
</CardBody> |
||||||
|
</Card> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default ProjectType; |
Loading…
Reference in new issue