development and linting
parent
66a697e097
commit
922603a1f2
|
|
@ -1,4 +1,4 @@
|
||||||
# Thank you for your interest in making the Lot Occupancy System better
|
# Thank you for your interest in making Sunrise CMS better
|
||||||
|
|
||||||
Together, we can build high quality software that meets the needs of municipalities,
|
Together, we can build high quality software that meets the needs of municipalities,
|
||||||
while remaining open and budget conscious.
|
while remaining open and budget conscious.
|
||||||
|
|
|
||||||
27
README.md
27
README.md
|
|
@ -15,15 +15,32 @@
|
||||||
|
|
||||||
This is a major refactoring of the now archived
|
This is a major refactoring of the now archived
|
||||||
[Lot Occupancy System](https://github.com/cityssm/lot-occupancy-system),
|
[Lot Occupancy System](https://github.com/cityssm/lot-occupancy-system),
|
||||||
originally built with multiple focuses. This fork completely reworks the project
|
originally built with multiple focuses in mind. This fork completely reworks the project
|
||||||
to focus exculsively on cemetery management.
|
to focus exculsively on cemetery management.
|
||||||
|
|
||||||
|
## Why Sunrise CMS?
|
||||||
|
|
||||||
|
### ✔️ Cemetery Maps are NOT Required
|
||||||
|
|
||||||
|
Many cemetery applications rely on maps to drill down into burial sites.
|
||||||
|
In Sunrise CMS, maps are completely optional, greatly reducing the effort needed to get started.
|
||||||
|
|
||||||
|
### ✔️ The System Requirements are Very Low
|
||||||
|
|
||||||
|
Sunrise CMS does not need an expensive server to run.
|
||||||
|
No separate database server is required either.
|
||||||
|
The whole application could run on a current, modest workstation.
|
||||||
|
|
||||||
|
### ✔️ Sunrise CMS is Free and Open Source
|
||||||
|
|
||||||
|
There's no cost to use it.
|
||||||
|
|
||||||
## About this Project
|
## About this Project
|
||||||
|
|
||||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
- 🤗 [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
- [Contributing Guidelines](CONTRIBUTING.md)
|
- 🥰 [Contributing Guidelines](CONTRIBUTING.md)
|
||||||
- [Security Policy](SECURITY.md)
|
- 🛡️ [Security Policy](SECURITY.md)
|
||||||
- [MIT Licence](LICENSE.md)
|
- 📃 [MIT Licence](LICENSE.md)
|
||||||
|
|
||||||
Although the system is quite niche, it's being released in an open source environment in hopes to pool developer resources from other municipalities looking to move away from older, legacy systems.
|
Although the system is quite niche, it's being released in an open source environment in hopes to pool developer resources from other municipalities looking to move away from older, legacy systems.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,36 @@ import { config as cemeteryConfig } from './config.baseOntario.js';
|
||||||
export const config = { ...cemeteryConfig };
|
export const config = { ...cemeteryConfig };
|
||||||
config.aliases.externalReceiptNumber = 'GP Receipt Number';
|
config.aliases.externalReceiptNumber = 'GP Receipt Number';
|
||||||
config.settings.burialSites.burialSiteNameSegments = {
|
config.settings.burialSites.burialSiteNameSegments = {
|
||||||
separator: '-',
|
|
||||||
includeCemeteryKey: true,
|
includeCemeteryKey: true,
|
||||||
|
separator: '-',
|
||||||
segments: {
|
segments: {
|
||||||
1: {
|
1: {
|
||||||
isRequired: false,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: false,
|
||||||
label: 'Block',
|
label: 'Block',
|
||||||
minLength: 1,
|
maxLength: 1,
|
||||||
maxLength: 1
|
minLength: 1
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
isRequired: false,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: false,
|
||||||
label: 'Range',
|
label: 'Range',
|
||||||
minLength: 1,
|
maxLength: 3,
|
||||||
maxLength: 3
|
minLength: 1
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
isRequired: true,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: true,
|
||||||
label: 'Lot',
|
label: 'Lot',
|
||||||
minLength: 1,
|
maxLength: 4,
|
||||||
maxLength: 4
|
minLength: 1
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
isRequired: true,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: true,
|
||||||
label: 'Grave',
|
label: 'Grave',
|
||||||
minLength: 1,
|
maxLength: 2,
|
||||||
maxLength: 2
|
minLength: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,36 +7,37 @@ export const config: Config = { ...cemeteryConfig }
|
||||||
config.aliases.externalReceiptNumber = 'GP Receipt Number'
|
config.aliases.externalReceiptNumber = 'GP Receipt Number'
|
||||||
|
|
||||||
config.settings.burialSites.burialSiteNameSegments = {
|
config.settings.burialSites.burialSiteNameSegments = {
|
||||||
separator: '-',
|
|
||||||
includeCemeteryKey: true,
|
includeCemeteryKey: true,
|
||||||
|
separator: '-',
|
||||||
|
|
||||||
segments: {
|
segments: {
|
||||||
1: {
|
1: {
|
||||||
isRequired: false,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: false,
|
||||||
label: 'Block',
|
label: 'Block',
|
||||||
minLength: 1,
|
maxLength: 1,
|
||||||
maxLength: 1
|
minLength: 1
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
isRequired: false,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: false,
|
||||||
label: 'Range',
|
label: 'Range',
|
||||||
minLength: 1,
|
maxLength: 3,
|
||||||
maxLength: 3
|
minLength: 1
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
isRequired: true,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: true,
|
||||||
label: 'Lot',
|
label: 'Lot',
|
||||||
minLength: 1,
|
maxLength: 4,
|
||||||
maxLength: 4
|
minLength: 1
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
isRequired: true,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: true,
|
||||||
label: 'Grave',
|
label: 'Grave',
|
||||||
minLength: 1,
|
maxLength: 2,
|
||||||
maxLength: 2
|
minLength: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ export declare const configDefaultValues: {
|
||||||
'application.httpPort': number;
|
'application.httpPort': number;
|
||||||
'application.logoURL': string;
|
'application.logoURL': string;
|
||||||
'application.maximumProcesses': number;
|
'application.maximumProcesses': number;
|
||||||
|
'application.ntfyStartup': ConfigNtfyStartup | undefined;
|
||||||
'application.userDomain': string;
|
'application.userDomain': string;
|
||||||
'application.useTestDatabases': boolean;
|
'application.useTestDatabases': boolean;
|
||||||
'application.ntfyStartup': ConfigNtfyStartup | undefined;
|
|
||||||
'reverseProxy.disableCompression': boolean;
|
'reverseProxy.disableCompression': boolean;
|
||||||
'reverseProxy.disableEtag': boolean;
|
'reverseProxy.disableEtag': boolean;
|
||||||
'reverseProxy.urlPrefix': string;
|
'reverseProxy.urlPrefix': string;
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ export const configDefaultValues = {
|
||||||
'application.httpPort': 9000,
|
'application.httpPort': 9000,
|
||||||
'application.logoURL': '/images/sunrise-cms.svg',
|
'application.logoURL': '/images/sunrise-cms.svg',
|
||||||
'application.maximumProcesses': 4,
|
'application.maximumProcesses': 4,
|
||||||
|
'application.ntfyStartup': undefined,
|
||||||
'application.userDomain': '',
|
'application.userDomain': '',
|
||||||
'application.useTestDatabases': false,
|
'application.useTestDatabases': false,
|
||||||
'application.ntfyStartup': undefined,
|
|
||||||
'reverseProxy.disableCompression': false,
|
'reverseProxy.disableCompression': false,
|
||||||
'reverseProxy.disableEtag': false,
|
'reverseProxy.disableEtag': false,
|
||||||
'reverseProxy.urlPrefix': '',
|
'reverseProxy.urlPrefix': '',
|
||||||
|
|
@ -30,15 +30,15 @@ export const configDefaultValues = {
|
||||||
'settings.longitudeMax': 180,
|
'settings.longitudeMax': 180,
|
||||||
'settings.longitudeMin': -180,
|
'settings.longitudeMin': -180,
|
||||||
'settings.burialSites.burialSiteNameSegments': {
|
'settings.burialSites.burialSiteNameSegments': {
|
||||||
separator: '-',
|
|
||||||
includeCemeteryKey: false,
|
includeCemeteryKey: false,
|
||||||
|
separator: '-',
|
||||||
segments: {
|
segments: {
|
||||||
1: {
|
1: {
|
||||||
isRequired: true,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: true,
|
||||||
label: 'Plot Number',
|
label: 'Plot Number',
|
||||||
minLength: 1,
|
maxLength: 20,
|
||||||
maxLength: 20
|
minLength: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,10 @@ export const configDefaultValues = {
|
||||||
'application.httpPort': 9000,
|
'application.httpPort': 9000,
|
||||||
'application.logoURL': '/images/sunrise-cms.svg',
|
'application.logoURL': '/images/sunrise-cms.svg',
|
||||||
'application.maximumProcesses': 4,
|
'application.maximumProcesses': 4,
|
||||||
|
'application.ntfyStartup': undefined as ConfigNtfyStartup | undefined,
|
||||||
'application.userDomain': '',
|
'application.userDomain': '',
|
||||||
'application.useTestDatabases': false,
|
'application.useTestDatabases': false,
|
||||||
|
|
||||||
'application.ntfyStartup': undefined as ConfigNtfyStartup | undefined,
|
|
||||||
|
|
||||||
'reverseProxy.disableCompression': false,
|
'reverseProxy.disableCompression': false,
|
||||||
'reverseProxy.disableEtag': false,
|
'reverseProxy.disableEtag': false,
|
||||||
'reverseProxy.urlPrefix': '',
|
'reverseProxy.urlPrefix': '',
|
||||||
|
|
@ -49,15 +48,16 @@ export const configDefaultValues = {
|
||||||
'settings.longitudeMin': -180,
|
'settings.longitudeMin': -180,
|
||||||
|
|
||||||
'settings.burialSites.burialSiteNameSegments': {
|
'settings.burialSites.burialSiteNameSegments': {
|
||||||
separator: '-',
|
|
||||||
includeCemeteryKey: false,
|
includeCemeteryKey: false,
|
||||||
|
separator: '-',
|
||||||
|
|
||||||
segments: {
|
segments: {
|
||||||
1: {
|
1: {
|
||||||
isRequired: true,
|
|
||||||
isAvailable: true,
|
isAvailable: true,
|
||||||
|
isRequired: true,
|
||||||
label: 'Plot Number',
|
label: 'Plot Number',
|
||||||
minLength: 1,
|
maxLength: 20,
|
||||||
maxLength: 20
|
minLength: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as unknown as ConfigBurialSiteNameSegments,
|
} as unknown as ConfigBurialSiteNameSegments,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
import type { PoolConnection } from 'better-sqlite-pool'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
dateIntegerToString,
|
dateIntegerToString,
|
||||||
timeIntegerToPeriodString,
|
timeIntegerToPeriodString,
|
||||||
timeIntegerToString
|
timeIntegerToString
|
||||||
} from '@cityssm/utils-datetime'
|
} from '@cityssm/utils-datetime'
|
||||||
import type { PoolConnection } from 'better-sqlite-pool'
|
|
||||||
|
|
||||||
import type { BurialSiteComment } from '../types/recordTypes.js'
|
import type { BurialSiteComment } from '../types/recordTypes.js'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { BurialSiteType } from '../types/recordTypes.js';
|
import type { BurialSiteType } from '../types/recordTypes.js';
|
||||||
interface GetFilters {
|
|
||||||
cemeteryId?: number | string;
|
|
||||||
}
|
|
||||||
interface BurialSiteTypeSummary extends BurialSiteType {
|
interface BurialSiteTypeSummary extends BurialSiteType {
|
||||||
lotCount: number;
|
lotCount: number;
|
||||||
}
|
}
|
||||||
|
interface GetFilters {
|
||||||
|
cemeteryId?: number | string;
|
||||||
|
}
|
||||||
export default function getBurialSiteTypeSummary(filters: GetFilters): Promise<BurialSiteTypeSummary[]>;
|
export default function getBurialSiteTypeSummary(filters: GetFilters): Promise<BurialSiteTypeSummary[]>;
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ import type { BurialSiteType } from '../types/recordTypes.js'
|
||||||
|
|
||||||
import { acquireConnection } from './pool.js'
|
import { acquireConnection } from './pool.js'
|
||||||
|
|
||||||
interface GetFilters {
|
|
||||||
cemeteryId?: number | string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BurialSiteTypeSummary extends BurialSiteType {
|
interface BurialSiteTypeSummary extends BurialSiteType {
|
||||||
lotCount: number
|
lotCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetFilters {
|
||||||
|
cemeteryId?: number | string
|
||||||
|
}
|
||||||
|
|
||||||
export default async function getBurialSiteTypeSummary(
|
export default async function getBurialSiteTypeSummary(
|
||||||
filters: GetFilters
|
filters: GetFilters
|
||||||
): Promise<BurialSiteTypeSummary[]> {
|
): Promise<BurialSiteTypeSummary[]> {
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,14 @@ export default async function getCemeteries() {
|
||||||
m.cemeteryLatitude, m.cemeteryLongitude, m.cemeterySvg,
|
m.cemeteryLatitude, m.cemeteryLongitude, m.cemeterySvg,
|
||||||
m.cemeteryAddress1, m.cemeteryAddress2, m.cemeteryCity, m.cemeteryProvince, m.cemeteryPostalCode,
|
m.cemeteryAddress1, m.cemeteryAddress2, m.cemeteryCity, m.cemeteryProvince, m.cemeteryPostalCode,
|
||||||
m.cemeteryPhoneNumber,
|
m.cemeteryPhoneNumber,
|
||||||
ifnull(l.burialSiteCount, 0) as burialSiteCount
|
count(b.burialSiteId) as burialSiteCount
|
||||||
from Cemeteries m
|
from Cemeteries m
|
||||||
left join (
|
left join BurialSites b on m.cemeteryId = b.cemeteryId and b.recordDelete_timeMillis is null
|
||||||
select cemeteryId, count(burialSiteId) as burialSiteCount
|
|
||||||
from BurialSites
|
|
||||||
where recordDelete_timeMillis is null
|
|
||||||
group by cemeteryId
|
|
||||||
) l on m.cemeteryId = l.cemeteryId
|
|
||||||
where m.recordDelete_timeMillis is null
|
where m.recordDelete_timeMillis is null
|
||||||
|
group by m.cemeteryId, m.cemeteryName, m.cemeteryDescription,
|
||||||
|
m.cemeteryLatitude, m.cemeteryLongitude, m.cemeterySvg,
|
||||||
|
m.cemeteryAddress1, m.cemeteryAddress2, m.cemeteryCity, m.cemeteryProvince, m.cemeteryPostalCode,
|
||||||
|
m.cemeteryPhoneNumber
|
||||||
order by m.cemeteryName, m.cemeteryId`)
|
order by m.cemeteryName, m.cemeteryId`)
|
||||||
.all();
|
.all();
|
||||||
database.release();
|
database.release();
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,14 @@ export default async function getCemeteries(): Promise<Cemetery[]> {
|
||||||
m.cemeteryLatitude, m.cemeteryLongitude, m.cemeterySvg,
|
m.cemeteryLatitude, m.cemeteryLongitude, m.cemeterySvg,
|
||||||
m.cemeteryAddress1, m.cemeteryAddress2, m.cemeteryCity, m.cemeteryProvince, m.cemeteryPostalCode,
|
m.cemeteryAddress1, m.cemeteryAddress2, m.cemeteryCity, m.cemeteryProvince, m.cemeteryPostalCode,
|
||||||
m.cemeteryPhoneNumber,
|
m.cemeteryPhoneNumber,
|
||||||
ifnull(l.burialSiteCount, 0) as burialSiteCount
|
count(b.burialSiteId) as burialSiteCount
|
||||||
from Cemeteries m
|
from Cemeteries m
|
||||||
left join (
|
left join BurialSites b on m.cemeteryId = b.cemeteryId and b.recordDelete_timeMillis is null
|
||||||
select cemeteryId, count(burialSiteId) as burialSiteCount
|
|
||||||
from BurialSites
|
|
||||||
where recordDelete_timeMillis is null
|
|
||||||
group by cemeteryId
|
|
||||||
) l on m.cemeteryId = l.cemeteryId
|
|
||||||
where m.recordDelete_timeMillis is null
|
where m.recordDelete_timeMillis is null
|
||||||
|
group by m.cemeteryId, m.cemeteryName, m.cemeteryDescription,
|
||||||
|
m.cemeteryLatitude, m.cemeteryLongitude, m.cemeterySvg,
|
||||||
|
m.cemeteryAddress1, m.cemeteryAddress2, m.cemeteryCity, m.cemeteryProvince, m.cemeteryPostalCode,
|
||||||
|
m.cemeteryPhoneNumber
|
||||||
order by m.cemeteryName, m.cemeteryId`
|
order by m.cemeteryName, m.cemeteryId`
|
||||||
)
|
)
|
||||||
.all() as Cemetery[]
|
.all() as Cemetery[]
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ async function postHandler(request, response) {
|
||||||
else if (userName !== '' && passwordPlain !== '') {
|
else if (userName !== '' && passwordPlain !== '') {
|
||||||
isAuthenticated = await authenticate(userName, passwordPlain);
|
isAuthenticated = await authenticate(userName, passwordPlain);
|
||||||
}
|
}
|
||||||
let userObject = undefined;
|
let userObject;
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
const userNameLowerCase = userName.toLowerCase();
|
const userNameLowerCase = userName.toLowerCase();
|
||||||
const canLogin = getConfigProperty('users.canLogin').some((currentUserName) => userNameLowerCase === currentUserName.toLowerCase());
|
const canLogin = getConfigProperty('users.canLogin').some((currentUserName) => userNameLowerCase === currentUserName.toLowerCase());
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ async function postHandler(
|
||||||
isAuthenticated = await authenticate(userName, passwordPlain)
|
isAuthenticated = await authenticate(userName, passwordPlain)
|
||||||
}
|
}
|
||||||
|
|
||||||
let userObject: User | undefined = undefined
|
let userObject: User | undefined
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
const userNameLowerCase = userName.toLowerCase()
|
const userNameLowerCase = userName.toLowerCase()
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,8 @@
|
||||||
name="burialSiteNameSegment<%= segmentIndexString %>"
|
name="burialSiteNameSegment<%= segmentIndexString %>"
|
||||||
value="<%= burialSite[`burialSiteNameSegment${segmentIndex}`] %>"
|
value="<%= burialSite[`burialSiteNameSegment${segmentIndex}`] %>"
|
||||||
type="text"
|
type="text"
|
||||||
minlength="<%= Math.min(segment.minLength ?? 1, 20) %>"
|
minlength="<%= Math.max(Math.min(segment.minLength ?? 1, 20), 1) %>"
|
||||||
maxlength="<%= Math.min(segment.maxLength ?? 20, 20) %>"
|
maxlength="<%= Math.max(Math.min(segment.maxLength ?? 20, 20), 1) %>"
|
||||||
placeholder="<%= segment.label ?? '' %>"
|
placeholder="<%= segment.label ?? '' %>"
|
||||||
aria-label="<%= segment.label ?? '' %>"
|
aria-label="<%= segment.label ?? '' %>"
|
||||||
<%= (segment.isRequired ?? false) ? ' required' : '' %>
|
<%= (segment.isRequired ?? false) ? ' required' : '' %>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue