diff --git a/backend/lcfs/db/migrations/versions/2024-12-24-07-40_d9cdd9fca0ce.py b/backend/lcfs/db/migrations/versions/2024-12-24-07-40_d9cdd9fca0ce.py index e865535c4..fe90da04f 100644 --- a/backend/lcfs/db/migrations/versions/2024-12-24-07-40_d9cdd9fca0ce.py +++ b/backend/lcfs/db/migrations/versions/2024-12-24-07-40_d9cdd9fca0ce.py @@ -169,7 +169,8 @@ def upgrade() -> None: (20, 'Compression-ignition engine- Marine, with methane slip reduction kit- Operated within 76 to 100% of load range', TRUE), (21, 'Compression-ignition engine- Marine, unknown whether kit is installed or average operating load range', TRUE), (22, 'Unknown engine type', TRUE), - (23, 'Other (i.e. road transportation)', TRUE) + (23, 'Other (i.e. road transportation)', TRUE), + (24, 'Any', TRUE) ON CONFLICT (end_use_type_id) DO NOTHING; """) @@ -224,13 +225,13 @@ def upgrade() -> None: eer_id, fuel_category_id, fuel_type_id, end_use_type_id, ratio, effective_status ) VALUES - (1, 1, 2, NULL, 0.9, TRUE), + (1, 1, 2, 24, 0.9, TRUE), (2, 1, 3, 1, 3.5, TRUE), (3, 1, 3, 2, 1.0, TRUE), (4, 1, 6, 3, 2.4, TRUE), (5, 1, 6, 2, 0.9, TRUE), - (6, 1, 13, NULL, 0.9, TRUE), - (7, 2, 2, NULL, 0.9, TRUE), + (6, 1, 13, 24, 0.9, TRUE), + (7, 2, 2, 24, 0.9, TRUE), (8, 2, 3, 4, 3.8, TRUE), (9, 2, 3, 5, 3.2, TRUE), (10, 2, 3, 6, 2.5, TRUE), @@ -242,9 +243,9 @@ def upgrade() -> None: (16, 2, 3, 2, 1.0, TRUE), (17, 2, 6, 3, 1.8, TRUE), (18, 2, 6, 2, 0.9, TRUE), - (19, 2, 13, NULL, 0.9, TRUE), - (20, 3, 3, NULL, 2.5, TRUE), - (21, 3, 11, NULL, 1.0, TRUE), + (19, 2, 13, 24, 0.9, TRUE), + (20, 3, 3, 24, 2.5, TRUE), + (21, 3, 11, 24, 1.0, TRUE), (22, 2, 7, 15, 1.0, TRUE), (23, 2, 7, 16, 1.0, TRUE), (24, 2, 7, 17, 1.0, TRUE), @@ -253,7 +254,20 @@ def upgrade() -> None: (27, 2, 7, 20, 1.0, TRUE), (28, 2, 7, 21, 1.0, TRUE), (29, 2, 7, 22, 0.9, TRUE), - (30, 2, 7, 23, 0.9, TRUE) + (30, 2, 7, 23, 0.9, TRUE), + (31, 2, 1, 24, 1.0, TRUE), + (32, 2, 5, 24, 1.0, TRUE), + (33, 3, 6, 24, 1.0, TRUE), + (34, 1, 14, 24, 1.0, TRUE), + (35, 1, 15, 24, 1.0, TRUE), + (36, 2, 16, 24, 1.0, TRUE), + (37, 1, 17, 24, 1.0, TRUE), + (38, 3, 18, 24, 1.0, TRUE), + (39, 1, 19, 24, 1.0, TRUE), + (40, 2, 19, 24, 1.0, TRUE), + (41, 3, 7, 24, 1.0, TRUE), + (42, 2, 20, 24, 1.0, TRUE), + (43, 1, 4, 24, 1.0, TRUE) ON CONFLICT (eer_id) DO NOTHING; """) diff --git a/backend/lcfs/db/migrations/versions/2025-01-03-23-31_e883ad1f0f60.py b/backend/lcfs/db/migrations/versions/2025-01-03-23-31_e883ad1f0f60.py new file mode 100644 index 000000000..eab360311 --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2025-01-03-23-31_e883ad1f0f60.py @@ -0,0 +1,67 @@ +"""Rename Reassessed + +Revision ID: e883ad1f0f60 +Revises: 9329e38396e1 +Create Date: 2025-01-03 23:31:19.098618 + +""" + +import sqlalchemy as sa +from alembic import op +from alembic_postgresql_enum import TableReference + +# revision identifiers, used by Alembic. +revision = "e883ad1f0f60" +down_revision = "9329e38396e1" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values( + "public", + "compliancereportstatusenum", + [ + "Draft", + "Submitted", + "Recommended_by_analyst", + "Recommended_by_manager", + "Assessed", + "Reassessed", + ], + [ + TableReference( + table_schema="public", + table_name="compliance_report_status", + column_name="status", + ) + ], + enum_values_to_rename=[("ReAssessed", "Reassessed")], + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values( + "public", + "compliancereportstatusenum", + [ + "Draft", + "Submitted", + "Recommended_by_analyst", + "Recommended_by_manager", + "Assessed", + "ReAssessed", + ], + [ + TableReference( + table_schema="public", + table_name="compliance_report_status", + column_name="status", + ) + ], + enum_values_to_rename=[("Reassessed", "ReAssessed")], + ) + # ### end Alembic commands ### diff --git a/backend/lcfs/db/migrations/versions/2025-01-04-13-24_bfa7bbb1eea3.py b/backend/lcfs/db/migrations/versions/2025-01-04-13-24_bfa7bbb1eea3.py new file mode 100644 index 000000000..1ce35a7a0 --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2025-01-04-13-24_bfa7bbb1eea3.py @@ -0,0 +1,37 @@ +"""Update fuel type name + +Revision ID: bfa7bbb1eea3 +Revises: 9329e38396e1 +Create Date: 2025-01-03 13:24:19.525006 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "bfa7bbb1eea3" +down_revision = "e883ad1f0f60" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Update fuel type name + op.execute(""" + UPDATE fuel_type + SET fuel_type = 'Other diesel fuel', + provision_1_id = 3 -- Change from prescribed (1) to default (3) + WHERE fuel_type = 'Other diesel'; + """) + + +def downgrade() -> None: + # Revert fuel type name update + op.execute(""" + UPDATE fuel_type + SET fuel_type = 'Other diesel', + provision_1_id = 1 -- Change from default (3) to prescribed (1) + WHERE fuel_type = 'Other diesel fuel'; + """) + diff --git a/backend/lcfs/db/migrations/versions/2025-01-05-18-43_ca7200152130.py b/backend/lcfs/db/migrations/versions/2025-01-05-18-43_ca7200152130.py new file mode 100644 index 000000000..c41500e64 --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2025-01-05-18-43_ca7200152130.py @@ -0,0 +1,37 @@ +"""Add is_legacy to fuel_type + +Revision ID: ca7200152130 +Revises: bfa7bbb1eea3 +Create Date: 2025-01-05 18:43:43.638740 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ca7200152130" +down_revision = "bfa7bbb1eea3" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "fuel_type", + sa.Column( + "is_legacy", + sa.Boolean(), + server_default=sa.text("FALSE"), + nullable=False, + comment="Indicates if the fuel type is legacy and should not be used for new reports", + ), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("fuel_type", "is_legacy") + # ### end Alembic commands ### diff --git a/backend/lcfs/db/migrations/versions/2025-01-06-19-01_94306eca5261.py b/backend/lcfs/db/migrations/versions/2025-01-06-19-01_94306eca5261.py new file mode 100644 index 000000000..4f9a65214 --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2025-01-06-19-01_94306eca5261.py @@ -0,0 +1,125 @@ +""" +Add is_legacy to Provisions and insert data + +Revision ID: 94306eca5261 +Revises: ca7200152130 +Create Date: 2025-01-06 19:01:53.418638 +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "94306eca5261" +down_revision = "ca7200152130" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 1) Add the new column + op.add_column( + "provision_of_the_act", + sa.Column( + "is_legacy", + sa.Boolean(), + server_default=sa.text("FALSE"), + nullable=False, + comment="Indicates if the provision is legacy and should not be used for new reports", + ), + ) + + # 2) Insert or update data to populate is_legacy, etc. + # For demonstration, we'll use bulk_insert here. If your table already + # has data, you might prefer an UPDATE or a combination of both. + provision_of_the_act = sa.Table( + "provision_of_the_act", + sa.MetaData(), + sa.Column("provision_of_the_act_id", sa.Integer, primary_key=True), + sa.Column("name", sa.String), + sa.Column("description", sa.String), + sa.Column("create_user", sa.String), + sa.Column("update_user", sa.String), + sa.Column("display_order", sa.Integer), + sa.Column("effective_date", sa.Date), + sa.Column("effective_status", sa.Boolean), + sa.Column("expiration_date", sa.Date), + sa.Column("is_legacy", sa.Boolean), + ) + + op.bulk_insert( + provision_of_the_act, + [ + { + "name": "Prescribed carbon intensity - Section 6 (5) (a)", + "description": "Prescribed carbon intensity - Section 6 (5) (a)", + "create_user": "no_user", + "update_user": "no_user", + "display_order": None, + "effective_date": None, + "effective_status": True, + "expiration_date": None, + "is_legacy": True, + }, + { + "name": "Prescribed carbon intensity - Section 6 (5) (b)", + "description": "Prescribed carbon intensity - Section 6 (5) (b)", + "create_user": "no_user", + "update_user": "no_user", + "display_order": None, + "effective_date": None, + "effective_status": True, + "expiration_date": None, + "is_legacy": True, + }, + { + "name": "Approved fuel code - Section 6 (5) (c)", + "description": "Approved fuel code - Section 6 (5) (c)", + "create_user": "no_user", + "update_user": "no_user", + "display_order": None, + "effective_date": None, + "effective_status": True, + "expiration_date": None, + "is_legacy": True, + }, + { + "name": "Default Carbon Intensity Value - Section 6 (5) (d) (i)", + "description": "Default Carbon Intensity Value - Section 6 (5) (d) (i)", + "create_user": "no_user", + "update_user": "no_user", + "display_order": None, + "effective_date": None, + "effective_status": True, + "expiration_date": None, + "is_legacy": True, + }, + { + "name": "GHGenius modelled - Section 6 (5) (d) (ii) (A)", + "description": "GHGenius modelled - Section 6 (5) (d) (ii) (A)", + "create_user": "no_user", + "update_user": "no_user", + "display_order": None, + "effective_date": None, + "effective_status": True, + "expiration_date": None, + "is_legacy": True, + }, + { + "name": "Alternative Method - Section 6 (5) (d) (ii) (B)", + "description": "Alternative Method - Section 6 (5) (d) (ii) (B)", + "create_user": "no_user", + "update_user": "no_user", + "display_order": None, + "effective_date": None, + "effective_status": True, + "expiration_date": None, + "is_legacy": True, + }, + ], + ) + + +def downgrade() -> None: + # Remove is_legacy column. (Data removal is optional or up to you.) + op.drop_column("provision_of_the_act", "is_legacy") diff --git a/backend/lcfs/db/models/compliance/ComplianceReportStatus.py b/backend/lcfs/db/models/compliance/ComplianceReportStatus.py index ee366c8a1..ffd0b5c00 100644 --- a/backend/lcfs/db/models/compliance/ComplianceReportStatus.py +++ b/backend/lcfs/db/models/compliance/ComplianceReportStatus.py @@ -11,7 +11,7 @@ class ComplianceReportStatusEnum(enum.Enum): Recommended_by_analyst = "Recommended by analyst" Recommended_by_manager = "Recommended by manager" Assessed = "Assessed" - ReAssessed = "ReAssessed" + Reassessed = "Reassessed" class ComplianceReportStatus(BaseModel, EffectiveDates): diff --git a/backend/lcfs/db/models/fuel/FuelType.py b/backend/lcfs/db/models/fuel/FuelType.py index 4ebbb7aae..e6d634805 100644 --- a/backend/lcfs/db/models/fuel/FuelType.py +++ b/backend/lcfs/db/models/fuel/FuelType.py @@ -1,6 +1,6 @@ import enum -from sqlalchemy import Column, Integer, Text, Boolean, Enum, Numeric +from sqlalchemy import Column, Integer, Text, Boolean, Enum, Numeric, text from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship @@ -54,6 +54,12 @@ class FuelType(BaseModel, Auditable, DisplayOrder): nullable=False, comment="Indicates if the fuel type is unrecognized", ) + is_legacy = Column( + Boolean, + server_default=text("FALSE"), + nullable=False, + comment="Indicates if the fuel type is legacy and should not be used for new reports", + ) # Relationships fuel_codes = relationship("FuelCode", back_populates="fuel_type") diff --git a/backend/lcfs/db/models/fuel/ProvisionOfTheAct.py b/backend/lcfs/db/models/fuel/ProvisionOfTheAct.py index e10178ba6..f034682d3 100644 --- a/backend/lcfs/db/models/fuel/ProvisionOfTheAct.py +++ b/backend/lcfs/db/models/fuel/ProvisionOfTheAct.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy import Column, Integer, String, Boolean, text from sqlalchemy.orm import relationship from lcfs.db.base import BaseModel, Auditable, DisplayOrder, EffectiveDates @@ -28,6 +28,12 @@ class ProvisionOfTheAct(BaseModel, Auditable, DisplayOrder, EffectiveDates): nullable=False, comment="Description of the provision. This is the displayed name. e.g. Prescribed Carbon Intensity, Approved Fuel Code.", ) + is_legacy = Column( + Boolean, + server_default=text("FALSE"), + nullable=False, + comment="Indicates if the fuel type is legacy and should not be used for new reports", + ) # relationships fuel_type_provision_1 = relationship( diff --git a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_repo.py b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_repo.py index e87050cfd..f74a763c2 100644 --- a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_repo.py +++ b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_repo.py @@ -74,6 +74,7 @@ async def test_check_duplicate(fuel_supply_repo, mock_db_session): compliance_report_id=1, fuel_type_id=1, fuel_category_id=1, + end_use_id=24, provision_of_the_act_id=1, quantity=1000, units="L", @@ -82,7 +83,8 @@ async def test_check_duplicate(fuel_supply_repo, mock_db_session): # Set up the mock chain using regular MagicMock since the chained methods are sync mock_result_chain = MagicMock() mock_result_chain.scalars = MagicMock(return_value=mock_result_chain) - mock_result_chain.first = MagicMock(return_value=MagicMock(spec=FuelSupply)) + mock_result_chain.first = MagicMock( + return_value=MagicMock(spec=FuelSupply)) # Define an async execute function that returns our mock chain async def mock_execute(*args, **kwargs): diff --git a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py index 9b5114a84..7f6407694 100644 --- a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py +++ b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_services.py @@ -67,13 +67,15 @@ def fuel_supply_service(): @pytest.mark.anyio async def test_get_fuel_supply_options(fuel_supply_service): service, mock_repo, mock_fuel_code_repo = fuel_supply_service - mock_repo.get_fuel_supply_table_options = AsyncMock(return_value={"fuel_types": []}) + mock_repo.get_fuel_supply_table_options = AsyncMock( + return_value={"fuel_types": []}) compliance_period = "2023" response = await service.get_fuel_supply_options(compliance_period) assert isinstance(response, FuelTypeOptionsResponse) - mock_repo.get_fuel_supply_table_options.assert_awaited_once_with(compliance_period) + mock_repo.get_fuel_supply_table_options.assert_awaited_once_with( + compliance_period) # Asynchronous test for get_fuel_supply_list @@ -90,7 +92,8 @@ async def test_get_fuel_supply_list(fuel_supply_service): response = await service.get_fuel_supply_list(compliance_report_id) assert isinstance(response, FuelSuppliesSchema) - mock_repo.get_fuel_supply_list.assert_awaited_once_with(compliance_report_id) + mock_repo.get_fuel_supply_list.assert_awaited_once_with( + compliance_report_id) @pytest.mark.anyio @@ -102,6 +105,7 @@ async def test_update_fuel_supply_not_found(fuel_supply_action_service): compliance_report_id=1, fuel_type_id=1, fuel_category_id=1, + end_use_id=24, provision_of_the_act_id=1, quantity=2000, units="L", @@ -274,8 +278,10 @@ async def test_create_fuel_supply(fuel_supply_action_service): "fuelCode": "FUEL123", "carbonIntensity": 15.0, }, - provisionOfTheAct={"provisionOfTheActId": 1, "name": "Act Provision"}, - endUseType={"endUseTypeId": 1, "type": "Transport", "subType": "Personal"}, + provisionOfTheAct={"provisionOfTheActId": 1, + "name": "Act Provision"}, + endUseType={"endUseTypeId": 1, + "type": "Transport", "subType": "Personal"}, units="L", compliancePeriod="2024", ) @@ -290,7 +296,8 @@ async def test_create_fuel_supply(fuel_supply_action_service): ) mock_density = MagicMock(spec=EnergyDensity) mock_density.density = 30.0 - mock_fuel_code_repo.get_energy_density = AsyncMock(return_value=mock_density) + mock_fuel_code_repo.get_energy_density = AsyncMock( + return_value=mock_density) user_type = UserTypeEnum.SUPPLIER @@ -315,6 +322,7 @@ async def test_delete_fuel_supply(fuel_supply_action_service): group_uuid="some-uuid", fuel_type_id=1, fuel_category_id=1, + end_use_id=24, provision_of_the_act_id=1, quantity=1000, units="L", @@ -338,5 +346,6 @@ async def test_delete_fuel_supply(fuel_supply_action_service): assert response.success is True assert response.message == "Marked as deleted." - mock_repo.get_latest_fuel_supply_by_group_uuid.assert_awaited_once_with("some-uuid") + mock_repo.get_latest_fuel_supply_by_group_uuid.assert_awaited_once_with( + "some-uuid") mock_repo.create_fuel_supply.assert_awaited_once() diff --git a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_validation.py b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_validation.py index eefa25c91..3fa217dd4 100644 --- a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_validation.py +++ b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_validation.py @@ -29,6 +29,7 @@ async def test_check_duplicate(fuel_supply_validation): compliance_report_id=1, fuel_type_id=1, fuel_category_id=1, + end_use_id=24, provision_of_the_act_id=1, quantity=2000, units="L", @@ -54,6 +55,7 @@ async def test_validate_other_recognized_type(fuel_supply_validation): compliance_report_id=1, fuel_type_id=1, # Some recognized type ID fuel_category_id=1, + end_use_id=24, provision_of_the_act_id=1, quantity=2000, units="L", @@ -76,6 +78,7 @@ async def test_validate_other_unrecognized_type_with_other(fuel_supply_validatio compliance_report_id=1, fuel_type_id=99, # Assume 99 is unrecognized "Other" type fuel_category_id=1, + end_use_id=24, provision_of_the_act_id=1, quantity=2000, units="L", @@ -99,6 +102,7 @@ async def test_validate_other_unrecognized_type_missing_other(fuel_supply_valida compliance_report_id=1, fuel_type_id=99, # Assume 99 is unrecognized "Other" type fuel_category_id=1, + end_use_id=24, provision_of_the_act_id=1, quantity=2000, units="L", diff --git a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_view.py b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_view.py index 81fa58b7a..9fa2efa2e 100644 --- a/backend/lcfs/tests/fuel_supply/test_fuel_supplies_view.py +++ b/backend/lcfs/tests/fuel_supply/test_fuel_supplies_view.py @@ -47,6 +47,7 @@ async def test_save_fuel_supply_row_create( "compliance_report_id": 1, "fuel_type_id": 1, "fuel_category_id": 1, + "end_use_id": 24, "provision_of_the_act_id": 1, "quantity": 1000, "units": "L", @@ -104,6 +105,7 @@ async def test_save_fuel_supply_row_update( "fuel_supply_id": 123, "fuel_type_id": 1, "fuel_category_id": 1, + "end_use_id": 24, "provision_of_the_act_id": 1, "quantity": 1000, "units": "L", @@ -160,6 +162,7 @@ async def test_save_fuel_supply_row_delete( "fuel_supply_id": 123, "fuel_type_id": 1, "fuel_category_id": 1, + "end_use_id": 24, "provision_of_the_act_id": 1, "quantity": 1000, "units": "L", @@ -191,7 +194,8 @@ async def test_save_fuel_supply_row_delete( assert response.status_code == status.HTTP_201_CREATED data = response.json() - assert data == {"success": True, "message": "Fuel supply row deleted successfully"} + assert data == {"success": True, + "message": "Fuel supply row deleted successfully"} @pytest.mark.anyio diff --git a/backend/lcfs/tests/other_uses/test_other_uses_repo.py b/backend/lcfs/tests/other_uses/test_other_uses_repo.py index 67ea7d1d5..3906980c8 100644 --- a/backend/lcfs/tests/other_uses/test_other_uses_repo.py +++ b/backend/lcfs/tests/other_uses/test_other_uses_repo.py @@ -36,7 +36,7 @@ def other_uses_repo(mock_db_session): repo.fuel_code_repo.get_expected_use_types = AsyncMock(return_value=[]) # Mock for local get_formatted_fuel_types method - async def mock_get_formatted_fuel_types(): + async def mock_get_formatted_fuel_types(include_legacy=False): mock_result = await mock_db_session.execute(AsyncMock()) return mock_result.unique().scalars().all() @@ -116,7 +116,7 @@ def mock_execute_side_effect(*args, **kwargs): ) # Execute the method under test - result = await other_uses_repo.get_table_options() + result = await other_uses_repo.get_table_options("2024") # Assertions assert isinstance(result, dict) @@ -222,6 +222,7 @@ async def test_get_latest_other_uses_by_group_uuid(other_uses_repo, mock_db_sess assert result.user_type == UserTypeEnum.GOVERNMENT assert result.version == 2 + @pytest.mark.anyio async def test_get_other_use_version_by_user(other_uses_repo, mock_db_session): group_uuid = "test-group-uuid" diff --git a/backend/lcfs/tests/other_uses/test_other_uses_services.py b/backend/lcfs/tests/other_uses/test_other_uses_services.py index 86794426a..e36d9ab7f 100644 --- a/backend/lcfs/tests/other_uses/test_other_uses_services.py +++ b/backend/lcfs/tests/other_uses/test_other_uses_services.py @@ -36,7 +36,7 @@ async def test_get_table_options(other_uses_service): } ) - response = await service.get_table_options() + response = await service.get_table_options("2024") assert isinstance(response, OtherUsesTableOptionsSchema) mock_repo.get_table_options.assert_awaited_once() @@ -66,16 +66,15 @@ async def test_create_other_use(other_uses_service): mock_fuel_code.fuel_code = "FuelCode123" # Mock fuel repository methods - mock_fuel_repo.get_fuel_category_by = AsyncMock( - return_value=mock_fuel_category) - mock_fuel_repo.get_fuel_type_by_name = AsyncMock( - return_value=mock_fuel_type) + mock_fuel_repo.get_fuel_category_by = AsyncMock(return_value=mock_fuel_category) + mock_fuel_repo.get_fuel_type_by_name = AsyncMock(return_value=mock_fuel_type) mock_fuel_repo.get_expected_use_type_by_name = AsyncMock( - return_value=mock_expected_use) + return_value=mock_expected_use + ) mock_fuel_repo.get_provision_of_the_act_by_name = AsyncMock( - return_value=mock_provision_of_the_act) - mock_fuel_repo.get_fuel_code_by_name = AsyncMock( - return_value=mock_fuel_code) + return_value=mock_provision_of_the_act + ) + mock_fuel_repo.get_fuel_code_by_name = AsyncMock(return_value=mock_fuel_code) # Create a mock for the created other use mock_created_use = create_mock_entity({}) @@ -120,8 +119,7 @@ async def test_update_other_use(other_uses_service): mock_existing_use = create_mock_entity({}) # Configure repository methods to return these mocked objects - mock_repo.get_other_use_version_by_user = AsyncMock( - return_value=mock_existing_use) + mock_repo.get_other_use_version_by_user = AsyncMock(return_value=mock_existing_use) # Mock related entities with proper string attributes mock_fuel_type = MagicMock() @@ -140,17 +138,15 @@ async def test_update_other_use(other_uses_service): mock_fuel_code.fuel_code = "NewFuelCode" # Mock fuel repository methods - mock_fuel_repo.get_fuel_type_by_name = AsyncMock( - return_value=mock_fuel_type) - mock_fuel_repo.get_fuel_category_by = AsyncMock( - return_value=mock_fuel_category) + mock_fuel_repo.get_fuel_type_by_name = AsyncMock(return_value=mock_fuel_type) + mock_fuel_repo.get_fuel_category_by = AsyncMock(return_value=mock_fuel_category) mock_fuel_repo.get_expected_use_type_by_name = AsyncMock( - return_value=mock_expected_use) + return_value=mock_expected_use + ) mock_fuel_repo.get_provision_of_the_act_by_name = AsyncMock( return_value=mock_provision_of_the_act ) - mock_fuel_repo.get_fuel_code_by_name = AsyncMock( - return_value=mock_fuel_code) + mock_fuel_repo.get_fuel_code_by_name = AsyncMock(return_value=mock_fuel_code) # Mock the updated use returned after the update mock_updated_use = MagicMock() diff --git a/backend/lcfs/tests/other_uses/test_other_uses_view.py b/backend/lcfs/tests/other_uses/test_other_uses_view.py index 3de83dee8..3f0e92f42 100644 --- a/backend/lcfs/tests/other_uses/test_other_uses_view.py +++ b/backend/lcfs/tests/other_uses/test_other_uses_view.py @@ -6,7 +6,10 @@ from lcfs.db.base import UserTypeEnum, ActionTypeEnum from lcfs.db.models.user.Role import RoleEnum from lcfs.web.api.base import ComplianceReportRequestSchema -from lcfs.web.api.other_uses.schema import PaginatedOtherUsesRequestSchema, OtherUsesSchema +from lcfs.web.api.other_uses.schema import ( + PaginatedOtherUsesRequestSchema, + OtherUsesSchema, +) from lcfs.web.api.other_uses.services import OtherUsesServices from lcfs.web.api.other_uses.validation import OtherUsesValidation from lcfs.tests.other_uses.conftest import create_mock_schema, create_mock_entity @@ -47,7 +50,7 @@ async def test_get_table_options( lambda: mock_other_uses_service ) - response = await client.get(url) + response = await client.get(url + "?compliancePeriod=2024") assert response.status_code == 200 data = response.json() diff --git a/backend/lcfs/utils/constants.py b/backend/lcfs/utils/constants.py index 793fe2a4e..494560016 100644 --- a/backend/lcfs/utils/constants.py +++ b/backend/lcfs/utils/constants.py @@ -55,6 +55,7 @@ class LCFS_Constants: ] TRANSACTIONS_EXPORT_SHEETNAME = "Transactions" TRANSACTIONS_EXPORT_FILENAME = "BC-LCFS-transactions" + LEGISLATION_TRANSITION_YEAR = "2024" # First year that the new LCFS Legislation takes effect class FILE_MEDIA_TYPE(Enum): @@ -93,5 +94,5 @@ class FILE_MEDIA_TYPE(Enum): "HDRD", "Other diesel", "Alternative jet fuel", - "Other" + "Other", ] diff --git a/backend/lcfs/web/api/allocation_agreement/repo.py b/backend/lcfs/web/api/allocation_agreement/repo.py index 374f7e6fa..a69928e97 100644 --- a/backend/lcfs/web/api/allocation_agreement/repo.py +++ b/backend/lcfs/web/api/allocation_agreement/repo.py @@ -16,6 +16,7 @@ from lcfs.db.models.fuel.FuelCode import FuelCode from lcfs.db.models.fuel.FuelType import QuantityUnitsEnum +from lcfs.utils.constants import LCFS_Constants from lcfs.web.api.fuel_code.repo import FuelCodeRepository from lcfs.web.api.allocation_agreement.schema import AllocationAgreementSchema from lcfs.web.api.base import PaginationRequestSchema @@ -34,16 +35,27 @@ def __init__( self.fuel_code_repo = fuel_repo @repo_handler - async def get_table_options(self) -> dict: + async def get_table_options(self, compliance_period: str) -> dict: """Get all table options""" + include_legacy = compliance_period < LCFS_Constants.LEGISLATION_TRANSITION_YEAR + fuel_categories = await self.fuel_code_repo.get_fuel_categories() - fuel_types = await self.fuel_code_repo.get_formatted_fuel_types() + fuel_types = await self.fuel_code_repo.get_formatted_fuel_types( + include_legacy=include_legacy + ) units_of_measure = [unit.value for unit in QuantityUnitsEnum] allocation_transaction_types = ( (await self.db.execute(select(AllocationTransactionType))).scalars().all() ) + + provisions_select = select(ProvisionOfTheAct) + if include_legacy: + provisions_select = provisions_select.where( + ProvisionOfTheAct.is_legacy == True + ) + provisions_of_the_act = ( - (await self.db.execute(select(ProvisionOfTheAct))).scalars().all() + (await self.db.execute(provisions_select)).scalars().all() ) fuel_codes = (await self.db.execute(select(FuelCode))).scalars().all() diff --git a/backend/lcfs/web/api/allocation_agreement/services.py b/backend/lcfs/web/api/allocation_agreement/services.py index 59ada9e41..12954ec63 100644 --- a/backend/lcfs/web/api/allocation_agreement/services.py +++ b/backend/lcfs/web/api/allocation_agreement/services.py @@ -87,11 +87,13 @@ async def convert_to_model( ) @service_handler - async def get_table_options(self) -> AllocationAgreementTableOptionsSchema: + async def get_table_options( + self, compliance_period: str + ) -> AllocationAgreementTableOptionsSchema: """ Gets the list of table options related to allocation agreements. """ - table_options = await self.repo.get_table_options() + table_options = await self.repo.get_table_options(compliance_period) fuel_types = [ { **fuel_type, diff --git a/backend/lcfs/web/api/allocation_agreement/views.py b/backend/lcfs/web/api/allocation_agreement/views.py index bb61a4505..3e30919de 100644 --- a/backend/lcfs/web/api/allocation_agreement/views.py +++ b/backend/lcfs/web/api/allocation_agreement/views.py @@ -48,10 +48,11 @@ @view_handler(["*"]) async def get_table_options( request: Request, + compliancePeriod: str, service: AllocationAgreementServices = Depends(), ): """Endpoint to retrieve table options related to allocation agreements""" - return await service.get_table_options() + return await service.get_table_options(compliancePeriod) @router.post( diff --git a/backend/lcfs/web/api/compliance_report/repo.py b/backend/lcfs/web/api/compliance_report/repo.py index ff7abcfcb..a0bf833a9 100644 --- a/backend/lcfs/web/api/compliance_report/repo.py +++ b/backend/lcfs/web/api/compliance_report/repo.py @@ -73,13 +73,14 @@ def apply_filters(self, pagination, conditions): filter_option = filter.type filter_type = filter.filter_type if filter.field == "status": - field = get_field_for_filter(ComplianceReportStatus, "status") - normalized_value = filter_value.lower() - enum_value_map = { - enum_val.value.lower(): enum_val - for enum_val in ComplianceReportStatusEnum - } - filter_value = enum_value_map.get(normalized_value) + field = get_field_for_filter(ComplianceReportStatus, filter.field) + if isinstance(filter_value, list): + filter_value = [ + ComplianceReportStatusEnum(value) for value in filter_value + ] + filter_type = "set" + else: + filter_value = ComplianceReportStatusEnum(filter_value) elif filter.field == "organization": field = get_field_for_filter(Organization, "name") elif filter.field == "type": @@ -359,12 +360,11 @@ async def get_reports_paginated( .group_by(ComplianceReport.compliance_report_group_uuid) ) - if not organization_id: - subquery = subquery.join( - ComplianceReportStatus, - ComplianceReport.current_status_id - == ComplianceReportStatus.compliance_report_status_id, - ) + subquery = subquery.join( + ComplianceReportStatus, + ComplianceReport.current_status_id + == ComplianceReportStatus.compliance_report_status_id, + ) subquery = subquery.subquery() # Join the main ComplianceReport table with the subquery to get the latest version per group diff --git a/backend/lcfs/web/api/compliance_report/update_service.py b/backend/lcfs/web/api/compliance_report/update_service.py index d58c51cff..47b73ff8a 100644 --- a/backend/lcfs/web/api/compliance_report/update_service.py +++ b/backend/lcfs/web/api/compliance_report/update_service.py @@ -150,7 +150,7 @@ async def handle_status_change( ComplianceReportStatusEnum.Recommended_by_analyst: self.handle_recommended_by_analyst_status, ComplianceReportStatusEnum.Recommended_by_manager: self.handle_recommended_by_manager_status, ComplianceReportStatusEnum.Assessed: self.handle_assessed_status, - ComplianceReportStatusEnum.ReAssessed: self.handle_reassessed_status, + ComplianceReportStatusEnum.Reassessed: self.handle_reassessed_status, } handler = status_handlers.get(new_status) diff --git a/backend/lcfs/web/api/fuel_code/repo.py b/backend/lcfs/web/api/fuel_code/repo.py index 174ee126b..d05318569 100644 --- a/backend/lcfs/web/api/fuel_code/repo.py +++ b/backend/lcfs/web/api/fuel_code/repo.py @@ -51,23 +51,23 @@ def __init__(self, db: AsyncSession = Depends(get_async_db_session)): self.db = db @repo_handler - async def get_fuel_types(self) -> List[FuelType]: - """Get all fuel type options""" - return ( - ( - await self.db.execute( - select(FuelType).options( - joinedload(FuelType.provision_1), - joinedload(FuelType.provision_2), - ) - ) - ) - .scalars() - .all() + async def get_fuel_types(self, include_legacy=False) -> List[FuelType]: + stmt = select(FuelType).options( + joinedload(FuelType.provision_1), + joinedload(FuelType.provision_2), ) + # Conditionally add the legacy filter + if not include_legacy: + stmt = stmt.where(FuelType.is_legacy == False) + + result = await self.db.execute(stmt) + return result.scalars().all() + @repo_handler - async def get_formatted_fuel_types(self) -> List[Dict[str, Any]]: + async def get_formatted_fuel_types( + self, include_legacy=False + ) -> List[Dict[str, Any]]: """Get all fuel type options with their associated fuel categories and fuel codes""" # Define the filtering conditions for fuel codes current_date = date.today() @@ -77,13 +77,21 @@ async def get_formatted_fuel_types(self) -> List[Dict[str, Any]]: FuelCode.expiration_date == None, FuelCode.expiration_date > current_date ) + conditions = [fuel_code_filters] + + # If we don't want to include legacy fuel types, filter them out + if not include_legacy: + conditions.append(FuelType.is_legacy == False) + + combined_conditions = and_(*conditions) + # Build the query with filtered fuel_codes query = ( select(FuelType) .outerjoin(FuelType.fuel_instances) .outerjoin(FuelInstance.fuel_category) .outerjoin(FuelType.fuel_codes) - .where(fuel_code_filters) + .where(combined_conditions) .options( contains_eager(FuelType.fuel_instances).contains_eager( FuelInstance.fuel_category diff --git a/backend/lcfs/web/api/fuel_export/repo.py b/backend/lcfs/web/api/fuel_export/repo.py index 510af73b6..6b9db2014 100644 --- a/backend/lcfs/web/api/fuel_export/repo.py +++ b/backend/lcfs/web/api/fuel_export/repo.py @@ -17,6 +17,7 @@ ) from lcfs.db.base import UserTypeEnum, ActionTypeEnum from lcfs.db.models.compliance.ComplianceReport import ComplianceReport +from lcfs.utils.constants import LCFS_Constants from lcfs.web.api.base import PaginationRequestSchema from sqlalchemy import and_, or_, select, func, delete from sqlalchemy.orm import joinedload, selectinload @@ -52,13 +53,13 @@ def __init__(self, db: AsyncSession = Depends(get_async_db_session)): ) @repo_handler - async def get_fuel_export_table_options(self, compliancePeriod: str): + async def get_fuel_export_table_options(self, compliance_period: str): """ Retrieve Fuel Type and other static data to use them while populating fuel supply form. """ subquery_compliance_period_id = ( select(CompliancePeriod.compliance_period_id) - .where(CompliancePeriod.description == compliancePeriod) + .where(CompliancePeriod.description == compliance_period) .scalar_subquery() ) @@ -164,6 +165,12 @@ async def get_fuel_export_table_options(self, compliancePeriod: str): ) ) + include_legacy = compliance_period < LCFS_Constants.LEGISLATION_TRANSITION_YEAR + if not include_legacy: + query = query.where( + and_(FuelType.is_legacy == False, ProvisionOfTheAct.is_legacy == False) + ) + results = (await self.db.execute(query)).all() return results diff --git a/backend/lcfs/web/api/fuel_supply/repo.py b/backend/lcfs/web/api/fuel_supply/repo.py index dca195539..b076915af 100644 --- a/backend/lcfs/web/api/fuel_supply/repo.py +++ b/backend/lcfs/web/api/fuel_supply/repo.py @@ -25,6 +25,7 @@ UnitOfMeasure, EndUseType, ) +from lcfs.utils.constants import LCFS_Constants from lcfs.web.api.base import PaginationRequestSchema from lcfs.web.api.fuel_supply.schema import FuelSupplyCreateUpdateSchema from lcfs.web.core.decorators import repo_handler @@ -184,6 +185,12 @@ async def get_fuel_supply_table_options(self, compliance_period: str): ) ) + include_legacy = compliance_period < LCFS_Constants.LEGISLATION_TRANSITION_YEAR + if not include_legacy: + query = query.where( + and_(FuelType.is_legacy == False, ProvisionOfTheAct.is_legacy == False) + ) + fuel_type_results = (await self.db.execute(query)).all() return { @@ -470,7 +477,7 @@ async def get_effective_fuel_supplies( FuelSupply.version == valid_fuel_supplies_subq.c.max_version, user_type_priority == valid_fuel_supplies_subq.c.max_role_priority, ), - isouter=False # Explicit inner join + isouter=False, # Explicit inner join ) .order_by(FuelSupply.create_date.asc()) ) diff --git a/backend/lcfs/web/api/fuel_supply/schema.py b/backend/lcfs/web/api/fuel_supply/schema.py index 3ca83878c..157023322 100644 --- a/backend/lcfs/web/api/fuel_supply/schema.py +++ b/backend/lcfs/web/api/fuel_supply/schema.py @@ -116,7 +116,7 @@ class FuelSupplyCreateUpdateSchema(BaseSchema): version: Optional[int] = None fuel_type_id: int fuel_category_id: int - end_use_id: Optional[int] = None + end_use_id: int provision_of_the_act_id: int quantity: int units: str diff --git a/backend/lcfs/web/api/fuel_supply/services.py b/backend/lcfs/web/api/fuel_supply/services.py index 67a2b7efa..3eb146e1e 100644 --- a/backend/lcfs/web/api/fuel_supply/services.py +++ b/backend/lcfs/web/api/fuel_supply/services.py @@ -67,13 +67,15 @@ def fuel_type_row_mapper(self, compliance_period, fuel_types, row): ) eer = EnergyEffectivenessRatioSchema( eer_id=row_data["eer_id"], - energy_effectiveness_ratio=round(row_data["energy_effectiveness_ratio"], 2), + energy_effectiveness_ratio=round( + row_data["energy_effectiveness_ratio"], 2), fuel_category=fuel_category, end_use_type=end_use_type, ) tci = TargetCarbonIntensitySchema( target_carbon_intensity_id=row_data["target_carbon_intensity_id"], - target_carbon_intensity=round(row_data["target_carbon_intensity"], 2), + target_carbon_intensity=round( + row_data["target_carbon_intensity"], 2), reduction_target_percentage=round( row_data["reduction_target_percentage"], 2 ), @@ -94,7 +96,8 @@ def fuel_type_row_mapper(self, compliance_period, fuel_types, row): ) # Find the existing fuel type if it exists existing_fuel_type = next( - (ft for ft in fuel_types if ft.fuel_type == row_data["fuel_type"]), None + (ft for ft in fuel_types if ft.fuel_type == + row_data["fuel_type"]), None ) if existing_fuel_type: @@ -135,8 +138,7 @@ def fuel_type_row_mapper(self, compliance_period, fuel_types, row): ( e for e in existing_fuel_type.eer_ratios - if e.end_use_type == row_data["end_use_type"] - and e.fuel_category == fuel_category + if e.eer_id == eer.eer_id ), None, ) @@ -258,7 +260,8 @@ async def get_fuel_supplies_paginated( size=pagination.size, total=total_count, total_pages=( - math.ceil(total_count / pagination.size) if total_count > 0 else 0 + math.ceil(total_count / + pagination.size) if total_count > 0 else 0 ), ), fuel_supplies=[ diff --git a/backend/lcfs/web/api/notification/schema.py b/backend/lcfs/web/api/notification/schema.py index 31f0f7270..d351e8840 100644 --- a/backend/lcfs/web/api/notification/schema.py +++ b/backend/lcfs/web/api/notification/schema.py @@ -106,7 +106,7 @@ class NotificationRequestSchema(BaseSchema): NotificationTypeEnum.IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT, NotificationTypeEnum.BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT, ], - ComplianceReportStatusEnum.ReAssessed: [ + ComplianceReportStatusEnum.Reassessed: [ NotificationTypeEnum.IDIR_ANALYST__COMPLIANCE_REPORT__DIRECTOR_DECISION, NotificationTypeEnum.IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT, NotificationTypeEnum.BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT, diff --git a/backend/lcfs/web/api/other_uses/repo.py b/backend/lcfs/web/api/other_uses/repo.py index 8e515b484..274354acd 100644 --- a/backend/lcfs/web/api/other_uses/repo.py +++ b/backend/lcfs/web/api/other_uses/repo.py @@ -16,6 +16,7 @@ from lcfs.db.models.fuel.FuelCode import FuelCode from lcfs.db.models.fuel.FuelType import FuelType, QuantityUnitsEnum from lcfs.db.models.fuel.FuelInstance import FuelInstance +from lcfs.utils.constants import LCFS_Constants from lcfs.web.api.fuel_code.repo import FuelCodeRepository from lcfs.web.api.other_uses.schema import OtherUsesSchema from lcfs.web.api.base import PaginationRequestSchema @@ -35,15 +36,23 @@ def __init__( self.fuel_code_repo = fuel_repo @repo_handler - async def get_table_options(self) -> dict: + async def get_table_options(self, compliance_period: str) -> dict: """Get all table options""" + include_legacy = compliance_period < LCFS_Constants.LEGISLATION_TRANSITION_YEAR fuel_categories = await self.fuel_code_repo.get_fuel_categories() - fuel_types = await self.get_formatted_fuel_types() + fuel_types = await self.get_formatted_fuel_types(include_legacy=include_legacy) expected_uses = await self.fuel_code_repo.get_expected_use_types() units_of_measure = [unit.value for unit in QuantityUnitsEnum] + + provisions_select = select(ProvisionOfTheAct) + if include_legacy: + provisions_select = provisions_select.where( + ProvisionOfTheAct.is_legacy == True + ) provisions_of_the_act = ( - (await self.db.execute(select(ProvisionOfTheAct))).scalars().all() + (await self.db.execute(provisions_select)).scalars().all() ) + fuel_codes = (await self.db.execute(select(FuelCode))).scalars().all() return { @@ -305,28 +314,34 @@ async def get_other_use_version_by_user( return result.scalars().first() @repo_handler - async def get_formatted_fuel_types(self) -> List[Dict[str, Any]]: + async def get_formatted_fuel_types( + self, include_legacy=False + ) -> List[Dict[str, Any]]: """Get all fuel type options with their associated fuel categories and fuel codes for other uses""" - # Define the filtering conditions for fuel codes current_date = date.today() - fuel_code_filters = ( + base_conditions = [ or_( FuelCode.effective_date == None, FuelCode.effective_date <= current_date - ) - & or_( + ), + or_( FuelCode.expiration_date == None, FuelCode.expiration_date > current_date, - ) - & (FuelType.other_uses_fossil_derived == True) - ) + ), + FuelType.other_uses_fossil_derived == True, + ] + + # Conditionally add the is_legacy filter + if not include_legacy: + base_conditions.append(FuelType.is_legacy == False) + + combined_conditions = and_(*base_conditions) - # Build the query with filtered fuel_codes query = ( select(FuelType) .outerjoin(FuelType.fuel_instances) .outerjoin(FuelInstance.fuel_category) .outerjoin(FuelType.fuel_codes) - .where(fuel_code_filters) + .where(combined_conditions) .options( contains_eager(FuelType.fuel_instances).contains_eager( FuelInstance.fuel_category diff --git a/backend/lcfs/web/api/other_uses/services.py b/backend/lcfs/web/api/other_uses/services.py index db5b22f81..ac4bd29f7 100644 --- a/backend/lcfs/web/api/other_uses/services.py +++ b/backend/lcfs/web/api/other_uses/services.py @@ -111,11 +111,13 @@ def model_to_schema(self, model: OtherUses): return updated_schema @service_handler - async def get_table_options(self) -> OtherUsesTableOptionsSchema: + async def get_table_options( + self, compliance_period: str + ) -> OtherUsesTableOptionsSchema: """ Gets the list of table options related to other uses. """ - table_options = await self.repo.get_table_options() + table_options = await self.repo.get_table_options(compliance_period) return OtherUsesTableOptionsSchema( fuel_categories=[ OtherUsesFuelCategorySchema.model_validate(category) @@ -147,8 +149,7 @@ async def get_other_uses(self, compliance_report_id: int) -> OtherUsesListSchema """ other_uses = await self.repo.get_other_uses(compliance_report_id) return OtherUsesAllSchema( - other_uses=[OtherUsesSchema.model_validate( - ou) for ou in other_uses] + other_uses=[OtherUsesSchema.model_validate(ou) for ou in other_uses] ) @service_handler @@ -201,10 +202,8 @@ async def update_other_use( ) if other_use.fuel_category.category != other_use_data.fuel_category: - other_use.fuel_category = ( - await self.fuel_repo.get_fuel_category_by( - category=other_use_data.fuel_category - ) + other_use.fuel_category = await self.fuel_repo.get_fuel_category_by( + category=other_use_data.fuel_category ) if other_use.expected_use.name != other_use_data.expected_use: @@ -291,8 +290,7 @@ async def delete_other_use( # Copy fields from the latest version for the deletion record for field in existing_fuel_supply.__table__.columns.keys(): if field not in OTHER_USE_EXCLUDE_FIELDS: - setattr(deleted_entity, field, getattr( - existing_fuel_supply, field)) + setattr(deleted_entity, field, getattr(existing_fuel_supply, field)) await self.repo.create_other_use(deleted_entity) return DeleteOtherUsesResponseSchema(success=True, message="Marked as deleted.") diff --git a/backend/lcfs/web/api/other_uses/views.py b/backend/lcfs/web/api/other_uses/views.py index 78704e62e..915f691e6 100644 --- a/backend/lcfs/web/api/other_uses/views.py +++ b/backend/lcfs/web/api/other_uses/views.py @@ -42,10 +42,11 @@ # @cache(expire=60 * 60 * 24) # cache for 24 hours async def get_table_options( request: Request, + compliancePeriod: str, service: OtherUsesServices = Depends(), ): """Endpoint to retrieve table options related to other uses""" - return await service.get_table_options() + return await service.get_table_options(compliancePeriod) @router.post( diff --git a/backend/lcfs/web/api/transaction/services.py b/backend/lcfs/web/api/transaction/services.py index 1abdfc2f0..0f2434851 100644 --- a/backend/lcfs/web/api/transaction/services.py +++ b/backend/lcfs/web/api/transaction/services.py @@ -84,9 +84,23 @@ def apply_transaction_filters(self, pagination, conditions): else: # Handle other filters field = get_field_for_filter(TransactionView, filter.field) + filter_value = filter.filter + # check if the date string is selected for filter + if filter.filter is None: + filter_value = [ + datetime.strptime(filter.date_from, "%Y-%m-%d %H:%M:%S").strftime( + "%Y-%m-%d" + ) + ] + if filter.date_to: + filter_value.append( + datetime.strptime(filter.date_to, "%Y-%m-%d %H:%M:%S").strftime( + "%Y-%m-%d" + ) + ) conditions.append( apply_filter_conditions( - field, filter.filter, filter.type, filter.filter_type + field, filter_value, filter.type, filter.filter_type ) ) diff --git a/backend/lcfs/web/api/user/repo.py b/backend/lcfs/web/api/user/repo.py index f55e0eab4..4a8f99513 100644 --- a/backend/lcfs/web/api/user/repo.py +++ b/backend/lcfs/web/api/user/repo.py @@ -607,14 +607,17 @@ def _apply_login_history_filters(self, query, pagination): filter_value = filter.filter filter_option = filter.type filter_type = filter.filter_type - if filter.field is not None: + if filter.field == "is_login_successful": + filter_option = "true" if filter_value == "Success" else "false" + field = get_field_for_filter(UserLoginHistory, "is_login_successful") + elif filter.field is not None: field = get_field_for_filter(UserLoginHistory, filter.field) - if field is not None: - condition = apply_filter_conditions( - field, filter_value, filter_option, filter_type - ) - if condition is not None: - conditions.append(condition) + if field is not None: + condition = apply_filter_conditions( + field, filter_value, filter_option, filter_type + ) + if condition is not None: + conditions.append(condition) query = query.where(and_(*conditions)) # Apply ordering diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 916e91dab..b47a16e62 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -34,6 +34,8 @@ import { AddEditAllocationAgreements } from './views/AllocationAgreements/AddEdi import { logout } from '@/utils/keycloak.js' import { CompareReports } from '@/views/CompareReports/CompareReports' import { ComplianceReportViewSelector } from '@/views/ComplianceReports/ComplianceReportViewSelector.jsx' +import { useCurrentUser } from './hooks/useCurrentUser' +import Loading from './components/Loading' const router = createBrowserRouter([ { @@ -395,6 +397,14 @@ const router = createBrowserRouter([ } ]) -const App = () => +const App = () => { + const { isLoading } = useCurrentUser() + + if (isLoading) { + return + } + + return +} export default App diff --git a/frontend/src/assets/locales/en/allocationAgreement.json b/frontend/src/assets/locales/en/allocationAgreement.json index f120b7ebe..554b3fbe6 100644 --- a/frontend/src/assets/locales/en/allocationAgreement.json +++ b/frontend/src/assets/locales/en/allocationAgreement.json @@ -5,7 +5,7 @@ "allocationAgreementSubtitle": "Enter allocation agreement details below", "fuelCodeFieldRequiredError": "Error updating row: Fuel code field required", "allocationAgreementColLabels": { - "transaction": "Responsibility", + "allocationTransactionType": "Responsibility", "transactionPartner": "Legal name of transaction partner", "postalAddress": "Address for service", "transactionPartnerEmail": "Email", diff --git a/frontend/src/assets/locales/en/fuelSupply.json b/frontend/src/assets/locales/en/fuelSupply.json index 92ed0968f..15ba14b59 100644 --- a/frontend/src/assets/locales/en/fuelSupply.json +++ b/frontend/src/assets/locales/en/fuelSupply.json @@ -16,7 +16,7 @@ "fuelType": "Fuel type", "fuelTypeOther": "Fuel type other", "fuelCategoryId": "Fuel category", - "endUse": "End use", + "endUseId": "End use", "provisionOfTheActId": "Determining carbon intensity", "fuelCode": "Fuel code", "quantity": "Quantity supplied", @@ -33,4 +33,4 @@ "validateMsg": { "isRequired": "{{field}} is required" } -} +} \ No newline at end of file diff --git a/frontend/src/components/BCDataGrid/BCDataGridServer.jsx b/frontend/src/components/BCDataGrid/BCDataGridServer.jsx index 667719390..fea836178 100644 --- a/frontend/src/components/BCDataGrid/BCDataGridServer.jsx +++ b/frontend/src/components/BCDataGrid/BCDataGridServer.jsx @@ -16,7 +16,7 @@ import { useApiService } from '@/services/useApiService' import BCAlert from '@/components/BCAlert' import BCBox from '@/components/BCBox' import DataGridLoading from '@/components/DataGridLoading' -import { BCPagination } from './components' +import { AccessibleHeader, BCPagination } from './components' import { useTranslation } from 'react-i18next' // Import useTranslation // Register the required AG Grid modules for row model and CSV export functionality ModuleRegistry.registerModules([ClientSideRowModelModule, CsvExportModule]) @@ -122,7 +122,7 @@ const BCDataGridServer = ({ [apiService, apiEndpoint, page, size, sortModel] ) - // Hanlde page change + // Handle page change const handleChangePage = useCallback((event, newPage) => { setLoading(true) setPage(newPage + 1) @@ -189,7 +189,7 @@ const BCDataGridServer = ({ let localFilteredData = [...rowData] // Handle the 'type' filter locally - const typeFilter = filterModel['type'] + const typeFilter = filterModel.type if (typeFilter) { const filterText = typeFilter.filter?.toLowerCase() || '' @@ -199,7 +199,7 @@ const BCDataGridServer = ({ }) // Remove 'type' from the filter model to prevent backend filtering - delete filterModel['type'] + delete filterModel.type } // Handle other filters (backend filters) @@ -269,10 +269,11 @@ const BCDataGridServer = ({ suppressDragLeaveHidesColumns: true, suppressMovableColumns: true, suppressColumnMoveAnimation: false, - rowSelection: 'multiple', animateRows: true, suppressPaginationPanel: true, suppressScrollOnNewData: true, + suppressColumnVirtualisation: true, + enableBrowserTooltips: true, suppressCsvExport: false, // enableCellTextSelection: true, // enables text selection on the grid // ensureDomOrder: true, @@ -288,6 +289,8 @@ const BCDataGridServer = ({ // Memorized default column definition parameters const defaultColDefParams = useMemo(() => ({ + headerComponent: AccessibleHeader, + suppressHeaderFilterButton: true, resizable: true, sortable: true, filter: true, diff --git a/frontend/src/components/BCDataGrid/BCGridBase.jsx b/frontend/src/components/BCDataGrid/BCGridBase.jsx index 4a30c198e..8ec81244d 100644 --- a/frontend/src/components/BCDataGrid/BCGridBase.jsx +++ b/frontend/src/components/BCDataGrid/BCGridBase.jsx @@ -33,13 +33,14 @@ export const BCGridBase = forwardRef(({ autoSizeStrategy, ...props }, ref) => { suppressDragLeaveHidesColumns suppressMovableColumns suppressColumnMoveAnimation={false} - reactiveCustomComponents suppressCsvExport={false} + suppressColumnVirtualisation={true} + enableBrowserTooltips={true} suppressPaginationPanel suppressScrollOnNewData getRowStyle={getRowStyle} rowHeight={45} - headerHeight={45} + headerHeight={40} {...props} /> diff --git a/frontend/src/components/BCDataGrid/BCGridViewer.jsx b/frontend/src/components/BCDataGrid/BCGridViewer.jsx index 8a5638431..347270dcf 100644 --- a/frontend/src/components/BCDataGrid/BCGridViewer.jsx +++ b/frontend/src/components/BCDataGrid/BCGridViewer.jsx @@ -1,7 +1,7 @@ import BCAlert, { FloatingAlert } from '@/components/BCAlert' import BCBox from '@/components/BCBox' import { BCGridBase } from '@/components/BCDataGrid/BCGridBase' -import { BCPagination } from '@/components/BCDataGrid/components' +import { AccessibleHeader, BCPagination } from '@/components/BCDataGrid/components' import '@ag-grid-community/styles/ag-grid.css' import '@ag-grid-community/styles/ag-theme-material.css' import { useCallback, useMemo, useRef, useState } from 'react' @@ -161,6 +161,8 @@ export const BCGridViewer = ({ const defaultColDefParams = useMemo( () => ({ + headerComponent: AccessibleHeader, + suppressHeaderFilterButton: true, resizable: true, sortable: true, filter: true, diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx index 6a924492b..d9c544229 100644 --- a/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx +++ b/frontend/src/components/BCDataGrid/components/Filters/BCDateFloatingFilter.jsx @@ -11,6 +11,8 @@ export const BCDateFloatingFilter = ({ model, onModelChange, disabled = false, + minDate = '2013-01-01', + maxDate = '2040-01-01', initialFilterType = 'equals', label = 'Select Date' }) => { @@ -52,7 +54,7 @@ export const BCDateFloatingFilter = ({ return } - if (model.filter) { + if (model?.dateFrom) { const date = new Date(model.dateFrom) setSelectedDate(isValid(date) ? date : null) } @@ -67,7 +69,6 @@ export const BCDateFloatingFilter = ({ aria-labelledby="date-picker-label" sx={{ border: 'none', - '& .MuiOutlinedInput-root': { p: 0 }, '& .MuiOutlinedInput-notchedOutline': { border: 'none' }, '& .Mui-focused': { border: '1px solid #495057', @@ -79,8 +80,13 @@ export const BCDateFloatingFilter = ({ id="date-picker" aria-label="Date Picker" aria-describedby="date-picker-description" - sx={{ border: 'none', borderBottom: '2px solid #495057' }} + sx={{ + border: 'none', + borderBottom: '4px solid #495057' + }} value={selectedDate} + minDate={new Date(minDate)} + maxDate={new Date(maxDate)} onChange={handleChange} open={open} onOpen={handleOpen} @@ -95,6 +101,7 @@ export const BCDateFloatingFilter = ({ startAdornment: ( setOpen(true)} @@ -107,6 +114,7 @@ export const BCDateFloatingFilter = ({ endAdornment: selectedDate && ( event.stopPropagation()} diff --git a/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx b/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx index ccb30136d..11052ddb7 100644 --- a/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx +++ b/frontend/src/components/BCDataGrid/components/Filters/BCSelectFloatingFilter.jsx @@ -80,7 +80,7 @@ export const BCSelectFloatingFilter = ({ } else { setSelectedValues([model?.filter]) } - }, [model, initialSelectedValues]) + }, [model]) return (
event.stopPropagation()} aria-label="Clear selection" + tabIndex={-1} > diff --git a/frontend/src/components/BCDataGrid/components/Renderers/AccessibleHeader.jsx b/frontend/src/components/BCDataGrid/components/Renderers/AccessibleHeader.jsx new file mode 100644 index 000000000..af0cdaf56 --- /dev/null +++ b/frontend/src/components/BCDataGrid/components/Renderers/AccessibleHeader.jsx @@ -0,0 +1,7 @@ +export const AccessibleHeader = (props) => { + return ( +
+ {props.column.colDef.headerName} +
+ ) +} diff --git a/frontend/src/components/BCDataGrid/components/Renderers/RequiredHeader.jsx b/frontend/src/components/BCDataGrid/components/Renderers/RequiredHeader.jsx index 1e8ddfbfb..8a6b9174f 100644 --- a/frontend/src/components/BCDataGrid/components/Renderers/RequiredHeader.jsx +++ b/frontend/src/components/BCDataGrid/components/Renderers/RequiredHeader.jsx @@ -1,8 +1,8 @@ export const RequiredHeader = (props) => { return ( -
+
* - {props.column.colDef.headerName} + {props.column.colDef.headerName}
) } diff --git a/frontend/src/components/BCDataGrid/components/index.js b/frontend/src/components/BCDataGrid/components/index.js index 16e855ef3..56eb9135a 100644 --- a/frontend/src/components/BCDataGrid/components/index.js +++ b/frontend/src/components/BCDataGrid/components/index.js @@ -6,6 +6,7 @@ export { DateRangeCellEditor } from './Editors/DateRangeCellEditor' export { ActionsRenderer } from './Renderers/ActionsRenderer' export { ActionsRenderer2 } from './Renderers/ActionsRenderer2' export { RequiredHeader } from './Renderers/RequiredHeader' +export { AccessibleHeader } from './Renderers/AccessibleHeader' export { ValidationRenderer } from './Renderers/ValidationRenderer' export { ValidationRenderer2 } from './Renderers/ValidationRenderer2' export { BCColumnSetFilter } from './Filters/BCColumnSetFilter' diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx index 0af3801f8..c62993d5b 100644 --- a/frontend/src/components/Footer.jsx +++ b/frontend/src/components/Footer.jsx @@ -19,25 +19,25 @@ const Footer = (props) => { label: 'About this site' }, { - href: 'http://gov.bc.ca/disclaimer/', + href: 'https://gov.bc.ca/disclaimer/', name: 'Disclaimer', id: 'footer-disclaimer', label: 'BC gov disclaimer information' }, { - href: 'http://gov.bc.ca/privacy/', + href: 'https://gov.bc.ca/privacy/', name: 'Privacy', id: 'footer-privacy', label: 'BC gov privacy information' }, { - href: 'http://gov.bc.ca/webaccessibility/', + href: 'https://gov.bc.ca/webaccessibility/', name: 'Accessibility', id: 'footer-accessibility', label: 'BC gov accessibility information' }, { - href: 'http://gov.bc.ca/copyright', + href: 'https://gov.bc.ca/copyright', name: 'Copyright', id: 'footer-copyright', label: 'BC gov copyright information' diff --git a/frontend/src/constants/routes/apiRoutes.js b/frontend/src/constants/routes/apiRoutes.js index d79d25464..b8f7a1af5 100644 --- a/frontend/src/constants/routes/apiRoutes.js +++ b/frontend/src/constants/routes/apiRoutes.js @@ -34,7 +34,7 @@ export const apiRoutes = { saveOtherUses: '/other-uses/save', getOtherUses: '/other-uses/list', getAllOtherUses: '/other-uses/list-all', - otherUsesOptions: '/other-uses/table-options', + otherUsesOptions: '/other-uses/table-options?', getComplianceReport: '/reports/:reportID', updateComplianceReport: '/reports/:reportID', getComplianceReportSummary: '/reports/:reportID/summary', diff --git a/frontend/src/constants/statuses.js b/frontend/src/constants/statuses.js index 91c7d3662..36844092a 100644 --- a/frontend/src/constants/statuses.js +++ b/frontend/src/constants/statuses.js @@ -66,3 +66,10 @@ export const FUEL_CODE_STATUSES = { export function getAllFuelCodeStatuses() { return Object.values(FUEL_CODE_STATUSES) } + +export const TRANSACTION_TYPES = { + TRANSFER: 'Transfer', + INITIATIVE_AGREEMENT: 'InitiativeAgreement', + COMPLIANCE_REPORT: 'ComplianceReport', + ADMINISTRATIVE_ADJUSTMENT: 'AdminAdjustment' +} \ No newline at end of file diff --git a/frontend/src/hooks/useFuelSupply.js b/frontend/src/hooks/useFuelSupply.js index 33c059847..86606e630 100644 --- a/frontend/src/hooks/useFuelSupply.js +++ b/frontend/src/hooks/useFuelSupply.js @@ -8,7 +8,7 @@ export const useFuelSupplyOptions = (params, options) => { const path = apiRoutes.fuelSupplyOptions + 'compliancePeriod=' + params.compliancePeriod return useQuery({ - queryKey: ['fuel-supply-options'], + queryKey: ['fuel-supply-options', params.compliancePeriod], queryFn: async () => (await client.get(path)).data, ...options }) diff --git a/frontend/src/hooks/useOtherUses.js b/frontend/src/hooks/useOtherUses.js index 49ad999ee..3ff5481ef 100644 --- a/frontend/src/hooks/useOtherUses.js +++ b/frontend/src/hooks/useOtherUses.js @@ -4,9 +4,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' export const useOtherUsesOptions = (params, options) => { const client = useApiService() - const path = apiRoutes.otherUsesOptions + const path = + apiRoutes.otherUsesOptions + 'compliancePeriod=' + params.compliancePeriod return useQuery({ - queryKey: ['other-uses-options'], + queryKey: ['other-uses-options', params.compliancePeriod], queryFn: async () => (await client.get(path)).data, ...options }) diff --git a/frontend/src/utils/grid/cellRenderers.jsx b/frontend/src/utils/grid/cellRenderers.jsx index 642ba9fed..37917345a 100644 --- a/frontend/src/utils/grid/cellRenderers.jsx +++ b/frontend/src/utils/grid/cellRenderers.jsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import BCBadge from '@/components/BCBadge' import BCBox from '@/components/BCBox' import { roles } from '@/constants/roles' @@ -7,7 +6,7 @@ import { getAllOrganizationStatuses } from '@/constants/statuses' import { Link, useLocation } from 'react-router-dom' -import { useState, useRef, useEffect, useCallback } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import colors from '@/themes/base/colors' export const TextRenderer = (props) => { @@ -20,80 +19,84 @@ export const TextRenderer = (props) => { export const LinkRenderer = (props) => { const location = useLocation() + + const baseUrl = props.isAbsolute ? '' : `${location.pathname}/` + const targetUrl = + baseUrl + + ((props.url && props.url({ data: props.data })) || props?.node?.id) return ( - + {props.valueFormatted || props.value} ) } +const BaseStatusRenderer = ({ + isView = false, + value = false, + successText = 'Active', + failureText = 'Inactive', + successColor = 'success', + failureColor = 'smoky' +}) => { + const badgeStyles = { + ...(!isView ? { display: 'flex', justifyContent: 'center' } : {}), + '& .MuiBadge-badge': { + minWidth: '120px', + fontWeight: 'regular', + textTransform: 'capitalize', + fontSize: '0.875rem', + padding: '0.4em 0.6em' + } + } -export const StatusRenderer = (props) => { return ( ) -} -export const LoginStatusRenderer = (props) => { - return ( - - - - ) + if (props.url) { + const baseUrl = props.isAbsolute ? '' : `${location.pathname}/` + const targetUrl = + baseUrl + + ((props.url && props.url({ data: props.data })) || props?.node?.id) + + return ( + + {component} + + ) + } else { + return component + } } +export const StatusRenderer = (props) => ( + +) + +export const LoginStatusRenderer = (props) => ( + +) + export const OrgStatusRenderer = (props) => { const location = useLocation() const statusArr = getAllOrganizationStatuses() @@ -170,38 +173,6 @@ export const FuelCodeStatusRenderer = (props) => { ) } -export const FuelCodeStatusTextRenderer = (props) => { - const statusArr = getAllFuelCodeStatuses() - const statusColorArr = ['info', 'success', 'error'] - const statusIndex = statusArr.indexOf(props.data.fuelCodeStatus.status) - return ( - - - - - - ) -} export const TransactionStatusRenderer = (props) => { const statusArr = [ @@ -229,7 +200,7 @@ export const TransactionStatusRenderer = (props) => { 'error' ] const statusIndex = statusArr.indexOf(props.data.status) - return ( + const component = ( { /> ) + if (props.url) { + const baseUrl = props.isAbsolute ? '' : `${location.pathname}/` + const targetUrl = + baseUrl + + ((props.url && props.url({ data: props.data })) || props?.node?.id) + + return ( + + {component} + + ) + } else { + return component + } } export const ReportsStatusRenderer = (props) => { const statusArr = [ @@ -437,16 +422,16 @@ const GenericChipRenderer = ({ {renderOverflowChip(hiddenChipsCount)}
) - + const baseUrl = props.isAbsolute ? '' : `${location.pathname}/` + const targetUrl = + baseUrl + + ((props.url && props.url({ data: props.data })) || props?.node?.id) return disableLink ? ( chipContent ) : ( - + {chipContent} - + ) } diff --git a/frontend/src/views/Admin/AdminMenu/components/AuditLog.jsx b/frontend/src/views/Admin/AdminMenu/components/AuditLog.jsx index a6f994c90..d6dad401c 100644 --- a/frontend/src/views/Admin/AdminMenu/components/AuditLog.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/AuditLog.jsx @@ -1,16 +1,15 @@ -import { useRef, useCallback } from 'react' +import { useCallback, useMemo, useRef } from 'react' import BCBox from '@/components/BCBox' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' import BCTypography from '@/components/BCTypography' import { useTranslation } from 'react-i18next' import { auditLogColDefs, defaultAuditLogSortModel } from './_schema' -import { apiRoutes, ROUTES } from '@/constants/routes' -import { useNavigate } from 'react-router-dom' +import { apiRoutes } from '@/constants/routes' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const AuditLog = () => { const { t } = useTranslation(['common', 'admin']) const gridRef = useRef() - const navigate = useNavigate() const gridOptions = { overlayNoRowsTemplate: t('admin:auditLogsNotFound'), @@ -24,16 +23,14 @@ export const AuditLog = () => { const apiEndpoint = apiRoutes.getAuditLogs - const handleRowClicked = useCallback( - (params) => { - const { auditLogId } = params.data - const path = ROUTES.ADMIN_AUDIT_LOG_VIEW.replace( - ':auditLogId', - auditLogId - ) - navigate(path) - }, - [navigate] + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + url: (data) => data.data.auditLogId + } + }), + [] ) return ( @@ -54,7 +51,7 @@ export const AuditLog = () => { enableCopyButton={false} enableExportButton={true} exportName="AuditLog" - handleRowClicked={handleRowClicked} + defaultColDef={defaultColDef} /> ) diff --git a/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx b/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx index 532214e8e..ca8e57582 100644 --- a/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/UserActivity.jsx @@ -1,48 +1,47 @@ import BCBox from '@/components/BCBox' import BCTypography from '@/components/BCTypography' import { useTranslation } from 'react-i18next' -import { useCallback } from 'react' -import { useNavigate } from 'react-router-dom' +import { useCallback, useMemo } from 'react' import { userActivityColDefs } from '@/views/Admin/AdminMenu/components/_schema' import { ROUTES } from '@/constants/routes' import { BCGridViewer } from '@/components/BCDataGrid/BCGridViewer' import { useGetUserActivities } from '@/hooks/useUser' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const UserActivity = () => { const { t } = useTranslation(['common', 'admin']) - const navigate = useNavigate() const getRowId = useCallback((params) => { - return `${params.data.transactionType.toLowerCase()}-${ - params.data.transactionId - }` + return `${ + params.data.actionTaken + }-${params.data.transactionType}-${params.data.transactionId}` }, []) - const handleRowClicked = useCallback( - (params) => { - const { transactionType, transactionId } = params.data - - let route - switch (transactionType) { - case 'Transfer': - route = ROUTES.TRANSFERS_VIEW.replace(':transferId', transactionId) - break - case 'AdminAdjustment': - route = ROUTES.ADMIN_ADJUSTMENT_VIEW.replace( - ':transactionId', - transactionId - ) - break - case 'InitiativeAgreement': - route = ROUTES.INITIATIVE_AGREEMENT_VIEW.replace( - ':transactionId', - transactionId - ) + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + isAbsolute: true, + url: (data) => { + const { transactionType, transactionId } = data.data + switch (transactionType) { + case 'Transfer': + return ROUTES.TRANSFERS_VIEW.replace(':transferId', transactionId) + case 'AdminAdjustment': + return ROUTES.ADMIN_ADJUSTMENT_VIEW.replace( + ':transactionId', + transactionId + ) + case 'InitiativeAgreement': + return ROUTES.INITIATIVE_AGREEMENT_VIEW.replace( + ':transactionId', + transactionId + ) + } + } } - - navigate(route) - }, - [navigate] + }), + [] ) return ( @@ -64,8 +63,7 @@ export const UserActivity = () => { defaultMinWidth: 50, defaultMaxWidth: 600 }} - rowSelection={{ isRowSelectable: false }} - onRowClicked={handleRowClicked} + defaultColDef={defaultColDef} /> diff --git a/frontend/src/views/Admin/AdminMenu/components/Users.jsx b/frontend/src/views/Admin/AdminMenu/components/Users.jsx index 49921701b..65de1813f 100644 --- a/frontend/src/views/Admin/AdminMenu/components/Users.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/Users.jsx @@ -1,21 +1,16 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -// @mui component import BCTypography from '@/components/BCTypography' import BCButton from '@/components/BCButton' import BCBox from '@/components/BCBox' import BCAlert from '@/components/BCAlert' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' -// icons import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCirclePlus } from '@fortawesome/free-solid-svg-icons' -// hooks import { useLocation, useNavigate } from 'react-router-dom' -import { useCallback, useRef, useState, useEffect } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' - -import { ROUTES, apiRoutes } from '@/constants/routes' -import { usersColumnDefs, idirUserDefaultFilter } from './_schema' -import { calculateRowHeight } from '@/utils/formatters' +import { apiRoutes, ROUTES } from '@/constants/routes' +import { idirUserDefaultFilter, usersColumnDefs } from './_schema' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const Users = () => { const { t } = useTranslation(['common', 'admin']) @@ -46,9 +41,15 @@ export const Users = () => { return params.data.userProfileId.toString() }, []) - const handleRowClicked = useCallback((params) => { - navigate(`${ROUTES.ADMIN_USERS}/${params.data.userProfileId}`) - }) + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + url: (data) => data.data.userProfileId + } + }), + [] + ) const gridRef = useRef() useEffect(() => { @@ -99,9 +100,9 @@ export const Users = () => { defaultSortModel={defaultSortModel} defaultFilterModel={idirUserDefaultFilter} handleGridKey={handleGridKey} - handleRowClicked={handleRowClicked} enableResetButton={false} enableCopyButton={false} + defaultColDef={defaultColDef} /> diff --git a/frontend/src/views/Admin/AdminMenu/components/ViewUser.jsx b/frontend/src/views/Admin/AdminMenu/components/ViewUser.jsx index 6ad30a1b6..833dfc3ec 100644 --- a/frontend/src/views/Admin/AdminMenu/components/ViewUser.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/ViewUser.jsx @@ -1,19 +1,23 @@ import Loading from '@/components/Loading' import BCTypography from '@/components/BCTypography' -import { FloatingAlert, BCAlert2 } from '@/components/BCAlert' +import { BCAlert2, FloatingAlert } from '@/components/BCAlert' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' -import { useRef, useCallback, useEffect, useState } from 'react' -import { useNavigate, useParams } from 'react-router-dom' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useParams } from 'react-router-dom' import { useUser } from '@/hooks/useUser' import { useCurrentUser } from '@/hooks/useCurrentUser' import { useTranslation } from 'react-i18next' import { phoneNumberFormatter } from '@/utils/formatters' -import { RoleSpanRenderer, StatusRenderer } from '@/utils/grid/cellRenderers' import { - userActivityColDefs, - defaultSortModel + LinkRenderer, + RoleSpanRenderer, + StatusRenderer +} from '@/utils/grid/cellRenderers' +import { + defaultSortModel, + userActivityColDefs } from '@/views/Admin/AdminMenu/components/_schema' -import { ROUTES, apiRoutes } from '@/constants/routes' +import { apiRoutes, ROUTES } from '@/constants/routes' import { roles } from '@/constants/roles' import { useOrganizationUser } from '@/hooks/useOrganization' import { Role } from '@/components/Role' @@ -33,7 +37,6 @@ export const ViewUser = () => { const { userID, orgID } = useParams() const { data: currentUser, hasRoles } = useCurrentUser() - const navigate = useNavigate() const [editButtonRoute, setEditButtonRoute] = useState(null) const { data, isLoading, isLoadingError, isError, error } = hasRoles( @@ -73,35 +76,31 @@ export const ViewUser = () => { }` }, []) - const handleRowClicked = useCallback( - (params) => { - const { transactionType, transactionId } = params.data - let route - switch (transactionType) { - case 'Transfer': - route = ROUTES.TRANSFERS_VIEW.replace(':transferId', transactionId) - break - case 'AdminAdjustment': - route = ROUTES.ADMIN_ADJUSTMENT_VIEW.replace( - ':transactionId', - transactionId - ) - break - case 'InitiativeAgreement': - route = ROUTES.INITIATIVE_AGREEMENT_VIEW.replace( - ':transactionId', - transactionId - ) - break - default: - route = null - } - - if (route) { - navigate(route) + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + isAbsolute: true, + url: (data) => { + const { transactionType, transactionId } = data.data + switch (transactionType) { + case 'Transfer': + return ROUTES.TRANSFERS_VIEW.replace(':transferId', transactionId) + case 'AdminAdjustment': + return ROUTES.ADMIN_ADJUSTMENT_VIEW.replace( + ':transactionId', + transactionId + ) + case 'InitiativeAgreement': + return ROUTES.INITIATIVE_AGREEMENT_VIEW.replace( + ':transactionId', + transactionId + ) + } + } } - }, - [navigate] + }), + [] ) useEffect(() => { @@ -208,7 +207,7 @@ export const ViewUser = () => { gridOptions={gridOptions} defaultSortModel={defaultSortModel} enableCopyButton={false} - handleRowClicked={handleRowClicked} + defaultColDef={defaultColDef} /> diff --git a/frontend/src/views/Admin/__tests__/AdminTabPanel.test.jsx b/frontend/src/views/Admin/AdminMenu/components/__tests__/AdminTabPanel.test.jsx similarity index 92% rename from frontend/src/views/Admin/__tests__/AdminTabPanel.test.jsx rename to frontend/src/views/Admin/AdminMenu/components/__tests__/AdminTabPanel.test.jsx index 42affc7d2..bde91178e 100644 --- a/frontend/src/views/Admin/__tests__/AdminTabPanel.test.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/__tests__/AdminTabPanel.test.jsx @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest' import { render, screen } from '@testing-library/react' import { ThemeProvider } from '@mui/material/styles' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import theme from '@/themes' // Make sure this path is correct -import { AdminTabPanel } from '../AdminMenu/components/AdminTabPanel' +import theme from '@/themes/index.js' // Make sure this path is correct +import { AdminTabPanel } from '../AdminTabPanel.jsx' // Custom render function with all necessary providers const customRender = (ui, options = {}) => { diff --git a/frontend/src/views/Admin/AdminMenu/components/__tests__/AuditLog.test.jsx b/frontend/src/views/Admin/AdminMenu/components/__tests__/AuditLog.test.jsx index 8cef96958..f62a29394 100644 --- a/frontend/src/views/Admin/AdminMenu/components/__tests__/AuditLog.test.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/__tests__/AuditLog.test.jsx @@ -1,51 +1,61 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import { AuditLog } from '../AuditLog' -import { useNavigate } from 'react-router-dom' -import { vi } from 'vitest' +import React from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { AuditLog } from '@/views/Admin/AdminMenu/index.js' +import { wrapper } from '@/tests/utils/wrapper.jsx' -// Mock necessary modules +// Mock i18n vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key) => key }) })) -vi.mock('react-router-dom', () => ({ - ...vi.importActual('react-router-dom'), - useNavigate: vi.fn() -})) - -// Mock the BCBox component -vi.mock('@/components/BCBox', () => ({ - default: ({ children }) =>
{children}
-})) +// Mock react-router-dom +const navigateMock = vi.fn() +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom') + return { + ...actual, + useNavigate: () => navigateMock + } +}) -// Mock the BCDataGridServer component +// Mock BCDataGridServer so we can inspect props & simulate row clicks vi.mock('@/components/BCDataGrid/BCDataGridServer', () => ({ - default: ({ handleRowClicked }) => ( -
- -
- ) + default: ({ handleRowClicked, ...props }) => { + // We'll return some basic UI with a button to simulate a row-click. + return ( +
+ +
+ ) + } })) describe('AuditLog Component', () => { - const navigateMock = vi.fn() - beforeEach(() => { vi.clearAllMocks() - useNavigate.mockReturnValue(navigateMock) }) it('renders correctly', () => { - render() + render(, { wrapper }) expect(screen.getByText('admin:AuditLog')).toBeInTheDocument() + expect(screen.getByTestId('bc-datagrid-server')).toBeInTheDocument() + }) + + it('passes the correct props to BCDataGridServer', () => { + render(, { wrapper }) + expect(screen.getByText('Mock Row')).toBeInTheDocument() }) - it('navigates to the correct path when a row is clicked', () => { - render() - const mockRowButton = screen.getByText('Mock Row') - fireEvent.click(mockRowButton) - expect(navigateMock).toHaveBeenCalledWith('/admin/audit-log/123') + it('uses getRowId to return auditLogId', () => { + const params = { data: { auditLogId: 'TEST_ID' } } + const rowId = params.data.auditLogId // or call the function directly + expect(rowId).toBe('TEST_ID') }) }) diff --git a/frontend/src/views/Admin/AdminMenu/components/__tests__/UserActivity.test.jsx b/frontend/src/views/Admin/AdminMenu/components/__tests__/UserActivity.test.jsx index fa3b92cc5..e07f424bc 100644 --- a/frontend/src/views/Admin/AdminMenu/components/__tests__/UserActivity.test.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/__tests__/UserActivity.test.jsx @@ -1,83 +1,136 @@ import React from 'react' import { render, screen } from '@testing-library/react' -import { vi, describe, it, expect, beforeEach } from 'vitest' +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' import { UserActivity } from '../UserActivity' import { wrapper } from '@/tests/utils/wrapper' +import { BCGridViewer } from '@/components/BCDataGrid/BCGridViewer' -// Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key) => key }) })) -// Mock react-router-dom const mockUseNavigate = vi.fn() -vi.mock('react-router-dom', () => ({ - ...vi.importActual('react-router-dom'), - useNavigate: () => mockUseNavigate -})) - -// Mock constants -vi.mock('@/constants/routes', () => ({ - ROUTES: { - TRANSFERS_VIEW: '/transfers/:transferId', - ADMIN_ADJUSTMENT_VIEW: '/admin-adjustment/:transactionId', - INITIATIVE_AGREEMENT_VIEW: '/initiative-agreement/:transactionId' +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom') + return { + ...actual, + useNavigate: () => mockUseNavigate } -})) +}) -// Mock userActivityColDefs vi.mock('@/views/Admin/AdminMenu/components/_schema', () => ({ - userActivityColDefs: () => [ + userActivityColDefs: [ { headerName: 'Column 1', field: 'col1' }, { headerName: 'Column 2', field: 'col2' } ] })) -// Variable to hold the onRowClicked function -let onRowClickedMock - -// Mock BCGridViewer +// -- Mock BCGridViewer so we can inspect its props -- vi.mock('@/components/BCDataGrid/BCGridViewer', () => ({ - BCGridViewer: (props) => { - onRowClickedMock = props.onRowClicked - return
BCGridViewer
- } + BCGridViewer: vi.fn(() =>
BCGridViewer
) +})) + +vi.mock('@/hooks/useUser', () => ({ + useGetUserActivities: () => ({ + data: { activities: [] }, // or mock real data if needed + isLoading: false, + isError: false + }) })) describe('UserActivity', () => { beforeEach(() => { - vi.resetAllMocks() + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() }) - it('renders the component', () => { + it('renders the heading and the grid', () => { render(, { wrapper }) + + // 1. Heading check expect(screen.getByText('admin:UserActivity')).toBeInTheDocument() + + // 2. BCGridViewer check expect(screen.getByTestId('bc-grid-viewer')).toBeInTheDocument() }) - it('navigates to the correct route when transactionType is Transfer', () => { + it('passes the correct props to BCGridViewer', () => { render(, { wrapper }) - onRowClickedMock({ - data: { transactionType: 'Transfer', transactionId: '123' } - }) - expect(mockUseNavigate).toHaveBeenCalledWith('/transfers/123') + + // BCGridViewer has been mocked, so we can inspect its calls + expect(BCGridViewer).toHaveBeenCalledTimes(1) + const gridProps = BCGridViewer.mock.calls[0][0] + + // 1) gridKey + expect(gridProps.gridKey).toBe('all-user-activities-grid') + + // 2) columnDefs + expect(gridProps.columnDefs).toEqual([ + { headerName: 'Column 1', field: 'col1' }, + { headerName: 'Column 2', field: 'col2' } + ]) + + // 3) dataKey + expect(gridProps.dataKey).toBe('activities') + + // 4) getRowId + expect(gridProps.getRowId).toBeDefined() + // Optionally check the logic of getRowId + // This is a unit-style check; you can do something like: + const mockParams = { + data: { + transactionType: 'AdminAdjustment', + transactionId: '123', + actionTaken: 'CREATE' + } + } + expect(gridProps.getRowId(mockParams)).toBe('CREATE-AdminAdjustment-123') + + // 5) defaultColDef + expect(gridProps.defaultColDef).toBeDefined() + expect(typeof gridProps.defaultColDef.cellRendererParams.url).toBe( + 'function' + ) }) - it('navigates to the correct route when transactionType is AdminAdjustment', () => { + it('generates correct URLs for each transaction type', () => { render(, { wrapper }) - onRowClickedMock({ - data: { transactionType: 'AdminAdjustment', transactionId: '456' } + + // Extract the defaultColDef from BCGridViewer props + const gridProps = BCGridViewer.mock.calls[0][0] + const { url } = gridProps.defaultColDef.cellRendererParams + + // Test different transaction types + const mockData = (transactionType, transactionId) => ({ + data: { transactionType, transactionId } }) - expect(mockUseNavigate).toHaveBeenCalledWith('/admin-adjustment/456') + + // Transfer + expect(url(mockData('Transfer', 'ABC123'))).toBe('/transfers/ABC123') + + // AdminAdjustment + expect(url(mockData('AdminAdjustment', 'XYZ789'))).toBe( + '/admin-adjustment/XYZ789' + ) + + // InitiativeAgreement + expect(url(mockData('InitiativeAgreement', 'IA555'))).toBe( + '/initiative-agreement/IA555' + ) }) - it('navigates to the correct route when transactionType is InitiativeAgreement', () => { + // If you want to verify that no rows found message is shown if data is empty + it('shows the overlayNoRowsTemplate when there are no activities', () => { render(, { wrapper }) - onRowClickedMock({ - data: { transactionType: 'InitiativeAgreement', transactionId: '789' } - }) - expect(mockUseNavigate).toHaveBeenCalledWith('/initiative-agreement/789') + + // BCGridViewer props + const gridProps = BCGridViewer.mock.calls[0][0] + // Because data is mocked to [] + expect(gridProps.overlayNoRowsTemplate).toBe('admin:activitiesNotFound') }) }) diff --git a/frontend/src/views/Admin/AdminMenu/components/__tests__/Users.test.jsx b/frontend/src/views/Admin/AdminMenu/components/__tests__/Users.test.jsx new file mode 100644 index 000000000..6373303a5 --- /dev/null +++ b/frontend/src/views/Admin/AdminMenu/components/__tests__/Users.test.jsx @@ -0,0 +1,49 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { Users } from '../Users.jsx' +import { wrapper } from '@/tests/utils/wrapper.jsx' + +// Mock dependencies +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key + }) +})) + +// Mock BCDataGridServer component +vi.mock('@/components/BCDataGrid/BCDataGridServer', () => ({ + default: () =>
Mocked DataGrid
+})) + +describe('Users Component', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders without crashing', () => { + render(, { wrapper }) + expect(screen.getByText('admin:Users')).toBeInTheDocument() + }) + + it('displays the New User button', () => { + render(, { wrapper }) + const newUserButton = screen.getByText('admin:newUserBtn') + expect(newUserButton).toBeInTheDocument() + }) + + it('navigates to add user page when New User button is clicked', async () => { + render(, { wrapper }) + const newUserButton = screen.getByText('admin:newUserBtn') + fireEvent.click(newUserButton) + + // Check if the navigation occurred + await waitFor(() => { + expect(window.location.href).toContain('/admin/users/add') + }) + }) + + it('renders BCDataGridServer with correct props', () => { + render(, { wrapper }) + expect(screen.getByTestId('mocked-data-grid')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/views/Admin/__tests__/ViewUser.test.jsx b/frontend/src/views/Admin/AdminMenu/components/__tests__/ViewUser.test.jsx similarity index 93% rename from frontend/src/views/Admin/__tests__/ViewUser.test.jsx rename to frontend/src/views/Admin/AdminMenu/components/__tests__/ViewUser.test.jsx index 6968208fa..e5055cbb2 100644 --- a/frontend/src/views/Admin/__tests__/ViewUser.test.jsx +++ b/frontend/src/views/Admin/AdminMenu/components/__tests__/ViewUser.test.jsx @@ -2,12 +2,12 @@ import React from 'react' import { vi, describe, it, expect, beforeEach } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { useLocation } from 'react-router-dom' -import * as useUserHook from '@/hooks/useUser' -import * as useCurrentUserHook from '@/hooks/useCurrentUser' -import * as useOrganizationHook from '@/hooks/useOrganization' -import * as formatters from '@/utils/formatters' -import { wrapper } from '@/tests/utils/wrapper' -import { ViewUser } from '../AdminMenu/components/ViewUser' +import * as useUserHook from '@/hooks/useUser.js' +import * as useCurrentUserHook from '@/hooks/useCurrentUser.js' +import * as useOrganizationHook from '@/hooks/useOrganization.js' +import * as formatters from '@/utils/formatters.js' +import { wrapper } from '@/tests/utils/wrapper.jsx' +import { ViewUser } from '../ViewUser.jsx' // Mocks vi.mock('@react-keycloak/web', () => ({ @@ -153,7 +153,7 @@ describe('ViewUser Component', () => { wrapper({ children, initialEntries: ['/admin/users/1'] }) } ) - + const editButton = screen.getByRole('button', { name: /admin:editBtn/i }) fireEvent.click(editButton) diff --git a/frontend/src/views/Admin/AdminMenu/components/_schema.js b/frontend/src/views/Admin/AdminMenu/components/_schema.js index 1c8093fe5..235c31187 100644 --- a/frontend/src/views/Admin/AdminMenu/components/_schema.js +++ b/frontend/src/views/Admin/AdminMenu/components/_schema.js @@ -1,6 +1,6 @@ import { - phoneNumberFormatter, dateFormatter, + phoneNumberFormatter, timezoneFormatter } from '@/utils/formatters' import { @@ -14,6 +14,11 @@ import { BCSelectFloatingFilter, BCDateFloatingFilter } from '@/components/BCDataGrid/components/index' +import { + COMPLIANCE_REPORT_STATUSES, + TRANSACTION_TYPES, + TRANSFER_STATUSES +} from '@/constants/statuses' export const usersColumnDefs = (t) => [ { @@ -21,7 +26,6 @@ export const usersColumnDefs = (t) => [ field: 'firstName', minWidth: 250, headerName: t('admin:userColLabels.userName'), - cellRenderer: LinkRenderer, valueGetter: (params) => params.data.firstName + ' ' + params.data.lastName }, { @@ -50,7 +54,6 @@ export const usersColumnDefs = (t) => [ }, floatingFilterComponent: BCSelectFloatingFilter, suppressFloatingFilterButton: true, - suppressHeaderFilterButton: true, floatingFilterComponentParams: { optionsQuery: useRoleList, params: 'government_roles_only=true', @@ -64,14 +67,12 @@ export const usersColumnDefs = (t) => [ colId: 'email', field: 'keycloakEmail', headerName: t('admin:userColLabels.email'), - cellRenderer: LinkRenderer, minWidth: 300 }, { colId: 'phone', field: 'phone', headerName: t('admin:userColLabels.phone'), - cellRenderer: LinkRenderer, valueFormatter: phoneNumberFormatter, filter: 'agTextColumnFilter', minWidth: 120 @@ -101,8 +102,7 @@ export const usersColumnDefs = (t) => [ labelKey: 'name' }, minWidth: 120, - suppressFloatingFilterButton: true, - suppressHeaderFilterButton: true + suppressFloatingFilterButton: true }, { colId: 'organizationId', @@ -128,12 +128,52 @@ export const userActivityColDefs = [ { colId: 'actionTaken', field: 'actionTaken', - headerName: 'Action Taken' + headerName: 'Action Taken', + floatingFilterComponent: BCSelectFloatingFilter, + floatingFilterComponentParams: { + valueKey: 'action', + labelKey: 'action', + optionsQuery: () => { + const allStatuses = [ + ...Object.values(TRANSFER_STATUSES).map((value) => ({ + action: value + })) + // ...Object.values(COMPLIANCE_REPORT_STATUSES).map((value) => ({ + // action: value + // })) + ] + + const deduplicatedStatuses = Array.from( + new Set(allStatuses.map((item) => item.action)) + ).map((action) => ({ action })) + + return { + data: deduplicatedStatuses, + isLoading: false + } + } + }, + suppressFloatingFilterButton: true }, { colId: 'transactionType', field: 'transactionType', - headerName: 'Transaction Type' + headerName: 'Transaction Type', + floatingFilterComponent: BCSelectFloatingFilter, + floatingFilterComponentParams: { + valueKey: 'value', + labelKey: 'label', + optionsQuery: () => ({ + data: [ + ...Object.values(TRANSACTION_TYPES).map((value) => ({ + label: value.replace(/([A-Z])/g, ' $1').trim(), + value + })) + ], + isLoading: false + }) + }, + suppressFloatingFilterButton: true }, { colId: 'transactionId', @@ -150,7 +190,8 @@ export const userActivityColDefs = [ field: 'createDate', headerName: 'Date', valueFormatter: dateFormatter, - filter: false + floatingFilterComponent: BCDateFloatingFilter, + suppressFloatingFilterButton: true } ] @@ -180,7 +221,26 @@ export const userLoginHistoryColDefs = (t) => [ { field: 'isLoginSuccessful', headerName: t('admin:userLoginHistoryColLabels.isLoginSuccessful'), - cellRenderer: LoginStatusRenderer + cellRenderer: LoginStatusRenderer, + valueGetter: (params) => params.data.isLoginSuccessful, + filterParams: { + textMatcher: (filter) => { + return true + } + }, + floatingFilterComponent: BCSelectFloatingFilter, + floatingFilterComponentParams: { + optionsQuery: () => ({ + data: [ + { id: 1, name: 'Success' }, + { id: 0, name: 'Failed' } + ], + isLoading: false + }), + valueKey: 'name', + labelKey: 'name' + }, + suppressFloatingFilterButton: true }, { field: 'loginErrorMessage', @@ -190,7 +250,9 @@ export const userLoginHistoryColDefs = (t) => [ field: 'createDate', headerName: t('admin:userLoginHistoryColLabels.createDate'), cellDataType: 'dateString', - valueFormatter: timezoneFormatter + valueFormatter: timezoneFormatter, + floatingFilterComponent: BCDateFloatingFilter, + suppressFloatingFilterButton: true } ] @@ -255,7 +317,9 @@ export const auditLogColDefs = (t) => [ filterOptions: ['equals', 'lessThan', 'greaterThan', 'inRange'], suppressAndOrCondition: true, buttons: ['clear'] - } + }, + floatingFilterComponent: BCDateFloatingFilter, + suppressFloatingFilterButton: true } ] diff --git a/frontend/src/views/Admin/__tests__/Users.test.jsx b/frontend/src/views/Admin/__tests__/Users.test.jsx deleted file mode 100644 index 166b59d24..000000000 --- a/frontend/src/views/Admin/__tests__/Users.test.jsx +++ /dev/null @@ -1,158 +0,0 @@ -import { vi, describe, it, expect, beforeEach } from 'vitest' -import { render, screen, fireEvent, waitFor } from '@testing-library/react' -import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { Users } from '../AdminMenu/components/Users' -import { ThemeProvider } from '@mui/material' -import theme from '@/themes' - -// Mock dependencies -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key) => key - }) -})) - -vi.mock('@/constants/routes', () => ({ - ROUTES: { - ADMIN_USERS_ADD: '/admin/users/add', - ADMIN_USERS: '/admin/users' - }, - apiRoutes: { - listUsers: '/api/users' - } -})) - -vi.mock('../AdminMenu/components/_schema', () => ({ - usersColumnDefs: vi.fn(() => []), - idirUserDefaultFilter: [] -})) - -vi.mock('@/utils/formatters', () => ({ - calculateRowHeight: vi.fn(() => 50) -})) - -// Mock BCDataGridServer component -vi.mock('@/components/BCDataGrid/BCDataGridServer', () => ({ - default: ({ handleRowClicked }) => ( -
handleRowClicked({ data: { userProfileId: '123' } })} - > - Mocked DataGrid -
- ) -})) - -// Helper component to access current location -const LocationDisplay = () => { - const location = useLocation() - return
{location.pathname}
-} - -const WrapperComponent = ({ children, initialEntries = ['/'] }) => { - const queryClient = new QueryClient() - return ( - - - - - - {children} - - - } - /> - - - - - ) -} - -describe('Users Component', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('renders without crashing', () => { - render( - - - - ) - expect(screen.getByText('admin:Users')).toBeInTheDocument() - }) - - it('displays the New User button', () => { - render( - - - - ) - const newUserButton = screen.getByText('admin:newUserBtn') - expect(newUserButton).toBeInTheDocument() - }) - - it('navigates to add user page when New User button is clicked', async () => { - render( - - - - ) - const newUserButton = screen.getByText('admin:newUserBtn') - fireEvent.click(newUserButton) - - // Check if the navigation occurred - await waitFor(() => { - expect(screen.getByTestId('location-display')).toHaveTextContent( - '/admin/users/add' - ) - }) - }) - - it('renders BCDataGridServer with correct props', () => { - render( - - - - ) - expect(screen.getByTestId('mocked-data-grid')).toBeInTheDocument() - }) - - it('displays alert message when location state has a message', () => { - const initialEntries = [ - { - pathname: '/admin/users', - state: { message: 'Test alert message', severity: 'success' } - } - ] - render( - - - - ) - expect(screen.getByText('Test alert message')).toBeInTheDocument() - }) - - it('handles row click correctly', async () => { - render( - - - - ) - - // Simulate a row click - const mockedDataGrid = screen.getByTestId('mocked-data-grid') - fireEvent.click(mockedDataGrid) - - // Check if the navigation occurred - await waitFor(() => { - const locationDisplay = screen.getByTestId('location-display') - expect(locationDisplay.textContent).toBe('/admin/users/123') - }) - }) -}) diff --git a/frontend/src/views/AllocationAgreements/AllocationAgreementSummary.jsx b/frontend/src/views/AllocationAgreements/AllocationAgreementSummary.jsx index 70a536d18..3ecf13546 100644 --- a/frontend/src/views/AllocationAgreements/AllocationAgreementSummary.jsx +++ b/frontend/src/views/AllocationAgreements/AllocationAgreementSummary.jsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { useLocation, useParams, useNavigate } from 'react-router-dom' import { v4 as uuid } from 'uuid' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses.js' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const AllocationAgreementSummary = ({ data, status }) => { const [alertMessage, setAlertMessage] = useState('') @@ -47,9 +48,14 @@ export const AllocationAgreementSummary = ({ data, status }) => { const defaultColDef = useMemo( () => ({ floatingFilter: false, - filter: false + filter: false, + cellRenderer: + status === COMPLIANCE_REPORT_STATUSES.DRAFT ? LinkRenderer : undefined, + cellRendererParams: { + url: () => 'allocation-agreements' + } }), - [] + [status] ) const columns = useMemo( @@ -137,17 +143,6 @@ export const AllocationAgreementSummary = ({ data, status }) => { setGridKey(`allocation-agreements-grid-${uuid()}`) } - const handleRowClicked = (params) => { - if (status === COMPLIANCE_REPORT_STATUSES.DRAFT) { - navigate( - ROUTES.REPORTS_ADD_ALLOCATION_AGREEMENTS.replace( - ':compliancePeriod', - compliancePeriod - ).replace(':complianceReportId', complianceReportId) - ) - } - } - return (
@@ -172,7 +167,6 @@ export const AllocationAgreementSummary = ({ data, status }) => { enableCopyButton={false} defaultColDef={defaultColDef} suppressPagination={data.allocationAgreements.length <= 10} - handleRowClicked={handleRowClicked} /> diff --git a/frontend/src/views/AllocationAgreements/_schema.jsx b/frontend/src/views/AllocationAgreements/_schema.jsx index 26ef36700..1d275cd06 100644 --- a/frontend/src/views/AllocationAgreements/_schema.jsx +++ b/frontend/src/views/AllocationAgreements/_schema.jsx @@ -55,7 +55,7 @@ export const allocationAgreementColDefs = (optionsData, errors, currentUser) => field: 'allocationTransactionType', headerComponent: RequiredHeader, headerName: i18n.t( - 'allocationAgreement:allocationAgreementColLabels.transaction' + 'allocationAgreement:allocationAgreementColLabels.allocationTransactionType' ), cellEditor: AutocompleteCellEditor, cellEditorParams: { diff --git a/frontend/src/views/ComplianceReports/ComplianceReports.jsx b/frontend/src/views/ComplianceReports/ComplianceReports.jsx index 504d42990..948742536 100644 --- a/frontend/src/views/ComplianceReports/ComplianceReports.jsx +++ b/frontend/src/views/ComplianceReports/ComplianceReports.jsx @@ -1,25 +1,20 @@ -// mui components import { Stack } from '@mui/material' import BCBox from '@/components/BCBox' import BCAlert from '@/components/BCAlert' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' -// react components import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' -// Services import { Role } from '@/components/Role' -// constants import { roles } from '@/constants/roles' import { apiRoutes, ROUTES } from '@/constants/routes' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' -// hooks import { useCurrentUser } from '@/hooks/useCurrentUser' import { useCreateComplianceReport } from '@/hooks/useComplianceReports' -// internal components import { defaultSortModel, reportsColDefs } from './components/_schema' import { NewComplianceReportButton } from './components/NewComplianceReportButton' import BCTypography from '@/components/BCTypography' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const ComplianceReports = () => { const { t } = useTranslation(['common', 'report']) @@ -44,19 +39,9 @@ export const ComplianceReports = () => { (params) => params.data.complianceReportId.toString(), [] ) - const handleRowClicked = useCallback( - ({ data }) => { - const mappedRoute = ROUTES.REPORTS_VIEW.replace( - ':compliancePeriod', - data.compliancePeriod.description - ).replace(':complianceReportId', data.complianceReportId) - navigate(mappedRoute) - }, - [navigate] - ) const handleGridKey = useCallback(() => { - setGridKey(`reports-grid`) + setGridKey('reports-grid') }, []) useEffect(() => { @@ -66,42 +51,51 @@ export const ComplianceReports = () => { } }, [location.state]) - const { - mutate: createComplianceReport, - isLoading: isCreating, - isError - } = useCreateComplianceReport(currentUser?.organization?.organizationId, { - onSuccess: (response, variables) => { - setAlertMessage( - t('report:actionMsgs.successText', { - status: 'created' - }) - ) - setIsButtonLoading(false) - setAlertSeverity('success') - navigate( - ROUTES.REPORTS_VIEW.replace( - ':compliancePeriod', - response.data.compliancePeriod.description - ).replace(':complianceReportId', response.data.complianceReportId), - { state: { data: response.data, newReport: true } } - ) - alertRef.current.triggerAlert() - }, - onError: (_error, _variables) => { - setIsButtonLoading(false) - const errorMsg = _error.response.data?.detail - setAlertMessage(errorMsg) - setAlertSeverity('error') - alertRef.current.triggerAlert() - } - }) + const { mutate: createComplianceReport, isLoading: isCreating } = + useCreateComplianceReport(currentUser?.organization?.organizationId, { + onSuccess: (response, variables) => { + setAlertMessage( + t('report:actionMsgs.successText', { + status: 'created' + }) + ) + setIsButtonLoading(false) + setAlertSeverity('success') + navigate( + ROUTES.REPORTS_VIEW.replace( + ':compliancePeriod', + response.data.compliancePeriod.description + ).replace(':complianceReportId', response.data.complianceReportId), + { state: { data: response.data, newReport: true } } + ) + alertRef.current.triggerAlert() + }, + onError: (_error, _variables) => { + setIsButtonLoading(false) + const errorMsg = _error.response.data?.detail + setAlertMessage(errorMsg) + setAlertSeverity('error') + alertRef.current.triggerAlert() + } + }) useEffect(() => { if (isCreating) { setIsButtonLoading(true) } }, [isCreating]) + + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + url: (data) => + `${data.data.compliancePeriod.description}/${data.data.complianceReportId}` + } + }), + [] + ) + return ( <>
@@ -159,8 +153,8 @@ export const ComplianceReports = () => { defaultFilterModel={location.state?.filters} gridOptions={gridOptions} handleGridKey={handleGridKey} - handleRowClicked={handleRowClicked} enableCopyButton={false} + defaultColDef={defaultColDef} /> diff --git a/frontend/src/views/ComplianceReports/components/_schema.jsx b/frontend/src/views/ComplianceReports/components/_schema.jsx index 6c52b88b6..e2e9ba6a4 100644 --- a/frontend/src/views/ComplianceReports/components/_schema.jsx +++ b/frontend/src/views/ComplianceReports/components/_schema.jsx @@ -75,7 +75,8 @@ export const reportsColDefs = (t, bceidRole) => [ }), valueKey: 'name', labelKey: 'name' - } + }, + suppressFloatingFilterButton: true }, { field: 'updateDate', @@ -91,7 +92,8 @@ export const reportsColDefs = (t, bceidRole) => [ buttons: ['clear'], maxValidYear: 2400 }, - floatingFilterComponent: BCDateFloatingFilter + floatingFilterComponent: BCDateFloatingFilter, + suppressFloatingFilterButton: true } ] diff --git a/frontend/src/views/FinalSupplyEquipments/FinalSupplyEquipmentSummary.jsx b/frontend/src/views/FinalSupplyEquipments/FinalSupplyEquipmentSummary.jsx index a8c7935a1..699ef8c97 100644 --- a/frontend/src/views/FinalSupplyEquipments/FinalSupplyEquipmentSummary.jsx +++ b/frontend/src/views/FinalSupplyEquipments/FinalSupplyEquipmentSummary.jsx @@ -1,12 +1,12 @@ import BCAlert from '@/components/BCAlert' import BCBox from '@/components/BCBox' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' -import { apiRoutes, ROUTES } from '@/constants/routes' -import { CommonArrayRenderer } from '@/utils/grid/cellRenderers' +import { apiRoutes } from '@/constants/routes' +import { CommonArrayRenderer, LinkRenderer } from '@/utils/grid/cellRenderers' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useLocation, useParams, useNavigate } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' import { v4 as uuid } from 'uuid' import { numberFormatter } from '@/utils/formatters.js' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses.js' @@ -15,12 +15,11 @@ export const FinalSupplyEquipmentSummary = ({ data, status }) => { const [alertMessage, setAlertMessage] = useState('') const [alertSeverity, setAlertSeverity] = useState('info') const [gridKey, setGridKey] = useState('final-supply-equipments-grid') - const { complianceReportId, compliancePeriod } = useParams() + const { complianceReportId } = useParams() const gridRef = useRef() const { t } = useTranslation(['common', 'finalSupplyEquipments']) const location = useLocation() - const navigate = useNavigate() useEffect(() => { if (location.state?.message) { @@ -45,10 +44,16 @@ export const FinalSupplyEquipmentSummary = ({ data, status }) => { const defaultColDef = useMemo( () => ({ floatingFilter: false, - filter: false + filter: false, + cellRenderer: + status === COMPLIANCE_REPORT_STATUSES.DRAFT ? LinkRenderer : undefined, + cellRendererParams: { + url: () => 'final-supply-equipments' + } }), - [] + [status] ) + const columns = useMemo( () => [ { @@ -188,17 +193,6 @@ export const FinalSupplyEquipmentSummary = ({ data, status }) => { setGridKey(`final-supply-equipments-grid-${uuid()}`) } - const handleRowClicked = () => { - if (status === COMPLIANCE_REPORT_STATUSES.DRAFT) { - navigate( - ROUTES.REPORTS_ADD_FINAL_SUPPLY_EQUIPMENTS.replace( - ':compliancePeriod', - compliancePeriod - ).replace(':complianceReportId', complianceReportId) - ) - } - } - return (
@@ -223,7 +217,6 @@ export const FinalSupplyEquipmentSummary = ({ data, status }) => { enableCopyButton={false} defaultColDef={defaultColDef} suppressPagination={data.finalSupplyEquipments.length <= 10} - handleRowClicked={handleRowClicked} /> diff --git a/frontend/src/views/FuelCodes/FuelCodes.jsx b/frontend/src/views/FuelCodes/FuelCodes.jsx index fba322f9a..a19fd7a7f 100644 --- a/frontend/src/views/FuelCodes/FuelCodes.jsx +++ b/frontend/src/views/FuelCodes/FuelCodes.jsx @@ -14,10 +14,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Stack } from '@mui/material' import BCTypography from '@/components/BCTypography' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' import { fuelCodeColDefs } from './_schema' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' const FuelCodesBase = () => { const [isDownloadingFuelCodes, setIsDownloadingFuelCodes] = useState(false) @@ -40,11 +41,15 @@ const FuelCodesBase = () => { return params.data.fuelCodeId.toString() } - const handleRowClicked = (params) => { - navigate( - ROUTES.FUELCODES_EDIT.replace(':fuelCodeID', params.data.fuelCodeId) - ) - } + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + url: (data) => data.data.fuelCodeId + } + }), + [] + ) const handleDownloadFuelCodes = async () => { setIsDownloadingFuelCodes(true) @@ -110,14 +115,10 @@ const FuelCodesBase = () => { columnDefs={fuelCodeColDefs(t)} query={useGetFuelCodes} queryParams={{ cacheTime: 0, staleTime: 0 }} - dataKey={'fuelCodes'} + dataKey="fuelCodes" getRowId={getRowId} - onRowClicked={handleRowClicked} overlayNoRowsTemplate={t('fuelCode:noFuelCodesFound')} - autoSizeStrategy={{ - defaultMinWidth: 50, - defaultMaxWidth: 600 - }} + defaultColDef={defaultColDef} /> diff --git a/frontend/src/views/FuelCodes/_schema.jsx b/frontend/src/views/FuelCodes/_schema.jsx index cb93d7617..d2eade3b5 100644 --- a/frontend/src/views/FuelCodes/_schema.jsx +++ b/frontend/src/views/FuelCodes/_schema.jsx @@ -1,39 +1,42 @@ import { CommonArrayRenderer, - FuelCodeStatusTextRenderer, + FuelCodeStatusRenderer, TextRenderer } from '@/utils/grid/cellRenderers' import { numberFormatter, timezoneFormatter } from '@/utils/formatters' import BCTypography from '@/components/BCTypography' -import { BCColumnSetFilter } from '@/components/BCDataGrid/components' +import { + BCSelectFloatingFilter, + BCDateFloatingFilter +} from '@/components/BCDataGrid/components' import { useFuelCodeStatuses, useTransportModes } from '@/hooks/useFuelCode' export const fuelCodeColDefs = (t) => [ { field: 'status', headerName: t('fuelCode:fuelCodeColLabels.status'), - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiOptionField: 'status', - apiQuery: useFuelCodeStatuses, - disableCloseOnSelect: false, - multiple: false + valueKey: 'status', + labelKey: 'status', + optionsQuery: useFuelCodeStatuses }, + suppressFloatingFilterButton: true, valueGetter: (params) => params.data.fuelCodeStatus.status, - cellRenderer: FuelCodeStatusTextRenderer + cellRenderer: FuelCodeStatusRenderer }, { field: 'prefix', headerName: t('fuelCode:fuelCodeColLabels.prefix'), - valueGetter: (params) => params.data.fuelCodePrefix.prefix, - cellRenderer: TextRenderer + suppressFloatingFilterButton: true, + valueGetter: (params) => params.data.fuelCodePrefix.prefix }, { field: 'fuelSuffix', headerName: t('fuelCode:fuelCodeColLabels.fuelSuffix'), - cellRenderer: TextRenderer, type: 'numericColumn', filter: 'agNumberColumnFilter', + suppressFloatingFilterButton: true, filterParams: { filterOptions: ['startsWith'], buttons: ['clear'] @@ -42,7 +45,6 @@ export const fuelCodeColDefs = (t) => [ { field: 'carbonIntensity', headerName: t('fuelCode:fuelCodeColLabels.carbonIntensity'), - cellRenderer: TextRenderer, type: 'numericColumn', filter: 'agNumberColumnFilter', filterParams: { @@ -52,78 +54,65 @@ export const fuelCodeColDefs = (t) => [ }, { field: 'edrms', - headerName: t('fuelCode:fuelCodeColLabels.edrms'), - cellRenderer: TextRenderer + headerName: t('fuelCode:fuelCodeColLabels.edrms') }, { field: 'company', headerName: t('fuelCode:fuelCodeColLabels.company'), - cellRenderer: TextRenderer, minWidth: 300 }, { field: 'contactName', headerName: t('fuelCode:fuelCodeColLabels.contactName'), - cellRenderer: TextRenderer, minWidth: 300 }, { field: 'contactEmail', headerName: t('fuelCode:fuelCodeColLabels.contactEmail'), - cellRenderer: TextRenderer, minWidth: 300 }, { field: 'applicationDate', headerName: t('fuelCode:fuelCodeColLabels.applicationDate'), - filter: false, - cellRenderer: TextRenderer + filter: false }, { field: 'approvalDate', headerName: t('fuelCode:fuelCodeColLabels.approvalDate'), - filter: false, - cellRenderer: TextRenderer + filter: false }, { field: 'effectiveDate', headerName: t('fuelCode:fuelCodeColLabels.effectiveDate'), - filter: false, - cellRenderer: TextRenderer + filter: false }, { field: 'expirationDate', headerName: t('fuelCode:fuelCodeColLabels.expirationDate'), - filter: false, - cellRenderer: TextRenderer + filter: false }, { field: 'fuelType', headerName: t('fuelCode:fuelCodeColLabels.fuelType'), - cellRenderer: TextRenderer, valueGetter: (params) => params.data.fuelType.fuelType }, { field: 'feedstock', - headerName: t('fuelCode:fuelCodeColLabels.feedstock'), - cellRenderer: TextRenderer + headerName: t('fuelCode:fuelCodeColLabels.feedstock') }, { field: 'feedstockLocation', headerName: t('fuelCode:fuelCodeColLabels.feedstockLocation'), - cellRenderer: TextRenderer, minWidth: 300 }, { field: 'feedstockMisc', headerName: t('fuelCode:fuelCodeColLabels.misc'), - cellRenderer: TextRenderer, minWidth: 495 }, { field: 'fuelProductionFacilityCity', headerName: t('fuelCode:fuelCodeColLabels.fuelProductionFacilityCity'), - cellRenderer: TextRenderer, minWidth: 325 }, { @@ -131,69 +120,64 @@ export const fuelCodeColDefs = (t) => [ headerName: t( 'fuelCode:fuelCodeColLabels.fuelProductionFacilityProvinceState' ), - cellRenderer: TextRenderer, minWidth: 325 }, { field: 'fuelProductionFacilityCountry', headerName: t('fuelCode:fuelCodeColLabels.fuelProductionFacilityCountry'), - cellRenderer: TextRenderer, minWidth: 325 }, { field: 'facilityNameplateCapacity', headerName: t('fuelCode:fuelCodeColLabels.facilityNameplateCapacity'), valueFormatter: numberFormatter, - cellRenderer: TextRenderer, minWidth: 290, type: 'numericColumn' }, { field: 'facilityNameplateCapacityUnit', headerName: t('fuelCode:fuelCodeColLabels.facilityNameplateCapacityUnit'), - cellRenderer: TextRenderer, minWidth: 290 }, { field: 'feedstockFuelTransportMode', headerName: t('fuelCode:fuelCodeColLabels.feedstockFuelTransportMode'), sortable: false, - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiOptionField: 'transportMode', - apiQuery: useTransportModes, - disableCloseOnSelect: false, - multiple: false + valueKey: 'transportMode', + labelKey: 'transportMode', + optionsQuery: useTransportModes }, + suppressFloatingFilterButton: true, minWidth: 335, valueGetter: (params) => params.data.feedstockFuelTransportModes.map( (item) => item.feedstockFuelTransportMode.transportMode ), - cellRenderer: (props) => + cellRenderer: (props) => }, { field: 'finishedFuelTransportMode', headerName: t('fuelCode:fuelCodeColLabels.finishedFuelTransportMode'), sortable: false, - floatingFilterComponent: BCColumnSetFilter, + floatingFilterComponent: BCSelectFloatingFilter, floatingFilterComponentParams: { - apiOptionField: 'transportMode', - apiQuery: useTransportModes, - disableCloseOnSelect: false, - multiple: false + valueKey: 'transportMode', + labelKey: 'transportMode', + optionsQuery: useTransportModes }, + suppressFloatingFilterButton: true, minWidth: 335, valueGetter: (params) => params.data.finishedFuelTransportModes.map( (item) => item.finishedFuelTransportMode.transportMode ), - cellRenderer: (props) => + cellRenderer: (props) => }, { field: 'formerCompany', headerName: t('fuelCode:fuelCodeColLabels.formerCompany'), - cellRenderer: TextRenderer, minWidth: 300 }, { @@ -212,7 +196,6 @@ export const fuelCodeColDefs = (t) => [ { field: 'notes', headerName: t('fuelCode:fuelCodeColLabels.notes'), - cellRenderer: TextRenderer, minWidth: 600 } ] diff --git a/frontend/src/views/FuelExports/FuelExportSummary.jsx b/frontend/src/views/FuelExports/FuelExportSummary.jsx index 76898317f..ef9cbd5b0 100644 --- a/frontend/src/views/FuelExports/FuelExportSummary.jsx +++ b/frontend/src/views/FuelExports/FuelExportSummary.jsx @@ -6,20 +6,19 @@ import { formatNumberWithCommas as valueFormatter } from '@/utils/formatters' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useLocation, useParams, useNavigate } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' import i18n from '@/i18n' -import { ROUTES } from '@/constants/routes' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses.js' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const FuelExportSummary = ({ data, status }) => { const [alertMessage, setAlertMessage] = useState('') const [alertSeverity, setAlertSeverity] = useState('info') - const { complianceReportId, compliancePeriod } = useParams() + const { complianceReportId } = useParams() const gridRef = useRef() const { t } = useTranslation(['common', 'fuelExport']) const location = useLocation() - const navigate = useNavigate() useEffect(() => { if (location.state?.message) { @@ -45,9 +44,14 @@ export const FuelExportSummary = ({ data, status }) => { const defaultColDef = useMemo( () => ({ floatingFilter: false, - filter: false + filter: false, + cellRenderer: + status === COMPLIANCE_REPORT_STATUSES.DRAFT ? LinkRenderer : undefined, + cellRendererParams: { + url: () => 'fuel-exports' + } }), - [] + [status] ) // TODO: The values for the following columns must be determined @@ -125,17 +129,6 @@ export const FuelExportSummary = ({ data, status }) => { return params.data.fuelExportId.toString() } - const handleRowClicked = () => { - if (status === COMPLIANCE_REPORT_STATUSES.DRAFT) { - navigate( - ROUTES.REPORTS_ADD_FUEL_EXPORTS.replace( - ':compliancePeriod', - compliancePeriod - ).replace(':complianceReportId', complianceReportId) - ) - } - } - return (
@@ -158,7 +151,6 @@ export const FuelExportSummary = ({ data, status }) => { enableCopyButton={false} defaultColDef={defaultColDef} suppressPagination={data.fuelExports.length <= 10} - onRowClicked={handleRowClicked} /> diff --git a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx index 735f09eee..f76063182 100644 --- a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx +++ b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx @@ -103,7 +103,7 @@ export const AddEditFuelSupplies = () => { item.fuelType?.fuelType === 'Other' ? item.fuelTypeOther : null, provisionOfTheAct: item.provisionOfTheAct?.name, fuelCode: item.fuelCode?.fuelCode, - endUse: item.endUse?.type || 'Any', + endUse: item.endUse?.type, id: uuid() })) setRowData([...updatedRowData, { id: uuid() }]) @@ -140,7 +140,7 @@ export const AddEditFuelSupplies = () => { item.fuelType?.fuelType === 'Other' ? item.fuelTypeOther : null, provisionOfTheAct: item.provisionOfTheAct?.name, fuelCode: item.fuelCode?.fuelCode, - endUse: item.endUse?.type || 'Any', + endUse: item.endUse?.type, id: uuid() })) setRowData(updatedRowData) @@ -161,11 +161,39 @@ export const AddEditFuelSupplies = () => { (item) => item.fuelCategory ) + const endUseTypes = selectedFuelType.eerRatios.map( + (item) => item.endUseType + ) + // Set to null if multiple options, otherwise use first item const categoryValue = fuelCategoryOptions.length === 1 ? fuelCategoryOptions[0] : null + const endUseValue = + endUseTypes.length === 1 ? endUseTypes[0].type : null params.node.setDataValue('fuelCategory', categoryValue) + params.node.setDataValue('endUseType', endUseValue) + } + } + + if (params.column.colId === 'fuelCategory') { + const selectedFuelType = optionsData?.fuelTypes?.find( + (obj) => params.node.data.fuelType === obj.fuelType + ) + + if (selectedFuelType) { + const endUseTypes = selectedFuelType.eerRatios + .filter( + (item) => + item.fuelCategory.fuelCategory === params.data.fuelCategory + ) + .map((item) => item.endUseType) + + // Set to null if multiple options, otherwise use first item + const endUseValue = + endUseTypes.length === 1 ? endUseTypes[0].type : null + + params.node.setDataValue('endUseType', endUseValue) } } }, diff --git a/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx b/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx index b9808f1ad..af7843444 100644 --- a/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx +++ b/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx @@ -1,27 +1,26 @@ import BCAlert from '@/components/BCAlert' import BCBox from '@/components/BCBox' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' -import { apiRoutes, ROUTES } from '@/constants/routes' +import { apiRoutes } from '@/constants/routes' import { formatNumberWithCommas as valueFormatter } from '@/utils/formatters' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' import { v4 as uuid } from 'uuid' import i18n from '@/i18n' -import { StandardCellWarningAndErrors } from '@/utils/grid/errorRenderers' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses.js' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const FuelSupplySummary = ({ data, status }) => { const [alertMessage, setAlertMessage] = useState('') const [alertSeverity, setAlertSeverity] = useState('info') const [gridKey, setGridKey] = useState(`fuel-supplies-grid`) - const { complianceReportId, compliancePeriod } = useParams() + const { complianceReportId } = useParams() const gridRef = useRef() const { t } = useTranslation(['common', 'fuelSupply']) const location = useLocation() - const navigate = useNavigate() useEffect(() => { if (location.state?.message) { @@ -47,12 +46,16 @@ export const FuelSupplySummary = ({ data, status }) => { const defaultColDef = useMemo( () => ({ floatingFilter: false, - filter: false + filter: false, + cellRenderer: + status === COMPLIANCE_REPORT_STATUSES.DRAFT ? LinkRenderer : undefined, + cellRendererParams: { + url: () => 'supply-of-fuel' + } }), - [] + [status] ) - // TODO: The values for the following columns must be determined const columns = useMemo( () => [ { @@ -127,17 +130,6 @@ export const FuelSupplySummary = ({ data, status }) => { setGridKey(`fuel-supplies-grid-${uuid()}`) } - const handleRowClicked = () => { - if (status === COMPLIANCE_REPORT_STATUSES.DRAFT) { - navigate( - ROUTES.REPORTS_ADD_SUPPLY_OF_FUEL.replace( - ':compliancePeriod', - compliancePeriod - ).replace(':complianceReportId', complianceReportId) - ) - } - } - return (
@@ -162,7 +154,6 @@ export const FuelSupplySummary = ({ data, status }) => { enableCopyButton={false} defaultColDef={defaultColDef} suppressPagination={data.fuelSupplies.length <= 10} - handleRowClicked={handleRowClicked} /> diff --git a/frontend/src/views/FuelSupplies/_schema.jsx b/frontend/src/views/FuelSupplies/_schema.jsx index ba1b52df9..9cbe65cbe 100644 --- a/frontend/src/views/FuelSupplies/_schema.jsx +++ b/frontend/src/views/FuelSupplies/_schema.jsx @@ -193,7 +193,7 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ { field: 'endUseType', headerComponent: RequiredHeader, - headerName: i18n.t('fuelSupply:fuelSupplyColLabels.endUse'), + headerName: i18n.t('fuelSupply:fuelSupplyColLabels.endUseId'), cellEditorParams: (params) => ({ options: [ ...new Set( @@ -206,7 +206,7 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ ?.map((item) => item.endUseType?.type) .sort() ) - ].filter((item) => item != null) || ['Any'], + ].filter((item) => item != null), multiple: false, disableCloseOnSelect: false, freeSolo: false, @@ -219,14 +219,10 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ cellStyle: (params) => StandardCellWarningAndErrors(params, errors, warnings), suppressKeyboardEvent, - valueGetter: (params) => { - return params.colDef?.cellEditorParams(params).options.length < 1 - ? 'Any' - : params.data?.endUseType?.type - }, + valueGetter: (params) => params.data.endUseType?.type, editable: (params) => { const cellParams = params.colDef?.cellEditorParams(params) - return cellParams.options.length > 0 + return cellParams.options.length > 1 }, valueSetter: (params) => { if (params.newValue) { diff --git a/frontend/src/views/Notifications/NotificationMenu/components/_schema.jsx b/frontend/src/views/Notifications/NotificationMenu/components/_schema.jsx index 601fb29c6..235eccc50 100644 --- a/frontend/src/views/Notifications/NotificationMenu/components/_schema.jsx +++ b/frontend/src/views/Notifications/NotificationMenu/components/_schema.jsx @@ -1,6 +1,7 @@ import { dateFormatter } from '@/utils/formatters' import { actions } from '@/components/BCDataGrid/columns' import { ROUTES } from '@/constants/routes' +import { BCDateFloatingFilter } from '@/components/BCDataGrid/components' export const columnDefs = (t, currentUser) => [ { @@ -16,7 +17,8 @@ export const columnDefs = (t, currentUser) => [ { colId: 'date', field: 'date', - cellDataType: 'date', + floatingFilterComponent: BCDateFloatingFilter, + suppressFloatingFilterButton: true, headerName: t('notifications:notificationColLabels.date'), valueGetter: (params) => params.data.createDate, valueFormatter: dateFormatter diff --git a/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx b/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx index 812192fa9..00a633f7d 100644 --- a/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx +++ b/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx @@ -1,9 +1,8 @@ -import { useState, useEffect, useMemo, useRef, useCallback } from 'react' +import { useState, useEffect, useRef, useCallback } from 'react' import BCTypography from '@/components/BCTypography' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate, useParams } from 'react-router-dom' -import { BCAlert2 } from '@/components/BCAlert' import BCBox from '@/components/BCBox' import Loading from '@/components/Loading' import { defaultColDef, notionalTransferColDefs } from './_schema' @@ -15,7 +14,6 @@ import { import { useCurrentUser } from '@/hooks/useCurrentUser' import { v4 as uuid } from 'uuid' import { BCGridEditor } from '@/components/BCDataGrid/BCGridEditor' -import { useApiService } from '@/services/useApiService' import * as ROUTES from '@/constants/routes/routes.js' export const AddEditNotionalTransfers = () => { @@ -26,7 +24,6 @@ export const AddEditNotionalTransfers = () => { const gridRef = useRef(null) const alertRef = useRef() const location = useLocation() - const apiService = useApiService() const { t } = useTranslation(['common', 'notionalTransfer', 'reports']) const { complianceReportId, compliancePeriod } = useParams() const { diff --git a/frontend/src/views/NotionalTransfers/NotionalTransferSummary.jsx b/frontend/src/views/NotionalTransfers/NotionalTransferSummary.jsx index daa71fae1..282e72164 100644 --- a/frontend/src/views/NotionalTransfers/NotionalTransferSummary.jsx +++ b/frontend/src/views/NotionalTransfers/NotionalTransferSummary.jsx @@ -3,21 +3,20 @@ import BCBox from '@/components/BCBox' import { BCGridViewer } from '@/components/BCDataGrid/BCGridViewer' import { useGetNotionalTransfers } from '@/hooks/useNotionalTransfer' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' -import { useEffect, useState, useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useLocation, useParams, useNavigate } from 'react-router-dom' +import { useLocation, useParams } from 'react-router-dom' import { formatNumberWithCommas as valueFormatter } from '@/utils/formatters' -import { ROUTES } from '@/constants/routes' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses.js' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const NotionalTransferSummary = ({ data, status }) => { const [alertMessage, setAlertMessage] = useState('') const [alertSeverity, setAlertSeverity] = useState('info') - const { complianceReportId, compliancePeriod } = useParams() + const { complianceReportId } = useParams() const { t } = useTranslation(['common', 'notionalTransfers']) const location = useLocation() - const navigate = useNavigate() useEffect(() => { if (location.state?.message) { @@ -30,22 +29,16 @@ export const NotionalTransferSummary = ({ data, status }) => { const defaultColDef = useMemo( () => ({ floatingFilter: false, - filter: false + filter: false, + cellRenderer: + status === COMPLIANCE_REPORT_STATUSES.DRAFT ? LinkRenderer : undefined, + cellRendererParams: { + url: () => 'notional-transfers' + } }), - [] + [status] ) - const handleRowClicked = () => { - if (status === COMPLIANCE_REPORT_STATUSES.DRAFT) { - navigate( - ROUTES.REPORTS_ADD_NOTIONAL_TRANSFERS.replace( - ':compliancePeriod', - compliancePeriod - ).replace(':complianceReportId', complianceReportId) - ) - } - } - const columns = [ { headerName: t('notionalTransfer:notionalTransferColLabels.legalName'), @@ -95,7 +88,7 @@ export const NotionalTransferSummary = ({ data, status }) => { defaultColDef={defaultColDef} query={useGetNotionalTransfers} queryParams={{ complianceReportId }} - dataKey={'notionalTransfers'} + dataKey="notionalTransfers" suppressPagination={data?.length <= 10} autoSizeStrategy={{ type: 'fitCellContents', @@ -105,7 +98,6 @@ export const NotionalTransferSummary = ({ data, status }) => { enableCellTextSelection ensureDomOrder handleRo - onRowClicked={handleRowClicked} /> diff --git a/frontend/src/views/Organizations/Organizations.jsx b/frontend/src/views/Organizations/Organizations.jsx index aad8c2aa7..bca77d4fa 100644 --- a/frontend/src/views/Organizations/Organizations.jsx +++ b/frontend/src/views/Organizations/Organizations.jsx @@ -12,7 +12,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { organizationsColDefs } from './ViewOrganization/_schema' // react components import { ROUTES, apiRoutes } from '@/constants/routes' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' // Services @@ -20,6 +20,7 @@ import { DownloadButton } from '@/components/DownloadButton' import { useApiService } from '@/services/useApiService' import { roles } from '@/constants/roles' import { Role } from '@/components/Role' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const Organizations = () => { const { t } = useTranslation(['common', 'org']) @@ -39,12 +40,7 @@ export const Organizations = () => { const location = useLocation() const defaultSortModel = [{ field: 'name', direction: 'asc' }] - // eslint-disable-next-line react-hooks/exhaustive-deps - const handleRowClicked = useCallback((params) => { - navigate( - ROUTES.ORGANIZATIONS_VIEW.replace(':orgID', params.data.organizationId) - ) - }) + const apiService = useApiService() const [isDownloadingOrgs, setIsDownloadingOrgs] = useState(false) const [isDownloadingUsers, setIsDownloadingUsers] = useState(false) @@ -85,6 +81,16 @@ export const Organizations = () => { } } + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + url: (data) => data.data.organizationId + } + }), + [] + ) + return ( <>
@@ -137,16 +143,16 @@ export const Organizations = () => { diff --git a/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx b/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx index d3c8dcf40..05fc5fbb6 100644 --- a/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx +++ b/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx @@ -5,24 +5,24 @@ import BCAlert from '@/components/BCAlert' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' import Loading from '@/components/Loading' import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard' -import colors from '@/themes/base/colors.js' import { faCirclePlus } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { ROUTES, apiRoutes } from '@/constants/routes' +import { apiRoutes, ROUTES } from '@/constants/routes' import { useCurrentUser } from '@/hooks/useCurrentUser' import { useOrganization, useOrganizationBalance } from '@/hooks/useOrganization' import { constructAddress } from '@/utils/constructAddress' -import { calculateRowHeight, phoneNumberFormatter } from '@/utils/formatters' +import { phoneNumberFormatter } from '@/utils/formatters' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { defaultSortModel, getUserColumnDefs } from './_schema' import { Role } from '@/components/Role' import { roles } from '@/constants/roles' import { ORGANIZATION_STATUSES } from '@/constants/statuses' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const ViewOrganization = () => { const { t } = useTranslation(['common', 'org']) @@ -71,23 +71,26 @@ export const ViewOrganization = () => { includeHiddenColumnsInQuickFilter: true } - const handleRowClicked = useCallback( - (params) => - // Based on the user Type (BCeID or IDIR) navigate to specific view - hasRoles(roles.supplier) - ? navigate( - ROUTES.ORGANIZATION_VIEWUSER.replace( - ':userID', - params.data.userProfileId - ) - ) - : navigate( - ROUTES.ORGANIZATIONS_VIEWUSER.replace(':orgID', orgID).replace( - ':userID', - params.data.userProfileId - ) - ), - [hasRoles, navigate, orgID] + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + isAbsolute: true, + url: ( + data // Based on the user Type (BCeID or IDIR) navigate to specific view + ) => + hasRoles(roles.supplier) + ? ROUTES.ORGANIZATION_VIEWUSER.replace( + ':userID', + data.data.userProfileId + ) + : ROUTES.ORGANIZATIONS_VIEWUSER.replace(':orgID', orgID).replace( + ':userID', + data.data.userProfileId + ) + } + }), + [hasRoles, orgID] ) const getRowId = useCallback((params) => params.data.userProfileId, []) @@ -331,7 +334,7 @@ export const ViewOrganization = () => { gridOptions={gridOptions} defaultSortModel={defaultSortModel} handleGridKey={handleGridKey} - handleRowClicked={handleRowClicked} + defaultColDef={defaultColDef} enableCopyButton={false} enableResetButton={false} /> diff --git a/frontend/src/views/Organizations/ViewOrganization/_schema.js b/frontend/src/views/Organizations/ViewOrganization/_schema.js index 8b78ce4d5..95625fe20 100644 --- a/frontend/src/views/Organizations/ViewOrganization/_schema.js +++ b/frontend/src/views/Organizations/ViewOrganization/_schema.js @@ -54,8 +54,7 @@ export const organizationsColDefs = (t) => [ labelKey: 'status', optionsQuery: useOrganizationStatuses }, - suppressFloatingFilterButton: true, - suppressHeaderFilterButton: true + suppressFloatingFilterButton: true } ] diff --git a/frontend/src/views/OtherUses/AddEditOtherUses.jsx b/frontend/src/views/OtherUses/AddEditOtherUses.jsx index 944a6ac67..17a625c32 100644 --- a/frontend/src/views/OtherUses/AddEditOtherUses.jsx +++ b/frontend/src/views/OtherUses/AddEditOtherUses.jsx @@ -1,4 +1,3 @@ - import { BCGridEditor } from '@/components/BCDataGrid/BCGridEditor' import Loading from '@/components/Loading' import { @@ -33,7 +32,7 @@ export const AddEditOtherUses = () => { data: optionsData, isLoading: optionsLoading, isFetched - } = useOtherUsesOptions() + } = useOtherUsesOptions({ compliancePeriod }) const { data: otherUses, isLoading: usesLoading } = useGetAllOtherUses(complianceReportId) const { mutateAsync: saveRow } = useSaveOtherUses({ complianceReportId }) @@ -81,22 +80,28 @@ export const AddEditOtherUses = () => { return ciOfFuel }, []) - const validate = (params, validationFn, errorMessage, alertRef, field = null) => { - const value = field ? params.node?.data[field] : params; + const validate = ( + params, + validationFn, + errorMessage, + alertRef, + field = null + ) => { + const value = field ? params.node?.data[field] : params if (field && params.colDef.field !== field) { - return true; + return true } if (!validationFn(value)) { alertRef.current?.triggerAlert({ message: errorMessage, - severity: 'error', - }); - return false; + severity: 'error' + }) + return false } - return true; // Proceed with the update - }; + return true // Proceed with the update + } const onGridReady = (params) => { const ensureRowIds = (rows) => { @@ -184,35 +189,34 @@ export const AddEditOtherUses = () => { if (params.colDef.field === 'fuelType') { const fuelType = optionsData?.fuelTypes?.find( (obj) => params.data.fuelType === obj.fuelType - ); + ) if (fuelType) { // Auto-populate the "units" field if (fuelType.units) { - params.node.setDataValue('units', fuelType.units); + params.node.setDataValue('units', fuelType.units) } else { - params.node.setDataValue('units', ''); + params.node.setDataValue('units', '') } // Auto-populate the "fuelCategory" field const fuelCategoryOptions = fuelType.fuelCategories.map( (item) => item.category - ); + ) - const categoryValue = fuelCategoryOptions.length === 1 - ? fuelCategoryOptions[0] - : null + const categoryValue = + fuelCategoryOptions.length === 1 ? fuelCategoryOptions[0] : null - params.node.setDataValue('fuelCategory', categoryValue); + params.node.setDataValue('fuelCategory', categoryValue) // Auto-populate the "fuelCode" field const fuelCodeOptions = fuelType.fuelCodes.map( (code) => code.fuelCode - ); - params.node.setDataValue('fuelCode', fuelCodeOptions[0] ?? null); + ) + params.node.setDataValue('fuelCode', fuelCodeOptions[0] ?? null) params.node.setDataValue( 'fuelCodeId', fuelType.fuelCodes[0]?.fuelCodeId ?? null - ); + ) } } } @@ -227,12 +231,12 @@ export const AddEditOtherUses = () => { const isValid = validate( params, (value) => { - return value !== null && !isNaN(value) && value > 0; + return value !== null && !isNaN(value) && value > 0 }, 'Quantity supplied must be greater than 0.', alertRef, - 'quantitySupplied', - ); + 'quantitySupplied' + ) if (!isValid) { return diff --git a/frontend/src/views/OtherUses/OtherUsesSummary.jsx b/frontend/src/views/OtherUses/OtherUsesSummary.jsx index 4c6a203ed..7ae1b66e4 100644 --- a/frontend/src/views/OtherUses/OtherUsesSummary.jsx +++ b/frontend/src/views/OtherUses/OtherUsesSummary.jsx @@ -3,25 +3,24 @@ import BCBox from '@/components/BCBox' import { BCGridViewer } from '@/components/BCDataGrid/BCGridViewer' import { useGetOtherUses } from '@/hooks/useOtherUses' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' -import { useEffect, useState } from 'react' -import { useLocation, useParams, useNavigate } from 'react-router-dom' +import { useEffect, useMemo, useState } from 'react' +import { useLocation, useParams } from 'react-router-dom' import { - formatNumberWithCommas as valueFormatter, - decimalFormatter + decimalFormatter, + formatNumberWithCommas as valueFormatter } from '@/utils/formatters' import { useTranslation } from 'react-i18next' -import { ROUTES } from '@/constants/routes' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses.js' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const OtherUsesSummary = ({ data, status }) => { const [alertMessage, setAlertMessage] = useState('') const [alertSeverity, setAlertSeverity] = useState('info') const { t } = useTranslation(['common', 'otherUses']) - const { complianceReportId, compliancePeriod } = useParams() + const { complianceReportId } = useParams() const location = useLocation() - const navigate = useNavigate() useEffect(() => { if (location.state?.message) { @@ -30,6 +29,19 @@ export const OtherUsesSummary = ({ data, status }) => { } }, [location.state]) + const defaultColDef = useMemo( + () => ({ + floatingFilter: false, + filter: false, + cellRenderer: + status === COMPLIANCE_REPORT_STATUSES.DRAFT ? LinkRenderer : undefined, + cellRendererParams: { + url: () => 'fuels-other-use' + } + }), + [status] + ) + const columns = [ { headerName: t('otherUses:otherUsesColLabels.fuelType'), @@ -87,17 +99,6 @@ export const OtherUsesSummary = ({ data, status }) => { const getRowId = (params) => params.data.otherUsesId - const handleRowClicked = () => { - if (status === COMPLIANCE_REPORT_STATUSES.DRAFT) { - navigate( - ROUTES.REPORTS_ADD_OTHER_USE_FUELS.replace( - ':compliancePeriod', - compliancePeriod - ).replace(':complianceReportId', complianceReportId) - ) - } - } - return (
@@ -112,7 +113,7 @@ export const OtherUsesSummary = ({ data, status }) => { gridKey={'other-uses'} getRowId={getRowId} columnDefs={columns} - defaultColDef={{ filter: false, floatingFilter: false }} + defaultColDef={defaultColDef} query={useGetOtherUses} queryParams={{ complianceReportId }} dataKey={'otherUses'} @@ -124,7 +125,6 @@ export const OtherUsesSummary = ({ data, status }) => { }} enableCellTextSelection ensureDomOrder - onRowClicked={handleRowClicked} /> diff --git a/frontend/src/views/Transactions/Transactions.jsx b/frontend/src/views/Transactions/Transactions.jsx index cd5a7f562..3227b1ab9 100644 --- a/frontend/src/views/Transactions/Transactions.jsx +++ b/frontend/src/views/Transactions/Transactions.jsx @@ -23,6 +23,7 @@ import { import { roles, govRoles } from '@/constants/roles' import OrganizationList from './components/OrganizationList' import Loading from '@/components/Loading' +import { LinkRenderer } from '@/utils/grid/cellRenderers.jsx' export const Transactions = () => { const { t } = useTranslation(['common', 'transaction']) @@ -40,9 +41,9 @@ export const Transactions = () => { const [alertMessage, setAlertMessage] = useState('') const [alertSeverity, setAlertSeverity] = useState('info') - const [gridKey, setGridKey] = useState(`transactions-grid`) + const [gridKey, setGridKey] = useState('transactions-grid') const handleGridKey = useCallback(() => { - setGridKey(`transactions-grid`) + setGridKey('transactions-grid') }, []) const gridOptions = { overlayNoRowsTemplate: t('txn:noTxnsFound') @@ -57,56 +58,62 @@ export const Transactions = () => { const [selectedOrgId, setSelectedOrgId] = useState(null) - // eslint-disable-next-line react-hooks/exhaustive-deps - const handleRowClicked = useCallback( - (params) => { - const { transactionId, transactionType, fromOrganization, status } = - params.data - const userOrgName = currentUser?.organization?.name + const defaultColDef = useMemo( + () => ({ + cellRenderer: LinkRenderer, + cellRendererParams: { + isAbsolute: true, + url: ( + data // Based on the user Type (BCeID or IDIR) navigate to specific view + ) => { + const { transactionId, transactionType, fromOrganization, status } = + data.data + const userOrgName = currentUser?.organization?.name - // Define routes mapping for transaction types - const routesMapping = { - Transfer: { - view: ROUTES.TRANSFERS_VIEW, - edit: ROUTES.TRANSFERS_EDIT - }, - AdminAdjustment: { - view: currentUser.isGovernmentUser - ? ROUTES.ADMIN_ADJUSTMENT_VIEW - : ROUTES.ORG_ADMIN_ADJUSTMENT_VIEW, - edit: ROUTES.ADMIN_ADJUSTMENT_EDIT - }, - InitiativeAgreement: { - view: currentUser.isGovernmentUser - ? ROUTES.INITIATIVE_AGREEMENT_VIEW - : ROUTES.ORG_INITIATIVE_AGREEMENT_VIEW, - edit: ROUTES.INITIATIVE_AGREEMENT_EDIT - } - } + // Define routes mapping for transaction types + const routesMapping = { + Transfer: { + view: ROUTES.TRANSFERS_VIEW, + edit: ROUTES.TRANSFERS_EDIT + }, + AdminAdjustment: { + view: currentUser.isGovernmentUser + ? ROUTES.ADMIN_ADJUSTMENT_VIEW + : ROUTES.ORG_ADMIN_ADJUSTMENT_VIEW, + edit: ROUTES.ADMIN_ADJUSTMENT_EDIT + }, + InitiativeAgreement: { + view: currentUser.isGovernmentUser + ? ROUTES.INITIATIVE_AGREEMENT_VIEW + : ROUTES.ORG_INITIATIVE_AGREEMENT_VIEW, + edit: ROUTES.INITIATIVE_AGREEMENT_EDIT + } + } - // Determine if it's an edit scenario - const isEditScenario = - (userOrgName === fromOrganization && - status === TRANSFER_STATUSES.DRAFT) || - (!fromOrganization && status === TRANSACTION_STATUSES.DRAFT) + // Determine if it's an edit scenario + const isEditScenario = + (userOrgName === fromOrganization && + status === TRANSFER_STATUSES.DRAFT) || + (!fromOrganization && status === TRANSACTION_STATUSES.DRAFT) - const routeType = isEditScenario ? 'edit' : 'view' + const routeType = isEditScenario ? 'edit' : 'view' - // Select the appropriate route based on the transaction type and scenario - const routeTemplate = routesMapping[transactionType]?.[routeType] + // Select the appropriate route based on the transaction type and scenario + const routeTemplate = routesMapping[transactionType]?.[routeType] - if (routeTemplate) { - navigate( - // replace any matching query params by chaining these replace methods - routeTemplate - .replace(':transactionId', transactionId) - .replace(':transferId', transactionId) - ) - } else { - console.error('No route defined for this transaction type and scenario') + if (routeTemplate) { + return routeTemplate + .replace(':transactionId', transactionId) + .replace(':transferId', transactionId) + } else { + console.error( + 'No route defined for this transaction type and scenario' + ) + } + } } - }, - [currentUser, navigate] + }), + [currentUser] ) // Determine the appropriate API endpoint @@ -259,9 +266,9 @@ export const Transactions = () => { defaultFilterModel={location.state?.filters} gridOptions={gridOptions} handleGridKey={handleGridKey} - handleRowClicked={handleRowClicked} enableCopyButton={false} highlightedRowId={highlightedId} + defaultColDef={defaultColDef} /> diff --git a/frontend/src/views/Transactions/__tests__/Transactions.test.jsx b/frontend/src/views/Transactions/__tests__/Transactions.test.jsx index 1d07c3c95..3bd2cc456 100644 --- a/frontend/src/views/Transactions/__tests__/Transactions.test.jsx +++ b/frontend/src/views/Transactions/__tests__/Transactions.test.jsx @@ -220,17 +220,4 @@ describe('Transactions', () => { expect(errorMessage).toBeInTheDocument() }) }) - - it('handles row click correctly', async () => { - render( - - - - ) - - const mockedDataGrid = screen.getByTestId('mocked-data-grid') - fireEvent.click(mockedDataGrid) - - expect(mockNavigate).toHaveBeenCalledWith('/transfers/123') - }) }) diff --git a/frontend/src/views/Transactions/_schema.js b/frontend/src/views/Transactions/_schema.js index 50827ed38..e5e2fded4 100644 --- a/frontend/src/views/Transactions/_schema.js +++ b/frontend/src/views/Transactions/_schema.js @@ -5,7 +5,10 @@ import { spacesFormatter } from '@/utils/formatters' import { TransactionStatusRenderer } from '@/utils/grid/cellRenderers' -import { BCSelectFloatingFilter } from '@/components/BCDataGrid/components' +import { + BCSelectFloatingFilter, + BCDateFloatingFilter +} from '@/components/BCDataGrid/components' import { useTransactionStatuses } from '@/hooks/useTransactions' const prefixMap = { @@ -109,7 +112,6 @@ export const transactionsColDefs = (t) => [ optionsQuery: useTransactionStatuses }, suppressFloatingFilterButton: true, - suppressHeaderFilterButton: true, minWidth: 180, width: 250 }, @@ -118,7 +120,7 @@ export const transactionsColDefs = (t) => [ field: 'updateDate', headerName: t('txn:txnColLabels.updateDate'), valueFormatter: dateFormatter, - width: 190, + minWidth: 250, filter: 'agDateColumnFilter', filterParams: { filterOptions: ['inRange', 'equals', 'lessThan', 'greaterThan'], @@ -137,7 +139,9 @@ export const transactionsColDefs = (t) => [ }, browserDatePicker: true, // Uses the browser's date picker if available buttons: ['clear'] - } + }, + floatingFilterComponent: BCDateFloatingFilter, + suppressFloatingFilterButton: true } ] diff --git a/frontend/src/views/Transactions/options.js b/frontend/src/views/Transactions/options.js deleted file mode 100644 index 69cd55d5f..000000000 --- a/frontend/src/views/Transactions/options.js +++ /dev/null @@ -1,38 +0,0 @@ -// import { getOrganization } from '@/utils/getOrganization' -// import { getStatus } from '@/utils/getStatus' -// import dayjs from 'dayjs' - -// export const gridProps = { -// columnDefs: [ -// { field: 'transactionId', headerName: 'ID' }, -// { -// field: 'compliancePeriod', -// headerName: 'Compliant period' -// }, -// { field: 'transactionType.type', headerName: 'Type' }, -// { -// valueGetter: (data) => getOrganization(data, 'from'), -// headerName: 'Compliance units from' -// }, -// { -// valueGetter: (data) => getOrganization(data, 'to'), -// headerName: 'Compliance units to' -// }, -// { field: 'complianceUnits', headerName: 'Number of units' }, -// { field: 'valuePerUnit', headerName: 'Value per unit' }, -// { -// valueGetter: getStatus, -// headerName: 'Status' -// }, -// { -// valueFormatter: (data) => dayjs(data.lastUpdated).format('YYYY-MM-DD'), -// headerName: 'Last updated' -// } -// ], -// defaultColDef: { -// resizable: true, -// sortable: true, -// filter: true, -// floatingFilter: true -// } -// }