Add gender requirement (آقا/خانم/فرقی نمیکند) + employee (کارجو) panel
- Gender enum + GenderRequirement on Shift/JobOpening + Gender on UserPreferences (migration) - Employer PostShift/PostJob + admin Review have a gender select; parser detects آقا/خانم/مرد/زن - Gender badge on cards + detail; gender filter on Shifts/Jobs; gender in preferences - Recommendations exclude listings whose gender requirement conflicts with the person's gender - Two panels: new /Me employee (کارجو) panel (recommendations + saved + applied + prefs) alongside /Employer; nav routes by role; /Account/Profile → /Me Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -148,6 +148,8 @@ public static class SeedData
|
||||
PayType = payType,
|
||||
PayAmount = amount,
|
||||
SharePercent = share,
|
||||
GenderRequirement = role.Name == "ماما" ? Gender.Female
|
||||
: rng.Next(0, 4) == 0 ? (Gender)rng.Next(1, 3) : Gender.Any,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Admin,
|
||||
});
|
||||
@@ -170,7 +172,7 @@ public static class SeedData
|
||||
Status = ShiftStatus.Open },
|
||||
new JobOpening { FacilityId = facilities[2].Id, RoleId = roles[3].Id,
|
||||
Title = "ماما جهت کلینیک زنان", EmploymentType = EmploymentType.PartTime,
|
||||
SalaryMin = null, SalaryMax = null,
|
||||
SalaryMin = null, SalaryMax = null, GenderRequirement = Gender.Female,
|
||||
Description = "همکاری پارهوقت ماما در کلینیک تخصصی زنان و زایمان.",
|
||||
Status = ShiftStatus.Open },
|
||||
new JobOpening { FacilityId = facilities[4].Id, RoleId = roles[4].Id,
|
||||
|
||||
+842
@@ -0,0 +1,842 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using JobsMedical.Web.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260603204652_GenderRequirement")]
|
||||
partial class GenderRequirement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.AppSetting", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AiApiKey")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<bool>("AiAutoApprove")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("AiEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("AiEndpoint")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("AiModel")
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("AiSystemPrompt")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<int>("AutoPublishMinConfidence")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Mode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Application", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("DoctorId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("ShiftId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DoctorId");
|
||||
|
||||
b.HasIndex("ShiftId", "DoctorId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Applications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.City", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Province")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Cities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.District", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("CityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CityId");
|
||||
|
||||
b.ToTable("Districts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.DoctorProfile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int?>("CityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsVerified")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LicenseNo")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<int?>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Specialty")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("YearsExperience")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CityId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DoctorProfiles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Facility", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("BaleId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<int>("CityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int?>("DistrictId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsVerified")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<double?>("Lat")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<double?>("Lng")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int?>("OwnerUserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CityId");
|
||||
|
||||
b.HasIndex("DistrictId");
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Facilities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.InterestEvent", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("JobOpeningId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("ShiftId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("VisitorId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("JobOpeningId");
|
||||
|
||||
b.HasIndex("ShiftId");
|
||||
|
||||
b.HasIndex("VisitorId", "CreatedAt");
|
||||
|
||||
b.ToTable("InterestEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.JobOpening", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<int>("EmploymentType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("FacilityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("GenderRequirement")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Requirements")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("SalaryMax")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("SalaryMin")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FacilityId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("JobOpenings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("Confidence")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTime>("FetchedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int?>("LinkedShiftId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ParsedJson")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RawText")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceChannel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ValidationNotes")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentHash");
|
||||
|
||||
b.HasIndex("LinkedShiftId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("RawListings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Role", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Roles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Shift", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateOnly>("Date")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1500)
|
||||
.HasColumnType("character varying(1500)");
|
||||
|
||||
b.Property<TimeOnly>("EndTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<int>("FacilityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("GenderRequirement")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("SharePercent")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ShiftType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("SpecialtyRequired")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FacilityId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Date", "Status");
|
||||
|
||||
b.ToTable("Shifts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("FullName")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<bool>("IsPhoneVerified")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Phone")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.UserPreferences", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("CityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Gender")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("MinPay")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int?>("PreferredShiftType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("VisitorId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CityId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("VisitorId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserPreferences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Visitor", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(36)
|
||||
.HasColumnType("character varying(36)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("LastSeenAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int?>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Visitors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Application", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.User", "Doctor")
|
||||
.WithMany("Applications")
|
||||
.HasForeignKey("DoctorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Shift", "Shift")
|
||||
.WithMany("Applications")
|
||||
.HasForeignKey("ShiftId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Doctor");
|
||||
|
||||
b.Navigation("Shift");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.District", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.City", "City")
|
||||
.WithMany()
|
||||
.HasForeignKey("CityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("City");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.DoctorProfile", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.City", "City")
|
||||
.WithMany()
|
||||
.HasForeignKey("CityId");
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId");
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.User", "User")
|
||||
.WithOne("DoctorProfile")
|
||||
.HasForeignKey("JobsMedical.Web.Models.DoctorProfile", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("City");
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Facility", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.City", "City")
|
||||
.WithMany("Facilities")
|
||||
.HasForeignKey("CityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.District", "District")
|
||||
.WithMany("Facilities")
|
||||
.HasForeignKey("DistrictId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.User", "OwnerUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("City");
|
||||
|
||||
b.Navigation("District");
|
||||
|
||||
b.Navigation("OwnerUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.InterestEvent", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.JobOpening", "JobOpening")
|
||||
.WithMany()
|
||||
.HasForeignKey("JobOpeningId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Shift", "Shift")
|
||||
.WithMany()
|
||||
.HasForeignKey("ShiftId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Visitor", "Visitor")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("VisitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("JobOpening");
|
||||
|
||||
b.Navigation("Shift");
|
||||
|
||||
b.Navigation("Visitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.JobOpening", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.Facility", "Facility")
|
||||
.WithMany()
|
||||
.HasForeignKey("FacilityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Facility");
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.Shift", "LinkedShift")
|
||||
.WithMany()
|
||||
.HasForeignKey("LinkedShiftId");
|
||||
|
||||
b.Navigation("LinkedShift");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Shift", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.Facility", "Facility")
|
||||
.WithMany("Shifts")
|
||||
.HasForeignKey("FacilityId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Role", "Role")
|
||||
.WithMany("Shifts")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Facility");
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.UserPreferences", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.City", "City")
|
||||
.WithMany()
|
||||
.HasForeignKey("CityId");
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId");
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Visitor", "Visitor")
|
||||
.WithOne("Preferences")
|
||||
.HasForeignKey("JobsMedical.Web.Models.UserPreferences", "VisitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("City");
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("Visitor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Visitor", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.City", b =>
|
||||
{
|
||||
b.Navigation("Facilities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.District", b =>
|
||||
{
|
||||
b.Navigation("Facilities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Facility", b =>
|
||||
{
|
||||
b.Navigation("Shifts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Role", b =>
|
||||
{
|
||||
b.Navigation("Shifts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Shift", b =>
|
||||
{
|
||||
b.Navigation("Applications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.User", b =>
|
||||
{
|
||||
b.Navigation("Applications");
|
||||
|
||||
b.Navigation("DoctorProfile");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Visitor", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("Preferences");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class GenderRequirement : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Gender",
|
||||
table: "UserPreferences",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "GenderRequirement",
|
||||
table: "Shifts",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "GenderRequirement",
|
||||
table: "JobOpenings",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Gender",
|
||||
table: "UserPreferences");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GenderRequirement",
|
||||
table: "Shifts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GenderRequirement",
|
||||
table: "JobOpenings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,6 +317,9 @@ namespace JobsMedical.Web.Migrations
|
||||
b.Property<int>("FacilityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("GenderRequirement")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Requirements")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
@@ -464,6 +467,9 @@ namespace JobsMedical.Web.Migrations
|
||||
b.Property<int>("FacilityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("GenderRequirement")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
@@ -553,6 +559,9 @@ namespace JobsMedical.Web.Migrations
|
||||
b.Property<int?>("CityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Gender")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long?>("MinPay")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
|
||||
@@ -76,6 +76,17 @@ public enum ListingKind
|
||||
Job = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gender. On a listing it's the requirement (Any = فرقی نمیکند); on a person it's their gender
|
||||
/// (Any = unspecified).
|
||||
/// </summary>
|
||||
public enum Gender
|
||||
{
|
||||
Any = 0, // فرقی نمیکند / نامشخص
|
||||
Male = 1, // آقا
|
||||
Female = 2 // خانم
|
||||
}
|
||||
|
||||
/// <summary>How ingested listings get onto the site.</summary>
|
||||
public enum IngestionMode
|
||||
{
|
||||
|
||||
@@ -32,6 +32,8 @@ public class JobOpening
|
||||
[MaxLength(1000)]
|
||||
public string? Requirements { get; set; } // شرایط احراز
|
||||
|
||||
public Gender GenderRequirement { get; set; } = Gender.Any; // جنسیت مورد نیاز
|
||||
|
||||
public ShiftStatus Status { get; set; } = ShiftStatus.Open;
|
||||
public ShiftSource Source { get; set; } = ShiftSource.Admin;
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ public class Shift
|
||||
public int? SharePercent { get; set; } // سهم درآمد (٪)؛ مثلاً ۵۰. میتواند همراه مبلغ هم باشد
|
||||
public PayType PayType { get; set; } = PayType.PerShift;
|
||||
|
||||
public Gender GenderRequirement { get; set; } = Gender.Any; // جنسیت مورد نیاز
|
||||
|
||||
[MaxLength(1500)]
|
||||
public string? Description { get; set; } // توضیحات
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public class UserPreferences
|
||||
|
||||
public ShiftType? PreferredShiftType { get; set; } // نوع شیفت ترجیحی
|
||||
public long? MinPay { get; set; } // حداقل حقوق مورد انتظار (تومان)
|
||||
public Gender Gender { get; set; } = Gender.Any; // جنسیت کارجو (برای تطبیق با نیاز آگهی)
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
|
||||
@@ -1,77 +1,3 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Account.ProfileModel
|
||||
@{
|
||||
ViewData["Title"] = "پروفایل من";
|
||||
string RoleLabel(UserRole r) => r switch
|
||||
{
|
||||
UserRole.Admin => "مدیر",
|
||||
UserRole.FacilityAdmin => "مدیر مرکز درمانی",
|
||||
_ => "کادر درمان",
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>پروفایل من</h1>
|
||||
<p class="muted">
|
||||
📱 <span dir="ltr">@JalaliDate.ToPersianDigits(Model.CurrentUser?.Phone ?? "")</span>
|
||||
— @RoleLabel(Model.CurrentUser?.Role ?? UserRole.Doctor)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="rec-banner">
|
||||
<div>
|
||||
<h2 style="margin:0 0 4px;">علاقهمندیهایت را کامل کن</h2>
|
||||
<span style="opacity:.9; font-size:14px;">تا پیشنهادهای دقیقتری بگیری</span>
|
||||
</div>
|
||||
<a class="btn btn-outline" asp-page="/Preferences/Index">تنظیم علاقهمندیها</a>
|
||||
</div>
|
||||
|
||||
@if (User.IsInRole("FacilityAdmin") || User.IsInRole("Admin"))
|
||||
{
|
||||
<p><a asp-page="/Employer/Index">→ ورود به پنل کارفرما</a></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">مرکز درمانی هستی و میخواهی شیفت یا استخدام منتشر کنی؟
|
||||
<a asp-page="/Employer/RegisterFacility">مرکز خود را ثبت کن</a></p>
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px;">شیفتهای ذخیرهشده</h2>
|
||||
@if (Model.SavedShifts.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز شیفتی ذخیره نکردهای.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var s in Model.SavedShifts) { <partial name="_ShiftCard" model="s" /> }
|
||||
</div>
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px; margin-top:32px;">شیفتهایی که اعلام تمایل کردی</h2>
|
||||
@if (Model.AppliedShifts.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز برای شیفتی اعلام تمایل نکردهای.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var s in Model.AppliedShifts) { <partial name="_ShiftCard" model="s" /> }
|
||||
</div>
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px; margin-top:32px;">موقعیتهای استخدامی که اعلام تمایل کردی</h2>
|
||||
@if (Model.AppliedJobs.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز برای موقعیتی اعلام تمایل نکردهای.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var j in Model.AppliedJobs) { <partial name="_JobCard" model="j" /> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@* Redirects to /Me (employee panel). *@
|
||||
|
||||
@@ -1,53 +1,12 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Account;
|
||||
|
||||
// Profile was merged into the employee panel (/Me). Keep the old URL working.
|
||||
[Authorize]
|
||||
public class ProfileModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public ProfileModel(AppDbContext db) => _db = db;
|
||||
|
||||
public User? CurrentUser { get; private set; }
|
||||
public List<Shift> SavedShifts { get; private set; } = new();
|
||||
public List<JobOpening> AppliedJobs { get; private set; } = new();
|
||||
public List<Shift> AppliedShifts { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
CurrentUser = await _db.Users.FindAsync(userId);
|
||||
|
||||
// All visitor ids this account has been linked to (across devices).
|
||||
var visitorIds = await _db.Visitors.Where(v => v.UserId == userId).Select(v => v.Id).ToListAsync();
|
||||
|
||||
var events = await _db.InterestEvents
|
||||
.Where(e => visitorIds.Contains(e.VisitorId))
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
var savedShiftIds = events.Where(e => e.EventType == InterestEventType.Save && e.ShiftId != null)
|
||||
.Select(e => e.ShiftId!.Value).Distinct().ToList();
|
||||
var appliedShiftIds = events.Where(e => e.EventType == InterestEventType.Apply && e.ShiftId != null)
|
||||
.Select(e => e.ShiftId!.Value).Distinct().ToList();
|
||||
var appliedJobIds = events.Where(e => e.EventType == InterestEventType.Apply && e.JobOpeningId != null)
|
||||
.Select(e => e.JobOpeningId!.Value).Distinct().ToList();
|
||||
|
||||
SavedShifts = await ShiftsByIds(savedShiftIds);
|
||||
AppliedShifts = await ShiftsByIds(appliedShiftIds);
|
||||
AppliedJobs = await _db.JobOpenings
|
||||
.Include(j => j.Facility).ThenInclude(f => f.City).Include(j => j.Role)
|
||||
.Where(j => appliedJobIds.Contains(j.Id)).ToListAsync();
|
||||
}
|
||||
|
||||
private Task<List<Shift>> ShiftsByIds(List<int> ids) => _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Facility).ThenInclude(f => f.District)
|
||||
.Include(s => s.Role)
|
||||
.Where(s => ids.Contains(s.Id)).ToListAsync();
|
||||
public IActionResult OnGet() => RedirectToPage("/Me/Index");
|
||||
}
|
||||
|
||||
@@ -63,6 +63,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>جنسیت مورد نیاز</label>
|
||||
<select name="GenderRequirement">
|
||||
<option value="0" selected="@(Model.GenderRequirement == JobsMedical.Web.Models.Gender.Any)">فرقی نمیکند</option>
|
||||
<option value="1" selected="@(Model.GenderRequirement == JobsMedical.Web.Models.Gender.Male)">آقا</option>
|
||||
<option value="2" selected="@(Model.GenderRequirement == JobsMedical.Web.Models.Gender.Female)">خانم</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="shiftFields">
|
||||
<div class="filter-group">
|
||||
<label>تاریخ شیفت (میلادی)</label>
|
||||
|
||||
@@ -38,6 +38,7 @@ public class ReviewModel : PageModel
|
||||
[BindProperty] public long? PayAmount { get; set; }
|
||||
[BindProperty] public int? SharePercent { get; set; }
|
||||
[BindProperty] public bool Negotiable { get; set; }
|
||||
[BindProperty] public Gender GenderRequirement { get; set; }
|
||||
// Job fields
|
||||
[BindProperty] public string? Title { get; set; }
|
||||
[BindProperty] public EmploymentType EmploymentType { get; set; }
|
||||
@@ -62,6 +63,7 @@ public class ReviewModel : PageModel
|
||||
ShiftDate = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1);
|
||||
Negotiable = Parsed.PayNegotiable;
|
||||
SharePercent = Parsed.SharePercent;
|
||||
GenderRequirement = Parsed.Gender;
|
||||
if (Parsed.PayAmount is not null) { PayAmount = Parsed.PayAmount; SalaryMin = Parsed.PayAmount; }
|
||||
Description = Raw.RawText;
|
||||
Title = Parsed.RoleName is not null ? $"استخدام {Parsed.RoleName}" : "موقعیت استخدامی";
|
||||
@@ -90,6 +92,7 @@ public class ReviewModel : PageModel
|
||||
: (PayAmount is null && SharePercent is not null ? PayType.Percentage : PayType.PerShift),
|
||||
PayAmount = Negotiable ? null : PayAmount,
|
||||
SharePercent = Negotiable ? null : SharePercent,
|
||||
GenderRequirement = GenderRequirement,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Aggregated,
|
||||
SourceUrl = Raw.SourceUrl,
|
||||
@@ -109,6 +112,7 @@ public class ReviewModel : PageModel
|
||||
EmploymentType = EmploymentType,
|
||||
SalaryMin = Negotiable ? null : SalaryMin,
|
||||
SalaryMax = Negotiable ? null : SalaryMax,
|
||||
GenderRequirement = GenderRequirement,
|
||||
Description = Description,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Aggregated,
|
||||
|
||||
@@ -43,6 +43,14 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>جنسیت مورد نیاز</label>
|
||||
<select name="GenderRequirement">
|
||||
<option value="0">فرقی نمیکند</option>
|
||||
<option value="1">آقا</option>
|
||||
<option value="2">خانم</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع همکاری</label>
|
||||
<select name="EmploymentType">
|
||||
|
||||
@@ -25,6 +25,7 @@ public class PostJobModel : PageModel
|
||||
[BindProperty] public long? SalaryMin { get; set; }
|
||||
[BindProperty] public long? SalaryMax { get; set; }
|
||||
[BindProperty] public bool Negotiable { get; set; }
|
||||
[BindProperty] public Gender GenderRequirement { get; set; }
|
||||
[BindProperty] public string? Description { get; set; }
|
||||
[BindProperty] public string? Requirements { get; set; }
|
||||
|
||||
@@ -48,6 +49,7 @@ public class PostJobModel : PageModel
|
||||
EmploymentType = EmploymentType,
|
||||
SalaryMin = Negotiable ? null : SalaryMin,
|
||||
SalaryMax = Negotiable ? null : SalaryMax,
|
||||
GenderRequirement = GenderRequirement,
|
||||
Description = Description,
|
||||
Requirements = Requirements,
|
||||
Status = ShiftStatus.Open,
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>جنسیت مورد نیاز</label>
|
||||
<select name="GenderRequirement">
|
||||
<option value="0">فرقی نمیکند</option>
|
||||
<option value="1">آقا</option>
|
||||
<option value="2">خانم</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>تاریخ (میلادی)</label>
|
||||
<input type="date" name="Date" value="@Model.Date.ToString("yyyy-MM-dd")" dir="ltr" />
|
||||
|
||||
@@ -27,6 +27,7 @@ public class PostShiftModel : PageModel
|
||||
[BindProperty] public long? PayAmount { get; set; }
|
||||
[BindProperty] public int? SharePercent { get; set; } // سهم درآمد (٪)
|
||||
[BindProperty] public bool Negotiable { get; set; }
|
||||
[BindProperty] public Gender GenderRequirement { get; set; }
|
||||
[BindProperty] public string? Description { get; set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
@@ -60,6 +61,7 @@ public class PostShiftModel : PageModel
|
||||
: (PayAmount is null && SharePercent is not null ? PayType.Percentage : PayType.PerShift),
|
||||
PayAmount = Negotiable ? null : PayAmount,
|
||||
SharePercent = Negotiable ? null : SharePercent,
|
||||
GenderRequirement = GenderRequirement,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Direct, // posted directly by the facility
|
||||
});
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
<h3 style="margin-top:0;">مشخصات موقعیت</h3>
|
||||
<div class="info-row"><span class="k">نوع همکاری</span><span class="v">@empLabel</span></div>
|
||||
<div class="info-row"><span class="k">نقش</span><span class="v">@j.Role?.Name</span></div>
|
||||
@if (j.GenderRequirement != Gender.Any)
|
||||
{
|
||||
<div class="info-row"><span class="k">جنسیت</span><span class="v">@JalaliDate.GenderLabel(j.GenderRequirement)</span></div>
|
||||
}
|
||||
<div class="info-row"><span class="k">حقوق ماهانه</span><span class="v" style="color:var(--primary-dark)">@salary</span></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>جنسیت</label>
|
||||
<select name="GenderFilter" onchange="this.form.submit()">
|
||||
<option value="">فرقی نمیکند</option>
|
||||
<option value="1" selected="@(Model.GenderFilter == JobsMedical.Web.Models.Gender.Male)">آقا</option>
|
||||
<option value="2" selected="@(Model.GenderFilter == JobsMedical.Web.Models.Gender.Female)">خانم</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع همکاری</label>
|
||||
<select name="EmploymentType" onchange="this.form.submit()">
|
||||
|
||||
@@ -16,6 +16,7 @@ public class IndexModel : PageModel
|
||||
[BindProperty(SupportsGet = true)] public int? DistrictId { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public int? RoleId { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public EmploymentType? EmploymentType { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public Gender? GenderFilter { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public double? Lat { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public double? Lng { get; set; }
|
||||
|
||||
@@ -44,6 +45,8 @@ public class IndexModel : PageModel
|
||||
if (DistrictId is not null) q = q.Where(j => j.Facility.DistrictId == DistrictId);
|
||||
if (RoleId is not null) q = q.Where(j => j.RoleId == RoleId);
|
||||
if (EmploymentType is not null) q = q.Where(j => j.EmploymentType == EmploymentType);
|
||||
if (GenderFilter is Gender g && g != Gender.Any)
|
||||
q = q.Where(j => j.GenderRequirement == Gender.Any || j.GenderRequirement == g);
|
||||
|
||||
var results = await q.ToListAsync();
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Me.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "پنل کارجو";
|
||||
string RoleLabel(UserRole r) => r switch
|
||||
{
|
||||
UserRole.Admin => "مدیر",
|
||||
UserRole.FacilityAdmin => "کارفرما",
|
||||
_ => "کارجو (کادر درمان)",
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>پنل کارجو</h1>
|
||||
<p class="muted">
|
||||
📱 <span dir="ltr">@JalaliDate.ToPersianDigits(Model.CurrentUser?.Phone ?? "")</span>
|
||||
— @RoleLabel(Model.CurrentUser?.Role ?? UserRole.Doctor)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="rec-banner">
|
||||
<div>
|
||||
<h2 style="margin:0 0 4px;">✨ پیشنهادهای ویژه شما</h2>
|
||||
<span style="opacity:.9; font-size:14px;">
|
||||
@if (Model.Prefs?.HasAny == true)
|
||||
{
|
||||
<text>بر اساس نقش، شهر، نوع شیفت و جنسیت شما</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>علاقهمندیهایت را تنظیم کن تا دقیقتر شوند</text>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<a class="btn btn-outline" asp-page="/Preferences/Index">تنظیم علاقهمندیها</a>
|
||||
</div>
|
||||
|
||||
@if (Model.Recommendations.Count > 0)
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var rec in Model.Recommendations)
|
||||
{
|
||||
<partial name="_RecommendationCard" model="rec" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card empty-state">فعلاً پیشنهادی نیست. <a asp-page="/Shifts/Index">جستجوی شیفتها</a></div>
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px; margin-top:32px;">شیفتهای ذخیرهشده</h2>
|
||||
@if (Model.SavedShifts.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز شیفتی ذخیره نکردهای.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">@foreach (var s in Model.SavedShifts) { <partial name="_ShiftCard" model="s" /> }</div>
|
||||
}
|
||||
|
||||
<h2 style="font-size:20px; margin-top:32px;">اعلام تمایلهای شما</h2>
|
||||
@if (Model.AppliedShifts.Count == 0 && Model.AppliedJobs.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">هنوز برای فرصتی اعلام تمایل نکردهای.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var s in Model.AppliedShifts) { <partial name="_ShiftCard" model="s" /> }
|
||||
@foreach (var j in Model.AppliedJobs) { <partial name="_JobCard" model="j" /> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Me;
|
||||
|
||||
/// <summary>The employee / job-seeker (کارجو) panel — the staff-side counterpart to /Employer.</summary>
|
||||
[Authorize]
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly RecommendationService _recs;
|
||||
private readonly InterestService _interest;
|
||||
|
||||
public IndexModel(AppDbContext db, RecommendationService recs, InterestService interest)
|
||||
{
|
||||
_db = db;
|
||||
_recs = recs;
|
||||
_interest = interest;
|
||||
}
|
||||
|
||||
public User? CurrentUser { get; private set; }
|
||||
public UserPreferences? Prefs { get; private set; }
|
||||
public List<Recommendation> Recommendations { get; private set; } = new();
|
||||
public List<Shift> SavedShifts { get; private set; } = new();
|
||||
public List<Shift> AppliedShifts { get; private set; } = new();
|
||||
public List<JobOpening> AppliedJobs { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
CurrentUser = await _db.Users.FindAsync(userId);
|
||||
Prefs = await _interest.GetPreferencesAsync();
|
||||
Recommendations = await _recs.GetForVisitorAsync(6);
|
||||
|
||||
var visitorIds = await _db.Visitors.Where(v => v.UserId == userId).Select(v => v.Id).ToListAsync();
|
||||
var events = await _db.InterestEvents.Where(e => visitorIds.Contains(e.VisitorId)).ToListAsync();
|
||||
|
||||
var savedShiftIds = events.Where(e => e.EventType == InterestEventType.Save && e.ShiftId != null)
|
||||
.Select(e => e.ShiftId!.Value).Distinct().ToList();
|
||||
var appliedShiftIds = events.Where(e => e.EventType == InterestEventType.Apply && e.ShiftId != null)
|
||||
.Select(e => e.ShiftId!.Value).Distinct().ToList();
|
||||
var appliedJobIds = events.Where(e => e.EventType == InterestEventType.Apply && e.JobOpeningId != null)
|
||||
.Select(e => e.JobOpeningId!.Value).Distinct().ToList();
|
||||
|
||||
SavedShifts = await ShiftsByIds(savedShiftIds);
|
||||
AppliedShifts = await ShiftsByIds(appliedShiftIds);
|
||||
AppliedJobs = await _db.JobOpenings
|
||||
.Include(j => j.Facility).ThenInclude(f => f.City).Include(j => j.Role)
|
||||
.Where(j => appliedJobIds.Contains(j.Id)).ToListAsync();
|
||||
}
|
||||
|
||||
private Task<List<Shift>> ShiftsByIds(List<int> ids) => _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Facility).ThenInclude(f => f.District)
|
||||
.Include(s => s.Role)
|
||||
.Where(s => ids.Contains(s.Id)).ToListAsync();
|
||||
}
|
||||
@@ -35,6 +35,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>جنسیت شما</label>
|
||||
<select name="Gender">
|
||||
<option value="0" selected="@(Model.Gender == JobsMedical.Web.Models.Gender.Any)">نمیخواهم بگویم</option>
|
||||
<option value="1" selected="@(Model.Gender == JobsMedical.Web.Models.Gender.Male)">آقا</option>
|
||||
<option value="2" selected="@(Model.Gender == JobsMedical.Web.Models.Gender.Female)">خانم</option>
|
||||
</select>
|
||||
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای حذف آگهیهایی که با جنسیت شما همخوان نیستند.</p>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع شیفت ترجیحی</label>
|
||||
<select name="PreferredShiftType">
|
||||
|
||||
@@ -25,6 +25,7 @@ public class IndexModel : PageModel
|
||||
[BindProperty] public int? CityId { get; set; }
|
||||
[BindProperty] public ShiftType? PreferredShiftType { get; set; }
|
||||
[BindProperty] public long? MinPay { get; set; }
|
||||
[BindProperty] public Gender Gender { get; set; }
|
||||
|
||||
public bool Saved { get; private set; }
|
||||
|
||||
@@ -38,12 +39,13 @@ public class IndexModel : PageModel
|
||||
CityId = prefs.CityId;
|
||||
PreferredShiftType = prefs.PreferredShiftType;
|
||||
MinPay = prefs.MinPay;
|
||||
Gender = prefs.Gender;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await _interest.SavePreferencesAsync(RoleId, CityId, PreferredShiftType, MinPay);
|
||||
await _interest.SavePreferencesAsync(RoleId, CityId, PreferredShiftType, MinPay, Gender);
|
||||
// Back to home so the personalized feed is the immediate payoff.
|
||||
TempData["prefsSaved"] = true;
|
||||
return RedirectToPage("/Index");
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
{
|
||||
<span class="badge badge-type">@Model.Role.Name</span>
|
||||
}
|
||||
@if (Model.GenderRequirement != Gender.Any)
|
||||
{
|
||||
<span class="badge badge-gender">@JalaliDate.GenderLabel(Model.GenderRequirement)</span>
|
||||
}
|
||||
<span>🏥 @Model.Facility?.Name</span>
|
||||
</div>
|
||||
<div class="row">📍 @Model.Facility?.City?.Name@(Model.Facility?.District is not null ? "، " + Model.Facility.District.Name : "")</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
{
|
||||
<a asp-page="/Employer/Index" style="margin-inline-end:14px; font-weight:600;">پنل کارفرما</a>
|
||||
}
|
||||
<a asp-page="/Account/Profile" style="margin-inline-end:10px; font-weight:600;">پروفایل</a>
|
||||
<a asp-page="/Me/Index" style="margin-inline-end:10px; font-weight:600;">پنل کارجو</a>
|
||||
<form method="post" asp-page="/Account/Logout" style="display:inline;">
|
||||
<button type="submit" class="btn btn-outline" style="padding:7px 14px;">خروج</button>
|
||||
</form>
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
{
|
||||
<span class="badge badge-type">@s.Role.Name</span>
|
||||
}
|
||||
@if (s.GenderRequirement != Gender.Any)
|
||||
{
|
||||
<span class="badge badge-gender">@JalaliDate.GenderLabel(s.GenderRequirement)</span>
|
||||
}
|
||||
<span>📍 @s.Facility?.City?.Name</span>
|
||||
</div>
|
||||
<div class="row">📅 @JalaliDate.WeekDayName(s.Date)، @JalaliDate.ToLongDate(s.Date) — 🕐 @JalaliDate.Time(s.StartTime)</div>
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
{
|
||||
<span class="badge badge-type">@Model.Role.Name</span>
|
||||
}
|
||||
@if (Model.GenderRequirement != Gender.Any)
|
||||
{
|
||||
<span class="badge badge-gender">@JalaliDate.GenderLabel(Model.GenderRequirement)</span>
|
||||
}
|
||||
<span>📍 @Model.Facility?.City?.Name@(Model.Facility?.District is not null ? "، " + Model.Facility.District.Name : "")</span>
|
||||
@if (Model.Facility?.IsVerified == true)
|
||||
{
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
<div class="info-row"><span class="k">ساعت</span><span class="v">@JalaliDate.Time(s.StartTime) تا @JalaliDate.Time(s.EndTime)</span></div>
|
||||
<div class="info-row"><span class="k">مدت</span><span class="v">@JalaliDate.ToPersianDigits(s.DurationHours.ToString("0.#")) ساعت</span></div>
|
||||
<div class="info-row"><span class="k">نقش مورد نیاز</span><span class="v">@(s.Role?.Name ?? s.SpecialtyRequired)</span></div>
|
||||
@if (s.GenderRequirement != Gender.Any)
|
||||
{
|
||||
<div class="info-row"><span class="k">جنسیت</span><span class="v">@JalaliDate.GenderLabel(s.GenderRequirement)</span></div>
|
||||
}
|
||||
<div class="info-row"><span class="k">پرداخت</span><span class="v" style="color:var(--primary-dark)">@JalaliDate.PayLabel(s.PayType, s.PayAmount, s.SharePercent)</span></div>
|
||||
<div style="padding-top:12px;">
|
||||
<span class="k" style="font-size:13px; color:var(--muted);">بازه ساعت کاری در شبانهروز</span>
|
||||
|
||||
@@ -78,6 +78,14 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>جنسیت</label>
|
||||
<select name="GenderFilter" onchange="this.form.submit()">
|
||||
<option value="">فرقی نمیکند</option>
|
||||
<option value="1" selected="@(Model.GenderFilter == JobsMedical.Web.Models.Gender.Male)">آقا</option>
|
||||
<option value="2" selected="@(Model.GenderFilter == JobsMedical.Web.Models.Gender.Female)">خانم</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع شیفت</label>
|
||||
<select name="ShiftType" onchange="this.form.submit()">
|
||||
|
||||
@@ -19,6 +19,7 @@ public class IndexModel : PageModel
|
||||
[BindProperty(SupportsGet = true)] public ShiftType? ShiftType { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public bool PaidOnly { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public bool ShareOnly { get; set; } // فقط شیفتهای سهم درآمد
|
||||
[BindProperty(SupportsGet = true)] public Gender? GenderFilter { get; set; }
|
||||
|
||||
// "Near me": the browser sends the visitor's coordinates and we sort by distance.
|
||||
[BindProperty(SupportsGet = true)] public double? Lat { get; set; }
|
||||
@@ -58,6 +59,9 @@ public class IndexModel : PageModel
|
||||
if (ShiftType is not null) q = q.Where(s => s.ShiftType == ShiftType);
|
||||
if (PaidOnly) q = q.Where(s => s.PayAmount != null);
|
||||
if (ShareOnly) q = q.Where(s => s.SharePercent != null);
|
||||
// A given gender sees listings open to them (their gender or "فرقی نمیکند").
|
||||
if (GenderFilter is Gender g && g != Gender.Any)
|
||||
q = q.Where(s => s.GenderRequirement == Gender.Any || s.GenderRequirement == g);
|
||||
|
||||
var results = await q.ToListAsync();
|
||||
|
||||
|
||||
@@ -71,7 +71,8 @@ public class InterestService
|
||||
return _db.UserPreferences.AsNoTracking().FirstOrDefaultAsync(p => p.VisitorId == id);
|
||||
}
|
||||
|
||||
public async Task SavePreferencesAsync(int? roleId, int? cityId, ShiftType? shiftType, long? minPay)
|
||||
public async Task SavePreferencesAsync(int? roleId, int? cityId, ShiftType? shiftType, long? minPay,
|
||||
Gender gender = Gender.Any)
|
||||
{
|
||||
await EnsureVisitorAsync();
|
||||
var prefs = await _db.UserPreferences.FirstOrDefaultAsync(p => p.VisitorId == VisitorId);
|
||||
@@ -84,6 +85,7 @@ public class InterestService
|
||||
prefs.CityId = cityId;
|
||||
prefs.PreferredShiftType = shiftType;
|
||||
prefs.MinPay = minPay;
|
||||
prefs.Gender = gender;
|
||||
prefs.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@@ -107,4 +107,12 @@ public static class JalaliDate
|
||||
if (parts.Count > 1) return string.Join(" یا ", parts) + " (به انتخاب شما)";
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
/// <summary>Gender label. As a listing requirement, Any → "فرقی نمیکند".</summary>
|
||||
public static string GenderLabel(Gender g, bool asRequirement = true) => g switch
|
||||
{
|
||||
Gender.Male => "آقا",
|
||||
Gender.Female => "خانم",
|
||||
_ => asRequirement ? "فرقی نمیکند" : "نامشخص",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public class ParsedListing
|
||||
public long? PayAmount { get; set; } // shift pay or single salary figure
|
||||
public int? SharePercent { get; set; } // profit-share % (درصدی / سهم درآمد)
|
||||
public bool PayNegotiable { get; set; }
|
||||
public Gender Gender { get; set; } = Gender.Any; // جنسیت مورد نیاز
|
||||
public string? CityName { get; set; }
|
||||
public string? DistrictName { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
@@ -65,6 +66,12 @@ public class HeuristicListingParser : IListingParser
|
||||
else if (text.Contains("قرارداد")) p.EmploymentType = Models.EmploymentType.Contract;
|
||||
else if (ContainsAny(text, "تمام وقت", "تماموقت")) p.EmploymentType = Models.EmploymentType.FullTime;
|
||||
|
||||
// --- Gender requirement ---
|
||||
if (ContainsAny(text, "خانم", "خانوم", "بانو", "زن ", "مامای")) p.Gender = Gender.Female;
|
||||
else if (ContainsAny(text, "آقا", "اقا", "مرد ", "مرد،", "پسر")) p.Gender = Gender.Male;
|
||||
if (p.Gender != Gender.Any)
|
||||
p.Notes.Add($"جنسیت: {(p.Gender == Gender.Female ? "خانم" : "آقا")}");
|
||||
|
||||
// --- City / district ---
|
||||
p.CityName = knownCities.FirstOrDefault(c => text.Contains(Normalize(c)));
|
||||
p.DistrictName = knownDistricts.OrderByDescending(d => d.Length)
|
||||
|
||||
@@ -79,6 +79,11 @@ public class RecommendationService
|
||||
var results = new List<Recommendation>();
|
||||
foreach (var s in candidates)
|
||||
{
|
||||
// Skip listings whose gender requirement conflicts with the person's gender.
|
||||
if (prefs?.Gender is Gender pg && pg != Gender.Any
|
||||
&& s.GenderRequirement != Gender.Any && s.GenderRequirement != pg)
|
||||
continue;
|
||||
|
||||
double score = 0;
|
||||
var reasons = new List<string>();
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ label { font-size: 13px; }
|
||||
.badge-type { background: #eef3f6; color: var(--muted); }
|
||||
.badge-distance { background: #fff1e8; color: #c5670f; }
|
||||
.badge-job { background: #eaf3ff; color: #2563eb; }
|
||||
.badge-gender { background: #f3eefb; color: #6b3fa0; }
|
||||
|
||||
/* ---------- Filters layout ---------- */
|
||||
.layout-2 { display: grid; grid-template-columns: 270px 1fr; gap: 24px; align-items: start; }
|
||||
|
||||
Reference in New Issue
Block a user