#8, added fullcalendar

This commit is contained in:
Magnus Åhall 2024-04-19 18:32:37 +02:00
parent 1a9d532d02
commit 0a6eaed89f
7 changed files with 188 additions and 8 deletions

View File

@ -761,7 +761,9 @@ func scheduleList(w http.ResponseWriter, r *http.Request, sess *session.T) { //
w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add("Access-Control-Allow-Origin", "*")
request := struct { request := struct {
NodeID int NodeID int
StartDate time.Time
EndDate time.Time
}{} }{}
body, _ := io.ReadAll(r.Body) body, _ := io.ReadAll(r.Body)
if len(body) > 0 { if len(body) > 0 {
@ -773,14 +775,14 @@ func scheduleList(w http.ResponseWriter, r *http.Request, sess *session.T) { //
} }
var schedules []Schedule var schedules []Schedule
schedules, err = FutureSchedules(sess.UserID, request.NodeID) schedules, err = FutureSchedules(sess.UserID, request.NodeID, request.StartDate, request.EndDate)
if err != nil { if err != nil {
responseError(w, err) responseError(w, err)
return return
} }
responseData(w, map[string]interface{}{ responseData(w, map[string]interface{}{
"OK": true, "OK": true,
"ScheduleEvents": schedules, "ScheduleEvents": schedules,
}) })
} // }}} } // }}}

View File

@ -215,9 +215,17 @@ func ExpiredSchedules() (schedules []Schedule) { // {{{
} }
return return
} // }}} } // }}}
func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {// {{{ func FutureSchedules(userID int, nodeID int, start time.Time, end time.Time) (schedules []Schedule, err error) { // {{{
schedules = []Schedule{} schedules = []Schedule{}
var foo string
row := service.Db.Conn.QueryRow(`SELECT TO_CHAR($1::date AT TIME ZONE 'UTC', 'yyyy-mm-dd HH24:MI')`, start)
err = row.Scan(&foo)
if err != nil {
return
}
logger.Info("FOO", "date", foo)
res := service.Db.Conn.QueryRow(` res := service.Db.Conn.QueryRow(`
WITH schedule_events AS ( WITH schedule_events AS (
SELECT SELECT
@ -243,6 +251,14 @@ func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
WHEN $2 > 0 THEN n.id = $2 WHEN $2 > 0 THEN n.id = $2
ELSE true ELSE true
END END
) AND (
CASE WHEN TO_CHAR($3::date, 'yyyy-mm-dd HH24:MI') = '0001-01-01 00:00' THEN TRUE
ELSE (s.time AT TIME ZONE u.timezone) >= $3
END
) AND (
CASE WHEN TO_CHAR($4::date, 'yyyy-mm-dd HH24:MI') = '0001-01-01 00:00' THEN TRUE
ELSE (s.time AT TIME ZONE u.timezone) <= $4
END
) AND ) AND
time >= NOW() AND time >= NOW() AND
NOT acknowledged NOT acknowledged
@ -251,8 +267,10 @@ func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
COALESCE(jsonb_agg(s.*), '[]'::jsonb) COALESCE(jsonb_agg(s.*), '[]'::jsonb)
FROM schedule_events s FROM schedule_events s
`, `,
userID, userID,
nodeID, nodeID,
start,
end,
) )
var j []byte var j []byte
err = res.Scan(&j) err = res.Scan(&j)
@ -268,4 +286,4 @@ func FutureSchedules(userID int, nodeID int) (schedules []Schedule, err error) {
} }
return return
}// }}} } // }}}

View File

@ -802,6 +802,38 @@ header .menu {
grid-gap: 8px; grid-gap: 8px;
margin-top: 8px; margin-top: 8px;
} }
#fullcalendar {
margin: 32px;
color: #444;
}
.folder .tabs {
border-left: 1px solid #888;
display: flex;
}
.folder .tabs .tab {
padding: 16px 32px;
border-top: 1px solid #888;
border-bottom: 1px solid #888;
border-right: 1px solid #888;
color: #444;
background: #eee;
cursor: pointer;
}
.folder .tabs .tab.selected {
border-bottom: none;
background: #fff;
}
.folder .tabs .hack {
border-bottom: 1px solid #888;
width: 100%;
}
.folder .content {
padding-top: 1px;
border-left: 1px solid #888;
border-right: 1px solid #888;
border-bottom: 1px solid #888;
padding-bottom: 1px;
}
@media only screen and (max-width: 932px) { @media only screen and (max-width: 932px) {
#app.node { #app.node {
grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "schedule" "files" "blank"; grid-template-areas: "header" "crumbs" "child-nodes" "name" "content" "checklist" "schedule" "files" "blank";

View File

@ -27,6 +27,7 @@
</script> </script>
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/sjcl.js"></script> <script type="text/javascript" src="/js/{{ .VERSION }}/lib/sjcl.js"></script>
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js"></script> <script type="text/javascript" src="/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js"></script>
<script type="text/javascript" src="/js/{{ .VERSION }}/lib/fullcalendar.min.js"></script>
</head> </head>
<body> <body>

6
static/js/lib/fullcalendar.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -986,6 +986,41 @@ class ProfileSettings extends Component {
} }
class ScheduleEventList extends Component { class ScheduleEventList extends Component {
static CALENDAR = Symbol('CALENDAR')
static LIST = Symbol('LIST')
constructor() {//{{{
super()
this.tab = signal(ScheduleEventList.CALENDAR)
}//}}}
render() {//{{{
var tab
switch (this.tab.value) {
case ScheduleEventList.CALENDAR:
tab = html`<${ScheduleCalendarTab} />`
break;
case ScheduleEventList.LIST:
tab = html`<${ScheduleEventListTab} />`
break;
}
return html`
<div style="margin: 32px">
<div class="folder">
<div class="tabs">
<div onclick=${() => this.tab.value = ScheduleEventList.CALENDAR} class="tab ${this.tab.value == ScheduleEventList.CALENDAR ? 'selected' : ''}">Calendar</div>
<div onclick=${() => this.tab.value = ScheduleEventList.LIST} class="tab ${this.tab.value == ScheduleEventList.LIST ? 'selected' : ''}">List</div>
<div class="hack"></div>
</div>
<div class="content">
${tab}
</div>
</div>
</div>
`
}//}}}
}
class ScheduleEventListTab extends Component {
constructor() {//{{{ constructor() {//{{{
super() super()
this.events = signal(null) this.events = signal(null)
@ -995,7 +1030,12 @@ class ScheduleEventList extends Component {
if (this.events.value === null) if (this.events.value === null)
return return
let events = this.events.value.map(evt => { let events = this.events.value.sort((a, b) => {
if (a.Time < b.Time) return -1
if (a.Time > b.Time) return 1
return 0
}).map(evt => {
console.log(evt)
const dt = evt.Time.split('T') const dt = evt.Time.split('T')
const remind = () => { const remind = () => {
if (evt.RemindMinutes > 0) if (evt.RemindMinutes > 0)
@ -1032,5 +1072,45 @@ class ScheduleEventList extends Component {
}//}}} }//}}}
} }
class ScheduleCalendarTab extends Component {
constructor() {//{{{
super()
}//}}}
componentDidMount() {
let calendarEl = document.getElementById('fullcalendar');
this.calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth',
events: this.events,
eventTimeFormat: {
hour12: false,
hour: '2-digit',
minute: '2-digit',
},
firstDay: 1,
});
this.calendar.render();
}
render() {
return html`<div id="fullcalendar"></div>`
}
events(info, successCallback, failureCallback) {
const req = {
StartDate: info.startStr,
EndDate: info.endStr,
}
_app.current.request('/schedule/list', req)
.then(data => {
const fullcalendarEvents = data.ScheduleEvents.map(sch => {
return {
title: sch.Description,
start: sch.Time,
url: `/?node=${sch.Node.ID}`,
}
})
successCallback(fullcalendarEvents)
})
}
}
// vim: foldmethod=marker // vim: foldmethod=marker

View File

@ -932,6 +932,47 @@ header {
} }
} }
#fullcalendar {
margin: 32px;
color: #444;
}
.folder {
.tabs {
border-left: 1px solid #888;
display: flex;
.tab {
padding: 16px 32px;
border-top: 1px solid #888;
border-bottom: 1px solid #888;
border-right: 1px solid #888;
color: #444;
background: #eee;
cursor: pointer;
&.selected {
border-bottom: none;
background: #fff;
}
}
.hack {
border-bottom: 1px solid #888;
width: 100%;
}
}
.content {
padding-top: 1px;
border-left: 1px solid #888;
border-right: 1px solid #888;
border-bottom: 1px solid #888;
padding-bottom: 1px;
}
}
@media only screen and (max-width: 932px) { @media only screen and (max-width: 932px) {
#app.node { #app.node {
.layout-crumbs(); .layout-crumbs();