From 059b3e34b2c2583753b79bb3c1165a19555f3be3 Mon Sep 17 00:00:00 2001 From: clr-li <111320104+clr-li@users.noreply.github.com> Date: Sun, 28 Apr 2024 12:08:09 -0700 Subject: [PATCH] Added absentemail setting --- .badges/coverage.svg | 8 ++-- migrations/0002_modify_businesses.mjs | 25 ++++++++++ migrations/0003_modify_businesses.mjs | 31 ++++++++++++ public/admin.js | 25 ++++++++++ public/calendar.js | 68 +++++++++++++-------------- public/components/DataTable.js | 11 ----- public/index.html | 11 ++--- public/payment.html | 2 +- server/Business.js | 39 ++++++++++++++- server/schema.sql | 3 +- server/super_admin/settings.json | 32 +++++++++++++ 11 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 migrations/0002_modify_businesses.mjs create mode 100644 migrations/0003_modify_businesses.mjs diff --git a/.badges/coverage.svg b/.badges/coverage.svg index 54570f9..c566e53 100644 --- a/.badges/coverage.svg +++ b/.badges/coverage.svg @@ -1,6 +1,6 @@ - - Coverage: 75% + + Coverage: 74% @@ -10,8 +10,8 @@ Coverage - - 75% + + 74% \ No newline at end of file diff --git a/migrations/0002_modify_businesses.mjs b/migrations/0002_modify_businesses.mjs new file mode 100644 index 0000000..1681662 --- /dev/null +++ b/migrations/0002_modify_businesses.mjs @@ -0,0 +1,25 @@ +// Automatically created by 'sqlite auto migrator (SAM)' on 2024-04-28T18:51:38.159Z + +import { Database } from 'sqlite-auto-migrator'; + +// Pragmas can't be changed in transactions, so they are tracked separately. +// Note that most pragmas are not persisted in the database file and will have to be set on each new connection. +export const PRAGMAS = {"analysis_limit":0,"application_id":0,"auto_vacuum":0,"automatic_index":1,"timeout":1000,"cache_size":-2000,"cache_spill":483,"cell_size_check":0,"checkpoint_fullfsync":0,"seq":0,"name":"Records","compile_options":"ATOMIC_INTRINSICS=1","count_changes":0,"data_version":1,"file":"","defer_foreign_keys":0,"empty_result_callbacks":0,"encoding":"UTF-8","foreign_keys":0,"freelist_count":0,"full_column_names":0,"fullfsync":0,"builtin":1,"type":"table","enc":"utf8","narg":2,"flags":2099200,"hard_heap_limit":0,"ignore_check_constraints":0,"integrity_check":"ok","journal_mode":"delete","journal_size_limit":-1,"legacy_alter_table":0,"locking_mode":"normal","max_page_count":1073741823,"page_count":12,"page_size":4096,"query_only":0,"quick_check":"ok","read_uncommitted":0,"recursive_triggers":0,"reverse_unordered_selects":0,"schema_version":5,"secure_delete":0,"short_column_names":1,"soft_heap_limit":0,"synchronous":2,"schema":"main","ncol":5,"wr":0,"strict":0,"temp_store":0,"threads":0,"trusted_schema":1,"user_version":0,"wal_autocheckpoint":1000,"busy":0,"log":-1,"checkpointed":-1,"writable_schema":0}; + +/** + * Runs the necessary SQL commands to migrate the database up to this version from the previous version. + * Automatically runs in a transaction with deferred foreign keys. + * @param {Database} db database instance to run SQL commands on + */ +export async function up(db) { + await db.run("ALTER TABLE \"businesses\" ADD COLUMN \"absentemail\" INTEGER DEFAULT 1"); +} + +/** + * Runs the necessary SQL commands to migrate the database down to the previous version from this version. + * Automatically runs in a transaction with deferred foreign keys. + * @param {Database} db database instance to run SQL commands on + */ +export async function down(db) { + await db.run("ALTER TABLE \"businesses\" DROP COLUMN \"absentemail\""); +} \ No newline at end of file diff --git a/migrations/0003_modify_businesses.mjs b/migrations/0003_modify_businesses.mjs new file mode 100644 index 0000000..2ef2562 --- /dev/null +++ b/migrations/0003_modify_businesses.mjs @@ -0,0 +1,31 @@ +// Automatically created by 'sqlite auto migrator (SAM)' on 2024-04-28T18:51:51.161Z + +import { Database } from 'sqlite-auto-migrator'; + +// Pragmas can't be changed in transactions, so they are tracked separately. +// Note that most pragmas are not persisted in the database file and will have to be set on each new connection. +export const PRAGMAS = {"analysis_limit":0,"application_id":0,"auto_vacuum":0,"automatic_index":1,"timeout":1000,"cache_size":-2000,"cache_spill":483,"cell_size_check":0,"checkpoint_fullfsync":0,"seq":0,"name":"Records","compile_options":"ATOMIC_INTRINSICS=1","count_changes":0,"data_version":1,"file":"","defer_foreign_keys":0,"empty_result_callbacks":0,"encoding":"UTF-8","foreign_keys":0,"freelist_count":0,"full_column_names":0,"fullfsync":0,"builtin":1,"type":"table","enc":"utf8","narg":2,"flags":2099200,"hard_heap_limit":0,"ignore_check_constraints":0,"integrity_check":"ok","journal_mode":"delete","journal_size_limit":-1,"legacy_alter_table":0,"locking_mode":"normal","max_page_count":1073741823,"page_count":12,"page_size":4096,"query_only":0,"quick_check":"ok","read_uncommitted":0,"recursive_triggers":0,"reverse_unordered_selects":0,"schema_version":5,"secure_delete":0,"short_column_names":1,"soft_heap_limit":0,"synchronous":2,"schema":"main","ncol":5,"wr":0,"strict":0,"temp_store":0,"threads":0,"trusted_schema":1,"user_version":0,"wal_autocheckpoint":1000,"busy":0,"log":-1,"checkpointed":-1,"writable_schema":0}; + +/** + * Runs the necessary SQL commands to migrate the database up to this version from the previous version. + * Automatically runs in a transaction with deferred foreign keys. + * @param {Database} db database instance to run SQL commands on + */ +export async function up(db) { + await db.run("CREATE TABLE temp_c7r62mgn8zu(id INTEGER PRIMARY KEY,name TEXT NOT NULL,joincode TEXT NOT NULL UNIQUE,subscriptionId TEXT NOT NULL UNIQUE,requireJoin INTEGER DEFAULT 0 NOT NULL,absentEmail INTEGER DEFAULT 1 NOT NULL)"); + await db.run("INSERT INTO temp_c7r62mgn8zu (\"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\") SELECT \"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\" FROM \"businesses\""); + await db.run("DROP TABLE \"businesses\""); + await db.run("ALTER TABLE temp_c7r62mgn8zu RENAME TO \"businesses\""); +} + +/** + * Runs the necessary SQL commands to migrate the database down to the previous version from this version. + * Automatically runs in a transaction with deferred foreign keys. + * @param {Database} db database instance to run SQL commands on + */ +export async function down(db) { + await db.run("CREATE TABLE temp_c7r62mgn8zu(id INTEGER PRIMARY KEY,name TEXT NOT NULL,joincode TEXT NOT NULL UNIQUE,subscriptionId TEXT NOT NULL UNIQUE,requireJoin INTEGER DEFAULT 0 NOT NULL,absentemail INTEGER DEFAULT 1)"); + await db.run("INSERT INTO temp_c7r62mgn8zu (\"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\") SELECT \"id\", \"name\", \"joincode\", \"subscriptionid\", \"requirejoin\", \"absentemail\" FROM \"businesses\""); + await db.run("DROP TABLE \"businesses\""); + await db.run("ALTER TABLE temp_c7r62mgn8zu RENAME TO \"businesses\""); +} \ No newline at end of file diff --git a/public/admin.js b/public/admin.js index 4fae2e9..4874844 100644 --- a/public/admin.js +++ b/public/admin.js @@ -51,6 +51,19 @@ class GroupSettings extends Component {
+


Manage Subscription   Take Attendance   @@ -81,7 +94,11 @@ class GroupSettings extends Component { const requireJoin = await GET(`/businesses/${getBusinessId()}/settings/requirejoin`).then( res => res.json(), ); + const absentEmail = await GET(`/businesses/${getBusinessId()}/settings/absentemail`).then( + res => res.json(), + ); this.shadowRoot.getElementById('require-join').checked = requireJoin.requireJoin === 1; + this.shadowRoot.getElementById('absent-email').checked = absentEmail.absentEmail === 1; // initialize email notification this.shadowRoot.getElementById('email-notification').textContent = ` @@ -113,6 +130,14 @@ class GroupSettings extends Component { }`, ); }; + const absentEmail = this.shadowRoot.getElementById('absent-email'); + absentEmail.onchange = async () => { + await PUT( + `/businesses/${getBusinessId()}/settings/absentemail?new=${ + absentEmail.checked ? 1 : 0 + }`, + ); + }; // send email notification const emailNotification = this.shadowRoot.getElementById('email-notification'); this.shadowRoot.getElementById('sent-email').onclick = async () => { diff --git a/public/calendar.js b/public/calendar.js index de5f261..c35663c 100644 --- a/public/calendar.js +++ b/public/calendar.js @@ -1,5 +1,5 @@ -import { GET, PUT, sendGmail } from './util/Client.js'; -import { requireLogin, getCurrentUser, requestGoogleCredential } from './util/Auth.js'; +import { GET, PUT, sendEmail } from './util/Client.js'; +import { requireLogin, getCurrentUser } from './util/Auth.js'; import { sanitizeText } from './util/util.js'; import { Popup } from './components/Popup.js'; const $ = window.$; @@ -147,44 +147,44 @@ window.markAbsent = async (businessId, eventId) => { await Popup.alert(await res.text(), 'var(--error)'); return; } - await Popup.alert( - 'You have been marked absent! Close this popup and click on your email account to send notifications to the event host(s).', - 'var(--success)', - ); + await Popup.alert('You have been marked absent!', 'var(--success)'); - // Send email notification to business owner and admins - const credential = await requestGoogleCredential([ - 'https://www.googleapis.com/auth/gmail.send', - ]); - let success = true; - const res1 = await GET(`/businesses/${businessId}/writemembers`); - const writeMembers = await res1.json(); - console.log(writeMembers); + // Send email notification to business owner and admins if setting is enabled + const absentEmail = await GET(`/businesses/${businessId}/settings/absentemail`).then(res => + res.json(), + ); + if (absentEmail.absentEmail === 1) { + let success = true; + const res1 = await GET(`/businesses/${businessId}/writemembers`); + const writeMembers = await res1.json(); - for (const member of writeMembers) { - const res = await sendGmail( - member.email, - 'Attendance Scanner Notification of Absence', - 'Hi ' + - member.name + - ',\n\n' + - user.name + - ' has marked themselves absent from the event.\n\n(automatically sent via Attendance Scanner QR)', - credential, - ); - if (!res.ok) { - success = false; - const obj = await res.json(); - const message = obj.error.message; + for (const member of writeMembers) { + const res = await sendEmail( + member.email, + 'Attendance Scanner Notification of Absence', + 'Hi ' + + member.name + + ',\n\n' + + user.name + + ' has marked themselves absent from the event.\n\n(automatically sent via Attendance Scanner QR)', + ); + if (!res.ok) { + success = false; + const obj = await res.json(); + const message = obj.error.message; + Popup.alert( + `Email to ${sanitizeText(member[1])} failed to send. ` + message, + 'var(--error)', + ); + } + } + if (success) { Popup.alert( - `Email to ${sanitizeText(member[1])} failed to send. ` + message, - 'var(--error)', + 'An email has been sent, notifying event host(s) of your absence', + 'var(--success)', ); } } - if (success) { - Popup.alert('Emails sent successfully!', 'var(--success)'); - } // manually change html since evo-calendar is broken when adding/removing or updating events const badge = document.createElement('span'); diff --git a/public/components/DataTable.js b/public/components/DataTable.js index 3685d84..deeb82c 100644 --- a/public/components/DataTable.js +++ b/public/components/DataTable.js @@ -352,17 +352,6 @@ export class DataTable extends Component { } } }; - - // let initialRows = this.rows.filter(row => - // row[header].toLowerCase().includes(search.value.toLowerCase()), - // ); - // search.oninput = () => { - // const searchValue = search.value; - // this.rows = initialRows.filter(row => - // row[header].toLowerCase().includes(searchValue.toLowerCase()), - // ); - // this.showPage(1); - // }; } /** diff --git a/public/index.html b/public/index.html index 0b918f4..b16ba79 100644 --- a/public/index.html +++ b/public/index.html @@ -165,7 +165,7 @@

Free Plan


Standard Plan


diff --git a/public/payment.html b/public/payment.html index 66c4940..5727408 100644 --- a/public/payment.html +++ b/public/payment.html @@ -59,7 +59,7 @@

Free Plan

>
  • Export/import custom data via CSV
  • -
  • Gmail invites and notifications
  • +
  • Email invites and notifications
  • diff --git a/server/Business.js b/server/Business.js index 63d5fce..144ab67 100644 --- a/server/Business.js +++ b/server/Business.js @@ -299,6 +299,43 @@ router.put('/businesses/:businessId/settings/requirejoin', async (request, respo response.sendStatus(changes === 1 ? 200 : 400); }); +/** + * Gets the absentEmail setting for the specified business. + * @pathParams businessId - id of the business to get the role in + * @requiredPrivileges read access to the specified business + * @response json object with the absentEmail setting of the specified business + */ +router.get('/businesses/:businessId/settings/absentemail', async (request, response) => { + const uid = await handleAuth(request, response, request.params.businessId, { read: true }); + if (!uid) return; + + const businessId = request.params.businessId; + + const absentEmail = await db().get( + ...SQL`SELECT absentEmail FROM Businesses WHERE id = ${businessId}`, + ); + response.send(absentEmail); +}); + +/** + * Updates the absentEmail setting for the specified business. + * @pathParams businessId - id of the business to update the role in + * @queryParams new - new absentEmail setting for the business + * @requiredPrivileges write access to the specified business + */ +router.put('/businesses/:businessId/settings/absentemail', async (request, response) => { + const uid = await handleAuth(request, response, request.params.businessId, { write: true }); + if (!uid) return; + + const businessId = request.params.businessId; + const newAbsentEmail = request.query.new; + + const { changes } = await db().run( + ...SQL`UPDATE Businesses SET absentEmail = ${newAbsentEmail} WHERE id = ${businessId}`, + ); + response.sendStatus(changes === 1 ? 200 : 400); +}); + /** * Sets the custom data for a user in the specified business. * @pathParams businessId - id of the business to set custom data in @@ -407,7 +444,7 @@ router.get('/businesses/:businessId/writemembers', async (request, response) => SELECT Users.name, Users.email FROM Members INNER JOIN Users on Members.user_id = Users.id WHERE Members.business_id = ${businessId} - AND Members.role = 'owner' OR Members.role = 'admin' OR Members.role = 'moderator'`, + AND (Members.role = 'owner' OR Members.role = 'admin' OR Members.role = 'moderator')`, ); response.send(rows); }); diff --git a/server/schema.sql b/server/schema.sql index 6c3b37c..20c6189 100644 --- a/server/schema.sql +++ b/server/schema.sql @@ -9,7 +9,8 @@ CREATE TABLE IF NOT EXISTS "Businesses" ( "name" TEXT NOT NULL, "joincode" TEXT NOT NULL UNIQUE, "subscriptionId" TEXT NOT NULL UNIQUE, - "requireJoin" INTEGER DEFAULT 0 NOT NULL + "requireJoin" INTEGER DEFAULT 0 NOT NULL, + "absentEmail" INTEGER DEFAULT 1 NOT NULL ); CREATE TABLE IF NOT EXISTS "Events" ( "id" INTEGER PRIMARY KEY, diff --git a/server/super_admin/settings.json b/server/super_admin/settings.json index 9774b8c..f5ece4b 100644 --- a/server/super_admin/settings.json +++ b/server/super_admin/settings.json @@ -418,6 +418,38 @@ "editview": { "show": true } + }, + { + "name": "absentemail", + "verbose": "absentemail", + "control": { + "text": true + }, + "type": "INTEGER", + "allowNull": true, + "defaultValue": "1", + "listview": { + "show": true + }, + "editview": { + "show": true + } + }, + { + "name": "absentEmail", + "verbose": "absentEmail", + "control": { + "text": true + }, + "type": "INTEGER", + "allowNull": false, + "defaultValue": "1", + "listview": { + "show": true + }, + "editview": { + "show": true + } } ], "mainview": {