diff --git a/backend/alembic/versions/14f212e85ab6_create_crew_and_crewavailability_tables.py b/backend/alembic/versions/14f212e85ab6_create_crew_and_crewavailability_tables.py index 9ed011a..4b7ae32 100644 --- a/backend/alembic/versions/14f212e85ab6_create_crew_and_crewavailability_tables.py +++ b/backend/alembic/versions/14f212e85ab6_create_crew_and_crewavailability_tables.py @@ -5,6 +5,7 @@ Revises: da13d44bd483 Create Date: 2026-05-31 11:50:59.485426 """ + from typing import Sequence, Union from alembic import op @@ -12,47 +13,69 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '14f212e85ab6' -down_revision: Union[str, None] = 'da13d44bd483' +revision: str = "14f212e85ab6" +down_revision: Union[str, None] = "da13d44bd483" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('crew', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('first_name', sa.String(length=100), nullable=False), - sa.Column('last_name', sa.String(length=100), nullable=False), - sa.Column('email', sa.String(length=255), nullable=True), - sa.Column('phone', sa.String(length=50), nullable=True), - sa.Column('role_title', sa.String(length=100), nullable=True), - sa.Column('hourly_rate', sa.Float(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "crew", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("first_name", sa.String(length=100), nullable=False), + sa.Column("last_name", sa.String(length=100), nullable=False), + sa.Column("email", sa.String(length=255), nullable=True), + sa.Column("phone", sa.String(length=50), nullable=True), + sa.Column("role_title", sa.String(length=100), nullable=True), + sa.Column("hourly_rate", sa.Float(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('crew_availabilities', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('crew_id', sa.String(length=36), nullable=False), - sa.Column('start_date', sa.DateTime(timezone=True), nullable=False), - sa.Column('end_date', sa.DateTime(timezone=True), nullable=False), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['crew_id'], ['crew.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "crew_availabilities", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("crew_id", sa.String(length=36), nullable=False), + sa.Column("start_date", sa.DateTime(timezone=True), nullable=False), + sa.Column("end_date", sa.DateTime(timezone=True), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["crew_id"], ["crew.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('crew_availabilities') - op.drop_table('crew') + op.drop_table("crew_availabilities") + op.drop_table("crew") # ### end Alembic commands ### diff --git a/backend/alembic/versions/32b02c118683_create_vehicle_and_vehicleassignment_.py b/backend/alembic/versions/32b02c118683_create_vehicle_and_vehicleassignment_.py index 6942f59..9e261ed 100644 --- a/backend/alembic/versions/32b02c118683_create_vehicle_and_vehicleassignment_.py +++ b/backend/alembic/versions/32b02c118683_create_vehicle_and_vehicleassignment_.py @@ -5,6 +5,7 @@ Revises: 14f212e85ab6 Create Date: 2026-05-31 12:05:27.261340 """ + from typing import Sequence, Union from alembic import op @@ -12,53 +13,75 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '32b02c118683' -down_revision: Union[str, None] = '14f212e85ab6' +revision: str = "32b02c118683" +down_revision: Union[str, None] = "14f212e85ab6" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('vehicles', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('license_plate', sa.String(length=50), nullable=True), - sa.Column('brand', sa.String(length=100), nullable=True), - sa.Column('model', sa.String(length=100), nullable=True), - sa.Column('year', sa.Integer(), nullable=True), - sa.Column('color', sa.String(length=50), nullable=True), - sa.Column('vehicle_type', sa.String(length=50), nullable=True), - sa.Column('payload_capacity_kg', sa.Float(), nullable=True), - sa.Column('load_volume_m3', sa.Float(), nullable=True), - sa.Column('fuel_type', sa.String(length=30), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('license_plate') + op.create_table( + "vehicles", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("license_plate", sa.String(length=50), nullable=True), + sa.Column("brand", sa.String(length=100), nullable=True), + sa.Column("model", sa.String(length=100), nullable=True), + sa.Column("year", sa.Integer(), nullable=True), + sa.Column("color", sa.String(length=50), nullable=True), + sa.Column("vehicle_type", sa.String(length=50), nullable=True), + sa.Column("payload_capacity_kg", sa.Float(), nullable=True), + sa.Column("load_volume_m3", sa.Float(), nullable=True), + sa.Column("fuel_type", sa.String(length=30), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("license_plate"), ) - op.create_table('vehicle_assignments', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('vehicle_id', sa.String(length=36), nullable=False), - sa.Column('project_id', sa.String(length=36), nullable=True), - sa.Column('start_date', sa.DateTime(timezone=True), nullable=False), - sa.Column('end_date', sa.DateTime(timezone=True), nullable=False), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['vehicle_id'], ['vehicles.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "vehicle_assignments", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("vehicle_id", sa.String(length=36), nullable=False), + sa.Column("project_id", sa.String(length=36), nullable=True), + sa.Column("start_date", sa.DateTime(timezone=True), nullable=False), + sa.Column("end_date", sa.DateTime(timezone=True), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["vehicle_id"], ["vehicles.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('vehicle_assignments') - op.drop_table('vehicles') + op.drop_table("vehicle_assignments") + op.drop_table("vehicles") # ### end Alembic commands ### diff --git a/backend/alembic/versions/714cc11a59c4_create_roles_table.py b/backend/alembic/versions/714cc11a59c4_create_roles_table.py index 98b25ef..f0b6217 100644 --- a/backend/alembic/versions/714cc11a59c4_create_roles_table.py +++ b/backend/alembic/versions/714cc11a59c4_create_roles_table.py @@ -5,6 +5,7 @@ Revises: f90ee7806a25 Create Date: 2026-05-31 10:35:56.717393 """ + from typing import Sequence, Union from alembic import op @@ -12,33 +13,36 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '714cc11a59c4' -down_revision: Union[str, None] = 'f90ee7806a25' +revision: str = "714cc11a59c4" +down_revision: Union[str, None] = "f90ee7806a25" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('roles', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('description', sa.String(length=500), nullable=True), - sa.Column('permissions', sa.JSON(), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "roles", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("description", sa.String(length=500), nullable=True), + sa.Column("permissions", sa.JSON(), nullable=False), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.create_foreign_key('fk_users_role_id_roles', 'roles', ['role_id'], ['id'], ondelete='SET NULL') + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.create_foreign_key( + "fk_users_role_id_roles", "roles", ["role_id"], ["id"], ondelete="SET NULL" + ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.drop_constraint(None, type_="foreignkey") - op.drop_table('roles') + op.drop_table("roles") # ### end Alembic commands ### diff --git a/backend/alembic/versions/a5b1e93690ff_create_contacts_and_tags.py b/backend/alembic/versions/a5b1e93690ff_create_contacts_and_tags.py index 484e649..139098e 100644 --- a/backend/alembic/versions/a5b1e93690ff_create_contacts_and_tags.py +++ b/backend/alembic/versions/a5b1e93690ff_create_contacts_and_tags.py @@ -5,6 +5,7 @@ Revises: 714cc11a59c4 Create Date: 2026-05-31 10:58:21.063736 """ + from typing import Sequence, Union from alembic import op @@ -12,64 +13,82 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = 'a5b1e93690ff' -down_revision: Union[str, None] = '714cc11a59c4' +revision: str = "a5b1e93690ff" +down_revision: Union[str, None] = "714cc11a59c4" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('contacts', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('type', sa.String(length=10), nullable=False), - sa.Column('company_name', sa.String(length=255), nullable=True), - sa.Column('first_name', sa.String(length=100), nullable=True), - sa.Column('last_name', sa.String(length=100), nullable=True), - sa.Column('email', sa.String(length=255), nullable=True), - sa.Column('phone', sa.String(length=50), nullable=True), - sa.Column('mobile', sa.String(length=50), nullable=True), - sa.Column('website', sa.String(length=500), nullable=True), - sa.Column('billing_street', sa.String(length=255), nullable=True), - sa.Column('billing_number', sa.String(length=20), nullable=True), - sa.Column('billing_postalcode', sa.String(length=20), nullable=True), - sa.Column('billing_city', sa.String(length=100), nullable=True), - sa.Column('billing_country', sa.String(length=100), nullable=True), - sa.Column('shipping_street', sa.String(length=255), nullable=True), - sa.Column('shipping_number', sa.String(length=20), nullable=True), - sa.Column('shipping_postalcode', sa.String(length=20), nullable=True), - sa.Column('shipping_city', sa.String(length=100), nullable=True), - sa.Column('shipping_country', sa.String(length=100), nullable=True), - sa.Column('tax_number', sa.String(length=50), nullable=True), - sa.Column('note', sa.String(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "contacts", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("type", sa.String(length=10), nullable=False), + sa.Column("company_name", sa.String(length=255), nullable=True), + sa.Column("first_name", sa.String(length=100), nullable=True), + sa.Column("last_name", sa.String(length=100), nullable=True), + sa.Column("email", sa.String(length=255), nullable=True), + sa.Column("phone", sa.String(length=50), nullable=True), + sa.Column("mobile", sa.String(length=50), nullable=True), + sa.Column("website", sa.String(length=500), nullable=True), + sa.Column("billing_street", sa.String(length=255), nullable=True), + sa.Column("billing_number", sa.String(length=20), nullable=True), + sa.Column("billing_postalcode", sa.String(length=20), nullable=True), + sa.Column("billing_city", sa.String(length=100), nullable=True), + sa.Column("billing_country", sa.String(length=100), nullable=True), + sa.Column("shipping_street", sa.String(length=255), nullable=True), + sa.Column("shipping_number", sa.String(length=20), nullable=True), + sa.Column("shipping_postalcode", sa.String(length=20), nullable=True), + sa.Column("shipping_city", sa.String(length=100), nullable=True), + sa.Column("shipping_country", sa.String(length=100), nullable=True), + sa.Column("tax_number", sa.String(length=50), nullable=True), + sa.Column("note", sa.String(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('tags', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=100), nullable=False), - sa.Column('color', sa.String(length=7), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "tags", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("color", sa.String(length=7), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('contact_tags', - sa.Column('contact_id', sa.String(length=36), nullable=False), - sa.Column('tag_id', sa.String(length=36), nullable=False), - sa.ForeignKeyConstraint(['contact_id'], ['contacts.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('contact_id', 'tag_id') + op.create_table( + "contact_tags", + sa.Column("contact_id", sa.String(length=36), nullable=False), + sa.Column("tag_id", sa.String(length=36), nullable=False), + sa.ForeignKeyConstraint(["contact_id"], ["contacts.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["tag_id"], ["tags.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("contact_id", "tag_id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('contact_tags') - op.drop_table('tags') - op.drop_table('contacts') + op.drop_table("contact_tags") + op.drop_table("tags") + op.drop_table("contacts") # ### end Alembic commands ### diff --git a/backend/alembic/versions/b183c967088f_add_project_equipment_and_crew.py b/backend/alembic/versions/b183c967088f_add_project_equipment_and_crew.py index 79a072b..85a1455 100644 --- a/backend/alembic/versions/b183c967088f_add_project_equipment_and_crew.py +++ b/backend/alembic/versions/b183c967088f_add_project_equipment_and_crew.py @@ -5,6 +5,7 @@ Revises: cfe638817921 Create Date: 2026-05-31 21:32:46.068246 """ + from typing import Sequence, Union from alembic import op @@ -12,54 +13,65 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = 'b183c967088f' -down_revision: Union[str, None] = 'cfe638817921' +revision: str = "b183c967088f" +down_revision: Union[str, None] = "cfe638817921" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('project_equipment_groups', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('project_id', sa.String(length=36), nullable=False), - sa.Column('sort_order', sa.Integer(), nullable=False), - sa.Column('start_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), - sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "project_equipment_groups", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("project_id", sa.String(length=36), nullable=False), + sa.Column("sort_order", sa.Integer(), nullable=False), + sa.Column("start_date", sa.DateTime(timezone=True), nullable=True), + sa.Column("end_date", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('project_equipment', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('project_equipment_group_id', sa.String(length=36), nullable=False), - sa.Column('equipment_id', sa.String(length=36), nullable=True), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('quantity', sa.Float(), nullable=False), - sa.Column('price', sa.Float(), nullable=False), - sa.Column('discount', sa.Float(), nullable=False), - sa.Column('sort_order', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['project_equipment_group_id'], ['project_equipment_groups.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "project_equipment", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("project_equipment_group_id", sa.String(length=36), nullable=False), + sa.Column("equipment_id", sa.String(length=36), nullable=True), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("quantity", sa.Float(), nullable=False), + sa.Column("price", sa.Float(), nullable=False), + sa.Column("discount", sa.Float(), nullable=False), + sa.Column("sort_order", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["equipment_id"], ["equipment.id"], ondelete="SET NULL" + ), + sa.ForeignKeyConstraint( + ["project_equipment_group_id"], + ["project_equipment_groups.id"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('project_crew', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('project_function_id', sa.String(length=36), nullable=False), - sa.Column('crew_id', sa.String(length=36), nullable=False), - sa.Column('start_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['crew_id'], ['crew.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['project_function_id'], ['project_functions.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "project_crew", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("project_function_id", sa.String(length=36), nullable=False), + sa.Column("crew_id", sa.String(length=36), nullable=False), + sa.Column("start_date", sa.DateTime(timezone=True), nullable=True), + sa.Column("end_date", sa.DateTime(timezone=True), nullable=True), + sa.Column("notes", sa.Text(), nullable=True), + sa.ForeignKeyConstraint(["crew_id"], ["crew.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["project_function_id"], ["project_functions.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('project_crew') - op.drop_table('project_equipment') - op.drop_table('project_equipment_groups') + op.drop_table("project_crew") + op.drop_table("project_equipment") + op.drop_table("project_equipment_groups") # ### end Alembic commands ### diff --git a/backend/alembic/versions/cfe638817921_add_function_type_and_group_dates.py b/backend/alembic/versions/cfe638817921_add_function_type_and_group_dates.py index 1909317..2cea50b 100644 --- a/backend/alembic/versions/cfe638817921_add_function_type_and_group_dates.py +++ b/backend/alembic/versions/cfe638817921_add_function_type_and_group_dates.py @@ -5,6 +5,7 @@ Revises: f5772dd5cd55 Create Date: 2026-05-31 21:17:13.064051 """ + from typing import Sequence, Union from alembic import op @@ -12,31 +13,37 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = 'cfe638817921' -down_revision: Union[str, None] = 'f5772dd5cd55' +revision: str = "cfe638817921" +down_revision: Union[str, None] = "f5772dd5cd55" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('project_function_groups', schema=None) as batch_op: - batch_op.add_column(sa.Column('start_date', sa.DateTime(timezone=True), nullable=True)) - batch_op.add_column(sa.Column('end_date', sa.DateTime(timezone=True), nullable=True)) + with op.batch_alter_table("project_function_groups", schema=None) as batch_op: + batch_op.add_column( + sa.Column("start_date", sa.DateTime(timezone=True), nullable=True) + ) + batch_op.add_column( + sa.Column("end_date", sa.DateTime(timezone=True), nullable=True) + ) - with op.batch_alter_table('project_functions', schema=None) as batch_op: - batch_op.add_column(sa.Column('function_type', sa.String(length=50), nullable=False)) + with op.batch_alter_table("project_functions", schema=None) as batch_op: + batch_op.add_column( + sa.Column("function_type", sa.String(length=50), nullable=False) + ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('project_functions', schema=None) as batch_op: - batch_op.drop_column('function_type') + with op.batch_alter_table("project_functions", schema=None) as batch_op: + batch_op.drop_column("function_type") - with op.batch_alter_table('project_function_groups', schema=None) as batch_op: - batch_op.drop_column('end_date') - batch_op.drop_column('start_date') + with op.batch_alter_table("project_function_groups", schema=None) as batch_op: + batch_op.drop_column("end_date") + batch_op.drop_column("start_date") # ### end Alembic commands ### diff --git a/backend/alembic/versions/da13d44bd483_create_equipment_stocklocation_.py b/backend/alembic/versions/da13d44bd483_create_equipment_stocklocation_.py index ca9694b..60c894d 100644 --- a/backend/alembic/versions/da13d44bd483_create_equipment_stocklocation_.py +++ b/backend/alembic/versions/da13d44bd483_create_equipment_stocklocation_.py @@ -5,6 +5,7 @@ Revises: a5b1e93690ff Create Date: 2026-05-31 11:31:54.796382 """ + from typing import Sequence, Union from alembic import op @@ -12,81 +13,121 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = 'da13d44bd483' -down_revision: Union[str, None] = 'a5b1e93690ff' +revision: str = "da13d44bd483" +down_revision: Union[str, None] = "a5b1e93690ff" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('stock_locations', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('address', sa.String(length=500), nullable=True), - sa.Column('is_default', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "stock_locations", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("address", sa.String(length=500), nullable=True), + sa.Column("is_default", sa.Boolean(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('equipment', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('category', sa.String(length=100), nullable=True), - sa.Column('brand', sa.String(length=255), nullable=True), - sa.Column('serial_number', sa.String(length=100), nullable=True), - sa.Column('barcode', sa.String(length=100), nullable=True), - sa.Column('qr_code', sa.String(length=500), nullable=True), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('purchase_price', sa.Float(), nullable=True), - sa.Column('current_value', sa.Float(), nullable=True), - sa.Column('weight_kg', sa.Float(), nullable=True), - sa.Column('dimensions', sa.String(length=255), nullable=True), - sa.Column('power_watt', sa.Float(), nullable=True), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('custom_fields', sa.Text(), nullable=True), - sa.Column('location_id', sa.String(length=36), nullable=True), - sa.Column('supplier_id', sa.String(length=36), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['location_id'], ['stock_locations.id'], ondelete='SET NULL'), - sa.ForeignKeyConstraint(['supplier_id'], ['contacts.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('barcode'), - sa.UniqueConstraint('serial_number') + op.create_table( + "equipment", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("category", sa.String(length=100), nullable=True), + sa.Column("brand", sa.String(length=255), nullable=True), + sa.Column("serial_number", sa.String(length=100), nullable=True), + sa.Column("barcode", sa.String(length=100), nullable=True), + sa.Column("qr_code", sa.String(length=500), nullable=True), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("purchase_price", sa.Float(), nullable=True), + sa.Column("current_value", sa.Float(), nullable=True), + sa.Column("weight_kg", sa.Float(), nullable=True), + sa.Column("dimensions", sa.String(length=255), nullable=True), + sa.Column("power_watt", sa.Float(), nullable=True), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column("custom_fields", sa.Text(), nullable=True), + sa.Column("location_id", sa.String(length=36), nullable=True), + sa.Column("supplier_id", sa.String(length=36), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["location_id"], ["stock_locations.id"], ondelete="SET NULL" + ), + sa.ForeignKeyConstraint(["supplier_id"], ["contacts.id"], ondelete="SET NULL"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("barcode"), + sa.UniqueConstraint("serial_number"), ) - op.create_table('equipment_groups', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('daily_rate', sa.Float(), nullable=True), - sa.Column('default_location_id', sa.String(length=36), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['default_location_id'], ['stock_locations.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "equipment_groups", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("daily_rate", sa.Float(), nullable=True), + sa.Column("default_location_id", sa.String(length=36), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["default_location_id"], ["stock_locations.id"], ondelete="SET NULL" + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('equipment_group_items', - sa.Column('group_id', sa.String(length=36), nullable=False), - sa.Column('equipment_id', sa.String(length=36), nullable=False), - sa.Column('quantity', sa.Float(), nullable=False), - sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['group_id'], ['equipment_groups.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('group_id', 'equipment_id') + op.create_table( + "equipment_group_items", + sa.Column("group_id", sa.String(length=36), nullable=False), + sa.Column("equipment_id", sa.String(length=36), nullable=False), + sa.Column("quantity", sa.Float(), nullable=False), + sa.ForeignKeyConstraint(["equipment_id"], ["equipment.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["group_id"], ["equipment_groups.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("group_id", "equipment_id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('equipment_group_items') - op.drop_table('equipment_groups') - op.drop_table('equipment') - op.drop_table('stock_locations') + op.drop_table("equipment_group_items") + op.drop_table("equipment_groups") + op.drop_table("equipment") + op.drop_table("stock_locations") # ### end Alembic commands ### diff --git a/backend/alembic/versions/f5772dd5cd55_create_project_tables.py b/backend/alembic/versions/f5772dd5cd55_create_project_tables.py index 10ea620..e48f045 100644 --- a/backend/alembic/versions/f5772dd5cd55_create_project_tables.py +++ b/backend/alembic/versions/f5772dd5cd55_create_project_tables.py @@ -5,6 +5,7 @@ Revises: 32b02c118683 Create Date: 2026-05-31 13:59:30.308076 """ + from typing import Sequence, Union from alembic import op @@ -12,68 +13,84 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = 'f5772dd5cd55' -down_revision: Union[str, None] = '32b02c118683' +revision: str = "f5772dd5cd55" +down_revision: Union[str, None] = "32b02c118683" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('projects', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('status', sa.String(length=20), nullable=False), - sa.Column('start_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), - sa.Column('budget', sa.Float(), nullable=True), - sa.Column('total_costs', sa.Float(), nullable=False), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('custom_fields', sa.Text(), nullable=True), - sa.Column('custom_field_defs', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "projects", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("start_date", sa.DateTime(timezone=True), nullable=True), + sa.Column("end_date", sa.DateTime(timezone=True), nullable=True), + sa.Column("budget", sa.Float(), nullable=True), + sa.Column("total_costs", sa.Float(), nullable=False), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column("custom_fields", sa.Text(), nullable=True), + sa.Column("custom_field_defs", sa.Text(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('project_function_groups', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('project_id', sa.String(length=36), nullable=False), - sa.Column('sort_order', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "project_function_groups", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("project_id", sa.String(length=36), nullable=False), + sa.Column("sort_order", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('sub_projects', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('project_id', sa.String(length=36), nullable=False), - sa.Column('parent_id', sa.String(length=36), nullable=True), - sa.Column('sort_order', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['parent_id'], ['sub_projects.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "sub_projects", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("project_id", sa.String(length=36), nullable=False), + sa.Column("parent_id", sa.String(length=36), nullable=True), + sa.Column("sort_order", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["parent_id"], ["sub_projects.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('project_functions', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('function_group_id', sa.String(length=36), nullable=False), - sa.Column('quantity', sa.Float(), nullable=False), - sa.Column('daily_costs', sa.Float(), nullable=False), - sa.Column('total_costs', sa.Float(), nullable=False), - sa.Column('sort_order', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['function_group_id'], ['project_function_groups.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "project_functions", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("function_group_id", sa.String(length=36), nullable=False), + sa.Column("quantity", sa.Float(), nullable=False), + sa.Column("daily_costs", sa.Float(), nullable=False), + sa.Column("total_costs", sa.Float(), nullable=False), + sa.Column("sort_order", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["function_group_id"], ["project_function_groups.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("id"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('project_functions') - op.drop_table('sub_projects') - op.drop_table('project_function_groups') - op.drop_table('projects') + op.drop_table("project_functions") + op.drop_table("sub_projects") + op.drop_table("project_function_groups") + op.drop_table("projects") # ### end Alembic commands ### diff --git a/backend/alembic/versions/f90ee7806a25_create_accounts_and_users_tables.py b/backend/alembic/versions/f90ee7806a25_create_accounts_and_users_tables.py index c0a6372..6f7b9d0 100644 --- a/backend/alembic/versions/f90ee7806a25_create_accounts_and_users_tables.py +++ b/backend/alembic/versions/f90ee7806a25_create_accounts_and_users_tables.py @@ -1,10 +1,11 @@ """create_accounts_and_users_tables Revision ID: f90ee7806a25 -Revises: +Revises: Create Date: 2026-05-31 10:25:47.726322 """ + from typing import Sequence, Union from alembic import op @@ -12,7 +13,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = 'f90ee7806a25' +revision: str = "f90ee7806a25" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -20,31 +21,48 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('accounts', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "accounts", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('users', - sa.Column('id', sa.String(length=36), nullable=False), - sa.Column('account_id', sa.String(length=36), nullable=False), - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('full_name', sa.String(length=255), nullable=False), - sa.Column('password_hash', sa.String(length=255), nullable=False), - sa.Column('role_id', sa.String(length=36), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), - sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') + op.create_table( + "users", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("account_id", sa.String(length=36), nullable=False), + sa.Column("email", sa.String(length=255), nullable=False), + sa.Column("full_name", sa.String(length=255), nullable=False), + sa.Column("password_hash", sa.String(length=255), nullable=False), + sa.Column("role_id", sa.String(length=36), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("(CURRENT_TIMESTAMP)"), + nullable=False, + ), + sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email"), ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('users') - op.drop_table('accounts') + op.drop_table("users") + op.drop_table("accounts") # ### end Alembic commands ### diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index aaf050d..16a3fbd 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -42,9 +42,7 @@ async def get_current_user( # Load user with role (and its permissions) eagerly result = await session.execute( - select(User) - .options(joinedload(User.role)) - .where(User.id == user_id) + select(User).options(joinedload(User.role)).where(User.id == user_id) ) user = result.unique().scalars().first() diff --git a/backend/app/api/v1/auth.py b/backend/app/api/v1/auth.py index 220c8d1..faa2fc6 100644 --- a/backend/app/api/v1/auth.py +++ b/backend/app/api/v1/auth.py @@ -42,7 +42,9 @@ def _build_user_info(user: User, role: Role | None = None) -> UserInfo: ) -@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED +) async def register( body: RegisterRequest, session: AsyncSession = Depends(get_async_session), @@ -57,7 +59,9 @@ async def register( ) # Check if account name already exists - result = await session.execute(select(Account).where(Account.name == body.account_name)) + result = await session.execute( + select(Account).where(Account.name == body.account_name) + ) if result.scalars().first(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, @@ -71,13 +75,26 @@ async def register( # Create admin role admin_permissions = [ - "projects:read", "projects:write", "projects:delete", - "equipment:read", "equipment:write", "equipment:delete", - "crew:read", "crew:write", "crew:delete", - "vehicles:read", "vehicles:write", "vehicles:delete", - "contacts:read", "contacts:write", "contacts:delete", - "users:read", "users:write", "users:delete", - "roles:read", "roles:write", + "projects:read", + "projects:write", + "projects:delete", + "equipment:read", + "equipment:write", + "equipment:delete", + "crew:read", + "crew:write", + "crew:delete", + "vehicles:read", + "vehicles:write", + "vehicles:delete", + "contacts:read", + "contacts:write", + "contacts:delete", + "users:read", + "users:write", + "users:delete", + "roles:read", + "roles:write", ] admin_role = Role( account_id=account.id, diff --git a/backend/app/api/v1/contacts.py b/backend/app/api/v1/contacts.py index 0cd6d6b..b436bd9 100644 --- a/backend/app/api/v1/contacts.py +++ b/backend/app/api/v1/contacts.py @@ -3,12 +3,11 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import joinedload, selectinload +from sqlalchemy.orm import joinedload -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Contact, Tag -from app.models.contact import contact_tags from app.schemas.contact import ( ContactCreateRequest, ContactUpdateRequest, @@ -24,7 +23,9 @@ async def list_contacts( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), search: str | None = Query(None, description="Search in name, email, phone"), - type: str | None = Query(None, pattern="^(company|person)$", description="Filter by contact type"), + type: str | None = Query( + None, pattern="^(company|person)$", description="Filter by contact type" + ), current_user: User = Depends(require_permission("contacts:read")), session: AsyncSession = Depends(get_async_session), ): @@ -56,8 +57,7 @@ async def list_contacts( # Fetch page with tags eager-loaded q = ( - base_q - .options(joinedload(Contact.tags)) + base_q.options(joinedload(Contact.tags)) .order_by(Contact.updated_at.desc()) .offset((page - 1) * size) .limit(size) diff --git a/backend/app/api/v1/crew.py b/backend/app/api/v1/crew.py index 55d3f79..c6a4cc5 100644 --- a/backend/app/api/v1/crew.py +++ b/backend/app/api/v1/crew.py @@ -3,9 +3,9 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import joinedload, selectinload +from sqlalchemy.orm import selectinload -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Crew, CrewAvailability from app.schemas.crew import ( @@ -24,6 +24,7 @@ avail_router = APIRouter(prefix="/crew-availabilities", tags=["crew-availabiliti # ===== Crew Endpoints ===== + @router.get("", response_model=CrewListResponse) async def list_crew( page: int = Query(1, ge=1), @@ -55,8 +56,7 @@ async def list_crew( total = (await session.execute(count_q)).scalar() or 0 q = ( - base_q - .options(selectinload(Crew.availabilities)) + base_q.options(selectinload(Crew.availabilities)) .order_by(Crew.last_name, Crew.first_name) .offset((page - 1) * size) .limit(size) @@ -102,7 +102,9 @@ async def get_crew( ) member = result.unique().scalars().first() if not member: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found" + ) return CrewResponse.model_validate(member) @@ -122,7 +124,9 @@ async def update_crew( ) member = result.unique().scalars().first() if not member: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -146,7 +150,9 @@ async def delete_crew( ) member = result.scalars().first() if not member: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found" + ) await session.delete(member) await session.commit() @@ -155,6 +161,7 @@ async def delete_crew( # ===== CrewAvailability Endpoints ===== + @avail_router.get("", response_model=CrewAvailabilityListResponse) async def list_availabilities( page: int = Query(1, ge=1), @@ -166,11 +173,7 @@ async def list_availabilities( ): """List availabilities with filters.""" account_id = current_user.account_id - base_q = ( - select(CrewAvailability) - .join(Crew) - .where(Crew.account_id == account_id) - ) + base_q = select(CrewAvailability).join(Crew).where(Crew.account_id == account_id) if crew_id: base_q = base_q.where(CrewAvailability.crew_id == crew_id) @@ -180,7 +183,11 @@ async def list_availabilities( count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 - q = base_q.order_by(CrewAvailability.start_date.desc()).offset((page - 1) * size).limit(size) + q = ( + base_q.order_by(CrewAvailability.start_date.desc()) + .offset((page - 1) * size) + .limit(size) + ) result = await session.execute(q) items = result.scalars().all() @@ -192,7 +199,9 @@ async def list_availabilities( ) -@avail_router.post("", response_model=CrewAvailabilityResponse, status_code=status.HTTP_201_CREATED) +@avail_router.post( + "", response_model=CrewAvailabilityResponse, status_code=status.HTTP_201_CREATED +) async def create_availability( body: CrewAvailabilityCreateRequest, current_user: User = Depends(require_permission("crew:write")), @@ -207,7 +216,9 @@ async def create_availability( ) ) if not crew_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found" + ) avail = CrewAvailability(**body.model_dump()) session.add(avail) @@ -234,7 +245,9 @@ async def update_availability( ) avail = result.scalars().first() if not avail: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -262,7 +275,9 @@ async def delete_availability( ) avail = result.scalars().first() if not avail: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found" + ) await session.delete(avail) await session.commit() diff --git a/backend/app/api/v1/equipment.py b/backend/app/api/v1/equipment.py index d0bdfd9..8740b54 100644 --- a/backend/app/api/v1/equipment.py +++ b/backend/app/api/v1/equipment.py @@ -5,7 +5,7 @@ from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Equipment, StockLocation, Contact from app.schemas.equipment import ( @@ -13,8 +13,6 @@ from app.schemas.equipment import ( EquipmentUpdateRequest, EquipmentResponse, EquipmentListResponse, - LocationRef, - SupplierRef, ) router = APIRouter(prefix="/equipment", tags=["equipment"]) @@ -24,7 +22,9 @@ router = APIRouter(prefix="/equipment", tags=["equipment"]) async def list_equipment( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), - search: str | None = Query(None, description="Search in name, brand, serial_number"), + search: str | None = Query( + None, description="Search in name, brand, serial_number" + ), category: str | None = Query(None, description="Filter by category"), status: str | None = Query(None, description="Filter by status"), location_id: str | None = Query(None, description="Filter by location"), @@ -58,8 +58,7 @@ async def list_equipment( total = (await session.execute(count_q)).scalar() or 0 q = ( - base_q - .options(joinedload(Equipment.location), joinedload(Equipment.supplier)) + base_q.options(joinedload(Equipment.location), joinedload(Equipment.supplier)) .order_by(Equipment.updated_at.desc()) .offset((page - 1) * size) .limit(size) @@ -142,7 +141,9 @@ async def get_equipment( ) item = result.unique().scalars().first() if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found" + ) return EquipmentResponse.model_validate(item) @@ -162,7 +163,9 @@ async def update_equipment( ) item = result.unique().scalars().first() if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -182,11 +185,15 @@ async def delete_equipment( """Delete an equipment item.""" account_id = current_user.account_id result = await session.execute( - select(Equipment).where(Equipment.id == item_id, Equipment.account_id == account_id) + select(Equipment).where( + Equipment.id == item_id, Equipment.account_id == account_id + ) ) item = result.scalars().first() if not item: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found" + ) await session.delete(item) await session.commit() diff --git a/backend/app/api/v1/equipment_groups.py b/backend/app/api/v1/equipment_groups.py index c1b2a1c..2fc384a 100644 --- a/backend/app/api/v1/equipment_groups.py +++ b/backend/app/api/v1/equipment_groups.py @@ -1,22 +1,20 @@ """EquipmentGroup (Bundle) CRUD endpoints.""" from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy import select, func, or_ +from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload, selectinload -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session -from app.models import User, EquipmentGroup, Equipment, StockLocation +from app.models import User, EquipmentGroup, Equipment from app.models.equipment_group import equipment_group_items from app.schemas.equipment_group import ( EquipmentGroupCreateRequest, EquipmentGroupUpdateRequest, EquipmentGroupResponse, EquipmentGroupListResponse, - EquipmentGroupItem as EquipmentGroupItemSchema, ) -from app.schemas.equipment import LocationRef router = APIRouter(prefix="/equipment-groups", tags=["equipment-groups"]) @@ -48,8 +46,10 @@ async def list_equipment_groups( total = (await session.execute(count_q)).scalar() or 0 q = ( - base_q - .options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) + base_q.options( + joinedload(EquipmentGroup.default_location), + selectinload(EquipmentGroup.items), + ) .order_by(EquipmentGroup.name) .offset((page - 1) * size) .limit(size) @@ -65,7 +65,9 @@ async def list_equipment_groups( ) -@router.post("", response_model=EquipmentGroupResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "", response_model=EquipmentGroupResponse, status_code=status.HTTP_201_CREATED +) async def create_equipment_group( body: EquipmentGroupCreateRequest, current_user: User = Depends(require_permission("equipment:write")), @@ -110,7 +112,10 @@ async def create_equipment_group( # Reload with relationships await session.execute( select(EquipmentGroup) - .options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) + .options( + joinedload(EquipmentGroup.default_location), + selectinload(EquipmentGroup.items), + ) .where(EquipmentGroup.id == group.id) ) await session.refresh(group) @@ -128,12 +133,17 @@ async def get_equipment_group( account_id = current_user.account_id result = await session.execute( select(EquipmentGroup) - .options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) + .options( + joinedload(EquipmentGroup.default_location), + selectinload(EquipmentGroup.items), + ) .where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id) ) group = result.unique().scalars().first() if not group: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found" + ) return EquipmentGroupResponse.model_validate(group) @@ -148,12 +158,17 @@ async def update_equipment_group( account_id = current_user.account_id result = await session.execute( select(EquipmentGroup) - .options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) + .options( + joinedload(EquipmentGroup.default_location), + selectinload(EquipmentGroup.items), + ) .where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id) ) group = result.unique().scalars().first() if not group: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found" + ) update_data = body.model_dump(exclude_unset=True, exclude={"items"}) for field, value in update_data.items(): @@ -201,7 +216,9 @@ async def delete_equipment_group( ) group = result.scalars().first() if not group: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found" + ) await session.delete(group) await session.commit() diff --git a/backend/app/api/v1/projects.py b/backend/app/api/v1/projects.py index 5b79998..fc462b8 100644 --- a/backend/app/api/v1/projects.py +++ b/backend/app/api/v1/projects.py @@ -1,11 +1,10 @@ """Project CRUD endpoints including sub-projects, function groups, and functions.""" from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy import select, func, or_ +from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Project, SubProject, ProjectFunctionGroup, ProjectFunction from app.schemas.project import ( @@ -38,7 +37,10 @@ async def list_projects( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), search: str | None = Query(None, description="Search in project name"), - status: str | None = Query(None, description="Filter by status (draft, confirmed, in_progress, completed, cancelled)"), + status: str | None = Query( + None, + description="Filter by status (draft, confirmed, in_progress, completed, cancelled)", + ), current_user: User = Depends(require_permission("projects:read")), session: AsyncSession = Depends(get_async_session), ): @@ -57,12 +59,7 @@ async def list_projects( count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 - q = ( - base_q - .order_by(Project.updated_at.desc()) - .offset((page - 1) * size) - .limit(size) - ) + q = base_q.order_by(Project.updated_at.desc()).offset((page - 1) * size).limit(size) result = await session.execute(q) items = result.scalars().all() @@ -98,11 +95,15 @@ async def get_project( """Get a specific project.""" account_id = current_user.account_id result = await session.execute( - select(Project).where(Project.id == project_id, Project.account_id == account_id) + select(Project).where( + Project.id == project_id, Project.account_id == account_id + ) ) project = result.scalars().first() if not project: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) return ProjectResponse.model_validate(project) @@ -116,11 +117,15 @@ async def update_project( """Update an existing project.""" account_id = current_user.account_id result = await session.execute( - select(Project).where(Project.id == project_id, Project.account_id == account_id) + select(Project).where( + Project.id == project_id, Project.account_id == account_id + ) ) project = result.scalars().first() if not project: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -140,11 +145,15 @@ async def delete_project( """Delete a project.""" account_id = current_user.account_id result = await session.execute( - select(Project).where(Project.id == project_id, Project.account_id == account_id) + select(Project).where( + Project.id == project_id, Project.account_id == account_id + ) ) project = result.scalars().first() if not project: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) await session.delete(project) await session.commit() @@ -167,10 +176,14 @@ async def list_subprojects( # Verify project ownership proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) base_q = select(SubProject).where(SubProject.project_id == project_id) count_q = select(func.count()).select_from(base_q.subquery()) @@ -188,7 +201,11 @@ async def list_subprojects( ) -@router.post("/{project_id}/subprojects", response_model=SubProjectResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "/{project_id}/subprojects", + response_model=SubProjectResponse, + status_code=status.HTTP_201_CREATED, +) async def create_subproject( project_id: str, body: SubProjectCreateRequest, @@ -200,10 +217,14 @@ async def create_subproject( # Verify project ownership proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) # Verify parent subproject if provided if body.parent_id: @@ -238,17 +259,25 @@ async def get_subproject( # Verify project ownership proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) result = await session.execute( - select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id) + select(SubProject).where( + SubProject.id == sub_id, SubProject.project_id == project_id + ) ) sub = result.scalars().first() if not sub: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found" + ) return SubProjectResponse.model_validate(sub) @@ -265,17 +294,25 @@ async def update_subproject( # Verify project ownership proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) result = await session.execute( - select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id) + select(SubProject).where( + SubProject.id == sub_id, SubProject.project_id == project_id + ) ) sub = result.scalars().first() if not sub: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -286,7 +323,9 @@ async def update_subproject( return SubProjectResponse.model_validate(sub) -@router.delete("/{project_id}/subprojects/{sub_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{project_id}/subprojects/{sub_id}", status_code=status.HTTP_204_NO_CONTENT +) async def delete_subproject( project_id: str, sub_id: str, @@ -298,17 +337,25 @@ async def delete_subproject( # Verify project ownership proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) result = await session.execute( - select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id) + select(SubProject).where( + SubProject.id == sub_id, SubProject.project_id == project_id + ) ) sub = result.scalars().first() if not sub: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found" + ) await session.delete(sub) await session.commit() @@ -318,7 +365,9 @@ async def delete_subproject( # ===== ProjectFunctionGroup Endpoints ===== -@router.get("/{project_id}/function-groups", response_model=ProjectFunctionGroupListResponse) +@router.get( + "/{project_id}/function-groups", response_model=ProjectFunctionGroupListResponse +) async def list_function_groups( project_id: str, page: int = Query(1, ge=1), @@ -330,16 +379,26 @@ async def list_function_groups( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) - base_q = select(ProjectFunctionGroup).where(ProjectFunctionGroup.project_id == project_id) + base_q = select(ProjectFunctionGroup).where( + ProjectFunctionGroup.project_id == project_id + ) count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 - q = base_q.order_by(ProjectFunctionGroup.sort_order).offset((page - 1) * size).limit(size) + q = ( + base_q.order_by(ProjectFunctionGroup.sort_order) + .offset((page - 1) * size) + .limit(size) + ) result = await session.execute(q) items = result.scalars().all() @@ -351,7 +410,11 @@ async def list_function_groups( ) -@router.post("/{project_id}/function-groups", response_model=ProjectFunctionGroupResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "/{project_id}/function-groups", + response_model=ProjectFunctionGroupResponse, + status_code=status.HTTP_201_CREATED, +) async def create_function_group( project_id: str, body: ProjectFunctionGroupCreateRequest, @@ -362,10 +425,14 @@ async def create_function_group( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) fg = ProjectFunctionGroup(project_id=project_id, **body.model_dump()) session.add(fg) @@ -374,7 +441,9 @@ async def create_function_group( return ProjectFunctionGroupResponse.model_validate(fg) -@router.get("/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse) +@router.get( + "/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse +) async def get_function_group( project_id: str, fg_id: str, @@ -385,10 +454,14 @@ async def get_function_group( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) result = await session.execute( select(ProjectFunctionGroup).where( @@ -398,11 +471,15 @@ async def get_function_group( ) fg = result.scalars().first() if not fg: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) return ProjectFunctionGroupResponse.model_validate(fg) -@router.put("/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse) +@router.put( + "/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse +) async def update_function_group( project_id: str, fg_id: str, @@ -414,10 +491,14 @@ async def update_function_group( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) result = await session.execute( select(ProjectFunctionGroup).where( @@ -427,7 +508,9 @@ async def update_function_group( ) fg = result.scalars().first() if not fg: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -438,7 +521,9 @@ async def update_function_group( return ProjectFunctionGroupResponse.model_validate(fg) -@router.delete("/{project_id}/function-groups/{fg_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{project_id}/function-groups/{fg_id}", status_code=status.HTTP_204_NO_CONTENT +) async def delete_function_group( project_id: str, fg_id: str, @@ -449,10 +534,14 @@ async def delete_function_group( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) result = await session.execute( select(ProjectFunctionGroup).where( @@ -462,7 +551,9 @@ async def delete_function_group( ) fg = result.scalars().first() if not fg: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) await session.delete(fg) await session.commit() @@ -472,7 +563,10 @@ async def delete_function_group( # ===== ProjectFunction Endpoints ===== -@router.get("/{project_id}/function-groups/{fg_id}/functions", response_model=ProjectFunctionListResponse) +@router.get( + "/{project_id}/function-groups/{fg_id}/functions", + response_model=ProjectFunctionListResponse, +) async def list_project_functions( project_id: str, fg_id: str, @@ -485,10 +579,14 @@ async def list_project_functions( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) # Verify function group belongs to project fg_result = await session.execute( @@ -498,13 +596,19 @@ async def list_project_functions( ) ) if not fg_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) base_q = select(ProjectFunction).where(ProjectFunction.function_group_id == fg_id) count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 - q = base_q.order_by(ProjectFunction.sort_order).offset((page - 1) * size).limit(size) + q = ( + base_q.order_by(ProjectFunction.sort_order) + .offset((page - 1) * size) + .limit(size) + ) result = await session.execute(q) items = result.scalars().all() @@ -516,7 +620,11 @@ async def list_project_functions( ) -@router.post("/{project_id}/function-groups/{fg_id}/functions", response_model=ProjectFunctionResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "/{project_id}/function-groups/{fg_id}/functions", + response_model=ProjectFunctionResponse, + status_code=status.HTTP_201_CREATED, +) async def create_project_function( project_id: str, fg_id: str, @@ -528,10 +636,14 @@ async def create_project_function( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) fg_result = await session.execute( select(ProjectFunctionGroup.id).where( @@ -540,7 +652,9 @@ async def create_project_function( ) ) if not fg_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) func_obj = ProjectFunction(function_group_id=fg_id, **body.model_dump()) session.add(func_obj) @@ -549,7 +663,10 @@ async def create_project_function( return ProjectFunctionResponse.model_validate(func_obj) -@router.get("/{project_id}/function-groups/{fg_id}/functions/{func_id}", response_model=ProjectFunctionResponse) +@router.get( + "/{project_id}/function-groups/{fg_id}/functions/{func_id}", + response_model=ProjectFunctionResponse, +) async def get_project_function( project_id: str, fg_id: str, @@ -561,10 +678,14 @@ async def get_project_function( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) fg_result = await session.execute( select(ProjectFunctionGroup.id).where( @@ -573,7 +694,9 @@ async def get_project_function( ) ) if not fg_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) result = await session.execute( select(ProjectFunction).where( @@ -583,11 +706,16 @@ async def get_project_function( ) func_obj = result.scalars().first() if not func_obj: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function not found" + ) return ProjectFunctionResponse.model_validate(func_obj) -@router.put("/{project_id}/function-groups/{fg_id}/functions/{func_id}", response_model=ProjectFunctionResponse) +@router.put( + "/{project_id}/function-groups/{fg_id}/functions/{func_id}", + response_model=ProjectFunctionResponse, +) async def update_project_function( project_id: str, fg_id: str, @@ -600,10 +728,14 @@ async def update_project_function( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) fg_result = await session.execute( select(ProjectFunctionGroup.id).where( @@ -612,7 +744,9 @@ async def update_project_function( ) ) if not fg_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) result = await session.execute( select(ProjectFunction).where( @@ -622,7 +756,9 @@ async def update_project_function( ) func_obj = result.scalars().first() if not func_obj: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -633,7 +769,10 @@ async def update_project_function( return ProjectFunctionResponse.model_validate(func_obj) -@router.delete("/{project_id}/function-groups/{fg_id}/functions/{func_id}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{project_id}/function-groups/{fg_id}/functions/{func_id}", + status_code=status.HTTP_204_NO_CONTENT, +) async def delete_project_function( project_id: str, fg_id: str, @@ -645,10 +784,14 @@ async def delete_project_function( account_id = current_user.account_id proj_result = await session.execute( - select(Project.id).where(Project.id == project_id, Project.account_id == account_id) + select(Project.id).where( + Project.id == project_id, Project.account_id == account_id + ) ) if not proj_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Project not found" + ) fg_result = await session.execute( select(ProjectFunctionGroup.id).where( @@ -657,7 +800,9 @@ async def delete_project_function( ) ) if not fg_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found" + ) result = await session.execute( select(ProjectFunction).where( @@ -667,7 +812,9 @@ async def delete_project_function( ) func_obj = result.scalars().first() if not func_obj: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Function not found" + ) await session.delete(func_obj) await session.commit() diff --git a/backend/app/api/v1/roles.py b/backend/app/api/v1/roles.py index 27aedf7..a338314 100644 --- a/backend/app/api/v1/roles.py +++ b/backend/app/api/v1/roles.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Role from app.schemas.role import RoleCreateRequest, RoleUpdateRequest, RoleResponse diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index fb59dad..a97c9bc 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -12,7 +12,10 @@ from app.api.v1.equipment import router as equipment_router from app.api.v1.stock_locations import router as stock_locations_router from app.api.v1.equipment_groups import router as equipment_groups_router from app.api.v1.crew import router as crew_router, avail_router as crew_avail_router -from app.api.v1.vehicles import router as vehicles_router, assign_router as vehicle_assign_router +from app.api.v1.vehicles import ( + router as vehicles_router, + assign_router as vehicle_assign_router, +) from app.api.v1.projects import router as projects_router api_v1_router = APIRouter() diff --git a/backend/app/api/v1/stock_locations.py b/backend/app/api/v1/stock_locations.py index a5b2033..2af2757 100644 --- a/backend/app/api/v1/stock_locations.py +++ b/backend/app/api/v1/stock_locations.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, StockLocation from app.schemas.stock_location import ( @@ -48,7 +48,9 @@ async def list_stock_locations( ) -@router.post("", response_model=StockLocationResponse, status_code=status.HTTP_201_CREATED) +@router.post( + "", response_model=StockLocationResponse, status_code=status.HTTP_201_CREATED +) async def create_stock_location( body: StockLocationCreateRequest, current_user: User = Depends(require_permission("equipment:write")), @@ -60,8 +62,9 @@ async def create_stock_location( # If this is set as default, unset others if body.is_default: await session.execute( - select(StockLocation) - .where(StockLocation.account_id == account_id, StockLocation.is_default == True) # noqa: E712 + select(StockLocation).where( + StockLocation.account_id == account_id, StockLocation.is_default == True + ) # noqa: E712 ) loc = StockLocation(account_id=account_id, **body.model_dump()) @@ -87,7 +90,9 @@ async def get_stock_location( ) loc = result.scalars().first() if not loc: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found" + ) return StockLocationResponse.model_validate(loc) @@ -108,7 +113,9 @@ async def update_stock_location( ) loc = result.scalars().first() if not loc: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -135,7 +142,9 @@ async def delete_stock_location( ) loc = result.scalars().first() if not loc: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found" + ) await session.delete(loc) await session.commit() diff --git a/backend/app/api/v1/tags.py b/backend/app/api/v1/tags.py index 8154341..f53b8f5 100644 --- a/backend/app/api/v1/tags.py +++ b/backend/app/api/v1/tags.py @@ -1,10 +1,10 @@ """Tags endpoints for listing and creating tags within a tenant.""" from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy import select, func +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Tag from app.schemas.contact import TagResponse diff --git a/backend/app/api/v1/users.py b/backend/app/api/v1/users.py index 5c81774..8727fac 100644 --- a/backend/app/api/v1/users.py +++ b/backend/app/api/v1/users.py @@ -4,11 +4,16 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.core.security import get_password_hash from app.db.session import get_async_session from app.models import User -from app.schemas.user import UserCreateRequest, UserUpdateRequest, UserResponse, UserListResponse +from app.schemas.user import ( + UserCreateRequest, + UserUpdateRequest, + UserResponse, + UserListResponse, +) router = APIRouter(prefix="/users", tags=["users"]) diff --git a/backend/app/api/v1/vehicles.py b/backend/app/api/v1/vehicles.py index 93e8fa4..c069721 100644 --- a/backend/app/api/v1/vehicles.py +++ b/backend/app/api/v1/vehicles.py @@ -5,7 +5,7 @@ from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.api.deps import get_current_user, require_permission +from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Vehicle, VehicleAssignment from app.schemas.vehicle import ( @@ -25,6 +25,7 @@ assign_router = APIRouter(prefix="/vehicle-assignments", tags=["vehicle-assignme # ===== Vehicle Endpoints ===== + @router.get("", response_model=VehicleListResponse) async def list_vehicles( page: int = Query(1, ge=1), @@ -41,8 +42,12 @@ async def list_vehicles( if search: pattern = f"%{search}%" base_q = base_q.where( - or_(Vehicle.name.ilike(pattern), Vehicle.license_plate.ilike(pattern), - Vehicle.brand.ilike(pattern), Vehicle.model.ilike(pattern)) + or_( + Vehicle.name.ilike(pattern), + Vehicle.license_plate.ilike(pattern), + Vehicle.brand.ilike(pattern), + Vehicle.model.ilike(pattern), + ) ) if vehicle_type: base_q = base_q.where(Vehicle.vehicle_type == vehicle_type) @@ -52,11 +57,21 @@ async def list_vehicles( count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 - q = base_q.options(selectinload(Vehicle.assignments)).order_by(Vehicle.name).offset((page-1)*size).limit(size) + q = ( + base_q.options(selectinload(Vehicle.assignments)) + .order_by(Vehicle.name) + .offset((page - 1) * size) + .limit(size) + ) result = await session.execute(q) items = result.unique().scalars().all() - return VehicleListResponse(items=[VehicleResponse.model_validate(i) for i in items], total=total, page=page, size=size) + return VehicleListResponse( + items=[VehicleResponse.model_validate(i) for i in items], + total=total, + page=page, + size=size, + ) @router.post("", response_model=VehicleResponse, status_code=status.HTTP_201_CREATED) @@ -81,12 +96,15 @@ async def get_vehicle( ): account_id = current_user.account_id result = await session.execute( - select(Vehicle).options(selectinload(Vehicle.assignments)) + select(Vehicle) + .options(selectinload(Vehicle.assignments)) .where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id) ) vehicle = result.unique().scalars().first() if not vehicle: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" + ) return VehicleResponse.model_validate(vehicle) @@ -99,12 +117,15 @@ async def update_vehicle( ): account_id = current_user.account_id result = await session.execute( - select(Vehicle).options(selectinload(Vehicle.assignments)) + select(Vehicle) + .options(selectinload(Vehicle.assignments)) .where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id) ) vehicle = result.unique().scalars().first() if not vehicle: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(vehicle, field, value) @@ -120,10 +141,16 @@ async def delete_vehicle( session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id - result = await session.execute(select(Vehicle).where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id)) + result = await session.execute( + select(Vehicle).where( + Vehicle.id == vehicle_id, Vehicle.account_id == account_id + ) + ) vehicle = result.scalars().first() if not vehicle: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" + ) await session.delete(vehicle) await session.commit() return None @@ -131,6 +158,7 @@ async def delete_vehicle( # ===== VehicleAssignment Endpoints ===== + @assign_router.get("", response_model=VehicleAssignmentListResponse) async def list_assignments( page: int = Query(1, ge=1), @@ -141,28 +169,47 @@ async def list_assignments( session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id - base_q = select(VehicleAssignment).join(Vehicle).where(Vehicle.account_id == account_id) + base_q = ( + select(VehicleAssignment).join(Vehicle).where(Vehicle.account_id == account_id) + ) if vehicle_id: base_q = base_q.where(VehicleAssignment.vehicle_id == vehicle_id) if status: base_q = base_q.where(VehicleAssignment.status == status) count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 - q = base_q.order_by(VehicleAssignment.start_date.desc()).offset((page-1)*size).limit(size) + q = ( + base_q.order_by(VehicleAssignment.start_date.desc()) + .offset((page - 1) * size) + .limit(size) + ) result = await session.execute(q) items = result.scalars().all() - return VehicleAssignmentListResponse(items=[VehicleAssignmentResponse.model_validate(i) for i in items], total=total, page=page, size=size) + return VehicleAssignmentListResponse( + items=[VehicleAssignmentResponse.model_validate(i) for i in items], + total=total, + page=page, + size=size, + ) -@assign_router.post("", response_model=VehicleAssignmentResponse, status_code=status.HTTP_201_CREATED) +@assign_router.post( + "", response_model=VehicleAssignmentResponse, status_code=status.HTTP_201_CREATED +) async def create_assignment( body: VehicleAssignmentCreateRequest, current_user: User = Depends(require_permission("vehicles:write")), session: AsyncSession = Depends(get_async_session), ): - v_result = await session.execute(select(Vehicle).where(Vehicle.id == body.vehicle_id, Vehicle.account_id == current_user.account_id)) + v_result = await session.execute( + select(Vehicle).where( + Vehicle.id == body.vehicle_id, Vehicle.account_id == current_user.account_id + ) + ) if not v_result.scalars().first(): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" + ) assign = VehicleAssignment(**body.model_dump()) session.add(assign) await session.commit() @@ -178,11 +225,18 @@ async def update_assignment( session: AsyncSession = Depends(get_async_session), ): result = await session.execute( - select(VehicleAssignment).join(Vehicle).where(VehicleAssignment.id == assign_id, Vehicle.account_id == current_user.account_id) + select(VehicleAssignment) + .join(Vehicle) + .where( + VehicleAssignment.id == assign_id, + Vehicle.account_id == current_user.account_id, + ) ) assign = result.scalars().first() if not assign: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found" + ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(assign, field, value) @@ -198,11 +252,18 @@ async def delete_assignment( session: AsyncSession = Depends(get_async_session), ): result = await session.execute( - select(VehicleAssignment).join(Vehicle).where(VehicleAssignment.id == assign_id, Vehicle.account_id == current_user.account_id) + select(VehicleAssignment) + .join(Vehicle) + .where( + VehicleAssignment.id == assign_id, + Vehicle.account_id == current_user.account_id, + ) ) assign = result.scalars().first() if not assign: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found" + ) await session.delete(assign) await session.commit() return None diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 69c9a84..40f5a07 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,7 +1,6 @@ """Application configuration using pydantic-settings.""" from pydantic_settings import BaseSettings -from typing import Optional class Settings(BaseSettings): @@ -38,7 +37,9 @@ class Settings(BaseSettings): @property def cors_origin_list(self) -> list[str]: """Return CORS origins as a list.""" - return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()] + return [ + origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip() + ] model_config = { "env_file": ".env", diff --git a/backend/app/core/security.py b/backend/app/core/security.py index adedbab..9d2d116 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -21,26 +21,36 @@ def get_password_hash(password: str) -> str: return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") -def create_access_token(subject: str | Any, expires_delta: timedelta | None = None) -> str: +def create_access_token( + subject: str | Any, expires_delta: timedelta | None = None +) -> str: """Create a short-lived JWT access token.""" if expires_delta is None: expires_delta = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) now = datetime.now(timezone.utc) expire = now + expires_delta to_encode = {"exp": expire, "sub": str(subject), "iat": now, "type": "access"} - return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) + return jwt.encode( + to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM + ) -def create_refresh_token(subject: str | Any, expires_delta: timedelta | None = None) -> str: +def create_refresh_token( + subject: str | Any, expires_delta: timedelta | None = None +) -> str: """Create a long-lived JWT refresh token.""" if expires_delta is None: expires_delta = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES) now = datetime.now(timezone.utc) expire = now + expires_delta to_encode = {"exp": expire, "sub": str(subject), "iat": now, "type": "refresh"} - return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) + return jwt.encode( + to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM + ) def verify_token(token: str) -> dict: """Decode and verify a JWT token. Returns the payload dict.""" - return jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]) + return jwt.decode( + token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM] + ) diff --git a/backend/app/db/base.py b/backend/app/db/base.py index daf9cdb..47be9dd 100644 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -5,4 +5,5 @@ from sqlalchemy.orm import DeclarativeBase class Base(DeclarativeBase): """Base class for all database models.""" + pass diff --git a/backend/app/models/account.py b/backend/app/models/account.py index 48cef20..5d3aec4 100644 --- a/backend/app/models/account.py +++ b/backend/app/models/account.py @@ -25,11 +25,23 @@ class Account(Base): # Relationships users: Mapped[list["User"]] = relationship("User", back_populates="account") roles: Mapped[list["Role"]] = relationship("Role", back_populates="account") - contacts: Mapped[list["Contact"]] = relationship("Contact", back_populates="account") + contacts: Mapped[list["Contact"]] = relationship( + "Contact", back_populates="account" + ) tags: Mapped[list["Tag"]] = relationship("Tag", back_populates="account") - equipment: Mapped[list["Equipment"]] = relationship("Equipment", back_populates="account") - stock_locations: Mapped[list["StockLocation"]] = relationship("StockLocation", back_populates="account") - equipment_groups: Mapped[list["EquipmentGroup"]] = relationship("EquipmentGroup", back_populates="account") + equipment: Mapped[list["Equipment"]] = relationship( + "Equipment", back_populates="account" + ) + stock_locations: Mapped[list["StockLocation"]] = relationship( + "StockLocation", back_populates="account" + ) + equipment_groups: Mapped[list["EquipmentGroup"]] = relationship( + "EquipmentGroup", back_populates="account" + ) crew_members: Mapped[list["Crew"]] = relationship("Crew", back_populates="account") - vehicles: Mapped[list["Vehicle"]] = relationship("Vehicle", back_populates="account") - projects: Mapped[list["Project"]] = relationship("Project", back_populates="account") + vehicles: Mapped[list["Vehicle"]] = relationship( + "Vehicle", back_populates="account" + ) + projects: Mapped[list["Project"]] = relationship( + "Project", back_populates="account" + ) diff --git a/backend/app/models/crew.py b/backend/app/models/crew.py index cb8b510..e05bf45 100644 --- a/backend/app/models/crew.py +++ b/backend/app/models/crew.py @@ -63,9 +63,7 @@ class CrewAvailability(Base): start_date: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False ) - end_date: Mapped[datetime] = mapped_column( - DateTime(timezone=True), nullable=False - ) + end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) status: Mapped[str] = mapped_column( String(20), nullable=False, default="available" ) # available, booked, unavailable diff --git a/backend/app/models/equipment.py b/backend/app/models/equipment.py index 166b675..1b05d6d 100644 --- a/backend/app/models/equipment.py +++ b/backend/app/models/equipment.py @@ -3,7 +3,7 @@ import uuid from datetime import datetime -from sqlalchemy import String, DateTime, ForeignKey, func, Float, Integer, Text +from sqlalchemy import String, DateTime, ForeignKey, func, Float, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base @@ -23,7 +23,9 @@ class Equipment(Base): name: Mapped[str] = mapped_column(String(255), nullable=False) category: Mapped[str | None] = mapped_column(String(100), nullable=True) brand: Mapped[str | None] = mapped_column(String(255), nullable=True) - serial_number: Mapped[str | None] = mapped_column(String(100), nullable=True, unique=True) + serial_number: Mapped[str | None] = mapped_column( + String(100), nullable=True, unique=True + ) barcode: Mapped[str | None] = mapped_column(String(100), nullable=True, unique=True) qr_code: Mapped[str | None] = mapped_column(String(500), nullable=True) status: Mapped[str] = mapped_column( diff --git a/backend/app/models/equipment_group.py b/backend/app/models/equipment_group.py index b2ee56f..6b8e979 100644 --- a/backend/app/models/equipment_group.py +++ b/backend/app/models/equipment_group.py @@ -62,7 +62,9 @@ class EquipmentGroup(Base): ) # Relationships - account: Mapped["Account"] = relationship("Account", back_populates="equipment_groups") + account: Mapped["Account"] = relationship( + "Account", back_populates="equipment_groups" + ) default_location: Mapped["StockLocation"] = relationship( "StockLocation", back_populates="equipment_groups" ) diff --git a/backend/app/models/project.py b/backend/app/models/project.py index a7c967b..9f5dac3 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -111,8 +111,12 @@ class ProjectFunctionGroup(Base): String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False ) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) - start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) - end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + start_date: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + end_date: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) # Relationships project: Mapped["Project"] = relationship( @@ -140,7 +144,9 @@ class ProjectFunction(Base): ForeignKey("project_function_groups.id", ondelete="CASCADE"), nullable=False, ) - function_type: Mapped[str] = mapped_column(String(50), nullable=False, default='crew') + function_type: Mapped[str] = mapped_column( + String(50), nullable=False, default="crew" + ) quantity: Mapped[float] = mapped_column(Float, nullable=False, default=1.0) daily_costs: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) total_costs: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) @@ -171,11 +177,17 @@ class ProjectEquipmentGroup(Base): String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False ) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) - start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) - end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + start_date: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + end_date: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) # Relationships - project: Mapped["Project"] = relationship("Project", back_populates="equipment_groups") + project: Mapped["Project"] = relationship( + "Project", back_populates="equipment_groups" + ) items: Mapped[list["ProjectEquipment"]] = relationship( "ProjectEquipment", back_populates="group", cascade="all, delete-orphan" ) @@ -234,8 +246,12 @@ class ProjectCrew(Base): crew_id: Mapped[str] = mapped_column( String(36), ForeignKey("crew.id", ondelete="CASCADE"), nullable=False ) - start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) - end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + start_date: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + end_date: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) notes: Mapped[str | None] = mapped_column(Text, nullable=True) # Relationships diff --git a/backend/app/models/stock_location.py b/backend/app/models/stock_location.py index 04cc6f5..64e7027 100644 --- a/backend/app/models/stock_location.py +++ b/backend/app/models/stock_location.py @@ -35,7 +35,9 @@ class StockLocation(Base): ) # Relationships - account: Mapped["Account"] = relationship("Account", back_populates="stock_locations") + account: Mapped["Account"] = relationship( + "Account", back_populates="stock_locations" + ) equipment_items: Mapped[list["Equipment"]] = relationship( "Equipment", back_populates="location" ) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index ece1427..4d961ba 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -31,7 +31,10 @@ class User(Base): DateTime(timezone=True), server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, ) # Relationships diff --git a/backend/app/models/vehicle.py b/backend/app/models/vehicle.py index a65050c..d70cbfc 100644 --- a/backend/app/models/vehicle.py +++ b/backend/app/models/vehicle.py @@ -21,7 +21,9 @@ class Vehicle(Base): String(36), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False ) name: Mapped[str] = mapped_column(String(255), nullable=False) - license_plate: Mapped[str | None] = mapped_column(String(50), nullable=True, unique=True) + license_plate: Mapped[str | None] = mapped_column( + String(50), nullable=True, unique=True + ) brand: Mapped[str | None] = mapped_column(String(100), nullable=True) model: Mapped[str | None] = mapped_column(String(100), nullable=True) year: Mapped[int | None] = mapped_column(nullable=True) @@ -64,15 +66,11 @@ class VehicleAssignment(Base): vehicle_id: Mapped[str] = mapped_column( String(36), ForeignKey("vehicles.id", ondelete="CASCADE"), nullable=False ) - project_id: Mapped[str | None] = mapped_column( - String(36), nullable=True - ) + project_id: Mapped[str | None] = mapped_column(String(36), nullable=True) start_date: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False ) - end_date: Mapped[datetime] = mapped_column( - DateTime(timezone=True), nullable=False - ) + end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) status: Mapped[str] = mapped_column( String(20), nullable=False, default="assigned" ) # assigned, in_use, returned @@ -92,4 +90,6 @@ class VehicleAssignment(Base): vehicle: Mapped["Vehicle"] = relationship("Vehicle", back_populates="assignments") def __repr__(self) -> str: - return f"" + return ( + f"" + ) diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 3343325..3af58b9 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -5,25 +5,35 @@ from pydantic import BaseModel, EmailStr, Field class RegisterRequest(BaseModel): """Request body for self-service registration.""" - account_name: str = Field(..., min_length=2, max_length=255, description="Company/account name") - full_name: str = Field(..., min_length=2, max_length=255, description="Admin user's full name") + + account_name: str = Field( + ..., min_length=2, max_length=255, description="Company/account name" + ) + full_name: str = Field( + ..., min_length=2, max_length=255, description="Admin user's full name" + ) email: EmailStr = Field(..., description="Admin user's email address") - password: str = Field(..., min_length=8, max_length=128, description="Password (min 8 characters)") + password: str = Field( + ..., min_length=8, max_length=128, description="Password (min 8 characters)" + ) class LoginRequest(BaseModel): """Request body for login.""" + email: EmailStr password: str class RefreshRequest(BaseModel): """Request body for token refresh.""" + refresh_token: str class UserInfo(BaseModel): """Basic user info returned with tokens (no password hash).""" + id: str email: str full_name: str @@ -35,6 +45,7 @@ class UserInfo(BaseModel): class TokenResponse(BaseModel): """Response with access and refresh tokens, plus user info.""" + access_token: str refresh_token: str token_type: str = "bearer" @@ -43,6 +54,7 @@ class TokenResponse(BaseModel): class UserResponse(BaseModel): """Public user representation (no password hash).""" + id: str email: str full_name: str diff --git a/backend/app/schemas/contact.py b/backend/app/schemas/contact.py index c6c5652..35e8a8a 100644 --- a/backend/app/schemas/contact.py +++ b/backend/app/schemas/contact.py @@ -1,10 +1,11 @@ """Pydantic schemas for contacts and tags.""" -from pydantic import BaseModel, Field, EmailStr +from pydantic import BaseModel, Field class TagResponse(BaseModel): """Public tag representation.""" + id: str name: str color: str | None = None @@ -14,6 +15,7 @@ class TagResponse(BaseModel): class ContactCreateRequest(BaseModel): """Request body for creating a new contact.""" + type: str = Field(..., pattern="^(company|person)$") company_name: str | None = Field(None, max_length=255) first_name: str | None = Field(None, max_length=100) @@ -39,6 +41,7 @@ class ContactCreateRequest(BaseModel): class ContactUpdateRequest(BaseModel): """Request body for updating an existing contact.""" + type: str | None = Field(None, pattern="^(company|person)$") company_name: str | None = Field(None, max_length=255) first_name: str | None = Field(None, max_length=100) @@ -64,6 +67,7 @@ class ContactUpdateRequest(BaseModel): class ContactResponse(BaseModel): """Public contact representation with tags.""" + id: str account_id: str type: str @@ -95,6 +99,7 @@ class ContactResponse(BaseModel): class ContactListResponse(BaseModel): """Paginated list of contacts.""" + items: list[ContactResponse] total: int page: int diff --git a/backend/app/schemas/crew.py b/backend/app/schemas/crew.py index 079342c..e102372 100644 --- a/backend/app/schemas/crew.py +++ b/backend/app/schemas/crew.py @@ -1,11 +1,11 @@ """Pydantic schemas for Crew and CrewAvailability.""" -from datetime import datetime from pydantic import BaseModel, Field class CrewAvailabilityResponse(BaseModel): """Public crew availability representation.""" + id: str crew_id: str start_date: str @@ -20,6 +20,7 @@ class CrewAvailabilityResponse(BaseModel): class CrewCreateRequest(BaseModel): """Request body for creating a new crew member.""" + first_name: str = Field(..., max_length=100) last_name: str = Field(..., max_length=100) email: str | None = Field(None, max_length=255) @@ -32,6 +33,7 @@ class CrewCreateRequest(BaseModel): class CrewUpdateRequest(BaseModel): """Request body for updating a crew member.""" + first_name: str | None = Field(None, max_length=100) last_name: str | None = Field(None, max_length=100) email: str | None = Field(None, max_length=255) @@ -44,6 +46,7 @@ class CrewUpdateRequest(BaseModel): class CrewResponse(BaseModel): """Public crew member representation.""" + id: str account_id: str first_name: str @@ -63,6 +66,7 @@ class CrewResponse(BaseModel): class CrewListResponse(BaseModel): """Paginated list of crew members.""" + items: list[CrewResponse] total: int page: int @@ -71,6 +75,7 @@ class CrewListResponse(BaseModel): class CrewAvailabilityCreateRequest(BaseModel): """Request body for creating crew availability.""" + crew_id: str start_date: str = Field(..., description="ISO datetime with timezone") end_date: str = Field(..., description="ISO datetime with timezone") @@ -80,6 +85,7 @@ class CrewAvailabilityCreateRequest(BaseModel): class CrewAvailabilityUpdateRequest(BaseModel): """Request body for updating crew availability.""" + start_date: str | None = None end_date: str | None = None status: str | None = Field(None, max_length=20) @@ -88,6 +94,7 @@ class CrewAvailabilityUpdateRequest(BaseModel): class CrewAvailabilityListResponse(BaseModel): """Paginated list of crew availabilities.""" + items: list[CrewAvailabilityResponse] total: int page: int diff --git a/backend/app/schemas/equipment.py b/backend/app/schemas/equipment.py index bff0de7..7100c9c 100644 --- a/backend/app/schemas/equipment.py +++ b/backend/app/schemas/equipment.py @@ -1,11 +1,11 @@ """Pydantic schemas for Equipment (inventory catalog).""" -from datetime import datetime from pydantic import BaseModel, Field class EquipmentCreateRequest(BaseModel): """Request body for creating new equipment.""" + name: str = Field(..., max_length=255) category: str | None = Field(None, max_length=100) brand: str | None = Field(None, max_length=255) @@ -26,6 +26,7 @@ class EquipmentCreateRequest(BaseModel): class EquipmentUpdateRequest(BaseModel): """Request body for updating existing equipment.""" + name: str | None = Field(None, max_length=255) category: str | None = Field(None, max_length=100) brand: str | None = Field(None, max_length=255) @@ -46,6 +47,7 @@ class EquipmentUpdateRequest(BaseModel): class LocationRef(BaseModel): """Minimal stock location reference.""" + id: str name: str @@ -54,6 +56,7 @@ class LocationRef(BaseModel): class SupplierRef(BaseModel): """Minimal supplier (contact) reference.""" + id: str name: str | None = None company_name: str | None = None @@ -63,6 +66,7 @@ class SupplierRef(BaseModel): class EquipmentResponse(BaseModel): """Public equipment representation.""" + id: str account_id: str name: str @@ -89,6 +93,7 @@ class EquipmentResponse(BaseModel): class EquipmentListResponse(BaseModel): """Paginated list of equipment.""" + items: list[EquipmentResponse] total: int page: int diff --git a/backend/app/schemas/equipment_group.py b/backend/app/schemas/equipment_group.py index 03cde43..6073297 100644 --- a/backend/app/schemas/equipment_group.py +++ b/backend/app/schemas/equipment_group.py @@ -1,7 +1,6 @@ """Pydantic schemas for EquipmentGroup (Bundles).""" from __future__ import annotations -from datetime import datetime from typing import TYPE_CHECKING from pydantic import BaseModel, Field @@ -11,6 +10,7 @@ if TYPE_CHECKING: class EquipmentGroupItem(BaseModel): """An item inside a bundle with quantity.""" + equipment_id: str name: str | None = None quantity: float = 1.0 @@ -20,6 +20,7 @@ class EquipmentGroupItem(BaseModel): class EquipmentGroupCreateRequest(BaseModel): """Request body for creating a new equipment group/bundle.""" + name: str = Field(..., max_length=255) description: str | None = None daily_rate: float | None = None @@ -29,6 +30,7 @@ class EquipmentGroupCreateRequest(BaseModel): class EquipmentGroupUpdateRequest(BaseModel): """Request body for updating an equipment group.""" + name: str | None = Field(None, max_length=255) description: str | None = None daily_rate: float | None = None @@ -38,6 +40,7 @@ class EquipmentGroupUpdateRequest(BaseModel): class EquipmentGroupResponse(BaseModel): """Public equipment group representation.""" + id: str account_id: str name: str @@ -53,6 +56,7 @@ class EquipmentGroupResponse(BaseModel): class EquipmentGroupListResponse(BaseModel): """Paginated list of equipment groups.""" + items: list[EquipmentGroupResponse] total: int page: int diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py index 1d1c999..42d65dc 100644 --- a/backend/app/schemas/project.py +++ b/backend/app/schemas/project.py @@ -6,8 +6,10 @@ from pydantic import BaseModel, Field # ===== Project Schemas ===== + class ProjectCreateRequest(BaseModel): """Request body for creating a new project.""" + name: str = Field(..., max_length=255) description: str | None = None status: str = Field("draft", max_length=20) @@ -22,6 +24,7 @@ class ProjectCreateRequest(BaseModel): class ProjectUpdateRequest(BaseModel): """Request body for updating an existing project.""" + name: str | None = Field(None, max_length=255) description: str | None = None status: str | None = Field(None, max_length=20) @@ -36,6 +39,7 @@ class ProjectUpdateRequest(BaseModel): class ProjectResponse(BaseModel): """Public project representation.""" + id: str account_id: str name: str @@ -56,6 +60,7 @@ class ProjectResponse(BaseModel): class ProjectListResponse(BaseModel): """Paginated list of projects.""" + items: list[ProjectResponse] total: int page: int @@ -64,8 +69,10 @@ class ProjectListResponse(BaseModel): # ===== SubProject Schemas ===== + class SubProjectCreateRequest(BaseModel): """Request body for creating a subproject.""" + name: str = Field(..., max_length=255) parent_id: str | None = None sort_order: int = 0 @@ -73,6 +80,7 @@ class SubProjectCreateRequest(BaseModel): class SubProjectUpdateRequest(BaseModel): """Request body for updating a subproject.""" + name: str | None = Field(None, max_length=255) parent_id: str | None = None sort_order: int | None = None @@ -80,6 +88,7 @@ class SubProjectUpdateRequest(BaseModel): class SubProjectResponse(BaseModel): """Public subproject representation.""" + id: str name: str project_id: str @@ -91,6 +100,7 @@ class SubProjectResponse(BaseModel): class SubProjectListResponse(BaseModel): """Paginated list of subprojects.""" + items: list[SubProjectResponse] total: int page: int @@ -99,8 +109,10 @@ class SubProjectListResponse(BaseModel): # ===== ProjectFunctionGroup Schemas ===== + class ProjectFunctionGroupCreateRequest(BaseModel): """Request body for creating a function group.""" + name: str = Field(..., max_length=255) sort_order: int = 0 start_date: datetime | None = None @@ -109,6 +121,7 @@ class ProjectFunctionGroupCreateRequest(BaseModel): class ProjectFunctionGroupUpdateRequest(BaseModel): """Request body for updating a function group.""" + name: str | None = Field(None, max_length=255) sort_order: int | None = None start_date: datetime | None = None @@ -117,6 +130,7 @@ class ProjectFunctionGroupUpdateRequest(BaseModel): class ProjectFunctionGroupResponse(BaseModel): """Public function group representation.""" + id: str name: str project_id: str @@ -129,6 +143,7 @@ class ProjectFunctionGroupResponse(BaseModel): class ProjectFunctionGroupListResponse(BaseModel): """Paginated list of function groups.""" + items: list[ProjectFunctionGroupResponse] total: int page: int @@ -137,8 +152,10 @@ class ProjectFunctionGroupListResponse(BaseModel): # ===== ProjectFunction Schemas ===== + class ProjectFunctionCreateRequest(BaseModel): """Request body for creating a project function.""" + name: str = Field(..., max_length=255) function_type: str = "crew" quantity: float = 1.0 @@ -149,6 +166,7 @@ class ProjectFunctionCreateRequest(BaseModel): class ProjectFunctionUpdateRequest(BaseModel): """Request body for updating a project function.""" + name: str | None = Field(None, max_length=255) function_type: str | None = None quantity: float | None = None @@ -159,6 +177,7 @@ class ProjectFunctionUpdateRequest(BaseModel): class ProjectFunctionResponse(BaseModel): """Public project function representation.""" + id: str name: str function_group_id: str @@ -173,6 +192,7 @@ class ProjectFunctionResponse(BaseModel): class ProjectFunctionListResponse(BaseModel): """Paginated list of project functions.""" + items: list[ProjectFunctionResponse] total: int page: int @@ -181,8 +201,10 @@ class ProjectFunctionListResponse(BaseModel): # ===== ProjectEquipmentGroup Schemas ===== + class ProjectEquipmentGroupCreateRequest(BaseModel): """Request body for creating an equipment group.""" + name: str = Field(..., max_length=255) sort_order: int = 0 start_date: datetime | None = None @@ -191,6 +213,7 @@ class ProjectEquipmentGroupCreateRequest(BaseModel): class ProjectEquipmentGroupUpdateRequest(BaseModel): """Request body for updating an equipment group.""" + name: str | None = Field(None, max_length=255) sort_order: int | None = None start_date: datetime | None = None @@ -199,6 +222,7 @@ class ProjectEquipmentGroupUpdateRequest(BaseModel): class ProjectEquipmentGroupResponse(BaseModel): """Public equipment group representation.""" + id: str name: str project_id: str @@ -211,6 +235,7 @@ class ProjectEquipmentGroupResponse(BaseModel): class ProjectEquipmentGroupListResponse(BaseModel): """Paginated list of equipment groups.""" + items: list[ProjectEquipmentGroupResponse] total: int page: int @@ -219,6 +244,7 @@ class ProjectEquipmentGroupListResponse(BaseModel): class ProjectEquipmentGroupDetailResponse(BaseModel): """Equipment group with nested items.""" + id: str name: str project_id: str @@ -232,8 +258,10 @@ class ProjectEquipmentGroupDetailResponse(BaseModel): # ===== ProjectEquipment Schemas ===== + class ProjectEquipmentCreateRequest(BaseModel): """Request body for adding equipment to a project group.""" + equipment_id: str | None = None name: str = Field(..., max_length=255) quantity: float = 1.0 @@ -244,6 +272,7 @@ class ProjectEquipmentCreateRequest(BaseModel): class ProjectEquipmentUpdateRequest(BaseModel): """Request body for updating project equipment.""" + equipment_id: str | None = None name: str | None = Field(None, max_length=255) quantity: float | None = None @@ -254,6 +283,7 @@ class ProjectEquipmentUpdateRequest(BaseModel): class ProjectEquipmentResponse(BaseModel): """Public project equipment representation.""" + id: str project_equipment_group_id: str equipment_id: str | None = None @@ -268,6 +298,7 @@ class ProjectEquipmentResponse(BaseModel): class ProjectEquipmentListResponse(BaseModel): """Paginated list of project equipment.""" + items: list[ProjectEquipmentResponse] total: int page: int diff --git a/backend/app/schemas/role.py b/backend/app/schemas/role.py index 2e9a4dd..7a3ae19 100644 --- a/backend/app/schemas/role.py +++ b/backend/app/schemas/role.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field class RoleCreateRequest(BaseModel): """Request body for creating a new role.""" + name: str = Field(..., min_length=2, max_length=100) description: str | None = None permissions: list[str] = Field(default_factory=list) @@ -12,6 +13,7 @@ class RoleCreateRequest(BaseModel): class RoleUpdateRequest(BaseModel): """Request body for updating an existing role.""" + name: str | None = Field(None, min_length=2, max_length=100) description: str | None = None permissions: list[str] | None = None @@ -19,6 +21,7 @@ class RoleUpdateRequest(BaseModel): class RoleResponse(BaseModel): """Public role representation.""" + id: str account_id: str name: str diff --git a/backend/app/schemas/stock_location.py b/backend/app/schemas/stock_location.py index a0b46c7..4e9c067 100644 --- a/backend/app/schemas/stock_location.py +++ b/backend/app/schemas/stock_location.py @@ -1,11 +1,11 @@ """Pydantic schemas for StockLocation.""" -from datetime import datetime from pydantic import BaseModel, Field class StockLocationCreateRequest(BaseModel): """Request body for creating a new stock location.""" + name: str = Field(..., max_length=255) address: str | None = Field(None, max_length=500) is_default: bool = False @@ -13,6 +13,7 @@ class StockLocationCreateRequest(BaseModel): class StockLocationUpdateRequest(BaseModel): """Request body for updating a stock location.""" + name: str | None = Field(None, max_length=255) address: str | None = Field(None, max_length=500) is_default: bool | None = None @@ -20,6 +21,7 @@ class StockLocationUpdateRequest(BaseModel): class StockLocationResponse(BaseModel): """Public stock location representation.""" + id: str account_id: str name: str @@ -33,6 +35,7 @@ class StockLocationResponse(BaseModel): class StockLocationListResponse(BaseModel): """Paginated list of stock locations.""" + items: list[StockLocationResponse] total: int page: int diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 2856415..8cc297e 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, EmailStr, Field class UserCreateRequest(BaseModel): """Request body for creating a new user (invite).""" + email: EmailStr full_name: str = Field(..., min_length=2, max_length=255) password: str = Field(..., min_length=8, max_length=128) @@ -13,6 +14,7 @@ class UserCreateRequest(BaseModel): class UserUpdateRequest(BaseModel): """Request body for updating an existing user.""" + full_name: str | None = Field(None, min_length=2, max_length=255) role_id: str | None = None is_active: bool | None = None @@ -20,6 +22,7 @@ class UserUpdateRequest(BaseModel): class UserResponse(BaseModel): """Public user representation (no password hash).""" + id: str account_id: str email: str @@ -34,6 +37,7 @@ class UserResponse(BaseModel): class UserListResponse(BaseModel): """Paginated list of users.""" + items: list[UserResponse] total: int page: int diff --git a/backend/app/schemas/vehicle.py b/backend/app/schemas/vehicle.py index 01cd132..4fa3da4 100644 --- a/backend/app/schemas/vehicle.py +++ b/backend/app/schemas/vehicle.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field class VehicleAssignmentResponse(BaseModel): """Public vehicle assignment representation.""" + id: str vehicle_id: str project_id: str | None = None @@ -20,6 +21,7 @@ class VehicleAssignmentResponse(BaseModel): class VehicleCreateRequest(BaseModel): """Request body for creating a new vehicle.""" + name: str = Field(..., max_length=255) license_plate: str | None = Field(None, max_length=50) brand: str | None = Field(None, max_length=100) @@ -36,6 +38,7 @@ class VehicleCreateRequest(BaseModel): class VehicleUpdateRequest(BaseModel): """Request body for updating a vehicle.""" + name: str | None = Field(None, max_length=255) license_plate: str | None = Field(None, max_length=50) brand: str | None = Field(None, max_length=100) @@ -52,6 +55,7 @@ class VehicleUpdateRequest(BaseModel): class VehicleResponse(BaseModel): """Public vehicle representation.""" + id: str account_id: str name: str @@ -75,6 +79,7 @@ class VehicleResponse(BaseModel): class VehicleListResponse(BaseModel): """Paginated list of vehicles.""" + items: list[VehicleResponse] total: int page: int @@ -83,6 +88,7 @@ class VehicleListResponse(BaseModel): class VehicleAssignmentCreateRequest(BaseModel): """Request body for creating a vehicle assignment.""" + vehicle_id: str project_id: str | None = None start_date: str = Field(..., description="ISO datetime") @@ -93,6 +99,7 @@ class VehicleAssignmentCreateRequest(BaseModel): class VehicleAssignmentUpdateRequest(BaseModel): """Request body for updating a vehicle assignment.""" + start_date: str | None = None end_date: str | None = None status: str | None = Field(None, max_length=20) @@ -101,6 +108,7 @@ class VehicleAssignmentUpdateRequest(BaseModel): class VehicleAssignmentListResponse(BaseModel): """Paginated list of vehicle assignments.""" + items: list[VehicleAssignmentResponse] total: int page: int