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'; import { config as baseConfig } from './config.base.js';
export const config = Object.assign({}, baseConfig); export const config = Object.assign({}, baseConfig);
config.settings.provinceDefault = 'ON'; 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; config.settings.fees.taxPercentageDefault = 13;
export default config; export default config;

View File

@ -4,6 +4,12 @@ export const config = Object.assign({}, baseConfig)
config.settings.provinceDefault = 'ON' 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 config.settings.fees.taxPercentageDefault = 13
export default config export default config

View File

@ -13,7 +13,7 @@ config.settings.burialSites.burialSiteNameSegments = {
maxLength: 1 maxLength: 1
}, },
2: { 2: {
isRequired: true, isRequired: false,
isAvailable: true, isAvailable: true,
label: 'Range', label: 'Range',
minLength: 1, minLength: 1,
@ -36,6 +36,10 @@ config.settings.burialSites.burialSiteNameSegments = {
} }
}; };
config.settings.cityDefault = 'Sault Ste. Marie'; 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 = [ config.settings.contracts.prints = [
'pdf/ssm.cemetery.burialPermit', 'pdf/ssm.cemetery.burialPermit',
'pdf/ssm.cemetery.contract' 'pdf/ssm.cemetery.contract'

View File

@ -18,7 +18,7 @@ config.settings.burialSites.burialSiteNameSegments = {
maxLength: 1 maxLength: 1
}, },
2: { 2: {
isRequired: true, isRequired: false,
isAvailable: true, isAvailable: true,
label: 'Range', label: 'Range',
minLength: 1, minLength: 1,
@ -43,6 +43,11 @@ config.settings.burialSites.burialSiteNameSegments = {
config.settings.cityDefault = 'Sault Ste. Marie' 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 = [ config.settings.contracts.prints = [
'pdf/ssm.cemetery.burialPermit', 'pdf/ssm.cemetery.burialPermit',
'pdf/ssm.cemetery.contract' 'pdf/ssm.cemetery.contract'

View File

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

View File

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

View File

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

View File

@ -1,224 +1,182 @@
import { dateIntegerToString, dateStringToInteger, dateToInteger, timeIntegerToString } from '@cityssm/utils-datetime'; import { dateIntegerToString, dateStringToInteger, dateToInteger, timeIntegerToString } from '@cityssm/utils-datetime';
import { acquireConnection } from './pool.js'; import { acquireConnection } from './pool.js';
// eslint-disable-next-line complexity 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,
cemeteryPostalCode,
cemeteryPhoneNumber
from Cemeteries
where recordDelete_timeMillis is null
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 = {}) { export default async function getReportData(reportName, reportParameters = {}) {
let sql = ''; let sql = '';
const sqlParameters = []; const sqlParameters = [];
switch (reportName) { // eslint-disable-next-line security/detect-object-injection
case 'cemeteries-all': { if (simpleReports[reportName] === undefined) {
sql = 'select * from Cemeteries'; switch (reportName) {
break; case 'burialSites-byBurialSiteTypeId': {
} sql = `select l.burialSiteId,
case 'cemeteries-formatted': { m.cemeteryName,
sql = `select cemeteryName, l.burialSiteName,
cemeteryDescription, t.burialSiteType,
cemeteryAddress1, cemeteryAddress2, s.burialSiteStatus
cemeteryCity, cemeteryProvince, from BurialSites l
cemeteryPostalCode, left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
cemeteryPhoneNumber left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
from Cemeteries left join Cemeteries m on l.cemeteryId = m.cemeteryId
where recordDelete_timeMillis is null where l.recordDelete_timeMillis is null
order by cemeteryName`; and l.burialSiteTypeId = ?`;
break; sqlParameters.push(reportParameters.burialSiteTypeId);
} break;
case 'burialSites-all': { }
sql = 'select * from BurialSites'; case 'burialSites-byBurialSiteStatusId': {
break; sql = `select l.burialSiteId,
} m.cemeteryName,
case 'burialSites-byBurialSiteTypeId': { l.burialSiteName,
sql = `select l.burialSiteId, t.burialSiteType,
m.cemeteryName, s.burialSiteStatus
l.burialSiteName, from BurialSites l
t.burialSiteType, left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
s.burialSiteStatus left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
from BurialSites l left join Cemeteries m on l.cemeteryId = m.cemeteryId
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId where l.recordDelete_timeMillis is null
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId and l.burialSiteStatusId = ?`;
left join Cemeteries m on l.cemeteryId = m.cemeteryId sqlParameters.push(reportParameters.burialSiteStatusId);
where l.recordDelete_timeMillis is null break;
and l.burialSiteTypeId = ?`; }
sqlParameters.push(reportParameters.burialSiteTypeId); case 'burialSites-byCemeteryId': {
break; sql = `select l.burialSiteId,
} m.cemeteryName,
case 'burialSites-byBurialSiteStatusId': { l.burialSiteName,
sql = `select l.burialSiteId, t.burialSiteType,
m.cemeteryName, s.burialSiteStatus
l.burialSiteName, from BurialSites l
t.burialSiteType, left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
s.burialSiteStatus left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
from BurialSites l left join Cemeteries m on l.cemeteryId = m.cemeteryId
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId where l.recordDelete_timeMillis is null
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId and l.cemeteryId = ?`;
left join Cemeteries m on l.cemeteryId = m.cemeteryId sqlParameters.push(reportParameters.cemeteryId);
where l.recordDelete_timeMillis is null break;
and l.burialSiteStatusId = ?`; }
sqlParameters.push(reportParameters.burialSiteStatusId); case 'contracts-current-byCemeteryId': {
break; sql = `select o.contractId,
} l.burialSiteName,
case 'burialSites-byCemeteryId': { m.cemeteryName,
sql = `select l.burialSiteId, ot.contractType,
m.cemeteryName, o.contractStartDate,
l.burialSiteName, o.contractEndDate
t.burialSiteType, from Contracts o
s.burialSiteStatus left join ContractTypes ot on o.contractTypeId = ot.contractTypeId
from BurialSites l left join BurialSites l on o.burialSiteId = l.burialSiteId
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId left join Cemeteries m on l.cemeteryId = m.cemeteryId
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId where o.recordDelete_timeMillis is null
left join Cemeteries m on l.cemeteryId = m.cemeteryId and (o.contractEndDate is null or o.contractEndDate >= ?)
where l.recordDelete_timeMillis is null and l.cemeteryId = ?`;
and l.cemeteryId = ?`; sqlParameters.push(dateToInteger(new Date()), reportParameters.cemeteryId);
sqlParameters.push(reportParameters.cemeteryId); break;
break; }
} case 'contractInterments-byContractId': {
case 'burialSiteComments-all': { sql = `select i.contractId, i.intermentNumber,
sql = 'select * from BurialSiteComments'; i.deceasedName, i.deceasedAddress1, i.deceasedAddress2,
break; i.deceasedCity, i.deceasedProvince, i.deceasedPostalCode,
} i.birthDate, i.birthPlace,
case 'burialSiteFields-all': { i.deathDate, i.deathPlace,
sql = 'select * from BurialSiteFields'; i.deathAge, i.deathAgePeriod
break; from ContractInterments i
} left join IntermentContainerTypes t on i.intermentContainerTypeId = t.intermentContainerTypeId
case 'contracts-all': { where i.recordDelete_timeMillis is null
sql = 'select * from Contracts'; and i.contractId = ?`;
break; sqlParameters.push(reportParameters.contractId);
} break;
case 'contracts-current-byCemeteryId': { }
sql = `select o.contractId, case 'contractTransactions-byTransactionDateString': {
l.burialSiteName, sql = `select t.contractId, t.transactionIndex,
m.cemeteryName, t.transactionDate, t.transactionTime,
ot.contractType, t.transactionAmount,
o.contractStartDate, t.externalReceiptNumber, t.transactionNote
o.contractEndDate from ContractTransactions t
from Contracts o where t.recordDelete_timeMillis is null
left join ContractTypes ot on o.contractTypeId = ot.contractTypeId and t.transactionDate = ?`;
left join BurialSites l on o.burialSiteId = l.burialSiteId sqlParameters.push(dateStringToInteger(reportParameters.transactionDateString));
left join Cemeteries m on l.cemeteryId = m.cemeteryId break;
where o.recordDelete_timeMillis is null }
and (o.contractEndDate is null or o.contractEndDate >= ?) case 'workOrders-open': {
and l.cemeteryId = ?`; sql = `select w.workOrderId, w.workOrderNumber,
sqlParameters.push(dateToInteger(new Date()), reportParameters.cemeteryId); t.workOrderType, w.workOrderDescription,
break; w.workOrderOpenDate,
} m.workOrderMilestoneCount, m.workOrderMilestoneCompletionCount
case 'contractComments-all': { from WorkOrders w
sql = 'select * from ContractComments'; left join WorkOrderTypes t on w.workOrderTypeId = t.workOrderTypeId
break; left join (
} select m.workOrderId,
case 'contractFees-all': { count(m.workOrderMilestoneId) as workOrderMilestoneCount,
sql = 'select * from ContractFees'; sum(case when m.workOrderMilestoneCompletionDate is null then 0 else 1 end) as workOrderMilestoneCompletionCount
break; from WorkOrderMilestones m
} where m.recordDelete_timeMillis is null
case 'contractFields-all': { group by m.workOrderId
sql = 'select * from ContractFields'; ) m on w.workOrderId = m.workOrderId
break; where w.recordDelete_timeMillis is null
} and w.workOrderCloseDate is null`;
case 'contractInterments-all': { break;
sql = 'select * from ContractInterments'; }
break; case 'workOrderMilestones-byWorkOrderId': {
} sql = `select t.workOrderMilestoneType,
case 'contractTransactions-all': { m.workOrderMilestoneDate,
sql = 'select * from ContractTransactions'; m.workOrderMilestoneTime,
break; m.workOrderMilestoneDescription,
} m.workOrderMilestoneCompletionDate,
case 'contractTransactions-byTransactionDateString': { m.workOrderMilestoneCompletionTime
sql = `select t.contractId, t.transactionIndex,
t.transactionDate, t.transactionTime,
t.transactionAmount,
t.externalReceiptNumber, t.transactionNote
from ContractTransactions t
where t.recordDelete_timeMillis is null
and t.transactionDate = ?`;
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,
w.workOrderOpenDate,
m.workOrderMilestoneCount, m.workOrderMilestoneCompletionCount
from WorkOrders w
left join WorkOrderTypes t on w.workOrderTypeId = t.workOrderTypeId
left join (
select m.workOrderId,
count(m.workOrderMilestoneId) as workOrderMilestoneCount,
sum(case when m.workOrderMilestoneCompletionDate is null then 0 else 1 end) as workOrderMilestoneCompletionCount
from WorkOrderMilestones m from WorkOrderMilestones m
left join WorkOrderMilestoneTypes t on m.workOrderMilestoneTypeId = t.workOrderMilestoneTypeId
where m.recordDelete_timeMillis is null where m.recordDelete_timeMillis is null
group by m.workOrderId and m.workOrderId = ?`;
) m on w.workOrderId = m.workOrderId sqlParameters.push(reportParameters.workOrderId);
where w.recordDelete_timeMillis is null break;
and w.workOrderCloseDate is null`; }
break; default: {
} return undefined;
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,
m.workOrderMilestoneTime,
m.workOrderMilestoneDescription,
m.workOrderMilestoneCompletionDate,
m.workOrderMilestoneCompletionTime
from WorkOrderMilestones m
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(); const database = await acquireConnection();
database.function('userFn_dateIntegerToString', dateIntegerToString); database.function('userFn_dateIntegerToString', dateIntegerToString);
database.function('userFn_timeIntegerToString', timeIntegerToString); database.function('userFn_timeIntegerToString', timeIntegerToString);

View File

@ -10,7 +10,54 @@ import { acquireConnection } from './pool.js'
export type ReportParameters = Record<string, string | number> export type ReportParameters = Record<string, string | number>
// eslint-disable-next-line complexity 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',
'cemeteries-all': 'select * from Cemeteries',
'cemeteries-formatted': `select cemeteryName,
cemeteryDescription,
cemeteryAddress1, cemeteryAddress2,
cemeteryCity, cemeteryProvince,
cemeteryPostalCode,
cemeteryPhoneNumber
from Cemeteries
where recordDelete_timeMillis is null
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( export default async function getReportData(
reportName: string, reportName: string,
reportParameters: ReportParameters = {} reportParameters: ReportParameters = {}
@ -18,265 +65,163 @@ export default async function getReportData(
let sql = '' let sql = ''
const sqlParameters: unknown[] = [] const sqlParameters: unknown[] = []
switch (reportName) { // eslint-disable-next-line security/detect-object-injection
case 'cemeteries-all': { if (simpleReports[reportName] === undefined) {
sql = 'select * from Cemeteries' switch (reportName) {
break case 'burialSites-byBurialSiteTypeId': {
} sql = `select l.burialSiteId,
m.cemeteryName,
l.burialSiteName,
t.burialSiteType,
s.burialSiteStatus
from BurialSites l
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where l.recordDelete_timeMillis is null
and l.burialSiteTypeId = ?`
case 'cemeteries-formatted': { sqlParameters.push(reportParameters.burialSiteTypeId)
sql = `select cemeteryName,
cemeteryDescription,
cemeteryAddress1, cemeteryAddress2,
cemeteryCity, cemeteryProvince,
cemeteryPostalCode,
cemeteryPhoneNumber
from Cemeteries
where recordDelete_timeMillis is null
order by cemeteryName`
break break
} }
case 'burialSites-all': { case 'burialSites-byBurialSiteStatusId': {
sql = 'select * from BurialSites' sql = `select l.burialSiteId,
break m.cemeteryName,
} l.burialSiteName,
t.burialSiteType,
s.burialSiteStatus
from BurialSites l
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where l.recordDelete_timeMillis is null
and l.burialSiteStatusId = ?`
case 'burialSites-byBurialSiteTypeId': { sqlParameters.push(reportParameters.burialSiteStatusId)
sql = `select l.burialSiteId,
m.cemeteryName,
l.burialSiteName,
t.burialSiteType,
s.burialSiteStatus
from BurialSites l
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where l.recordDelete_timeMillis is null
and l.burialSiteTypeId = ?`
sqlParameters.push(reportParameters.burialSiteTypeId) break
}
break case 'burialSites-byCemeteryId': {
} sql = `select l.burialSiteId,
m.cemeteryName,
l.burialSiteName,
t.burialSiteType,
s.burialSiteStatus
from BurialSites l
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where l.recordDelete_timeMillis is null
and l.cemeteryId = ?`
case 'burialSites-byBurialSiteStatusId': { sqlParameters.push(reportParameters.cemeteryId)
sql = `select l.burialSiteId,
m.cemeteryName,
l.burialSiteName,
t.burialSiteType,
s.burialSiteStatus
from BurialSites l
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where l.recordDelete_timeMillis is null
and l.burialSiteStatusId = ?`
sqlParameters.push(reportParameters.burialSiteStatusId) break
}
break case 'contracts-current-byCemeteryId': {
} sql = `select o.contractId,
l.burialSiteName,
m.cemeteryName,
ot.contractType,
o.contractStartDate,
o.contractEndDate
from Contracts o
left join ContractTypes ot on o.contractTypeId = ot.contractTypeId
left join BurialSites l on o.burialSiteId = l.burialSiteId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where o.recordDelete_timeMillis is null
and (o.contractEndDate is null or o.contractEndDate >= ?)
and l.cemeteryId = ?`
case 'burialSites-byCemeteryId': { sqlParameters.push(
sql = `select l.burialSiteId, dateToInteger(new Date()),
m.cemeteryName, reportParameters.cemeteryId
l.burialSiteName,
t.burialSiteType,
s.burialSiteStatus
from BurialSites l
left join BurialSiteTypes t on l.burialSiteTypeId = t.burialSiteTypeId
left join BurialSiteStatuses s on l.burialSiteStatusId = s.burialSiteStatusId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where l.recordDelete_timeMillis is null
and l.cemeteryId = ?`
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,
m.cemeteryName,
ot.contractType,
o.contractStartDate,
o.contractEndDate
from Contracts o
left join ContractTypes ot on o.contractTypeId = ot.contractTypeId
left join BurialSites l on o.burialSiteId = l.burialSiteId
left join Cemeteries m on l.cemeteryId = m.cemeteryId
where o.recordDelete_timeMillis is null
and (o.contractEndDate is null or o.contractEndDate >= ?)
and l.cemeteryId = ?`
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'
break
}
case 'contractTransactions-byTransactionDateString': {
sql = `select t.contractId, t.transactionIndex,
t.transactionDate, t.transactionTime,
t.transactionAmount,
t.externalReceiptNumber, t.transactionNote
from ContractTransactions t
where t.recordDelete_timeMillis is null
and t.transactionDate = ?`
sqlParameters.push(
dateStringToInteger(
reportParameters.transactionDateString as DateString
) )
)
break
}
case 'workOrders-all': { break
sql = 'select * from WorkOrders' }
break
}
case 'workOrders-open': { case 'contractInterments-byContractId': {
sql = `select w.workOrderId, w.workOrderNumber, sql = `select i.contractId, i.intermentNumber,
t.workOrderType, w.workOrderDescription, i.deceasedName, i.deceasedAddress1, i.deceasedAddress2,
w.workOrderOpenDate, i.deceasedCity, i.deceasedProvince, i.deceasedPostalCode,
m.workOrderMilestoneCount, m.workOrderMilestoneCompletionCount i.birthDate, i.birthPlace,
from WorkOrders w i.deathDate, i.deathPlace,
left join WorkOrderTypes t on w.workOrderTypeId = t.workOrderTypeId i.deathAge, i.deathAgePeriod
left join ( from ContractInterments i
select m.workOrderId, left join IntermentContainerTypes t on i.intermentContainerTypeId = t.intermentContainerTypeId
count(m.workOrderMilestoneId) as workOrderMilestoneCount, where i.recordDelete_timeMillis is null
sum(case when m.workOrderMilestoneCompletionDate is null then 0 else 1 end) as workOrderMilestoneCompletionCount and i.contractId = ?`
sqlParameters.push(reportParameters.contractId)
break
}
case 'contractTransactions-byTransactionDateString': {
sql = `select t.contractId, t.transactionIndex,
t.transactionDate, t.transactionTime,
t.transactionAmount,
t.externalReceiptNumber, t.transactionNote
from ContractTransactions t
where t.recordDelete_timeMillis is null
and t.transactionDate = ?`
sqlParameters.push(
dateStringToInteger(
reportParameters.transactionDateString as DateString
)
)
break
}
case 'workOrders-open': {
sql = `select w.workOrderId, w.workOrderNumber,
t.workOrderType, w.workOrderDescription,
w.workOrderOpenDate,
m.workOrderMilestoneCount, m.workOrderMilestoneCompletionCount
from WorkOrders w
left join WorkOrderTypes t on w.workOrderTypeId = t.workOrderTypeId
left join (
select m.workOrderId,
count(m.workOrderMilestoneId) as workOrderMilestoneCount,
sum(case when m.workOrderMilestoneCompletionDate is null then 0 else 1 end) as workOrderMilestoneCompletionCount
from WorkOrderMilestones m
where m.recordDelete_timeMillis is null
group by m.workOrderId
) m on w.workOrderId = m.workOrderId
where w.recordDelete_timeMillis is null
and w.workOrderCloseDate is null`
break
}
case 'workOrderMilestones-byWorkOrderId': {
sql = `select t.workOrderMilestoneType,
m.workOrderMilestoneDate,
m.workOrderMilestoneTime,
m.workOrderMilestoneDescription,
m.workOrderMilestoneCompletionDate,
m.workOrderMilestoneCompletionTime
from WorkOrderMilestones m from WorkOrderMilestones m
left join WorkOrderMilestoneTypes t on m.workOrderMilestoneTypeId = t.workOrderMilestoneTypeId
where m.recordDelete_timeMillis is null where m.recordDelete_timeMillis is null
group by m.workOrderId and m.workOrderId = ?`
) m on w.workOrderId = m.workOrderId
where w.recordDelete_timeMillis is null
and w.workOrderCloseDate is null`
break
}
case 'workOrderComments-all': { sqlParameters.push(reportParameters.workOrderId)
sql = 'select * from WorkOrderComments' break
break }
}
case 'workOrderBurialSites-all': { default: {
sql = 'select * from WorkOrderBurialSites' return undefined
break }
}
case 'workOrderMilestones-all': {
sql = 'select * from WorkOrderMilestones'
break
}
case 'workOrderMilestones-byWorkOrderId': {
sql = `select t.workOrderMilestoneType,
m.workOrderMilestoneDate,
m.workOrderMilestoneTime,
m.workOrderMilestoneDescription,
m.workOrderMilestoneCompletionDate,
m.workOrderMilestoneCompletionTime
from WorkOrderMilestones m
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() 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); const contractTypePrints = await getContractTypePrintsById(contract.contractTypeId);
response.render('contract-view', { response.render('contract-view', {
headTitle: 'Contract View', headTitle: `Contract #${contract.contractId.toString()}`,
contract, contract,
contractTypePrints contractTypePrints
}); });

View File

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

View File

@ -1,7 +1,5 @@
import { dateToString } from '@cityssm/utils-datetime'; import { dateToString } from '@cityssm/utils-datetime';
import getContracts from '../../database/getContracts.js';
import getWorkOrderMilestones from '../../database/getWorkOrderMilestones.js'; import getWorkOrderMilestones from '../../database/getWorkOrderMilestones.js';
import { getWorkOrders } from '../../database/getWorkOrders.js';
export default async function handler(_request, response) { export default async function handler(_request, response) {
const currentDateString = dateToString(new Date()); const currentDateString = dateToString(new Date());
const workOrderMilestones = await getWorkOrderMilestones({ const workOrderMilestones = await getWorkOrderMilestones({
@ -11,25 +9,8 @@ export default async function handler(_request, response) {
orderBy: 'completion', orderBy: 'completion',
includeWorkOrders: true 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', { response.render('dashboard', {
headTitle: 'Dashboard', headTitle: 'Dashboard',
workOrderMilestones, workOrderMilestones
workOrderCount: workOrderResults.count,
contractCount: contractResults.count
}); });
} }

View File

@ -1,9 +1,7 @@
import { dateToString } from '@cityssm/utils-datetime' import { dateToString } from '@cityssm/utils-datetime'
import type { Request, Response } from 'express' import type { Request, Response } from 'express'
import getContracts from '../../database/getContracts.js'
import getWorkOrderMilestones from '../../database/getWorkOrderMilestones.js' import getWorkOrderMilestones from '../../database/getWorkOrderMilestones.js'
import { getWorkOrders } from '../../database/getWorkOrders.js'
export default async function handler( export default async function handler(
_request: Request, _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', { response.render('dashboard', {
headTitle: 'Dashboard', headTitle: 'Dashboard',
workOrderMilestones, workOrderMilestones
workOrderCount: workOrderResults.count,
contractCount: contractResults.count
}) })
} }

View File

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

View File

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

14
package-lock.json generated
View File

@ -59,14 +59,12 @@
"@types/express-session": "^1.18.1", "@types/express-session": "^1.18.1",
"@types/http-errors": "^2.0.4", "@types/http-errors": "^2.0.4",
"@types/leaflet": "^1.9.16", "@types/leaflet": "^1.9.16",
"@types/mocha": "^10.0.10",
"@types/mssql": "^9.1.7", "@types/mssql": "^9.1.7",
"@types/node-windows": "^0.1.6", "@types/node-windows": "^0.1.6",
"@types/papaparse": "^5.3.15", "@types/papaparse": "^5.3.15",
"@types/randomcolor": "^0.5.9", "@types/randomcolor": "^0.5.9",
"@types/session-file-store": "^1.2.5", "@types/session-file-store": "^1.2.5",
"axe-core": "^4.10.3", "axe-core": "^4.10.3",
"bulma": "^1.0.3",
"cypress": "^14.2.0", "cypress": "^14.2.0",
"cypress-axe": "^1.6.0", "cypress-axe": "^1.6.0",
"eslint-config-cityssm": "^20.0.0", "eslint-config-cityssm": "^20.0.0",
@ -1831,8 +1829,9 @@
"version": "10.0.10", "version": "10.0.10",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
"integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
"devOptional": true, "license": "MIT",
"license": "MIT" "optional": true,
"peer": true
}, },
"node_modules/@types/ms": { "node_modules/@types/ms": {
"version": "0.7.31", "version": "0.7.31",
@ -3081,13 +3080,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/bulma-tooltip": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/bulma-tooltip/-/bulma-tooltip-3.0.2.tgz", "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:open": "cypress open --config-file cypress.config.js",
"cy:run": "cypress run --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", "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", "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" "temp:legacyImportFromCsv": "cross-env NODE_ENV=dev DEBUG=sunrise:* TEST_DATABASES=true node ./temp/legacyImportFromCsv/index.js"
}, },
"repository": { "repository": {
@ -82,14 +82,12 @@
"@types/express-session": "^1.18.1", "@types/express-session": "^1.18.1",
"@types/http-errors": "^2.0.4", "@types/http-errors": "^2.0.4",
"@types/leaflet": "^1.9.16", "@types/leaflet": "^1.9.16",
"@types/mocha": "^10.0.10",
"@types/mssql": "^9.1.7", "@types/mssql": "^9.1.7",
"@types/node-windows": "^0.1.6", "@types/node-windows": "^0.1.6",
"@types/papaparse": "^5.3.15", "@types/papaparse": "^5.3.15",
"@types/randomcolor": "^0.5.9", "@types/randomcolor": "^0.5.9",
"@types/session-file-store": "^1.2.5", "@types/session-file-store": "^1.2.5",
"axe-core": "^4.10.3", "axe-core": "^4.10.3",
"bulma": "^1.0.3",
"cypress": "^14.2.0", "cypress": "^14.2.0",
"cypress-axe": "^1.6.0", "cypress-axe": "^1.6.0",
"eslint-config-cityssm": "^20.0.0", "eslint-config-cityssm": "^20.0.0",

View File

@ -7,6 +7,9 @@
id="svg3" id="svg3"
sodipodi:docname="sunrise-cms.svg" sodipodi:docname="sunrise-cms.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)" 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:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -23,15 +26,16 @@
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:zoom="0.44790474" inkscape:zoom="0.07917912"
inkscape:cx="10007.708" inkscape:cx="4868.7078"
inkscape:cy="6998.1399" inkscape:cy="10880.394"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1009" inkscape:window-height="1009"
inkscape:window-x="-8" inkscape:window-x="-8"
inkscape:window-y="-8" inkscape:window-y="-8"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg3" /> inkscape:current-layer="svg3"
showgrid="false" />
<path <path
fill="#16272f" 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" 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', { cityssm.openHtmlModal('burialSite-editComment', {
onshow(modalElement) { onshow(modalElement) {
sunrise.populateAliases(modalElement); sunrise.populateAliases(modalElement);
modalElement.querySelector('#burialSiteCommentEdit--burialSiteId').value = burialSiteId; modalElement
modalElement.querySelector('#burialSiteCommentEdit--burialSiteCommentId').value = burialSiteCommentId.toString(); .querySelector('#burialSiteCommentEdit--burialSiteId')
?.setAttribute('value', burialSiteId);
modalElement
.querySelector('#burialSiteCommentEdit--burialSiteCommentId')
?.setAttribute('value', burialSiteCommentId.toString());
modalElement.querySelector('#burialSiteCommentEdit--comment').value = burialSiteComment.comment ?? ''; modalElement.querySelector('#burialSiteCommentEdit--comment').value = burialSiteComment.comment ?? '';
const commentDateStringElement = modalElement.querySelector('#burialSiteCommentEdit--commentDateString'); const commentDateStringElement = modalElement.querySelector('#burialSiteCommentEdit--commentDateString');
commentDateStringElement.value = commentDateStringElement.value =
@ -339,7 +343,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
cityssm.openHtmlModal('burialSite-addComment', { cityssm.openHtmlModal('burialSite-addComment', {
onshow(modalElement) { onshow(modalElement) {
sunrise.populateAliases(modalElement); sunrise.populateAliases(modalElement);
modalElement.querySelector('#burialSiteCommentAdd--burialSiteId').value = burialSiteId; modalElement
.querySelector('#burialSiteCommentAdd--burialSiteId')
?.setAttribute('value', burialSiteId);
modalElement modalElement
.querySelector('form') .querySelector('form')
?.addEventListener('submit', doAddComment); ?.addEventListener('submit', doAddComment);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -457,16 +457,14 @@ declare const bulmaJS: BulmaJS
tableRowElement tableRowElement
.querySelector('form') .querySelector('form')
?.addEventListener('submit', updateWorkOrderMilestoneType) ?.addEventListener('submit', updateWorkOrderMilestoneType)
;(
tableRowElement.querySelector( tableRowElement
'.button--moveWorkOrderMilestoneTypeUp' .querySelector('.button--moveWorkOrderMilestoneTypeUp')
) as HTMLButtonElement ?.addEventListener('click', moveWorkOrderMilestoneType)
).addEventListener('click', moveWorkOrderMilestoneType)
;( tableRowElement
tableRowElement.querySelector( .querySelector('.button--moveWorkOrderMilestoneTypeDown')
'.button--moveWorkOrderMilestoneTypeDown' ?.addEventListener('click', moveWorkOrderMilestoneType)
) as HTMLButtonElement
).addEventListener('click', moveWorkOrderMilestoneType)
tableRowElement tableRowElement
.querySelector('.button--deleteWorkOrderMilestoneType') .querySelector('.button--deleteWorkOrderMilestoneType')
@ -475,37 +473,36 @@ declare const bulmaJS: BulmaJS
containerElement.append(tableRowElement) containerElement.append(tableRowElement)
} }
} }
;(
document.querySelector(
'#form--addWorkOrderMilestoneType'
) as HTMLFormElement
).addEventListener('submit', (submitEvent: SubmitEvent) => {
submitEvent.preventDefault()
const formElement = submitEvent.currentTarget as HTMLFormElement document
.querySelector('#form--addWorkOrderMilestoneType')
?.addEventListener('submit', (submitEvent: SubmitEvent) => {
submitEvent.preventDefault()
cityssm.postJSON( const formElement = submitEvent.currentTarget as HTMLFormElement
`${sunrise.urlPrefix}/admin/doAddWorkOrderMilestoneType`,
formElement,
(rawResponseJSON) => {
const responseJSON =
rawResponseJSON as WorkOrderMilestoneTypeResponseJSON
if (responseJSON.success) { cityssm.postJSON(
workOrderMilestoneTypes = responseJSON.workOrderMilestoneTypes `${sunrise.urlPrefix}/admin/doAddWorkOrderMilestoneType`,
renderWorkOrderMilestoneTypes() formElement,
formElement.reset() (rawResponseJSON) => {
formElement.querySelector('input')?.focus() const responseJSON =
} else { rawResponseJSON as WorkOrderMilestoneTypeResponseJSON
bulmaJS.alert({
title: 'Error Adding Work Order Milestone Type', if (responseJSON.success) {
message: responseJSON.errorMessage ?? '', workOrderMilestoneTypes = responseJSON.workOrderMilestoneTypes
contextualColorName: 'danger' renderWorkOrderMilestoneTypes()
}) formElement.reset()
formElement.querySelector('input')?.focus()
} else {
bulmaJS.alert({
title: 'Error Adding Work Order Milestone Type',
message: responseJSON.errorMessage ?? '',
contextualColorName: 'danger'
})
}
} }
} )
) })
})
renderWorkOrderMilestoneTypes() renderWorkOrderMilestoneTypes()

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
import assert from 'node:assert' import assert from 'node:assert'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import { describe, it } from 'node:test'
import { initializeDatabase } from '../database/initializeDatabase.js' import { initializeDatabase } from '../database/initializeDatabase.js'
import { import {
@ -9,8 +10,8 @@ import {
useTestDatabases useTestDatabases
} from '../helpers/database.helpers.js' } from '../helpers/database.helpers.js'
describe('Initialize Database', () => { await describe('Initialize Database', async () => {
it('initializes the database', async () => { await it('initializes the database', async () => {
if (!useTestDatabases) { if (!useTestDatabases) {
assert.fail('Test database must be used!') 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 assert from 'node:assert';
import { exec } from 'node:child_process'; import { exec } from 'node:child_process';
import http from 'node:http'; 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 { app } from '../app.js';
import { portNumber } from './_globals.js'; import { portNumber } from './_globals.js';
function runCypress(browser, done) { function runCypress(browser, done) {
@ -23,7 +24,7 @@ function runCypress(browser, done) {
done(); done();
}); });
} }
describe('sunrise-cms', () => { await describe('sunrise-cms', async () => {
const httpServer = http.createServer(app); const httpServer = http.createServer(app);
let serverStarted = false; let serverStarted = false;
before(() => { before(() => {
@ -40,15 +41,19 @@ describe('sunrise-cms', () => {
// ignore // ignore
} }
}); });
it(`Ensure server starts on port ${portNumber.toString()}`, () => { await it(`Ensure server starts on port ${portNumber.toString()}`, () => {
assert.ok(serverStarted); assert.ok(serverStarted);
}); });
describe('Cypress tests', () => { await describe('Cypress tests', async () => {
it('Should run Cypress tests in Chrome', (done) => { await it('Should run Cypress tests in Chrome', {
timeout: hoursToMillis(1)
}, (context, done) => {
runCypress('chrome', 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); 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 assert from 'node:assert'
import { exec } from 'node:child_process' import { exec } from 'node:child_process'
import http from 'node:http' 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' 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) const httpServer = http.createServer(app)
let serverStarted = false 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) assert.ok(serverStarted)
}) })
describe('Cypress tests', () => { await describe('Cypress tests', async () => {
it('Should run Cypress tests in Chrome', (done) => { await it(
runCypress('chrome', done) 'Should run Cypress tests in Chrome',
}).timeout(minutesToMillis(30)) {
timeout: hoursToMillis(1)
},
(context, done) => {
runCypress('chrome', done)
}
)
it('Should run Cypress tests in Firefox', (done) => { await it(
runCypress('firefox', done) 'Should run Cypress tests in Firefox',
}).timeout(minutesToMillis(30)) {
timeout: hoursToMillis(1)
},
(context, done) => {
runCypress('firefox', done)
}
)
}) })
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -343,13 +343,19 @@
<div class="field"> <div class="field">
<label class="label" for="burialSite--burialSiteLatitude">Latitude</label> <label class="label" for="burialSite--burialSiteLatitude">Latitude</label>
<div class="control"> <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> </div>
<div class="field"> <div class="field">
<label class="label" for="burialSite--burialSiteLongitude">Longitude</label> <label class="label" for="burialSite--burialSiteLongitude">Longitude</label>
<div class="control"> <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> </div>
</div> </div>

View File

@ -83,247 +83,257 @@
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<form id="form--cemetery"> <form id="form--cemetery">
<input id="cemetery--cemeteryId" name="cemeteryId" type="hidden" value="<%= cemetery.cemeteryId %>" /> <input id="cemetery--cemeteryId" name="cemeteryId" type="hidden" value="<%= cemetery.cemeteryId %>" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div class="panel"> <div class="panel">
<div class="panel-block is-block"> <div class="panel-block is-block">
<div class="field"> <div class="field">
<label class="label" for="cemetery--cemeteryName">Cemetery Name</label> <label class="label" for="cemetery--cemeteryName">Cemetery Name</label>
<div class="control"> <div class="control">
<input class="input" id="cemetery--cemeteryName" name="cemeteryName" type="text" <input class="input" id="cemetery--cemeteryName" name="cemeteryName" type="text"
value="<%= cemetery.cemeteryName %>" maxlength="200" required value="<%= cemetery.cemeteryName %>" maxlength="200" required
accesskey="f" accesskey="f"
<%= (isCreate ? " autofocus" : "") %> /> <%= (isCreate ? " autofocus" : "") %> />
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="cemetery--cemeteryKey">Cemetery Key</label> <label class="label" for="cemetery--cemeteryKey">Cemetery Key</label>
<div class="control"> <div class="control">
<input class="input" id="cemetery--cemeteryKey" name="cemeteryKey" type="text" <input class="input" id="cemetery--cemeteryKey" name="cemeteryKey" type="text"
value="<%= cemetery.cemeteryKey %>" maxlength="20" required /> value="<%= cemetery.cemeteryKey %>" maxlength="20"
</div> <%= configFunctions.getConfigProperty('settings.burialSites.burialSiteNameSegments.includeCemeteryKey') ? ' required' : '' %> />
</div> <% if (configFunctions.getConfigProperty('settings.burialSites.burialSiteNameSegments.includeCemeteryKey')) { %>
<div class="field"> <p class="help">
<label class="label" for="cemetery--cemeteryDescription">Cemetery Description</label> The cemetery key is prepended to the burial site names.
<div class="control"> </p>
<textarea class="textarea" id="cemetery--cemeteryDescription" name="cemeteryDescription"><%= cemetery.cemeteryDescription %></textarea> <% } %>
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="cemetery--cemeteryDescription">Cemetery Description</label>
<div class="control">
<textarea class="textarea" id="cemetery--cemeteryDescription" name="cemeteryDescription"><%= cemetery.cemeteryDescription %></textarea>
</div> </div>
</div> </div>
</div> </div>
<div class="column"> </div>
<div class="panel">
<h2 class="panel-heading">Address</h2>
<div class="panel-block is-block">
<div class="field">
<label class="label" for="cemetery--cemeteryAddress1">Address</label>
<div class="control">
<input class="input" id="cemetery--cemeteryAddress1" name="cemeteryAddress1" type="text" value="<%= cemetery.cemeteryAddress1 %>" maxlength="50" placeholder="Line 1" />
</div>
</div>
<div class="field">
<div class="control">
<input class="input" id="cemetery--cemeteryAddress2" name="cemeteryAddress2" type="text" value="<%= cemetery.cemeteryAddress2 %>" maxlength="50" placeholder="Line 2" aria-label="Address Line 2" />
</div>
</div>
<div class="columns">
<div class="column is-8">
<div class="field">
<label class="label" for="cemetery--cemeteryCity">City</label>
<div class="control">
<input class="input" id="cemetery--cemeteryCity" name="cemeteryCity" value="<%= cemetery.cemeteryCity %>" maxlength="20" />
</div>
</div>
</div>
<div class="column">
<div class="field">
<label class="label" for="cemetery--cemeteryProvince">Province</label>
<div class="control">
<input class="input" id="cemetery--cemeteryProvince" name="cemeteryProvince" value="<%= cemetery.cemeteryProvince %>" maxlength="2" />
</div>
</div>
</div>
</div>
<div class="columns">
<div class="column">
<div class="field">
<label class="label" for="cemetery--cemeteryPostalCode">Postal Code</label>
<div class="control">
<input class="input" id="cemetery--cemeteryPostalCode" name="cemeteryPostalCode" value="<%= cemetery.cemeteryPostalCode %>" maxlength="7" />
</div>
</div>
</div>
<div class="column">
<div class="field">
<label class="label" for="cemetery--cemeteryPhoneNumber">Phone Number</label>
<div class="control">
<input class="input" id="cemetery--cemeteryPhoneNumber" name="cemeteryPhoneNumber" value="<%= cemetery.cemeteryPhoneNumber %>" maxlength="30" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<div class="columns"> <div class="column">
<div class="column"> <div class="panel">
<div class="panel"> <h2 class="panel-heading">Address</h2>
<h2 class="panel-heading">Geographic Location</h2> <div class="panel-block is-block">
<div class="panel-block is-block">
<div class="field"> <div class="field">
<label class="label" for="cemetery--cemeteryLatitude">Latitude</label> <label class="label" for="cemetery--cemeteryAddress1">Address</label>
<div class="control"> <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--cemeteryAddress1" name="cemeteryAddress1" type="text" value="<%= cemetery.cemeteryAddress1 %>" maxlength="50" placeholder="Line 1" />
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="cemetery--cemeteryLongitude">Longitude</label>
<div class="control"> <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--cemeteryAddress2" name="cemeteryAddress2" type="text" value="<%= cemetery.cemeteryAddress2 %>" maxlength="50" placeholder="Line 2" aria-label="Address Line 2" />
</div> </div>
</div> </div>
</div> <div class="columns">
</div> <div class="column is-8">
</div> <div class="field">
<div class="column"> <label class="label" for="cemetery--cemeteryCity">City</label>
<div class="panel"> <div class="control">
<h2 class="panel-heading">Image</h2> <input class="input" id="cemetery--cemeteryCity" name="cemeteryCity" value="<%= cemetery.cemeteryCity %>" maxlength="20" />
<div class="panel-block is-block"> </div>
<div class="field"> </div>
<label class="label" for="cemetery--cemeterySvg">SVG File</label> </div>
<div class="control"> <div class="column">
<div class="select is-fullwidth"> <div class="field">
<select id="cemetery--cemeterySvg" name="cemeterySvg"> <label class="label" for="cemetery--cemeteryProvince">Province</label>
<option value="">(Select a File)</option> <div class="control">
<% for (const cemeterySVG of cemeterySVGs) { %> <input class="input" id="cemetery--cemeteryProvince" name="cemeteryProvince" value="<%= cemetery.cemeteryProvince %>" maxlength="2" />
<option value="<%= cemeterySVG %>" <%= (cemetery.cemeterySvg === cemeterySVG) ? " selected" : "" %>> </div>
<%= cemeterySVG %>
</option>
<% } %>
</select>
</div> </div>
</div> </div>
</div> </div>
<div class="columns">
<div class="column">
<div class="field">
<label class="label" for="cemetery--cemeteryPostalCode">Postal Code</label>
<div class="control">
<input class="input" id="cemetery--cemeteryPostalCode" name="cemeteryPostalCode" value="<%= cemetery.cemeteryPostalCode %>" maxlength="7" />
</div>
</div>
</div>
<div class="column">
<div class="field">
<label class="label" for="cemetery--cemeteryPhoneNumber">Phone Number</label>
<div class="control">
<input class="input" id="cemetery--cemeteryPhoneNumber" name="cemeteryPhoneNumber" value="<%= cemetery.cemeteryPhoneNumber %>" maxlength="30" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns">
<div class="column">
<div class="panel">
<h2 class="panel-heading">Geographic Location</h2>
<div class="panel-block is-block">
<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="<%= 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="<%= configFunctions.getConfigProperty('settings.longitudeMin') %>"
max="<%= configFunctions.getConfigProperty('settings.longitudeMax') %>"
step="0.00000001" value="<%= cemetery.cemeteryLongitude %>" />
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
<div class="column">
<div class="panel">
<h2 class="panel-heading">Image</h2>
<div class="panel-block is-block">
<div class="field">
<label class="label" for="cemetery--cemeterySvg">SVG File</label>
<div class="control">
<div class="select is-fullwidth">
<select id="cemetery--cemeterySvg" name="cemeterySvg">
<option value="">(Select a File)</option>
<% for (const cemeterySVG of cemeterySVGs) { %>
<option value="<%= cemeterySVG %>" <%= (cemetery.cemeterySvg === cemeterySVG) ? " selected" : "" %>>
<%= cemeterySVG %>
</option>
<% } %>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form> </form>
<% if (!isCreate) { %> <% if (!isCreate) { %>
<% const burialSiteSearchUrl = urlPrefix + "/burialSites?cemeteryId=" + cemetery.cemeteryId; %> <% const burialSiteSearchUrl = urlPrefix + "/burialSites?cemeteryId=" + cemetery.cemeteryId; %>
<div class="panel mt-4"> <div class="panel mt-4">
<div class="panel-heading"> <div class="panel-heading">
<div class="level is-mobile"> <div class="level is-mobile">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
<h2 class="title is-5 has-text-white has-text-weight-bold"> <h2 class="title is-5 has-text-white has-text-weight-bold">
Burial Site Summaries Burial Site Summaries
<a class="tag is-link ml-2" href="<%= burialSiteSearchUrl %>"> <a class="tag is-link ml-2" href="<%= burialSiteSearchUrl %>">
<%= cemetery.burialSiteCount %> <%= cemetery.burialSiteCount %>
</a>
</h2>
</div>
</div>
<div class="level-right">
<div class="level-item">
<a class="button is-small is-success has-text-weight-normal" href="<%=urlPrefix %>/burialSites/new?cemeteryId=<%= cemetery.cemeteryId %>">
<span class="icon"><i class="fas fa-plus" aria-hidden="true"></i></span>
<span>Create a Burial Site</span>
</a>
</div>
<div class="level-item">
<a class="button is-small is-link has-text-weight-normal" href="<%=urlPrefix %>/reports/burialSites-byCemeteryId?cemeteryId=<%= cemetery.cemeteryId %>" download>
<span class="icon"><i class="fas fa-download" aria-hidden="true"></i></span>
<span>Export All</span>
</a> </a>
</div> </h2>
</div>
</div>
<div class="level-right">
<div class="level-item">
<a class="button is-small is-success has-text-weight-normal" href="<%=urlPrefix %>/burialSites/new?cemeteryId=<%= cemetery.cemeteryId %>">
<span class="icon"><i class="fas fa-plus" aria-hidden="true"></i></span>
<span>Create a Burial Site</span>
</a>
</div>
<div class="level-item">
<a class="button is-small is-link has-text-weight-normal" href="<%=urlPrefix %>/reports/burialSites-byCemeteryId?cemeteryId=<%= cemetery.cemeteryId %>" download>
<span class="icon"><i class="fas fa-download" aria-hidden="true"></i></span>
<span>Export All</span>
</a>
</div> </div>
</div> </div>
</div> </div>
<div class="panel-block is-block"> </div>
<% if (cemetery.burialSiteCount === 0) { %> <div class="panel-block is-block">
<div class="message is-info"> <% if (cemetery.burialSiteCount === 0) { %>
<p class="message-body"> <div class="message is-info">
There are no burial sites <p class="message-body">
associated with this cemetery. There are no burial sites
</p> associated with this cemetery.
</div> </p>
<% } else { %> </div>
<div class="columns"> <% } else { %>
<div class="column"> <div class="columns">
<table class="table is-fullwidth is-striped is-hoverable"> <div class="column">
<thead> <table class="table is-fullwidth is-striped is-hoverable">
<thead>
<tr>
<th>Type</th>
<th class="has-text-right">
Burial Site Count
</th>
<th class="has-text-right">Percentage</th>
</tr>
</thead>
<tbody>
<% for (const burialSiteType of burialSiteTypeSummary) { %>
<tr> <tr>
<th>Type</th> <td>
<th class="has-text-right"> <a class="has-text-weight-bold" href="<%= burialSiteSearchUrl %>&burialSiteTypeId=<%= burialSiteType.burialSiteTypeId %>">
Burial Site Count <%= burialSiteType.burialSiteType %>
</th> </a>
<th class="has-text-right">Percentage</th> </td>
<td class="has-text-right">
<%= burialSiteType.burialSiteCount %>
</td>
<td class="has-text-right">
<%= ((burialSiteType.burialSiteCount / cemetery.burialSiteCount) * 100).toFixed(1) %>%
</td>
</tr> </tr>
</thead> <% } %>
<tbody> </tbody>
<% for (const burialSiteType of burialSiteTypeSummary) { %> </table>
<tr>
<td>
<a class="has-text-weight-bold" href="<%= burialSiteSearchUrl %>&burialSiteTypeId=<%= burialSiteType.burialSiteTypeId %>">
<%= burialSiteType.burialSiteType %>
</a>
</td>
<td class="has-text-right">
<%= burialSiteType.burialSiteCount %>
</td>
<td class="has-text-right">
<%= ((burialSiteType.burialSiteCount / cemetery.burialSiteCount) * 100).toFixed(1) %>%
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
<div class="column">
<table class="table is-fullwidth is-striped is-hoverable">
<thead>
<tr>
<th>Status</th>
<th class="has-text-right">
Burial Site Count
</th>
<th class="has-text-right">Percentage</th>
</tr>
</thead>
<tbody>
<% for (const burialSiteStatus of burialSiteStatusSummary) { %>
<tr>
<td>
<a class="has-text-weight-bold" href="<%= burialSiteSearchUrl %>&burialSiteStatusId=<%= burialSiteStatus.burialSiteStatusId %>">
<%= burialSiteStatus.burialSiteStatus %>
</a>
</td>
<td class="has-text-right">
<%= burialSiteStatus.burialSiteCount %>
</td>
<td class="has-text-right">
<%= ((burialSiteStatus.burialSiteCount / cemetery.burialSiteCount) * 100).toFixed(1) %>%
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div> </div>
<% } %> <div class="column">
</div> <table class="table is-fullwidth is-striped is-hoverable">
</div> <thead>
<tr>
<th>Status</th>
<th class="has-text-right">
Burial Site Count
</th>
<th class="has-text-right">Percentage</th>
</tr>
</thead>
<tbody>
<% for (const burialSiteStatus of burialSiteStatusSummary) { %>
<tr>
<td>
<a class="has-text-weight-bold" href="<%= burialSiteSearchUrl %>&burialSiteStatusId=<%= burialSiteStatus.burialSiteStatusId %>">
<%= burialSiteStatus.burialSiteStatus %>
</a>
</td>
<td class="has-text-right">
<%= burialSiteStatus.burialSiteCount %>
</td>
<td class="has-text-right">
<%= ((burialSiteStatus.burialSiteCount / cemetery.burialSiteCount) * 100).toFixed(1) %>%
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
<% } %>
</div>
</div>
<% } %> <% } %>
<%- include('_footerA'); -%> <%- include('_footerA'); -%>

View File

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

View File

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

View File

@ -94,30 +94,23 @@
<div class="column"> <div class="column">
<div class="columns is-desktop"> <div class="columns is-desktop">
<div class="column"> <div class="column">
<div class="card is-hover-container"> <div class="panel">
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/workOrders">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<span class="fa-layers fa-4x fa-fw"> <i class="fa-solid fa-4x fa-fw fa-hard-hat" aria-hidden="true"></i>
<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>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/workOrders"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Work Orders Work Orders
</h2> </h2>
<p> <p>
View and maintain work orders.<br /> View and maintain work orders.
<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>
</p> </p>
</a> </div>
</div> </div>
</a>
<div class="panel-block is-block">
<% if (user.userProperties.canUpdate) { %> <% if (user.userProperties.canUpdate) { %>
<a class="button is-fullwidth is-success is-light mb-2" href="<%= urlPrefix %>/workOrders/new"> <a class="button is-fullwidth is-success is-light mb-2" href="<%= urlPrefix %>/workOrders/new">
<span class="icon"> <span class="icon">
@ -137,31 +130,26 @@
</div> </div>
<div class="column"> <div class="column">
<div class="card is-hover-container"> <div class="panel">
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/contracts">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true"> <span class="fa-layers fa-4x fa-fw" aria-hidden="true">
<i class="fas fa-vector-square"></i> <i class="fas fa-vector-square"></i>
<i class="fas fa-user" data-fa-transform="shrink-10"></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> </span>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/contracts"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Contracts Contracts
</h2> </h2>
<p> <p>
View and maintain current and past contracts.<br /> View and maintain current and past contracts.
<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>
</p> </p>
</a> </div>
</div> </div>
</a>
<div class="panel-block is-block">
<% if (user.userProperties.canUpdate) { %> <% if (user.userProperties.canUpdate) { %>
<a class="button is-fullwidth is-success is-light mb-2" href="<%= urlPrefix %>/contracts/new"> <a class="button is-fullwidth is-success is-light mb-2" href="<%= urlPrefix %>/contracts/new">
<span class="icon"> <span class="icon">
@ -182,89 +170,98 @@
</div> </div>
<div class="columns is-desktop"> <div class="columns is-desktop">
<div class="column"> <div class="column">
<div class="card is-hover-container"> <div class="panel">
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/burialSites">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<i class="fas fa-4x fa-fw fa-vector-square" aria-hidden="true"></i> <i class="fas fa-4x fa-fw fa-vector-square" aria-hidden="true"></i>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/burialSites"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Burial Sites Burial Sites
</h2> </h2>
<p> <p>
View and maintain burial sites within a cemetery.<br /> View and maintain burial sites within a cemetery.
<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>
</p> </p>
</a> </div>
</div> </div>
<% if (user.userProperties.canUpdate) { %> </a>
<% if (user.userProperties.canUpdate) { %>
<div class="panel-block is-block">
<a class="button is-fullwidth is-success is-light" href="<%= urlPrefix %>/burialSites/new"> <a class="button is-fullwidth is-success is-light" href="<%= urlPrefix %>/burialSites/new">
<span class="icon"> <span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i> <i class="fas fa-plus" aria-hidden="true"></i>
</span> </span>
<span>New Burial Site</span> <span>New Burial Site</span>
</a> </a>
<% } %> </div>
</div> <% } %>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<div class="card is-hover-container"> <div class="panel">
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/cemeteries">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<i class="far fa-4x fa-fw fa-map" aria-hidden="true"></i> <i class="far fa-4x fa-fw fa-map" aria-hidden="true"></i>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/cemeteries"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Cemeteries Cemeteries
</h2> </h2>
<p> <p>
View and maintain cemeteries. 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> </p>
</a> </div>
</div> </div>
<% if (user.userProperties.canUpdate) { %> </a>
<% if (user.userProperties.canUpdate) { %>
<div class="panel-block is-block">
<a class="button is-fullwidth is-success is-light" href="<%= urlPrefix %>/cemeteries/new"> <a class="button is-fullwidth is-success is-light" href="<%= urlPrefix %>/cemeteries/new">
<span class="icon"> <span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i> <i class="fas fa-plus" aria-hidden="true"></i>
</span> </span>
<span>New Cemetery</span> <span>New Cemetery</span>
</a> </a>
<% } %> </div>
</div> <% } %>
</div> </div>
</div> </div>
</div> </div>
<div class="columns is-desktop"> <div class="columns is-desktop">
<div class="column"> <div class="column">
<div class="card"> <div class="panel">
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/reports">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<i class="fas fa-4x fa-fw fa-file" aria-hidden="true"></i> <i class="fas fa-4x fa-fw fa-file" aria-hidden="true"></i>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/reports"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Report Library Report Library
</h2> </h2>
<p>Produce reports and export data.</p> <p>Produce reports and export data.</p>
</a> </div>
</div> </div>
</div> </a>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<div class="card is-hover-container"> <div class="panel">
<div class="card-content"> <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" <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"> href="https://github.com/cityssm/sunrise-cms" target="_blank" rel="noreferrer">
<span class="icon"> <span class="icon">
@ -278,10 +275,11 @@
</div> </div>
<% if (user.userProperties.isAdmin) { %> <% if (user.userProperties.isAdmin) { %>
<h2 class="title is-3">Administrator Tools</h2> <div class="panel">
<div class="panel-heading">
<div class="card"> Administrator Tools
<div class="card-content"> </div>
<a class="panel-block" href="<%= urlPrefix %>/admin/fees">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true"> <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> <i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span> </span>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/admin/fees"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Fee Management Fee Management
</h2> </h2>
@ -297,10 +295,10 @@
Manage fees for contracts Manage fees for contracts
and specific burial site types. and specific burial site types.
</p> </p>
</a> </div>
</div> </div>
</div> </a>
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/admin/contractTypes">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true"> <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> <i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span> </span>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/admin/contractTypes"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Contract Type Management Contract Type Management
</h2> </h2>
@ -317,10 +315,10 @@
the fields associated with them, the fields associated with them,
and their available print options. and their available print options.
</p> </p>
</a> </div>
</div> </div>
</div> </a>
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/admin/burialSiteTypes">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true"> <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> <i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span> </span>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/admin/burialSiteTypes"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Burial Site Type Management Burial Site Type Management
</h2> </h2>
<p> <p>
Manage burial site types and fields associated with them. Manage burial site types and fields associated with them.
</p> </p>
</a> </div>
</div> </div>
</div> </a>
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/admin/tables">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<span class="fa-layers fa-4x fa-fw" aria-hidden="true"> <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> <i class="fas fa-cog" data-fa-transform="shrink-8 right-8 down-5" data-fa-glow="10"></i>
</span> </span>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/admin/tables"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Config Table Management Config Table Management
</h2> </h2>
@ -355,15 +353,15 @@
work order types work order types
and burial site statuses. and burial site statuses.
</p> </p>
</a> </div>
</div> </div>
</div> </a>
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/admin/database">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<i class="fas fa-4x fa-fw fa-database" aria-hidden="true"></i> <i class="fas fa-4x fa-fw fa-database" aria-hidden="true"></i>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/admin/database"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Database Maintenance Database Maintenance
</h2> </h2>
@ -371,25 +369,25 @@
Backup the database before making significant updates. Backup the database before making significant updates.
Permanently delete records that have been previously deleted from the database. Permanently delete records that have been previously deleted from the database.
</p> </p>
</a> </div>
</div> </div>
</div> </a>
<% if (configFunctions.getConfigProperty("application.ntfyStartup")) { %> <% if (configFunctions.getConfigProperty("application.ntfyStartup")) { %>
<div class="card-content"> <a class="panel-block" href="<%= urlPrefix %>/admin/ntfyStartup">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<i class="far fa-4x fa-fw fa-comment-alt" aria-hidden="true"></i> <i class="far fa-4x fa-fw fa-comment-alt" aria-hidden="true"></i>
</div> </div>
<a class="media-content" href="<%= urlPrefix %>/admin/ntfyStartup"> <div class="media-content">
<h2 class="title is-4 mb-0 has-text-link"> <h2 class="title is-4 mb-0 has-text-link">
Ntfy Startup Notification Ntfy Startup Notification
</h2> </h2>
<p> <p>
Subscribe to application startup notifications on a phone or a desktop computer. Subscribe to application startup notifications on a phone or a desktop computer.
</p> </p>
</a> </div>
</div> </div>
</div> </a>
<% } %> <% } %>
</div> </div>
<% } %> <% } %>

View File

@ -83,7 +83,7 @@
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<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> <p>
All active work orders without completion dates. All active work orders without completion dates.
</p> </p>
@ -101,7 +101,7 @@
<span class="tag is-info">ICS</span> <span class="tag is-info">ICS</span>
</div> </div>
<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> <p>
Upcoming and recently passed work order milestones, Upcoming and recently passed work order milestones,
compatible with Microsoft Outlook and other calendar tools. compatible with Microsoft Outlook and other calendar tools.
@ -118,40 +118,40 @@
<div class="panel"> <div class="panel">
<form class="panel-block align-items-flex-start" method="get" action="<%= urlPrefix %>/reports/contracts-current-byCemeteryId"> <form class="panel-block align-items-flex-start" method="get" action="<%= urlPrefix %>/reports/contracts-current-byCemeteryId">
<div class="has-text-centered my-2 ml-2 mr-3"> <div class="has-text-centered my-2 ml-2 mr-3">
<span class="icon has-text-info"> <span class="icon has-text-info">
<i class="fas fa-2x fa-file" aria-hidden="true"></i> <i class="fas fa-2x fa-file" aria-hidden="true"></i>
</span><br /> </span><br />
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<div> <div>
<h2 class="title is-5 is-marginless"> <h2 class="title is-5 mb-0">
Current Contract By Cemetery Current Contract By Cemetery
</h2> </h2>
<div class="field has-addons mt-2"> <div class="field has-addons mt-2">
<div class="control"> <div class="control">
<label class="button is-small is-static" for="contracts-current-byCemeteryId--cemeteryId"> <label class="button is-small is-static" for="contracts-current-byCemeteryId--cemeteryId">
Cemetery Cemetery
</label> </label>
</div> </div>
<div class="control is-expanded"> <div class="control is-expanded">
<div class="select is-small is-fullwidth"> <div class="select is-small is-fullwidth">
<select id="contracts-current-byCemeteryId--cemeteryId" name="cemeteryId"> <select id="contracts-current-byCemeteryId--cemeteryId" name="cemeteryId">
<% for (const cemetery of cemeteries) { %> <% for (const cemetery of cemeteries) { %>
<option value="<%= cemetery.cemeteryId %>"> <option value="<%= cemetery.cemeteryId %>">
<%= cemetery.cemeteryName || "(No Name)" %> <%= cemetery.cemeteryName || "(No Name)" %>
</option> </option>
<% } %> <% } %>
</select> </select>
</div>
</div>
<div class="control">
<button class="button is-small is-primary" type="submit">
Export
</button>
</div>
</div> </div>
</div>
<div class="control">
<button class="button is-small is-primary" type="submit">
Export
</button>
</div>
</div> </div>
</div>
</form> </form>
<form class="panel-block align-items-flex-start" method="get" action="<%= urlPrefix %>/reports/contractTransactions-byTransactionDateString"> <form class="panel-block align-items-flex-start" method="get" action="<%= urlPrefix %>/reports/contractTransactions-byTransactionDateString">
<div class="has-text-centered my-2 ml-2 mr-3"> <div class="has-text-centered my-2 ml-2 mr-3">
@ -161,7 +161,7 @@
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<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="field has-addons mt-2">
<div class="control"> <div class="control">
<label class="button is-small is-static" for="contractTransactions-byTransactionDateString--transactionDateString"> <label class="button is-small is-static" for="contractTransactions-byTransactionDateString--transactionDateString">
@ -193,7 +193,7 @@
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<div> <div>
<h2 class="title is-5 is-marginless"> <h2 class="title is-5 mb-0">
Burial Sites By Cemetery Burial Sites By Cemetery
</h2> </h2>
<div class="field has-addons mt-2"> <div class="field has-addons mt-2">
@ -229,7 +229,7 @@
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<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="field has-addons mt-2">
<div class="control"> <div class="control">
<label class="button is-small is-static" for="burialSites-byBurialSiteTypeId--burialSiteTypeId"> <label class="button is-small is-static" for="burialSites-byBurialSiteTypeId--burialSiteTypeId">
@ -263,29 +263,29 @@
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<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="field has-addons mt-2">
<div class="control"> <div class="control">
<label class="button is-small is-static" for="burialSites-byBurialSiteStatusId--burialSiteStatusId"> <label class="button is-small is-static" for="burialSites-byBurialSiteStatusId--burialSiteStatusId">
Burial Site Status Burial Site Status
</label> </label>
</div> </div>
<div class="control is-expanded"> <div class="control is-expanded">
<div class="select is-small is-fullwidth"> <div class="select is-small is-fullwidth">
<select id="burialSites-byBurialSiteStatusId--burialSiteStatusId" name="burialSiteStatusId"> <select id="burialSites-byBurialSiteStatusId--burialSiteStatusId" name="burialSiteStatusId">
<% for (const burialSiteStatus of burialSiteStatuses) { %> <% for (const burialSiteStatus of burialSiteStatuses) { %>
<option value="<%= burialSiteStatus.burialSiteStatusId %>"> <option value="<%= burialSiteStatus.burialSiteStatusId %>">
<%= burialSiteStatus.burialSiteStatus %> <%= burialSiteStatus.burialSiteStatus %>
</option> </option>
<% } %> <% } %>
</select> </select>
</div>
</div>
<div class="control">
<button class="button is-small is-primary" type="submit">
Export
</button>
</div> </div>
</div>
<div class="control">
<button class="button is-small is-primary" type="submit">
Export
</button>
</div>
</div> </div>
</div> </div>
</form> </form>
@ -302,7 +302,7 @@
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<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> <p>
All active cemeteries. All active cemeteries.
</p> </p>
@ -368,26 +368,37 @@
</div> </div>
</a> </a>
<a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractInterments-all" download> <a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractInterments-all" download>
<div class="has-text-centered my-2 ml-2 mr-3"> <div class="has-text-centered my-2 ml-2 mr-3">
<span class="icon has-text-info"> <span class="icon has-text-info">
<i class="fas fa-2x fa-table" aria-hidden="true"></i> <i class="fas fa-2x fa-table" aria-hidden="true"></i>
</span><br /> </span><br />
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<div> <div>
<h3 class="title is-5 is-marginless">Full ContractInterments Table</h3> <h3 class="title is-5 is-marginless">Full ContractInterments Table</h3>
</div> </div>
</a> </a>
<a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractTransactions-all" download> <a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractTransactions-all" download>
<div class="has-text-centered my-2 ml-2 mr-3"> <div class="has-text-centered my-2 ml-2 mr-3">
<span class="icon has-text-info"> <span class="icon has-text-info">
<i class="fas fa-2x fa-table" aria-hidden="true"></i> <i class="fas fa-2x fa-table" aria-hidden="true"></i>
</span><br /> </span><br />
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<div> <div>
<h3 class="title is-5 is-marginless">Full ContractTransactions Table</h3> <h3 class="title is-5 is-marginless">Full ContractTransactions Table</h3>
</div> </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> </a>
</div> </div>
</div> </div>
@ -565,26 +576,37 @@
<div class="panel"> <div class="panel">
<h2 class="panel-heading">Contract Tables</h2> <h2 class="panel-heading">Contract Tables</h2>
<a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractTypes-all" download> <a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractTypes-all" download>
<div class="has-text-centered my-2 ml-2 mr-3"> <div class="has-text-centered my-2 ml-2 mr-3">
<span class="icon has-text-info"> <span class="icon has-text-info">
<i class="fas fa-2x fa-table" aria-hidden="true"></i> <i class="fas fa-2x fa-table" aria-hidden="true"></i>
</span><br /> </span><br />
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<div> <div>
<h3 class="title is-5 is-marginless">Full ContractTypes Table</h3> <h3 class="title is-5 is-marginless">Full ContractTypes Table</h3>
</div> </div>
</a> </a>
<a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractTypeFields-all" download> <a class="panel-block align-items-flex-start" href="<%= urlPrefix %>/reports/contractTypeFields-all" download>
<div class="has-text-centered my-2 ml-2 mr-3"> <div class="has-text-centered my-2 ml-2 mr-3">
<span class="icon has-text-info"> <span class="icon has-text-info">
<i class="fas fa-2x fa-table" aria-hidden="true"></i> <i class="fas fa-2x fa-table" aria-hidden="true"></i>
</span><br /> </span><br />
<span class="tag is-info">CSV</span> <span class="tag is-info">CSV</span>
</div> </div>
<div> <div>
<h3 class="title is-5 is-marginless">Full ContractTypeFields Table</h3> <h3 class="title is-5 is-marginless">Full ContractTypeFields Table</h3>
</div> </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> </a>
</div> </div>
<div class="panel"> <div class="panel">