diff --git a/src/config/chis-ke/config.json b/src/config/chis-ke/config.json index e409dd3..8d91f3b 100644 --- a/src/config/chis-ke/config.json +++ b/src/config/chis-ke/config.json @@ -215,6 +215,7 @@ "user_role": ["community_health_assistant"], "username_from_place": false, "deactivate_users_on_replace": false, + "can_assign_multiple": true, "hierarchy": [ { "friendly_name": "Sub County", diff --git a/src/config/index.ts b/src/config/index.ts index 5b2bd4d..2ba940d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -26,6 +26,7 @@ export type ContactType = { place_properties: ContactProperty[]; contact_properties: ContactProperty[]; deactivate_users_on_replace: boolean; + can_assign_multiple?: boolean; hint?: string; }; diff --git a/src/liquid/new/create_form.liquid b/src/liquid/new/create_form.liquid index eaed2dd..dcdce11 100644 --- a/src/liquid/new/create_form.liquid +++ b/src/liquid/new/create_form.liquid @@ -1,15 +1,14 @@
-
-

New {{ contactType.contact_friendly }}

-
+
+

New {{ contactType.contact_friendly }}

+

Contact details

@@ -43,10 +42,13 @@ {% render 'new/new_place_btn.liquid' with contactType.name as place_type %}
-
+
+
Cancel - +
@@ -55,7 +57,4 @@ {% render 'new/place_list.liquid', places: entry, contactType: contactType %} {% endfor %} {% endif %} -
- view history -
diff --git a/src/liquid/new/create_place.liquid b/src/liquid/new/create_place.liquid index 73d87e0..eec0ff4 100644 --- a/src/liquid/new/create_place.liquid +++ b/src/liquid/new/create_place.liquid @@ -4,13 +4,19 @@ New {{ contactType.contact_friendly }} - - + + - +
{% render 'new/create_form.liquid', contactType: contactType, hierarchy: hierarchy, data: data, places: places %}
diff --git a/src/routes/add-place.ts b/src/routes/add-place.ts index 6f228ab..2f25ef3 100644 --- a/src/routes/add-place.ts +++ b/src/routes/add-place.ts @@ -8,6 +8,7 @@ import RemotePlaceResolver from '../lib/remote-place-resolver'; import { UploadManager } from '../services/upload-manager'; import RemotePlaceCache from '../lib/remote-place-cache'; import WarningSystem from '../warnings'; +import semver from 'semver'; export default async function addPlace(fastify: FastifyInstance) { fastify.get('/add-place', async (req, resp) => { @@ -17,6 +18,12 @@ export default async function addPlace(fastify: FastifyInstance) { const contactType = queryParams.type ? Config.getContactType(queryParams.type) : contactTypes[contactTypes.length - 1]; + + if (semver.gte(req.chtSession.chtCoreVersion, '4.9.0') && contactType.can_assign_multiple) { + resp.redirect(`/new?place_type=${queryParams.type}`); + return; + } + const op = queryParams.op || 'new'; const tmplData = { view: 'add', diff --git a/src/routes/app.ts b/src/routes/app.ts index 96411dd..2a59dcd 100644 --- a/src/routes/app.ts +++ b/src/routes/app.ts @@ -95,7 +95,7 @@ export default async function sessionCache(fastify: FastifyInstance) { const directiveModel = new DirectiveModel(sessionCache, req.cookies.filter); const chtApi = new ChtApi(req.chtSession); - uploadManager.doUpload(sessionCache.getPlaces(), chtApi, ignoreWarnings === 'true'); + uploadManager.doUpload(sessionCache.getPlaces(), chtApi, true); return resp.view('src/liquid/place/directive.html', { directiveModel diff --git a/src/routes/new.ts b/src/routes/new.ts index 1587b56..c5ba7a2 100644 --- a/src/routes/new.ts +++ b/src/routes/new.ts @@ -4,8 +4,8 @@ import { ChtApi } from '../lib/cht-api'; import SessionCache from '../services/session-cache'; import _ from 'lodash'; import PlaceFactory from '../services/place-factory'; -import { UploadManager } from '../services/upload-manager'; import Validation from '../validation'; +import { PlaceUploadState } from '../services/place'; export default async function newHandler(fastify: FastifyInstance) { @@ -15,37 +15,36 @@ export default async function newHandler(fastify: FastifyInstance) { const sessionCache: SessionCache = req.sessionCache; const grouped = Object.values( - _.groupBy(sessionCache.getPlaces({ type: contactType.name }), (p) => p.contact.id) - ).filter(p => p.length > 0).reverse(); + _.groupBy(sessionCache.getPlaces({ type: contactType.name }).filter(p => p.state === PlaceUploadState.STAGED), (p) => p.contact.id) + ) + .filter(p => p.length > 0) + .reverse(); + const data = { hierarchy: Config.getHierarchyWithReplacement(contactType, 'desc'), contactType, - places: grouped + places: grouped, + logo: Config.getLogoBase64() }; return resp.view('src/liquid/new/create_place.liquid', data); }); fastify.post('/new', async (req, resp) => { - const { place_type } = req.query as any; + const { place_type, cont } = req.query as any; + const contactType = Config.getContactType(place_type); const chtApi = new ChtApi(req.chtSession); const sessionCache: SessionCache = req.sessionCache; - const uploadManager: UploadManager = fastify.uploadManager; - const places = await PlaceFactory.createManyWithSingleUser(req.body as {[key:string]:string}, contactType, sessionCache, chtApi); - uploadManager.uploadWithSingleUser(places, chtApi); - - const grouped = Object.values( - _.groupBy(sessionCache.getPlaces({ type: contactType.name }), (p) => p.contact.id) - ).filter(p => p.length > 0).reverse(); - const data = { - hierarchy: Config.getHierarchyWithReplacement(contactType, 'desc'), - contactType, - places: grouped - }; - - return resp.view('src/liquid/new/create_form.liquid', data); + await PlaceFactory.createManyWithSingleUser(req.body as {[key:string]:string}, contactType, sessionCache, chtApi); + + if (cont) { + resp.header('HX-Redirect', `/new?place_type=${place_type}`); + } else { + resp.header('HX-Redirect', `/`); + } + return; }); fastify.get('/place_form', async (req, resp) => { diff --git a/src/services/upload-manager.ts b/src/services/upload-manager.ts index 4b3d260..aebecc0 100644 --- a/src/services/upload-manager.ts +++ b/src/services/upload-manager.ts @@ -3,7 +3,7 @@ import EventEmitter from 'events'; import * as RetryLogic from '../lib/retry-logic'; import { ChtApi, CreatedPlaceResult, PlacePayload } from '../lib/cht-api'; import { Config } from '../config'; -import Place, { PlaceUploadState } from './place'; +import Place, { PlaceUploadState, UserCreationDetails } from './place'; import RemotePlaceCache from '../lib/remote-place-cache'; import { UploadNewPlace } from './upload.new'; import { UploadReplacementWithDeletion } from './upload.replacement'; @@ -21,20 +21,25 @@ export interface Uploader { export class UploadManager extends EventEmitter { doUpload = async (places: Place[], chtApi: ChtApi, ignoreWarnings: boolean = false) => { const placesNeedingUpload = places.filter(p => { - return !p.isCreated && !p.hasValidationErrors && !p.hasSharedUser && (ignoreWarnings || !p.warnings.length); + return !p.isCreated && !p.hasValidationErrors && (ignoreWarnings || !p.warnings.length); }); this.eventedPlaceStateChange(placesNeedingUpload, PlaceUploadState.SCHEDULED); - const independants = placesNeedingUpload.filter(p => !p.isDependant); - const dependants = placesNeedingUpload.filter(p => p.isDependant); + const independants = placesNeedingUpload.filter(p => !p.isDependant && !p.hasSharedUser); + const dependants = placesNeedingUpload.filter(p => p.isDependant && !p.hasSharedUser); + await this.uploadPlacesInBatches(independants, chtApi); await this.uploadPlacesInBatches(dependants, chtApi); + await this.uploadGrouped(placesNeedingUpload, chtApi); }; - uploadWithSingleUser = async (places: Place[], api: ChtApi) => { + uploadGrouped = async (places: Place[], api: ChtApi) => { const grouped = _.groupBy(places, place => place.contact.id); Object.keys(grouped).forEach(async k => { - await this.uploadGroup(grouped[k], api); + const places = grouped[k]; + if (places.length > 1) { + await this.uploadGroup(places[0].creationDetails, places.slice(1), api); + } }); }; @@ -89,34 +94,33 @@ export class UploadManager extends EventEmitter { } } - private async uploadGroup(places: Place[], api: ChtApi) { - let contactId; - const placeIds: string[] = []; - for (let i = 0; i < places.length; i++) { - const place = places[i]; + private async uploadGroup(creationDetails: UserCreationDetails, places: Place[], api: ChtApi) { + if (!creationDetails.username || !creationDetails.placeId) { + throw new Error('creationDetails must not be empty'); + } + const placeIds: {[key:string]: string} = { [creationDetails.placeId]: '' }; + for (const place of places) { this.eventedPlaceStateChange(place, PlaceUploadState.IN_PROGRESS); - const payload = place.asChtPayload(api.chtSession.username, contactId); + const payload = place.asChtPayload(api.chtSession.username, creationDetails.contactId); await Config.mutate(payload, api, place.isReplacement); const result = await api.createPlace(payload); - if (!contactId) { - if (!result.contactId) { - throw new Error('expected contact id'); - } - contactId = result.contactId; - } + place.creationDetails.contactId = result.contactId; - placeIds.push(result.placeId); + place.creationDetails.placeId = result.placeId; + placeIds[result.placeId] = result.placeId; } - const userPayload = new UserPayload(places[0], placeIds, contactId!); - const { username, password } = await RetryLogic.createUserWithRetries(userPayload, api); + + await api.updateUser({ username: creationDetails.username!, place: Object.keys(placeIds)}); const created_at = new Date().getTime(); places.forEach(place => { - place.creationDetails.username = username; - place.creationDetails.password = password; + place.creationDetails.username = creationDetails.username; + place.creationDetails.password = creationDetails.password; place.creationDetails.created_at = created_at; + this.eventedPlaceStateChange(place, PlaceUploadState.SUCCESS); }); - this.emit('refresh_grouped', contactId); + + this.emit('refresh_grouped', creationDetails.contactId); } public triggerRefresh(place_id: string) {