Graph update and little noe ui tweaks

This commit is contained in:
Max 2023-07-23 23:13:28 +00:00
parent d3acd62688
commit c61f0c0198
12 changed files with 1341 additions and 356 deletions

View File

@ -17,13 +17,14 @@
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden; /* overflow-x: hidden;*/
min-width: 320px; min-width: 320px;
background: #FFFFFF; background: green;
font-family: 'Roboto', system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-family: 'Roboto', system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px; font-size: 14px;
line-height: 1.4285em; line-height: 1.4285em;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
position: relative;
} }
:root { :root {
@ -95,7 +96,7 @@ body {
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif; font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
} }
#app { #app {
background: var(--body_bg_color); /* background: var(--body_bg_color);*/
} }
.ui.segment { .ui.segment {

View File

@ -87,6 +87,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden;
} }
.place-holder { .place-holder {
width: 100%; width: 100%;

View File

@ -0,0 +1,431 @@
<style type="text/css" scoped>
.an-graph {
background: #fefefe;
}
.inactive.segment {
}
.active.segment {
outline: 4px solid cyan;
outline-offset: -5px;
outline-style: dashed;
max-height: 2000px;
}
.not-padded {
margin-left: -5px;
margin-right: -5px;
margin-bottom: -10px;
padding-right: 5px;
padding-left: 5px;
}
.sticky-boy {
position: fixed;
top: -1px;
right: 10px;
z-index: 100;
width: 70%;
background: orange;
}
.animate-height {
transition: max-height 0.8s linear;
max-height: 450px;
overflow: hidden;
}
</style>
<template>
<div>
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
<div class="sixteen wide column" v-if="!editGraphs">
<div class="ui basic padded segment">
<!-- Just a space to keep things clickable -->
</div>
</div>
<div class="sixteen wide column">
<dix class="ui basic segment" v-if="!editGraphs">
<div class="ui button" v-on:click="toggleEditGraphs">
<i class="edit icon"></i>
<span>Add/Edit Graphs</span>
</div>
</dix>
<div v-if="editGraphs">
<div class="ui green button" v-on:click="addGraph()">
<i class="plus icon"></i>
New Graph
</div>
<div class="ui basic button" v-on:click="toggleEditGraphs">
<i class="check circle icon"></i>
Done Editing Graphs
</div>
</div>
</div>
</div>
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment animate-height`">
<!-- Edit options -->
<div class="ui small header" v-if="editGraphs">
<div class="ui grid">
<div class="eight wide column">
<b>Graph #{{ index+1 }}</b>
</div>
<div class="eight wide right aligned column">
<span class="ui tiny compact inverted red button" v-on:click="removeGraph(index)">
Remove Graph
<i class="close icon"></i>
</span>
</div>
</div>
</div>
<h3 class="ui center aligned dividing header">
{{ getGraphTitle(graph) }}
</h3>
<div v-if="graph?.type == PILL_CALENDAR">
<PillCalendarGraph
:graph="graph"
:tempChartDays="tempChartDays"
:userFields="userFields"
:cycleData="cycleData"
:edit-graphs="editGraphs"
:showZeroValues="graph?.options?.showZeroValues"
:showTextValues="graph?.options?.showTextValues"
:connectDays="graph?.options?.connectDays"
:hideValues="graph?.options?.hideValues"
:hideIcons="graph?.options?.hideIcons"
/>
<div v-if="editGraphs" class="ui segment">
<p>Calendar Graph Toggles</p>
<div v-on:click="toggelValue(index, 'hideIcons')"class="ui button">
<span v-if="graph?.options?.hideIcons">Show</span><span v-else>Hide</span> Icons
</div>
<div v-on:click="toggelValue(index, 'hideValues')"class="ui button">
<span v-if="graph?.options?.hideValues">Show</span><span v-else>Hide</span> Values
</div>
<div v-on:click="toggelValue(index, 'showZeroValues')"class="ui button">
<span v-if="!graph?.options?.showZeroValues">Show</span><span v-else>Hide</span> Lowest Value
</div>
<div v-on:click="toggelValue(index, 'showTextValues')"class="ui button">
<span v-if="!graph?.options?.showTextValues">Show</span><span v-else>Hide</span> Text Value
</div>
<div v-on:click="toggelValue(index, 'connectDays')"class="ui button">
<span v-if="!graph?.options?.connectDays">Connect</span><span v-else>Disconnect</span> Days
</div>
</div>
</div>
<div v-if="graph?.type == LAST_DONE">
Last done not implemented
</div>
<div v-if="!graph.fieldIds || graph.fieldIds && graph.fieldIds.length == 0">
<h5>Blank Graph</h5>
<span v-if="!editGraphs">Click "Edit Graphs" then,</span>
Select Graph type and Metrics to display
</div>
<div v-if="graph?.type == undefined && graph.fieldIds && graph.fieldIds.length > 0">
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
</div>
<div class="ui segment" v-if="editGraphs">
<!-- change graph type -->
<div v-for="(graphType, graphId) in graphTypesDef" class="ui buttons">
<div class="ui tiny button" v-on:click="changeGraphType(index, graphId)" :class="{'green':(String(graphId) == String(graph?.type))}">
{{ graphType }}
</div>
</div>
<div v-for="fieldId in fields">
<span v-if="graph.fieldIds && graph.fieldIds.includes(fieldId)" v-on:click="toggleGraphField(fieldId, index)">
<i class="green check square icon"></i>
</span>
<span v-else v-on:click="toggleGraphField(fieldId, index)">
<i class="square outline icon"></i>
</span>
<i :class="`${$parent.getFieldColor(fieldId)} ${$parent.getFieldIcon(fieldId)} icon`"></i>
<b>{{ userFields[fieldId]?.label }}</b>
</div>
</div>
</div>
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
<div class="sixteen wide column" v-if="!editGraphs">
<div class="ui basic padded segment">
<!-- Just a space to keep things clickable -->
</div>
</div>
<div class="sixteen wide column">
<dix class="ui basic segment" v-if="!editGraphs">
<div class="ui button" v-on:click="toggleEditGraphs">
<i class="edit icon"></i>
<span>Add/Edit Graphs</span>
</div>
</dix>
<div v-if="editGraphs">
<div class="ui green button" v-on:click="addGraph()">
<i class="plus icon"></i>
New Graph
</div>
<div class="ui basic button" v-on:click="toggleEditGraphs">
<i class="check circle icon"></i>
Done Editing Graphs
</div>
</div>
</div>
</div>
<!-- Anchor for scrolling to the bottom of graphs -->
<div ref="anchor"></div>
</div>
</template>
<script>
const PILL_CALENDAR = 'pillCalendar'
const LAST_DONE = 'lastDone'
export default {
name: 'MetricTrackingGraphs',
props: [
'tempChartDays', // Number of days to display
'fields', // field IDs for display/order
'userFields', // field values defined by user
'graphs', // Graph data defined by user
'cycleData', // ALL user data
'calendar', // Date data for currently open day
'editGraphs' // boolean for edit or not edit graphs
],
components: {
'PillCalendarGraph':require('@/components/Metrictracking/PillCalendarGraph.vue').default,
},
data: function(){
return {
graphTypesDef:{
// [LAST_DONE]: 'Last Done',
'undefined':'Line Graph (Default)',
[PILL_CALENDAR]:'Calendar Graph',
},
localGraphData:[],
}
},
beforeCreate() {
// Constants
this.PILL_CALENDAR = PILL_CALENDAR
this.LAST_DONE = LAST_DONE
// Include JS libraries
let graphsScript = document.createElement('script')
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
document.head.appendChild(graphsScript)
},
mounted(){
this.localGraphData = this.graphs
this.graphCurrentData()
},
updated(){
// update graphs here? Or watch graphs prop
},
watch: {
// whenever question changes, this function will run
userFields(newFields, oldFields) {
// console.log([newFields, oldFields])
if( JSON.stringify(oldFields) == "{}" ){
this.graphCurrentData()
}
},
tempChartDays(newDays, oldDays){
if( newDays != oldDays ){
this.graphCurrentData()
}
},
},
methods: {
saveGraphs(){
this.$emit('saveGraphs', this.localGraphData)
},
toggleEditGraphs(){
setTimeout(() => {
// scroll last graph into view
this.$refs.anchor.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
}, 800)
this.$emit('toggleEditGraphs')
},
changeGraphType(index, newType){
console.log(index + ' change to ' + newType)
this.localGraphData[index]['type'] = newType
this.saveGraphs()
},
addGraph(){
this.localGraphData.push({})
this.saveGraphs()
},
removeGraph(index){
this.localGraphData.splice(index, 1)
this.saveGraphs()
},
toggelValue(graphIndex, optionName){
if(!this.localGraphData[graphIndex].options){
this.localGraphData[graphIndex].options = {}
}
if(this.localGraphData[graphIndex].options[optionName]){
this.localGraphData[graphIndex].options[optionName] = false
}
else {
this.localGraphData[graphIndex].options[optionName] = true
}
console.log(this.localGraphData[graphIndex].options[optionName])
this.saveGraphs()
},
toggleGraphField(fieldId, graphIndex){
if(!Array.isArray(this.localGraphData[graphIndex].fieldIds)){
this.localGraphData[graphIndex].fieldIds = []
}
const inSetCheck = this.localGraphData[graphIndex]?.fieldIds.indexOf(fieldId)
if(inSetCheck == -1){
this.localGraphData[graphIndex]?.fieldIds.push(fieldId)
}
if(inSetCheck > -1){
this.localGraphData[graphIndex]?.fieldIds.splice(inSetCheck,1)
}
this.saveGraphs()
},
getGraphTitle(graph){
const graphFields = graph?.fieldIds || []
let fieldTitles = []
graphFields.forEach(fieldId => {
fieldTitles.push(this.userFields[fieldId]?.label)
})
// console.log(fieldTitles)
const title = fieldTitles.join(', ')
return title
},
graphCurrentData(){
// try again if dygraphs isn't loaded
if( typeof(window.Dygraph) != 'function' ){
setTimeout(() => {
this.graphCurrentData()
}, 100)
return
}
const graphOptions = {
interactionModel: {},
// pointClickCallback: function(e, pt){
// console.log(e)
// console.log(pt)
// console.log(this.getValue(pt.idx, 0))
// }
}
// Excel date format YYYYMMDD
const convertToExcelDate = (dateCode) => {
return dateCode
.split('.')
.reverse()
.map(item => String(item).padStart(2,0))
.join('')
}
// Generate set of keys for graph length
let dataKeys = Object.keys(this.cycleData)
dataKeys = dataKeys.splice(0, this.tempChartDays)
console.log(dataKeys)
// build CSV data for each graph
this.graphs.forEach((graph,index) => {
// only chart line graphs with dygraphs
if( graph.type != undefined ){
return
}
if( !graph.fieldIds ){
return
}
// CSV or path to a CSV file.
let dataString = ""
// Lookup graph field titles
let graphLabels = ['Date']
graph.fieldIds.forEach(fieldId => {
const graphLabel = this.userFields[fieldId]?.label
const escapedLabel = graphLabel.replaceAll(',','')
graphLabels.push(escapedLabel)
})
dataString += graphLabels.join(',') + '\n'
// build each row, for each day
for (var i = 0; i < dataKeys.length; i++) {
let nextFragment = []
// push date code to first column
nextFragment.push(convertToExcelDate(dataKeys[i]))
graph.fieldIds.forEach(fieldId => {
const currentEntry = this.cycleData[dataKeys[i]]
let currentValue = currentEntry[fieldId]
// setup correct float graphing
if(fieldId == 'BT'){
// parse temp to fixed length float 00.00
currentValue = parseFloat(currentValue).toFixed(2)
}
if( currentValue == undefined ){
currentValue = -1
}
nextFragment.push(currentValue)
})
dataString += nextFragment.join(',') + "\n"
}
let graphDiv = document.getElementById("graphdiv"+index)
const g = new Dygraph(graphDiv, dataString ,graphOptions)
})
return
},
}
}
</script>

View File

@ -1,26 +1,547 @@
<style type="text/css" scoped></style> <style type="text/css" scoped>
div.calendar {
width: calc(100% - 4px);
min-height: 350px;
display: flex;
margin: 5px 8px 15px;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-start;
}
.day {
flex: 0 0 calc(14.28% - 2px);
min-height: 50px;
border: 1px solid var(--border_color);
font-size: 1.2em;
overflow: hidden;
box-sizing: border-box;
position: relative;
line-height: 1em;
display: flex;
align-items: flex-end;
}
.today {
font-weight: bold;
text-decoration: underline;
}
.active-entry {
outline: #07f4f4;
outline-style: none;
outline-width: medium;
outline-style: none;
outline-offset: -1px;
outline-style: solid;
outline-width: 3px;
}
.day ~ .has-data {
}
.day ~ .no-data {
background: #c7c7c787;
opacity: 0.6;
}
.day > .number {
position: absolute;
top: 0;
right: 5px;
z-index: 10;
opacity: 0.4;
}
.day > .sex {
font-size: 0.7em;
border-radius: 5px;
background: rgba(249, 0, 0, 0.15);
color: white;
padding: 0 0 0 4px;
z-index: 10;
position: absolute;
left: 0;
height: 26px;
}
.day > .period {
position: absolute;
bottom: 1px;
left: 1px;
right: 1px;
height: 5px;
background: red;
z-index: 10;
}
.day > .mucus {
position: absolute;
bottom: 0;
left: 0;
right: 0;
min-height: 10px;
background: #abecff7d;
z-index: 2;
}
.day > .notes {
}
.pill-container {
width: 100%;
}
.pill {
width: calc(100% - 8px);
min-height: 2px;
margin: 0 4px;
box-sizing: border-box;
display: inline-block;
background: rgb(50 218 255 / 44%);
border-radius: 40px;
text-align: center;
line-height: 1em;
position: relative;
color: white;
font-size: 0.7em;
padding: 2px;
overflow: hidden;
white-space: nowrap;
}
.pill.did-last {
margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
width: calc(100% - 5px);
}
.pill.did-next {
margin-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
width: calc(100% - 5px);
}
.pill.did-next.did-last {
width: 100%;
}
/* .last-high:after {
content: '';
width: 0;
height: 0;
border-top: 15px solid transparent;
border-bottom: 3px solid transparent;
border-left: 10px solid rgb(50 218 255 / 44%);
position: absolute;
left: 0;
top: -13px;
}
.next-high:before {
content: '';
width: 0;
height: 0;
border-top: 15px solid transparent;
border-bottom: 3px solid transparent;
border-right: 10px solid rgb(50 218 255 / 44%);
position: absolute;
right: 0;
top: -13px;
}*/
.big-day {
display: inline-block;
width: 100%;
min-height: 2px;
margin: 0 auto;
text-align: center;
}
.zero-day {
opacity: 0.5;
}
.icon-spacer {
display: inline-block;
background-color: greenyellow;
width: 20px;
height: 2px;
}
.past-entries {
width: 100%;
display: flex;
justify-content: space-around;
/* padding: 0 10px;*/
overflow-x: scroll;
overflow-y: hidden;
}
.past-entry {
position: relative;
text-align: center;
border: 1px solid;
border-color: var(--dark_border_color);
color: var(--text_color);
flex-grow: 1;
cursor: pointer;
font-weight: bold;
min-width: 40px;
min-height: 40px;
margin: 5px 0 10px;
line-height: 2.3em;
}
.day-list {
width: 100%;
height: 80px;
background-color: green;
display: flex;
justify-content: space-around;
overflow-x: scroll;
overflow-y: hidden;
}
.day-list-item {
flex-grow: 1;
border: 1px solid black;
width: 25px;
}
.pill.red { background-color: #db2828 }
.pill.orange { background-color: #f2711c }
.pill.yellow { background-color: #fbbd08 }
.pill.olive { background-color: #b5cc18 }
.pill.green { background-color: #21ba45 }
.pill.teal { background-color: #00b5ad }
.pill.blue { background-color: #2185d0 }
.pill.violet { background-color: #6435c9 }
.pill.purple { background-color: #a333c8 }
.pill.pink { background-color: #e03997 }
.pill.brown { background-color: #a5673f }
.pill.grey { background-color: #767676 }
.pill.black { background-color: #1b1c1d }
</style> </style>
<template> <template>
<div> <div>
I'm a calednar yo <div class="calendar">
<div v-for="day in calendar.weekdays" class="day">
{{ day }}
</div>
<div v-for="day in calendar.days" class="day"
:class="{
'today':day == calendar.today,
'active-entry':calendar.dateCode == `${day}.${calendar.month}.${calendar.year}`,
'has-data':cycleData[`${day}.${calendar.month}.${calendar.year}`],
'no-data':showDayDataColor(day),
}">
<!-- v-on:click="openDayData(`${day}.${calendar.month}.${calendar.year}`)" -->
<span class="number">{{ day }}</span>
<!-- {{ `${day}.${calendar.month}.${calendar.year}` }} -->
<span class="pill-container" v-for="(entry, dateCode) in getChartData" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
<span
v-for="(dayData, fieldId) in entry"
v-if="showZeroValuesCheck(dayData.value, fieldId)"
class="pill"
:class="[$parent.$parent.getFieldColor(fieldId), {
'did-next':dayData.didNext,
'did-last':dayData.didLast,
'last-high':dayData.lastHigh,
'next-high':dayData.nextHigh,
}]">
<!-- 'zero-day':dayData.value == lowestGraphValue, -->
<!-- <i v-if="dayData.value != 0" :class="`tiny ${$parent.$parent.getFieldColor(fieldId)} ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i> -->
<!-- <span v-else class="icon-spacer"></span>
:style="{height:(Math.round(dayData.value*5)+'px')}"
-->
<span v-if="dayData.value > lowestGraphValue-1" class="big-day">
<i v-if="!hideIcons" :class="`tiny white ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i>
<span v-if="!hideValues">
{{ getDayValue(fieldId, dayData.value) }}
</span>
</span>
</span>
</span>
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
// let chartData = {}
export default { export default {
props: [ props: [
'graphOptions', // options associated with this graph 'graph', // options associated with this graph
'userFields', // all field attributes
'tempChartDays', // number of days to display 'tempChartDays', // number of days to display
'cycleData', // all users metric data 'cycleData', // all users metric data
'editGraphs', // display additional edit options
// Graph options
'showZeroValues', // Hide graph data with value of zero
'showTextValues', // Show button text or button value
'connectDays', // Calculates next and previous day connections.
'hideValues', // Hide all values on the graph
'hideIcons', // option to hide icons
], ],
data: function(){ data: function(){
return { return {
openModel:true, openModel:true,
calendar: {
dateObject: null,
dateCode: null,
monthName: '',
dayName:'',
daysAgo:0,
month: '',
year: '',
days: [],
weekdays: ['S','M','T','W','T','F','S'],
today: 0,
},
chartDateCodes: [], // array of date codes in chart
listDateCodes: [],
dayList: true,
lowestGraphValue: 0,
} }
}, },
mounted(){
this.setupCalendar(new Date())
},
computed: {
getChartData(){
let chartData = {}
let chartValues = []
// iterate every day in month by day code
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
// lookup data for that day
const cycleDayData = this.cycleData[chartDayCode]
// if chart data is set for this day
if( cycleDayData && Object.keys(cycleDayData).length > 0){
chartData[chartDayCode] = {}
// go over each field to be displayed on graph
this.graph.fieldIds.forEach((graphFieldId) => {
if( cycleDayData[graphFieldId] == undefined ){
return
}
// track all chart values
chartValues.push(cycleDayData[graphFieldId])
chartData[chartDayCode][graphFieldId] = {
didLast: false,
lastHigh: false,
didNext: false,
nextHigh: false,
value: cycleDayData[graphFieldId]
}
})
}
})
this.lowestGraphValue = Math.min(...chartValues)
// determine next and previous states for display
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
if(chartData[chartDayCode] && this.connectDays){
const previousDateCode = this.chartDateCodes[codeIndex-1]
const nextDateCode = this.chartDateCodes[codeIndex+1]
Object.keys(chartData[chartDayCode]).forEach((graphFieldId) => {
const currentValue = chartData[chartDayCode][graphFieldId].value
// check for previous entry
if( chartData[previousDateCode] && chartData[previousDateCode][graphFieldId] ){
chartData[chartDayCode][graphFieldId].didLast = true
// set low value flag
const lastHigh = chartData[previousDateCode][graphFieldId].value > 0
chartData[chartDayCode][graphFieldId].lastHigh = lastHigh && currentValue == 0
}
// check for next entry
if( chartData[nextDateCode] && chartData[nextDateCode][graphFieldId] ){
chartData[chartDayCode][graphFieldId].didNext = true
// set low value flag
const nextHigh = chartData[nextDateCode][graphFieldId].value > 0
chartData[chartDayCode][graphFieldId].nextHigh = nextHigh && currentValue == 0
}
})
}
})
// console.log(chartData)
return chartData
},
},
methods: { methods: {
closeModel(){ showZeroValuesCheck(dayValue, fieldId){
// if graph type is boolean or there are two options
let isBooleanField = this.userFields[fieldId].type == 'boolean'
if(this.userFields[fieldId].customOptions){
let options = this.userFields[fieldId].customOptions
isBooleanField = options.split(',').length == 2
}
if(isBooleanField && !this.showZeroValues){
const parsedValue = this.getDayValue(fieldId, dayValue)
if(parsedValue == 'Yes'){
return true
} else {
return false
}
}
return this.showZeroValues || dayValue > this.lowestGraphValue
},
getDayValue(fieldId, value){
if( !this.showTextValues ){
return value
}
let options = 'error, Yes, No'
if(this.userFields[fieldId].customOptions){
options = this.userFields[fieldId].customOptions
}
const values = options.split(',')
const selection = String(values[value]).trim()
return selection
},
displayDayFromCode(dateCode){
const parts = dateCode.split('.')
return `${parts[0]}`
},
showDayDataColor(day){
// Determine if day has any data set
if(day == ''){
return false
}
return !(this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`])
},
generateDateCode(date){
const dateSetup = [
date.getDate(), // 1-31 (Day)
date.getMonth()+1, // 0-11 (Month)
date.getFullYear(), // 1888-2022 (Year)
]
return dateSetup.join('.')
},
setupCalendar(date){
// visualize each day change
this.working = true
setTimeout(() => {
this.working = false
}, 500)
if(!date && this.dateObject){
date = this.dateObject
}
if(!date){
date = new Date()
}
this.calendar.dateObject = date
this.calendar.dateCode = this.generateDateCode(date)
// calculate days ago since current date
const now = new Date()
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
const dayInterval = diffSeconds / 86400 // seconds in a day
this.calendar.daysAgo = Math.floor(dayInterval)
// ------------
// setup calendar display
var y = date.getFullYear()
var m = date.getMonth()
var firstDay = new Date(y, m, 1);
var lastDay = new Date(y, m + 1, 0);
function getDaysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
const currentYear = date.getFullYear();
const currentMonth = date.getMonth() + 1;
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
this.calendar.year = currentYear
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
const monthStartDay = firstDay.getDay()
let days = Array(monthStartDay).fill(""); // Pad days to start on correct weekday
for (let i = 0; i < daysInCurrentMonth; i++) {
days.push(i+1)
}
this.calendar.days = days
// set today
this.calendar.today = date.getDate()
this.calendar.month = date.getMonth()+1
// setup date codes for key matching on calendar
this.calendar.days.forEach((day) => {
if( day !== "" ){
let dateDay = new Date(y, m, day);
let dayCode = this.generateDateCode(dateDay)
this.chartDateCodes.push(dayCode)
}
})
// generate past date codes for list
for (let i = 0; i < this.tempChartDays; i++) {
const now = new Date()
const pastDate = now.setDate(now.getDate() - i)
const pastDateObj = new Date(pastDate)
const newCode = this.generateDateCode(pastDateObj)
this.listDateCodes.push(newCode)
}
// return codes.reverse()
/*
October 2022
S M T W T F S
1 2 3 4 5 6
7 8 9
*/
// -------
}, },
} }
} }

View File

@ -30,7 +30,7 @@
@media only screen and (max-width: 740px) { @media only screen and (max-width: 740px) {
.modal-content { .modal-content {
width: 100%; width: 100%;
padding-bottom: 55px; /* padding-bottom: 55px;*/
} }
} }

View File

@ -1369,6 +1369,7 @@
} }
.edit-button { .edit-button {
padding: 6px 0px 0; padding: 6px 0px 0;
flex-grow: 1;
} }
.edit-button > span:not(.ui) { .edit-button > span:not(.ui) {
display: none; display: none;

View File

@ -285,23 +285,28 @@
}, },
justClosed(){ justClosed(){
// Dont do anything when not is closed.
// Its already saved, this will make interface feel snappy
// Scroll note into view // Scroll note into view
this.$el.scrollIntoView({ // this.$el.scrollIntoView({
behavior: 'smooth', // behavior: 'smooth',
block: 'center', // block: 'center',
inline: 'center' // inline: 'center'
}) // })
//After scroll, trigger green outline animation // this.$bus.$emit('notification','Note Saved')
setTimeout(() => {
this.triggerClosedAnimation = true // //After scroll, trigger green outline animation
setTimeout(()=>{ // setTimeout(() => {
//After 3 seconds, hide it
this.triggerClosedAnimation = false
}, 1500)
}, 500) // this.triggerClosedAnimation = true
// setTimeout(()=>{
// //After 3 seconds, hide it
// this.triggerClosedAnimation = false
// }, 1500)
// }, 500)
}, },
}, },

View File

@ -172,15 +172,16 @@ const SquireButtonFunctions = {
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
this.$router.go(-1)
Array.from( container.getElementsByClassName('active') ).forEach(item => { setTimeout(()=>{
item.classList.remove('active');
}) Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
},600)
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
}, },
deleteCompletedListItems(){ deleteCompletedListItems(){
// //
@ -190,53 +191,57 @@ const SquireButtonFunctions = {
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists //Close menu if user is on mobile, then sort list
container.childNodes.forEach( (node) => { this.$router.go(-1)
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items setTimeout(()=>{
let undoneElements = document.createDocumentFragment()
//Go through each item in each list we found //Go through each item, on first level, look for Unordered Lists
node.childNodes.forEach( (checkListItem, index) => { container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together //Create two categories, done and not done list items
if(checkListItem.nodeName == 'UL'){ let undoneElements = document.createDocumentFragment()
return
}
//Check if list item has active class //Go through each item in each list we found
const checkedItem = checkListItem.classList.contains('active') node.childNodes.forEach( (checkListItem, index) => {
//Check if the next item is a list, Keep lists with intented items together //Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
let sublist = null if(checkListItem.nodeName == 'UL'){
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){ return
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(!checkedItem){
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
} }
} //Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
}) //Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Remove all HTML from node, push unfinished items, then finished below them //Push checked items and their sub lists to the done set
node.innerHTML = null if(!checkedItem){
node.appendChild(undoneElements)
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
}, 600)
}
})
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
}, },
sortList(){ sortList(){
// //
@ -246,61 +251,65 @@ const SquireButtonFunctions = {
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(checkedItem){
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
}
} else {
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
//Close menu if user is on mobile //Close menu if user is on mobile
if(this.$store.getters.getIsUserOnMobile){ this.$router.go(-1)
this.$router.go(-1)
} setTimeout(()=>{
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(checkedItem){
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
}
} else {
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
},600)
}, },
calculateMath(){ calculateMath(){
// //
@ -310,6 +319,9 @@ const SquireButtonFunctions = {
//Fetch the container //Fetch the container
let container = document.getElementById('squire-id') let container = document.getElementById('squire-id')
//Close menu if user is on mobile, then sort list
this.$router.go(-1)
// simple function that trys to evaluate javascript // simple function that trys to evaluate javascript
const shittyMath = (string) => { const shittyMath = (string) => {
//Remove all chars but math chars //Remove all chars but math chars
@ -322,38 +334,39 @@ const SquireButtonFunctions = {
} }
} }
//Go through each item, on first level, look for Unordered Lists setTimeout(()=>{
container.childNodes.forEach( (node) => {
const line = node.innerText.trim() //Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
// = sign exists and its the last character in the string const line = node.innerText.trim()
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
//Pull out everything before the formula and try to evaluate it // = sign exists and its the last character in the string
const formula = line.split('=').shift() if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
const output = shittyMath(formula)
//If its a number and didn't throw an error, update the line //Pull out everything before the formula and try to evaluate it
if(!isNaN(output) && output != null){ const formula = line.split('=').shift()
const output = shittyMath(formula)
//Since there is HTML in the line, splice in the number after the = sign //If its a number and didn't throw an error, update the line
let equalLocation = node.innerHTML.indexOf('=') if(!isNaN(output) && output != null){
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//Slam in that new HTML with the output //Since there is HTML in the line, splice in the number after the = sign
node.innerHTML = newLine let equalLocation = node.innerHTML.indexOf('=')
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//Slam in that new HTML with the output
node.innerHTML = newLine
}
} }
}
}) })
},600)
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
}, },
setText(inText){ setText(inText){

View File

@ -1,4 +1,4 @@
<style> <style scoped>
div.no-padding { div.no-padding {
padding: 10px 0 40px !important; padding: 10px 0 40px !important;
box-sizing: border-box; box-sizing: border-box;
@ -233,16 +233,26 @@
} }
} }
.an-graph { .days-ago-display {
background: #fefefe82; font-size: 0.7em;
} }
.input-grid {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
}
.bottom.aligned.row {
flex: 1 1 auto;
}
</style> </style>
<template> <template>
<div class="squire-box no-padding" ref="scrollcontainer"> <div class="squire-box no-padding" ref="scrollcontainer">
<!-- intro modal -->
<modal v-if="!noteId" :click-out-close="false"> <modal v-if="!noteId" :click-out-close="false">
<div class="ui segment"> <div class="ui segment">
<div class="ui center aligned middle aligned grid"> <div class="ui center aligned middle aligned grid">
@ -273,10 +283,10 @@
</div> </div>
<!-- section 1 entry --> <!-- section 1 entry -->
<section class="swipe-section"> <section class="swipe-section" v-if="true">
<!-- <delete-button v-if="noteId > 0" class="ui small button" :note-id="noteId" /> --> <!-- <delete-button v-if="noteId > 0" class="ui small button" :note-id="noteId" /> -->
<div class="ui small centered dividing header"> <div class="ui small centered dividing header">
Metric Tracking Bet Metric Tracking
<span class="sub header"><i class="small lock icon"></i>All data Encrypted. Only accessible by you.</span> <span class="sub header"><i class="small lock icon"></i>All data Encrypted. Only accessible by you.</span>
</div> </div>
@ -314,10 +324,18 @@
<!-- data input --> <!-- data input -->
<div class="ui basic segment"> <div class="ui basic segment">
<div class="ui very compact grid"> <div class="ui very compact grid">
<div class="ui twelve wide middle aligned column"> <div class="four wide column"></div>
<div class="ui eight wide middle aligned center aligned column">
<div class="ui header"> <div class="ui header">
Entry for {{ calendar.monthName }}, <span :class="{'loading-day-title':loadingDay}">
<span :class="{'loading-day-title':loadingDay}">{{ calendar.today }}</span> {{ calendar.monthName }}
{{ calendar.today }}, {{ calendar.dayName }}
<span class="days-ago-display">
<!-- <span v-if="calendar.daysAgo == 0">Today</span> -->
<span v-if="calendar.daysAgo == 1"><br>{{ calendar.daysAgo }} day ago</span>
<span v-if="calendar.daysAgo > 1"><br>{{ calendar.daysAgo }} days ago</span>
</span>
</span>
</div> </div>
</div> </div>
<div class="ui four wide right aligned middle aligned column"> <div class="ui four wide right aligned middle aligned column">
@ -339,77 +357,89 @@
<div class="ui form" :class="{'loading-day':loadingDay}"> <div class="ui form" :class="{'loading-day':loadingDay}">
<draggable v-model="fields" class="ui compact grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable"> <draggable v-model="fields" class="ui compact grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable">
<div v-for="field in fields" :key="field" <div v-for="field in fields" :key="field"
:class="userFields[field]?.width ? userFields[field]?.width+' wide column':'eight wide column'"> :class="userFields[field]?.width ? userFields[field]?.width+' wide stretched column':'eight wide stretched column'">
<!-- field label display -->
<div class="ui very compact grid">
<div class="ui sixteen wide center aligned column">
<i :class="`${getFieldColor(field)} ${getFieldIcon(field)} icon`"></i>
<b>{{ userFields[field]?.label }}</b>
<span>{{ field }}</span>
</div>
</div>
<!-- float --> <div class="ui very compact grid input-grid">
<div v-if="userFields[field]?.type == 'float'" class="ui fluid input"> <!-- field label display -->
<input type="text" :placeholder="userFields[field]?.label" v-on:keyup="e => saveField(field, e.target.value)" :value="openDay[field]"> <div class="row">
</div> <div class="ui sixteen wide column">
<b><i :class="`${getFieldColor(field)} ${getFieldIcon(field)} icon`"></i>
<!-- range --> {{ userFields[field]?.label }}</b>
<div v-if="userFields[field]?.type == 'shortRange'"> <!-- <span>{{ field }}</span> -->
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">1</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">2</div>
<div :class="{green:(openDay[field] == 3)}" v-on:click="saveField(field, 3)" class="ui button">3</div>
<div :class="{green:(openDay[field] == 4)}" v-on:click="saveField(field, 4)" class="ui button">4</div>
<div :class="{green:(openDay[field] == 5)}" v-on:click="saveField(field, 5)" class="ui button">5</div>
</div>
<!-- text area -->
<div v-if="userFields[field]?.type == 'text'">
<textarea rows="3" v-on:keyup="e => saveField(field, e.target.value, 2000)" :value="openDay[field]">
</textarea>
</div>
<!-- boolean -->
<div v-if="userFields[field]?.type == 'boolean'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">Yes</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">No</div>
</div>
<div v-if="['sex','period','mucus','pms'].includes(userFields[field]?.type)">
<div class="option-buttons">
<div :class="{green:(openDay[field] == key)}" v-on:click="saveField(field, key)" class="ui compact button" v-for="(item,key) in fieldTypes[userFields[field]?.type].split(',')">{{ item }}</div>
</div>
</div>
<div v-if="userFields[field]?.type == 'custom'">
<div class="option-buttons">
<div
v-for="(item, value) in userFields[field]?.customOptions.split(',')"
v-on:click="saveField(field, value)"
:class="{green:(openDay[field] == value) && openDay[field] !== ''}"
class="ui compact button">
{{ item.trim() }}
</div> </div>
</div> </div>
</div>
<!-- input display -->
<div class="row">
<div class="sixteen wide column">
<div class="ui very compact grid"> <!-- float -->
<div class="ui six wide column"> <div v-if="userFields[field]?.type == 'float'" class="ui fluid input">
<span v-on:click="editField(field)"> <input type="text" :placeholder="userFields[field]?.label" v-on:keyup="e => saveField(field, e.target.value)" :value="openDay[field]">
<i class="clickable grey edit outline icon"></i> </div>
</span>
<!-- range -->
<div v-if="userFields[field]?.type == 'shortRange'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">1</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">2</div>
<div :class="{green:(openDay[field] == 3)}" v-on:click="saveField(field, 3)" class="ui button">3</div>
<div :class="{green:(openDay[field] == 4)}" v-on:click="saveField(field, 4)" class="ui button">4</div>
<div :class="{green:(openDay[field] == 5)}" v-on:click="saveField(field, 5)" class="ui button">5</div>
</div>
<!-- text area -->
<div v-if="userFields[field]?.type == 'text'">
<textarea rows="3" v-on:keyup="e => saveField(field, e.target.value, 2000)" :value="openDay[field]">
</textarea>
</div>
<!-- boolean -->
<div v-if="userFields[field]?.type == 'boolean'">
<div class="option-buttons">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui compact button">Yes</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui compact button">No</div>
</div>
</div>
<div v-if="['sex','period','mucus','pms'].includes(userFields[field]?.type)">
<div class="option-buttons">
<div :class="{green:(openDay[field] == key)}" v-on:click="saveField(field, key)" class="ui compact button" v-for="(item,key) in fieldTypesDef[userFields[field]?.type].split(',')">{{ item }}</div>
</div>
</div>
<div v-if="userFields[field]?.type == 'custom'">
<div class="option-buttons">
<div
v-for="(item, value) in userFields[field]?.customOptions.split(',')"
v-on:click="saveField(field, value)"
:class="{green:(openDay[field] == value) && openDay[field] !== ''}"
class="ui compact button">
{{ item.trim() }}
</div>
</div>
</div>
</div>
</div> </div>
<div class="four wide center aligned column draggable">
<span> <div class="bottom aligned row">
<i class="grey grip lines icon"></i> <div class="ui six wide column">
</span> <span v-on:click="editField(field)">
</div> <i class="clickable grey edit outline icon"></i>
<div class="ui six wide right aligned column"> </span>
<span v-if="openDay[field] !== ''" v-on:click="saveField(field, '')"> </div>
<i class="grey clickable reply icon"></i> <div class="four wide center aligned column draggable">
</span> <span>
<i class="grey grip lines icon"></i>
</span>
</div>
<div class="ui six wide right aligned column">
<span v-if="openDay[field] !== ''" v-on:click="saveField(field, '')">
<i class="grey clickable reply icon"></i>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -420,7 +450,7 @@
</section> </section>
<!-- section 2 analysis --> <!-- section 2 analysis -->
<section class="swipe-section"> <section class="swipe-section" v-if="!loading">
<div class="ui small centered dividing header"> <div class="ui small centered dividing header">
Review Data Review Data
@ -445,7 +475,7 @@
</div> </div>
<!-- calendar --> <!-- calendar -->
<div class="calendar"> <div class="calendar" v-if="false">
<div v-for="day in calendar.weekdays" class="day"> <div v-for="day in calendar.weekdays" class="day">
{{ day }} {{ day }}
@ -476,7 +506,7 @@
</div> </div>
</div> </div>
<div class="ui segment"> <div class="ui segment" v-if="false">
<a class="ui clickable" v-on:click="toggleFolded('key')"> <a class="ui clickable" v-on:click="toggleFolded('key')">
<i class="tiny circular blue clickable plus icon"></i> <i class="tiny circular blue clickable plus icon"></i>
Calendar Explanation Calendar Explanation
@ -558,31 +588,40 @@
<!-- Temp graph --> <!-- Temp graph -->
<div class="ui basic segment" > <div class="ui basic segment" >
<div class="ui dividing header"> <div class="ui dividing header">
Chart data for the last {{ tempChartDays }} days Chart data for the last {{ tempChartDays }} entries
</div> </div>
<div class="ui tiny compact fluid buttons"> <div class="ui tiny compact fluid buttons">
<div :class="{'green':(tempChartDays == 1000000)}" v-on:click="tempChartDays = 1000000; graphCurrentData()" class="ui button">ALL</div> <div :class="{'green':(tempChartDays == 1000000)}" v-on:click="tempChartDays = 1000000; " class="ui button">ALL</div>
<div :class="{'green':(tempChartDays == 90)}" v-on:click="tempChartDays = 90; graphCurrentData()" class="ui button">90</div> <div :class="{'green':(tempChartDays == 90)}" v-on:click="tempChartDays = 90; " class="ui button">90</div>
<div :class="{'green':(tempChartDays == 60)}" v-on:click="tempChartDays = 60; graphCurrentData()" class="ui button">60</div> <div :class="{'green':(tempChartDays == 60)}" v-on:click="tempChartDays = 60; " class="ui button">60</div>
<div :class="{'green':(tempChartDays == 30)}" v-on:click="tempChartDays = 30; graphCurrentData()" class="ui button">30</div> <div :class="{'green':(tempChartDays == 30)}" v-on:click="tempChartDays = 30; " class="ui button">30</div>
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; graphCurrentData()" class="ui button">7</div> <div :class="{'green':(tempChartDays == 15)}" v-on:click="tempChartDays = 15; " class="ui button">15</div>
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; " class="ui button">7</div>
</div> </div>
<!-- <div id="graphdiv" style="width: 100%; min-height: 320px;"></div> --> <!-- <div id="graphdiv" style="width: 100%; min-height: 320px;"></div> -->
<div class="ui divider"></div> <div class="ui divider"></div>
<div v-for="(graph, index) in graphs" class="an-graph"> <MetricGraphs
<div class="ui small dividing header"> :key="'graph-updates-'+graphUpdates"
{{ graph?.title }} :tempChartDays="tempChartDays"
</div> :fields="fields"
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div> :userFields="userFields"
<br> :graphs="graphs"
:cycleData="cycleData"
:calendar="calendar"
@saveGraphs="saveGraphs"
@toggleEditGraphs="toggleEditGraphs"
:editGraphs="editGraphs"
/>
<div class="ui very padded basic segment">
</div> </div>
</div> </div>
<!-- notes --> <!-- notes -->
<div class="ui basic segment"> <div class="ui basic segment" v-if="false">
<div class="ui clickable" v-on:click="toggleFolded('notes')"> <div class="ui clickable" v-on:click="toggleFolded('notes')">
<i class="tiny circular blue clickable plus icon"></i> <i class="tiny circular blue clickable plus icon"></i>
Additional Notes Additional Notes
@ -780,21 +819,21 @@
</div> </div>
<div v-for="(entry, key) in fieldDefinition" class="row" v-if="!['id'].includes(key)"> <div v-for="(entry, key) in fieldDefinition" class="row" v-if="!['id'].includes(key)">
<div v-if="fieldDefinitionOptions[key]" class="sixteen wide column"> <div v-if="fieldDefinitionOptionsDef[key]" class="sixteen wide column">
{{ mapFormTerm(key) }} {{ mapFormTerm(key) }}
<div v-if="fieldDefinitionOptions[key].type == 'text'" class="ui fluid input"> <div v-if="fieldDefinitionOptionsDef[key].type == 'text'" class="ui fluid input">
<input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)"> <input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)">
</div> </div>
<div v-if="fieldDefinitionOptions[key].type == 'commatextoptions' && editFieldObject?.type == 'custom'" class="ui fluid input"> <div v-if="fieldDefinitionOptionsDef[key].type == 'commatextoptions' && editFieldObject?.type == 'custom'" class="ui fluid input">
<input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)"> <input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)">
</div> </div>
<div v-if="fieldDefinitionOptions[key].type == 'option'"> <div v-if="fieldDefinitionOptionsDef[key].type == 'option'">
<div v-for="option in fieldDefinitionOptions[key].options" <div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)" v-on:click="setNewFieldOption(null, key, option)"
:class="{'green':editFieldObject[key] == option}" :class="{'green':editFieldObject[key] == option}"
class="ui button"> class="ui button">
@ -802,8 +841,8 @@
</div> </div>
</div> </div>
<div v-if="fieldDefinitionOptions[key].type == 'icons'"> <div v-if="fieldDefinitionOptionsDef[key].type == 'icons'">
<div v-for="option in fieldDefinitionOptions[key].options" <div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)" v-on:click="setNewFieldOption(null, key, option)"
:class="{'green':editFieldObject[key] == option}" :class="{'green':editFieldObject[key] == option}"
class="ui icon button"> class="ui icon button">
@ -812,8 +851,8 @@
</div> </div>
<!-- :class="{'green':}" --> <!-- :class="{'green':}" -->
<div v-if="fieldDefinitionOptions[key].type == 'color'"> <div v-if="fieldDefinitionOptionsDef[key].type == 'color'">
<div v-for="option in fieldDefinitionOptions[key].options" <div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)" v-on:click="setNewFieldOption(null, key, option)"
:class="`ui ${option} icon button`"> :class="`ui ${option} icon button`">
<i v-if="editFieldObject[key] == option" class="white check icon"></i> <i v-if="editFieldObject[key] == option" class="white check icon"></i>
@ -865,10 +904,6 @@
import axios from 'axios' import axios from 'axios'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import { Chart } from 'chart.js/auto'
var BASAL_TEMP = 'BT'
export default { export default {
name: 'MetricTracking', name: 'MetricTracking',
components: { components: {
@ -876,6 +911,7 @@
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default, 'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
'modal': require('@/components/ModalComponent.vue').default, 'modal': require('@/components/ModalComponent.vue').default,
draggable, draggable,
'MetricGraphs':require('@/components/Metrictracking/MetricGraphsComponent.vue').default,
}, },
data () { data () {
return { return {
@ -886,15 +922,8 @@
showNotes: false, showNotes: false,
fields:[], // Array of field IDs fields:[], // Array of field IDs
userFields:{}, // Objects of field definitions userFields:{}, // Objects of field definitions
graphs:[ graphs:[],
// types [null, 'lastDone', 'pillCalendar'] fieldTypesDef:{
{type:'pillCalendar', title:'Pill Cal', fieldIds:['JX3QD','K3KII']},
{type:'lastDone', title:'Last Done', fieldIds:['JX3QD','K3KII']},
{title:'Basal Temp', fieldIds:['BT']},
{title:'Basal Temp - Cervical Fluid', fieldIds:['BT','CM']},
{title:'Test Graph', fieldIds:['40DA6','MI9B9']},
],
fieldTypes:{
'float':'Precise Number', 'float':'Precise Number',
'shortRange':'Options 1-5', 'shortRange':'Options 1-5',
'longRange':'Options 1-10', 'longRange':'Options 1-10',
@ -922,7 +951,7 @@
fieldDefinition:{ fieldDefinition:{
'id':'','type':'','label':'','icon':'','color':'','width':'','customOptions':'' 'id':'','type':'','label':'','icon':'','color':'','width':'','customOptions':''
}, },
fieldDefinitionOptions:{ fieldDefinitionOptionsDef:{
label:{'type':'text'}, label:{'type':'text'},
customOptions:{'type':'commatextoptions'}, customOptions:{'type':'commatextoptions'},
icon:{'type':'icons', 'options':['heart','smile','cat','crow','dog','dove','dragon','feather','feather alternate','fish','frog','hippo','horse','horse head','kiwi bird','otter','paw','spider','video','headphones','motorcycle','truck','monster truck','campground','cloud sun','drumstick bite','football ball','fruit-apple','hiking','mountain','tractor','tree','wind','wine bottle','coffee','flask','glass cheers','glass martini','beer','toilet paper','gift','globe','hand holding heart','comment','graduation cap','hat cowboy','hat wizard','mitten','user tie','laptop code','microchip','shield alternate','mouse','plug','power off','satellite','hammer','wrench','bell','eye','marker','paperclip','atom','award','theater masks','music','grin alternate','grin tongue squint outline','laugh wink','fire','fire alternate','poop','sun','money bill alternate','piggy bank','heart outline','heartbeat','running','walking','bacon','bone','bread slice','candy cane','carrot','cheese','cloud meatball','cookie','egg','hamburger','hotdog','ice cream','lemon','lemon outline','pepper hot','pizza slice','seedling','stroopwafel','leaf','book dead','broom','cloud moon','ghost','mask','skull crossbones','certificate','check','check circle','joint','cannabis','bong','gem','futbol','brain','dna','hand spock','hand spock outline','meteor','moon','moon outline','robot','rocket','satellite dish','space shuttle','user astronaut','fingerprint','thumbs up','thumbs down']}, icon:{'type':'icons', 'options':['heart','smile','cat','crow','dog','dove','dragon','feather','feather alternate','fish','frog','hippo','horse','horse head','kiwi bird','otter','paw','spider','video','headphones','motorcycle','truck','monster truck','campground','cloud sun','drumstick bite','football ball','fruit-apple','hiking','mountain','tractor','tree','wind','wine bottle','coffee','flask','glass cheers','glass martini','beer','toilet paper','gift','globe','hand holding heart','comment','graduation cap','hat cowboy','hat wizard','mitten','user tie','laptop code','microchip','shield alternate','mouse','plug','power off','satellite','hammer','wrench','bell','eye','marker','paperclip','atom','award','theater masks','music','grin alternate','grin tongue squint outline','laugh wink','fire','fire alternate','poop','sun','money bill alternate','piggy bank','heart outline','heartbeat','running','walking','bacon','bone','bread slice','candy cane','carrot','cheese','cloud meatball','cookie','egg','hamburger','hotdog','ice cream','lemon','lemon outline','pepper hot','pizza slice','seedling','stroopwafel','leaf','book dead','broom','cloud moon','ghost','mask','skull crossbones','certificate','check','check circle','joint','cannabis','bong','gem','futbol','brain','dna','hand spock','hand spock outline','meteor','moon','moon outline','robot','rocket','satellite dish','space shuttle','user astronaut','fingerprint','thumbs up','thumbs down']},
@ -931,15 +960,18 @@
}, },
cycleData: {}, cycleData: {},
totalEntries: 0, totalEntries: 0,
openDay: {}, openDay: {}, // current day values, updates into cycleData when saved
loadingDayTimeout: null, loadingDayTimeout: null,
loadingDay: false, loadingDay: false,
saveDataDebounce:null, saveDataDebounce:null,
loading: true,
saving: 0, // 0 blank, 1 modified, 2 saving, 3 saved saving: 0, // 0 blank, 1 modified, 2 saving, 3 saved
calendar: { calendar: {
dateObject: null, dateObject: null,
dateCode: null, dateCode: null,
monthName: '', monthName: '',
dayName:'',
daysAgo:0,
month: '', month: '',
year: '', year: '',
days: [], days: [],
@ -952,6 +984,8 @@
editFieldId:'', editFieldId:'',
editFieldObject:{}, editFieldObject:{},
appDataImport:'', appDataImport:'',
graphUpdates:0,
editGraphs: false,
} }
}, },
beforeCreate() { beforeCreate() {
@ -972,11 +1006,6 @@
location.reload(); location.reload();
}) })
// Include JS libraries
let graphsScript = document.createElement('script')
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
document.head.appendChild(graphsScript)
// setup date to today // setup date to today
this.setupCalendar() this.setupCalendar()
@ -1035,6 +1064,19 @@
} }
}, },
methods: { methods: {
saveGraphs(newGraphData){
this.graphs = newGraphData
this.saveCycleData()
// re-render graph data on update
this.$nextTick(() => {
this.graphUpdates++
})
},
toggleEditGraphs(){
this.editGraphs = !this.editGraphs
},
getFieldColor(field){ getFieldColor(field){
let color = null let color = null
@ -1134,14 +1176,14 @@
validateCustomFieldsForm(){ validateCustomFieldsForm(){
const checks = [] //check const checks = [] //check
const checkFields = Object.keys(this.fieldDefinitionOptions) const checkFields = Object.keys(this.fieldDefinitionOptionsDef)
checkFields.forEach(row => { checkFields.forEach(row => {
// console.log(this.editFieldObject[row]) // console.log(this.editFieldObject[row])
// don't worry about optional fields // don't worry about optional fields
if(this.fieldDefinitionOptions[row].optional){ if(this.fieldDefinitionOptionsDef[row].optional){
checks.push(true) checks.push(true)
return return
} }
@ -1323,98 +1365,6 @@
this.fields.push(fieldId) this.fields.push(fieldId)
this.saveCycleData() this.saveCycleData()
}, },
graphCurrentData(){
const graphOptions = {
interactionModel: {},
// pointClickCallback: function(e, pt){
// console.log(e)
// console.log(pt)
// console.log(this.getValue(pt.idx, 0))
// }
}
// Excel date format YYYYMMDD
const convertToExcelDate = (dateCode) => {
return dateCode
.split('.')
.reverse()
.map(item => String(item).padStart(2,0))
.join('')
}
const sortableDate = (dateCode) => {
return parseInt(
dateCode
.split('.')
.map(i => String(i).padStart(2, '0'))
.reverse()
.join('')
)
}
// Generate set of keys for graph length
let dataKeys = Object.keys(this.cycleData)
dataKeys.sort((a,b) => {
a = sortableDate(a)
b = sortableDate(b)
return b - a
})
dataKeys = dataKeys.splice(0, this.tempChartDays)
// build CSV data for each graph
this.graphs.forEach((graph,index) => {
// CSV or path to a CSV file.
let dataString = ""
// Lookup graph field titles
let graphLabels = ['Date']
graph.fieldIds.forEach(fieldId => {
const graphLabel = this.userFields[fieldId]?.label
graphLabels.push(graphLabel)
})
dataString += graphLabels.join(',') + '\n'
// build each row, for each day
for (var i = 0; i < dataKeys.length; i++) {
let nextFragment = []
// push date code to first column
nextFragment.push(convertToExcelDate(dataKeys[i]))
graph.fieldIds.forEach(fieldId => {
const currentEntry = this.cycleData[dataKeys[i]]
let currentValue = currentEntry[fieldId]
if(fieldId == 'BT'){
// parse temp to fixed length float 00.00
currentValue = parseFloat(currentValue).toFixed(2)
}
if(fieldId == 'CM'){
currentValue = parseFloat('97.'+currentValue)
}
nextFragment.push(currentValue)
})
dataString += nextFragment.join(',') + "\n"
}
let graphDiv = document.getElementById("graphdiv"+index)
const g = new Dygraph(graphDiv, dataString ,graphOptions)
})
return
},
saveField(fieldId, value, optionalTimeout){ saveField(fieldId, value, optionalTimeout){
// Dont save value if it hasn't changed // Dont save value if it hasn't changed
@ -1472,8 +1422,6 @@
this.cycleData[this.calendar.dateCode] = cleanDayData this.cycleData[this.calendar.dateCode] = cleanDayData
} }
this.graphCurrentData()
this.saveCycleData() this.saveCycleData()
}, },
@ -1499,6 +1447,7 @@
axios.post('/api/metric-tracking/get') axios.post('/api/metric-tracking/get')
.then(({ data }) => { .then(({ data }) => {
this.loading = false
this.setApplicationStateJson(data) this.setApplicationStateJson(data)
}) })
@ -1532,27 +1481,49 @@
this.cycleData = json?.cycleData || this.cycleData this.cycleData = json?.cycleData || this.cycleData
this.fields = [...new Set(json?.fields)] || this.fields this.fields = [...new Set(json?.fields)] || this.fields
this.userFields = json?.userFields || this.userFields this.userFields = json?.userFields || this.userFields
// this.graphs = json?.graphs || this.graphs this.graphs = json?.graphs || this.graphs
// console.log(this.fields)
this.$nextTick(() => { this.$nextTick(() => {
this.getApplicationStateJson()
this.totalEntries = Object.keys(this.cycleData).length this.totalEntries = Object.keys(this.cycleData).length
this.setupFields() this.setupFields()
this.openDayData(this.calendar.dateCode) this.openDayData(this.calendar.dateCode)
this.graphCurrentData()
this.generateTonsOfRandomData() this.generateTonsOfRandomData()
}) })
}, },
getApplicationStateJson(){ getApplicationStateJson(){
// convert date code into sortable int
const sortableDate = (code) => {
code = code
.split('.')
.reverse()
.map(i => String(i).padStart(2, '0'))
.join('')
return parseInt(code)
}
// Sort cycle data, newest first
let sortedData = Object.keys(this.cycleData)
.sort((a,b) => {
return sortableDate(b) - sortableDate(a)
})
// setup new object with sorted data
let sortedCycleData = sortedData.reduce((result, key) => {
result[key] = this.cycleData[key]
return result
},{})
return JSON.stringify({ return JSON.stringify({
fields: this.fields, fields: this.fields,
cycleData: this.cycleData, cycleData: sortedCycleData,
userFields: this.userFields, userFields: this.userFields,
// graphs: this.graphs, graphs: this.graphs,
}) })
}, },
saveCycleData(){ saveCycleData(){
@ -1624,6 +1595,7 @@
}, },
setupCalendar(date){ setupCalendar(date){
// visualize each day change
this.working = true this.working = true
setTimeout(() => { setTimeout(() => {
this.working = false this.working = false
@ -1640,6 +1612,13 @@
this.calendar.dateCode = this.generateDateCode(date) this.calendar.dateCode = this.generateDateCode(date)
// calculate days ago since current date
const now = new Date()
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
const dayInterval = diffSeconds / 86400 // seconds in a day
this.calendar.daysAgo = Math.floor(dayInterval)
// ------------ // ------------
// setup calendar display // setup calendar display
@ -1656,6 +1635,7 @@
const currentYear = date.getFullYear(); const currentYear = date.getFullYear();
const currentMonth = date.getMonth() + 1; const currentMonth = date.getMonth() + 1;
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" }); this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
this.calendar.year = currentYear this.calendar.year = currentYear
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth); const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
@ -1704,7 +1684,6 @@
workingDate.setDate(workingDate.getDate()-1) workingDate.setDate(workingDate.getDate()-1)
} }
this.graphCurrentData(5000)
}, },
} }
} }

View File

@ -44,7 +44,7 @@ export default new Vuex.Store({
'menu-text': '#5e6268', 'menu-text': '#5e6268',
}, },
'black':{ 'black':{
'body_bg_color': 'linear-gradient(135deg, rgba(0,0,0,1) 0%, rgba(23,12,46,1) 100%)', 'body_bg_color': 'rgb(12 4 30)',
//'#0f0f0f',//'#000', //'#0f0f0f',//'#000',
'small_element_bg_color': '#000', 'small_element_bg_color': '#000',
'text_color': '#FFF', 'text_color': '#FFF',

View File

@ -539,3 +539,11 @@ Attachment.processUrl = (userId, noteId, url) => {
}, scrapeTime ) }, scrapeTime )
}) })
} }
Attachment.generatePushKey = (userId) => {}
Attachment.deletePushKey = (userId) => {}
Attachment.getPushkey = (userId) => {}
Attachment.pushUrl = (userId) => {}

View File

@ -65,5 +65,30 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
}) })
//
// Push URL to attachments
//
// get push key
router.get('/getpushkey', function (req, res) {
Attachment.delete(userId, req.body.attachmentId)
.then( data => res.send(data) )
})
// generate new push key
router.post('/generatepushkey', function (req, res) {
})
// delete push key
router.post('/deletepushkey', function (req, res) {
})
// push url to attchments
router.get('/pushurl', function (req, res) {
})
module.exports = router module.exports = router