Add «آماده به کار» (talent) listing type — workers offering themselves
Adds a third listing kind alongside Shift/Job for healthcare staff who advertise their own availability (very common in Iranian medical channels, e.g. "دندانپزشک آماده همکاری… ۵۰٪ تسویه"). These have no facility; the contact phone is the key field. - Model: TalentListing (role, person name, years, licensed, city/district, area note, availability, gender, comp, phone) + ListingKind.Talent + RawListing.LinkedTalentId + DbSet/relations/indexes + EF migration. - Parser: detect آمادهبهکار/جویای کار → Kind=Talent; extract person name, years of experience, licensed flag, area («منطقه ۱»), phone. Facility name extraction now skipped for talent. - Validator: talent path scores role + phone + medical (no facility/pay required). - Ingestion auto-publish: creates a TalentListing for talent kind. - Review (manual publish): Talent option + talent fields; publishes a TalentListing without a facility. Shift/Job facility now falls back to a shared «نامشخص / ثبت نشده» record when the ad names none — publishing never fails on a missing facility. - Browse /Talent (indexable, filters: city/district/role/gender), details /Talent/Details (noindex — personal contact, tel: call button), _TalentCard, badge-talent, nav link, home section. - Sitemap includes /Talent; robots disallows /Talent/Details. Archiver expires stale talent listings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1473
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTalentListing : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "LinkedTalentId",
|
||||
table: "RawListings",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TalentListings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RoleId = table.Column<int>(type: "integer", nullable: false),
|
||||
PersonName = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: true),
|
||||
YearsExperience = table.Column<int>(type: "integer", nullable: true),
|
||||
IsLicensed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CityId = table.Column<int>(type: "integer", nullable: false),
|
||||
DistrictId = table.Column<int>(type: "integer", nullable: true),
|
||||
AreaNote = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: true),
|
||||
Availability = table.Column<int>(type: "integer", nullable: true),
|
||||
Gender = table.Column<int>(type: "integer", nullable: false),
|
||||
PayType = table.Column<int>(type: "integer", nullable: false),
|
||||
PayAmount = table.Column<long>(type: "bigint", nullable: true),
|
||||
SharePercent = table.Column<int>(type: "integer", nullable: true),
|
||||
Phone = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: true),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
Source = table.Column<int>(type: "integer", nullable: false),
|
||||
SourceUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TalentListings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TalentListings_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_TalentListings_Districts_DistrictId",
|
||||
column: x => x.DistrictId,
|
||||
principalTable: "Districts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_TalentListings_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TalentListings_CityId_RoleId",
|
||||
table: "TalentListings",
|
||||
columns: new[] { "CityId", "RoleId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TalentListings_DistrictId",
|
||||
table: "TalentListings",
|
||||
column: "DistrictId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TalentListings_RoleId",
|
||||
table: "TalentListings",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TalentListings_Status",
|
||||
table: "TalentListings",
|
||||
column: "Status");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TalentListings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LinkedTalentId",
|
||||
table: "RawListings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,6 +673,9 @@ namespace JobsMedical.Web.Migrations
|
||||
b.Property<int?>("LinkedShiftId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("LinkedTalentId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ParsedJson")
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -887,6 +890,86 @@ namespace JobsMedical.Web.Migrations
|
||||
b.ToTable("Shifts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.TalentListing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AreaNote")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<int?>("Availability")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int?>("DistrictId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Gender")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsLicensed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("PersonName")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("SharePercent")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("YearsExperience")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DistrictId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("CityId", "RoleId");
|
||||
|
||||
b.ToTable("TalentListings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1282,6 +1365,32 @@ namespace JobsMedical.Web.Migrations
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.TalentListing", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.City", "City")
|
||||
.WithMany()
|
||||
.HasForeignKey("CityId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.District", "District")
|
||||
.WithMany()
|
||||
.HasForeignKey("DistrictId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("City");
|
||||
|
||||
b.Navigation("District");
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.UserPreferences", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.City", "City")
|
||||
|
||||
Reference in New Issue
Block a user