development

- add latitude and longitude bounds
- add purchaser relationship suggestions
- clean up dashboard
- switch to native node testing
- linting
pull/3/head
Dan Gowans 2025-03-20 11:53:56 -04:00
parent 28fe138fb5
commit 0dbbd3f750
47 changed files with 1265 additions and 1209 deletions

View File

@ -1,5 +1,9 @@
import { config as baseConfig } from './config.base.js';
export const config = Object.assign({}, baseConfig);
config.settings.provinceDefault = 'ON';
config.settings.latitudeMax = 56.85;
config.settings.latitudeMin = 41.68;
config.settings.longitudeMax = -74;
config.settings.longitudeMin = -95.15;
config.settings.fees.taxPercentageDefault = 13;
export default config;

View File

@ -4,6 +4,12 @@ export const config = Object.assign({}, baseConfig)
config.settings.provinceDefault = 'ON'
config.settings.latitudeMax = 56.85
config.settings.latitudeMin = 41.68
config.settings.longitudeMax = -74
config.settings.longitudeMin = -95.15
config.settings.fees.taxPercentageDefault = 13
export default config

View File

@ -13,7 +13,7 @@ config.settings.burialSites.burialSiteNameSegments = {
maxLength: 1
},
2: {
isRequired: true,
isRequired: false,
isAvailable: true,
label: 'Range',
minLength: 1,
@ -36,6 +36,10 @@ config.settings.burialSites.burialSiteNameSegments = {
}
};
config.settings.cityDefault = 'Sault Ste. Marie';
config.settings.latitudeMax = 46.75;
config.settings.latitudeMin = 46.4;
config.settings.longitudeMax = -84.2;
config.settings.longitudeMin = -84.5;
config.settings.contracts.prints = [
'pdf/ssm.cemetery.burialPermit',
'pdf/ssm.cemetery.contract'

View File

@ -18,7 +18,7 @@ config.settings.burialSites.burialSiteNameSegments = {
maxLength: 1
},
2: {
isRequired: true,
isRequired: false,
isAvailable: true,
label: 'Range',
minLength: 1,
@ -43,6 +43,11 @@ config.settings.burialSites.burialSiteNameSegments = {
config.settings.cityDefault = 'Sault Ste. Marie'
config.settings.latitudeMax = 46.75
config.settings.latitudeMin = 46.4
config.settings.longitudeMax = -84.2
config.settings.longitudeMin = -84.5
config.settings.contracts.prints = [
'pdf/ssm.cemetery.burialPermit',
'pdf/ssm.cemetery.contract'

View File

@ -26,9 +26,15 @@ export declare const configDefaultValues: {
'aliases.workOrderCloseDate': string;
'settings.cityDefault': string;
'settings.provinceDefault': string;
'settings.latitudeMin': number;
'settings.latitudeMax': number;
'settings.longitudeMin': number;
'settings.longitudeMax': number;
'settings.burialSites.burialSiteNameSegments': ConfigBurialSiteNameSegments;
'settings.burialSites.burialSiteNameSegments.includeCemeteryKey': boolean;
'settings.contracts.burialSiteIdIsRequired': boolean;
'settings.contracts.contractEndDateIsRequired': boolean;
'settings.contracts.purchaserRelationships': string[];
'settings.contracts.deathAgePeriods': string[];
'settings.contracts.prints': string[];
'settings.fees.taxPercentageDefault': number;

View File

@ -4,7 +4,7 @@ export const configDefaultValues = {
'application.applicationName': 'Sunrise CMS',
'application.backgroundURL': '/images/cemetery-background.jpg',
'application.logoURL': '/images/sunrise-cms.svg',
'application.httpPort': 7000,
'application.httpPort': 9000,
'application.userDomain': '',
'application.useTestDatabases': false,
'application.maximumProcesses': 4,
@ -25,8 +25,13 @@ export const configDefaultValues = {
'aliases.workOrderCloseDate': 'Completion Date',
'settings.cityDefault': '',
'settings.provinceDefault': '',
'settings.latitudeMin': -90,
'settings.latitudeMax': 90,
'settings.longitudeMin': -180,
'settings.longitudeMax': 180,
'settings.burialSites.burialSiteNameSegments': {
separator: '-',
includeCemeteryKey: false,
segments: {
1: {
isRequired: true,
@ -37,9 +42,23 @@ export const configDefaultValues = {
}
}
},
'settings.burialSites.burialSiteNameSegments.includeCemeteryKey': false,
'settings.contracts.burialSiteIdIsRequired': true,
'settings.contracts.contractEndDateIsRequired': false,
'settings.contracts.deathAgePeriods': ['Years', 'Months', 'Days', 'Stillborn'],
'settings.contracts.purchaserRelationships': [
'Spouse',
'Child',
'Parent',
'Sibling',
'Friend',
'Self'
],
'settings.contracts.deathAgePeriods': [
'Years',
'Months',
'Days',
'Stillborn'
],
'settings.contracts.prints': ['screen/contract'],
'settings.fees.taxPercentageDefault': 0,
'settings.workOrders.workOrderNumberLength': 6,

View File

@ -14,7 +14,7 @@ export const configDefaultValues = {
'application.applicationName': 'Sunrise CMS',
'application.backgroundURL': '/images/cemetery-background.jpg',
'application.logoURL': '/images/sunrise-cms.svg',
'application.httpPort': 7000,
'application.httpPort': 9000,
'application.userDomain': '',
'application.useTestDatabases': false,
'application.maximumProcesses': 4,
@ -42,8 +42,14 @@ export const configDefaultValues = {
'settings.cityDefault': '',
'settings.provinceDefault': '',
'settings.latitudeMin': -90,
'settings.latitudeMax': 90,
'settings.longitudeMin': -180,
'settings.longitudeMax': 180,
'settings.burialSites.burialSiteNameSegments': {
separator: '-',
includeCemeteryKey: false,
segments: {
1: {
isRequired: true,
@ -55,10 +61,24 @@ export const configDefaultValues = {
}
} as unknown as ConfigBurialSiteNameSegments,
'settings.burialSites.burialSiteNameSegments.includeCemeteryKey': false,
'settings.contracts.burialSiteIdIsRequired': true,
'settings.contracts.contractEndDateIsRequired': false,
'settings.contracts.deathAgePeriods': ['Years', 'Months', 'Days', 'Stillborn'],
'settings.contracts.purchaserRelationships': [
'Spouse',
'Child',
'Parent',
'Sibling',
'Friend',
'Self'
],
'settings.contracts.deathAgePeriods': [
'Years',
'Months',
'Days',
'Stillborn'
],
'settings.contracts.prints': ['screen/contract'],
'settings.fees.taxPercentageDefault': 0,

View File

@ -1,16 +1,14 @@
import { dateIntegerToString, dateStringToInteger, dateToInteger, timeIntegerToString } from '@cityssm/utils-datetime';
import { acquireConnection } from './pool.js';
// eslint-disable-next-line complexity
export default async function getReportData(reportName, reportParameters = {}) {
let sql = '';
const sqlParameters = [];
switch (reportName) {
case 'cemeteries-all': {
sql = 'select * from Cemeteries';
break;
}
case 'cemeteries-formatted': {
sql = `select cemeteryName,
const simpleReports = {
'burialSiteComments-all': 'select * from BurialSiteComments',
'burialSiteFields-all': 'select * from BurialSiteFields',
'burialSites-all': 'select * from BurialSites',
'burialSiteStatuses-all': 'select * from BurialSiteStatuses',
'burialSiteTypeFields-all': 'select * from BurialSiteTypeFields',
'burialSiteTypes-all': 'select * from BurialSiteTypes',
'cemeteries-all': 'select * from Cemeteries',
'cemeteries-formatted': `select cemeteryName,
cemeteryDescription,
cemeteryAddress1, cemeteryAddress2,
cemeteryCity, cemeteryProvince,
@ -18,13 +16,39 @@ export default async function getReportData(reportName, reportParameters = {}) {
cemeteryPhoneNumber
from Cemeteries
where recordDelete_timeMillis is null
order by cemeteryName`;
break;
}
case 'burialSites-all': {
sql = 'select * from BurialSites';
break;
}
order by cemeteryName`,
'contractComments-all': 'select * from ContractComments',
'contractFees-all': 'select * from ContractFees',
'contractFields-all': 'select * from ContractFields',
'contractInterments-all': 'select * from ContractInterments',
'contracts-all': 'select * from Contracts',
'contractTransactions-all': 'select * from ContractTransactions',
'contractTypeFields-all': 'select * from ContractTypeFields',
'contractTypes-all': 'select * from ContractTypes',
'feeCategories-all': 'select * from FeeCategories',
'fees-all': 'select * from Fees',
'funeralHomes-all': 'select * from FuneralHomes',
'funeralHomes-formatted': `select funeralHomeName,
funeralHomeAddress1, funeralHomeAddress2,
funeralHomeCity, funeralHomeProvince,
funeralHomePostalCode,
funeralHomePhoneNumber
from FuneralHomes
where recordDelete_timeMillis is null`,
'intermentContainerTypes-all': 'select * from IntermentContainerTypes',
'workOrderBurialSites-all': 'select * from WorkOrderBurialSites',
'workOrderComments-all': 'select * from WorkOrderComments',
'workOrderMilestones-all': 'select * from WorkOrderMilestones',
'workOrderMilestoneTypes-all': 'select * from WorkOrderMilestoneTypes',
'workOrders-all': 'select * from WorkOrders',
'workOrderTypes-all': 'select * from WorkOrderTypes'
};
export default async function getReportData(reportName, reportParameters = {}) {
let sql = '';
const sqlParameters = [];
// eslint-disable-next-line security/detect-object-injection
if (simpleReports[reportName] === undefined) {
switch (reportName) {
case 'burialSites-byBurialSiteTypeId': {
sql = `select l.burialSiteId,
m.cemeteryName,
@ -70,18 +94,6 @@ export default async function getReportData(reportName, reportParameters = {}) {
sqlParameters.push(reportParameters.cemeteryId);
break;
}
case 'burialSiteComments-all': {
sql = 'select * from BurialSiteComments';
break;
}
case 'burialSiteFields-all': {
sql = 'select * from BurialSiteFields';
break;
}
case 'contracts-all': {
sql = 'select * from Contracts';
break;
}
case 'contracts-current-byCemeteryId': {
sql = `select o.contractId,
l.burialSiteName,
@ -99,24 +111,18 @@ export default async function getReportData(reportName, reportParameters = {}) {
sqlParameters.push(dateToInteger(new Date()), reportParameters.cemeteryId);
break;
}
case 'contractComments-all': {
sql = 'select * from ContractComments';
break;
}
case 'contractFees-all': {
sql = 'select * from ContractFees';
break;
}
case 'contractFields-all': {
sql = 'select * from ContractFields';
break;
}
case 'contractInterments-all': {
sql = 'select * from ContractInterments';
break;
}
case 'contractTransactions-all': {
sql = 'select * from ContractTransactions';
case 'contractInterments-byContractId': {
sql = `select i.contractId, i.intermentNumber,
i.deceasedName, i.deceasedAddress1, i.deceasedAddress2,
i.deceasedCity, i.deceasedProvince, i.deceasedPostalCode,
i.birthDate, i.birthPlace,
i.deathDate, i.deathPlace,
i.deathAge, i.deathAgePeriod
from ContractInterments i
left join IntermentContainerTypes t on i.intermentContainerTypeId = t.intermentContainerTypeId
where i.recordDelete_timeMillis is null
and i.contractId = ?`;
sqlParameters.push(reportParameters.contractId);
break;
}
case 'contractTransactions-byTransactionDateString': {
@ -130,10 +136,6 @@ export default async function getReportData(reportName, reportParameters = {}) {
sqlParameters.push(dateStringToInteger(reportParameters.transactionDateString));
break;
}
case 'workOrders-all': {
sql = 'select * from WorkOrders';
break;
}
case 'workOrders-open': {
sql = `select w.workOrderId, w.workOrderNumber,
t.workOrderType, w.workOrderDescription,
@ -153,18 +155,6 @@ export default async function getReportData(reportName, reportParameters = {}) {
and w.workOrderCloseDate is null`;
break;
}
case 'workOrderComments-all': {
sql = 'select * from WorkOrderComments';
break;
}
case 'workOrderBurialSites-all': {
sql = 'select * from WorkOrderBurialSites';
break;
}
case 'workOrderMilestones-all': {
sql = 'select * from WorkOrderMilestones';
break;
}
case 'workOrderMilestones-byWorkOrderId': {
sql = `select t.workOrderMilestoneType,
m.workOrderMilestoneDate,
@ -179,46 +169,14 @@ export default async function getReportData(reportName, reportParameters = {}) {
sqlParameters.push(reportParameters.workOrderId);
break;
}
case 'fees-all': {
sql = 'select * from Fees';
break;
}
case 'feeCategories-all': {
sql = 'select * from FeeCategories';
break;
}
case 'burialSiteTypes-all': {
sql = 'select * from BurialSiteTypes';
break;
}
case 'burialSiteTypeFields-all': {
sql = 'select * from BurialSiteTypeFields';
break;
}
case 'burialSiteStatuses-all': {
sql = 'select * from BurialSiteStatuses';
break;
}
case 'contractTypes-all': {
sql = 'select * from ContractTypes';
break;
}
case 'contractTypeFields-all': {
sql = 'select * from ContractTypeFields';
break;
}
case 'workOrderTypes-all': {
sql = 'select * from WorkOrderTypes';
break;
}
case 'workOrderMilestoneTypes-all': {
sql = 'select * from WorkOrderMilestoneTypes';
break;
}
default: {
return undefined;
}
}
}
else {
sql = simpleReports[reportName];
}
const database = await acquireConnection();
database.function('userFn_dateIntegerToString', dateIntegerToString);
database.function('userFn_timeIntegerToString', timeIntegerToString);

View File

@ -10,22 +10,16 @@ import { acquireConnection } from './pool.js'
export type ReportParameters = Record<string, string | number>
// eslint-disable-next-line complexity
export default async function getReportData(
reportName: string,
reportParameters: ReportParameters = {}
): Promise<unknown[] | undefined> {
let sql = ''
const sqlParameters: unknown[] = []
const simpleReports: Record<`${string}-all` | `${string}-formatted`, string> = {
'burialSiteComments-all': 'select * from BurialSiteComments',
'burialSiteFields-all': 'select * from BurialSiteFields',
'burialSites-all': 'select * from BurialSites',
'burialSiteStatuses-all': 'select * from BurialSiteStatuses',
'burialSiteTypeFields-all': 'select * from BurialSiteTypeFields',
'burialSiteTypes-all': 'select * from BurialSiteTypes',
switch (reportName) {
case 'cemeteries-all': {
sql = 'select * from Cemeteries'
break
}
case 'cemeteries-formatted': {
sql = `select cemeteryName,
'cemeteries-all': 'select * from Cemeteries',
'cemeteries-formatted': `select cemeteryName,
cemeteryDescription,
cemeteryAddress1, cemeteryAddress2,
cemeteryCity, cemeteryProvince,
@ -33,16 +27,47 @@ export default async function getReportData(
cemeteryPhoneNumber
from Cemeteries
where recordDelete_timeMillis is null
order by cemeteryName`
order by cemeteryName`,
break
'contractComments-all': 'select * from ContractComments',
'contractFees-all': 'select * from ContractFees',
'contractFields-all': 'select * from ContractFields',
'contractInterments-all': 'select * from ContractInterments',
'contracts-all': 'select * from Contracts',
'contractTransactions-all': 'select * from ContractTransactions',
'contractTypeFields-all': 'select * from ContractTypeFields',
'contractTypes-all': 'select * from ContractTypes',
'feeCategories-all': 'select * from FeeCategories',
'fees-all': 'select * from Fees',
'funeralHomes-all': 'select * from FuneralHomes',
'funeralHomes-formatted': `select funeralHomeName,
funeralHomeAddress1, funeralHomeAddress2,
funeralHomeCity, funeralHomeProvince,
funeralHomePostalCode,
funeralHomePhoneNumber
from FuneralHomes
where recordDelete_timeMillis is null`,
'intermentContainerTypes-all': 'select * from IntermentContainerTypes',
'workOrderBurialSites-all': 'select * from WorkOrderBurialSites',
'workOrderComments-all': 'select * from WorkOrderComments',
'workOrderMilestones-all': 'select * from WorkOrderMilestones',
'workOrderMilestoneTypes-all': 'select * from WorkOrderMilestoneTypes',
'workOrders-all': 'select * from WorkOrders',
'workOrderTypes-all': 'select * from WorkOrderTypes'
}
case 'burialSites-all': {
sql = 'select * from BurialSites'
break
}
export default async function getReportData(
reportName: string,
reportParameters: ReportParameters = {}
): Promise<unknown[] | undefined> {
let sql = ''
const sqlParameters: unknown[] = []
// eslint-disable-next-line security/detect-object-injection
if (simpleReports[reportName] === undefined) {
switch (reportName) {
case 'burialSites-byBurialSiteTypeId': {
sql = `select l.burialSiteId,
m.cemeteryName,
@ -97,21 +122,6 @@ export default async function getReportData(
break
}
case 'burialSiteComments-all': {
sql = 'select * from BurialSiteComments'
break
}
case 'burialSiteFields-all': {
sql = 'select * from BurialSiteFields'
break
}
case 'contracts-all': {
sql = 'select * from Contracts'
break
}
case 'contracts-current-byCemeteryId': {
sql = `select o.contractId,
l.burialSiteName,
@ -127,33 +137,28 @@ export default async function getReportData(
and (o.contractEndDate is null or o.contractEndDate >= ?)
and l.cemeteryId = ?`
sqlParameters.push(dateToInteger(new Date()), reportParameters.cemeteryId)
sqlParameters.push(
dateToInteger(new Date()),
reportParameters.cemeteryId
)
break
}
case 'contractComments-all': {
sql = 'select * from ContractComments'
break
}
case 'contractInterments-byContractId': {
sql = `select i.contractId, i.intermentNumber,
i.deceasedName, i.deceasedAddress1, i.deceasedAddress2,
i.deceasedCity, i.deceasedProvince, i.deceasedPostalCode,
i.birthDate, i.birthPlace,
i.deathDate, i.deathPlace,
i.deathAge, i.deathAgePeriod
from ContractInterments i
left join IntermentContainerTypes t on i.intermentContainerTypeId = t.intermentContainerTypeId
where i.recordDelete_timeMillis is null
and i.contractId = ?`
case 'contractFees-all': {
sql = 'select * from ContractFees'
break
}
sqlParameters.push(reportParameters.contractId)
case 'contractFields-all': {
sql = 'select * from ContractFields'
break
}
case 'contractInterments-all': {
sql = 'select * from ContractInterments'
break
}
case 'contractTransactions-all': {
sql = 'select * from ContractTransactions'
break
}
@ -174,11 +179,6 @@ export default async function getReportData(
break
}
case 'workOrders-all': {
sql = 'select * from WorkOrders'
break
}
case 'workOrders-open': {
sql = `select w.workOrderId, w.workOrderNumber,
t.workOrderType, w.workOrderDescription,
@ -196,21 +196,7 @@ export default async function getReportData(
) m on w.workOrderId = m.workOrderId
where w.recordDelete_timeMillis is null
and w.workOrderCloseDate is null`
break
}
case 'workOrderComments-all': {
sql = 'select * from WorkOrderComments'
break
}
case 'workOrderBurialSites-all': {
sql = 'select * from WorkOrderBurialSites'
break
}
case 'workOrderMilestones-all': {
sql = 'select * from WorkOrderMilestones'
break
}
@ -225,59 +211,18 @@ export default async function getReportData(
left join WorkOrderMilestoneTypes t on m.workOrderMilestoneTypeId = t.workOrderMilestoneTypeId
where m.recordDelete_timeMillis is null
and m.workOrderId = ?`
sqlParameters.push(reportParameters.workOrderId)
break
}
case 'fees-all': {
sql = 'select * from Fees'
break
}
case 'feeCategories-all': {
sql = 'select * from FeeCategories'
break
}
case 'burialSiteTypes-all': {
sql = 'select * from BurialSiteTypes'
break
}
case 'burialSiteTypeFields-all': {
sql = 'select * from BurialSiteTypeFields'
break
}
case 'burialSiteStatuses-all': {
sql = 'select * from BurialSiteStatuses'
break
}
case 'contractTypes-all': {
sql = 'select * from ContractTypes'
break
}
case 'contractTypeFields-all': {
sql = 'select * from ContractTypeFields'
break
}
case 'workOrderTypes-all': {
sql = 'select * from WorkOrderTypes'
break
}
case 'workOrderMilestoneTypes-all': {
sql = 'select * from WorkOrderMilestoneTypes'
break
}
default: {
return undefined
}
}
} else {
sql = simpleReports[reportName]
}
const database = await acquireConnection()

7
docs/README.md 100644
View File

@ -0,0 +1,7 @@
[Home](https://cityssm.github.io/sunrise-csm/)
# Help Documentation
**Thank you for taking the time to read the documentation.**
**Coming soon.**

View File

@ -9,7 +9,7 @@ export default async function handler(request, response) {
}
const contractTypePrints = await getContractTypePrintsById(contract.contractTypeId);
response.render('contract-view', {
headTitle: 'Contract View',
headTitle: `Contract #${contract.contractId.toString()}`,
contract,
contractTypePrints
});

View File

@ -26,7 +26,7 @@ export default async function handler(
)
response.render('contract-view', {
headTitle: 'Contract View',
headTitle: `Contract #${contract.contractId.toString()}`,
contract,
contractTypePrints
})

View File

@ -1,7 +1,5 @@
import { dateToString } from '@cityssm/utils-datetime';
import getContracts from '../../database/getContracts.js';
import getWorkOrderMilestones from '../../database/getWorkOrderMilestones.js';
import { getWorkOrders } from '../../database/getWorkOrders.js';
export default async function handler(_request, response) {
const currentDateString = dateToString(new Date());
const workOrderMilestones = await getWorkOrderMilestones({
@ -11,25 +9,8 @@ export default async function handler(_request, response) {
orderBy: 'completion',
includeWorkOrders: true
});
const workOrderResults = await getWorkOrders({
workOrderOpenDateString: currentDateString
}, {
limit: 1, // only using the count
offset: 0
});
const contractResults = await getContracts({
contractStartDateString: currentDateString
}, {
limit: 1, // only using the count
offset: 0,
includeFees: false,
includeInterments: false,
includeTransactions: false
});
response.render('dashboard', {
headTitle: 'Dashboard',
workOrderMilestones,
workOrderCount: workOrderResults.count,
contractCount: contractResults.count
workOrderMilestones
});
}

View File

@ -1,9 +1,7 @@
import { dateToString } from '@cityssm/utils-datetime'
import type { Request, Response } from 'express'
import getContracts from '../../database/getContracts.js'
import getWorkOrderMilestones from '../../database/getWorkOrderMilestones.js'
import { getWorkOrders } from '../../database/getWorkOrders.js'
export default async function handler(
_request: Request,
@ -22,33 +20,8 @@ export default async function handler(
}
)
const workOrderResults = await getWorkOrders(
{
workOrderOpenDateString: currentDateString
},
{
limit: 1, // only using the count
offset: 0
}
)
const contractResults = await getContracts(
{
contractStartDateString: currentDateString
},
{
limit: 1, // only using the count
offset: 0,
includeFees: false,
includeInterments: false,
includeTransactions: false
}
)
response.render('dashboard', {
headTitle: 'Dashboard',
workOrderMilestones,
workOrderCount: workOrderResults.count,
contractCount: contractResults.count
workOrderMilestones
})
}

View File

@ -31,7 +31,9 @@ function cacheBurialSiteIds(burialSiteId, nextBurialSiteId, relayMessage = true)
process.send(workerMessage);
}
}
catch { }
catch {
// Ignore
}
}
export async function getNextBurialSiteId(burialSiteId) {
let nextBurialSiteId = nextBurialSiteIdCache.get(burialSiteId);
@ -82,7 +84,9 @@ export function clearNextPreviousBurialSiteIdCache(burialSiteId = -1, relayMessa
process.send(workerMessage);
}
}
catch { }
catch {
// Ignore
}
}
const segmentConfig = getConfigProperty('settings.burialSites.burialSiteNameSegments');
export function buildBurialSiteName(cemeteryKey, segments) {

View File

@ -52,7 +52,9 @@ function cacheBurialSiteIds(
process.send(workerMessage)
}
} catch {}
} catch {
// Ignore
}
}
export async function getNextBurialSiteId(
@ -131,7 +133,9 @@ export function clearNextPreviousBurialSiteIdCache(
process.send(workerMessage)
}
} catch {}
} catch {
// Ignore
}
}
const segmentConfig = getConfigProperty(

14
package-lock.json generated
View File

@ -59,14 +59,12 @@
"@types/express-session": "^1.18.1",
"@types/http-errors": "^2.0.4",
"@types/leaflet": "^1.9.16",
"@types/mocha": "^10.0.10",
"@types/mssql": "^9.1.7",
"@types/node-windows": "^0.1.6",
"@types/papaparse": "^5.3.15",
"@types/randomcolor": "^0.5.9",
"@types/session-file-store": "^1.2.5",
"axe-core": "^4.10.3",
"bulma": "^1.0.3",
"cypress": "^14.2.0",
"cypress-axe": "^1.6.0",
"eslint-config-cityssm": "^20.0.0",
@ -1831,8 +1829,9 @@
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
"integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@types/ms": {
"version": "0.7.31",
@ -3081,13 +3080,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bulma": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.3.tgz",
"integrity": "sha512-9eVXBrXwlU337XUXBjIIq7i88A+tRbJYAjXQjT/21lwam+5tpvKF0R7dCesre9N+HV9c6pzCNEPKrtgvBBes2g==",
"dev": true,
"license": "MIT"
},
"node_modules/bulma-tooltip": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/bulma-tooltip/-/bulma-tooltip-3.0.2.tgz",

View File

@ -15,9 +15,9 @@
"cy:open": "cypress open --config-file cypress.config.js",
"cy:run": "cypress run --config-file cypress.config.js",
"cy:run:firefox": "cypress run --config-file cypress.config.js --browser firefox",
"test": "cross-env NODE_ENV=dev DEBUG=sunrise:* TEST_DATABASES=true mocha --timeout 30000 --exit",
"test": "cross-env NODE_ENV=dev DEBUG=sunrise:* TEST_DATABASES=true node --test",
"test:startup": "cross-env NODE_ENV=dev DEBUG=sunrise:* TEST_DATABASES=true STARTUP_TEST=true node ./bin/www.js",
"coverage": "cross-env NODE_ENV=dev DEBUG=sunrise:* TEST_DATABASES=true c8 --reporter=lcov --reporter=text --reporter=text-summary mocha --timeout 30000 --exit",
"coverage": "cross-env NODE_ENV=dev DEBUG=sunrise:* TEST_DATABASES=true c8 --reporter=lcov --reporter=text --reporter=text-summary node --test",
"temp:legacyImportFromCsv": "cross-env NODE_ENV=dev DEBUG=sunrise:* TEST_DATABASES=true node ./temp/legacyImportFromCsv/index.js"
},
"repository": {
@ -82,14 +82,12 @@
"@types/express-session": "^1.18.1",
"@types/http-errors": "^2.0.4",
"@types/leaflet": "^1.9.16",
"@types/mocha": "^10.0.10",
"@types/mssql": "^9.1.7",
"@types/node-windows": "^0.1.6",
"@types/papaparse": "^5.3.15",
"@types/randomcolor": "^0.5.9",
"@types/session-file-store": "^1.2.5",
"axe-core": "^4.10.3",
"bulma": "^1.0.3",
"cypress": "^14.2.0",
"cypress-axe": "^1.6.0",
"eslint-config-cityssm": "^20.0.0",

View File

@ -7,6 +7,9 @@
id="svg3"
sodipodi:docname="sunrise-cms.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
inkscape:export-filename="sunrise-cms.png"
inkscape:export-xdpi="2.6444559"
inkscape:export-ydpi="2.6444559"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@ -23,15 +26,16 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.44790474"
inkscape:cx="10007.708"
inkscape:cy="6998.1399"
inkscape:zoom="0.07917912"
inkscape:cx="4868.7078"
inkscape:cy="10880.394"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg3" />
inkscape:current-layer="svg3"
showgrid="false" />
<path
fill="#16272f"
d="M95 3826c-44-23-66-45-81-81l-14-32v-216l11-39c6-22 22-57 35-78l24-38 41-29 40-30 68-17 67-18 2-520 2-521 12-50c27-122 67-220 131-315l29-45 82-80 82-80 89-47 90-46 90-29 90-28h350l90 28 90 29 74 39c164 88 273 202 360 377l41 81 20 86 20 86v1036l168 6 167 5 43 16 44 16 38 30 39 31 30 61 31 61v237l-15.922 78.571-30.507 50.95L1530 3842H125Z"

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -213,8 +213,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
cityssm.openHtmlModal('burialSite-editComment', {
onshow(modalElement) {
sunrise.populateAliases(modalElement);
modalElement.querySelector('#burialSiteCommentEdit--burialSiteId').value = burialSiteId;
modalElement.querySelector('#burialSiteCommentEdit--burialSiteCommentId').value = burialSiteCommentId.toString();
modalElement
.querySelector('#burialSiteCommentEdit--burialSiteId')
?.setAttribute('value', burialSiteId);
modalElement
.querySelector('#burialSiteCommentEdit--burialSiteCommentId')
?.setAttribute('value', burialSiteCommentId.toString());
modalElement.querySelector('#burialSiteCommentEdit--comment').value = burialSiteComment.comment ?? '';
const commentDateStringElement = modalElement.querySelector('#burialSiteCommentEdit--commentDateString');
commentDateStringElement.value =
@ -339,7 +343,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
cityssm.openHtmlModal('burialSite-addComment', {
onshow(modalElement) {
sunrise.populateAliases(modalElement);
modalElement.querySelector('#burialSiteCommentAdd--burialSiteId').value = burialSiteId;
modalElement
.querySelector('#burialSiteCommentAdd--burialSiteId')
?.setAttribute('value', burialSiteId);
modalElement
.querySelector('form')
?.addEventListener('submit', doAddComment);

View File

@ -333,20 +333,18 @@ declare const exports: Record<string, unknown>
cityssm.openHtmlModal('burialSite-editComment', {
onshow(modalElement) {
sunrise.populateAliases(modalElement)
;(
modalElement.querySelector(
'#burialSiteCommentEdit--burialSiteId'
) as HTMLInputElement
).value = burialSiteId
;(
modalElement.querySelector(
'#burialSiteCommentEdit--burialSiteCommentId'
) as HTMLInputElement
).value = burialSiteCommentId.toString()
modalElement
.querySelector('#burialSiteCommentEdit--burialSiteId')
?.setAttribute('value', burialSiteId)
modalElement
.querySelector('#burialSiteCommentEdit--burialSiteCommentId')
?.setAttribute('value', burialSiteCommentId.toString())
;(
modalElement.querySelector(
'#burialSiteCommentEdit--comment'
) as HTMLInputElement
) as HTMLTextAreaElement
).value = burialSiteComment.comment ?? ''
const commentDateStringElement = modalElement.querySelector(
@ -526,11 +524,11 @@ declare const exports: Record<string, unknown>
cityssm.openHtmlModal('burialSite-addComment', {
onshow(modalElement) {
sunrise.populateAliases(modalElement)
;(
modalElement.querySelector(
'#burialSiteCommentAdd--burialSiteId'
) as HTMLInputElement
).value = burialSiteId
modalElement
.querySelector('#burialSiteCommentAdd--burialSiteId')
?.setAttribute('value', burialSiteId)
modalElement
.querySelector('form')
?.addEventListener('submit', doAddComment)

View File

@ -9,15 +9,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
const contractCommentId = Number.parseInt(clickEvent.currentTarget.closest('tr')?.dataset
.contractCommentId ?? '', 10);
const contractComment = contractComments.find((currentComment) => currentComment.contractCommentId === contractCommentId);
let editFormElement;
let editCloseModalFunction;
let editFormElement = undefined;
let editCloseModalFunction = undefined;
function editContractComment(submitEvent) {
submitEvent.preventDefault();
cityssm.postJSON(`${sunrise.urlPrefix}/contracts/doUpdateContractComment`, editFormElement, (rawResponseJSON) => {
const responseJSON = rawResponseJSON;
if (responseJSON.success) {
contractComments = responseJSON.contractComments ?? [];
if (editCloseModalFunction !== undefined) {
editCloseModalFunction();
}
renderContractComments();
}
else {
@ -32,18 +34,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
cityssm.openHtmlModal('contract-editComment', {
onshow(modalElement) {
sunrise.populateAliases(modalElement);
modalElement.querySelector('#contractCommentEdit--contractId').value = contractId;
modalElement.querySelector('#contractCommentEdit--contractCommentId').value = contractCommentId.toString();
modalElement.querySelector('#contractCommentEdit--comment').value = contractComment.comment ?? '';
modalElement
.querySelector('#contractCommentEdit--contractId')
?.setAttribute('value', contractId);
modalElement
.querySelector('#contractCommentEdit--contractCommentId')
?.setAttribute('value', contractCommentId.toString());
modalElement.querySelector('#contractCommentEdit--comment').value = contractComment.comment;
const contractCommentDateStringElement = modalElement.querySelector('#contractCommentEdit--commentDateString');
contractCommentDateStringElement.value =
contractComment.commentDateString ?? '';
contractComment.commentDateString;
const currentDateString = cityssm.dateToString(new Date());
contractCommentDateStringElement.max =
// eslint-disable-next-line unicorn/prefer-math-min-max
contractComment.commentDateString <= currentDateString
? currentDateString
: contractComment.commentDateString ?? '';
modalElement.querySelector('#contractCommentEdit--commentTimeString').value = contractComment.commentTimeString ?? '';
: contractComment.commentDateString;
modalElement.querySelector('#contractCommentEdit--commentTimeString').value = contractComment.commentTimeString;
},
onshown(modalElement, closeModalFunction) {
bulmaJS.toggleHtmlClipped();
@ -109,15 +116,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
for (const contractComment of contractComments) {
const tableRowElement = document.createElement('tr');
tableRowElement.dataset.contractCommentId =
contractComment.contractCommentId?.toString();
contractComment.contractCommentId.toString();
tableRowElement.innerHTML = `<td>${cityssm.escapeHTML(contractComment.recordCreate_userName ?? '')}</td>
<td>
${cityssm.escapeHTML(contractComment.commentDateString ?? '')}
${cityssm.escapeHTML(contractComment.commentDateString)}
${cityssm.escapeHTML(contractComment.commentTime === 0
? ''
: contractComment.commentTimePeriodString ?? '')}
: contractComment.commentTimePeriodString)}
</td>
<td>${cityssm.escapeHTML(contractComment.comment ?? '')}</td>
<td>${cityssm.escapeHTML(contractComment.comment)}</td>
<td class="is-hidden-print">
<div class="buttons are-small is-justify-content-end">
<button class="button is-primary button--edit" type="button">
@ -151,7 +158,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
const responseJSON = rawResponseJSON;
if (responseJSON.success) {
contractComments = responseJSON.contractComments;
if (addCloseModalFunction !== undefined) {
addCloseModalFunction();
}
renderContractComments();
}
else {

View File

@ -29,8 +29,8 @@ declare const exports: Record<string, unknown>
(currentComment) => currentComment.contractCommentId === contractCommentId
) as ContractComment
let editFormElement: HTMLFormElement
let editCloseModalFunction: () => void
let editFormElement: HTMLFormElement | undefined = undefined
let editCloseModalFunction: (() => void) | undefined = undefined
function editContractComment(submitEvent: SubmitEvent): void {
submitEvent.preventDefault()
@ -47,7 +47,11 @@ declare const exports: Record<string, unknown>
if (responseJSON.success) {
contractComments = responseJSON.contractComments ?? []
if (editCloseModalFunction !== undefined) {
editCloseModalFunction()
}
renderContractComments()
} else {
bulmaJS.alert({
@ -63,44 +67,42 @@ declare const exports: Record<string, unknown>
cityssm.openHtmlModal('contract-editComment', {
onshow(modalElement) {
sunrise.populateAliases(modalElement)
;(
modalElement.querySelector(
'#contractCommentEdit--contractId'
) as HTMLInputElement
).value = contractId
;(
modalElement.querySelector(
'#contractCommentEdit--contractCommentId'
) as HTMLInputElement
).value = contractCommentId.toString()
modalElement
.querySelector('#contractCommentEdit--contractId')
?.setAttribute('value', contractId)
modalElement
.querySelector('#contractCommentEdit--contractCommentId')
?.setAttribute('value', contractCommentId.toString())
;(
modalElement.querySelector(
'#contractCommentEdit--comment'
) as HTMLInputElement
).value = contractComment.comment ?? ''
) as HTMLTextAreaElement
).value = contractComment.comment
const contractCommentDateStringElement = modalElement.querySelector(
'#contractCommentEdit--commentDateString'
) as HTMLInputElement
contractCommentDateStringElement.value =
contractComment.commentDateString ?? ''
contractComment.commentDateString
const currentDateString = cityssm.dateToString(new Date())
contractCommentDateStringElement.max =
contractComment.commentDateString! <= currentDateString
// eslint-disable-next-line unicorn/prefer-math-min-max
contractComment.commentDateString <= currentDateString
? currentDateString
: contractComment.commentDateString ?? ''
: contractComment.commentDateString
;(
modalElement.querySelector(
'#contractCommentEdit--commentTimeString'
) as HTMLInputElement
).value = contractComment.commentTimeString ?? ''
).value = contractComment.commentTimeString
},
onshown(modalElement, closeModalFunction) {
bulmaJS.toggleHtmlClipped()
;(
modalElement.querySelector(
'#contractCommentEdit--comment'
@ -190,18 +192,18 @@ declare const exports: Record<string, unknown>
for (const contractComment of contractComments) {
const tableRowElement = document.createElement('tr')
tableRowElement.dataset.contractCommentId =
contractComment.contractCommentId?.toString()
contractComment.contractCommentId.toString()
tableRowElement.innerHTML = `<td>${cityssm.escapeHTML(contractComment.recordCreate_userName ?? '')}</td>
<td>
${cityssm.escapeHTML(contractComment.commentDateString ?? '')}
${cityssm.escapeHTML(contractComment.commentDateString)}
${cityssm.escapeHTML(
contractComment.commentTime === 0
? ''
: contractComment.commentTimePeriodString ?? ''
: contractComment.commentTimePeriodString
)}
</td>
<td>${cityssm.escapeHTML(contractComment.comment ?? '')}</td>
<td>${cityssm.escapeHTML(contractComment.comment)}</td>
<td class="is-hidden-print">
<div class="buttons are-small is-justify-content-end">
<button class="button is-primary button--edit" type="button">
@ -232,8 +234,8 @@ declare const exports: Record<string, unknown>
document
.querySelector('#button--addComment')
?.addEventListener('click', () => {
let addFormElement: HTMLFormElement
let addCloseModalFunction: () => void
let addFormElement: HTMLFormElement | undefined
let addCloseModalFunction: (() => void) | undefined
function addComment(submitEvent: SubmitEvent): void {
submitEvent.preventDefault()
@ -250,7 +252,11 @@ declare const exports: Record<string, unknown>
if (responseJSON.success) {
contractComments = responseJSON.contractComments
if (addCloseModalFunction !== undefined) {
addCloseModalFunction()
}
renderContractComments()
} else {
bulmaJS.alert({

View File

@ -497,7 +497,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
function doDelete() {
cityssm.postJSON(`${sunrise.urlPrefix}/admin/doDeleteFee`, {
feeId
}, (rawResponseJSON) => {
},
// eslint-disable-next-line sonarjs/no-nested-functions
(rawResponseJSON) => {
const responseJSON = rawResponseJSON;
if (responseJSON.success) {
feeCategories = responseJSON.feeCategories;

View File

@ -791,6 +791,7 @@ declare const exports: Record<string, unknown>
{
feeId
},
// eslint-disable-next-line sonarjs/no-nested-functions
(rawResponseJSON) => {
const responseJSON = rawResponseJSON as ResponseJSON

View File

@ -310,16 +310,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
tableRowElement
.querySelector('form')
?.addEventListener('submit', updateWorkOrderMilestoneType);
tableRowElement.querySelector('.button--moveWorkOrderMilestoneTypeUp').addEventListener('click', moveWorkOrderMilestoneType);
tableRowElement.querySelector('.button--moveWorkOrderMilestoneTypeDown').addEventListener('click', moveWorkOrderMilestoneType);
tableRowElement
.querySelector('.button--moveWorkOrderMilestoneTypeUp')
?.addEventListener('click', moveWorkOrderMilestoneType);
tableRowElement
.querySelector('.button--moveWorkOrderMilestoneTypeDown')
?.addEventListener('click', moveWorkOrderMilestoneType);
tableRowElement
.querySelector('.button--deleteWorkOrderMilestoneType')
?.addEventListener('click', deleteWorkOrderMilestoneType);
containerElement.append(tableRowElement);
}
}
;
document.querySelector('#form--addWorkOrderMilestoneType').addEventListener('submit', (submitEvent) => {
document
.querySelector('#form--addWorkOrderMilestoneType')
?.addEventListener('submit', (submitEvent) => {
submitEvent.preventDefault();
const formElement = submitEvent.currentTarget;
cityssm.postJSON(`${sunrise.urlPrefix}/admin/doAddWorkOrderMilestoneType`, formElement, (rawResponseJSON) => {

View File

@ -457,16 +457,14 @@ declare const bulmaJS: BulmaJS
tableRowElement
.querySelector('form')
?.addEventListener('submit', updateWorkOrderMilestoneType)
;(
tableRowElement.querySelector(
'.button--moveWorkOrderMilestoneTypeUp'
) as HTMLButtonElement
).addEventListener('click', moveWorkOrderMilestoneType)
;(
tableRowElement.querySelector(
'.button--moveWorkOrderMilestoneTypeDown'
) as HTMLButtonElement
).addEventListener('click', moveWorkOrderMilestoneType)
tableRowElement
.querySelector('.button--moveWorkOrderMilestoneTypeUp')
?.addEventListener('click', moveWorkOrderMilestoneType)
tableRowElement
.querySelector('.button--moveWorkOrderMilestoneTypeDown')
?.addEventListener('click', moveWorkOrderMilestoneType)
tableRowElement
.querySelector('.button--deleteWorkOrderMilestoneType')
@ -475,11 +473,10 @@ declare const bulmaJS: BulmaJS
containerElement.append(tableRowElement)
}
}
;(
document.querySelector(
'#form--addWorkOrderMilestoneType'
) as HTMLFormElement
).addEventListener('submit', (submitEvent: SubmitEvent) => {
document
.querySelector('#form--addWorkOrderMilestoneType')
?.addEventListener('submit', (submitEvent: SubmitEvent) => {
submitEvent.preventDefault()
const formElement = submitEvent.currentTarget as HTMLFormElement

View File

@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
function doLogout() {
const urlPrefix = document.querySelector('main')?.getAttribute('data-url-prefix') ?? '';
globalThis.localStorage.clear();
globalThis.location.href = urlPrefix + '/logout';
globalThis.location.href = `${urlPrefix}/logout`;
}
document
.querySelector('#cityssm-theme--logout-button')
@ -27,7 +27,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
.querySelector('main')
?.getAttribute('data-session-keep-alive-millis');
function doKeepAlive() {
cityssm.postJSON(urlPrefix + '/keepAlive', {
cityssm.postJSON(`${urlPrefix}/keepAlive`, {
t: Date.now()
}, () => {
// Do nothing

View File

@ -13,7 +13,7 @@ declare const bulmaJS: BulmaJS
document.querySelector('main')?.getAttribute('data-url-prefix') ?? ''
globalThis.localStorage.clear()
globalThis.location.href = urlPrefix + '/logout'
globalThis.location.href = `${urlPrefix}/logout`
}
document
@ -46,7 +46,7 @@ declare const bulmaJS: BulmaJS
function doKeepAlive(): void {
cityssm.postJSON(
urlPrefix + '/keepAlive',
`${urlPrefix}/keepAlive`,
{
t: Date.now()
},

View File

@ -1,10 +1,11 @@
/* eslint-disable unicorn/filename-case, @eslint-community/eslint-comments/disable-enable-pair */
import assert from 'node:assert';
import fs from 'node:fs/promises';
import { describe, it } from 'node:test';
import { initializeDatabase } from '../database/initializeDatabase.js';
import { sunriseDB as databasePath, useTestDatabases } from '../helpers/database.helpers.js';
describe('Initialize Database', () => {
it('initializes the database', async () => {
await describe('Initialize Database', async () => {
await it('initializes the database', async () => {
if (!useTestDatabases) {
assert.fail('Test database must be used!');
}

View File

@ -2,6 +2,7 @@
import assert from 'node:assert'
import fs from 'node:fs/promises'
import { describe, it } from 'node:test'
import { initializeDatabase } from '../database/initializeDatabase.js'
import {
@ -9,8 +10,8 @@ import {
useTestDatabases
} from '../helpers/database.helpers.js'
describe('Initialize Database', () => {
it('initializes the database', async () => {
await describe('Initialize Database', async () => {
await it('initializes the database', async () => {
if (!useTestDatabases) {
assert.fail('Test database must be used!')
}

View File

@ -1,8 +1,9 @@
/* eslint-disable unicorn/filename-case, @eslint-community/eslint-comments/disable-enable-pair */
/* eslint-disable no-console, unicorn/filename-case, @eslint-community/eslint-comments/disable-enable-pair */
import assert from 'node:assert';
import { exec } from 'node:child_process';
import http from 'node:http';
import { minutesToMillis } from '@cityssm/to-millis';
import { after, before, describe, it } from 'node:test';
import { hoursToMillis } from '@cityssm/to-millis';
import { app } from '../app.js';
import { portNumber } from './_globals.js';
function runCypress(browser, done) {
@ -23,7 +24,7 @@ function runCypress(browser, done) {
done();
});
}
describe('sunrise-cms', () => {
await describe('sunrise-cms', async () => {
const httpServer = http.createServer(app);
let serverStarted = false;
before(() => {
@ -40,15 +41,19 @@ describe('sunrise-cms', () => {
// ignore
}
});
it(`Ensure server starts on port ${portNumber.toString()}`, () => {
await it(`Ensure server starts on port ${portNumber.toString()}`, () => {
assert.ok(serverStarted);
});
describe('Cypress tests', () => {
it('Should run Cypress tests in Chrome', (done) => {
await describe('Cypress tests', async () => {
await it('Should run Cypress tests in Chrome', {
timeout: hoursToMillis(1)
}, (context, done) => {
runCypress('chrome', done);
}).timeout(minutesToMillis(30));
it('Should run Cypress tests in Firefox', (done) => {
});
await it('Should run Cypress tests in Firefox', {
timeout: hoursToMillis(1)
}, (context, done) => {
runCypress('firefox', done);
}).timeout(minutesToMillis(30));
});
});
});

View File

@ -1,10 +1,11 @@
/* eslint-disable unicorn/filename-case, @eslint-community/eslint-comments/disable-enable-pair */
/* eslint-disable no-console, unicorn/filename-case, @eslint-community/eslint-comments/disable-enable-pair */
import assert from 'node:assert'
import { exec } from 'node:child_process'
import http from 'node:http'
import { after, before, describe, it } from 'node:test'
import { minutesToMillis } from '@cityssm/to-millis'
import { hoursToMillis } from '@cityssm/to-millis'
import { app } from '../app.js'
@ -34,7 +35,7 @@ function runCypress(browser: 'chrome' | 'firefox', done: () => void): void {
})
}
describe('sunrise-cms', () => {
await describe('sunrise-cms', async () => {
const httpServer = http.createServer(app)
let serverStarted = false
@ -55,17 +56,29 @@ describe('sunrise-cms', () => {
}
})
it(`Ensure server starts on port ${portNumber.toString()}`, () => {
await it(`Ensure server starts on port ${portNumber.toString()}`, () => {
assert.ok(serverStarted)
})
describe('Cypress tests', () => {
it('Should run Cypress tests in Chrome', (done) => {
await describe('Cypress tests', async () => {
await it(
'Should run Cypress tests in Chrome',
{
timeout: hoursToMillis(1)
},
(context, done) => {
runCypress('chrome', done)
}).timeout(minutesToMillis(30))
}
)
it('Should run Cypress tests in Firefox', (done) => {
await it(
'Should run Cypress tests in Firefox',
{
timeout: hoursToMillis(1)
},
(context, done) => {
runCypress('firefox', done)
}).timeout(minutesToMillis(30))
}
)
})
})

View File

@ -1,20 +1,18 @@
import assert from 'node:assert';
import fs from 'node:fs';
// skipcq: JS-C1003 - Testing functions
import { before, describe, it } from 'node:test';
import * as cacheFunctions from '../helpers/functions.cache.js';
// skipcq: JS-C1003 - Testing functions
import * as sqlFilterFunctions from '../helpers/functions.sqlFilters.js';
// skipcq: JS-C1003 - Testing functions
import * as userFunctions from '../helpers/functions.user.js';
describe('functions.cache', () => {
await describe('functions.cache', async () => {
const badId = -3;
// eslint-disable-next-line no-secrets/no-secrets
const badName = 'qwertyuiopasdfghjklzxcvbnm';
before(() => {
cacheFunctions.clearCaches();
});
describe('Burial Site Statuses', () => {
it('returns Burial Site Statuses', async () => {
await describe('Burial Site Statuses', async () => {
await it('returns Burial Site Statuses', async () => {
cacheFunctions.clearCacheByTableName('BurialSiteStatuses');
const burialSiteStatuses = await cacheFunctions.getBurialSiteStatuses();
assert.ok(burialSiteStatuses.length > 0);
@ -25,17 +23,17 @@ describe('functions.cache', () => {
assert.strictEqual(burialSiteStatus.burialSiteStatus, byName?.burialSiteStatus);
}
});
it('returns undefined with a bad burialSiteStatusId', async () => {
await it('returns undefined with a bad burialSiteStatusId', async () => {
const byBadId = await cacheFunctions.getBurialSiteStatusById(badId);
assert.ok(byBadId === undefined);
});
it('returns undefined with a bad lotStatus', async () => {
await it('returns undefined with a bad lotStatus', async () => {
const byBadName = await cacheFunctions.getBurialSiteStatusByBurialSiteStatus(badName);
assert.ok(byBadName === undefined);
});
});
describe('Lot Types', () => {
it('returns Lot Types', async () => {
await describe('Burial Site Types', async () => {
await it('returns Burial Site Types', async () => {
cacheFunctions.clearCacheByTableName('BurialSiteTypes');
const burialSiteTypes = await cacheFunctions.getBurialSiteTypes();
assert.ok(burialSiteTypes.length > 0);
@ -46,17 +44,17 @@ describe('functions.cache', () => {
assert.strictEqual(burialSiteType.burialSiteType, byName?.burialSiteType);
}
});
it('returns undefined with a bad burialSiteTypeId', async () => {
await it('returns undefined with a bad burialSiteTypeId', async () => {
const byBadId = await cacheFunctions.getBurialSiteTypeById(badId);
assert.ok(byBadId === undefined);
});
it('returns undefined with a bad lotType', async () => {
await it('returns undefined with a bad lotType', async () => {
const byBadName = await cacheFunctions.getBurialSiteTypesByBurialSiteType(badName);
assert.ok(byBadName === undefined);
});
});
describe('Occupancy Types', () => {
it('returns Contract Types', async () => {
await describe('Contract Types', async () => {
await it('returns Contract Types', async () => {
cacheFunctions.clearCacheByTableName('ContractTypes');
const contractTypes = await cacheFunctions.getContractTypes();
assert.ok(contractTypes.length > 0);
@ -67,17 +65,17 @@ describe('functions.cache', () => {
assert.strictEqual(contractType.contractType, byName?.contractType);
}
});
it('returns undefined with a bad contractTypeId', async () => {
await it('returns undefined with a bad contractTypeId', async () => {
const byBadId = await cacheFunctions.getContractTypeById(badId);
assert.ok(byBadId === undefined);
});
it('returns undefined with a bad contractType', async () => {
await it('returns undefined with a bad contractType', async () => {
const byBadName = await cacheFunctions.getContractTypeByContractType(badName);
assert.ok(byBadName === undefined);
});
});
describe('Work Order Types', () => {
it('returns Work Order Types', async () => {
await describe('Work Order Types', async () => {
await it('returns Work Order Types', async () => {
cacheFunctions.clearCacheByTableName('WorkOrderTypes');
const workOrderTypes = await cacheFunctions.getWorkOrderTypes();
assert.ok(workOrderTypes.length > 0);
@ -86,13 +84,13 @@ describe('functions.cache', () => {
assert.strictEqual(workOrderType.workOrderTypeId, byId?.workOrderTypeId);
}
});
it('returns undefined with a bad workOrderTypeId', async () => {
await it('returns undefined with a bad workOrderTypeId', async () => {
const byBadId = await cacheFunctions.getWorkOrderTypeById(badId);
assert.ok(byBadId === undefined);
});
});
describe('Work Order Milestone Types', () => {
it('returns Work Order Milestone Types', async () => {
await describe('Work Order Milestone Types', async () => {
await it('returns Work Order Milestone Types', async () => {
cacheFunctions.clearCacheByTableName('WorkOrderMilestoneTypes');
const workOrderMilestoneTypes = await cacheFunctions.getWorkOrderMilestoneTypes();
assert.ok(workOrderMilestoneTypes.length > 0);
@ -103,49 +101,49 @@ describe('functions.cache', () => {
assert.strictEqual(workOrderMilestoneType.workOrderMilestoneType, byName?.workOrderMilestoneType);
}
});
it('returns undefined with a bad workOrderMilestoneTypeId', async () => {
await it('returns undefined with a bad workOrderMilestoneTypeId', async () => {
const byBadId = await cacheFunctions.getWorkOrderMilestoneTypeById(badId);
assert.ok(byBadId === undefined);
});
it('returns undefined with a bad workOrderMilestoneType', async () => {
await it('returns undefined with a bad workOrderMilestoneType', async () => {
const byBadName = await cacheFunctions.getWorkOrderMilestoneTypeByWorkOrderMilestoneType(badName);
assert.ok(byBadName === undefined);
});
});
});
describe('functions.sqlFilters', () => {
describe('BurialSiteName filter', () => {
it('returns startsWith filter', () => {
await describe('functions.sqlFilters', async () => {
await describe('BurialSiteName filter', async () => {
await it('returns startsWith filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause('TEST1 TEST2', 'startsWith', 'l');
assert.strictEqual(filter.sqlWhereClause, " and l.burialSiteName like ? || '%'");
assert.strictEqual(filter.sqlParameters.length, 1);
assert.ok(filter.sqlParameters.includes('TEST1 TEST2'));
});
it('returns endsWith filter', () => {
await it('returns endsWith filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause('TEST1 TEST2', 'endsWith', 'l');
assert.strictEqual(filter.sqlWhereClause, " and l.burialSiteName like '%' || ?");
assert.strictEqual(filter.sqlParameters.length, 1);
assert.strictEqual(filter.sqlParameters[0], 'TEST1 TEST2');
});
it('returns contains filter', () => {
await it('returns contains filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause('TEST1 TEST2', '', 'l');
assert.strictEqual(filter.sqlWhereClause, ' and instr(lower(l.burialSiteName), ?) and instr(lower(l.burialSiteName), ?)');
assert.ok(filter.sqlParameters.includes('test1'));
assert.ok(filter.sqlParameters.includes('test2'));
});
it('handles empty filter', () => {
await it('handles empty filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause('', '');
assert.strictEqual(filter.sqlWhereClause, '');
assert.strictEqual(filter.sqlParameters.length, 0);
});
it('handles undefined filter', () => {
await it('handles undefined filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause(undefined, undefined, 'l');
assert.strictEqual(filter.sqlWhereClause, '');
assert.strictEqual(filter.sqlParameters.length, 0);
});
});
describe('OccupancyTime filter', () => {
it('creates three different filters', () => {
await describe('OccupancyTime filter', async () => {
await it('creates three different filters', () => {
const currentFilter = sqlFilterFunctions.getContractTimeWhereClause('current');
assert.notStrictEqual(currentFilter.sqlWhereClause, '');
const pastFilter = sqlFilterFunctions.getContractTimeWhereClause('past');
@ -156,50 +154,50 @@ describe('functions.sqlFilters', () => {
assert.notStrictEqual(currentFilter.sqlWhereClause, futureFilter.sqlWhereClause);
assert.notStrictEqual(pastFilter.sqlWhereClause, futureFilter.sqlWhereClause);
});
it('handles empty filter', () => {
await it('handles empty filter', () => {
const filter = sqlFilterFunctions.getContractTimeWhereClause('');
assert.strictEqual(filter.sqlWhereClause, '');
assert.strictEqual(filter.sqlParameters.length, 0);
});
it('handles undefined filter', () => {
await it('handles undefined filter', () => {
const filter = sqlFilterFunctions.getContractTimeWhereClause(undefined, 'o');
assert.strictEqual(filter.sqlWhereClause, '');
assert.strictEqual(filter.sqlParameters.length, 0);
});
});
describe('DeceasedName filter', () => {
it('returns filter', () => {
await describe('DeceasedName filter', async () => {
await it('returns filter', () => {
const filter = sqlFilterFunctions.getDeceasedNameWhereClause('TEST1 TEST2', 'o');
assert.strictEqual(filter.sqlWhereClause, ' and instr(lower(o.deceasedName), ?) and instr(lower(o.deceasedName), ?)');
assert.ok(filter.sqlParameters.length === 2);
assert.ok(filter.sqlParameters.includes('test1'));
assert.ok(filter.sqlParameters.includes('test2'));
});
it('handles empty filter', () => {
await it('handles empty filter', () => {
const filter = sqlFilterFunctions.getDeceasedNameWhereClause('');
assert.strictEqual(filter.sqlWhereClause, '');
assert.strictEqual(filter.sqlParameters.length, 0);
});
it('handles undefined filter', () => {
await it('handles undefined filter', () => {
const filter = sqlFilterFunctions.getDeceasedNameWhereClause(undefined, 'o');
assert.strictEqual(filter.sqlWhereClause, '');
assert.strictEqual(filter.sqlParameters.length, 0);
});
});
});
describe('functions.user', () => {
describe('unauthenticated, no user in session', () => {
await describe('functions.user', async () => {
await describe('unauthenticated, no user in session', async () => {
const noUserRequest = {
session: {}
};
it('can not update', () => {
await it('can not update', () => {
assert.strictEqual(userFunctions.userCanUpdate(noUserRequest), false);
});
it('is not admin', () => {
await it('is not admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(noUserRequest), false);
});
});
describe('read only user, no update, no admin', () => {
await describe('read only user, no update, no admin', async () => {
const readOnlyRequest = {
session: {
user: {
@ -212,14 +210,14 @@ describe('functions.user', () => {
}
}
};
it('can not update', () => {
await it('can not update', () => {
assert.strictEqual(userFunctions.userCanUpdate(readOnlyRequest), false);
});
it('is not admin', () => {
await it('is not admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(readOnlyRequest), false);
});
});
describe('update only user, no admin', () => {
await describe('update only user, no admin', async () => {
const updateOnlyRequest = {
session: {
user: {
@ -232,14 +230,14 @@ describe('functions.user', () => {
}
}
};
it('can update', () => {
await it('can update', () => {
assert.strictEqual(userFunctions.userCanUpdate(updateOnlyRequest), true);
});
it('is not admin', () => {
await it('is not admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(updateOnlyRequest), false);
});
});
describe('admin only user, no update', () => {
await describe('admin only user, no update', async () => {
const adminOnlyRequest = {
session: {
user: {
@ -252,14 +250,14 @@ describe('functions.user', () => {
}
}
};
it('can not update', () => {
await it('can not update', () => {
assert.strictEqual(userFunctions.userCanUpdate(adminOnlyRequest), false);
});
it('is admin', () => {
await it('is admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(adminOnlyRequest), true);
});
});
describe('update admin user', () => {
await describe('update admin user', async () => {
const updateAdminRequest = {
session: {
user: {
@ -272,15 +270,15 @@ describe('functions.user', () => {
}
}
};
it('can update', () => {
await it('can update', () => {
assert.strictEqual(userFunctions.userCanUpdate(updateAdminRequest), true);
});
it('is admin', () => {
await it('is admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(updateAdminRequest), true);
});
});
describe('API key check', () => {
it('authenticates with a valid API key', async () => {
await describe('API key check', async () => {
await it('authenticates with a valid API key', async () => {
const apiKeysJSON = JSON.parse(fs.readFileSync('data/apiKeys.json', 'utf8'));
const apiKey = Object.values(apiKeysJSON)[0];
const apiRequest = {
@ -290,7 +288,7 @@ describe('functions.user', () => {
};
assert.strictEqual(await userFunctions.apiKeyIsValid(apiRequest), true);
});
it('fails to authenticate with an invalid API key', async () => {
await it('fails to authenticate with an invalid API key', async () => {
const apiRequest = {
params: {
apiKey: 'badKey'
@ -298,7 +296,7 @@ describe('functions.user', () => {
};
assert.strictEqual(await userFunctions.apiKeyIsValid(apiRequest), false);
});
it('fails to authenticate with no API key', async () => {
await it('fails to authenticate with no API key', async () => {
const apiRequest = {
params: {}
};

View File

@ -1,14 +1,12 @@
import assert from 'node:assert'
import fs from 'node:fs'
import { before, describe, it } from 'node:test'
// skipcq: JS-C1003 - Testing functions
import * as cacheFunctions from '../helpers/functions.cache.js'
// skipcq: JS-C1003 - Testing functions
import * as sqlFilterFunctions from '../helpers/functions.sqlFilters.js'
// skipcq: JS-C1003 - Testing functions
import * as userFunctions from '../helpers/functions.user.js'
describe('functions.cache', () => {
await describe('functions.cache', async () => {
const badId = -3
// eslint-disable-next-line no-secrets/no-secrets
const badName = 'qwertyuiopasdfghjklzxcvbnm'
@ -17,8 +15,8 @@ describe('functions.cache', () => {
cacheFunctions.clearCaches()
})
describe('Burial Site Statuses', () => {
it('returns Burial Site Statuses', async () => {
await describe('Burial Site Statuses', async () => {
await it('returns Burial Site Statuses', async () => {
cacheFunctions.clearCacheByTableName('BurialSiteStatuses')
const burialSiteStatuses = await cacheFunctions.getBurialSiteStatuses()
@ -29,28 +27,36 @@ describe('functions.cache', () => {
const byId = await cacheFunctions.getBurialSiteStatusById(
burialSiteStatus.burialSiteStatusId
)
assert.strictEqual(burialSiteStatus.burialSiteStatusId, byId?.burialSiteStatusId)
assert.strictEqual(
burialSiteStatus.burialSiteStatusId,
byId?.burialSiteStatusId
)
const byName = await cacheFunctions.getBurialSiteStatusByBurialSiteStatus(
const byName =
await cacheFunctions.getBurialSiteStatusByBurialSiteStatus(
burialSiteStatus.burialSiteStatus
)
assert.strictEqual(burialSiteStatus.burialSiteStatus, byName?.burialSiteStatus)
assert.strictEqual(
burialSiteStatus.burialSiteStatus,
byName?.burialSiteStatus
)
}
})
it('returns undefined with a bad burialSiteStatusId', async () => {
await it('returns undefined with a bad burialSiteStatusId', async () => {
const byBadId = await cacheFunctions.getBurialSiteStatusById(badId)
assert.ok(byBadId === undefined)
})
it('returns undefined with a bad lotStatus', async () => {
const byBadName = await cacheFunctions.getBurialSiteStatusByBurialSiteStatus(badName)
await it('returns undefined with a bad lotStatus', async () => {
const byBadName =
await cacheFunctions.getBurialSiteStatusByBurialSiteStatus(badName)
assert.ok(byBadName === undefined)
})
})
describe('Lot Types', () => {
it('returns Lot Types', async () => {
await describe('Burial Site Types', async () => {
await it('returns Burial Site Types', async () => {
cacheFunctions.clearCacheByTableName('BurialSiteTypes')
const burialSiteTypes = await cacheFunctions.getBurialSiteTypes()
@ -58,29 +64,38 @@ describe('functions.cache', () => {
assert.ok(burialSiteTypes.length > 0)
for (const burialSiteType of burialSiteTypes) {
const byId = await cacheFunctions.getBurialSiteTypeById(burialSiteType.burialSiteTypeId)
assert.strictEqual(burialSiteType.burialSiteTypeId, byId?.burialSiteTypeId)
const byId = await cacheFunctions.getBurialSiteTypeById(
burialSiteType.burialSiteTypeId
)
assert.strictEqual(
burialSiteType.burialSiteTypeId,
byId?.burialSiteTypeId
)
const byName = await cacheFunctions.getBurialSiteTypesByBurialSiteType(
burialSiteType.burialSiteType
)
assert.strictEqual(burialSiteType.burialSiteType, byName?.burialSiteType)
assert.strictEqual(
burialSiteType.burialSiteType,
byName?.burialSiteType
)
}
})
it('returns undefined with a bad burialSiteTypeId', async () => {
await it('returns undefined with a bad burialSiteTypeId', async () => {
const byBadId = await cacheFunctions.getBurialSiteTypeById(badId)
assert.ok(byBadId === undefined)
})
it('returns undefined with a bad lotType', async () => {
const byBadName = await cacheFunctions.getBurialSiteTypesByBurialSiteType(badName)
await it('returns undefined with a bad lotType', async () => {
const byBadName =
await cacheFunctions.getBurialSiteTypesByBurialSiteType(badName)
assert.ok(byBadName === undefined)
})
})
describe('Occupancy Types', () => {
it('returns Contract Types', async () => {
await describe('Contract Types', async () => {
await it('returns Contract Types', async () => {
cacheFunctions.clearCacheByTableName('ContractTypes')
const contractTypes = await cacheFunctions.getContractTypes()
@ -100,21 +115,20 @@ describe('functions.cache', () => {
}
})
it('returns undefined with a bad contractTypeId', async () => {
await it('returns undefined with a bad contractTypeId', async () => {
const byBadId = await cacheFunctions.getContractTypeById(badId)
assert.ok(byBadId === undefined)
})
it('returns undefined with a bad contractType', async () => {
const byBadName = await cacheFunctions.getContractTypeByContractType(
badName
)
await it('returns undefined with a bad contractType', async () => {
const byBadName =
await cacheFunctions.getContractTypeByContractType(badName)
assert.ok(byBadName === undefined)
})
})
describe('Work Order Types', () => {
it('returns Work Order Types', async () => {
await describe('Work Order Types', async () => {
await it('returns Work Order Types', async () => {
cacheFunctions.clearCacheByTableName('WorkOrderTypes')
const workOrderTypes = await cacheFunctions.getWorkOrderTypes()
@ -129,14 +143,14 @@ describe('functions.cache', () => {
}
})
it('returns undefined with a bad workOrderTypeId', async () => {
await it('returns undefined with a bad workOrderTypeId', async () => {
const byBadId = await cacheFunctions.getWorkOrderTypeById(badId)
assert.ok(byBadId === undefined)
})
})
describe('Work Order Milestone Types', () => {
it('returns Work Order Milestone Types', async () => {
await describe('Work Order Milestone Types', async () => {
await it('returns Work Order Milestone Types', async () => {
cacheFunctions.clearCacheByTableName('WorkOrderMilestoneTypes')
const workOrderMilestoneTypes =
@ -164,12 +178,12 @@ describe('functions.cache', () => {
}
})
it('returns undefined with a bad workOrderMilestoneTypeId', async () => {
await it('returns undefined with a bad workOrderMilestoneTypeId', async () => {
const byBadId = await cacheFunctions.getWorkOrderMilestoneTypeById(badId)
assert.ok(byBadId === undefined)
})
it('returns undefined with a bad workOrderMilestoneType', async () => {
await it('returns undefined with a bad workOrderMilestoneType', async () => {
const byBadName =
await cacheFunctions.getWorkOrderMilestoneTypeByWorkOrderMilestoneType(
badName
@ -179,33 +193,39 @@ describe('functions.cache', () => {
})
})
describe('functions.sqlFilters', () => {
describe('BurialSiteName filter', () => {
it('returns startsWith filter', () => {
await describe('functions.sqlFilters', async () => {
await describe('BurialSiteName filter', async () => {
await it('returns startsWith filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause(
'TEST1 TEST2',
'startsWith',
'l'
)
assert.strictEqual(filter.sqlWhereClause, " and l.burialSiteName like ? || '%'")
assert.strictEqual(
filter.sqlWhereClause,
" and l.burialSiteName like ? || '%'"
)
assert.strictEqual(filter.sqlParameters.length, 1)
assert.ok(filter.sqlParameters.includes('TEST1 TEST2'))
})
it('returns endsWith filter', () => {
await it('returns endsWith filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause(
'TEST1 TEST2',
'endsWith',
'l'
)
assert.strictEqual(filter.sqlWhereClause, " and l.burialSiteName like '%' || ?")
assert.strictEqual(
filter.sqlWhereClause,
" and l.burialSiteName like '%' || ?"
)
assert.strictEqual(filter.sqlParameters.length, 1)
assert.strictEqual(filter.sqlParameters[0], 'TEST1 TEST2')
})
it('returns contains filter', () => {
await it('returns contains filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause(
'TEST1 TEST2',
'',
@ -220,14 +240,14 @@ describe('functions.sqlFilters', () => {
assert.ok(filter.sqlParameters.includes('test2'))
})
it('handles empty filter', () => {
await it('handles empty filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause('', '')
assert.strictEqual(filter.sqlWhereClause, '')
assert.strictEqual(filter.sqlParameters.length, 0)
})
it('handles undefined filter', () => {
await it('handles undefined filter', () => {
const filter = sqlFilterFunctions.getBurialSiteNameWhereClause(
undefined,
undefined,
@ -239,8 +259,8 @@ describe('functions.sqlFilters', () => {
})
})
describe('OccupancyTime filter', () => {
it('creates three different filters', () => {
await describe('OccupancyTime filter', async () => {
await it('creates three different filters', () => {
const currentFilter =
sqlFilterFunctions.getContractTimeWhereClause('current')
assert.notStrictEqual(currentFilter.sqlWhereClause, '')
@ -266,13 +286,13 @@ describe('functions.sqlFilters', () => {
)
})
it('handles empty filter', () => {
await it('handles empty filter', () => {
const filter = sqlFilterFunctions.getContractTimeWhereClause('')
assert.strictEqual(filter.sqlWhereClause, '')
assert.strictEqual(filter.sqlParameters.length, 0)
})
it('handles undefined filter', () => {
await it('handles undefined filter', () => {
const filter = sqlFilterFunctions.getContractTimeWhereClause(
undefined,
'o'
@ -282,8 +302,8 @@ describe('functions.sqlFilters', () => {
})
})
describe('DeceasedName filter', () => {
it('returns filter', () => {
await describe('DeceasedName filter', async () => {
await it('returns filter', () => {
const filter = sqlFilterFunctions.getDeceasedNameWhereClause(
'TEST1 TEST2',
'o'
@ -300,14 +320,14 @@ describe('functions.sqlFilters', () => {
assert.ok(filter.sqlParameters.includes('test2'))
})
it('handles empty filter', () => {
await it('handles empty filter', () => {
const filter = sqlFilterFunctions.getDeceasedNameWhereClause('')
assert.strictEqual(filter.sqlWhereClause, '')
assert.strictEqual(filter.sqlParameters.length, 0)
})
it('handles undefined filter', () => {
await it('handles undefined filter', () => {
const filter = sqlFilterFunctions.getDeceasedNameWhereClause(
undefined,
'o'
@ -319,22 +339,22 @@ describe('functions.sqlFilters', () => {
})
})
describe('functions.user', () => {
describe('unauthenticated, no user in session', () => {
await describe('functions.user', async () => {
await describe('unauthenticated, no user in session', async () => {
const noUserRequest = {
session: {}
}
it('can not update', () => {
await it('can not update', () => {
assert.strictEqual(userFunctions.userCanUpdate(noUserRequest), false)
})
it('is not admin', () => {
await it('is not admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(noUserRequest), false)
})
})
describe('read only user, no update, no admin', () => {
await describe('read only user, no update, no admin', async () => {
const readOnlyRequest: userFunctions.UserRequest = {
session: {
user: {
@ -348,16 +368,16 @@ describe('functions.user', () => {
}
}
it('can not update', () => {
await it('can not update', () => {
assert.strictEqual(userFunctions.userCanUpdate(readOnlyRequest), false)
})
it('is not admin', () => {
await it('is not admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(readOnlyRequest), false)
})
})
describe('update only user, no admin', () => {
await describe('update only user, no admin', async () => {
const updateOnlyRequest: userFunctions.UserRequest = {
session: {
user: {
@ -371,16 +391,16 @@ describe('functions.user', () => {
}
}
it('can update', () => {
await it('can update', () => {
assert.strictEqual(userFunctions.userCanUpdate(updateOnlyRequest), true)
})
it('is not admin', () => {
await it('is not admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(updateOnlyRequest), false)
})
})
describe('admin only user, no update', () => {
await describe('admin only user, no update', async () => {
const adminOnlyRequest: userFunctions.UserRequest = {
session: {
user: {
@ -394,16 +414,16 @@ describe('functions.user', () => {
}
}
it('can not update', () => {
await it('can not update', () => {
assert.strictEqual(userFunctions.userCanUpdate(adminOnlyRequest), false)
})
it('is admin', () => {
await it('is admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(adminOnlyRequest), true)
})
})
describe('update admin user', () => {
await describe('update admin user', async () => {
const updateAdminRequest: userFunctions.UserRequest = {
session: {
user: {
@ -417,17 +437,17 @@ describe('functions.user', () => {
}
}
it('can update', () => {
await it('can update', () => {
assert.strictEqual(userFunctions.userCanUpdate(updateAdminRequest), true)
})
it('is admin', () => {
await it('is admin', () => {
assert.strictEqual(userFunctions.userIsAdmin(updateAdminRequest), true)
})
})
describe('API key check', () => {
it('authenticates with a valid API key', async () => {
await describe('API key check', async () => {
await it('authenticates with a valid API key', async () => {
const apiKeysJSON: Record<string, string> = JSON.parse(
fs.readFileSync('data/apiKeys.json', 'utf8')
) as Record<string, string>
@ -443,7 +463,7 @@ describe('functions.user', () => {
assert.strictEqual(await userFunctions.apiKeyIsValid(apiRequest), true)
})
it('fails to authenticate with an invalid API key', async () => {
await it('fails to authenticate with an invalid API key', async () => {
const apiRequest: userFunctions.APIRequest = {
params: {
apiKey: 'badKey'
@ -453,7 +473,7 @@ describe('functions.user', () => {
assert.strictEqual(await userFunctions.apiKeyIsValid(apiRequest), false)
})
it('fails to authenticate with no API key', async () => {
await it('fails to authenticate with no API key', async () => {
const apiRequest: userFunctions.APIRequest = {
params: {}
}

View File

@ -1,8 +1,9 @@
import assert from 'node:assert';
import fs from 'node:fs';
import { describe, it } from 'node:test';
import { version } from '../version.js';
describe('version', () => {
it('has a version that matches the package.json', () => {
await describe('version', async () => {
await it('has a version that matches the package.json', () => {
const packageJSON = JSON.parse(fs.readFileSync('package.json', 'utf8'));
assert.strictEqual(version, packageJSON.version);
});

View File

@ -1,10 +1,11 @@
import assert from 'node:assert'
import fs from 'node:fs'
import { describe, it } from 'node:test'
import { version } from '../version.js'
describe('version', () => {
it('has a version that matches the package.json', () => {
await describe('version', async () => {
await it('has a version that matches the package.json', () => {
const packageJSON = JSON.parse(fs.readFileSync('package.json', 'utf8'))
assert.strictEqual(version, packageJSON.version)
})

View File

@ -22,6 +22,10 @@ export interface Config {
settings: {
cityDefault?: string;
provinceDefault?: string;
latitudeMin?: number;
latitudeMax?: number;
longitudeMin?: number;
longitudeMax?: number;
fees: {
taxPercentageDefault?: number;
};
@ -31,6 +35,7 @@ export interface Config {
contracts: {
burialSiteIdIsRequired?: boolean;
contractEndDateIsRequired?: boolean;
purchaserRelationships?: string[];
deathAgePeriods?: string[];
prints?: string[];
};

View File

@ -23,6 +23,10 @@ export interface Config {
settings: {
cityDefault?: string
provinceDefault?: string
latitudeMin?: number
latitudeMax?: number
longitudeMin?: number
longitudeMax?: number
fees: {
taxPercentageDefault?: number
}
@ -32,6 +36,7 @@ export interface Config {
contracts: {
burialSiteIdIsRequired?: boolean
contractEndDateIsRequired?: boolean
purchaserRelationships?: string[]
deathAgePeriods?: string[]
prints?: string[]
}

View File

@ -208,14 +208,14 @@ export interface ContractInterment extends Record {
recordUpdate_timeMillisMax?: number;
}
export interface ContractComment extends Record {
contractCommentId?: number;
contractCommentId: number;
contractId?: number;
commentDate?: number;
commentDateString?: string;
commentTime?: number;
commentTimeString?: string;
commentTimePeriodString?: string;
comment?: string;
commentDate: number;
commentDateString: string;
commentTime: number;
commentTimeString: string;
commentTimePeriodString: string;
comment: string;
}
export interface ContractField extends ContractTypeField, Record {
contractId: number;

View File

@ -267,17 +267,17 @@ export interface ContractInterment extends Record {
}
export interface ContractComment extends Record {
contractCommentId?: number
contractCommentId: number
contractId?: number
commentDate?: number
commentDateString?: string
commentDate: number
commentDateString: string
commentTime?: number
commentTimeString?: string
commentTimePeriodString?: string
commentTime: number
commentTimeString: string
commentTimePeriodString: string
comment?: string
comment: string
}
export interface ContractField extends ContractTypeField, Record {

View File

@ -343,13 +343,19 @@
<div class="field">
<label class="label" for="burialSite--burialSiteLatitude">Latitude</label>
<div class="control">
<input class="input" id="burialSite--burialSiteLatitude" name="burialSiteLatitude" type="number" min="-90" max="90" step="0.00000001" value="<%= burialSite.burialSiteLatitude %>" onwheel="return false" />
<input class="input" id="burialSite--burialSiteLatitude" name="burialSiteLatitude" type="number"
min="<%= configFunctions.getConfigProperty('settings.latitudeMin') %>"
max="<%= configFunctions.getConfigProperty('settings.latitudeMax') %>"
step="0.00000001" value="<%= burialSite.burialSiteLatitude %>" onwheel="return false" />
</div>
</div>
<div class="field">
<label class="label" for="burialSite--burialSiteLongitude">Longitude</label>
<div class="control">
<input class="input" id="burialSite--burialSiteLongitude" name="burialSiteLongitude" type="number" min="-180" max="180" step="0.00000001" value="<%= burialSite.burialSiteLongitude %>" onwheel="return false" />
<input class="input" id="burialSite--burialSiteLongitude" name="burialSiteLongitude" type="number"
min="<%= configFunctions.getConfigProperty('settings.longitudeMin') %>"
max="<%= configFunctions.getConfigProperty('settings.longitudeMax') %>"
step="0.00000001" value="<%= burialSite.burialSiteLongitude %>" onwheel="return false" />
</div>
</div>
</div>

View File

@ -83,7 +83,6 @@
</button>
</div>
</div>
</div>
<form id="form--cemetery">
@ -106,7 +105,13 @@
<label class="label" for="cemetery--cemeteryKey">Cemetery Key</label>
<div class="control">
<input class="input" id="cemetery--cemeteryKey" name="cemeteryKey" type="text"
value="<%= cemetery.cemeteryKey %>" maxlength="20" required />
value="<%= cemetery.cemeteryKey %>" maxlength="20"
<%= configFunctions.getConfigProperty('settings.burialSites.burialSiteNameSegments.includeCemeteryKey') ? ' required' : '' %> />
<% if (configFunctions.getConfigProperty('settings.burialSites.burialSiteNameSegments.includeCemeteryKey')) { %>
<p class="help">
The cemetery key is prepended to the burial site names.
</p>
<% } %>
</div>
</div>
<div class="field">
@ -181,13 +186,19 @@
<div class="field">
<label class="label" for="cemetery--cemeteryLatitude">Latitude</label>
<div class="control">
<input class="input" id="cemetery--cemeteryLatitude" name="cemeteryLatitude" type="number" min="-90" max="90" step="0.00000001" value="<%= cemetery.cemeteryLatitude %>" />
<input class="input" id="cemetery--cemeteryLatitude" name="cemeteryLatitude" type="number"
min="<%= configFunctions.getConfigProperty('settings.latitudeMin') %>"
max="<%= configFunctions.getConfigProperty('settings.latitudeMax') %>"
step="0.00000001" value="<%= cemetery.cemeteryLatitude %>" />
</div>
</div>
<div class="field">
<label class="label" for="cemetery--cemeteryLongitude">Longitude</label>
<div class="control">
<input class="input" id="cemetery--cemeteryLongitude" name="cemeteryLongitude" type="number" min="-180" max="180" step="0.00000001" value="<%= cemetery.cemeteryLongitude %>" />
<input class="input" id="cemetery--cemeteryLongitude" name="cemeteryLongitude" type="number"
min="<%= configFunctions.getConfigProperty('settings.longitudeMin') %>"
max="<%= configFunctions.getConfigProperty('settings.longitudeMax') %>"
step="0.00000001" value="<%= cemetery.cemeteryLongitude %>" />
</div>
</div>
</div>
@ -216,7 +227,6 @@
</div>
</div>
</div>
</form>
<% if (!isCreate) { %>

View File

@ -158,7 +158,7 @@
required accesskey="f"
<%= (isCreate ? " autofocus" : "") %>>
<% if (isCreate) { %>
<option value="" data-is-preneed="false">(No Type)</option>
<option value="" data-is-preneed="false">(Select a Type)</option>
<% } %>
<% let typeIsFound = false; %>
<% for (const contractType of contractTypes) { %>
@ -498,9 +498,17 @@
</span>
</label>
<div class="control">
<input class="input" id="contract--purchaserRelationship" name="purchaserRelationship" type="text" maxlength="100" autocomplete="off" value="<%= contract.purchaserRelationship %>" />
<input class="input" id="contract--purchaserRelationship" name="purchaserRelationship" type="text"
maxlength="100" autocomplete="off"
list="datalist--purchaserRelationships"
value="<%= contract.purchaserRelationship %>" />
</div>
</div>
<datalist id="datalist--purchaserRelationships">
<% for (const relationship of configFunctions.getConfigProperty('settings.contracts.purchaserRelationships')) { %>
<option value="<%= relationship %>">
<% } %>
</datalist>
</div>
</div>
</div>

View File

@ -254,20 +254,28 @@
<strong>Birth:</strong>
</div>
<div class="column">
<%= contractInterment.birthDateString ?? '(No Birth Date)' %><br />
<%= contractInterment.birthPlace ?? '(No Birth Place)' %>
<% if (contractInterment.birthDateString === '') { %>
<span class="has-text-grey">(No Birth Date)</span>
<% } else { %>
<%= contractInterment.birthDateString %>
<% } %><br />
<%= contractInterment.birthPlace %>
</div>
</div>
<div class="columns">
<div class="columns mb-0">
<div class="column">
<strong>Death:</strong>
</div>
<div class="column">
<%= contractInterment.deathDateString ?? '(No Death Date)' %><br />
<%= contractInterment.deathPlace ?? '(No Death Place)' %>
<% if (contractInterment.deathDateString === '') { %>
<span class="has-text-grey">(No Death Date)</span>
<% } else { %>
<%= contractInterment.deathDateString %>
<% } %><br />
<%= contractInterment.deathPlace %>
</div>
</div>
<div class="columns">
<div class="columns mb-0">
<div class="column">
<strong>Age:</strong>
</div>

View File

@ -94,30 +94,23 @@
<div class="column">
<div class="columns is-desktop">
<div class="column">
<div class="card is-hover-container">
<div class="card-content">
<div class="panel">
<a class="panel-block" href="<%= urlPrefix %>/workOrders">
<div class="media">
<div class="media-left">
<span class="fa-layers fa-4x fa-fw">
<i class="fas fa-fw fa-hard-hat" aria-hidden="true"></i>
<% if (workOrderCount > 0) { %>
<a class="fa-layers-counter has-background-success has-text-white" href="<%= urlPrefix %>/workOrders/?workOrderOpenDateString=<%= dateTimeFunctions.dateToString(new Date()) %>"><%= workOrderCount %></a>
<% } %>
</span>
<i class="fa-solid fa-4x fa-fw fa-hard-hat" aria-hidden="true"></i>
</div>
<a class="media-content" href="<%= urlPrefix %>/workOrders">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Work Orders
</h2>
<p>
View and maintain work orders.<br />
<span class="tags has-addons is-invisible is-visible-hover">
<span class="tag is-link is-light">Shortcut</span>
<kbd class="tag">1</kbd>
</span>
View and maintain work orders.
</p>
</a>
</div>
</div>
</a>
<div class="panel-block is-block">
<% if (user.userProperties.canUpdate) { %>
<a class="button is-fullwidth is-success is-light mb-2" href="<%= urlPrefix %>/workOrders/new">
<span class="icon">
@ -137,31 +130,26 @@
</div>
<div class="column">
<div class="card is-hover-container">
<div class="card-content">
<div class="panel">
<a class="panel-block" href="<%= urlPrefix %>/contracts">
<div class="media">
<div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true">
<i class="fas fa-vector-square"></i>
<i class="fas fa-user" data-fa-transform="shrink-10"></i>
<% if (contractCount > 0) { %>
<span class="fa-layers-counter has-background-success"><%= contractCount %></span>
<% } %>
</span>
</div>
<a class="media-content" href="<%= urlPrefix %>/contracts">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Contracts
</h2>
<p>
View and maintain current and past contracts.<br />
<span class="tags has-addons is-invisible is-visible-hover">
<span class="tag is-link is-light">Shortcut</span>
<kbd class="tag">2</kbd>
</span>
View and maintain current and past contracts.
</p>
</a>
</div>
</div>
</a>
<div class="panel-block is-block">
<% if (user.userProperties.canUpdate) { %>
<a class="button is-fullwidth is-success is-light mb-2" href="<%= urlPrefix %>/contracts/new">
<span class="icon">
@ -182,89 +170,98 @@
</div>
<div class="columns is-desktop">
<div class="column">
<div class="card is-hover-container">
<div class="card-content">
<div class="panel">
<a class="panel-block" href="<%= urlPrefix %>/burialSites">
<div class="media">
<div class="media-left">
<i class="fas fa-4x fa-fw fa-vector-square" aria-hidden="true"></i>
</div>
<a class="media-content" href="<%= urlPrefix %>/burialSites">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Burial Sites
</h2>
<p>
View and maintain burial sites within a cemetery.<br />
<span class="tags has-addons is-invisible is-visible-hover">
<span class="tag is-link is-light">Shortcut</span>
<kbd class="tag">3</kbd>
</span>
View and maintain burial sites within a cemetery.
</p>
</a>
</div>
</div>
</a>
<% if (user.userProperties.canUpdate) { %>
<div class="panel-block is-block">
<a class="button is-fullwidth is-success is-light" href="<%= urlPrefix %>/burialSites/new">
<span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i>
</span>
<span>New Burial Site</span>
</a>
</div>
<% } %>
</div>
</div>
</div>
<div class="column">
<div class="card is-hover-container">
<div class="card-content">
<div class="panel">
<a class="panel-block" href="<%= urlPrefix %>/cemeteries">
<div class="media">
<div class="media-left">
<i class="far fa-4x fa-fw fa-map" aria-hidden="true"></i>
</div>
<a class="media-content" href="<%= urlPrefix %>/cemeteries">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Cemeteries
</h2>
<p>
View and maintain cemeteries.
<span class="tags has-addons is-invisible is-visible-hover">
<span class="tag is-link is-light">Shortcut</span>
<kbd class="tag">4</kbd>
</span>
</p>
</a>
</div>
</div>
</a>
<% if (user.userProperties.canUpdate) { %>
<div class="panel-block is-block">
<a class="button is-fullwidth is-success is-light" href="<%= urlPrefix %>/cemeteries/new">
<span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i>
</span>
<span>New Cemetery</span>
</a>
<% } %>
</div>
<% } %>
</div>
</div>
</div>
<div class="columns is-desktop">
<div class="column">
<div class="card">
<div class="card-content">
<div class="panel">
<a class="panel-block" href="<%= urlPrefix %>/reports">
<div class="media">
<div class="media-left">
<i class="fas fa-4x fa-fw fa-file" aria-hidden="true"></i>
</div>
<a class="media-content" href="<%= urlPrefix %>/reports">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Report Library
</h2>
<p>Produce reports and export data.</p>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
<div class="column">
<div class="card is-hover-container">
<div class="card-content">
<div class="panel">
<a class="panel-block" href="https://cityssm.github.io/sunrise-cms/docs" rel="noopener noreferrer" target="_blank">
<div class="media">
<div class="media-left">
<i class="fas fa-4x fa-fw fa-circle-question" aria-hidden="true"></i>
</div>
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Help Documentation
</h2>
<p>Tips and tricks to get the most out of Sunrise CMS.</p>
</div>
</div>
</a>
<div class="panel-block is-block">
<a class="button is-fullwidth is-link is-light has-tooltip-bottom" data-tooltip="Latest Updates, Issue Tracker, Say Hello"
href="https://github.com/cityssm/sunrise-cms" target="_blank" rel="noreferrer">
<span class="icon">
@ -278,10 +275,11 @@
</div>
<% if (user.userProperties.isAdmin) { %>
<h2 class="title is-3">Administrator Tools</h2>
<div class="card">
<div class="card-content">
<div class="panel">
<div class="panel-heading">
Administrator Tools
</div>
<a class="panel-block" href="<%= urlPrefix %>/admin/fees">
<div class="media">
<div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true">
@ -289,7 +287,7 @@
<i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span>
</div>
<a class="media-content" href="<%= urlPrefix %>/admin/fees">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Fee Management
</h2>
@ -297,10 +295,10 @@
Manage fees for contracts
and specific burial site types.
</p>
</div>
</div>
</a>
</div>
</div>
<div class="card-content">
<a class="panel-block" href="<%= urlPrefix %>/admin/contractTypes">
<div class="media">
<div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true">
@ -308,7 +306,7 @@
<i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span>
</div>
<a class="media-content" href="<%= urlPrefix %>/admin/contractTypes">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Contract Type Management
</h2>
@ -317,10 +315,10 @@
the fields associated with them,
and their available print options.
</p>
</div>
</div>
</a>
</div>
</div>
<div class="card-content">
<a class="panel-block" href="<%= urlPrefix %>/admin/burialSiteTypes">
<div class="media">
<div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true">
@ -328,17 +326,17 @@
<i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span>
</div>
<a class="media-content" href="<%= urlPrefix %>/admin/burialSiteTypes">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Burial Site Type Management
</h2>
<p>
Manage burial site types and fields associated with them.
</p>
</div>
</div>
</a>
</div>
</div>
<div class="card-content">
<a class="panel-block" href="<%= urlPrefix %>/admin/tables">
<div class="media">
<div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true">
@ -346,7 +344,7 @@
<i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span>
</div>
<a class="media-content" href="<%= urlPrefix %>/admin/tables">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Config Table Management
</h2>
@ -355,15 +353,15 @@
work order types
and burial site statuses.
</p>
</div>
</div>
</a>
</div>
</div>
<div class="card-content">
<a class="panel-block" href="<%= urlPrefix %>/admin/database">
<div class="media">
<div class="media-left">
<i class="fas fa-4x fa-fw fa-database" aria-hidden="true"></i>
</div>
<a class="media-content" href="<%= urlPrefix %>/admin/database">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Database Maintenance
</h2>
@ -371,25 +369,25 @@
Backup the database before making significant updates.
Permanently delete records that have been previously deleted from the database.
</p>
</div>
</div>
</a>
</div>
</div>
<% if (configFunctions.getConfigProperty("application.ntfyStartup")) { %>
<div class="card-content">
<a class="panel-block" href="<%= urlPrefix %>/admin/ntfyStartup">
<div class="media">
<div class="media-left">
<i class="far fa-4x fa-fw fa-comment-alt" aria-hidden="true"></i>
</div>
<a class="media-content" href="<%= urlPrefix %>/admin/ntfyStartup">
<div class="media-content">
<h2 class="title is-4 mb-0 has-text-link">
Ntfy Startup Notification
</h2>
<p>
Subscribe to application startup notifications on a phone or a desktop computer.
</p>
</div>
</div>
</a>
</div>
</div>
<% } %>
</div>
<% } %>

View File

@ -83,7 +83,7 @@
<span class="tag is-info">CSV</span>
</div>
<div>
<h2 class="title is-5 is-marginless">Open Work Orders</h2>
<h2 class="title is-5 mb-0">Open Work Orders</h2>
<p>
All active work orders without completion dates.
</p>
@ -101,7 +101,7 @@
<span class="tag is-info">ICS</span>
</div>
<div>
<h2 class="title is-5 is-marginless">Work Order Milestone Calendar</h2>
<h2 class="title is-5 mb-0">Work Order Milestone Calendar</h2>
<p>
Upcoming and recently passed work order milestones,
compatible with Microsoft Outlook and other calendar tools.
@ -125,7 +125,7 @@
<span class="tag is-info">CSV</span>
</div>
<div>
<h2 class="title is-5 is-marginless">
<h2 class="title is-5 mb-0">
Current Contract By Cemetery
</h2>
<div class="field has-addons mt-2">
@ -161,7 +161,7 @@
<span class="tag is-info">CSV</span>
</div>
<div>
<h2 class="title is-5 is-marginless">Transactions by Date</h2>
<h2 class="title is-5 mb-0">Transactions by Date</h2>
<div class="field has-addons mt-2">
<div class="control">
<label class="button is-small is-static" for="contractTransactions-byTransactionDateString--transactionDateString">
@ -193,7 +193,7 @@
<span class="tag is-info">CSV</span>
</div>
<div>
<h2 class="title is-5 is-marginless">
<h2 class="title is-5 mb-0">
Burial Sites By Cemetery
</h2>
<div class="field has-addons mt-2">
@ -229,7 +229,7 @@
<span class="tag is-info">CSV</span>
</div>
<div>
<h2 class="title is-5 is-marginless">Burial Sites By Type</h2>
<h2 class="title is-5 mb-0">Burial Sites By Type</h2>
<div class="field has-addons mt-2">
<div class="control">
<label class="button is-small is-static" for="burialSites-byBurialSiteTypeId--burialSiteTypeId">
@ -263,7 +263,7 @@
<span class="tag is-info">CSV</span>
</div>
<div>
<h2 class="title is-5 is-marginless">Burial Sites By Status</h2>
<h2 class="title is-5 mb-0">Burial Sites By Status</h2>
<div class="field has-addons mt-2">
<div class="control">
<label class="button is-small is-static" for="burialSites-byBurialSiteStatusId--burialSiteStatusId">
@ -302,7 +302,7 @@
<span class="tag is-info">CSV</span>
</div>
<div>
<h2 class="title is-5 is-marginless">Full Cemetery List</h2>
<h2 class="title is-5 mb-0">Full Cemetery List</h2>
<p>
All active cemeteries.
</p>
@ -389,6 +389,17 @@
<h3 class="title is-5 is-marginless">Full ContractTransactions Table</h3>
</div>
</a>
<a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/funeralHomes-all" download>
<div class="has-text-centered my-2 ml-2 mr-3">
<span class="icon has-text-info">
<i class="fas fa-2x fa-table" aria-hidden="true"></i>
</span><br />
<span class="tag is-info">CSV</span>
</div>
<div>
<h3 class="title is-5 is-marginless">Full FuneralHomes Table</h3>
</div>
</a>
</div>
</div>
<div class="column">
@ -586,6 +597,17 @@
<h3 class="title is-5 is-marginless">Full ContractTypeFields Table</h3>
</div>
</a>
<a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/intermentContainerTypes-all" download>
<div class="has-text-centered my-2 ml-2 mr-3">
<span class="icon has-text-info">
<i class="fas fa-2x fa-table" aria-hidden="true"></i>
</span><br />
<span class="tag is-info">CSV</span>
</div>
<div>
<h3 class="title is-5 is-marginless">Full IntermentContainerTypes Table</h3>
</div>
</a>
</div>
<div class="panel">
<h2 class="panel-heading">Work Order Tables</h2>