diff --git a/server/AyaNova/Controllers/TagGroupController.cs b/server/AyaNova/Controllers/TagGroupController.cs
new file mode 100644
index 00000000..4b3ea5ed
--- /dev/null
+++ b/server/AyaNova/Controllers/TagGroupController.cs
@@ -0,0 +1,381 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.JsonPatch;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using AyaNova.Models;
+using AyaNova.Api.ControllerHelpers;
+using AyaNova.Biz;
+
+
+namespace AyaNova.Api.Controllers
+{
+
+ ///
+ /// TagGroup controller
+ ///
+ [ApiVersion("8.0")]
+ [Route("api/v{version:apiVersion}/[controller]")]
+ [Produces("application/json")]
+ [Authorize]
+ public class TagGroupController : Controller
+ {
+ private readonly AyContext ct;
+ private readonly ILogger log;
+ private readonly ApiServerState serverState;
+
+
+ ///
+ /// ctor
+ ///
+ ///
+ ///
+ ///
+ public TagGroupController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState)
+ {
+ ct = dbcontext;
+ log = logger;
+ serverState = apiServerState;
+ }
+
+
+ ///
+ /// Get TagGroup
+ ///
+ /// Required roles:
+ /// AnyOne
+ ///
+ ///
+ /// A TagGroup
+ [HttpGet("{id}")]
+ public async Task GetTagGroup([FromRoute] long id)
+ {
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.TagGroup))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ //Instantiate the business object handler
+ TagGroupBiz biz = new TagGroupBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ var o = await biz.GetAsync(id);
+
+ if (o == null)
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+
+ return Ok(new ApiOkResponse(o));
+ }
+
+
+ ///
+ /// Get TagGroup pick list
+ ///
+ /// Required roles: AnyRole
+ ///
+ /// This endpoint queries the Name property of TagGroups for
+ /// items that **START WITH** the characters submitted in the
+ /// "q" parameter
+ ///
+ /// Unlike most other picklists, wildcard characters if found in the query will be escaped and be considered part of the search string
+ /// Query is case insensitive as all TagGroups are lowercase
+ ///
+ /// Empty queries will return all TagGroups
+ ///
+ ///
+ /// Paged id/name collection of TagGroups with paging data
+ [HttpGet("PickList", Name = nameof(PickList))]
+ public async Task PickList([FromQuery] string q, [FromQuery] PagingOptions pagingOptions)
+ {
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.TagGroup))//Note: anyone can read a TagGroup, but that might change in future so keeping this code in
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ //Instantiate the business object handler
+ TagGroupBiz biz = new TagGroupBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ ApiPagedResponse pr = await biz.GetPickListAsync(Url, nameof(PickList), pagingOptions, q);
+ return Ok(new ApiOkWithPagingResponse(pr));
+ }
+
+
+
+ ///
+ /// Post TagGroup
+ ///
+ /// Required roles:
+ /// BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
+ ///
+ /// String name of TagGroup
+ /// object
+ [HttpPost]
+ public async Task PostTagGroup([FromBody] NameItem inObj)
+ {
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ //If a user has change roles, or editOwnRoles then they can create, true is passed for isOwner since they are creating so by definition the owner
+ if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.TagGroup))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ //Instantiate the business object handler
+ TagGroupBiz biz = new TagGroupBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ //Create and validate
+ TagGroup o = await biz.CreateAsync(inObj.Name);
+
+ if (o == null)
+ {
+ //error return
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+ else
+ {
+ //save and get ID for log then success return
+ await ct.SaveChangesAsync();
+
+ //Log
+ EventLogProcessor.AddEntry(new Event(biz.userId, o.Id, AyaType.TagGroup, AyaEvent.Created), ct);
+ await ct.SaveChangesAsync();
+
+ return CreatedAtAction("GetTagGroup", new { id = o.Id }, new ApiCreatedResponse(o));
+ }
+ }
+
+
+
+ ///
+ /// Put (update) TagGroup
+ ///
+ /// Required roles:
+ /// BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPut("{id}")]
+ public async Task PutTagGroup([FromRoute] long id, [FromBody] TagGroup oIn)
+ {
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ var oFromDb = await ct.TagGroup.SingleOrDefaultAsync(m => m.Id == id);
+
+ if (oFromDb == null)
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+
+ if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.TagGroup, oFromDb.OwnerId))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ //Instantiate the business object handler
+ TagGroupBiz biz = new TagGroupBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ if (!biz.Put(oFromDb, oIn))
+ {
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+
+ //Log
+ EventLogProcessor.AddEntry(new Event(biz.userId, oFromDb.Id, AyaType.TagGroup, AyaEvent.Modified), ct);
+
+ try
+ {
+ await ct.SaveChangesAsync();
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ if (!TagGroupExists(id))
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+ else
+ {
+ //exists but was changed by another user
+ //I considered returning new and old record, but where would it end?
+ //Better to let the client decide what to do than to send extra data that is not required
+ return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
+ }
+ }
+
+
+ return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
+ }
+
+
+ ///
+ /// Patch (update) TagGroup
+ /// Required roles: BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpPatch("{id}/{concurrencyToken}")]
+ public async Task PatchTagGroup([FromRoute] long id, [FromRoute] uint concurrencyToken, [FromBody]JsonPatchDocument objectPatch)
+ {
+ //https://dotnetcoretutorials.com/2017/11/29/json-patch-asp-net-core/
+
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ //Instantiate the business object handler
+ TagGroupBiz biz = new TagGroupBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+
+ var oFromDb = await ct.TagGroup.SingleOrDefaultAsync(m => m.Id == id);
+
+ if (oFromDb == null)
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+
+ if (!Authorized.IsAuthorizedToModify(HttpContext.Items, AyaType.TagGroup, oFromDb.OwnerId))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ //patch and validate
+ if (!biz.Patch(oFromDb, objectPatch, concurrencyToken))
+ {
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+
+ //Log
+ EventLogProcessor.AddEntry(new Event(biz.userId, oFromDb.Id, AyaType.TagGroup, AyaEvent.Modified), ct);
+
+ try
+ {
+ await ct.SaveChangesAsync();
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ if (!TagGroupExists(id))
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+ else
+ {
+ return StatusCode(409, new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT));
+ }
+ }
+
+ return Ok(new ApiOkResponse(new { ConcurrencyToken = oFromDb.ConcurrencyToken }));
+ }
+
+
+
+ ///
+ /// Delete TagGroup
+ /// Required roles: BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
+ ///
+ ///
+ /// Ok
+ [HttpDelete("{id}")]
+ public async Task DeleteTagGroup([FromRoute] long id)
+ {
+
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ var dbObj = await ct.TagGroup.SingleOrDefaultAsync(m => m.Id == id);
+ if (dbObj == null)
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+
+ if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.TagGroup, dbObj.OwnerId))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ //Instantiate the business object handler
+ TagGroupBiz biz = new TagGroupBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ if (!biz.Delete(dbObj))
+ {
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+ //Log
+ EventLogProcessor.DeleteObject(biz.userId, AyaType.TagGroup, dbObj.Id, dbObj.Name, ct);
+ await ct.SaveChangesAsync();
+
+
+
+ return NoContent();
+ }
+
+
+
+ private bool TagGroupExists(long id)
+ {
+ return ct.TagGroup.Any(e => e.Id == id);
+ }
+
+
+
+ //------------
+
+
+ }
+}
\ No newline at end of file
diff --git a/server/AyaNova/Controllers/TagGroupMapController.cs b/server/AyaNova/Controllers/TagGroupMapController.cs
new file mode 100644
index 00000000..e1b06046
--- /dev/null
+++ b/server/AyaNova/Controllers/TagGroupMapController.cs
@@ -0,0 +1,197 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using AyaNova.Models;
+using AyaNova.Api.ControllerHelpers;
+using AyaNova.Biz;
+
+
+namespace AyaNova.Api.Controllers
+{
+
+ ///
+ /// TagGroupMap controller
+ ///
+ [ApiVersion("8.0")]
+ [Route("api/v{version:apiVersion}/[controller]")]
+ [Produces("application/json")]
+ [Authorize]
+ public class TagGroupMapController : Controller
+ {
+ private readonly AyContext ct;
+ private readonly ILogger log;
+ private readonly ApiServerState serverState;
+
+
+ ///
+ /// ctor
+ ///
+ ///
+ ///
+ ///
+ public TagGroupMapController(AyContext dbcontext, ILogger logger, ApiServerState apiServerState)
+ {
+ ct = dbcontext;
+ log = logger;
+ serverState = apiServerState;
+ }
+
+
+ ///
+ /// Get TagGroupMap object
+ /// Required roles: BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
+ ///
+ ///
+ /// A TagGroupMap
+ [HttpGet("{id}")]
+ public async Task GetTagGroupMap([FromRoute] long id)
+ {
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!Authorized.IsAuthorizedToReadFullRecord(HttpContext.Items, AyaType.TagGroupMap))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ //Instantiate the business object handler
+ TagGroupMapBiz biz = new TagGroupMapBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ var o = await biz.GetAsync(id);
+
+ if (o == null)
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+
+
+ return Ok(new ApiOkResponse(o));
+ }
+
+
+ ///
+ /// Post TagGroupMap - Map a tag to a group
+ /// Required roles: BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
+ ///
+ /// TagGroupMapInfo
+ /// object
+ [HttpPost]
+ public async Task PostTagGroupMap([FromBody] TagGroupMapInfo inObj)
+ {
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!Authorized.IsAuthorizedToCreate(HttpContext.Items, AyaType.TagGroupMap))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ //Instantiate the business object handler
+ TagGroupMapBiz biz = new TagGroupMapBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ //Create and validate
+ TagGroupMap o = await biz.CreateAsync(inObj);
+
+ if (o == null)
+ {
+ //error return
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+ else
+ {
+ //save and success return
+ await ct.SaveChangesAsync();
+
+ //BIZLOG: Not going to log this for now, it's too common an operation and would require bringing in more info. If decide to implement should log the parent object with text of tag instead
+ //and don't forget about import from v7 as well
+
+ return CreatedAtAction("GetTagGroupMap", new { id = o.Id }, new ApiCreatedResponse(o));
+ }
+ }
+
+
+ ///
+ /// Delete TagGroupMap
+ /// Required roles: BizAdminFull, DispatchFull, InventoryFull, TechFull, AccountingFull
+ ///
+ ///
+ /// Ok
+ [HttpDelete("{id}")]
+ public async Task DeleteTagGroupMap([FromRoute] long id)
+ {
+
+ if (!serverState.IsOpen)
+ {
+ return StatusCode(503, new ApiErrorResponse(ApiErrorCode.API_CLOSED, null, serverState.Reason));
+ }
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest(new ApiErrorResponse(ModelState));
+ }
+
+ var dbObj = await ct.TagGroupMap.SingleOrDefaultAsync(m => m.Id == id);
+ if (dbObj == null)
+ {
+ return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
+ }
+
+ if (!Authorized.IsAuthorizedToDelete(HttpContext.Items, AyaType.TagGroupMap, dbObj.OwnerId))
+ {
+ return StatusCode(401, new ApiNotAuthorizedResponse());
+ }
+
+ //Instantiate the business object handler
+ TagGroupMapBiz biz = new TagGroupMapBiz(ct, UserIdFromContext.Id(HttpContext.Items), UserRolesFromContext.Roles(HttpContext.Items));
+
+ if (!biz.Delete(dbObj))
+ {
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+
+ await ct.SaveChangesAsync();
+
+ //BIZLOG: Not going to log this for now, it's too common an operation and would require bringing in more info. If decide to implement should log the parent object with text of tag instead
+
+ return NoContent();
+ }
+
+
+
+
+
+ private bool TagGroupMapExists(long id)
+ {
+ return ct.TagGroupMap.Any(e => e.Id == id);
+ }
+
+
+
+ //------------
+
+
+
+
+
+ }//eoc
+}
\ No newline at end of file
diff --git a/server/AyaNova/biz/BizRoles.cs b/server/AyaNova/biz/BizRoles.cs
index 5b6beb94..d723450b 100644
--- a/server/AyaNova/biz/BizRoles.cs
+++ b/server/AyaNova/biz/BizRoles.cs
@@ -124,10 +124,10 @@ namespace AyaNova.Biz
////////////////////////////////////////////////////////////
//TAGGROUPMAP - MIRROR TAGMAP
- //Any roles can tag objects and remove tags as per their rights to the taggable object type in question
+ //Full roles can make new taggroupmaps and can edit or delete existing taggroupmaps
roles.Add(AyaType.TagGroupMap, new BizRoleSet()
{
- Change = AuthorizationRoles.AnyRole,
+ Change = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryFull | AuthorizationRoles.TechFull | AuthorizationRoles.AccountingFull,
EditOwn = AuthorizationRoles.NoRole,
ReadFullRecord = AuthorizationRoles.AnyRole
});