🔨 Add manager

This commit is contained in:
Sebastien Clement 2021-01-30 20:33:06 +01:00
parent 85630ec233
commit 39a02da5f4
30 changed files with 952 additions and 172 deletions

12
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="db.sqlite" uuid="d3100bad-6ce1-4918-8a79-868dad8b16a7">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db.sqlite</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

11
app.js
View File

@ -6,10 +6,13 @@ const bodyParser = require('body-parser')
const logger = require('./config/winston'); const logger = require('./config/winston');
const sassMiddleware = require('node-sass-middleware'); const sassMiddleware = require('node-sass-middleware');
const expressWinston = require('express-winston'); const expressWinston = require('express-winston');
const flash = require('connect-flash');
const i18n = require('i18n'); const i18n = require('i18n');
const session = require('express-session');
const indexRouter = require('./routes/index'); const indexRouter = require('./routes/index');
const loginRouter = require('./routes/login'); const loginRouter = require('./routes/login');
const presetManagerRouter = require('./routes/preset-manager');
const passport = require('./config/passport'); const passport = require('./config/passport');
@ -43,7 +46,7 @@ app.use(expressWinston.logger({
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(require('express-session')({ secret: 'keyboard cat', resave: false, saveUninitialized: false })); app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
// app.use(sassMiddleware({ // app.use(sassMiddleware({
@ -61,14 +64,20 @@ app.use(i18n.init)
app.use(passport.initialize()); app.use(passport.initialize());
app.use(passport.session()); app.use(passport.session());
app.use(flash());
app.use('/', indexRouter); app.use('/', indexRouter);
app.use('/', loginRouter); app.use('/', loginRouter);
app.use('/', presetManagerRouter);
// Boootstrap JS Files // Boootstrap JS Files
app.use('/js/bootstrap.min.js', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/js/bootstrap.min.js'))) app.use('/js/bootstrap.min.js', express.static(path.join(__dirname, '/node_modules/bootstrap/dist/js/bootstrap.min.js')))
// Fontawesome Files
app.use('/css/fa-all.min.css', express.static(path.join(__dirname, '/node_modules/@fortawesome/fontawesome-free/css/all.min.css')))
app.use('/webfonts/', express.static(path.join(__dirname, '/node_modules/@fortawesome/fontawesome-free/webfonts')))
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function (req, res, next) { app.use(function (req, res, next) {
next(createError(404)); next(createError(404));

View File

@ -86,7 +86,7 @@ function onListening() {
} }
async function init() { async function init() {
console.log(`Checking database connection...`); logger.info(`Checking database connection...`);
try { try {
await sequelize.authenticate(); await sequelize.authenticate();
logger.info('Database connection OK!'); logger.info('Database connection OK!');

BIN
db.sqlite

Binary file not shown.

View File

@ -15,5 +15,34 @@
"Cutting Speed": "Cutting Speed", "Cutting Speed": "Cutting Speed",
"Feed By Tooth": "Feed By Tooth", "Feed By Tooth": "Feed By Tooth",
"Step Down factor": "Step Down factor", "Step Down factor": "Step Down factor",
"Spindle Max Speed": "Spindle Max Speed" "Spindle Max Speed": "Spindle Max Speed",
"Username": "Username",
"Password": "Password",
"Log In": "Log In",
"Login": "Login",
"Logout": "Logout",
"Manage Presets": "Manage Presets",
"Preset Manager": "Preset Manager",
"Name": "Name",
"Cut Speed": "Cut Speed",
"∅ > 1mm": "∅ > 1mm",
"∅ > 2mm": "∅ > 2mm",
"∅ > 3mm": "∅ > 3mm",
"∅ > 4mm": "∅ > 4mm",
"∅ > 5mm": "∅ > 5mm",
"∅ > 6mm": "∅ > 6mm",
"∅ > 8mm": "∅ > 8mm",
"Feed By Tooth (By tool diameter)": "Feed By Tooth (By tool diameter)",
"∅ ≥ 1mm": "∅ ≥ 1mm",
"∅ ≥ 2mm": "∅ ≥ 2mm",
"∅ ≥ 3mm": "∅ ≥ 3mm",
"∅ ≥ 4mm": "∅ ≥ 4mm",
"∅ ≥ 5mm": "∅ ≥ 5mm",
"∅ ≥ 6mm": "∅ ≥ 6mm",
"∅ ≥ 8mm": "∅ ≥ 8mm",
"K factor (By tool diameter)": "K factor (By tool diameter)",
"∅ < 2mm": "∅ < 2mm",
"Material Preset Editor": "Material Preset Editor",
"name": "name",
"Save": "Save"
} }

10
middleware/is-admin.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = function (req, res, next){
if(req.user !== null && req.user.is_admin){
next()
}
else{
res.redirect('/')
}
}

26
package-lock.json generated
View File

@ -14,6 +14,11 @@
"kuler": "^2.0.0" "kuler": "^2.0.0"
} }
}, },
"@fortawesome/fontawesome-free": {
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.2.tgz",
"integrity": "sha512-7l/AX41m609L/EXI9EKH3Vs3v0iA8tKlIOGtw+kgcoanI7p+e4I4GYLqW3UXWiTnjSFymKSmTTPKYrivzbxxqA=="
},
"@types/node": { "@types/node": {
"version": "14.14.22", "version": "14.14.22",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz",
@ -428,6 +433,11 @@
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz", "resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz",
"integrity": "sha1-F03MUSQ7nqwj+NmCFa62aU4uihI=" "integrity": "sha1-F03MUSQ7nqwj+NmCFa62aU4uihI="
}, },
"connect-flash": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz",
"integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA="
},
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -701,6 +711,22 @@
} }
} }
}, },
"express-validator": {
"version": "6.9.2",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.9.2.tgz",
"integrity": "sha512-Yqlsw2/uBobtBVkP+gnds8OMmVAEb3uTI4uXC93l0Ym5JGHgr8Vd4ws7oSo7GGYpWn5YCq4UePMEppKchURXrw==",
"requires": {
"lodash": "^4.17.20",
"validator": "^13.5.2"
},
"dependencies": {
"validator": {
"version": "13.5.2",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.5.2.tgz",
"integrity": "sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ=="
}
}
},
"express-winston": { "express-winston": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/express-winston/-/express-winston-4.0.5.tgz", "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-4.0.5.tgz",

View File

@ -6,15 +6,18 @@
"start": "node ./bin/www" "start": "node ./bin/www"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.2",
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"bootstrap": "^5.0.0-beta1", "bootstrap": "^5.0.0-beta1",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-flash": "^0.1.1",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"debug": "~2.6.9", "debug": "~2.6.9",
"ejs": "~2.6.1", "ejs": "~2.6.1",
"express": "~4.16.1", "express": "~4.16.1",
"express-session": "^1.17.1", "express-session": "^1.17.1",
"express-validator": "^6.9.2",
"express-winston": "^4.0.5", "express-winston": "^4.0.5",
"http-errors": "~1.6.3", "http-errors": "~1.6.3",
"i18n": "^0.13.2", "i18n": "^0.13.2",

View File

@ -5,13 +5,13 @@
@import "../../node_modules/bootstrap/scss/images"; @import "../../node_modules/bootstrap/scss/images";
@import "../../node_modules/bootstrap/scss/containers"; @import "../../node_modules/bootstrap/scss/containers";
@import "../../node_modules/bootstrap/scss/grid"; @import "../../node_modules/bootstrap/scss/grid";
//@import "tables"; @import "../../node_modules/bootstrap/scss/tables";
@import "../../node_modules/bootstrap/scss/forms"; @import "../../node_modules/bootstrap/scss/forms";
@import "../../node_modules/bootstrap/scss/buttons"; @import "../../node_modules/bootstrap/scss/buttons";
@import "../../node_modules/bootstrap/scss/transitions"; @import "../../node_modules/bootstrap/scss/transitions";
@import "../../node_modules/bootstrap/scss/dropdown"; @import "../../node_modules/bootstrap/scss/dropdown";
//@import "button-group"; //@import "button-group";
//@import "nav"; @import "../../node_modules/bootstrap/scss/nav";
@import "../../node_modules/bootstrap/scss/navbar"; @import "../../node_modules/bootstrap/scss/navbar";
@import "../../node_modules/bootstrap/scss/card"; @import "../../node_modules/bootstrap/scss/card";
//@import "accordion"; //@import "accordion";

View File

@ -1916,6 +1916,201 @@ progress {
--bs-gutter-y: 3rem; --bs-gutter-y: 3rem;
} }
} }
.table {
--bs-table-bg: transparent;
--bs-table-striped-color: #212529;
--bs-table-striped-bg: rgba(0, 0, 0, 0.05);
--bs-table-active-color: #212529;
--bs-table-active-bg: rgba(0, 0, 0, 0.1);
--bs-table-hover-color: #212529;
--bs-table-hover-bg: rgba(0, 0, 0, 0.075);
width: 100%;
margin-bottom: 1rem;
color: #212529;
vertical-align: top;
border-color: #dee2e6;
}
.table > :not(caption) > * > * {
padding: 0.5rem 0.5rem;
background-color: var(--bs-table-bg);
background-image: linear-gradient(var(--bs-table-accent-bg), var(--bs-table-accent-bg));
border-bottom-width: 1px;
}
.table > tbody {
vertical-align: inherit;
}
.table > thead {
vertical-align: bottom;
}
.table > :not(:last-child) > :last-child > * {
border-bottom-color: currentColor;
}
.caption-top {
caption-side: top;
}
.table-sm > :not(caption) > * > * {
padding: 0.25rem 0.25rem;
}
.table-bordered > :not(caption) > * {
border-width: 1px 0;
}
.table-bordered > :not(caption) > * > * {
border-width: 0 1px;
}
.table-borderless > :not(caption) > * > * {
border-bottom-width: 0;
}
.table-striped > tbody > tr:nth-of-type(odd) {
--bs-table-accent-bg: var(--bs-table-striped-bg);
color: var(--bs-table-striped-color);
}
.table-active {
--bs-table-accent-bg: var(--bs-table-active-bg);
color: var(--bs-table-active-color);
}
.table-hover > tbody > tr:hover {
--bs-table-accent-bg: var(--bs-table-hover-bg);
color: var(--bs-table-hover-color);
}
.table-primary {
--bs-table-bg: #cfe2ff;
--bs-table-striped-bg: #c5d7f2;
--bs-table-striped-color: #000;
--bs-table-active-bg: #bacbe6;
--bs-table-active-color: #000;
--bs-table-hover-bg: #bfd1ec;
--bs-table-hover-color: #000;
color: #000;
border-color: #bacbe6;
}
.table-secondary {
--bs-table-bg: #d6d8d9;
--bs-table-striped-bg: #cbcdce;
--bs-table-striped-color: #000;
--bs-table-active-bg: #c1c2c3;
--bs-table-active-color: #000;
--bs-table-hover-bg: #c6c8c9;
--bs-table-hover-color: #000;
color: #000;
border-color: #c1c2c3;
}
.table-success {
--bs-table-bg: #d1e7dd;
--bs-table-striped-bg: #c7dbd2;
--bs-table-striped-color: #000;
--bs-table-active-bg: #bcd0c7;
--bs-table-active-color: #000;
--bs-table-hover-bg: #c1d6cc;
--bs-table-hover-color: #000;
color: #000;
border-color: #bcd0c7;
}
.table-info {
--bs-table-bg: #cff4fc;
--bs-table-striped-bg: #c5e8ef;
--bs-table-striped-color: #000;
--bs-table-active-bg: #badce3;
--bs-table-active-color: #000;
--bs-table-hover-bg: #bfe2e9;
--bs-table-hover-color: #000;
color: #000;
border-color: #badce3;
}
.table-warning {
--bs-table-bg: #fff3cd;
--bs-table-striped-bg: #f2e7c3;
--bs-table-striped-color: #000;
--bs-table-active-bg: #e6dbb9;
--bs-table-active-color: #000;
--bs-table-hover-bg: #ece1be;
--bs-table-hover-color: #000;
color: #000;
border-color: #e6dbb9;
}
.table-danger {
--bs-table-bg: #f8d7da;
--bs-table-striped-bg: #eccccf;
--bs-table-striped-color: #000;
--bs-table-active-bg: #dfc2c4;
--bs-table-active-color: #000;
--bs-table-hover-bg: #e5c7ca;
--bs-table-hover-color: #000;
color: #000;
border-color: #dfc2c4;
}
.table-light {
--bs-table-bg: #f8f9fa;
--bs-table-striped-bg: #ecedee;
--bs-table-striped-color: #000;
--bs-table-active-bg: #dfe0e1;
--bs-table-active-color: #000;
--bs-table-hover-bg: #e5e6e7;
--bs-table-hover-color: #000;
color: #000;
border-color: #dfe0e1;
}
.table-dark {
--bs-table-bg: #292929;
--bs-table-striped-bg: #343434;
--bs-table-striped-color: #fff;
--bs-table-active-bg: #3e3e3e;
--bs-table-active-color: #fff;
--bs-table-hover-bg: #393939;
--bs-table-hover-color: #fff;
color: #fff;
border-color: #3e3e3e;
}
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
@media (max-width: 575.98px) {
.table-responsive-sm {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 767.98px) {
.table-responsive-md {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 991.98px) {
.table-responsive-lg {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 1199.98px) {
.table-responsive-xl {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 1399.98px) {
.table-responsive-xxl {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
.form-label { .form-label {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -3513,6 +3708,93 @@ textarea.form-control-lg {
color: #adb5bd; color: #adb5bd;
} }
.nav {
display: flex;
flex-wrap: wrap;
padding-left: 0;
margin-bottom: 0;
list-style: none;
}
.nav-link {
display: block;
padding: 0.5rem 1rem;
color: white;
text-decoration: none;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.nav-link {
transition: none;
}
}
.nav-link:hover, .nav-link:focus {
color: #b58e51;
}
.nav-link.disabled {
color: #6c757d;
pointer-events: none;
cursor: default;
}
.nav-tabs {
border-bottom: 1px solid #b58e51;
}
.nav-tabs .nav-link {
margin-bottom: -1px;
border: 1px solid transparent;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
border-color: #343a40 #343a40 #b58e51;
}
.nav-tabs .nav-link.disabled {
color: #6c757d;
background-color: transparent;
border-color: transparent;
}
.nav-tabs .nav-link.active,
.nav-tabs .nav-item.show .nav-link {
color: #b58e51;
background-color: #222222;
border-color: #b58e51 #b58e51 #222222;
}
.nav-tabs .dropdown-menu {
margin-top: -1px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.nav-pills .nav-link {
border-radius: 0.25rem;
}
.nav-pills .nav-link.active,
.nav-pills .show > .nav-link {
color: #fff;
background-color: #b58e51;
}
.nav-fill > .nav-link,
.nav-fill .nav-item {
flex: 1 1 auto;
text-align: center;
}
.nav-justified > .nav-link,
.nav-justified .nav-item {
flex-basis: 0;
flex-grow: 1;
text-align: center;
}
.tab-content > .tab-pane {
display: none;
}
.tab-content > .active {
display: block;
}
.navbar { .navbar {
position: relative; position: relative;
display: flex; display: flex;

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,6 @@ $custom-colors:(
); );
$enable-shadows: true; $enable-shadows: true;
$btn-box-shadow: none; $btn-box-shadow: none;
@ -39,6 +38,16 @@ $alert-bg-scale: 0%;
$alert-border-scale: -10%; $alert-border-scale: -10%;
$alert-color-scale: -100%; $alert-color-scale: -100%;
$nav-tabs-border-color: $accent;
$nav-tabs-link-active-border-color: $accent $accent $body-bg;
$nav-tabs-link-active-color: $accent;
$nav-tabs-link-hover-border-color: $secondary $secondary $nav-tabs-border-color;
$nav-link-color: white;
$nav-link-hover-color: $accent;
// Configuration // Configuration
@import "../../node_modules/bootstrap/scss/functions"; @import "../../node_modules/bootstrap/scss/functions";
@import "../../node_modules/bootstrap/scss/variables"; @import "../../node_modules/bootstrap/scss/variables";

118
public/js/index.js Normal file
View File

@ -0,0 +1,118 @@
preset_cut = JSON.parse(preset_cut);
preset_step_down_factor = JSON.parse(preset_step_down_factor);
let preset_cut_elem = document.querySelector('#preset_cut');
let preset_step_down_factor_elem = document.querySelector('#preset_step_down_factor');
let tooth_nbr_elem = document.querySelector('#tooth_nbr');
let tool_diameter_elem = document.querySelector('#tool_diameter');
let max_rpm_elem = document.querySelector('#max_rpm')
let rpm_elem = document.querySelector('#rpm');
let feed_rate_elem = document.querySelector('#feed_rate');
let feed_rate_sec_elem = document.querySelector('#feed_rate_sec');
let step_down_elem = document.querySelector('#step_down');
let cutting_speed_elem = document.querySelector('#cutting_speed');
let feed_tooth_elem = document.querySelector('#feed_tooth');
let step_down_factor_elem = document.querySelector('#step_down_factor');
document.addEventListener("DOMContentLoaded", function (event) {
load();
onChange();
});
function onChange() {
if (preset_cut_elem.value === "" || preset_step_down_factor_elem.value === "" || tool_diameter_elem.value === "0" || tooth_nbr_elem.value === "0" || max_rpm_elem.value === "0") {
return;
}
save();
let new_preset_cut = getPresetValue(preset_cut, parseInt(preset_cut_elem.value, 10));
let new_preset_steep_down = getPresetValue(preset_step_down_factor, parseInt(preset_step_down_factor_elem.value, 10))
let new_tool_diameter = parseFloat(tool_diameter_elem.value);
let new_tooth_nbr = parseInt(tooth_nbr_elem.value);
let max_rpm = parseInt(max_rpm_elem.value);
let rpm = (1000 * new_preset_cut.cut_speed) / (3.14 * new_tool_diameter);
if (rpm > max_rpm)
rpm = max_rpm;
let feed_rate = rpm * getFeedByTooth(new_tool_diameter, new_preset_cut) * new_tooth_nbr;
let feed_rate_sec = feed_rate / 60;
let step_down = new_tool_diameter * getStepDownFactor(new_tool_diameter, new_preset_steep_down);
rpm = Math.round(rpm);
feed_rate = Math.round(feed_rate);
feed_rate_sec = feed_rate_sec.toFixed(2)
step_down = step_down.toFixed(2);
rpm_elem.value = rpm;
feed_rate_elem.value = feed_rate;
feed_rate_sec_elem.value = feed_rate_sec;
step_down_elem.value = step_down;
cutting_speed_elem.value = new_preset_cut.cut_speed;
feed_tooth_elem.value = getFeedByTooth(new_tool_diameter, new_preset_cut);
step_down_factor_elem.value = getStepDownFactor(new_tool_diameter, new_preset_steep_down);
}
function getPresetValue(list, id) {
for (let elem of list) {
if (elem.id === id) {
return elem;
}
}
return null
}
function getFeedByTooth(tool_diam, preset) {
if (tool_diam >= 8)
return preset.feed_by_tooth_more_8;
if (tool_diam >= 6)
return preset.feed_by_tooth_more_6;
if (tool_diam >= 5)
return preset.feed_by_tooth_more_5;
if (tool_diam >= 4)
return preset.feed_by_tooth_more_4;
if (tool_diam >= 3)
return preset.feed_by_tooth_more_3;
if (tool_diam >= 2)
return preset.feed_by_tooth_more_2;
return preset.feed_by_tooth_more_1;
}
function getStepDownFactor(tool_diam, preset) {
if (tool_diam >= 6)
return preset.k_more_6;
if (tool_diam >= 5)
return preset.k_more_5;
if (tool_diam >= 4)
return preset.k_more_4;
if (tool_diam >= 3)
return preset.k_more_3;
if (tool_diam >= 2)
return preset.k_more_2;
return preset.k_less_2;
}
function save(){
let values = {
preset_cut: preset_cut_elem.value,
preset_step_down_factor: preset_step_down_factor_elem.value,
tool_diameter: tool_diameter_elem.value,
tooth_nbr: tooth_nbr_elem.value,
max_rpm: max_rpm_elem.value
}
localStorage.setItem('previous_data', JSON.stringify(values));
}
function load(){
let previous_data = localStorage.getItem('previous_data')
if(previous_data === null)
return;
previous_data = JSON.parse(previous_data);
preset_cut_elem.value = previous_data.preset_cut;
preset_step_down_factor_elem.value = previous_data.preset_step_down_factor;
tool_diameter_elem.value = previous_data.tool_diameter;
tooth_nbr_elem.value = previous_data.tooth_nbr;
max_rpm_elem.value = previous_data.max_rpm;
}

View File

@ -6,10 +6,9 @@ const sequelize = require('../sequelize')
router.get('/', function (req, res, next) { router.get('/', function (req, res, next) {
sequelize.models.preset_cut.findAll({ order: ['name'] }).then((preset_cut) => { sequelize.models.preset_cut.findAll({ order: ['name'] }).then((preset_cut) => {
sequelize.models.preset_step_down_factor.findAll({ order: ['name'] }).then((step_down_factor) => { sequelize.models.preset_step_down_factor.findAll({ order: ['name'] }).then((step_down_factor) => {
res.render('index', { preset_cut: preset_cut, step_down_factor: step_down_factor }); res.render('index', { preset_cut: preset_cut, step_down_factor: step_down_factor, user: req.user });
}) });
}) });
}); });
module.exports = router; module.exports = router;

View File

@ -1,14 +1,21 @@
var express = require('express'); const express = require('express');
var router = express.Router(); const router = express.Router();
const passport = require('../config/passport') const passport = require('../config/passport')
/* GET home page. */ /* GET home page. */
router.get('/login', function (req, res, next) { router.get('/login', function (req, res, next) {
res.render('login'); res.render('login', {user: req.user});
}); });
router.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), router.post('/login', passport.authenticate('local', { failureRedirect: '/login' }),
function (req, res) { function (req, res) {
res.redirect('/'); if (req.session.returnTo !== undefined && req.session.returnTo !== null){
let url = req.session.returnTo;
delete req.session.returnTo;
res.redirect(url);
}
else
res.redirect('/');
}); });
router.get('/logout', router.get('/logout',

79
routes/preset-manager.js Normal file
View File

@ -0,0 +1,79 @@
const express = require('express');
const router = express.Router();
const sequelize = require('../sequelize');
const { Op } = require("sequelize");
const ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn;
const isAdmin = require('../middleware/is-admin');
const logger = require('../config/winston');
const preset_cut_tools = require('../tools/preset_cut_tools')
const { body, validationResult, matchedData } = require('express-validator');
/* GET home page. */
router.get('/preset-manager', ensureLoggedIn(), isAdmin, function (req, res, next) {
sequelize.models.preset_cut.findAll({ order: ['name'] }).then((preset_cut) => {
sequelize.models.preset_step_down_factor.findAll({ order: ['name'] }).then((step_down_factor) => {
res.render('preset-manager', {
preset_cut: preset_cut,
step_down_factor: step_down_factor,
user: req.user
});
});
});
});
router.get(
'/preset-manager/preset-cut/:id',
ensureLoggedIn(),
isAdmin,
function (req, res, next) {
sequelize.models.preset_cut.findOne({ where: { id: req.params.id } }).then((preset) => {
if (preset == null) {
res.status(404);
} else {
res.render('preset-cut-editor', { preset: preset, user: req.user });
}
}).catch((err) => {
res.status(500);
});
});
router.post(
'/preset-manager/preset-cut/:id',
ensureLoggedIn(),
isAdmin,
body('name').isString(),
body('feed_by_tooth_more_1').isFloat({ min: 0 }).toFloat(),
body('feed_by_tooth_more_2').isFloat({ min: 0 }).toFloat(),
body('feed_by_tooth_more_3').isFloat({ min: 0 }).toFloat(),
body('feed_by_tooth_more_4').isFloat({ min: 0 }).toFloat(),
body('feed_by_tooth_more_5').isFloat({ min: 0 }).toFloat(),
body('feed_by_tooth_more_6').isFloat({ min: 0 }).toFloat(),
body('feed_by_tooth_more_8').isFloat({ min: 0 }).toFloat(),
body('cutting_speed').isInt({ min: 0 }).toInt(),
function (req, res, next) {
const errors = validationResult(req);
const data = matchedData(req);
if (!errors.isEmpty()) {
// TODO Handle error
req.flash('error', 'Error');
res.redirect(`/preset-manager/preset-cut/${req.params.id}`);
} else {
preset_cut_tools.edit(req.params.id, data).then(() => {
res.redirect('/preset-manager');
}).catch((reason => {
if (reason === "NOT_FOUND") {
res.status(404);
} else {
logger.error(`Fail to save: Code ${reason}`);
res.status(500);
}
}));
}
});
module.exports = router;

View File

@ -104,14 +104,14 @@ async function reset() {
k_more_6: 0.35 k_more_6: 0.35
}, },
]) ])
await sequelize.models.user.create({username: "admin" , password: bcrypt.hashSync("love_cnc", 10)}) await sequelize.models.user.create({username: "admin", password: bcrypt.hashSync("love_cnc", 10), is_admin: true} )
await sequelize.models.need_init.create({name: 'init'}) await sequelize.models.need_init.create({name: 'init'})
logger.info('...Done') logger.info('...Done')
} }
async function check_database() { async function check_database() {
await sequelize.sync(); await sequelize.sync({alter: true});
let val = await sequelize.models.need_init.findAll({where: {name: 'init'}}); let val = await sequelize.models.need_init.findAll({where: {name: 'init'}});
if(val.length === 0){ if(val.length === 0){
logger.info('Need init !'); logger.info('Need init !');

View File

@ -16,6 +16,11 @@ module.exports = (sequelize) => {
password: { password: {
allowNull: false, allowNull: false,
type: DataTypes.STRING, type: DataTypes.STRING,
},
is_admin: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN
} }
}) })
} }

31
tools/preset_cut_tools.js Normal file
View File

@ -0,0 +1,31 @@
const sequelize = require('../sequelize');
let edit = function (id, form_data){
return new Promise(((resolve, reject) => {
// TODO Add debug logs
sequelize.models.preset_cut.findOne({ where: { id: id } }).then((preset) => {
if (preset == null) {
reject('NOT_FOUND');
} else {
preset.name = form_data.name;
preset.cut_speed = form_data.cutting_speed;
preset.feed_by_tooth_more_1 = form_data.feed_by_tooth_more_1;
preset.feed_by_tooth_more_2 = form_data.feed_by_tooth_more_2;
preset.feed_by_tooth_more_3 = form_data.feed_by_tooth_more_3;
preset.feed_by_tooth_more_4 = form_data.feed_by_tooth_more_4;
preset.feed_by_tooth_more_5 = form_data.feed_by_tooth_more_5;
preset.feed_by_tooth_more_6 = form_data.feed_by_tooth_more_6;
preset.feed_by_tooth_more_8 = form_data.feed_by_tooth_more_8;
preset.save().then(() => {
resolve();
}).catch((err) => {
reject('SAVE_FAIL')
});
}
}).catch((err) => {
reject('DB_ERROR');
});
}))
}
exports.edit = edit;

View File

@ -0,0 +1,28 @@
<nav class="navbar navbar-expand-md navbar-dark">
<div class="container">
<a href="/" class="navbar-brand">
CNC Speed Calculator
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<% if(user && user.is_admin){ %>
<li class="nav-item">
<a class="nav-link" href="/preset-manager"><%= __('Manage Presets') %></a>
</li>
<% } %>
</ul>
<div class="">
<% if(user){ %>
<span class="navbar-text"><%= user.username %></span>
<a class="ms-2 btn btn-sm btn-secondary" href="/logout">Logout</a>
<% } else { %>
<a class="ml-2 btn btn-sm btn-secondary" href="/login">Login</a>
<% } %>
</div>
</div>
</div>
</nav>

View File

@ -0,0 +1,4 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel='stylesheet' href='/css/style.css'/>
<link rel="stylesheet" href="/css/custom_bootstrap.css">
<link rel="stylesheet" href="/css/fa-all.min.css">

View File

@ -0,0 +1,38 @@
<table class="table table-bordered table-dark table-striped table-hover text-center align-middle mt-3 mb-0">
<thead class="">
<tr>
<th colspan="2"></th>
<th colspan="7"><%= __('Feed By Tooth (By tool diameter)') %></th>
<th rowspan="2"></th>
</tr>
<tr>
<th scope="col"><%= __('Name') %></th>
<th scope="col"><%= __('Cut Speed') %></th>
<th scope="col"><%= __('∅ ≥ 1mm') %></th>
<th scope="col"><%= __('∅ ≥ 2mm') %></th>
<th scope="col"><%= __('∅ ≥ 3mm') %></th>
<th scope="col"><%= __('∅ ≥ 4mm') %></th>
<th scope="col"><%= __('∅ ≥ 5mm') %></th>
<th scope="col"><%= __('∅ ≥ 6mm') %></th>
<th scope="col"><%= __('∅ ≥ 8mm') %></th>
</tr>
</thead>
<tbody>
<% preset_cut.forEach(function (preset){ %>
<tr>
<td><%= preset.name %></td>
<td><%= preset.cut_speed %></td>
<td><%= preset.feed_by_tooth_more_1 %></td>
<td><%= preset.feed_by_tooth_more_2 %></td>
<td><%= preset.feed_by_tooth_more_3 %></td>
<td><%= preset.feed_by_tooth_more_4 %></td>
<td><%= preset.feed_by_tooth_more_5 %></td>
<td><%= preset.feed_by_tooth_more_6 %></td>
<td><%= preset.feed_by_tooth_more_8 %></td>
<td><a href="/preset-manager/preset-cut/<%= preset.id %>" class="btn btn-sm btn-success"><i class="fas fa-edit"></i></a></td>
</tr>
<% }) %>
</tbody>
</table>

View File

@ -0,0 +1,33 @@
<table class="table table-bordered table-dark table-striped table-hover text-center align-middle mt-2 mb-0">
<thead class="">
<tr>
<th rowspan="2"><%= __('Name') %></th>
<th colspan="6"><%= __('K factor (By tool diameter)') %></th>
<th rowspan="2"></th>
</tr>
<tr>
<th scope="col"><%= __('∅ < 2mm') %></th>
<th scope="col"><%= __('∅ ≥ 2mm') %></th>
<th scope="col"><%= __('∅ ≥ 3mm') %></th>
<th scope="col"><%= __('∅ ≥ 4mm') %></th>
<th scope="col"><%= __('∅ ≥ 5mm') %></th>
<th scope="col"><%= __('∅ ≥ 6mm') %></th>
</tr>
</thead>
<tbody>
<% step_down_factor.forEach(function (preset){ %>
<tr>
<td><%= preset.name %></td>
<td><%= preset.k_less_2 %></td>
<td><%= preset.k_more_2 %></td>
<td><%= preset.k_more_3 %></td>
<td><%= preset.k_more_4 %></td>
<td><%= preset.k_more_5 %></td>
<td><%= preset.k_more_6 %></td>
<td><a href="/preset-manager/step-down/<%= preset.id %>" class="btn btn-sm btn-success"><i class="fas fa-edit"></i></a></td>
</tr>
<% }) %>
</tbody>
</table>

View File

@ -2,30 +2,22 @@
<html> <html>
<head> <head>
<title>CNC Speed Calculator</title> <title>CNC Speed Calculator</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <%- include('includes/base/styles') %>
<link rel='stylesheet' href='/css/style.css'/>
<link rel="stylesheet" href="/css/custom_bootstrap.css">
</head> </head>
<body> <body>
<nav class="navbar navbar-dark navbar-expand"> <%- include('includes/base/navbar')%>
<div class="container">
<a href="#" class="navbar-brand">
CNC Speed Calculator
</a>
</div>
</nav>
<div class="container mt-4 text-white"> <div class="container mt-4 text-white">
<div class="row"> <div class="row">
<div class="col-12 col-lg-8 offset-lg-2"> <div class="col-12 col-lg-8 offset-lg-2">
<%- include('settings'); -%> <%- include('includes/index/settings'); -%>
</div> </div>
</div> </div>
<div class="row mb-4 mt-lg-2"> <div class="row mb-4 mt-lg-2">
<div class="col-12 col-lg-6 mt-3"> <div class="col-12 col-lg-6 mt-3">
<%- include('calculated'); -%> <%- include('includes/index/calculated'); -%>
</div> </div>
<div class="col-12 col-lg-6 mt-3"> <div class="col-12 col-lg-6 mt-3">
<%- include('constants'); -%> <%- include('includes/index/constants'); -%>
</div> </div>
</div> </div>
</div> </div>
@ -36,124 +28,6 @@
<script> <script>
let preset_cut = '<%- JSON.stringify(preset_cut) %>'; let preset_cut = '<%- JSON.stringify(preset_cut) %>';
let preset_step_down_factor = '<%- JSON.stringify(step_down_factor) %>'; let preset_step_down_factor = '<%- JSON.stringify(step_down_factor) %>';
preset_cut = JSON.parse(preset_cut);
preset_step_down_factor = JSON.parse(preset_step_down_factor);
let preset_cut_elem = document.querySelector('#preset_cut');
let preset_step_down_factor_elem = document.querySelector('#preset_step_down_factor');
let tooth_nbr_elem = document.querySelector('#tooth_nbr');
let tool_diameter_elem = document.querySelector('#tool_diameter');
let max_rpm_elem = document.querySelector('#max_rpm')
let rpm_elem = document.querySelector('#rpm');
let feed_rate_elem = document.querySelector('#feed_rate');
let feed_rate_sec_elem = document.querySelector('#feed_rate_sec');
let step_down_elem = document.querySelector('#step_down');
let cutting_speed_elem = document.querySelector('#cutting_speed');
let feed_tooth_elem = document.querySelector('#feed_tooth');
let step_down_factor_elem = document.querySelector('#step_down_factor');
document.addEventListener("DOMContentLoaded", function (event) {
load();
onChange();
});
function onChange() {
if (preset_cut_elem.value === "" || preset_step_down_factor_elem.value === "" || tool_diameter_elem.value === "0" || tooth_nbr_elem.value === "0" || max_rpm_elem.value === "0") {
return;
}
save();
let new_preset_cut = getPresetValue(preset_cut, parseInt(preset_cut_elem.value, 10));
let new_preset_steep_down = getPresetValue(preset_step_down_factor, parseInt(preset_step_down_factor_elem.value, 10))
let new_tool_diameter = parseFloat(tool_diameter_elem.value);
let new_tooth_nbr = parseInt(tooth_nbr_elem.value);
let max_rpm = parseInt(max_rpm_elem.value);
let rpm = (1000 * new_preset_cut.cut_speed) / (3.14 * new_tool_diameter);
if (rpm > max_rpm)
rpm = max_rpm;
let feed_rate = rpm * getFeedByTooth(new_tool_diameter, new_preset_cut) * new_tooth_nbr;
let feed_rate_sec = feed_rate / 60;
let step_down = new_tool_diameter * getStepDownFactor(new_tool_diameter, new_preset_steep_down);
rpm = Math.round(rpm);
feed_rate = Math.round(feed_rate);
feed_rate_sec = feed_rate_sec.toFixed(2)
step_down = step_down.toFixed(2);
rpm_elem.value = rpm;
feed_rate_elem.value = feed_rate;
feed_rate_sec_elem.value = feed_rate_sec;
step_down_elem.value = step_down;
cutting_speed_elem.value = new_preset_cut.cut_speed;
feed_tooth_elem.value = getFeedByTooth(new_tool_diameter, new_preset_cut);
step_down_factor_elem.value = getStepDownFactor(new_tool_diameter, new_preset_steep_down);
}
function getPresetValue(list, id) {
for (let elem of list) {
if (elem.id === id) {
return elem;
}
}
return null
}
function getFeedByTooth(tool_diam, preset) {
if (tool_diam >= 8)
return preset.feed_by_tooth_more_8;
if (tool_diam >= 6)
return preset.feed_by_tooth_more_6;
if (tool_diam >= 5)
return preset.feed_by_tooth_more_5;
if (tool_diam >= 4)
return preset.feed_by_tooth_more_4;
if (tool_diam >= 3)
return preset.feed_by_tooth_more_3;
if (tool_diam >= 2)
return preset.feed_by_tooth_more_2;
return preset.feed_by_tooth_more_1;
}
function getStepDownFactor(tool_diam, preset) {
if (tool_diam >= 6)
return preset.k_more_6;
if (tool_diam >= 5)
return preset.k_more_5;
if (tool_diam >= 4)
return preset.k_more_4;
if (tool_diam >= 3)
return preset.k_more_3;
if (tool_diam >= 2)
return preset.k_more_2;
return preset.k_less_2;
}
function save(){
let values = {
preset_cut: preset_cut_elem.value,
preset_step_down_factor: preset_step_down_factor_elem.value,
tool_diameter: tool_diameter_elem.value,
tooth_nbr: tooth_nbr_elem.value,
max_rpm: max_rpm_elem.value
}
localStorage.setItem('previous_data', JSON.stringify(values));
}
function load(){
let previous_data = localStorage.getItem('previous_data')
if(previous_data === null)
return;
previous_data = JSON.parse(previous_data);
preset_cut_elem.value = previous_data.preset_cut;
preset_step_down_factor_elem.value = previous_data.preset_step_down_factor;
tool_diameter_elem.value = previous_data.tool_diameter;
tooth_nbr_elem.value = previous_data.tooth_nbr;
max_rpm_elem.value = previous_data.max_rpm;
}
</script> </script>
<script src="/js/index.js"></script>
</html> </html>

View File

@ -2,36 +2,43 @@
<html> <html>
<head> <head>
<title>CNC Speed Calculator</title> <title>CNC Speed Calculator</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <%- include('includes/base/styles') %>
<link rel='stylesheet' href='/css/style.css'/>
<link rel="stylesheet" href="/css/custom_bootstrap.css">
</head> </head>
<body> <body>
<nav class="navbar navbar-dark navbar-expand"> <%- include('includes/base/navbar') %>
<div class="container">
<a href="#" class="navbar-brand">
CNC Speed Calculator
</a>
</div>
</nav>
<div class="container mt-4 text-white"> <div class="container mt-4 text-white">
<form action="/login" method="post"> <div class="row justify-content-center">
<div> <div class="col-12 col-md-6 col-lg-4">
<label>Username:</label> <div class="card bg-dark border-secondary">
<input type="text" name="username"/> <div class="card-header text-center h4 border-secondary">
<%= __('Login') %>
</div>
<div class="card-body">
<form action="/login" method="post" autocomplete="on">
<div class="mb-3 text-center">
<label class="form-label" for="username"><%= __('Username') %></label>
<input class="form-control text-center border-accent" type="text" name="username" id="username"/>
</div>
<div class="mb-3 text-center">
<label class="form-label" for="password"><%= __('Password') %></label>
<input class="form-control text-center border-accent" type="password" name="password" id="password"/>
</div>
<div class="d-flex justify-content-center">
<button type="submit" class="btn btn-success"><%= __('Log In') %></button>
</div>
</form>
</div>
</div>
</div> </div>
<div> </div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>
</div> </div>
</body> </body>
<script src="/js/bootstrap.min.js"></script> <script src="/js/bootstrap.min.js"></script>
</html> </html>

114
views/preset-cut-editor.ejs Normal file
View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html>
<head>
<title>CNC Speed Calculator</title>
<%- include('includes/base/styles') %>
</head>
<body>
<%- include('includes/base/navbar') %>
<div class="container mt-4 text-white">
<div class="row justify-content-center">
<div class="col-12 col-md-12">
<div class="card bg-dark border-secondary mb-4">
<div class="card-header text-center h4 border-secondary">
<%= __('Material Preset Editor') %>
</div>
<div class="card-body">
<form method="post">
<div class="row justify-content-center">
<div class="col-12 col-md-4">
<label for="name" class="form-label"><%= __('Name') %></label>
<input type="text" class="form-control border-accent" id="name" name="name" value="<%= preset.name %>">
</div>
<div class="col-12 col-md-4 mt-3 mt-md-0">
<label for="cutting_speed" class="form-label"><%= __('Cutting Speed') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="cutting_speed" min=0
step="1" value="<%= preset.cut_speed %>" name="cutting_speed">
<span class="input-group-text border-accent">m/min</span>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12 text-center border-bottom border-secondary">
<h5><b><%= __('Feed By Tooth (By tool diameter)') %></b></h5>
</div>
</div>
<div class="row row-cols-md-4 row-cols-lg-5 row-cols-xl-6 row-cols-1 justify-content-center">
<div class="col mt-3 text-center">
<label for="feed_by_tooth_more_1" class="form-label fw-bold"><%= __('∅ ≥ 1mm') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="feed_by_tooth_more_1" min=0
step="0.001" value="<%= preset.feed_by_tooth_more_1 %>" name="feed_by_tooth_more_1">
<span class="input-group-text border-accent">mm</span>
</div>
</div>
<div class="col mt-3 text-center">
<label for="feed_by_tooth_more_2" class="form-label fw-bold"><%= __('∅ ≥ 2mm') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="feed_by_tooth_more_2" min=0
step="0.001" value="<%= preset.feed_by_tooth_more_2 %>" name="feed_by_tooth_more_2">
<span class="input-group-text border-accent">mm</span>
</div>
</div>
<div class="col mt-3 text-center">
<label for="feed_by_tooth_more_3" class="form-label fw-bold"><%= __('∅ ≥ 3mm') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="feed_by_tooth_more_3" min=0
step="0.001" value="<%= preset.feed_by_tooth_more_3 %>" name="feed_by_tooth_more_3">
<span class="input-group-text border-accent">mm</span>
</div>
</div>
<div class="col mt-3 text-center">
<label for="feed_by_tooth_more_4" class="form-label fw-bold"><%= __('∅ ≥ 4mm') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="feed_by_tooth_more_4" min=0
step="0.001" value="<%= preset.feed_by_tooth_more_4 %>" name="feed_by_tooth_more_4">
<span class="input-group-text border-accent">mm</span>
</div>
</div>
<div class="col mt-3 text-center">
<label for="feed_by_tooth_more_5" class="form-label fw-bold"><%= __('∅ ≥ 5mm') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="feed_by_tooth_more_5" min=0
step="0.001" value="<%= preset.feed_by_tooth_more_5 %>" name="feed_by_tooth_more_5">
<span class="input-group-text border-accent">mm</span>
</div>
</div>
<div class="col mt-3 text-center">
<label for="feed_by_tooth_more_6" class="form-label fw-bold"><%= __('∅ ≥ 6mm') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="feed_by_tooth_more_6" min=0
step="0.001" value="<%= preset.feed_by_tooth_more_6 %>" name="feed_by_tooth_more_6">
<span class="input-group-text border-accent">mm</span>
</div>
</div>
<div class="col mt-3 text-center">
<label for="feed_by_tooth_more_8" class="form-label fw-bold"><%= __('∅ ≥ 8mm') %></label>
<div class="input-group">
<input type="number" class="form-control border-accent border-end-0" id="feed_by_tooth_more_8" min=0
step="0.001" value="<%= preset.feed_by_tooth_more_8 %>" name="feed_by_tooth_more_8">
<span class="input-group-text border-accent">mm</span>
</div>
</div>
</div>
<div class="row justify-content-center mt-4">
<div class="col-12 d-flex justify-content-center">
<button class="btn btn-success"><i class="fas fa-save"></i> <%= __('Save') %></button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
<script src="/js/bootstrap.min.js"></script>
</html>

63
views/preset-manager.ejs Normal file
View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<title>CNC Speed Calculator</title>
<%- include('includes/base/styles') %>
</head>
<body>
<%- include('includes/base/navbar') %>
<div class="container mt-4 text-white">
<div class="row justify-content-center">
<div class="col-12">
<div class="card bg-dark border-secondary">
<div class="card-header h4 text-center"><%= __('Preset Manager') %></div>
<div class="card-body px-0 pb-0">
<ul class="nav nav-tabs nav-fill" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="material-tab-btn" data-bs-toggle="tab"
href="#material_content"
role="tab"><%= __("Material") %></a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="step-down-tab-btn" data-bs-toggle="tab" href="#step_down_content"
role="tab"><%= __('Step down factor') %></a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active table-responsive" id="material_content" role="tabpanel">
<%- include('includes/preset-manager/material-table') %>
</div>
<div class="tab-pane fade table-responsive" id="step_down_content" role="tabpanel">
<%- include('includes/preset-manager/step-down-factor-table') %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
<script src="/js/bootstrap.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function (event) {
document.getElementById("material-tab-btn").addEventListener('click', onClick);
document.getElementById("step-down-tab-btn").addEventListener('click', onClick);
if (document.location.hash === "#step_down")
(new bootstrap.Tab(document.getElementById('step-down-tab-btn'))).show();
});
function onClick(ev) {
if (ev.target.id === "material-tab-btn") {
window.location.hash = "";
} else {
window.location.hash = "step_down";
}
}
</script>
</html>