diff --git a/epictrack-api/src/api/models/special_field.py b/epictrack-api/src/api/models/special_field.py
index 71a7975c9..bb7460093 100644
--- a/epictrack-api/src/api/models/special_field.py
+++ b/epictrack-api/src/api/models/special_field.py
@@ -41,3 +41,16 @@ class SpecialField(BaseModelVersioned):
time_range = Column(TSTZRANGE, nullable=False)
__table_args__ = (Index('entity_field_index', "entity", "entity_id", "field_name", "time_range"), )
+
+ @classmethod
+ def find_by_params(cls, params: dict, default_filters=True):
+ """Returns based on the params"""
+ query = {}
+ for key, value in params.items():
+ query[key] = value
+ if default_filters and hasattr(cls, 'is_active'):
+ query['is_active'] = True
+ if hasattr(cls, 'is_deleted'):
+ query['is_deleted'] = False
+ rows = cls.query.filter_by(**query).order_by(SpecialField.time_range.desc()).all()
+ return rows
diff --git a/epictrack-api/src/api/resources/special_field.py b/epictrack-api/src/api/resources/special_field.py
index f92af51a9..25861f924 100644
--- a/epictrack-api/src/api/resources/special_field.py
+++ b/epictrack-api/src/api/resources/special_field.py
@@ -22,8 +22,7 @@
from api.schemas import request as req
from api.schemas import response as res
from api.services.special_field import SpecialFieldService
-from api.utils import auth, constants
-from api.utils.caching import AppCache
+from api.utils import auth
from api.utils.profiler import profiletime
from api.utils.util import cors_preflight
@@ -40,7 +39,6 @@ class SpecialFields(Resource):
@cors.crossdomain(origin="*")
@auth.require
@profiletime
- @AppCache.cache.cached(timeout=constants.CACHE_DAY_TIMEOUT, query_string=True)
def get():
"""Return all special field values based on params."""
params = req.SpecialFieldQueryParamSchema().load(request.args)
@@ -85,23 +83,3 @@ def put(special_field_id):
request_json = req.SpecialFieldBodyParameterSchema().load(API.payload)
special_field_entry = SpecialFieldService.update_special_field_entry(special_field_id, request_json)
return res.SpecialFieldResponseSchema().dump(special_field_entry), HTTPStatus.OK
-
-
-@cors_preflight('GET')
-@API.route('/exists', methods=['GET', 'OPTIONS'])
-class ValidateSpecialFieldEntry(Resource):
- """Endpoint resource to check for existing special field entry."""
-
- @staticmethod
- @cors.crossdomain(origin='*')
- @auth.require
- @profiletime
- def get():
- """Checks for existing special field entries."""
- args = req.SpecialFieldExistanceQueryParamSchema().load(request.args)
- special_field_entry_id = args.pop('spcial_field_id')
- exists = SpecialFieldService.check_existence(args, special_field_id=special_field_entry_id)
- return (
- {'exists': exists},
- HTTPStatus.OK,
- )
diff --git a/epictrack-api/src/api/schemas/request/__init__.py b/epictrack-api/src/api/schemas/request/__init__.py
index 5c391fed5..99678bc2c 100644
--- a/epictrack-api/src/api/schemas/request/__init__.py
+++ b/epictrack-api/src/api/schemas/request/__init__.py
@@ -38,8 +38,7 @@
ProponentBodyParameterSchema, ProponentExistenceQueryParamSchema, ProponentIdPathParameterSchema)
from .reminder_configuration_request import ReminderConfigurationExistenceQueryParamSchema
from .special_field_request import (
- SpecialFieldBodyParameterSchema, SpecialFieldExistanceQueryParamSchema, SpecialFieldIdPathParameterSchema,
- SpecialFieldQueryParamSchema)
+ SpecialFieldBodyParameterSchema, SpecialFieldIdPathParameterSchema, SpecialFieldQueryParamSchema)
from .staff_request import (
StaffBodyParameterSchema, StaffByPositionsQueryParamSchema, StaffEmailPathParameterSchema,
StaffExistanceQueryParamSchema, StaffIdPathParameterSchema)
diff --git a/epictrack-api/src/api/schemas/request/special_field_request.py b/epictrack-api/src/api/schemas/request/special_field_request.py
index e4eeb3660..414839815 100644
--- a/epictrack-api/src/api/schemas/request/special_field_request.py
+++ b/epictrack-api/src/api/schemas/request/special_field_request.py
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Special field resource's input validations"""
-from marshmallow import fields, validate
+from marshmallow import EXCLUDE, fields, validate
from api.models.special_field import EntityEnum
@@ -71,11 +71,11 @@ class SpecialFieldBodyParameterSchema(RequestBodyParameterSchema):
active_from = fields.DateTime(
metadata={"description": "Lower bound for time range"}, required=True
)
- active_to = fields.DateTime(
- metadata={"description": "Upper bound for time range"},
- allow_none=True,
- missing=None,
- )
+
+ class Meta: # pylint: disable=too-few-public-methods
+ """Meta information"""
+
+ unknown = EXCLUDE
class SpecialFieldIdPathParameterSchema(RequestPathParameterSchema):
@@ -86,49 +86,3 @@ class SpecialFieldIdPathParameterSchema(RequestPathParameterSchema):
validate=validate.Range(min=1),
required=True,
)
-
-
-class SpecialFieldExistanceQueryParamSchema(RequestQueryParameterSchema):
- """Special field existance check query parameters"""
-
- entity = fields.Str(
- metadata={"description": "Entity name"},
- required=True,
- validate=validate.OneOf([x.value for x in EntityEnum]),
- )
-
- entity_id = fields.Int(
- metadata={"description": "The id of the entity"},
- validate=validate.Range(min=1),
- required=True,
- )
-
- field_name = fields.Str(
- metadata={"description": "Name of the special field"},
- validate=validate.Length(max=150),
- required=True,
- )
-
- field_value = fields.Str(
- metadata={"description": "Value of the special field"},
- validate=validate.Length(min=1),
- required=True,
- )
-
- active_from = fields.DateTime(
- metadata={"description": "Lower bound for time range"}, required=True
- )
-
- active_to = fields.DateTime(
- metadata={"description": "Upper bound for time range"},
- allow_none=True,
- missing=None,
- )
-
- spcial_field_id = fields.Int(
- metadata={"description": "The id of the special field entry"},
- validate=validate.Range(min=1),
- required=False,
- allow_none=True,
- missing=None
- )
diff --git a/epictrack-api/src/api/services/project.py b/epictrack-api/src/api/services/project.py
index d594f8fbd..deadde806 100644
--- a/epictrack-api/src/api/services/project.py
+++ b/epictrack-api/src/api/services/project.py
@@ -26,11 +26,13 @@
from api.models.project import ProjectStateEnum
from api.models.proponent import Proponent
from api.models.region import Region
+from api.models.special_field import EntityEnum
from api.models.sub_types import SubType
from api.models.types import Type
from api.models.work import Work
from api.models.work_type import WorkType
from api.schemas.types import TypeSchema
+from api.services.special_field import SpecialFieldService
from api.utils.constants import PROJECT_STATE_ENUM_MAPS
from api.utils.enums import ProjectCodeMethod
from api.utils.token_info import TokenInfo
@@ -66,6 +68,23 @@ def create_project(cls, payload: dict):
project = Project(**payload)
project.project_state = ProjectStateEnum.PRE_WORK
current_app.logger.info(f"Project obj {dir(project)}")
+ project.flush()
+ proponent_special_field_data = {
+ "entity": EntityEnum.PROJECT,
+ "entity_id": project.id,
+ "field_name": "proponent_id",
+ "field_value": project.proponent_id,
+ "active_from": project.created_at
+ }
+ SpecialFieldService.create_special_field_entry(proponent_special_field_data)
+ project_name_special_field_data = {
+ "entity": EntityEnum.PROJECT,
+ "entity_id": project.id,
+ "field_name": "name",
+ "field_value": project.name,
+ "active_from": project.created_at
+ }
+ SpecialFieldService.create_special_field_entry(project_name_special_field_data)
project.save()
return project
diff --git a/epictrack-api/src/api/services/special_field.py b/epictrack-api/src/api/services/special_field.py
index 4b46ae061..c976d8b75 100644
--- a/epictrack-api/src/api/services/special_field.py
+++ b/epictrack-api/src/api/services/special_field.py
@@ -12,11 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Service to manage Special fields."""
+from datetime import datetime, timedelta
+from typing import Union
+
from flask import current_app
from psycopg2.extras import DateTimeTZRange
+from sqlalchemy import func, or_
+from sqlalchemy.dialects.postgresql.ranges import Range
-from api.exceptions import ResourceNotFoundError, UnprocessableEntityError
+from api.exceptions import ResourceNotFoundError
from api.models import SpecialField, db
+from api.utils.constants import SPECIAL_FIELD_ENTITY_MODEL_MAPS
class SpecialFieldService: # pylint:disable=too-few-public-methods
@@ -31,48 +37,77 @@ def find_all_by_params(cls, args: dict):
@classmethod
def create_special_field_entry(cls, payload: dict):
"""Create special field entry"""
- # payload["time_range"] = DateTimeTZRange([payload["time_range"]] + ["[)"])
- existing = cls.check_existence(payload)
- if existing:
- raise UnprocessableEntityError("Value with overlapping time range exists. Please fix it before continuing.")
+ upper_limit = cls._get_upper_limit(payload)
payload["time_range"] = DateTimeTZRange(
- payload.pop("active_from"), payload.pop("active_to", None), "[)"
+ payload.pop("active_from"), upper_limit, bounds="[)"
)
- entry = SpecialField(**payload)
- entry.save()
- return entry
+ special_field = SpecialField(**payload)
+ special_field.save()
+ cls._update_original_model(special_field)
+ db.session.commit()
+ return special_field
@classmethod
def update_special_field_entry(cls, special_field_id: int, payload: dict):
"""Create special field entry"""
- exists = cls.check_existence(payload, special_field_id)
- if exists:
- raise UnprocessableEntityError("Value with overlapping time range exists. Please fix it before continuing.")
special_field = SpecialField.find_by_id(special_field_id)
+ upper_limit = cls._get_upper_limit(payload, special_field_id)
+
if not special_field:
- raise ResourceNotFoundError(f"Special field entry with id '{special_field_id}' not found")
- payload["time_range"] = DateTimeTZRange(
- payload.pop("active_from"), payload.pop("active_to", None), "[)"
+ raise ResourceNotFoundError(
+ f"Special field entry with id '{special_field_id}' not found"
+ )
+ payload["time_range"] = Range(
+ payload.pop("active_from"), upper_limit, bounds="[)"
)
special_field = special_field.update(payload)
+ cls._update_original_model(special_field)
+ db.session.commit()
+ return special_field
+
+ @classmethod
+ def find_by_id(cls, _id):
+ """Find special field entry by id."""
+ special_field = SpecialField.find_by_id(_id)
return special_field
@classmethod
- def check_existence(cls, payload: dict, special_field_id: int = None) -> bool:
- """Validate time range"""
- new_range = DateTimeTZRange(payload["active_from"], payload["active_to"], "[)")
+ def _get_upper_limit(
+ cls, payload: dict, special_field_id: int = None
+ ) -> Union[datetime, None]:
+ """Finds and returns the upper limit of time range and updates existing entries to match new time range"""
exists_query = db.session.query(SpecialField).filter(
SpecialField.entity == payload["entity"],
SpecialField.entity_id == payload["entity_id"],
SpecialField.field_name == payload["field_name"],
- SpecialField.time_range.overlaps(new_range),
+ or_(
+ SpecialField.time_range.contains(payload["active_from"]),
+ func.lower(SpecialField.time_range) > payload["active_from"],
+ ),
)
if special_field_id:
exists_query = exists_query.filter(SpecialField.id != special_field_id)
- return bool(exists_query.first())
+ existing_entry = exists_query.order_by(SpecialField.time_range.asc()).first()
+ upper_limit = None
+ if existing_entry:
+ if existing_entry.time_range.lower > payload["active_from"]:
+ upper_limit = existing_entry.time_range.lower + timedelta(days=-1)
+ else:
+ upper_limit = existing_entry.time_range.upper
+ new_range = DateTimeTZRange(
+ existing_entry.time_range.lower,
+ payload["active_from"] + timedelta(days=-1),
+ "[)",
+ )
+ existing_entry.time_range = new_range
+ db.session.add(existing_entry)
+ return upper_limit
@classmethod
- def find_by_id(cls, _id):
- """Find special field entry by id."""
- special_field = SpecialField.find_by_id(_id)
- return special_field
+ def _update_original_model(cls, special_field_entry: SpecialField) -> None:
+ """If `special_field_entry` is latest, update original table with new value"""
+ if special_field_entry.time_range.upper is None:
+ model_class = SPECIAL_FIELD_ENTITY_MODEL_MAPS[special_field_entry.entity]
+ model_class.query.filter(
+ model_class.id == special_field_entry.entity_id
+ ).update({special_field_entry.field_name: special_field_entry.field_value})
diff --git a/epictrack-api/src/api/utils/constants.py b/epictrack-api/src/api/utils/constants.py
index 4b82d16f4..75736c895 100644
--- a/epictrack-api/src/api/utils/constants.py
+++ b/epictrack-api/src/api/utils/constants.py
@@ -1,6 +1,9 @@
"""File representing constants used in the application"""
-from api.models.project import ProjectStateEnum
+from api.models.project import Project, ProjectStateEnum
+from api.models.proponent import Proponent
+from api.models.special_field import EntityEnum
+from api.models.work import Work
SCHEMA_MAPS = {
@@ -27,3 +30,9 @@
}
PIP_LINK_URL_BASE = "https://apps.nrs.gov.bc.ca/int/fnp/FirstNationDetail.xhtml?name="
+
+SPECIAL_FIELD_ENTITY_MODEL_MAPS = {
+ EntityEnum.PROJECT: Project,
+ EntityEnum.WORK: Work,
+ EntityEnum.PROPONENT: Proponent,
+}
diff --git a/epictrack-web/package-lock.json b/epictrack-web/package-lock.json
index 8c6e04bd6..824c59b84 100644
--- a/epictrack-web/package-lock.json
+++ b/epictrack-web/package-lock.json
@@ -30,7 +30,7 @@
"draft-js": "^0.11.7",
"json-2-csv": "^4.0.0",
"keycloak-js": "^21.1.1",
- "material-react-table": "^1.13.0",
+ "material-react-table": "^2.0.5",
"moment": "^2.29.4",
"moment-range": "^4.0.2",
"notistack": "^3.0.1",
@@ -39,6 +39,7 @@
"react-draft-wysiwyg": "^1.15.0",
"react-hook-form": "^7.37.0",
"react-if": "^4.1.5",
+ "react-imask": "^7.1.3",
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"react-scripts": "5.0.1",
@@ -2145,6 +2146,18 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.5.tgz",
+ "integrity": "sha512-7+ziVclejQTLYhXl+Oi1f6gTGD1XDCeLa4R472TNGQxb08zbEJ0OdNoh5Piz+57Ltmui6xR88BXR4gS3/Toslw==",
+ "dependencies": {
+ "core-js-pure": "^3.30.2",
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
@@ -3999,18 +4012,19 @@
}
},
"node_modules/@tanstack/react-virtual": {
- "version": "3.0.0-beta.65",
- "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.65.tgz",
- "integrity": "sha512-Q21cUoE0C8Oyzy3RAMV+u4BuB+RwIf2/oQRCWksmIBp1PqLEtvXhAldh7v/wUt7WKEkislKDICZAvbYYs7EAyQ==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz",
+ "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==",
"dependencies": {
- "@tanstack/virtual-core": "3.0.0-beta.65"
+ "@tanstack/virtual-core": "3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/table-core": {
@@ -4026,9 +4040,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
- "version": "3.0.0-beta.65",
- "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.65.tgz",
- "integrity": "sha512-ObP2pvXBdbivinr7BWDbGqYt4TK8wNzYsOWio+qBkDx5AJFuvqcdJxcCCYnv4dzVTe5ELA1MT4tkt8NB/tnEdA==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz",
+ "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
@@ -9470,6 +9484,17 @@
"node": ">= 4"
}
},
+ "node_modules/imask": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/imask/-/imask-7.1.3.tgz",
+ "integrity": "sha512-jZCqTI5Jgukhl2ff+znBQd8BiHOTlnFYCIgggzHYDdoJsHmSSWr1BaejcYBxsjy4ZIs8Rm0HhbOxQcobcdESRQ==",
+ "dependencies": {
+ "@babel/runtime-corejs3": "^7.22.6"
+ },
+ "engines": {
+ "npm": ">=4.0.0"
+ }
+ },
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
@@ -11436,29 +11461,30 @@
}
},
"node_modules/material-react-table": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-1.15.1.tgz",
- "integrity": "sha512-TXidRV7lGtCV5G/ON9Y38TztRcmpKFodFmyTCjvlKXCl5/9X+KY4waP8U0l16FFslg1f7HGWhfkqV5OfUfEIoA==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-2.0.5.tgz",
+ "integrity": "sha512-axRrqa/2QQ+AO3SiJbOtSyemlHX0S03X+IXW72z344d3LT+u/jsKiAmdWMLTN8ARScYMAN5NgrArujiLEmftSQ==",
"dependencies": {
"@tanstack/match-sorter-utils": "8.8.4",
"@tanstack/react-table": "8.10.7",
- "@tanstack/react-virtual": "3.0.0-beta.65",
+ "@tanstack/react-virtual": "3.0.1",
"highlight-words": "1.2.2"
},
"engines": {
- "node": ">=14"
+ "node": ">=16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kevinvandy"
},
"peerDependencies": {
- "@emotion/react": ">=11",
- "@emotion/styled": ">=11",
- "@mui/icons-material": ">=5",
- "@mui/material": ">=5",
- "react": ">=17.0",
- "react-dom": ">=17.0"
+ "@emotion/react": ">=11.11",
+ "@emotion/styled": ">=11.11",
+ "@mui/icons-material": ">=5.11",
+ "@mui/material": ">=5.13",
+ "@mui/x-date-pickers": ">=6.15.0",
+ "react": ">=18.0",
+ "react-dom": ">=18.0"
}
},
"node_modules/mdn-data": {
@@ -14032,6 +14058,21 @@
"react": "^16.x || ^17.x || ^18.x"
}
},
+ "node_modules/react-imask": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.1.3.tgz",
+ "integrity": "sha512-anCnzdkqpDzNwe7ot76kQSvmnz4Sw7AW/QFjjLh3B87HVNv9e2oHC+1m9hQKSIui2Tqm7w68ooMgDFsCQlDMyg==",
+ "dependencies": {
+ "imask": "^7.1.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "npm": ">=4.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=0.14.0"
+ }
+ },
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -18848,6 +18889,15 @@
"regenerator-runtime": "^0.14.0"
}
},
+ "@babel/runtime-corejs3": {
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.5.tgz",
+ "integrity": "sha512-7+ziVclejQTLYhXl+Oi1f6gTGD1XDCeLa4R472TNGQxb08zbEJ0OdNoh5Piz+57Ltmui6xR88BXR4gS3/Toslw==",
+ "requires": {
+ "core-js-pure": "^3.30.2",
+ "regenerator-runtime": "^0.14.0"
+ }
+ },
"@babel/template": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
@@ -19976,11 +20026,11 @@
}
},
"@tanstack/react-virtual": {
- "version": "3.0.0-beta.65",
- "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.65.tgz",
- "integrity": "sha512-Q21cUoE0C8Oyzy3RAMV+u4BuB+RwIf2/oQRCWksmIBp1PqLEtvXhAldh7v/wUt7WKEkislKDICZAvbYYs7EAyQ==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz",
+ "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==",
"requires": {
- "@tanstack/virtual-core": "3.0.0-beta.65"
+ "@tanstack/virtual-core": "3.0.0"
}
},
"@tanstack/table-core": {
@@ -19989,9 +20039,9 @@
"integrity": "sha512-KQk5OMg5OH6rmbHZxuNROvdI+hKDIUxANaHlV+dPlNN7ED3qYQ/WkpY2qlXww1SIdeMlkIhpN/2L00rof0fXFw=="
},
"@tanstack/virtual-core": {
- "version": "3.0.0-beta.65",
- "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.65.tgz",
- "integrity": "sha512-ObP2pvXBdbivinr7BWDbGqYt4TK8wNzYsOWio+qBkDx5AJFuvqcdJxcCCYnv4dzVTe5ELA1MT4tkt8NB/tnEdA=="
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz",
+ "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg=="
},
"@testing-library/dom": {
"version": "9.3.3",
@@ -24070,6 +24120,14 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ=="
},
+ "imask": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/imask/-/imask-7.1.3.tgz",
+ "integrity": "sha512-jZCqTI5Jgukhl2ff+znBQd8BiHOTlnFYCIgggzHYDdoJsHmSSWr1BaejcYBxsjy4ZIs8Rm0HhbOxQcobcdESRQ==",
+ "requires": {
+ "@babel/runtime-corejs3": "^7.22.6"
+ }
+ },
"immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
@@ -25525,13 +25583,13 @@
}
},
"material-react-table": {
- "version": "1.15.1",
- "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-1.15.1.tgz",
- "integrity": "sha512-TXidRV7lGtCV5G/ON9Y38TztRcmpKFodFmyTCjvlKXCl5/9X+KY4waP8U0l16FFslg1f7HGWhfkqV5OfUfEIoA==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-2.0.5.tgz",
+ "integrity": "sha512-axRrqa/2QQ+AO3SiJbOtSyemlHX0S03X+IXW72z344d3LT+u/jsKiAmdWMLTN8ARScYMAN5NgrArujiLEmftSQ==",
"requires": {
"@tanstack/match-sorter-utils": "8.8.4",
"@tanstack/react-table": "8.10.7",
- "@tanstack/react-virtual": "3.0.0-beta.65",
+ "@tanstack/react-virtual": "3.0.1",
"highlight-words": "1.2.2"
}
},
@@ -27193,6 +27251,15 @@
"integrity": "sha512-Uk+Ub2gC83PAakuU4+7iLdTEP4LPi2ihNEPCtz/vr8SLGbzkMApbpYbkDZ5z9zYXurd0gg+EK/bpOLFFC1r1eQ==",
"requires": {}
},
+ "react-imask": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/react-imask/-/react-imask-7.1.3.tgz",
+ "integrity": "sha512-anCnzdkqpDzNwe7ot76kQSvmnz4Sw7AW/QFjjLh3B87HVNv9e2oHC+1m9hQKSIui2Tqm7w68ooMgDFsCQlDMyg==",
+ "requires": {
+ "imask": "^7.1.3",
+ "prop-types": "^15.8.1"
+ }
+ },
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
diff --git a/epictrack-web/package.json b/epictrack-web/package.json
index ee7fbf4fe..b8c0d5d2a 100644
--- a/epictrack-web/package.json
+++ b/epictrack-web/package.json
@@ -25,7 +25,7 @@
"draft-js": "^0.11.7",
"json-2-csv": "^4.0.0",
"keycloak-js": "^21.1.1",
- "material-react-table": "^1.13.0",
+ "material-react-table": "^2.0.5",
"moment": "^2.29.4",
"moment-range": "^4.0.2",
"notistack": "^3.0.1",
diff --git a/epictrack-web/src/components/icons/index.tsx b/epictrack-web/src/components/icons/index.tsx
index 89566a9c7..16b24f861 100644
--- a/epictrack-web/src/components/icons/index.tsx
+++ b/epictrack-web/src/components/icons/index.tsx
@@ -375,6 +375,46 @@ const ClockIcon = (props: IconProps) => {
);
};
+const AddIcon = (props: IconProps) => {
+ return (
+
+ );
+};
+
+const CheckIcon = (props: IconProps) => {
+ return (
+
+ );
+};
+
+const CloseXIcon = (props: IconProps) => {
+ return (
+
+ );
+};
+
+const LockClosedIcon = (props: IconProps) => {
+ return (
+
+ );
+};
+
+const LockOpenIcon = (props: IconProps) => {
+ return (
+
+ );
+};
+
const icons: { [x: string]: React.FC } = {
AllIcon,
DashboardIcon,
@@ -413,6 +453,11 @@ const icons: { [x: string]: React.FC } = {
DotIcon,
ClockIcon,
EyeIcon,
+ AddIcon,
+ CheckIcon,
+ CloseXIcon,
+ LockClosedIcon,
+ LockOpenIcon,
};
export default icons;
diff --git a/epictrack-web/src/components/project/ProjectForm.tsx b/epictrack-web/src/components/project/ProjectForm.tsx
index f57cc881c..5829c7d24 100644
--- a/epictrack-web/src/components/project/ProjectForm.tsx
+++ b/epictrack-web/src/components/project/ProjectForm.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Grid, Box } from "@mui/material";
+import { Grid, Box, IconButton } from "@mui/material";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
@@ -14,10 +14,20 @@ import subTypeService from "../../services/subTypeService";
import ControlledSelectV2 from "../shared/controlledInputComponents/ControlledSelectV2";
import { MasterContext } from "../shared/MasterContext";
import projectService from "../../services/projectService/projectService";
-import LockClosed from "../../assets/images/lock-closed.svg";
import ControlledSwitch from "../shared/controlledInputComponents/ControlledSwitch";
import { Palette } from "../../styles/theme";
import ControlledTextField from "../shared/controlledInputComponents/ControlledTextField";
+import { SpecialFieldGrid } from "../shared/specialField";
+import {
+ SpecialFieldEntityEnum,
+ SpecialFields,
+} from "../../constants/application-constant";
+import { Else, If, Then, When } from "react-if";
+import Icons from "../icons";
+import { IconProps } from "../icons/type";
+
+const LockClosedIcon: React.FC = Icons["LockClosedIcon"];
+const LockOpenIcon: React.FC = Icons["LockOpenIcon"];
const schema = yup.object().shape({
name: yup
@@ -64,6 +74,8 @@ export default function ProjectForm({ ...props }) {
const [types, setTypes] = React.useState([]);
const [proponents, setProponents] = React.useState();
const [disabled, setDisabled] = React.useState();
+ const [specialField, setSpecialField] = React.useState("");
+
const ctx = React.useContext(MasterContext);
React.useEffect(() => {
@@ -214,17 +226,24 @@ export default function ProjectForm({ ...props }) {
}}
>
Name
-
-
-
+
+
+
+ setSpecialField("")}>
+
+
+
+
+
+ setSpecialField(SpecialFields.PROJECT.NAME)
+ }
+ >
+
+
+
+
+
Proponent
-
-
-
+
+
+
+ setSpecialField("")}>
+
+
+
+
+
+ setSpecialField(SpecialFields.PROJECT.PROPONENT)
+ }
+ >
+
+
+
+
+
+
+
+
+
+ Update the Proponent of this Project.{" "}
+ Click this link for detailed
+ instructions.
+
+
+ Update the legal name of the Project and the dates each
+ name was in legal use. Click this link{" "}
+ for detailed instructions
+
+ >
+ }
+ options={
+ specialField === SpecialFields.PROJECT.PROPONENT
+ ? proponents?.map((p) => ({
+ label: p.name,
+ value: p.id.toString(),
+ })) || []
+ : []
+ }
+ onSave={() => {
+ // TODO: Refresh form field value for the specific field?
+ // OR do we just call form save/submit handler
+ ctx.setId(props.projectId);
+ ctx.getData();
+ }}
+ />
+
+
Type
-
+
>
)}
data={rfData}
diff --git a/epictrack-web/src/components/shared/MasterTrackTable/index.tsx b/epictrack-web/src/components/shared/MasterTrackTable/index.tsx
index 92aedb288..65335773b 100644
--- a/epictrack-web/src/components/shared/MasterTrackTable/index.tsx
+++ b/epictrack-web/src/components/shared/MasterTrackTable/index.tsx
@@ -1,6 +1,10 @@
import React from "react";
-import MaterialReactTable, {
- MaterialReactTableProps,
+import {
+ MaterialReactTable,
+ MRT_ColumnDef,
+ MRT_RowData,
+ MRT_TableOptions,
+ useMaterialReactTable,
} from "material-react-table";
import { Box, Container, Typography } from "@mui/material";
import SearchIcon from "../../../assets/images/search.svg";
@@ -49,161 +53,167 @@ const NoDataComponent = ({ ...props }) => {
>
);
};
-const MasterTrackTable = >({
+
+export interface MaterialReactTableProps
+ extends MRT_TableOptions {
+ columns: MRT_ColumnDef[];
+ data: TData[];
+}
+
+const MasterTrackTable = ({
columns,
data,
...rest
-}: MaterialReactTableProps) => {
- return (
- <>
- ({
- disabled: true,
- sx: {
- padding: "0.5rem 0.5rem 0.5rem 1rem",
- "& .MuiCheckbox-root": {
- width: "2.75rem !important",
- height: "2rem",
- borderRadius: "4px",
- padding: "8px !important",
- "&.Mui-disabled": {
- svg: {
- fill: Palette.neutral.light,
- },
- },
- },
- },
- })}
- muiTableHeadCellFilterTextFieldProps={({ column }) => ({
- placeholder: column.columnDef.header,
- variant: "outlined",
- sx: {
- backgroundColor: "white",
- "& .MuiInputBase-input::placeholder": {
- color: Palette.neutral.light,
- fontSize: "0.875rem",
- lineHeight: "1rem",
- opacity: 1,
- },
- "& .MuiInputAdornment-root": {
- display: "none",
- },
- "& .MuiSelect-icon": {
- mr: "0px !important",
+}: MaterialReactTableProps) => {
+ const table = useMaterialReactTable({
+ columns,
+ data,
+ globalFilterFn: "contains",
+ enableHiding: false,
+ enableGlobalFilter: false,
+ enableStickyHeader: true,
+ enableDensityToggle: false,
+ enableColumnFilters: true,
+ enableFullScreenToggle: false,
+ enableSorting: true,
+ enableFilters: true,
+ enableColumnActions: false,
+ enablePinning: true,
+ enablePagination: false,
+ positionActionsColumn: "last",
+ muiTableHeadProps: {
+ sx: {
+ "& .MuiTableRow-root": {
+ boxShadow: "none",
+ },
+ },
+ },
+ muiTableHeadCellProps: {
+ sx: {
+ backgroundColor: Palette.neutral.bg.light,
+ padding: "1rem 0.5rem 0.5rem 1rem !important",
+ "& .Mui-TableHeadCell-Content-Labels": {
+ fontSize: "1rem",
+ fontWeight: MET_Header_Font_Weight_Bold,
+ color: Palette.neutral.dark,
+ paddingBottom: "0.5rem",
+ },
+ "& .MuiTextField-root": {
+ minWidth: "0",
+ },
+ "& .MuiCheckbox-root": {
+ width: "2.75rem !important",
+ height: "2rem",
+ padding: "8px !important",
+ borderRadius: "4px",
+ },
+ },
+ },
+ muiTableProps: {
+ sx: {
+ tableLayout: "fixed",
+ },
+ },
+ muiTableBodyCellProps: ({ row }) => ({
+ disabled: true,
+ sx: {
+ padding: "0.5rem 0.5rem 0.5rem 1rem",
+ "& .MuiCheckbox-root": {
+ width: "2.75rem !important",
+ height: "2rem",
+ borderRadius: "4px",
+ padding: "8px !important",
+ "&.Mui-disabled": {
+ svg: {
+ fill: Palette.neutral.light,
},
},
- })}
- muiTableContainerProps={(table) => ({
- sx: {
- maxHeight: "100%",
- },
- })}
- muiTableBodyProps={{
- sx: {
- "& tr:hover td": {
- backgroundColor: Palette.primary.bg.light,
- },
- },
- }}
- muiTableBodyRowProps={{
- hover: true,
- sx: {
- "&.Mui-selected": {
- backgroundColor: Palette.primary.bg.main,
- },
- "&.MuiTableRow-hover:hover": {
- backgroundColor: Palette.primary.bg.light,
- },
- },
- }}
- sortingFns={{
- sortFn: (rowA: any, rowB: any, columnId: string) => {
- return rowA
- ?.getValue(columnId)
- ?.localeCompare(rowB?.getValue(columnId), "en", {
- numeric: true,
- ignorePunctuation: false,
- sensitivity: "base",
- });
- },
- }}
- renderToolbarInternalActions={({ table }) => (
- <>{/* */}>
- )}
- renderEmptyRowsFallback={({ table }) => (
-
- )}
- {...rest}
- initialState={{
- showColumnFilters: true,
- density: "compact",
- columnPinning: { right: ["mrt-row-actions"] },
- ...rest.initialState,
- }}
- state={{
- showGlobalFilter: true,
- columnPinning: { right: ["mrt-row-actions"] },
- ...rest.state,
- }}
- icons={{
- FilterAltIcon: () => null,
- CloseIcon: () => null,
- }}
- filterFns={{
- multiSelectFilter: (row, id, filterValue) => {
- if (filterValue.length === 0) return true;
- return filterValue.includes(row.getValue(id));
- },
- }}
- />
+ },
+ },
+ }),
+ muiFilterTextFieldProps: ({ column }) => ({
+ placeholder: column.columnDef.header,
+ variant: "outlined",
+ sx: {
+ backgroundColor: "white",
+ "& .MuiInputBase-input::placeholder": {
+ color: Palette.neutral.light,
+ fontSize: "0.875rem",
+ lineHeight: "1rem",
+ opacity: 1,
+ },
+ "& .MuiInputAdornment-root": {
+ display: "none",
+ },
+ "& .MuiSelect-icon": {
+ mr: "0px !important",
+ },
+ },
+ }),
+ muiTableContainerProps: (table) => ({
+ sx: {
+ maxHeight: "100%",
+ },
+ }),
+ muiTableBodyProps: {
+ sx: {
+ "& tr:hover td": {
+ backgroundColor: Palette.primary.bg.light,
+ },
+ },
+ },
+ muiTableBodyRowProps: {
+ hover: true,
+ sx: {
+ "&.Mui-selected": {
+ backgroundColor: Palette.primary.bg.main,
+ },
+ "&.MuiTableRow-hover:hover": {
+ backgroundColor: Palette.primary.bg.light,
+ },
+ },
+ },
+ sortingFns: {
+ sortFn: (rowA: any, rowB: any, columnId: string) => {
+ return rowA
+ ?.getValue(columnId)
+ ?.localeCompare(rowB?.getValue(columnId), "en", {
+ numeric: true,
+ ignorePunctuation: false,
+ sensitivity: "base",
+ });
+ },
+ },
+ renderToolbarInternalActions: ({ table }) => (
+ <>{/* */}>
+ ),
+ renderEmptyRowsFallback: ({ table }) => ,
+ ...rest,
+ initialState: {
+ showColumnFilters: true,
+ density: "compact",
+ columnPinning: { right: ["mrt-row-actions"] },
+ ...rest.initialState,
+ },
+ state: {
+ showGlobalFilter: true,
+ columnPinning: { right: ["mrt-row-actions"] },
+ ...rest.state,
+ },
+ icons: {
+ FilterAltIcon: () => null,
+ CloseIcon: () => null,
+ },
+ filterFns: {
+ multiSelectFilter: (row, id, filterValue) => {
+ if (filterValue.length === 0) return true;
+ return filterValue.includes(row.getValue(id));
+ },
+ },
+ });
+ return (
+ <>
+
>
);
};
diff --git a/epictrack-web/src/components/shared/specialField/components/InLineDatePicker.tsx b/epictrack-web/src/components/shared/specialField/components/InLineDatePicker.tsx
new file mode 100644
index 000000000..756570ed3
--- /dev/null
+++ b/epictrack-web/src/components/shared/specialField/components/InLineDatePicker.tsx
@@ -0,0 +1,62 @@
+import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
+import dayjs from "dayjs";
+import {
+ MRT_Cell,
+ MRT_Column,
+ MRT_Row,
+ MRT_TableInstance,
+} from "material-react-table";
+import React from "react";
+import { SpecialField } from "../type";
+
+interface Props {
+ cell: MRT_Cell;
+ column: MRT_Column;
+ row: MRT_Row;
+ table: MRT_TableInstance;
+ isCreating: boolean;
+ name: string;
+}
+
+export const InLineDatePicker = ({
+ cell,
+ column,
+ row,
+ table,
+ isCreating,
+ name,
+}: Props) => {
+ const onBlur = (newValue: any) => {
+ row._valuesCache[column.id] = newValue;
+ if (isCreating) {
+ table.setCreatingRow(row);
+ } else {
+ table.setEditingRow(row);
+ }
+ };
+
+ const value = dayjs(row._valuesCache[column.id]);
+
+ return (
+
+
+
+ );
+};
diff --git a/epictrack-web/src/components/shared/specialField/index.tsx b/epictrack-web/src/components/shared/specialField/index.tsx
new file mode 100644
index 000000000..a4999c054
--- /dev/null
+++ b/epictrack-web/src/components/shared/specialField/index.tsx
@@ -0,0 +1,313 @@
+import { Box, Button, IconButton, TextField } from "@mui/material";
+import React, { useEffect, useMemo, useState } from "react";
+import { When } from "react-if";
+import {
+ MRT_ColumnDef,
+ MRT_TableOptions,
+ MaterialReactTable,
+ useMaterialReactTable,
+} from "material-react-table";
+import ReactSelect, { CSSObjectWithLabel } from "react-select";
+import { Palette } from "../../../styles/theme";
+import { ETCaption2, ETCaption3, ETParagraph } from "..";
+import Icons from "../../icons";
+import { IconProps } from "../../icons/type";
+import { SpecialField, SpecialFieldProps } from "./type";
+import specialFieldService from "../../../services/specialFieldService";
+import { dateUtils } from "../../../utils";
+import { InLineDatePicker } from "./components/InLineDatePicker";
+
+const AddIcon: React.FC = Icons["AddIcon"];
+const EditIcon: React.FC = Icons["PencilEditIcon"];
+const CheckIcon: React.FC = Icons["CheckIcon"];
+const CancelIcon: React.FC = Icons["CloseXIcon"];
+
+const Styles = {
+ flexStart: {
+ display: "flex",
+ alignItems: "flex-start",
+ },
+};
+
+export const SpecialFieldGrid = ({
+ entity,
+ entity_id,
+ fieldLabel,
+ fieldName,
+ fieldType,
+ title,
+ description,
+ options,
+ onSave,
+}: SpecialFieldProps) => {
+ const [loading, setLoading] = useState(false);
+ const [entries, setEntries] = useState([]);
+
+ const getEntries = async () => {
+ setLoading(true);
+ const specialFieldEntries = await specialFieldService.getEntries(
+ entity,
+ entity_id,
+ fieldName
+ );
+ if (specialFieldEntries.status === 200) {
+ setEntries(specialFieldEntries.data as SpecialField[]);
+ }
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ getEntries();
+ }, [fieldName]);
+
+ const columns = useMemo[]>(
+ () => [
+ {
+ accessorKey: "field_value",
+ header: fieldLabel,
+ editVariant: fieldType,
+ editSelectOptions: options,
+ size: 300,
+ Cell: ({ cell }) => {
+ const id = cell.getValue();
+ let value;
+ if (fieldType === "select") {
+ const option = options?.find((o) => id.toString() === o.value);
+ value = option?.label;
+ } else {
+ value = id;
+ }
+ return {value};
+ },
+ muiEditTextFieldProps: ({ cell, column, row, table }) => {
+ return {
+ required: true,
+ InputProps: {
+ sx: { fontSize: "14px" },
+ },
+ };
+ },
+ Edit: ({ cell, column, row, table }) => {
+ let value: any = cell.getValue();
+ if (fieldType === "select") {
+ value = options?.find((o) => row.original.field_value == o.value);
+ }
+ const onBlur = (newValue: any) => {
+ debugger;
+ row._valuesCache[column.id] =
+ fieldType === "select" ? newValue.value : newValue.target.value;
+ if (Boolean(tableState.creatingRow)) {
+ table.setCreatingRow(row);
+ } else {
+ table.setEditingRow(row);
+ }
+ };
+
+ return (
+ <>
+
+ {
+ return;
+ }}
+ menuPortalTarget={document.body}
+ name={fieldName}
+ styles={{
+ container: (base: CSSObjectWithLabel) => ({
+ ...base,
+ maxWidth: "284px", // 300 - padding of 16px
+ }),
+ menuPortal: (base: CSSObjectWithLabel) => ({
+ ...base,
+ zIndex: 99999,
+ fontSize: "14px",
+ }),
+ }}
+ defaultValue={value || ""}
+ onChange={onBlur}
+ />
+
+
+
+
+ >
+ );
+ },
+ },
+ {
+ accessorKey: "active_from",
+ header: "From",
+ editVariant: "text",
+ size: 170,
+ Edit: ({ cell, column, row, table }) => {
+ return (
+
+ );
+ },
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+ return {dateUtils.formatDate(value)};
+ },
+ },
+ {
+ accessorKey: "active_to",
+ header: "To",
+ size: 170,
+ enableEditing: false,
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+ return {value ? dateUtils.formatDate(value) : "Today"};
+ },
+ },
+ ],
+ [fieldLabel, fieldName, options]
+ );
+
+ const handleEditRowSave: MRT_TableOptions["onEditingRowSave"] =
+ async ({ values, table, row }) => {
+ await saveEntry(values, row.original.id);
+ table.setEditingRow(null); //exit editing mode
+ };
+
+ const handleCreateRowSave: MRT_TableOptions["onCreatingRowSave"] =
+ async ({ values, table }) => {
+ debugger;
+ await saveEntry(values);
+ table.setCreatingRow(null); //exit creating mode
+ };
+
+ const saveEntry = async (
+ payload: SpecialField,
+ objectId: number | undefined = undefined
+ ) => {
+ const data = {
+ ...payload,
+ entity,
+ entity_id,
+ field_name: fieldName,
+ };
+
+ if (objectId) {
+ await specialFieldService.updateSpecialFieldEntry(data, objectId);
+ } else {
+ await specialFieldService.createSpecialFieldEntry(data);
+ }
+ getEntries();
+ if (onSave) {
+ onSave();
+ }
+ };
+
+ const table = useMaterialReactTable({
+ columns: columns,
+ data: entries,
+ state: {
+ isLoading: loading,
+ },
+ enableSorting: false,
+ editDisplayMode: "row",
+ createDisplayMode: "row",
+ enableFilters: false,
+ enableGlobalFilter: false,
+ enableEditing: true,
+ positionActionsColumn: "last",
+ enableHiding: false,
+ enableStickyHeader: true,
+ enableDensityToggle: false,
+ enableColumnFilters: true,
+ enableFullScreenToggle: false,
+ enableColumnActions: false,
+ enablePinning: false,
+ enablePagination: false,
+ getRowId: (originalRow) => originalRow.id?.toString() || "",
+ renderTopToolbarCustomActions: ({ table }) => (
+ }
+ onClick={() => {
+ table.setCreatingRow(true);
+ }}
+ >
+ New Entry
+
+ ),
+ muiTableBodyCellProps: {
+ sx: {
+ fontSize: "14px",
+ },
+ },
+ muiTableHeadCellProps: {
+ sx: {
+ fontSize: "14px",
+ },
+ },
+ onEditingRowSave: handleEditRowSave,
+ onCreatingRowSave: handleCreateRowSave,
+ renderRowActions: ({ row, table }) => (
+ {
+ table.setEditingRow(row);
+ }}
+ >
+
+
+ ),
+ icons: {
+ SaveIcon: (props: any) => (
+
+ ),
+ CancelIcon: (props: any) => (
+
+ ),
+ },
+ });
+
+ const tableState = table.getState();
+
+ return (
+
+
+
+ {title}
+
+ {description}
+
+
+
+
+
+ );
+};
diff --git a/epictrack-web/src/components/shared/specialField/type.ts b/epictrack-web/src/components/shared/specialField/type.ts
new file mode 100644
index 000000000..0d60ee387
--- /dev/null
+++ b/epictrack-web/src/components/shared/specialField/type.ts
@@ -0,0 +1,38 @@
+import { SpecialFieldEntityEnum } from "../../../constants/application-constant";
+import { OptionType } from "../filterSelect/type";
+
+export interface EditSelectOptionType {
+ text: string;
+ value: any;
+}
+export interface SpecialFieldBaseProps {
+ entity: SpecialFieldEntityEnum;
+ title: string;
+ description: React.ReactElement;
+ entity_id: number;
+ fieldLabel: string; // Display label
+ fieldName: string; // Name in API requests
+ onSave?: () => any;
+}
+
+interface SpecialFieldTextProps extends SpecialFieldBaseProps {
+ fieldType: "text";
+ options?: OptionType[];
+}
+
+interface SpecialFieldSelectProps extends SpecialFieldBaseProps {
+ fieldType: "select";
+ options: OptionType[];
+}
+
+export type SpecialFieldProps = SpecialFieldTextProps | SpecialFieldSelectProps;
+
+export interface SpecialField {
+ id?: number;
+ entity: SpecialFieldEntityEnum;
+ entity_id: number;
+ field_name: string;
+ field_value: string;
+ active_from: string;
+ active_to: string;
+}
diff --git a/epictrack-web/src/components/user/UserList.tsx b/epictrack-web/src/components/user/UserList.tsx
index e199f3495..c98de41a1 100644
--- a/epictrack-web/src/components/user/UserList.tsx
+++ b/epictrack-web/src/components/user/UserList.tsx
@@ -1,6 +1,6 @@
import React, { useEffect } from "react";
-import MaterialReactTable, {
- type MaterialReactTableProps,
+import {
+ MaterialReactTable,
type MRT_ColumnDef,
type MRT_Row,
} from "material-react-table";
@@ -18,7 +18,9 @@ import { RESULT_STATUS } from "../../constants/application-constant";
import UserService from "../../services/userService";
import { ETPageContainer } from "../shared";
import Select from "react-select";
-import MasterTrackTable from "../shared/MasterTrackTable";
+import MasterTrackTable, {
+ MaterialReactTableProps,
+} from "../shared/MasterTrackTable";
import { UserGroupUpdate } from "../../services/userService/type";
import { useAppSelector } from "../../hooks";
@@ -159,7 +161,7 @@ const UserList = () => {
{
diff --git a/epictrack-web/src/constants/api-endpoint.ts b/epictrack-web/src/constants/api-endpoint.ts
index de0e2fb1b..e39168965 100644
--- a/epictrack-web/src/constants/api-endpoint.ts
+++ b/epictrack-web/src/constants/api-endpoint.ts
@@ -99,5 +99,9 @@ const Endpoints = {
Position: {
GET_ALL: "positions",
},
+ SpecialFields: {
+ SPECIAL_FIELDS: "/special-fields",
+ UPDATE: "/special-fields/:specialFieldId",
+ },
};
export default Endpoints;
diff --git a/epictrack-web/src/constants/application-constant.ts b/epictrack-web/src/constants/application-constant.ts
index 1f71a2fd0..25ae18f86 100644
--- a/epictrack-web/src/constants/application-constant.ts
+++ b/epictrack-web/src/constants/application-constant.ts
@@ -89,3 +89,16 @@ export const ROLES = {
MANAGE_USERS: "manage_users",
EXTENDED_EDIT: "extended_edit",
};
+
+export enum SpecialFieldEntityEnum {
+ PROJECT = "PROJECT",
+ PROPONENT = "PROPONENT",
+ WORK = "WORK",
+}
+
+export const SpecialFields = {
+ PROJECT: {
+ NAME: "name",
+ PROPONENT: "proponent_id",
+ },
+};
diff --git a/epictrack-web/src/services/specialFieldService/index.ts b/epictrack-web/src/services/specialFieldService/index.ts
new file mode 100644
index 000000000..1c06a885b
--- /dev/null
+++ b/epictrack-web/src/services/specialFieldService/index.ts
@@ -0,0 +1,37 @@
+import Endpoints from "../../constants/api-endpoint";
+import http from "../../apiManager/http-request-handler";
+import { SpecialFieldEntityEnum } from "../../constants/application-constant";
+import { SpecialField } from "../../components/shared/specialField/type";
+
+class SpecialFieldService {
+ getEntries = async (
+ entity: SpecialFieldEntityEnum,
+ entity_id: number,
+ field_name: string
+ ) => {
+ return await http.GetRequest(Endpoints.SpecialFields.SPECIAL_FIELDS, {
+ entity,
+ entity_id,
+ field_name,
+ });
+ };
+
+ createSpecialFieldEntry = async (payload: SpecialField) => {
+ return await http.PostRequest(
+ Endpoints.SpecialFields.SPECIAL_FIELDS,
+ payload
+ );
+ };
+
+ updateSpecialFieldEntry = async (payload: SpecialField, objectId: number) => {
+ return await http.PutRequest(
+ Endpoints.SpecialFields.UPDATE.replace(
+ ":specialFieldId",
+ objectId.toString()
+ ),
+ payload
+ );
+ };
+}
+
+export default new SpecialFieldService();