[Verify+Complaints] Facility document review + facility complaints; card location line
CI/CD / CI · dotnet build (push) Successful in 1m27s
CI/CD / Deploy · hamkadr (push) Successful in 1m13s

Card: move location to its own line above the date in the shift card (job card already did). Verification workflow: employers upload documents (license/permit) on a new Employer/Verify page; uploading marks the facility Pending. Admins see pending facilities with their documents on Admin/Facilities, can download each doc, and approve (تأیید شد) or reject with a reason. Documents stored as bytea in the DB (survives deploys via the existing volume); served only to the owner or an admin via /facility-doc/{id}. Facility model gains Verification status enum + note + requested-at; IsVerified kept in sync. Complaints: registered users/visitors can file a شکایت about a facility from shift/job detail pages (targets ReportTargetType.Facility, surfaces in Admin/Reports as مرکز). Migration backfills existing verified facilities to Verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 16:26:15 +03:30
parent 962196d5cb
commit 1f34fd126f
16 changed files with 1632 additions and 24 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace JobsMedical.Web.Migrations
{
/// <inheritdoc />
public partial class FacilityVerificationDocs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Verification",
table: "Facilities",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "VerificationNote",
table: "Facilities",
type: "character varying(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "VerificationRequestedAt",
table: "Facilities",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.CreateTable(
name: "FacilityDocuments",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
FacilityId = table.Column<int>(type: "integer", nullable: false),
FileName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
ContentType = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Size = table.Column<long>(type: "bigint", nullable: false),
Data = table.Column<byte[]>(type: "bytea", nullable: false),
UploadedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FacilityDocuments", x => x.Id);
table.ForeignKey(
name: "FK_FacilityDocuments_Facilities_FacilityId",
column: x => x.FacilityId,
principalTable: "Facilities",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_FacilityDocuments_FacilityId",
table: "FacilityDocuments",
column: "FacilityId");
// Backfill: already-verified facilities get Verification = Verified (2).
migrationBuilder.Sql("UPDATE \"Facilities\" SET \"Verification\" = 2 WHERE \"IsVerified\" = true;");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "FacilityDocuments");
migrationBuilder.DropColumn(
name: "Verification",
table: "Facilities");
migrationBuilder.DropColumn(
name: "VerificationNote",
table: "Facilities");
migrationBuilder.DropColumn(
name: "VerificationRequestedAt",
table: "Facilities");
}
}
}
@@ -337,6 +337,16 @@ namespace JobsMedical.Web.Migrations
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<int>("Verification")
.HasColumnType("integer");
b.Property<string>("VerificationNote")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTime?>("VerificationRequestedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CityId");
@@ -348,6 +358,44 @@ namespace JobsMedical.Web.Migrations
b.ToTable("Facilities");
});
modelBuilder.Entity("JobsMedical.Web.Models.FacilityDocument", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.Property<byte[]>("Data")
.IsRequired()
.HasColumnType("bytea");
b.Property<int>("FacilityId")
.HasColumnType("integer");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<long>("Size")
.HasColumnType("bigint");
b.Property<DateTime>("UploadedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("FacilityId");
b.ToTable("FacilityDocuments");
});
modelBuilder.Entity("JobsMedical.Web.Models.InterestEvent", b =>
{
b.Property<long>("Id")
@@ -902,6 +950,17 @@ namespace JobsMedical.Web.Migrations
b.Navigation("OwnerUser");
});
modelBuilder.Entity("JobsMedical.Web.Models.FacilityDocument", b =>
{
b.HasOne("JobsMedical.Web.Models.Facility", "Facility")
.WithMany("Documents")
.HasForeignKey("FacilityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Facility");
});
modelBuilder.Entity("JobsMedical.Web.Models.InterestEvent", b =>
{
b.HasOne("JobsMedical.Web.Models.JobOpening", "JobOpening")
@@ -1030,6 +1089,8 @@ namespace JobsMedical.Web.Migrations
modelBuilder.Entity("JobsMedical.Web.Models.Facility", b =>
{
b.Navigation("Documents");
b.Navigation("Shifts");
});