Initial commit — Hamkadr (همکادر) healthcare-staffing marketplace
ASP.NET Core 10 Razor Pages + PostgreSQL/EF Core. RTL Persian, Jalali dates, self-hosted Vazirmatn, teal/coral brand. Features: - Shift listings: browse/filter (city, district, role, type, pay), weekly Jalali calendar, detail + interest handoff, near-me distance sort - Hiring (استخدام) listings with employment type + salary range - Pattern-engine recommendations + anonymous interest tracking (visitor cookie) - Heuristic Persian listing-parser + admin queue (raw channel post → shift/job) - Phone-OTP cookie auth + visitor-history linking + profile Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<City> Cities => Set<City>();
|
||||
public DbSet<District> Districts => Set<District>();
|
||||
public DbSet<Role> Roles => Set<Role>();
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<DoctorProfile> DoctorProfiles => Set<DoctorProfile>();
|
||||
public DbSet<Facility> Facilities => Set<Facility>();
|
||||
public DbSet<Shift> Shifts => Set<Shift>();
|
||||
public DbSet<JobOpening> JobOpenings => Set<JobOpening>();
|
||||
public DbSet<Application> Applications => Set<Application>();
|
||||
public DbSet<RawListing> RawListings => Set<RawListing>();
|
||||
public DbSet<Visitor> Visitors => Set<Visitor>();
|
||||
public DbSet<UserPreferences> UserPreferences => Set<UserPreferences>();
|
||||
public DbSet<InterestEvent> InterestEvents => Set<InterestEvent>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder b)
|
||||
{
|
||||
base.OnModelCreating(b);
|
||||
|
||||
// Phone is the unique identity.
|
||||
b.Entity<User>().HasIndex(u => u.Phone).IsUnique();
|
||||
|
||||
// One-to-one User <-> DoctorProfile.
|
||||
b.Entity<DoctorProfile>()
|
||||
.HasOne(d => d.User)
|
||||
.WithOne(u => u.DoctorProfile)
|
||||
.HasForeignKey<DoctorProfile>(d => d.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// A doctor can apply to a shift only once.
|
||||
b.Entity<Application>()
|
||||
.HasIndex(a => new { a.ShiftId, a.DoctorId })
|
||||
.IsUnique();
|
||||
|
||||
b.Entity<Application>()
|
||||
.HasOne(a => a.Doctor)
|
||||
.WithMany(u => u.Applications)
|
||||
.HasForeignKey(a => a.DoctorId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Entity<Application>()
|
||||
.HasOne(a => a.Shift)
|
||||
.WithMany(s => s.Applications)
|
||||
.HasForeignKey(a => a.ShiftId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Facility owner is optional (Phase 2 self-serve); don't cascade-delete users.
|
||||
b.Entity<Facility>()
|
||||
.HasOne(f => f.OwnerUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(f => f.OwnerUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Common query indexes: filtering shifts by city/date/status happens constantly.
|
||||
b.Entity<Shift>().HasIndex(s => new { s.Date, s.Status });
|
||||
b.Entity<Shift>().HasIndex(s => s.FacilityId);
|
||||
b.Entity<Shift>().HasIndex(s => s.RoleId);
|
||||
b.Entity<Facility>().HasIndex(f => f.CityId);
|
||||
b.Entity<Facility>().HasIndex(f => f.DistrictId);
|
||||
|
||||
b.Entity<District>()
|
||||
.HasOne(d => d.City).WithMany()
|
||||
.HasForeignKey(d => d.CityId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.Entity<Facility>()
|
||||
.HasOne(f => f.District).WithMany(d => d.Facilities)
|
||||
.HasForeignKey(f => f.DistrictId).OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Don't delete shifts/profiles just because a Role is removed.
|
||||
b.Entity<Shift>()
|
||||
.HasOne(s => s.Role).WithMany(r => r.Shifts)
|
||||
.HasForeignKey(s => s.RoleId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Visitor identity + behavioral tracking.
|
||||
b.Entity<Visitor>()
|
||||
.HasOne(v => v.User).WithMany()
|
||||
.HasForeignKey(v => v.UserId).OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Entity<UserPreferences>()
|
||||
.HasOne(p => p.Visitor).WithOne(v => v.Preferences)
|
||||
.HasForeignKey<UserPreferences>(p => p.VisitorId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Entity<InterestEvent>()
|
||||
.HasOne(e => e.Visitor).WithMany(v => v.Events)
|
||||
.HasForeignKey(e => e.VisitorId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.Entity<InterestEvent>()
|
||||
.HasOne(e => e.Shift).WithMany()
|
||||
.HasForeignKey(e => e.ShiftId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.Entity<InterestEvent>()
|
||||
.HasOne(e => e.JobOpening).WithMany()
|
||||
.HasForeignKey(e => e.JobOpeningId).OnDelete(DeleteBehavior.Cascade);
|
||||
// Reading "this visitor's recent events" is the hot path for recommendations.
|
||||
b.Entity<InterestEvent>().HasIndex(e => new { e.VisitorId, e.CreatedAt });
|
||||
|
||||
// Job openings: same query patterns as shifts.
|
||||
b.Entity<JobOpening>()
|
||||
.HasOne(j => j.Role).WithMany()
|
||||
.HasForeignKey(j => j.RoleId).OnDelete(DeleteBehavior.Restrict);
|
||||
b.Entity<JobOpening>()
|
||||
.HasOne(j => j.Facility).WithMany()
|
||||
.HasForeignKey(j => j.FacilityId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.Entity<JobOpening>().HasIndex(j => j.Status);
|
||||
b.Entity<JobOpening>().HasIndex(j => j.FacilityId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Seeds a believable Tehran-focused board so the marketplace doesn't look empty on first run
|
||||
/// (the cold-start problem). Idempotent: only seeds when the DB is empty.
|
||||
/// </summary>
|
||||
public static class SeedData
|
||||
{
|
||||
public static async Task EnsureSeededAsync(AppDbContext db)
|
||||
{
|
||||
if (await db.Cities.AnyAsync()) return;
|
||||
|
||||
var tehran = new City { Name = "تهران", Province = "تهران", IsActive = true };
|
||||
var cities = new[]
|
||||
{
|
||||
tehran,
|
||||
new City { Name = "کرج", Province = "البرز", IsActive = false },
|
||||
new City { Name = "مشهد", Province = "خراسان رضوی", IsActive = false },
|
||||
new City { Name = "اصفهان", Province = "اصفهان", IsActive = false },
|
||||
new City { Name = "شیراز", Province = "فارس", IsActive = false },
|
||||
};
|
||||
db.Cities.AddRange(cities);
|
||||
await db.SaveChangesAsync(); // need tehran.Id before districts reference it
|
||||
|
||||
var roles = new[]
|
||||
{
|
||||
new Role { Name = "پزشک عمومی", Category = "پزشک", SortOrder = 1 },
|
||||
new Role { Name = "پزشک متخصص", Category = "پزشک", SortOrder = 2 },
|
||||
new Role { Name = "پرستار", Category = "پرستار", SortOrder = 3 },
|
||||
new Role { Name = "ماما", Category = "ماما", SortOrder = 4 },
|
||||
new Role { Name = "تکنسین اتاق عمل", Category = "تکنسین", SortOrder = 5 },
|
||||
new Role { Name = "تکنسین فوریتهای پزشکی", Category = "تکنسین", SortOrder = 6 },
|
||||
new Role { Name = "کارشناس آزمایشگاه", Category = "تکنسین", SortOrder = 7 },
|
||||
};
|
||||
db.Roles.AddRange(roles);
|
||||
|
||||
// Tehran neighborhoods (محله/منطقه) for the within-city filter.
|
||||
var saadatAbad = new District { Name = "سعادتآباد", CityId = tehran.Id };
|
||||
var shahrakGharb = new District { Name = "شهرک غرب", CityId = tehran.Id };
|
||||
var valiasr = new District { Name = "ولیعصر / پارکوی", CityId = tehran.Id };
|
||||
var narmak = new District { Name = "نارمک", CityId = tehran.Id };
|
||||
var tehranpars = new District { Name = "تهرانپارس", CityId = tehran.Id };
|
||||
var gisha = new District { Name = "گیشا / برج میلاد", CityId = tehran.Id };
|
||||
db.Districts.AddRange(saadatAbad, shahrakGharb, valiasr, narmak, tehranpars, gisha,
|
||||
new District { Name = "ونک", CityId = tehran.Id },
|
||||
new District { Name = "تجریش", CityId = tehran.Id });
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var facilities = new[]
|
||||
{
|
||||
new Facility { Name = "بیمارستان میلاد", Type = FacilityType.Hospital, CityId = tehran.Id,
|
||||
DistrictId = gisha.Id,
|
||||
Address = "تهران، بزرگراه همت، روبهروی برج میلاد", Phone = "021-82032000",
|
||||
Lat = 35.7448, Lng = 51.3753, IsVerified = true },
|
||||
new Facility { Name = "بیمارستان دی", Type = FacilityType.Hospital, CityId = tehran.Id,
|
||||
DistrictId = valiasr.Id,
|
||||
Address = "تهران، خیابان ولیعصر، بالاتر از پارکوی", Phone = "021-23601",
|
||||
Lat = 35.7986, Lng = 51.4087, IsVerified = true },
|
||||
new Facility { Name = "کلینیک تخصصی پارسیان", Type = FacilityType.Clinic, CityId = tehran.Id,
|
||||
DistrictId = saadatAbad.Id,
|
||||
Address = "تهران، سعادتآباد، میدان کاج", Phone = "021-22360000",
|
||||
Lat = 35.7872, Lng = 51.3760, IsVerified = false },
|
||||
new Facility { Name = "درمانگاه شبانهروزی البرز", Type = FacilityType.Polyclinic, CityId = tehran.Id,
|
||||
DistrictId = narmak.Id,
|
||||
Address = "تهران، نارمک، میدان هلال احمر", Phone = "021-77900000",
|
||||
Lat = 35.7448, Lng = 51.5085, IsVerified = true },
|
||||
new Facility { Name = "بیمارستان آتیه", Type = FacilityType.Hospital, CityId = tehran.Id,
|
||||
DistrictId = shahrakGharb.Id,
|
||||
Address = "تهران، شهرک غرب، بلوار فرحزادی", Phone = "021-82721",
|
||||
Lat = 35.7570, Lng = 51.3680, IsVerified = true },
|
||||
new Facility { Name = "کلینیک درمانی مهر", Type = FacilityType.Clinic, CityId = tehran.Id,
|
||||
DistrictId = tehranpars.Id,
|
||||
Address = "تهران، تهرانپارس، فلکه دوم", Phone = "021-77700000",
|
||||
Lat = 35.7350, Lng = 51.5400, IsVerified = false },
|
||||
};
|
||||
db.Facilities.AddRange(facilities);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Build ~2 weeks of shifts starting today, a few per facility per day, across roles.
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var rng = new Random(20260602); // deterministic seed for reproducible sample data
|
||||
var shifts = new List<Shift>();
|
||||
|
||||
// Weighted role pool — GP and nurse most common, others sprinkled in.
|
||||
var rolePool = new[]
|
||||
{
|
||||
roles[0], roles[0], roles[0], // پزشک عمومی (most common)
|
||||
roles[2], roles[2], // پرستار
|
||||
roles[1], // پزشک متخصص
|
||||
roles[3], // ماما
|
||||
roles[4], roles[5], roles[6], // تکنسینها
|
||||
};
|
||||
|
||||
var templates = new[]
|
||||
{
|
||||
(ShiftType.Day, new TimeOnly(8, 0), new TimeOnly(14, 0), "شیفت صبح", 1_500_000L),
|
||||
(ShiftType.Evening, new TimeOnly(14, 0), new TimeOnly(20, 0), "شیفت عصر", 1_800_000L),
|
||||
(ShiftType.Night, new TimeOnly(20, 0), new TimeOnly(8, 0), "شیفت شب", 2_500_000L),
|
||||
(ShiftType.OnCall, new TimeOnly(8, 0), new TimeOnly(8, 0), "آنکال", 0L),
|
||||
};
|
||||
|
||||
foreach (var f in facilities)
|
||||
{
|
||||
for (var d = 0; d < 14; d++)
|
||||
{
|
||||
var date = today.AddDays(d);
|
||||
var count = rng.Next(0, 3); // 0–2 shifts per facility per day
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var t = templates[rng.Next(templates.Length)];
|
||||
var negotiable = rng.Next(0, 4) == 0;
|
||||
var role = rolePool[rng.Next(rolePool.Length)];
|
||||
shifts.Add(new Shift
|
||||
{
|
||||
FacilityId = f.Id,
|
||||
RoleId = role.Id,
|
||||
Date = date,
|
||||
StartTime = t.Item2,
|
||||
EndTime = t.Item3,
|
||||
ShiftType = t.Item1,
|
||||
SpecialtyRequired = role.Name,
|
||||
Description = $"{t.Item4} - نیازمند {role.Name} مسلط به امور درمانگاه/اورژانس",
|
||||
PayType = t.Item1 == ShiftType.OnCall || negotiable ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = t.Item1 == ShiftType.OnCall || negotiable ? null : t.Item5,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Admin,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
db.Shifts.AddRange(shifts);
|
||||
|
||||
// Permanent hiring openings (استخدام) — the hiring side of the marketplace.
|
||||
db.JobOpenings.AddRange(
|
||||
new JobOpening { FacilityId = facilities[0].Id, RoleId = roles[2].Id,
|
||||
Title = "استخدام پرستار بخش اورژانس", EmploymentType = EmploymentType.FullTime,
|
||||
SalaryMin = 18_000_000, SalaryMax = 25_000_000,
|
||||
Description = "استخدام تماموقت پرستار جهت بخش اورژانس با سابقه کار.",
|
||||
Requirements = "حداقل ۲ سال سابقه، مسلط به ICU", Status = ShiftStatus.Open },
|
||||
new JobOpening { FacilityId = facilities[1].Id, RoleId = roles[0].Id,
|
||||
Title = "پزشک عمومی مقیم", EmploymentType = EmploymentType.Contract,
|
||||
SalaryMin = 40_000_000, SalaryMax = 55_000_000,
|
||||
Description = "پزشک عمومی مقیم جهت بیمارستان، شیفتهای چرخشی.",
|
||||
Status = ShiftStatus.Open },
|
||||
new JobOpening { FacilityId = facilities[2].Id, RoleId = roles[3].Id,
|
||||
Title = "ماما جهت کلینیک زنان", EmploymentType = EmploymentType.PartTime,
|
||||
SalaryMin = null, SalaryMax = null,
|
||||
Description = "همکاری پارهوقت ماما در کلینیک تخصصی زنان و زایمان.",
|
||||
Status = ShiftStatus.Open },
|
||||
new JobOpening { FacilityId = facilities[4].Id, RoleId = roles[4].Id,
|
||||
Title = "تکنسین اتاق عمل", EmploymentType = EmploymentType.FullTime,
|
||||
SalaryMin = 16_000_000, SalaryMax = 22_000_000,
|
||||
Description = "تکنسین اتاق عمل جهت بیمارستان، تماموقت با بیمه.",
|
||||
Requirements = "مدرک تکنسین اتاق عمل، آشنا به ابزار جراحی", Status = ShiftStatus.Open },
|
||||
new JobOpening { FacilityId = facilities[3].Id, RoleId = roles[0].Id,
|
||||
Title = "پزشک عمومی طرح", EmploymentType = EmploymentType.Plan,
|
||||
SalaryMin = 30_000_000, SalaryMax = 30_000_000,
|
||||
Description = "جذب پزشک عمومی جهت گذراندن طرح در درمانگاه شبانهروزی.",
|
||||
Status = ShiftStatus.Open });
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// A couple of raw listings waiting in the admin normalization queue.
|
||||
db.RawListings.AddRange(
|
||||
new RawListing
|
||||
{
|
||||
SourceChannel = "کانال شیفت پزشکان تهران",
|
||||
RawText = "نیازمند پزشک عمومی جهت شیفت شب درمانگاه در منطقه غرب تهران، کارانه توافقی. تماس: ۰۹۱۲xxxxxxx",
|
||||
Status = RawListingStatus.New,
|
||||
},
|
||||
new RawListing
|
||||
{
|
||||
SourceChannel = "Divar - استخدام پزشک",
|
||||
RawText = "بیمارستان خصوصی جهت تکمیل کادر درمان به پزشک عمومی برای شیفتهای روز نیازمند است.",
|
||||
Status = RawListingStatus.New,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,418 @@
|
||||
// <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("20260602202926_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <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.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.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<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("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<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("OwnerUserId");
|
||||
|
||||
b.ToTable("Facilities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
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.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedShiftId");
|
||||
|
||||
b.ToTable("RawListings");
|
||||
});
|
||||
|
||||
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<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PayType")
|
||||
.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("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.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.DoctorProfile", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.City", "City")
|
||||
.WithMany()
|
||||
.HasForeignKey("CityId");
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.User", "User")
|
||||
.WithOne("DoctorProfile")
|
||||
.HasForeignKey("JobsMedical.Web.Models.DoctorProfile", "UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("City");
|
||||
|
||||
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.User", "OwnerUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("City");
|
||||
|
||||
b.Navigation("OwnerUser");
|
||||
});
|
||||
|
||||
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.Navigation("Facility");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.City", b =>
|
||||
{
|
||||
b.Navigation("Facilities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.Facility", 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");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Cities",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Province = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
IsActive = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Cities", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
FullName = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: true),
|
||||
Role = table.Column<int>(type: "integer", nullable: false),
|
||||
IsPhoneVerified = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DoctorProfiles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<int>(type: "integer", nullable: false),
|
||||
LicenseNo = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||
Specialty = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
CityId = table.Column<int>(type: "integer", nullable: true),
|
||||
YearsExperience = table.Column<int>(type: "integer", nullable: false),
|
||||
Bio = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
IsVerified = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DoctorProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DoctorProfiles_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_DoctorProfiles_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Facilities",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
CityId = table.Column<int>(type: "integer", nullable: false),
|
||||
Address = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Lat = table.Column<double>(type: "double precision", nullable: true),
|
||||
Lng = table.Column<double>(type: "double precision", nullable: true),
|
||||
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||
BaleId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
IsVerified = table.Column<bool>(type: "boolean", nullable: false),
|
||||
OwnerUserId = table.Column<int>(type: "integer", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Facilities", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Facilities_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Facilities_Users_OwnerUserId",
|
||||
column: x => x.OwnerUserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Shifts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
FacilityId = table.Column<int>(type: "integer", nullable: false),
|
||||
Date = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
StartTime = table.Column<TimeOnly>(type: "time without time zone", nullable: false),
|
||||
EndTime = table.Column<TimeOnly>(type: "time without time zone", nullable: false),
|
||||
SpecialtyRequired = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
ShiftType = table.Column<int>(type: "integer", nullable: false),
|
||||
PayAmount = table.Column<long>(type: "bigint", nullable: true),
|
||||
PayType = table.Column<int>(type: "integer", nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(1500)", maxLength: 1500, 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_Shifts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Shifts_Facilities_FacilityId",
|
||||
column: x => x.FacilityId,
|
||||
principalTable: "Facilities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Applications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ShiftId = table.Column<int>(type: "integer", nullable: false),
|
||||
DoctorId = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
Message = 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_Applications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Applications_Shifts_ShiftId",
|
||||
column: x => x.ShiftId,
|
||||
principalTable: "Shifts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Applications_Users_DoctorId",
|
||||
column: x => x.DoctorId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RawListings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
SourceChannel = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
RawText = table.Column<string>(type: "text", nullable: false),
|
||||
ParsedJson = table.Column<string>(type: "text", nullable: true),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
LinkedShiftId = table.Column<int>(type: "integer", nullable: true),
|
||||
SourceUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
FetchedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RawListings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RawListings_Shifts_LinkedShiftId",
|
||||
column: x => x.LinkedShiftId,
|
||||
principalTable: "Shifts",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Applications_DoctorId",
|
||||
table: "Applications",
|
||||
column: "DoctorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Applications_ShiftId_DoctorId",
|
||||
table: "Applications",
|
||||
columns: new[] { "ShiftId", "DoctorId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DoctorProfiles_CityId",
|
||||
table: "DoctorProfiles",
|
||||
column: "CityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DoctorProfiles_UserId",
|
||||
table: "DoctorProfiles",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Facilities_CityId",
|
||||
table: "Facilities",
|
||||
column: "CityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Facilities_OwnerUserId",
|
||||
table: "Facilities",
|
||||
column: "OwnerUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RawListings_LinkedShiftId",
|
||||
table: "RawListings",
|
||||
column: "LinkedShiftId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Shifts_Date_Status",
|
||||
table: "Shifts",
|
||||
columns: new[] { "Date", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Shifts_FacilityId",
|
||||
table: "Shifts",
|
||||
column: "FacilityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Users_Phone",
|
||||
table: "Users",
|
||||
column: "Phone",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Applications");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DoctorProfiles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RawListings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Shifts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Facilities");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Cities");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
+626
@@ -0,0 +1,626 @@
|
||||
// <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("20260602211515_AddRolesAndInterestTracking")]
|
||||
partial class AddRolesAndInterestTracking
|
||||
{
|
||||
/// <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.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.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<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("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>("ShiftId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("VisitorId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShiftId");
|
||||
|
||||
b.HasIndex("VisitorId", "CreatedAt");
|
||||
|
||||
b.ToTable("InterestEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
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.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedShiftId");
|
||||
|
||||
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<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.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<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.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.User", "OwnerUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("OwnerUserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("City");
|
||||
|
||||
b.Navigation("OwnerUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.InterestEvent", b =>
|
||||
{
|
||||
b.HasOne("JobsMedical.Web.Models.Shift", "Shift")
|
||||
.WithMany()
|
||||
.HasForeignKey("ShiftId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Visitor", "Visitor")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("VisitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Shift");
|
||||
|
||||
b.Navigation("Visitor");
|
||||
});
|
||||
|
||||
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.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,223 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRolesAndInterestTracking : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RoleId",
|
||||
table: "Shifts",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RoleId",
|
||||
table: "DoctorProfiles",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Roles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Category = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
IsActive = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Roles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Visitors",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "character varying(36)", maxLength: 36, nullable: false),
|
||||
UserId = table.Column<int>(type: "integer", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
LastSeenAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Visitors", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Visitors_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InterestEvents",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
VisitorId = table.Column<string>(type: "character varying(36)", nullable: false),
|
||||
ShiftId = table.Column<int>(type: "integer", nullable: false),
|
||||
EventType = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InterestEvents", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_InterestEvents_Shifts_ShiftId",
|
||||
column: x => x.ShiftId,
|
||||
principalTable: "Shifts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_InterestEvents_Visitors_VisitorId",
|
||||
column: x => x.VisitorId,
|
||||
principalTable: "Visitors",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserPreferences",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
VisitorId = table.Column<string>(type: "character varying(36)", nullable: false),
|
||||
RoleId = table.Column<int>(type: "integer", nullable: true),
|
||||
CityId = table.Column<int>(type: "integer", nullable: true),
|
||||
PreferredShiftType = table.Column<int>(type: "integer", nullable: true),
|
||||
MinPay = table.Column<long>(type: "bigint", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserPreferences", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserPreferences_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_UserPreferences_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_UserPreferences_Visitors_VisitorId",
|
||||
column: x => x.VisitorId,
|
||||
principalTable: "Visitors",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Shifts_RoleId",
|
||||
table: "Shifts",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DoctorProfiles_RoleId",
|
||||
table: "DoctorProfiles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InterestEvents_ShiftId",
|
||||
table: "InterestEvents",
|
||||
column: "ShiftId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InterestEvents_VisitorId_CreatedAt",
|
||||
table: "InterestEvents",
|
||||
columns: new[] { "VisitorId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserPreferences_CityId",
|
||||
table: "UserPreferences",
|
||||
column: "CityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserPreferences_RoleId",
|
||||
table: "UserPreferences",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserPreferences_VisitorId",
|
||||
table: "UserPreferences",
|
||||
column: "VisitorId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Visitors_UserId",
|
||||
table: "Visitors",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DoctorProfiles_Roles_RoleId",
|
||||
table: "DoctorProfiles",
|
||||
column: "RoleId",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Shifts_Roles_RoleId",
|
||||
table: "Shifts",
|
||||
column: "RoleId",
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DoctorProfiles_Roles_RoleId",
|
||||
table: "DoctorProfiles");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Shifts_Roles_RoleId",
|
||||
table: "Shifts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "InterestEvents");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserPreferences");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Roles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Visitors");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Shifts_RoleId",
|
||||
table: "Shifts");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DoctorProfiles_RoleId",
|
||||
table: "DoctorProfiles");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RoleId",
|
||||
table: "Shifts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RoleId",
|
||||
table: "DoctorProfiles");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,680 @@
|
||||
// <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("20260602214055_AddDistricts")]
|
||||
partial class AddDistricts
|
||||
{
|
||||
/// <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.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>("ShiftId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("VisitorId")
|
||||
.IsRequired()
|
||||
.HasColumnType("character varying(36)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShiftId");
|
||||
|
||||
b.HasIndex("VisitorId", "CreatedAt");
|
||||
|
||||
b.ToTable("InterestEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
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.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedShiftId");
|
||||
|
||||
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<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.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<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.Shift", "Shift")
|
||||
.WithMany()
|
||||
.HasForeignKey("ShiftId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("JobsMedical.Web.Models.Visitor", "Visitor")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("VisitorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Shift");
|
||||
|
||||
b.Navigation("Visitor");
|
||||
});
|
||||
|
||||
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,79 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDistricts : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DistrictId",
|
||||
table: "Facilities",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Districts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CityId = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
IsActive = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Districts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Districts_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Facilities_DistrictId",
|
||||
table: "Facilities",
|
||||
column: "DistrictId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Districts_CityId",
|
||||
table: "Districts",
|
||||
column: "CityId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Facilities_Districts_DistrictId",
|
||||
table: "Facilities",
|
||||
column: "DistrictId",
|
||||
principalTable: "Districts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Facilities_Districts_DistrictId",
|
||||
table: "Facilities");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Districts");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Facilities_DistrictId",
|
||||
table: "Facilities");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DistrictId",
|
||||
table: "Facilities");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
// <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("20260602220651_JobsAndAuth")]
|
||||
partial class JobsAndAuth
|
||||
{
|
||||
/// <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.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<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<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.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedShiftId");
|
||||
|
||||
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<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.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<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,123 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class JobsAndAuth : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "ShiftId",
|
||||
table: "InterestEvents",
|
||||
type: "integer",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "JobOpeningId",
|
||||
table: "InterestEvents",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "JobOpenings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
FacilityId = table.Column<int>(type: "integer", nullable: false),
|
||||
RoleId = table.Column<int>(type: "integer", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
EmploymentType = table.Column<int>(type: "integer", nullable: false),
|
||||
SalaryMin = table.Column<long>(type: "bigint", nullable: true),
|
||||
SalaryMax = table.Column<long>(type: "bigint", nullable: true),
|
||||
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||
Requirements = table.Column<string>(type: "character varying(1000)", maxLength: 1000, 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_JobOpenings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_JobOpenings_Facilities_FacilityId",
|
||||
column: x => x.FacilityId,
|
||||
principalTable: "Facilities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_JobOpenings_Roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "Roles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_InterestEvents_JobOpeningId",
|
||||
table: "InterestEvents",
|
||||
column: "JobOpeningId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JobOpenings_FacilityId",
|
||||
table: "JobOpenings",
|
||||
column: "FacilityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JobOpenings_RoleId",
|
||||
table: "JobOpenings",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_JobOpenings_Status",
|
||||
table: "JobOpenings",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_InterestEvents_JobOpenings_JobOpeningId",
|
||||
table: "InterestEvents",
|
||||
column: "JobOpeningId",
|
||||
principalTable: "JobOpenings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_InterestEvents_JobOpenings_JobOpeningId",
|
||||
table: "InterestEvents");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "JobOpenings");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_InterestEvents_JobOpeningId",
|
||||
table: "InterestEvents");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JobOpeningId",
|
||||
table: "InterestEvents");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "ShiftId",
|
||||
table: "InterestEvents",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using JobsMedical.Web.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JobsMedical.Web.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(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.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<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<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.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedShiftId");
|
||||
|
||||
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<long?>("PayAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.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<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,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>A doctor expressing interest in a shift. MVP = lightweight "interested" + contact handoff.</summary>
|
||||
public class Application
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int ShiftId { get; set; }
|
||||
public Shift Shift { get; set; } = null!;
|
||||
|
||||
public int DoctorId { get; set; } // User.Id of the doctor
|
||||
public User Doctor { get; set; } = null!;
|
||||
|
||||
public ApplicationStatus Status { get; set; } = ApplicationStatus.Interested;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Message { get; set; } // پیام اختیاری پزشک
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>Canonical city list used for filtering. Launch focuses on Tehran.</summary>
|
||||
public class City
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required, MaxLength(100)]
|
||||
public string Name { get; set; } = ""; // نام شهر، مثل «تهران»
|
||||
|
||||
[MaxLength(100)]
|
||||
public string Province { get; set; } = ""; // استان
|
||||
|
||||
public bool IsActive { get; set; } = true; // آیا در این شهر سرویس فعال است
|
||||
|
||||
public ICollection<Facility> Facilities { get; set; } = new List<Facility>();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A neighborhood / area within a city (e.g. سعادتآباد، تهرانپارس). Lets people narrow a
|
||||
/// city down to where they actually want to work, alongside the "near me" distance filter.
|
||||
/// </summary>
|
||||
public class District
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int CityId { get; set; }
|
||||
public City City { get; set; } = null!;
|
||||
|
||||
[Required, MaxLength(120)]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public ICollection<Facility> Facilities { get; set; } = new List<Facility>();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>Profile for a doctor. Trust signal is the نظام پزشکی (medical council) number.</summary>
|
||||
public class DoctorProfile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int UserId { get; set; }
|
||||
public User User { get; set; } = null!;
|
||||
|
||||
public int? RoleId { get; set; } // نقش این فرد (پزشک/پرستار/ماما/...)
|
||||
public Role? Role { get; set; }
|
||||
|
||||
[MaxLength(20)]
|
||||
public string? LicenseNo { get; set; } // شماره نظام پزشکی/پرستاری
|
||||
|
||||
[MaxLength(100)]
|
||||
public string Specialty { get; set; } = "پزشک عمومی"; // جزئیات تخصص (اختیاری)
|
||||
|
||||
public int? CityId { get; set; }
|
||||
public City? City { get; set; }
|
||||
|
||||
public int YearsExperience { get; set; }
|
||||
|
||||
[MaxLength(1000)]
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public bool IsVerified { get; set; } // تأیید نظام پزشکی توسط ادمین
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
Doctor = 0,
|
||||
FacilityAdmin = 1,
|
||||
Admin = 2
|
||||
}
|
||||
|
||||
public enum FacilityType
|
||||
{
|
||||
Hospital = 0, // بیمارستان
|
||||
Clinic = 1, // کلینیک
|
||||
Polyclinic = 2 // درمانگاه
|
||||
}
|
||||
|
||||
public enum ShiftType
|
||||
{
|
||||
Day = 0, // روز
|
||||
Evening = 1, // عصر
|
||||
Night = 2, // شب
|
||||
OnCall = 3 // آنکال
|
||||
}
|
||||
|
||||
public enum ShiftStatus
|
||||
{
|
||||
Open = 0, // باز
|
||||
Filled = 1, // پر شده
|
||||
Expired = 2, // منقضی
|
||||
Cancelled = 3 // لغو شده
|
||||
}
|
||||
|
||||
public enum ShiftSource
|
||||
{
|
||||
Direct = 0, // ثبت مستقیم مرکز درمانی
|
||||
Admin = 1, // ثبت توسط ادمین
|
||||
Aggregated = 2 // جمعآوری شده از کانالها
|
||||
}
|
||||
|
||||
public enum PayType
|
||||
{
|
||||
PerShift = 0, // مقطوع برای هر شیفت
|
||||
PerHour = 1, // ساعتی
|
||||
Negotiable = 2 // توافقی
|
||||
}
|
||||
|
||||
public enum ApplicationStatus
|
||||
{
|
||||
Interested = 0, // اعلام تمایل
|
||||
Accepted = 1, // پذیرفته شده
|
||||
Rejected = 2, // رد شده
|
||||
Withdrawn = 3 // انصراف
|
||||
}
|
||||
|
||||
public enum RawListingStatus
|
||||
{
|
||||
New = 0, // جدید
|
||||
Normalized = 1, // تبدیل شده به شیفت
|
||||
Discarded = 2 // کنار گذاشته شده
|
||||
}
|
||||
|
||||
public enum EmploymentType
|
||||
{
|
||||
FullTime = 0, // تماموقت
|
||||
PartTime = 1, // پارهوقت
|
||||
Contract = 2, // قراردادی
|
||||
Plan = 3 // طرح
|
||||
}
|
||||
|
||||
/// <summary>What an aggregated/raw listing turned out to be — a shift or a hiring opening.</summary>
|
||||
public enum ListingKind
|
||||
{
|
||||
Shift = 0,
|
||||
Job = 1
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>A hospital, clinic, or polyclinic that posts open shifts.</summary>
|
||||
public class Facility
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required, MaxLength(200)]
|
||||
public string Name { get; set; } = ""; // نام مرکز درمانی
|
||||
|
||||
public FacilityType Type { get; set; } = FacilityType.Hospital;
|
||||
|
||||
public int CityId { get; set; }
|
||||
public City City { get; set; } = null!;
|
||||
|
||||
public int? DistrictId { get; set; } // محله/منطقه
|
||||
public District? District { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Address { get; set; } // آدرس
|
||||
|
||||
public double? Lat { get; set; } // مختصات برای نقشه نشان/بلد
|
||||
public double? Lng { get; set; }
|
||||
|
||||
[MaxLength(20)]
|
||||
public string? Phone { get; set; } // تلفن تماس مرکز
|
||||
|
||||
[MaxLength(50)]
|
||||
public string? BaleId { get; set; } // شناسه بله برای ارتباط
|
||||
|
||||
public bool IsVerified { get; set; } // نشان «تأیید شده»
|
||||
|
||||
// Phase 2: facility self-serve. Null in MVP (admin manages).
|
||||
public int? OwnerUserId { get; set; }
|
||||
public User? OwnerUser { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ICollection<Shift> Shifts { get; set; } = new List<Shift>();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
public enum InterestEventType
|
||||
{
|
||||
View = 0, // باز کردن صفحه شیفت
|
||||
Click = 1, // کلیک از فهرست
|
||||
Save = 2, // ذخیره/علاقهمندی
|
||||
Apply = 3, // اعلام تمایل
|
||||
Dismiss = 4, // رد کردن
|
||||
HideFacility = 5 // پنهان کردن یک مرکز
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One behavioral signal: this visitor did X to this shift. The accumulated log is the fuel
|
||||
/// for collaborative filtering and ML ranking later; for now the pattern engine reads recent
|
||||
/// events to infer affinity (e.g. repeated interest in night shifts at a given facility).
|
||||
/// </summary>
|
||||
public class InterestEvent
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public string VisitorId { get; set; } = "";
|
||||
public Visitor Visitor { get; set; } = null!;
|
||||
|
||||
// Exactly one target is set — a shift or a job opening.
|
||||
public int? ShiftId { get; set; }
|
||||
public Shift? Shift { get; set; }
|
||||
|
||||
public int? JobOpeningId { get; set; }
|
||||
public JobOpening? JobOpening { get; set; }
|
||||
|
||||
public InterestEventType EventType { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A permanent / ongoing hiring position (استخدام) — the hiring side of the marketplace,
|
||||
/// alongside one-off <see cref="Shift"/>s. No date/time; instead an employment type and a
|
||||
/// monthly salary range. Reuses <see cref="ShiftStatus"/> for lifecycle (Open/Filled/…).
|
||||
/// </summary>
|
||||
public class JobOpening
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int FacilityId { get; set; }
|
||||
public Facility Facility { get; set; } = null!;
|
||||
|
||||
public int RoleId { get; set; }
|
||||
public Role Role { get; set; } = null!;
|
||||
|
||||
[Required, MaxLength(200)]
|
||||
public string Title { get; set; } = ""; // عنوان موقعیت
|
||||
|
||||
public EmploymentType EmploymentType { get; set; } = EmploymentType.FullTime;
|
||||
|
||||
public long? SalaryMin { get; set; } // حقوق ماهانه (تومان)؛ null = توافقی
|
||||
public long? SalaryMax { get; set; }
|
||||
|
||||
[MaxLength(2000)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[MaxLength(1000)]
|
||||
public string? Requirements { get; set; } // شرایط احراز
|
||||
|
||||
public ShiftStatus Status { get; set; } = ShiftStatus.Open;
|
||||
public ShiftSource Source { get; set; } = ShiftSource.Admin;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? SourceUrl { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Transient: distance (km) when "near me" is active. Not persisted.
|
||||
[NotMapped] public double? DistanceKm { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Staging area for shift listings aggregated from Telegram / Bale / Divar channels.
|
||||
/// An admin reviews and normalizes these into real <see cref="Shift"/> records.
|
||||
/// This is how we beat the cold-start problem.
|
||||
/// </summary>
|
||||
public class RawListing
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string SourceChannel { get; set; } = ""; // نام کانال/منبع
|
||||
|
||||
[Required]
|
||||
public string RawText { get; set; } = ""; // متن خام آگهی
|
||||
|
||||
public string? ParsedJson { get; set; } // نتیجهی تجزیهی خودکار (در صورت وجود)
|
||||
|
||||
public RawListingStatus Status { get; set; } = RawListingStatus.New;
|
||||
|
||||
public int? LinkedShiftId { get; set; } // شیفت ساختهشده از این آگهی
|
||||
public Shift? LinkedShift { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? SourceUrl { get; set; }
|
||||
|
||||
public DateTime FetchedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A healthcare staff role the platform serves. The taxonomy spans all of کادر درمان —
|
||||
/// doctors, nurses, midwives, technicians — not just GPs. Used to tag shifts/openings and
|
||||
/// to match people to opportunities.
|
||||
/// </summary>
|
||||
public class Role
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required, MaxLength(100)]
|
||||
public string Name { get; set; } = ""; // مثل «پزشک عمومی»، «پرستار»، «ماما»
|
||||
|
||||
[MaxLength(50)]
|
||||
public string Category { get; set; } = ""; // گروه: پزشک / پرستار / ماما / تکنسین
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public ICollection<Shift> Shifts { get; set; } = new List<Shift>();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// An open shift at a facility. The heart of the platform.
|
||||
/// Dates are stored as UTC <see cref="DateOnly"/>/<see cref="TimeOnly"/> and displayed as Jalali.
|
||||
/// </summary>
|
||||
public class Shift
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int FacilityId { get; set; }
|
||||
public Facility Facility { get; set; } = null!;
|
||||
|
||||
public int RoleId { get; set; } // نقش مورد نیاز (پزشک/پرستار/ماما/...)
|
||||
public Role Role { get; set; } = null!;
|
||||
|
||||
public DateOnly Date { get; set; } // تاریخ شیفت (در نمایش به شمسی تبدیل میشود)
|
||||
public TimeOnly StartTime { get; set; } // ساعت شروع
|
||||
public TimeOnly EndTime { get; set; } // ساعت پایان
|
||||
|
||||
[MaxLength(100)]
|
||||
public string SpecialtyRequired { get; set; } = "پزشک عمومی";
|
||||
|
||||
public ShiftType ShiftType { get; set; } = ShiftType.Day;
|
||||
|
||||
public long? PayAmount { get; set; } // مبلغ (تومان)؛ null یعنی توافقی
|
||||
public PayType PayType { get; set; } = PayType.PerShift;
|
||||
|
||||
[MaxLength(1500)]
|
||||
public string? Description { get; set; } // توضیحات
|
||||
|
||||
public ShiftStatus Status { get; set; } = ShiftStatus.Open;
|
||||
public ShiftSource Source { get; set; } = ShiftSource.Admin;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? SourceUrl { get; set; } // لینک منبع در صورت جمعآوری از کانال
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ICollection<Application> Applications { get; set; } = new List<Application>();
|
||||
|
||||
// Transient: distance (km) from the visitor when "near me" is active. Not persisted.
|
||||
[System.ComponentModel.DataAnnotations.Schema.NotMapped]
|
||||
public double? DistanceKm { get; set; }
|
||||
|
||||
// Convenience (not mapped): duration in hours, handles overnight shifts.
|
||||
public double DurationHours
|
||||
{
|
||||
get
|
||||
{
|
||||
var span = EndTime.ToTimeSpan() - StartTime.ToTimeSpan();
|
||||
if (span <= TimeSpan.Zero) span += TimeSpan.FromDays(1); // شیفت شب که به روز بعد میرسد
|
||||
return span.TotalHours;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Identity is the phone number (OTP login, no passwords). A user is a doctor,
|
||||
/// a facility admin, or a platform admin.
|
||||
/// </summary>
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required, MaxLength(20)]
|
||||
public string Phone { get; set; } = ""; // شماره موبایل، شناسه یکتا
|
||||
|
||||
[MaxLength(150)]
|
||||
public string? FullName { get; set; }
|
||||
|
||||
public UserRole Role { get; set; } = UserRole.Doctor;
|
||||
|
||||
public bool IsPhoneVerified { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public DoctorProfile? DoctorProfile { get; set; }
|
||||
public ICollection<Application> Applications { get; set; } = new List<Application>();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// What a visitor says they want — the explicit signal for the recommendation engine.
|
||||
/// Stored per visitor (works pre-login); merges to the user account on login.
|
||||
/// </summary>
|
||||
public class UserPreferences
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string VisitorId { get; set; } = "";
|
||||
public Visitor Visitor { get; set; } = null!;
|
||||
|
||||
public int? RoleId { get; set; } // نقش مورد علاقه
|
||||
public Role? Role { get; set; }
|
||||
|
||||
public int? CityId { get; set; } // شهر مورد علاقه
|
||||
public City? City { get; set; }
|
||||
|
||||
public ShiftType? PreferredShiftType { get; set; } // نوع شیفت ترجیحی
|
||||
public long? MinPay { get; set; } // حداقل حقوق مورد انتظار (تومان)
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public bool HasAny =>
|
||||
RoleId is not null || CityId is not null || PreferredShiftType is not null || MinPay is not null;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace JobsMedical.Web.Models;
|
||||
|
||||
/// <summary>
|
||||
/// An anonymous visitor, identified by a cookie before they ever log in. This lets us track
|
||||
/// interest and personalize from the very first visit; once the person logs in we link the
|
||||
/// Visitor to their <see cref="User"/> and keep all the behavioral history.
|
||||
/// </summary>
|
||||
public class Visitor
|
||||
{
|
||||
[MaxLength(36)]
|
||||
public string Id { get; set; } = ""; // GUID string stored in the hk_vid cookie
|
||||
|
||||
public int? UserId { get; set; } // set after login (history carries over)
|
||||
public User? User { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime LastSeenAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public UserPreferences? Preferences { get; set; }
|
||||
public ICollection<InterestEvent> Events { get; set; } = new List<InterestEvent>();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Account.LoginModel
|
||||
@{
|
||||
ViewData["Title"] = "ورود کادر درمان";
|
||||
}
|
||||
|
||||
<div class="container section" style="max-width:440px;">
|
||||
<div class="card card-pad">
|
||||
<h1 style="margin-top:0; font-size:22px;">ورود / ثبتنام</h1>
|
||||
<p class="muted">با شماره موبایل وارد شو تا فرصتهای متناسب با تو را ذخیره و پیشنهاد کنیم.</p>
|
||||
|
||||
@if (Model.Error is not null)
|
||||
{
|
||||
<div class="alert" style="background:#fdeaea; color:var(--danger);">@Model.Error</div>
|
||||
}
|
||||
|
||||
@if (!Model.CodeSent)
|
||||
{
|
||||
<form method="post">
|
||||
<div class="filter-group">
|
||||
<label>شماره موبایل</label>
|
||||
<input type="tel" name="Phone" value="@Model.Phone" placeholder="۰۹۱۲ ..." dir="ltr" />
|
||||
</div>
|
||||
<button type="submit" asp-page-handler="RequestCode" class="btn btn-accent btn-block btn-lg">دریافت کد تأیید</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (Model.DevCode is not null)
|
||||
{
|
||||
<div class="alert alert-success">
|
||||
کد تأیید (حالت توسعه): <strong dir="ltr">@Model.DevCode</strong><br />
|
||||
<span style="font-size:12px;">در نسخهی نهایی این کد از طریق پیامک (کاوهنگار/SMS.ir) ارسال میشود.</span>
|
||||
</div>
|
||||
}
|
||||
<form method="post">
|
||||
<input type="hidden" name="Phone" value="@Model.Phone" />
|
||||
<div class="filter-group">
|
||||
<label>کد تأیید پنجرقمی</label>
|
||||
<input type="text" name="Code" placeholder="- - - - -" dir="ltr" inputmode="numeric" />
|
||||
</div>
|
||||
<button type="submit" asp-page-handler="Verify" class="btn btn-accent btn-block btn-lg">ورود</button>
|
||||
</form>
|
||||
<form method="post" style="margin-top:8px;">
|
||||
<input type="hidden" name="Phone" value="@Model.Phone" />
|
||||
<button type="submit" asp-page-handler="RequestCode" class="btn btn-outline btn-block">ارسال مجدد کد</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,100 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Account;
|
||||
|
||||
public class LoginModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly OtpService _otp;
|
||||
private readonly VisitorContext _visitor;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
public LoginModel(AppDbContext db, OtpService otp, VisitorContext visitor, IConfiguration config)
|
||||
{
|
||||
_db = db;
|
||||
_otp = otp;
|
||||
_visitor = visitor;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[BindProperty] public string Phone { get; set; } = "";
|
||||
[BindProperty] public string? Code { get; set; }
|
||||
|
||||
public bool CodeSent { get; private set; }
|
||||
public string? DevCode { get; private set; } // shown only in dev (no SMS gateway yet)
|
||||
public string? Error { get; private set; }
|
||||
|
||||
public void OnGet() { }
|
||||
|
||||
public IActionResult OnPostRequestCode()
|
||||
{
|
||||
var phone = OtpService.Normalize(Phone);
|
||||
if (phone.Length < 10)
|
||||
{
|
||||
Error = "شماره موبایل معتبر وارد کنید.";
|
||||
return Page();
|
||||
}
|
||||
Phone = phone;
|
||||
DevCode = _otp.Issue(phone); // dev: surface the code; prod: SMS gateway sends it
|
||||
CodeSent = true;
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostVerifyAsync(string? returnUrl)
|
||||
{
|
||||
var phone = OtpService.Normalize(Phone);
|
||||
if (!_otp.Verify(phone, Code ?? ""))
|
||||
{
|
||||
Error = "کد واردشده نادرست یا منقضی شده است.";
|
||||
CodeSent = true;
|
||||
return Page();
|
||||
}
|
||||
|
||||
// Find or create the user. The configured admin phone is granted the Admin role.
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Phone == phone);
|
||||
var isAdmin = phone == OtpService.Normalize(_config["Auth:AdminPhone"] ?? "");
|
||||
if (user is null)
|
||||
{
|
||||
user = new User { Phone = phone, IsPhoneVerified = true,
|
||||
Role = isAdmin ? UserRole.Admin : UserRole.Doctor };
|
||||
_db.Users.Add(user);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.IsPhoneVerified = true;
|
||||
if (isAdmin) user.Role = UserRole.Admin;
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// Link the anonymous visitor (and its interest history) to this account.
|
||||
var vid = _visitor.VisitorId;
|
||||
if (!string.IsNullOrEmpty(vid))
|
||||
{
|
||||
var visitor = await _db.Visitors.FirstOrDefaultAsync(v => v.Id == vid);
|
||||
if (visitor is null) { visitor = new Visitor { Id = vid }; _db.Visitors.Add(visitor); }
|
||||
visitor.UserId = user.Id;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new(ClaimTypes.MobilePhone, user.Phone),
|
||||
new(ClaimTypes.Name, user.FullName ?? user.Phone),
|
||||
new(ClaimTypes.Role, user.Role.ToString()),
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(identity));
|
||||
|
||||
return LocalRedirect(string.IsNullOrEmpty(returnUrl) ? "/" : returnUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Account.LogoutModel
|
||||
@* POST-only; OnGet redirects home. *@
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Account;
|
||||
|
||||
public class LogoutModel : PageModel
|
||||
{
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
|
||||
public IActionResult OnGet() => RedirectToPage("/Index");
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@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>
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Security.Claims;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Account;
|
||||
|
||||
[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();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Admin.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "مدیریت — صف آگهیها";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>پنل مدیریت — صف آگهیهای خام</h1>
|
||||
<p class="muted">
|
||||
آگهیهای جمعآوریشده از کانالها را اینجا بررسی، ساختارمند و منتشر کن.
|
||||
(@JalaliDate.ToPersianDigits(Model.Queue.Count.ToString()) در انتظار بررسی)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="layout-2">
|
||||
<aside class="card card-pad filter-card">
|
||||
<h3>افزودن آگهی خام</h3>
|
||||
<form method="post">
|
||||
<div class="filter-group">
|
||||
<label>منبع (کانال/سایت)</label>
|
||||
<input type="text" name="SourceChannel" placeholder="مثلاً کانال شیفت تهران" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>متن آگهی</label>
|
||||
<textarea name="RawText" rows="6" placeholder="متن کپیشده از تلگرام/بله/دیوار را اینجا بچسبان..."></textarea>
|
||||
</div>
|
||||
<button type="submit" asp-page-handler="Add" class="btn btn-primary btn-block">افزودن به صف</button>
|
||||
</form>
|
||||
<p class="muted" style="font-size:12px; margin-bottom:0;">
|
||||
منتشرشده: @JalaliDate.ToPersianDigits(Model.PublishedShifts.ToString()) شیفت،
|
||||
@JalaliDate.ToPersianDigits(Model.PublishedJobs.ToString()) استخدام
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<div>
|
||||
@if (Model.Queue.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">صف خالی است. آگهی جدیدی برای بررسی وجود ندارد.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var r in Model.Queue)
|
||||
{
|
||||
<div class="card card-pad" style="margin-bottom:14px;">
|
||||
<div class="row" style="display:flex; justify-content:space-between;">
|
||||
<strong>@r.SourceChannel</strong>
|
||||
<span class="muted" style="font-size:12px;">@JalaliDate.ToLongDate(DateOnly.FromDateTime(r.FetchedAt))</span>
|
||||
</div>
|
||||
<p style="margin:10px 0; white-space:pre-wrap;">@r.RawText</p>
|
||||
<a class="btn btn-accent" asp-page="/Admin/Review" asp-route-id="@r.Id">بررسی و انتشار ←</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
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.Admin;
|
||||
|
||||
[Authorize(Roles = "Admin")] // secured by the OTP-auth Admin role
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
public List<RawListing> Queue { get; private set; } = new();
|
||||
public int PublishedShifts { get; private set; }
|
||||
public int PublishedJobs { get; private set; }
|
||||
|
||||
[BindProperty] public string? SourceChannel { get; set; }
|
||||
[BindProperty] public string? RawText { get; set; }
|
||||
|
||||
public async Task OnGetAsync() => await LoadAsync();
|
||||
|
||||
public async Task<IActionResult> OnPostAddAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(RawText))
|
||||
{
|
||||
_db.RawListings.Add(new RawListing
|
||||
{
|
||||
SourceChannel = string.IsNullOrWhiteSpace(SourceChannel) ? "ورود دستی" : SourceChannel.Trim(),
|
||||
RawText = RawText.Trim(),
|
||||
Status = RawListingStatus.New,
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
return RedirectToPage();
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
Queue = await _db.RawListings
|
||||
.Where(r => r.Status == RawListingStatus.New)
|
||||
.OrderByDescending(r => r.FetchedAt).ToListAsync();
|
||||
PublishedShifts = await _db.Shifts.CountAsync(s => s.Source != ShiftSource.Direct);
|
||||
PublishedJobs = await _db.JobOpenings.CountAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
@page "{id:int}"
|
||||
@model JobsMedical.Web.Pages.Admin.ReviewModel
|
||||
@{
|
||||
ViewData["Title"] = "بررسی و انتشار آگهی";
|
||||
var r = Model.Raw!;
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container"><h1>بررسی و انتشار آگهی</h1><p class="muted">منبع: @r.SourceChannel</p></div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="detail-grid">
|
||||
<div>
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">متن خام</h3>
|
||||
<p style="white-space:pre-wrap; margin:0;">@r.RawText</p>
|
||||
</div>
|
||||
|
||||
@if (Model.Parsed is not null)
|
||||
{
|
||||
<div class="card card-pad" style="margin-top:16px;">
|
||||
<h3 style="margin-top:0;">🤖 تشخیص خودکار (پارسر)</h3>
|
||||
<div class="rec-reasons">
|
||||
@foreach (var note in Model.Parsed.Notes)
|
||||
{
|
||||
<span class="rec-reason">• @note</span>
|
||||
}
|
||||
@if (Model.Parsed.CityName is not null) { <span class="rec-reason">• شهر: @Model.Parsed.CityName</span> }
|
||||
@if (Model.Parsed.DistrictName is not null) { <span class="rec-reason">• محله: @Model.Parsed.DistrictName</span> }
|
||||
@if (Model.Parsed.Phone is not null) { <span class="rec-reason">• تلفن: @Model.Parsed.Phone</span> }
|
||||
</div>
|
||||
<p class="muted" style="font-size:12px; margin-bottom:0;">اینها فقط پیشنهاد هستند؛ قبل از انتشار بررسی و اصلاح کن.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<form method="post" class="card card-pad">
|
||||
<div class="filter-group">
|
||||
<label>نوع آگهی</label>
|
||||
<select name="Kind" id="kindSelect">
|
||||
<option value="0" selected="@(Model.Kind == JobsMedical.Web.Models.ListingKind.Shift)">شیفت</option>
|
||||
<option value="1" selected="@(Model.Kind == JobsMedical.Web.Models.ListingKind.Job)">استخدام</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>مرکز درمانی</label>
|
||||
<select name="FacilityId">
|
||||
@foreach (var f in Model.Facilities)
|
||||
{
|
||||
<option value="@f.Id">@f.Name — @f.City?.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نقش</label>
|
||||
<select name="RoleId">
|
||||
@foreach (var role in Model.Roles)
|
||||
{
|
||||
<option value="@role.Id" selected="@(Model.RoleId == role.Id)">@role.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="shiftFields">
|
||||
<div class="filter-group">
|
||||
<label>تاریخ شیفت (میلادی)</label>
|
||||
<input type="date" name="ShiftDate" value="@Model.ShiftDate.ToString("yyyy-MM-dd")" dir="ltr" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع شیفت</label>
|
||||
<select name="ShiftType">
|
||||
<option value="0" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Day)">صبح</option>
|
||||
<option value="1" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Evening)">عصر</option>
|
||||
<option value="2" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Night)">شب</option>
|
||||
<option value="3" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.OnCall)">آنکال</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;"><label>شروع</label><input type="time" name="StartTime" value="@Model.StartTime.ToString("HH:mm")" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>پایان</label><input type="time" name="EndTime" value="@Model.EndTime.ToString("HH:mm")" dir="ltr" /></div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>حقوق هر شیفت (تومان)</label>
|
||||
<input type="number" name="PayAmount" value="@Model.PayAmount" dir="ltr" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="jobFields" style="display:none;">
|
||||
<div class="filter-group">
|
||||
<label>عنوان موقعیت</label>
|
||||
<input type="text" name="Title" value="@Model.Title" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع همکاری</label>
|
||||
<select name="EmploymentType">
|
||||
<option value="0" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.FullTime)">تماموقت</option>
|
||||
<option value="1" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.PartTime)">پارهوقت</option>
|
||||
<option value="2" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.Contract)">قراردادی</option>
|
||||
<option value="3" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.Plan)">طرح</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group" style="display:flex; gap:8px;">
|
||||
<div style="flex:1;"><label>حقوق از</label><input type="number" name="SalaryMin" value="@Model.SalaryMin" dir="ltr" /></div>
|
||||
<div style="flex:1;"><label>تا</label><input type="number" name="SalaryMax" value="@Model.SalaryMax" dir="ltr" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-weight:600;">
|
||||
<input type="checkbox" name="Negotiable" value="true" style="width:auto;" checked="@Model.Negotiable" /> توافقی
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>توضیحات</label>
|
||||
<textarea name="Description" rows="3">@Model.Description</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" asp-page-handler="Publish" asp-route-id="@r.Id" class="btn btn-accent btn-block btn-lg">انتشار</button>
|
||||
<button type="submit" asp-page-handler="Discard" asp-route-id="@r.Id" class="btn btn-outline btn-block" style="margin-top:8px;">رد و حذف از صف</button>
|
||||
</form>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
var kind = document.getElementById('kindSelect');
|
||||
function toggleKind() {
|
||||
var isJob = kind.value === '1';
|
||||
document.getElementById('jobFields').style.display = isJob ? 'block' : 'none';
|
||||
document.getElementById('shiftFields').style.display = isJob ? 'none' : 'block';
|
||||
}
|
||||
kind.addEventListener('change', toggleKind);
|
||||
toggleKind();
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Admin;
|
||||
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class ReviewModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IListingParser _parser;
|
||||
|
||||
public ReviewModel(AppDbContext db, IListingParser parser)
|
||||
{
|
||||
_db = db;
|
||||
_parser = parser;
|
||||
}
|
||||
|
||||
public RawListing? Raw { get; private set; }
|
||||
public ParsedListing? Parsed { get; private set; }
|
||||
public List<Facility> Facilities { get; private set; } = new();
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
|
||||
// The editable form (prefilled from the parser, admin can override everything).
|
||||
[BindProperty] public ListingKind Kind { get; set; }
|
||||
[BindProperty] public int FacilityId { get; set; }
|
||||
[BindProperty] public int RoleId { get; set; }
|
||||
[BindProperty] public string? Description { get; set; }
|
||||
// Shift fields
|
||||
[BindProperty] public DateOnly ShiftDate { get; set; }
|
||||
[BindProperty] public ShiftType ShiftType { get; set; }
|
||||
[BindProperty] public TimeOnly StartTime { get; set; }
|
||||
[BindProperty] public TimeOnly EndTime { get; set; }
|
||||
[BindProperty] public long? PayAmount { get; set; }
|
||||
[BindProperty] public bool Negotiable { get; set; }
|
||||
// Job fields
|
||||
[BindProperty] public string? Title { get; set; }
|
||||
[BindProperty] public EmploymentType EmploymentType { get; set; }
|
||||
[BindProperty] public long? SalaryMin { get; set; }
|
||||
[BindProperty] public long? SalaryMax { get; set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
await LoadListsAsync();
|
||||
Raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (Raw is null) return NotFound();
|
||||
|
||||
Parsed = _parser.Parse(Raw.RawText,
|
||||
Roles.Select(r => r.Name), await CityNamesAsync(), await DistrictNamesAsync());
|
||||
|
||||
// Prefill the form from the parser's best guess.
|
||||
Kind = Parsed.Kind;
|
||||
RoleId = Roles.FirstOrDefault(r => r.Name == Parsed.RoleName)?.Id ?? Roles.FirstOrDefault()?.Id ?? 0;
|
||||
ShiftType = Parsed.ShiftType ?? ShiftType.Day;
|
||||
EmploymentType = Parsed.EmploymentType ?? EmploymentType.FullTime;
|
||||
(StartTime, EndTime) = DefaultTimes(ShiftType);
|
||||
ShiftDate = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1);
|
||||
Negotiable = Parsed.PayNegotiable;
|
||||
if (Parsed.PayAmount is not null) { PayAmount = Parsed.PayAmount; SalaryMin = Parsed.PayAmount; }
|
||||
Description = Raw.RawText;
|
||||
Title = Parsed.RoleName is not null ? $"استخدام {Parsed.RoleName}" : "موقعیت استخدامی";
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostPublishAsync(int id)
|
||||
{
|
||||
Raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (Raw is null) return NotFound();
|
||||
|
||||
if (Kind == ListingKind.Shift)
|
||||
{
|
||||
var role = await _db.Roles.FindAsync(RoleId);
|
||||
var shift = new Shift
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
Date = ShiftDate,
|
||||
StartTime = StartTime,
|
||||
EndTime = EndTime,
|
||||
ShiftType = ShiftType,
|
||||
SpecialtyRequired = role?.Name ?? "",
|
||||
Description = Description,
|
||||
PayType = Negotiable ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = Negotiable ? null : PayAmount,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Aggregated,
|
||||
SourceUrl = Raw.SourceUrl,
|
||||
};
|
||||
_db.Shifts.Add(shift);
|
||||
await _db.SaveChangesAsync();
|
||||
Raw.Status = RawListingStatus.Normalized;
|
||||
Raw.LinkedShiftId = shift.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
var job = new JobOpening
|
||||
{
|
||||
FacilityId = FacilityId,
|
||||
RoleId = RoleId,
|
||||
Title = string.IsNullOrWhiteSpace(Title) ? "موقعیت استخدامی" : Title.Trim(),
|
||||
EmploymentType = EmploymentType,
|
||||
SalaryMin = Negotiable ? null : SalaryMin,
|
||||
SalaryMax = Negotiable ? null : SalaryMax,
|
||||
Description = Description,
|
||||
Status = ShiftStatus.Open,
|
||||
Source = ShiftSource.Aggregated,
|
||||
SourceUrl = Raw.SourceUrl,
|
||||
};
|
||||
_db.JobOpenings.Add(job);
|
||||
Raw.Status = RawListingStatus.Normalized;
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage("/Admin/Index");
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDiscardAsync(int id)
|
||||
{
|
||||
var raw = await _db.RawListings.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (raw is null) return NotFound();
|
||||
raw.Status = RawListingStatus.Discarded;
|
||||
await _db.SaveChangesAsync();
|
||||
return RedirectToPage("/Admin/Index");
|
||||
}
|
||||
|
||||
private static (TimeOnly, TimeOnly) DefaultTimes(ShiftType t) => t switch
|
||||
{
|
||||
ShiftType.Day => (new TimeOnly(8, 0), new TimeOnly(14, 0)),
|
||||
ShiftType.Evening => (new TimeOnly(14, 0), new TimeOnly(20, 0)),
|
||||
ShiftType.Night => (new TimeOnly(20, 0), new TimeOnly(8, 0)),
|
||||
_ => (new TimeOnly(8, 0), new TimeOnly(8, 0)),
|
||||
};
|
||||
|
||||
private async Task LoadListsAsync()
|
||||
{
|
||||
Facilities = await _db.Facilities.Include(f => f.City).OrderBy(f => f.Name).ToListAsync();
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
}
|
||||
|
||||
private Task<List<string>> CityNamesAsync() => _db.Cities.Select(c => c.Name).ToListAsync();
|
||||
private Task<List<string>> DistrictNamesAsync() => _db.Districts.Select(d => d.Name).ToListAsync();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Calendar.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "تقویم هفتگی شیفتها";
|
||||
var weekEnd = Model.WeekStart.AddDays(6);
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>تقویم هفتگی شیفتها</h1>
|
||||
<form method="get" style="margin-top:12px; max-width:360px;">
|
||||
<input type="hidden" name="WeekOffset" value="@Model.WeekOffset" />
|
||||
<select name="FacilityId" onchange="this.form.submit()">
|
||||
<option value="">همه مراکز درمانی</option>
|
||||
@foreach (var f in Model.Facilities)
|
||||
{
|
||||
<option value="@f.Id" selected="@(Model.FacilityId == f.Id)">@f.Name</option>
|
||||
}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="cal-nav">
|
||||
<a class="btn btn-outline" asp-page="/Calendar/Index"
|
||||
asp-route-FacilityId="@Model.FacilityId" asp-route-WeekOffset="@(Model.WeekOffset - 1)">→ هفته قبل</a>
|
||||
<strong>
|
||||
@JalaliDate.DayOfMonth(Model.WeekStart) @JalaliDate.MonthName(Model.WeekStart)
|
||||
تا
|
||||
@JalaliDate.DayOfMonth(weekEnd) @JalaliDate.MonthName(weekEnd)
|
||||
</strong>
|
||||
<a class="btn btn-outline" asp-page="/Calendar/Index"
|
||||
asp-route-FacilityId="@Model.FacilityId" asp-route-WeekOffset="@(Model.WeekOffset + 1)">هفته بعد ←</a>
|
||||
</div>
|
||||
|
||||
<table class="cal">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach (var (date, _) in Model.Days)
|
||||
{
|
||||
<th>@JalaliDate.WeekDayName(date)</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
@foreach (var (date, dayShifts) in Model.Days)
|
||||
{
|
||||
var isToday = date == Model.Today;
|
||||
<td class="@(isToday ? "today" : "") @(dayShifts.Count == 0 ? "empty" : "")">
|
||||
<div class="day-num">@JalaliDate.DayOfMonth(date)</div>
|
||||
@foreach (var s in dayShifts)
|
||||
{
|
||||
var cls = s.ShiftType switch
|
||||
{
|
||||
ShiftType.Day => "day",
|
||||
ShiftType.Evening => "evening",
|
||||
ShiftType.Night => "night",
|
||||
_ => "oncall",
|
||||
};
|
||||
<a class="cal-chip @cls" asp-page="/Shifts/Details" asp-route-id="@s.Id"
|
||||
title="@s.Facility?.Name">
|
||||
@JalaliDate.Time(s.StartTime) @s.Facility?.Name
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Calendar;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
[BindProperty(SupportsGet = true)] public int? FacilityId { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public int WeekOffset { get; set; } // 0 = current week
|
||||
|
||||
public List<Facility> Facilities { get; private set; } = new();
|
||||
public DateOnly WeekStart { get; private set; }
|
||||
public DateOnly Today { get; private set; }
|
||||
|
||||
/// <summary>7 days (Saturday→Friday), each with its open shifts.</summary>
|
||||
public List<(DateOnly Date, List<Shift> Shifts)> Days { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
Today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
Facilities = await _db.Facilities.OrderBy(f => f.Name).ToListAsync();
|
||||
|
||||
WeekStart = JalaliDate.StartOfPersianWeek(Today).AddDays(WeekOffset * 7);
|
||||
var weekEnd = WeekStart.AddDays(6);
|
||||
|
||||
var q = _db.Shifts
|
||||
.Include(s => s.Facility)
|
||||
.Where(s => s.Status == ShiftStatus.Open && s.Date >= WeekStart && s.Date <= weekEnd);
|
||||
|
||||
if (FacilityId is not null) q = q.Where(s => s.FacilityId == FacilityId);
|
||||
|
||||
var shifts = await q.OrderBy(s => s.StartTime).ToListAsync();
|
||||
|
||||
Days = Enumerable.Range(0, 7)
|
||||
.Select(i => WeekStart.AddDays(i))
|
||||
.Select(d => (d, shifts.Where(s => s.Date == d).ToList()))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JobsMedical.Web.Pages;
|
||||
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Facilities.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "مراکز درمانی";
|
||||
string TypeLabel(FacilityType t) => t switch
|
||||
{
|
||||
FacilityType.Hospital => "بیمارستان",
|
||||
FacilityType.Clinic => "کلینیک",
|
||||
_ => "درمانگاه",
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container"><h1>مراکز درمانی</h1></div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="grid grid-3">
|
||||
@foreach (var row in Model.Rows)
|
||||
{
|
||||
<div class="card card-pad">
|
||||
<div class="row" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span class="facility" style="font-weight:800; font-size:16px;">@row.Facility.Name</span>
|
||||
@if (row.Facility.IsVerified)
|
||||
{
|
||||
<span class="badge badge-verified">✓</span>
|
||||
}
|
||||
</div>
|
||||
<p class="muted" style="margin:8px 0;">
|
||||
<span class="badge badge-type">@TypeLabel(row.Facility.Type)</span>
|
||||
📍 @row.Facility.City?.Name
|
||||
</p>
|
||||
<div class="foot" style="display:flex; justify-content:space-between; align-items:center; border-top:1px solid var(--line); padding-top:12px;">
|
||||
<span class="pay" style="color:var(--primary-dark); font-weight:800;">
|
||||
@JalaliDate.ToPersianDigits(row.OpenShifts.ToString()) شیفت باز
|
||||
</span>
|
||||
<a class="btn btn-outline" style="padding:6px 14px;"
|
||||
asp-page="/Calendar/Index" asp-route-FacilityId="@row.Facility.Id">تقویم</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Facilities;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
public record FacilityRow(Facility Facility, int OpenShifts);
|
||||
public List<FacilityRow> Rows { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var facilities = await _db.Facilities.Include(f => f.City).OrderBy(f => f.Name).ToListAsync();
|
||||
var counts = await _db.Shifts
|
||||
.Where(s => s.Status == ShiftStatus.Open && s.Date >= today)
|
||||
.GroupBy(s => s.FacilityId)
|
||||
.Select(g => new { g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.Key, x => x.Count);
|
||||
|
||||
Rows = facilities
|
||||
.Select(f => new FacilityRow(f, counts.GetValueOrDefault(f.Id)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
@{
|
||||
ViewData["Title"] = null; // use default site title for the home page (best for SEO)
|
||||
ViewData["Description"] = "همکادر؛ سریعترین راه برای کادر درمان (پزشک، پرستار، ماما، تکنسین) جهت یافتن شیفت و موقعیت استخدامی در بیمارستانها و کلینیکهای تهران. بهجای گشتن در کانالهای تلگرام و بله، همه فرصتها یکجا.";
|
||||
}
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>شیفت و شغل بعدیات را در چند ثانیه پیدا کن</h1>
|
||||
<p>
|
||||
دیگر لازم نیست دهها کانال تلگرام، بله و آگهی دیوار را زیر و رو کنی.
|
||||
همهی شیفتها و فرصتهای استخدامی کادر درمان تهران، دستهبندیشده بر اساس
|
||||
مرکز درمانی، محل و تقویم هفتگی — یکجا.
|
||||
</p>
|
||||
|
||||
<form class="search-card" method="get" asp-page="/Shifts/Index">
|
||||
<div class="field">
|
||||
<label>شهر</label>
|
||||
<select name="cityId">
|
||||
<option value="">همه شهرها</option>
|
||||
@foreach (var c in Model.Cities)
|
||||
{
|
||||
<option value="@c.Id">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>نقش</label>
|
||||
<select name="roleId">
|
||||
<option value="">همه نقشها</option>
|
||||
@foreach (var r in Model.Roles)
|
||||
{
|
||||
<option value="@r.Id">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>نوع شیفت</label>
|
||||
<select name="shiftType">
|
||||
<option value="">همه</option>
|
||||
<option value="0">صبح</option>
|
||||
<option value="1">عصر</option>
|
||||
<option value="2">شب</option>
|
||||
<option value="3">آنکال</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label> </label>
|
||||
<button type="submit" class="btn btn-accent btn-block btn-lg">جستجوی فرصتها</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="stat-pills">
|
||||
<div class="stat-pill"><span class="n">@JalaliDate.ToPersianDigits(Model.OpenShiftCount.ToString())</span><span class="l">شیفت باز</span></div>
|
||||
<div class="stat-pill"><span class="n">@JalaliDate.ToPersianDigits(Model.FacilityCount.ToString())</span><span class="l">مرکز درمانی</span></div>
|
||||
<div class="stat-pill"><span class="n">@JalaliDate.ToPersianDigits(Model.CityCount.ToString())</span><span class="l">شهر فعال</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (Model.Recommendations.Count > 0)
|
||||
{
|
||||
<section class="section" style="padding-bottom:0;">
|
||||
<div class="container">
|
||||
@if (Model.HasPersonalization)
|
||||
{
|
||||
<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>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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>
|
||||
}
|
||||
<div class="grid grid-3">
|
||||
@foreach (var rec in Model.Recommendations)
|
||||
{
|
||||
<partial name="_RecommendationCard" model="rec" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<h2>جدیدترین شیفتها</h2>
|
||||
<a asp-page="/Shifts/Index">مشاهده همه ←</a>
|
||||
</div>
|
||||
@if (Model.LatestShifts.Count == 0)
|
||||
{
|
||||
<div class="empty-state">فعلاً شیفت بازی ثبت نشده است.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var s in Model.LatestShifts)
|
||||
{
|
||||
<partial name="_ShiftCard" model="s" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (Model.LatestJobs.Count > 0)
|
||||
{
|
||||
<section class="section" style="padding-top:0;">
|
||||
<div class="container">
|
||||
<div class="section-head">
|
||||
<h2>فرصتهای استخدامی</h2>
|
||||
<a asp-page="/Jobs/Index">مشاهده همه ←</a>
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
@foreach (var j in Model.LatestJobs)
|
||||
{
|
||||
<partial name="_JobCard" model="j" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="section" style="background: var(--surface); border-top: 1px solid var(--line);">
|
||||
<div class="container">
|
||||
<div class="section-head"><h2>چطور کار میکند؟</h2></div>
|
||||
<div class="grid grid-3">
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">۱. جستجو کن</h3>
|
||||
<p class="muted">بر اساس شهر، بیمارستان، تاریخ و نوع شیفت، موقعیت مناسب خودت را فیلتر کن.</p>
|
||||
</div>
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">۲. تقویم را ببین</h3>
|
||||
<p class="muted">شیفتهای خالی هر مرکز را در یک نمای هفتگی شمسی مشاهده کن.</p>
|
||||
</div>
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">۳. اعلام تمایل کن</h3>
|
||||
<p class="muted">روی شیفت دلخواه «اعلام تمایل» بزن تا مرکز درمانی با تو تماس بگیرد.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,64 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages;
|
||||
|
||||
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 List<Recommendation> Recommendations { get; private set; } = new();
|
||||
public bool HasPersonalization { get; private set; }
|
||||
public List<Shift> LatestShifts { get; private set; } = new();
|
||||
public List<JobOpening> LatestJobs { get; private set; } = new();
|
||||
public List<City> Cities { get; private set; } = new();
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
public int OpenShiftCount { get; private set; }
|
||||
public int FacilityCount { get; private set; }
|
||||
public int CityCount { get; private set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
|
||||
Recommendations = await _recs.GetForVisitorAsync(6);
|
||||
// "Personalized" = we actually used a signal (prefs or behavior), not just cold-start freshness.
|
||||
HasPersonalization = (await _interest.GetPreferencesAsync())?.HasAny == true
|
||||
|| (await _interest.RecentEventsAsync(1)).Count > 0;
|
||||
|
||||
LatestShifts = await _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Role)
|
||||
.Where(s => s.Status == ShiftStatus.Open && s.Date >= today)
|
||||
.OrderBy(s => s.Date).ThenBy(s => s.StartTime)
|
||||
.Take(6)
|
||||
.ToListAsync();
|
||||
|
||||
LatestJobs = await _db.JobOpenings
|
||||
.Include(j => j.Facility).ThenInclude(f => f.City)
|
||||
.Include(j => j.Facility).ThenInclude(f => f.District)
|
||||
.Include(j => j.Role)
|
||||
.Where(j => j.Status == ShiftStatus.Open)
|
||||
.OrderByDescending(j => j.CreatedAt)
|
||||
.Take(3)
|
||||
.ToListAsync();
|
||||
|
||||
Cities = await _db.Cities.Where(c => c.IsActive).OrderBy(c => c.Name).ToListAsync();
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
OpenShiftCount = await _db.Shifts.CountAsync(s => s.Status == ShiftStatus.Open && s.Date >= today);
|
||||
FacilityCount = await _db.Facilities.CountAsync();
|
||||
CityCount = await _db.Cities.CountAsync(c => c.IsActive);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
@page "{id:int}"
|
||||
@model JobsMedical.Web.Pages.Jobs.DetailsModel
|
||||
@{
|
||||
var j = Model.Job!;
|
||||
var f = j.Facility!;
|
||||
ViewData["Title"] = j.Title;
|
||||
ViewData["Description"] = $"{j.Title} در {f.Name}، {f.City?.Name}. موقعیت استخدامی برای {j.Role?.Name}.";
|
||||
string empLabel = j.EmploymentType switch
|
||||
{
|
||||
EmploymentType.FullTime => "تماموقت",
|
||||
EmploymentType.PartTime => "پارهوقت",
|
||||
EmploymentType.Contract => "قراردادی",
|
||||
_ => "طرح",
|
||||
};
|
||||
string salary;
|
||||
if (j.SalaryMin is null && j.SalaryMax is null) salary = "توافقی";
|
||||
else if (j.SalaryMin == j.SalaryMax) salary = JalaliDate.Toman(j.SalaryMin) + " ماهانه";
|
||||
else salary = $"از {JalaliDate.ToPersianDigits((j.SalaryMin ?? 0).ToString("#,0"))} تا {JalaliDate.Toman(j.SalaryMax)} ماهانه";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<div class="row" style="display:flex; gap:10px; align-items:center;">
|
||||
<span class="badge badge-job">@empLabel</span>
|
||||
@if (j.Role is not null) { <span class="badge badge-type">@j.Role.Name</span> }
|
||||
@if (f.IsVerified) { <span class="badge badge-verified">✓ مرکز تأیید شده</span> }
|
||||
</div>
|
||||
<h1 style="margin-top:8px;">@j.Title</h1>
|
||||
<p class="muted">🏥 @f.Name — 📍 @f.City?.Name@(f.District is not null ? "، " + f.District.Name : "")</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="detail-grid">
|
||||
<div>
|
||||
@if (Model.ShowContact)
|
||||
{
|
||||
<div class="alert alert-success">
|
||||
✓ تمایل شما ثبت شد. برای پیگیری استخدام با مرکز تماس بگیرید:
|
||||
<strong>@(f.Phone ?? "شماره ثبت نشده")</strong>
|
||||
@if (!string.IsNullOrEmpty(f.BaleId)) { <text> — بله: @f.BaleId</text> }
|
||||
</div>
|
||||
}
|
||||
@if (Model.Saved)
|
||||
{
|
||||
<div class="alert alert-success">✓ این موقعیت ذخیره شد.</div>
|
||||
}
|
||||
|
||||
<div class="card card-pad">
|
||||
<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>
|
||||
<div class="info-row"><span class="k">حقوق ماهانه</span><span class="v" style="color:var(--primary-dark)">@salary</span></div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(j.Description))
|
||||
{
|
||||
<div class="card card-pad" style="margin-top:16px;">
|
||||
<h3 style="margin-top:0;">شرح موقعیت</h3>
|
||||
<p class="muted" style="margin:0;">@j.Description</p>
|
||||
</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(j.Requirements))
|
||||
{
|
||||
<div class="card card-pad" style="margin-top:16px;">
|
||||
<h3 style="margin-top:0;">شرایط احراز</h3>
|
||||
<p class="muted" style="margin:0;">@j.Requirements</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<div class="card card-pad">
|
||||
<div class="pay" style="font-size:19px; margin-bottom:6px; color:var(--primary-dark); font-weight:800;">@salary</div>
|
||||
<p class="muted" style="font-size:13px; margin-top:0;">@empLabel</p>
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@j.Id"
|
||||
class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||
</form>
|
||||
<div style="display:flex; gap:8px; margin-top:8px;">
|
||||
<form method="post" style="flex:1;">
|
||||
<button type="submit" asp-page-handler="Save" asp-route-id="@j.Id" class="btn btn-outline btn-block">♡ ذخیره</button>
|
||||
</form>
|
||||
<form method="post" style="flex:1;">
|
||||
<button type="submit" asp-page-handler="Dismiss" asp-route-id="@j.Id" class="btn btn-outline btn-block">✕ علاقهمند نیستم</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Jobs;
|
||||
|
||||
public class DetailsModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly InterestService _interest;
|
||||
|
||||
public DetailsModel(AppDbContext db, InterestService interest)
|
||||
{
|
||||
_db = db;
|
||||
_interest = interest;
|
||||
}
|
||||
|
||||
public JobOpening? Job { get; private set; }
|
||||
public bool ShowContact { get; private set; }
|
||||
public bool Saved { get; private set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
await LoadAsync(id);
|
||||
if (Job is null) return NotFound();
|
||||
await _interest.LogJobAsync(InterestEventType.View, id);
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostInterestAsync(int id)
|
||||
{
|
||||
await LoadAsync(id);
|
||||
if (Job is null) return NotFound();
|
||||
await _interest.LogJobAsync(InterestEventType.Apply, id);
|
||||
ShowContact = true;
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostSaveAsync(int id)
|
||||
{
|
||||
await LoadAsync(id);
|
||||
if (Job is null) return NotFound();
|
||||
await _interest.LogJobAsync(InterestEventType.Save, id);
|
||||
Saved = true;
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDismissAsync(int id)
|
||||
{
|
||||
await _interest.LogJobAsync(InterestEventType.Dismiss, id);
|
||||
return RedirectToPage("/Jobs/Index");
|
||||
}
|
||||
|
||||
private async Task LoadAsync(int id)
|
||||
{
|
||||
Job = await _db.JobOpenings
|
||||
.Include(j => j.Facility).ThenInclude(f => f.City)
|
||||
.Include(j => j.Facility).ThenInclude(f => f.District)
|
||||
.Include(j => j.Role)
|
||||
.FirstOrDefaultAsync(j => j.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Jobs.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "موقعیتهای استخدامی";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>موقعیتهای استخدامی</h1>
|
||||
<p class="muted">
|
||||
@JalaliDate.ToPersianDigits(Model.Results.Count.ToString()) موقعیت شغلی پیدا شد
|
||||
@if (Model.NearMeActive)
|
||||
{
|
||||
<span> — مرتبشده بر اساس نزدیکترین به شما 📍</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="layout-2">
|
||||
<aside class="card card-pad filter-card">
|
||||
<h3>فیلترها</h3>
|
||||
<form method="get" id="filterForm">
|
||||
<input type="hidden" name="Lat" value="@Model.Lat" />
|
||||
<input type="hidden" name="Lng" value="@Model.Lng" />
|
||||
<div class="filter-group">
|
||||
@if (Model.NearMeActive)
|
||||
{
|
||||
<a asp-page="/Jobs/Index" asp-route-CityId="@Model.CityId" asp-route-RoleId="@Model.RoleId"
|
||||
class="btn btn-accent btn-block">✓ نزدیکترینها — حذف</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" id="nearMeBtn" class="btn btn-outline btn-block">📍 نزدیک من</button>
|
||||
}
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>شهر</label>
|
||||
<select name="CityId" onchange="this.form.submit()">
|
||||
<option value="">همه شهرها</option>
|
||||
@foreach (var c in Model.Cities)
|
||||
{
|
||||
<option value="@c.Id" selected="@(Model.CityId == c.Id)">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>محله / منطقه</label>
|
||||
<select name="DistrictId" onchange="this.form.submit()">
|
||||
<option value="">همه محلهها</option>
|
||||
@foreach (var d in Model.Districts)
|
||||
{
|
||||
<option value="@d.Id" selected="@(Model.DistrictId == d.Id)">@d.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نقش / رشته</label>
|
||||
<select name="RoleId" onchange="this.form.submit()">
|
||||
<option value="">همه نقشها</option>
|
||||
@foreach (var r in Model.Roles)
|
||||
{
|
||||
<option value="@r.Id" selected="@(Model.RoleId == r.Id)">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع همکاری</label>
|
||||
<select name="EmploymentType" onchange="this.form.submit()">
|
||||
<option value="">همه</option>
|
||||
<option value="0" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.FullTime)">تماموقت</option>
|
||||
<option value="1" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.PartTime)">پارهوقت</option>
|
||||
<option value="2" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.Contract)">قراردادی</option>
|
||||
<option value="3" selected="@(Model.EmploymentType == JobsMedical.Web.Models.EmploymentType.Plan)">طرح</option>
|
||||
</select>
|
||||
</div>
|
||||
<a asp-page="/Jobs/Index" class="btn btn-outline btn-block">حذف فیلترها</a>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<div>
|
||||
@if (Model.Results.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">موقعیتی با این فیلترها پیدا نشد.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var j in Model.Results)
|
||||
{
|
||||
<partial name="_JobCard" model="j" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
var btn = document.getElementById('nearMeBtn');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
if (!navigator.geolocation) { alert('مرورگر شما از موقعیتیابی پشتیبانی نمیکند.'); return; }
|
||||
btn.textContent = 'در حال یافتن موقعیت شما...'; btn.disabled = true;
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
var form = document.getElementById('filterForm');
|
||||
form.querySelector('[name=Lat]').value = pos.coords.latitude;
|
||||
form.querySelector('[name=Lng]').value = pos.coords.longitude;
|
||||
form.submit();
|
||||
}, function () {
|
||||
alert('دسترسی به موقعیت داده نشد.'); btn.textContent = '📍 نزدیک من'; btn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Jobs;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
[BindProperty(SupportsGet = true)] public int? CityId { get; set; }
|
||||
[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 double? Lat { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public double? Lng { get; set; }
|
||||
|
||||
public bool NearMeActive => Lat is not null && Lng is not null;
|
||||
|
||||
public List<JobOpening> Results { get; private set; } = new();
|
||||
public List<City> Cities { get; private set; } = new();
|
||||
public List<District> Districts { get; private set; } = new();
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
Cities = await _db.Cities.Where(c => c.IsActive).OrderBy(c => c.Name).ToListAsync();
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
Districts = await _db.Districts
|
||||
.Where(d => d.IsActive && (CityId == null || d.CityId == CityId))
|
||||
.OrderBy(d => d.Name).ToListAsync();
|
||||
|
||||
var q = _db.JobOpenings
|
||||
.Include(j => j.Facility).ThenInclude(f => f.City)
|
||||
.Include(j => j.Facility).ThenInclude(f => f.District)
|
||||
.Include(j => j.Role)
|
||||
.Where(j => j.Status == ShiftStatus.Open);
|
||||
|
||||
if (CityId is not null) q = q.Where(j => j.Facility.CityId == CityId);
|
||||
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);
|
||||
|
||||
var results = await q.ToListAsync();
|
||||
|
||||
if (NearMeActive)
|
||||
{
|
||||
foreach (var j in results)
|
||||
if (j.Facility.Lat is double flat && j.Facility.Lng is double flng)
|
||||
j.DistanceKm = Geo.DistanceKm(Lat!.Value, Lng!.Value, flat, flng);
|
||||
Results = results.OrderBy(j => j.DistanceKm ?? double.MaxValue)
|
||||
.ThenByDescending(j => j.CreatedAt).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
Results = results.OrderByDescending(j => j.CreatedAt).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Preferences.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "علاقهمندیهای شما";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>علاقهمندیهای شما</h1>
|
||||
<p class="muted">بگو دنبال چه فرصتی هستی تا «همکادر» بهترین شیفتها و موقعیتها را برایت پیشنهاد دهد.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section" style="max-width:560px;">
|
||||
<form method="post" class="card card-pad">
|
||||
<div class="filter-group">
|
||||
<label>نقش / رشته</label>
|
||||
<select name="RoleId">
|
||||
<option value="">مهم نیست</option>
|
||||
@foreach (var r in Model.Roles)
|
||||
{
|
||||
<option value="@r.Id" selected="@(Model.RoleId == r.Id)">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>شهر</label>
|
||||
<select name="CityId">
|
||||
<option value="">مهم نیست</option>
|
||||
@foreach (var c in Model.Cities)
|
||||
{
|
||||
<option value="@c.Id" selected="@(Model.CityId == c.Id)">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>نوع شیفت ترجیحی</label>
|
||||
<select name="PreferredShiftType">
|
||||
<option value="">مهم نیست</option>
|
||||
<option value="0" selected="@(Model.PreferredShiftType == ShiftType.Day)">صبح</option>
|
||||
<option value="1" selected="@(Model.PreferredShiftType == ShiftType.Evening)">عصر</option>
|
||||
<option value="2" selected="@(Model.PreferredShiftType == ShiftType.Night)">شب</option>
|
||||
<option value="3" selected="@(Model.PreferredShiftType == ShiftType.OnCall)">آنکال</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>حداقل حقوق مورد انتظار (تومان)</label>
|
||||
<input type="number" name="MinPay" value="@Model.MinPay" placeholder="مثلاً ۲۰۰۰۰۰۰" dir="ltr" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block btn-lg">ذخیره و دیدن پیشنهادها</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Preferences;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly InterestService _interest;
|
||||
|
||||
public IndexModel(AppDbContext db, InterestService interest)
|
||||
{
|
||||
_db = db;
|
||||
_interest = interest;
|
||||
}
|
||||
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
public List<City> Cities { get; private set; } = new();
|
||||
|
||||
[BindProperty] public int? RoleId { get; set; }
|
||||
[BindProperty] public int? CityId { get; set; }
|
||||
[BindProperty] public ShiftType? PreferredShiftType { get; set; }
|
||||
[BindProperty] public long? MinPay { get; set; }
|
||||
|
||||
public bool Saved { get; private set; }
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
await LoadListsAsync();
|
||||
var prefs = await _interest.GetPreferencesAsync();
|
||||
if (prefs is not null)
|
||||
{
|
||||
RoleId = prefs.RoleId;
|
||||
CityId = prefs.CityId;
|
||||
PreferredShiftType = prefs.PreferredShiftType;
|
||||
MinPay = prefs.MinPay;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await _interest.SavePreferencesAsync(RoleId, CityId, PreferredShiftType, MinPay);
|
||||
// Back to home so the personalized feed is the immediate payoff.
|
||||
TempData["prefsSaved"] = true;
|
||||
return RedirectToPage("/Index");
|
||||
}
|
||||
|
||||
private async Task LoadListsAsync()
|
||||
{
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
Cities = await _db.Cities.Where(c => c.IsActive).OrderBy(c => c.Name).ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@page
|
||||
@model PrivacyModel
|
||||
@{
|
||||
ViewData["Title"] = "Privacy Policy";
|
||||
}
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
|
||||
<p>Use this page to detail your site's privacy policy.</p>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace JobsMedical.Web.Pages;
|
||||
|
||||
public class PrivacyModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
@model JobsMedical.Web.Models.JobOpening
|
||||
@{
|
||||
string empLabel = Model.EmploymentType switch
|
||||
{
|
||||
EmploymentType.FullTime => "تماموقت",
|
||||
EmploymentType.PartTime => "پارهوقت",
|
||||
EmploymentType.Contract => "قراردادی",
|
||||
_ => "طرح",
|
||||
};
|
||||
string salary;
|
||||
if (Model.SalaryMin is null && Model.SalaryMax is null) salary = "توافقی";
|
||||
else if (Model.SalaryMin == Model.SalaryMax) salary = JalaliDate.Toman(Model.SalaryMin) + " ماهانه";
|
||||
else salary = $"از {JalaliDate.ToPersianDigits((Model.SalaryMin ?? 0).ToString("#,0"))} تا {JalaliDate.Toman(Model.SalaryMax)} ماهانه";
|
||||
}
|
||||
<a class="card card-pad shift-card" asp-page="/Jobs/Details" asp-route-id="@Model.Id">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<span class="facility">@Model.Title</span>
|
||||
<span class="badge badge-job">@empLabel</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
@if (Model.Role is not null)
|
||||
{
|
||||
<span class="badge badge-type">@Model.Role.Name</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>
|
||||
@if (Model.DistanceKm is double km)
|
||||
{
|
||||
<div class="row"><span class="badge badge-distance">📍 @JalaliDate.ToPersianDigits(km.ToString("0.#")) کیلومتر از شما</span></div>
|
||||
}
|
||||
<div class="foot">
|
||||
<span class="pay">@salary</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,68 @@
|
||||
@{
|
||||
var title = ViewData["Title"] as string;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@(title is null ? "همکادر | شیفت و استخدام کادر درمان" : title + " | همکادر")</title>
|
||||
<meta name="description" content="@(ViewData["Description"] as string ?? "همکادر؛ سامانه یافتن شیفت و موقعیت استخدامی برای کادر درمان (پزشک، پرستار، ماما و تکنسین) در بیمارستانها و کلینیکهای تهران.")" />
|
||||
@* Preload the body-weight font so the swap from Tahoma happens fast. Vazirmatn is
|
||||
self-hosted under wwwroot/fonts (@@font-face in site.css) — no external CDN. *@
|
||||
<link rel="preload" href="~/fonts/Vazirmatn-Regular.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" asp-page="/Index">
|
||||
<span class="brand-mark">ه</span>
|
||||
<span class="brand-text">همکادر</span>
|
||||
</a>
|
||||
<nav class="main-nav">
|
||||
<a asp-page="/Index">خانه</a>
|
||||
<a asp-page="/Shifts/Index">شیفتها</a>
|
||||
<a asp-page="/Jobs/Index">استخدام</a>
|
||||
<a asp-page="/Calendar/Index">تقویم هفتگی</a>
|
||||
<a asp-page="/Facilities/Index">مراکز درمانی</a>
|
||||
<a asp-page="/Preferences/Index">علاقهمندیها</a>
|
||||
</nav>
|
||||
<div class="header-actions">
|
||||
@if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
@if (User.IsInRole("Admin"))
|
||||
{
|
||||
<a asp-page="/Admin/Index" style="margin-inline-end:14px; font-weight:600;">پنل مدیریت</a>
|
||||
}
|
||||
<a asp-page="/Account/Profile" 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>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="btn btn-outline" asp-page="/Account/Login">ورود</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
@RenderBody()
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<div>
|
||||
<span class="brand-mark sm">ه</span>
|
||||
<strong>همکادر</strong>
|
||||
<p class="muted">سامانه واسط میان کادر درمان و مراکز درمانی برای شیفت و استخدام</p>
|
||||
</div>
|
||||
<div class="muted">© ۱۴۰۵ همکادر — همه حقوق محفوظ است</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
for details on configuring this project to bundle and minify static web assets. */
|
||||
|
||||
a.navbar-brand {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0077cc;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
button.accept-policy {
|
||||
font-size: 1rem;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
line-height: 60px;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
@model JobsMedical.Web.Services.Recommendation
|
||||
@{
|
||||
var s = Model.Shift;
|
||||
var (badgeClass, typeLabel) = s.ShiftType switch
|
||||
{
|
||||
ShiftType.Day => ("badge-day", "صبح"),
|
||||
ShiftType.Evening => ("badge-evening", "عصر"),
|
||||
ShiftType.Night => ("badge-night", "شب"),
|
||||
_ => ("badge-oncall", "آنکال"),
|
||||
};
|
||||
}
|
||||
<a class="card card-pad shift-card" asp-page="/Shifts/Details" asp-route-id="@s.Id">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<span class="facility">@s.Facility?.Name</span>
|
||||
<span class="badge @badgeClass">@typeLabel</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
@if (s.Role is not null)
|
||||
{
|
||||
<span class="badge badge-type">@s.Role.Name</span>
|
||||
}
|
||||
<span>📍 @s.Facility?.City?.Name</span>
|
||||
</div>
|
||||
<div class="row">📅 @JalaliDate.WeekDayName(s.Date)، @JalaliDate.ToLongDate(s.Date) — 🕐 @JalaliDate.Time(s.StartTime)</div>
|
||||
|
||||
@* The "why" — what makes a pattern engine trustworthy: every pick is explained. *@
|
||||
<div class="rec-reasons">
|
||||
@foreach (var reason in Model.Reasons)
|
||||
{
|
||||
<span class="rec-reason">✓ @reason</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<span class="pay">@JalaliDate.Toman(s.PayAmount)</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,37 @@
|
||||
@model JobsMedical.Web.Models.Shift
|
||||
@{
|
||||
var (badgeClass, typeLabel) = Model.ShiftType switch
|
||||
{
|
||||
ShiftType.Day => ("badge-day", "صبح"),
|
||||
ShiftType.Evening => ("badge-evening", "عصر"),
|
||||
ShiftType.Night => ("badge-night", "شب"),
|
||||
_ => ("badge-oncall", "آنکال"),
|
||||
};
|
||||
}
|
||||
<a class="card card-pad shift-card" asp-page="/Shifts/Details" asp-route-id="@Model.Id">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<span class="facility">@Model.Facility?.Name</span>
|
||||
<span class="badge @badgeClass">@typeLabel</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
@if (Model.Role is not null)
|
||||
{
|
||||
<span class="badge badge-type">@Model.Role.Name</span>
|
||||
}
|
||||
<span>📍 @Model.Facility?.City?.Name@(Model.Facility?.District is not null ? "، " + Model.Facility.District.Name : "")</span>
|
||||
@if (Model.Facility?.IsVerified == true)
|
||||
{
|
||||
<span class="badge badge-verified">✓ تأیید شده</span>
|
||||
}
|
||||
</div>
|
||||
@if (Model.DistanceKm is double km)
|
||||
{
|
||||
<div class="row"><span class="badge badge-distance">📍 @JalaliDate.ToPersianDigits(km.ToString("0.#")) کیلومتر از شما</span></div>
|
||||
}
|
||||
<div class="row">📅 @JalaliDate.WeekDayName(Model.Date)، @JalaliDate.ToLongDate(Model.Date)</div>
|
||||
<div class="row">🕐 @JalaliDate.Time(Model.StartTime) تا @JalaliDate.Time(Model.EndTime)</div>
|
||||
<div class="foot">
|
||||
<span class="pay">@JalaliDate.Toman(Model.PayAmount)</span>
|
||||
<span class="btn btn-outline" style="padding: 6px 14px;">جزئیات</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -0,0 +1,2 @@
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>
|
||||
@@ -0,0 +1,119 @@
|
||||
@page "{id:int}"
|
||||
@model JobsMedical.Web.Pages.Shifts.DetailsModel
|
||||
@{
|
||||
var s = Model.Shift!;
|
||||
var f = s.Facility!;
|
||||
ViewData["Title"] = $"شیفت {s.SpecialtyRequired} - {f.Name}";
|
||||
ViewData["Description"] = $"شیفت {s.SpecialtyRequired} در {f.Name}، {f.City?.Name}، تاریخ {JalaliDate.ToLongDate(s.Date)} از ساعت {JalaliDate.Time(s.StartTime)}.";
|
||||
var (badgeClass, typeLabel) = s.ShiftType switch
|
||||
{
|
||||
ShiftType.Day => ("badge-day", "شیفت صبح"),
|
||||
ShiftType.Evening => ("badge-evening", "شیفت عصر"),
|
||||
ShiftType.Night => ("badge-night", "شیفت شب"),
|
||||
_ => ("badge-oncall", "آنکال"),
|
||||
};
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<div class="row" style="display:flex; gap:10px; align-items:center;">
|
||||
<span class="badge @badgeClass">@typeLabel</span>
|
||||
@if (f.IsVerified)
|
||||
{
|
||||
<span class="badge badge-verified">✓ مرکز تأیید شده</span>
|
||||
}
|
||||
</div>
|
||||
<h1 style="margin-top:8px;">@s.SpecialtyRequired — @f.Name</h1>
|
||||
<p class="muted">📍 @f.City?.Name @(string.IsNullOrEmpty(f.Address) ? "" : "، " + f.Address)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="detail-grid">
|
||||
<div>
|
||||
@if (Model.ShowContact)
|
||||
{
|
||||
<div class="alert alert-success">
|
||||
✓ تمایل شما ثبت شد. برای هماهنگی شیفت با مرکز درمانی تماس بگیرید:
|
||||
<strong>@(f.Phone ?? "شماره ثبت نشده")</strong>
|
||||
@if (!string.IsNullOrEmpty(f.BaleId))
|
||||
{
|
||||
<text> — بله: @f.BaleId</text>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card card-pad">
|
||||
<h3 style="margin-top:0;">جزئیات شیفت</h3>
|
||||
<div class="info-row"><span class="k">تاریخ</span><span class="v">@JalaliDate.WeekDayName(s.Date)، @JalaliDate.ToLongDate(s.Date)</span></div>
|
||||
<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>
|
||||
<div class="info-row"><span class="k">حقوق</span><span class="v" style="color:var(--primary-dark)">@JalaliDate.Toman(s.PayAmount)</span></div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(s.Description))
|
||||
{
|
||||
<div class="card card-pad" style="margin-top:16px;">
|
||||
<h3 style="margin-top:0;">توضیحات</h3>
|
||||
<p class="muted" style="margin:0;">@s.Description</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.MoreAtFacility.Count > 0)
|
||||
{
|
||||
<h3 style="margin:26px 0 14px;">شیفتهای دیگر این مرکز</h3>
|
||||
<div class="grid grid-3">
|
||||
@foreach (var more in Model.MoreAtFacility)
|
||||
{
|
||||
<partial name="_ShiftCard" model="more" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside>
|
||||
<div class="card card-pad">
|
||||
<div class="pay" style="font-size:20px; margin-bottom:6px; color:var(--primary-dark); font-weight:800;">
|
||||
@JalaliDate.Toman(s.PayAmount)
|
||||
</div>
|
||||
<p class="muted" style="font-size:13px; margin-top:0;">@(s.PayType == PayType.Negotiable ? "توافقی با مرکز درمانی" : "برای هر شیفت")</p>
|
||||
@if (Model.Saved)
|
||||
{
|
||||
<div class="alert alert-success" style="margin-bottom:12px;">✓ این فرصت ذخیره شد و در پیشنهادهای شما لحاظ میشود.</div>
|
||||
}
|
||||
<form method="post">
|
||||
<button type="submit" asp-page-handler="Interest" asp-route-id="@s.Id"
|
||||
class="btn btn-accent btn-block btn-lg">اعلام تمایل و مشاهده راه ارتباطی</button>
|
||||
</form>
|
||||
<p class="muted center" style="font-size:12px; margin:8px 0;">با اعلام تمایل، اطلاعات تماس مرکز نمایش داده میشود.</p>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<form method="post" style="flex:1;">
|
||||
<button type="submit" asp-page-handler="Save" asp-route-id="@s.Id"
|
||||
class="btn btn-outline btn-block">♡ ذخیره</button>
|
||||
</form>
|
||||
<form method="post" style="flex:1;">
|
||||
<button type="submit" asp-page-handler="Dismiss" asp-route-id="@s.Id"
|
||||
class="btn btn-outline btn-block">✕ علاقهمند نیستم</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad" style="margin-top:16px;">
|
||||
<h3 style="margin-top:0;">موقعیت مکانی</h3>
|
||||
@if (f.Lat is not null && f.Lng is not null)
|
||||
{
|
||||
<div style="background:var(--primary-soft); border-radius:10px; height:170px; display:grid; place-items:center; color:var(--primary-dark); text-align:center; padding:10px;">
|
||||
🗺️<br />نقشه نشان/بلد<br />
|
||||
<small class="muted">@f.Lat، @f.Lng</small>
|
||||
</div>
|
||||
<p class="muted" style="font-size:12px; margin-bottom:0;">نقشه تعاملی در فاز بعد اضافه میشود (Neshan/Balad).</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted" style="margin:0;">مختصات این مرکز هنوز ثبت نشده است.</p>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Shifts;
|
||||
|
||||
public class DetailsModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly InterestService _interest;
|
||||
|
||||
public DetailsModel(AppDbContext db, InterestService interest)
|
||||
{
|
||||
_db = db;
|
||||
_interest = interest;
|
||||
}
|
||||
|
||||
public Shift? Shift { get; private set; }
|
||||
public List<Shift> MoreAtFacility { get; private set; } = new();
|
||||
|
||||
// Set after the visitor taps "interested" — reveals the facility contact (handoff model).
|
||||
public bool ShowContact { get; private set; }
|
||||
public bool Saved { get; private set; }
|
||||
|
||||
public async Task<IActionResult> OnGetAsync(int id)
|
||||
{
|
||||
await LoadAsync(id);
|
||||
if (Shift is null) return NotFound();
|
||||
await _interest.LogAsync(InterestEventType.View, id); // behavioral signal for recommendations
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostInterestAsync(int id)
|
||||
{
|
||||
await LoadAsync(id);
|
||||
if (Shift is null) return NotFound();
|
||||
await _interest.LogAsync(InterestEventType.Apply, id);
|
||||
ShowContact = true; // MVP handoff: reveal contact. Records an Application once auth lands.
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostSaveAsync(int id)
|
||||
{
|
||||
await LoadAsync(id);
|
||||
if (Shift is null) return NotFound();
|
||||
await _interest.LogAsync(InterestEventType.Save, id);
|
||||
Saved = true;
|
||||
return Page();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostDismissAsync(int id)
|
||||
{
|
||||
await _interest.LogAsync(InterestEventType.Dismiss, id);
|
||||
return RedirectToPage("/Shifts/Index"); // not interested → back to the list
|
||||
}
|
||||
|
||||
private async Task LoadAsync(int id)
|
||||
{
|
||||
Shift = await _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Role)
|
||||
.FirstOrDefaultAsync(s => s.Id == id);
|
||||
|
||||
if (Shift is not null)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
MoreAtFacility = await _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Role)
|
||||
.Where(s => s.FacilityId == Shift.FacilityId && s.Id != id
|
||||
&& s.Status == ShiftStatus.Open && s.Date >= today)
|
||||
.OrderBy(s => s.Date).Take(3).ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
@page
|
||||
@model JobsMedical.Web.Pages.Shifts.IndexModel
|
||||
@{
|
||||
ViewData["Title"] = "شیفتهای موجود";
|
||||
}
|
||||
|
||||
<div class="page-head">
|
||||
<div class="container">
|
||||
<h1>شیفتهای موجود</h1>
|
||||
<p class="muted">
|
||||
@JalaliDate.ToPersianDigits(Model.Results.Count.ToString()) شیفت باز پیدا شد
|
||||
@if (Model.NearMeActive)
|
||||
{
|
||||
<span> — مرتبشده بر اساس نزدیکترین به شما 📍</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container section">
|
||||
<div class="layout-2">
|
||||
<aside class="card card-pad filter-card">
|
||||
<h3>فیلترها</h3>
|
||||
<form method="get" id="filterForm">
|
||||
@* Preserves the visitor's coordinates across filter changes when "near me" is on. *@
|
||||
<input type="hidden" name="Lat" value="@Model.Lat" />
|
||||
<input type="hidden" name="Lng" value="@Model.Lng" />
|
||||
|
||||
<div class="filter-group">
|
||||
@if (Model.NearMeActive)
|
||||
{
|
||||
<a asp-page="/Shifts/Index" asp-route-CityId="@Model.CityId" asp-route-RoleId="@Model.RoleId"
|
||||
class="btn btn-accent btn-block">✓ نزدیکترینها — حذف</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" id="nearMeBtn" class="btn btn-outline btn-block">📍 نزدیک من</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>شهر</label>
|
||||
<select name="CityId" onchange="this.form.submit()">
|
||||
<option value="">همه شهرها</option>
|
||||
@foreach (var c in Model.Cities)
|
||||
{
|
||||
<option value="@c.Id" selected="@(Model.CityId == c.Id)">@c.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>محله / منطقه</label>
|
||||
<select name="DistrictId" onchange="this.form.submit()">
|
||||
<option value="">همه محلهها</option>
|
||||
@foreach (var d in Model.Districts)
|
||||
{
|
||||
<option value="@d.Id" selected="@(Model.DistrictId == d.Id)">@d.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نقش / رشته</label>
|
||||
<select name="RoleId" onchange="this.form.submit()">
|
||||
<option value="">همه نقشها</option>
|
||||
@foreach (var r in Model.Roles)
|
||||
{
|
||||
<option value="@r.Id" selected="@(Model.RoleId == r.Id)">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>مرکز درمانی</label>
|
||||
<select name="FacilityId" onchange="this.form.submit()">
|
||||
<option value="">همه مراکز</option>
|
||||
@foreach (var f in Model.Facilities)
|
||||
{
|
||||
<option value="@f.Id" selected="@(Model.FacilityId == f.Id)">@f.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>نوع شیفت</label>
|
||||
<select name="ShiftType" onchange="this.form.submit()">
|
||||
<option value="">همه</option>
|
||||
<option value="0" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Day)">صبح</option>
|
||||
<option value="1" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Evening)">عصر</option>
|
||||
<option value="2" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.Night)">شب</option>
|
||||
<option value="3" selected="@(Model.ShiftType == JobsMedical.Web.Models.ShiftType.OnCall)">آنکال</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label style="display:flex; align-items:center; gap:8px; font-weight:600;">
|
||||
<input type="checkbox" name="PaidOnly" value="true" style="width:auto;"
|
||||
onchange="this.form.submit()" checked="@Model.PaidOnly" />
|
||||
فقط شیفتهای با حقوق مشخص
|
||||
</label>
|
||||
</div>
|
||||
<a asp-page="/Shifts/Index" class="btn btn-outline btn-block">حذف فیلترها</a>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<div>
|
||||
@if (Model.Results.Count == 0)
|
||||
{
|
||||
<div class="card empty-state">
|
||||
شیفتی با این فیلترها پیدا نشد. فیلترها را تغییر بده یا حذف کن.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-3">
|
||||
@foreach (var s in Model.Results)
|
||||
{
|
||||
<partial name="_ShiftCard" model="s" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// "نزدیک من": ask the browser for the visitor's location, then re-run the search
|
||||
// sorted by distance. Coordinates are sent only as query params for this request.
|
||||
var btn = document.getElementById('nearMeBtn');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
if (!navigator.geolocation) { alert('مرورگر شما از موقعیتیابی پشتیبانی نمیکند.'); return; }
|
||||
btn.textContent = 'در حال یافتن موقعیت شما...';
|
||||
btn.disabled = true;
|
||||
navigator.geolocation.getCurrentPosition(function (pos) {
|
||||
var form = document.getElementById('filterForm');
|
||||
form.querySelector('[name=Lat]').value = pos.coords.latitude;
|
||||
form.querySelector('[name=Lng]').value = pos.coords.longitude;
|
||||
form.submit();
|
||||
}, function () {
|
||||
alert('دسترسی به موقعیت داده نشد. لطفاً اجازه دسترسی به موقعیت مکانی را بدهید.');
|
||||
btn.textContent = '📍 نزدیک من';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Pages.Shifts;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
public IndexModel(AppDbContext db) => _db = db;
|
||||
|
||||
[BindProperty(SupportsGet = true)] public int? CityId { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public int? DistrictId { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public int? RoleId { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public int? FacilityId { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public ShiftType? ShiftType { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public bool PaidOnly { get; set; }
|
||||
|
||||
// "Near me": the browser sends the visitor's coordinates and we sort by distance.
|
||||
[BindProperty(SupportsGet = true)] public double? Lat { get; set; }
|
||||
[BindProperty(SupportsGet = true)] public double? Lng { get; set; }
|
||||
|
||||
public bool NearMeActive => Lat is not null && Lng is not null;
|
||||
|
||||
public List<Shift> Results { get; private set; } = new();
|
||||
public List<City> Cities { get; private set; } = new();
|
||||
public List<District> Districts { get; private set; } = new();
|
||||
public List<Role> Roles { get; private set; } = new();
|
||||
public List<Facility> Facilities { get; private set; } = new();
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
|
||||
Cities = await _db.Cities.Where(c => c.IsActive).OrderBy(c => c.Name).ToListAsync();
|
||||
Roles = await _db.Roles.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToListAsync();
|
||||
Districts = await _db.Districts
|
||||
.Where(d => d.IsActive && (CityId == null || d.CityId == CityId))
|
||||
.OrderBy(d => d.Name).ToListAsync();
|
||||
Facilities = await _db.Facilities
|
||||
.Where(f => CityId == null || f.CityId == CityId)
|
||||
.OrderBy(f => f.Name).ToListAsync();
|
||||
|
||||
var q = _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Facility).ThenInclude(f => f.District)
|
||||
.Include(s => s.Role)
|
||||
.Where(s => s.Status == ShiftStatus.Open && s.Date >= today);
|
||||
|
||||
if (CityId is not null) q = q.Where(s => s.Facility.CityId == CityId);
|
||||
if (DistrictId is not null) q = q.Where(s => s.Facility.DistrictId == DistrictId);
|
||||
if (RoleId is not null) q = q.Where(s => s.RoleId == RoleId);
|
||||
if (FacilityId is not null) q = q.Where(s => s.FacilityId == FacilityId);
|
||||
if (ShiftType is not null) q = q.Where(s => s.ShiftType == ShiftType);
|
||||
if (PaidOnly) q = q.Where(s => s.PayAmount != null);
|
||||
|
||||
var results = await q.ToListAsync();
|
||||
|
||||
if (NearMeActive)
|
||||
{
|
||||
// Compute distance to each facility, then nearest-first (shifts without coords last).
|
||||
foreach (var s in results)
|
||||
{
|
||||
if (s.Facility.Lat is double flat && s.Facility.Lng is double flng)
|
||||
s.DistanceKm = Geo.DistanceKm(Lat!.Value, Lng!.Value, flat, flng);
|
||||
}
|
||||
Results = results
|
||||
.OrderBy(s => s.DistanceKm ?? double.MaxValue)
|
||||
.ThenBy(s => s.Date).ThenBy(s => s.StartTime)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
Results = results.OrderBy(s => s.Date).ThenBy(s => s.StartTime).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@using JobsMedical.Web
|
||||
@using JobsMedical.Web.Models
|
||||
@using JobsMedical.Web.Services
|
||||
@namespace JobsMedical.Web.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Services;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorPages();
|
||||
|
||||
// Interest tracking + recommendation engine.
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddScoped<VisitorContext>();
|
||||
builder.Services.AddScoped<InterestService>();
|
||||
builder.Services.AddScoped<RecommendationService>();
|
||||
builder.Services.AddScoped<OtpService>();
|
||||
// Listing parser: heuristic now; swap for an LLM-backed IListingParser later.
|
||||
builder.Services.AddSingleton<IListingParser, HeuristicListingParser>();
|
||||
|
||||
// Phone-OTP cookie auth.
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(o =>
|
||||
{
|
||||
o.LoginPath = "/Account/Login";
|
||||
o.AccessDeniedPath = "/Account/Login";
|
||||
o.ExpireTimeSpan = TimeSpan.FromDays(30);
|
||||
o.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
// Emit Persian/Arabic characters directly in HTML instead of \u-style entities.
|
||||
builder.Services.AddSingleton(HtmlEncoder.Create(
|
||||
UnicodeRanges.BasicLatin, UnicodeRanges.Arabic, UnicodeRanges.ArabicSupplement,
|
||||
UnicodeRanges.ArabicExtendedA, UnicodeRanges.GeneralPunctuation));
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>(opt =>
|
||||
opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Apply migrations + seed on startup (fine for MVP single-instance deploy).
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
db.Database.Migrate();
|
||||
await SeedData.EnsureSeededAsync(db);
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
// Assign every visitor a stable cookie id so we can track interest from the first visit.
|
||||
app.UseMiddleware<VisitorCookieMiddleware>();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorPages()
|
||||
.WithStaticAssets();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5020",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7025;http://localhost:5020",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
public static class Geo
|
||||
{
|
||||
private const double EarthRadiusKm = 6371.0;
|
||||
|
||||
/// <summary>Great-circle (Haversine) distance in kilometers between two lat/lng points.</summary>
|
||||
public static double DistanceKm(double lat1, double lng1, double lat2, double lng2)
|
||||
{
|
||||
double dLat = ToRad(lat2 - lat1);
|
||||
double dLng = ToRad(lng2 - lng1);
|
||||
double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2)
|
||||
+ Math.Cos(ToRad(lat1)) * Math.Cos(ToRad(lat2))
|
||||
* Math.Sin(dLng / 2) * Math.Sin(dLng / 2);
|
||||
return EarthRadiusKm * 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
|
||||
}
|
||||
|
||||
private static double ToRad(double deg) => deg * Math.PI / 180.0;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Persists visitor preferences and behavioral events. Creates the <see cref="Visitor"/> row
|
||||
/// lazily on first write, so anonymous browsing doesn't hit the DB on every request.
|
||||
/// </summary>
|
||||
public class InterestService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly VisitorContext _visitor;
|
||||
|
||||
public InterestService(AppDbContext db, VisitorContext visitor)
|
||||
{
|
||||
_db = db;
|
||||
_visitor = visitor;
|
||||
}
|
||||
|
||||
public string VisitorId => _visitor.VisitorId;
|
||||
|
||||
private async Task EnsureVisitorAsync()
|
||||
{
|
||||
var id = VisitorId;
|
||||
if (string.IsNullOrEmpty(id)) return;
|
||||
var exists = await _db.Visitors.AnyAsync(v => v.Id == id);
|
||||
if (!exists)
|
||||
{
|
||||
_db.Visitors.Add(new Visitor { Id = id });
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _db.Visitors.Where(v => v.Id == id)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(v => v.LastSeenAt, DateTime.UtcNow));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogAsync(InterestEventType type, int shiftId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(VisitorId)) return;
|
||||
await EnsureVisitorAsync();
|
||||
_db.InterestEvents.Add(new InterestEvent
|
||||
{
|
||||
VisitorId = VisitorId,
|
||||
ShiftId = shiftId,
|
||||
EventType = type,
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task LogJobAsync(InterestEventType type, int jobOpeningId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(VisitorId)) return;
|
||||
await EnsureVisitorAsync();
|
||||
_db.InterestEvents.Add(new InterestEvent
|
||||
{
|
||||
VisitorId = VisitorId,
|
||||
JobOpeningId = jobOpeningId,
|
||||
EventType = type,
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public Task<UserPreferences?> GetPreferencesAsync()
|
||||
{
|
||||
var id = VisitorId;
|
||||
if (string.IsNullOrEmpty(id)) return Task.FromResult<UserPreferences?>(null);
|
||||
return _db.UserPreferences.AsNoTracking().FirstOrDefaultAsync(p => p.VisitorId == id);
|
||||
}
|
||||
|
||||
public async Task SavePreferencesAsync(int? roleId, int? cityId, ShiftType? shiftType, long? minPay)
|
||||
{
|
||||
await EnsureVisitorAsync();
|
||||
var prefs = await _db.UserPreferences.FirstOrDefaultAsync(p => p.VisitorId == VisitorId);
|
||||
if (prefs is null)
|
||||
{
|
||||
prefs = new UserPreferences { VisitorId = VisitorId };
|
||||
_db.UserPreferences.Add(prefs);
|
||||
}
|
||||
prefs.RoleId = roleId;
|
||||
prefs.CityId = cityId;
|
||||
prefs.PreferredShiftType = shiftType;
|
||||
prefs.MinPay = minPay;
|
||||
prefs.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>Recent events for this visitor (newest first) — the behavioral signal.</summary>
|
||||
public Task<List<InterestEvent>> RecentEventsAsync(int take = 100)
|
||||
{
|
||||
var id = VisitorId;
|
||||
if (string.IsNullOrEmpty(id)) return Task.FromResult(new List<InterestEvent>());
|
||||
return _db.InterestEvents.AsNoTracking()
|
||||
.Where(e => e.VisitorId == id)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Jalali (Shamsi) date helpers built on .NET's <see cref="PersianCalendar"/>.
|
||||
/// Rule of thumb in this app: store Gregorian, display Jalali.
|
||||
/// </summary>
|
||||
public static class JalaliDate
|
||||
{
|
||||
private static readonly PersianCalendar Pc = new();
|
||||
|
||||
private static readonly string[] MonthNames =
|
||||
{
|
||||
"فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
|
||||
"مهر", "آبان", "آذر", "دی", "بهمن", "اسفند"
|
||||
};
|
||||
|
||||
// PersianCalendar: 0=Saturday .. 6=Friday is NOT how DayOfWeek maps; map explicitly.
|
||||
private static readonly string[] WeekDayNames =
|
||||
{
|
||||
"شنبه", "یکشنبه", "دوشنبه", "سهشنبه", "چهارشنبه", "پنجشنبه", "جمعه"
|
||||
};
|
||||
|
||||
private static readonly char[] PersianDigits = { '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' };
|
||||
|
||||
/// <summary>Convert Latin digits in a string to Persian digits.</summary>
|
||||
public static string ToPersianDigits(string input)
|
||||
{
|
||||
var chars = input.ToCharArray();
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
if (chars[i] is >= '0' and <= '9')
|
||||
chars[i] = PersianDigits[chars[i] - '0'];
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
/// <summary>e.g. "۱۵ خرداد ۱۴۰۵".</summary>
|
||||
public static string ToLongDate(DateOnly date)
|
||||
{
|
||||
var dt = date.ToDateTime(TimeOnly.MinValue);
|
||||
var y = Pc.GetYear(dt);
|
||||
var m = Pc.GetMonth(dt);
|
||||
var d = Pc.GetDayOfMonth(dt);
|
||||
return ToPersianDigits($"{d} {MonthNames[m - 1]} {y}");
|
||||
}
|
||||
|
||||
/// <summary>e.g. "۱۴۰۵/۰۳/۱۵".</summary>
|
||||
public static string ToShortDate(DateOnly date)
|
||||
{
|
||||
var dt = date.ToDateTime(TimeOnly.MinValue);
|
||||
return ToPersianDigits($"{Pc.GetYear(dt):0000}/{Pc.GetMonth(dt):00}/{Pc.GetDayOfMonth(dt):00}");
|
||||
}
|
||||
|
||||
/// <summary>Persian weekday name, e.g. "سهشنبه".</summary>
|
||||
public static string WeekDayName(DateOnly date)
|
||||
{
|
||||
var dt = date.ToDateTime(TimeOnly.MinValue);
|
||||
return WeekDayNames[WeekIndex(dt)];
|
||||
}
|
||||
|
||||
/// <summary>0 = Saturday (شنبه, start of Persian week) .. 6 = Friday (جمعه).</summary>
|
||||
public static int WeekIndex(DateTime dt) => ((int)dt.DayOfWeek + 1) % 7;
|
||||
|
||||
/// <summary>The Saturday that starts the Persian week containing <paramref name="date"/>.</summary>
|
||||
public static DateOnly StartOfPersianWeek(DateOnly date)
|
||||
{
|
||||
var idx = WeekIndex(date.ToDateTime(TimeOnly.MinValue));
|
||||
return date.AddDays(-idx);
|
||||
}
|
||||
|
||||
/// <summary>Persian month name for a given date.</summary>
|
||||
public static string MonthName(DateOnly date)
|
||||
{
|
||||
var dt = date.ToDateTime(TimeOnly.MinValue);
|
||||
return MonthNames[Pc.GetMonth(dt) - 1];
|
||||
}
|
||||
|
||||
/// <summary>Just the day-of-month in Persian digits.</summary>
|
||||
public static string DayOfMonth(DateOnly date)
|
||||
{
|
||||
var dt = date.ToDateTime(TimeOnly.MinValue);
|
||||
return ToPersianDigits(Pc.GetDayOfMonth(dt).ToString());
|
||||
}
|
||||
|
||||
/// <summary>Format a time as "۰۸:۰۰".</summary>
|
||||
public static string Time(TimeOnly t) => ToPersianDigits(t.ToString("HH\\:mm"));
|
||||
|
||||
/// <summary>Format a Toman amount, e.g. "۱٬۵۰۰٬۰۰۰ تومان" or "توافقی" if null.</summary>
|
||||
public static string Toman(long? amount)
|
||||
=> amount is null ? "توافقی" : ToPersianDigits(amount.Value.ToString("#,0")) + " تومان";
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using JobsMedical.Web.Models;
|
||||
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
/// <summary>Structured guess extracted from a raw channel post. All fields are best-effort.</summary>
|
||||
public class ParsedListing
|
||||
{
|
||||
public ListingKind Kind { get; set; } = ListingKind.Shift;
|
||||
public string? RoleName { get; set; }
|
||||
public ShiftType? ShiftType { get; set; }
|
||||
public EmploymentType? EmploymentType { get; set; }
|
||||
public long? PayAmount { get; set; } // shift pay or single salary figure
|
||||
public bool PayNegotiable { get; set; }
|
||||
public string? CityName { get; set; }
|
||||
public string? DistrictName { get; set; }
|
||||
public string? Phone { get; set; }
|
||||
public List<string> Notes { get; set; } = new(); // what was/wasn't detected (shown to admin)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns a messy Persian channel/Divar post into a structured listing guess. This is the
|
||||
/// Stage-1 implementation: transparent keyword + regex heuristics, no AI dependency (important
|
||||
/// since LLM APIs are blocked from Iran). A future LlmListingParser can implement the same
|
||||
/// interface and be swapped in via DI without touching the admin queue.
|
||||
/// </summary>
|
||||
public interface IListingParser
|
||||
{
|
||||
ParsedListing Parse(string rawText, IEnumerable<string> knownRoles,
|
||||
IEnumerable<string> knownCities, IEnumerable<string> knownDistricts);
|
||||
}
|
||||
|
||||
public class HeuristicListingParser : IListingParser
|
||||
{
|
||||
public ParsedListing Parse(string raw, IEnumerable<string> knownRoles,
|
||||
IEnumerable<string> knownCities, IEnumerable<string> knownDistricts)
|
||||
{
|
||||
var p = new ParsedListing();
|
||||
var text = Normalize(raw);
|
||||
|
||||
// --- Kind: shift vs hiring ---
|
||||
bool jobSignals = ContainsAny(text, "استخدام", "جذب", "دعوت به همکاری", "تمام وقت", "تماموقت", "قرارداد", "ماهانه", "حقوق ثابت");
|
||||
bool shiftSignals = ContainsAny(text, "شیفت", "آنکال", "انکال", "نوبت", "کشیک");
|
||||
p.Kind = (jobSignals && !shiftSignals) ? ListingKind.Job : ListingKind.Shift;
|
||||
p.Notes.Add(p.Kind == ListingKind.Job ? "نوع: استخدام (تشخیص خودکار)" : "نوع: شیفت (تشخیص خودکار)");
|
||||
|
||||
// --- Role (longest match first so «پزشک متخصص» beats «پزشک») ---
|
||||
foreach (var role in knownRoles.OrderByDescending(r => r.Length))
|
||||
{
|
||||
if (text.Contains(Normalize(role))) { p.RoleName = role; break; }
|
||||
}
|
||||
if (p.RoleName is null && ContainsAny(text, "پزشک", "دکتر")) p.RoleName = "پزشک عمومی";
|
||||
p.Notes.Add(p.RoleName is null ? "نقش: تشخیص داده نشد" : $"نقش: {p.RoleName}");
|
||||
|
||||
// --- Shift type ---
|
||||
if (ContainsAny(text, "آنکال", "انکال")) p.ShiftType = Models.ShiftType.OnCall;
|
||||
else if (text.Contains("شب")) p.ShiftType = Models.ShiftType.Night;
|
||||
else if (text.Contains("عصر")) p.ShiftType = Models.ShiftType.Evening;
|
||||
else if (ContainsAny(text, "صبح", "روز")) p.ShiftType = Models.ShiftType.Day;
|
||||
|
||||
// --- Employment type ---
|
||||
if (ContainsAny(text, "پاره وقت", "پارهوقت", "پارت تایم")) p.EmploymentType = Models.EmploymentType.PartTime;
|
||||
else if (text.Contains("طرح")) p.EmploymentType = Models.EmploymentType.Plan;
|
||||
else if (text.Contains("قرارداد")) p.EmploymentType = Models.EmploymentType.Contract;
|
||||
else if (ContainsAny(text, "تمام وقت", "تماموقت")) p.EmploymentType = Models.EmploymentType.FullTime;
|
||||
|
||||
// --- City / district ---
|
||||
p.CityName = knownCities.FirstOrDefault(c => text.Contains(Normalize(c)));
|
||||
p.DistrictName = knownDistricts.OrderByDescending(d => d.Length)
|
||||
.FirstOrDefault(d => text.Contains(Normalize(d)));
|
||||
|
||||
// --- Pay ---
|
||||
if (ContainsAny(text, "توافقی", "توافق")) { p.PayNegotiable = true; p.Notes.Add("حقوق: توافقی"); }
|
||||
else
|
||||
{
|
||||
var amount = ExtractAmount(text);
|
||||
if (amount is not null) { p.PayAmount = amount; p.Notes.Add($"حقوق تخمینی: {amount:#,0} تومان"); }
|
||||
else p.Notes.Add("حقوق: تشخیص داده نشد");
|
||||
}
|
||||
|
||||
// --- Phone ---
|
||||
var phone = Regex.Match(ToLatinDigits(text), @"0?9\d{9}");
|
||||
if (phone.Success) p.Phone = phone.Value;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>Pull a Toman figure out of free text, handling «میلیون» and Persian digits.</summary>
|
||||
private static long? ExtractAmount(string text)
|
||||
{
|
||||
var latin = ToLatinDigits(text);
|
||||
// e.g. "۲ میلیون" / "2.5 میلیون"
|
||||
var million = Regex.Match(latin, @"(\d+(?:[.,]\d+)?)\s*میلیون");
|
||||
if (million.Success && double.TryParse(million.Groups[1].Value.Replace(",", "."),
|
||||
System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var m))
|
||||
return (long)(m * 1_000_000);
|
||||
|
||||
// Otherwise the largest plain number that looks like money (>= 6 digits after removing separators).
|
||||
long best = 0;
|
||||
foreach (Match num in Regex.Matches(latin, @"[\d٬,،.]{6,}"))
|
||||
{
|
||||
var digits = Regex.Replace(num.Value, @"[^\d]", "");
|
||||
if (digits.Length >= 6 && long.TryParse(digits, out var v) && v > best) best = v;
|
||||
}
|
||||
return best > 0 ? best : null;
|
||||
}
|
||||
|
||||
private static string Normalize(string s) => s
|
||||
.Replace('ي', 'ی').Replace('ك', 'ک').Replace('', ' ').Trim();
|
||||
|
||||
private static bool ContainsAny(string text, params string[] needles)
|
||||
=> needles.Any(n => text.Contains(n));
|
||||
|
||||
private static string ToLatinDigits(string s)
|
||||
{
|
||||
var chars = s.ToCharArray();
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
{
|
||||
if (chars[i] >= '۰' && chars[i] <= '۹') chars[i] = (char)('0' + (chars[i] - '۰'));
|
||||
else if (chars[i] >= '٠' && chars[i] <= '٩') chars[i] = (char)('0' + (chars[i] - '٠'));
|
||||
}
|
||||
return new string(chars);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// One-time-code issuing/verification. Codes live in memory for 5 minutes. In dev the code is
|
||||
/// returned to the caller so it can be shown on screen; in production this is where an Iranian
|
||||
/// SMS gateway (Kavenegar / SMS.ir) would send the code instead.
|
||||
/// </summary>
|
||||
public class OtpService
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
public OtpService(IMemoryCache cache) => _cache = cache;
|
||||
|
||||
private static string Key(string phone) => $"otp:{Normalize(phone)}";
|
||||
|
||||
/// <summary>Generate, store, and (in dev) return a 5-digit code for the phone.</summary>
|
||||
public string Issue(string phone)
|
||||
{
|
||||
var code = Random.Shared.Next(10000, 100000).ToString();
|
||||
_cache.Set(Key(phone), code, TimeSpan.FromMinutes(5));
|
||||
// TODO(prod): send `code` via Kavenegar/SMS.ir instead of returning it.
|
||||
return code;
|
||||
}
|
||||
|
||||
public bool Verify(string phone, string code)
|
||||
{
|
||||
if (_cache.TryGetValue(Key(phone), out string? stored) && stored == code?.Trim())
|
||||
{
|
||||
_cache.Remove(Key(phone));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Normalize Iranian mobile numbers (Persian digits → Latin, strip spaces).</summary>
|
||||
public static string Normalize(string phone)
|
||||
{
|
||||
var chars = phone.Trim().ToCharArray();
|
||||
for (var i = 0; i < chars.Length; i++)
|
||||
if (chars[i] >= '۰' && chars[i] <= '۹') chars[i] = (char)('0' + (chars[i] - '۰'));
|
||||
return new string(chars).Replace(" ", "").Replace("-", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
using JobsMedical.Web.Data;
|
||||
using JobsMedical.Web.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
public record Recommendation(Shift Shift, double Score, List<string> Reasons);
|
||||
|
||||
/// <summary>
|
||||
/// Stage 1 of the recommendation engine: a transparent, rule-based pattern engine.
|
||||
/// It scores open shifts against a visitor's explicit preferences AND their recent behavior
|
||||
/// (which roles/facilities/shift-types they keep engaging with), and returns the top matches
|
||||
/// each with a human-readable reason. No ML/AI infra required — works from the first visit,
|
||||
/// and every result is explainable. Behavioral data logged now feeds the ML stages later.
|
||||
/// </summary>
|
||||
public class RecommendationService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly InterestService _interest;
|
||||
|
||||
public RecommendationService(AppDbContext db, InterestService interest)
|
||||
{
|
||||
_db = db;
|
||||
_interest = interest;
|
||||
}
|
||||
|
||||
// Tunable weights — the whole point of a pattern engine is that these are legible.
|
||||
private const double WRolePref = 40, WRoleBehavior = 15;
|
||||
private const double WCityPref = 20;
|
||||
private const double WShiftTypePref = 15, WShiftTypeBehavior = 8;
|
||||
private const double WPayMeetsExpectation = 10;
|
||||
private const double WFacilityAffinity = 12;
|
||||
private const double WFreshness = 5, WSoon = 6;
|
||||
private const double PenaltyDismissedFacility = 60;
|
||||
|
||||
public async Task<List<Recommendation>> GetForVisitorAsync(int take = 6)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var prefs = await _interest.GetPreferencesAsync();
|
||||
var events = await _interest.RecentEventsAsync(150);
|
||||
|
||||
var candidates = await _db.Shifts
|
||||
.Include(s => s.Facility).ThenInclude(f => f.City)
|
||||
.Include(s => s.Role)
|
||||
.Where(s => s.Status == ShiftStatus.Open && s.Date >= today)
|
||||
.ToListAsync();
|
||||
|
||||
// Cold start: no preferences and no behavior → just show the freshest opportunities.
|
||||
if (prefs is null && events.Count == 0)
|
||||
{
|
||||
return candidates
|
||||
.OrderBy(s => s.Date).ThenBy(s => s.StartTime)
|
||||
.Take(take)
|
||||
.Select(s => new Recommendation(s, 0, new() { "جدیدترین فرصتها" }))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Derive behavioral affinities from the event log (shift events only — jobs are separate).
|
||||
var shiftEvents = events.Where(e => e.ShiftId is not null).ToList();
|
||||
var eventShiftIds = shiftEvents.Select(e => e.ShiftId!.Value).Distinct().ToList();
|
||||
var eventShifts = candidates.Where(s => eventShiftIds.Contains(s.Id))
|
||||
.Concat(await _db.Shifts.Include(s => s.Role)
|
||||
.Where(s => eventShiftIds.Contains(s.Id)).ToListAsync())
|
||||
.DistinctBy(s => s.Id)
|
||||
.ToDictionary(s => s.Id);
|
||||
|
||||
var positive = new[] { InterestEventType.View, InterestEventType.Click,
|
||||
InterestEventType.Save, InterestEventType.Apply };
|
||||
|
||||
var roleAffinity = TopBy(shiftEvents, positive, eventShifts, s => s.RoleId);
|
||||
var shiftTypeAffinity = TopBy(shiftEvents, positive, eventShifts, s => (int)s.ShiftType);
|
||||
var facilityAffinity = TopBy(shiftEvents, positive, eventShifts, s => s.FacilityId);
|
||||
|
||||
var dismissedFacilities = shiftEvents
|
||||
.Where(e => e.EventType is InterestEventType.Dismiss or InterestEventType.HideFacility)
|
||||
.Select(e => eventShifts.TryGetValue(e.ShiftId!.Value, out var s) ? s.FacilityId : 0)
|
||||
.Where(id => id != 0).ToHashSet();
|
||||
|
||||
var results = new List<Recommendation>();
|
||||
foreach (var s in candidates)
|
||||
{
|
||||
double score = 0;
|
||||
var reasons = new List<string>();
|
||||
|
||||
if (prefs?.RoleId is int pr && pr == s.RoleId)
|
||||
{ score += WRolePref; reasons.Add($"متناسب با نقش مورد علاقه شما ({s.Role.Name})"); }
|
||||
else if (roleAffinity.Contains(s.RoleId))
|
||||
{ score += WRoleBehavior; reasons.Add($"چون به فرصتهای «{s.Role.Name}» علاقه نشان دادی"); }
|
||||
|
||||
if (prefs?.CityId is int pc && pc == s.Facility.CityId)
|
||||
{ score += WCityPref; reasons.Add($"در شهر مورد علاقه شما ({s.Facility.City.Name})"); }
|
||||
|
||||
if (prefs?.PreferredShiftType is ShiftType pst && pst == s.ShiftType)
|
||||
{ score += WShiftTypePref; reasons.Add($"نوع شیفت دلخواه شما ({ShiftTypeLabel(s.ShiftType)})"); }
|
||||
else if (shiftTypeAffinity.Contains((int)s.ShiftType))
|
||||
{ score += WShiftTypeBehavior; reasons.Add($"شبیه شیفتهایی که دیدهای ({ShiftTypeLabel(s.ShiftType)})"); }
|
||||
|
||||
if (prefs?.MinPay is long min && s.PayAmount is long pay && pay >= min)
|
||||
{ score += WPayMeetsExpectation; reasons.Add("حقوق بالاتر از حد انتظار شما"); }
|
||||
|
||||
if (facilityAffinity.Contains(s.FacilityId))
|
||||
{ score += WFacilityAffinity; reasons.Add($"از مرکزی که قبلاً به آن علاقه نشان دادی ({s.Facility.Name})"); }
|
||||
|
||||
if (dismissedFacilities.Contains(s.FacilityId))
|
||||
score -= PenaltyDismissedFacility;
|
||||
|
||||
// Sooner shifts and freshly posted ones get a small nudge.
|
||||
var daysOut = s.Date.DayNumber - today.DayNumber;
|
||||
if (daysOut <= 3) score += WSoon;
|
||||
if ((DateTime.UtcNow - s.CreatedAt).TotalDays <= 2) score += WFreshness;
|
||||
|
||||
if (reasons.Count == 0) reasons.Add("پیشنهاد بر اساس فعالیت شما");
|
||||
results.Add(new Recommendation(s, score, reasons));
|
||||
}
|
||||
|
||||
return results
|
||||
.Where(r => r.Score > 0)
|
||||
.OrderByDescending(r => r.Score).ThenBy(r => r.Shift.Date)
|
||||
.Take(take)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Keys the visitor engaged with most (positive events), top 3.</summary>
|
||||
private static HashSet<int> TopBy(
|
||||
List<InterestEvent> events, InterestEventType[] positive,
|
||||
Dictionary<int, Shift> shiftById, Func<Shift, int> key)
|
||||
{
|
||||
return events
|
||||
.Where(e => e.ShiftId is not null && positive.Contains(e.EventType)
|
||||
&& shiftById.ContainsKey(e.ShiftId.Value))
|
||||
.GroupBy(e => key(shiftById[e.ShiftId!.Value]))
|
||||
.OrderByDescending(g => g.Count())
|
||||
.Take(3)
|
||||
.Select(g => g.Key)
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
private static string ShiftTypeLabel(ShiftType t) => t switch
|
||||
{
|
||||
ShiftType.Day => "صبح",
|
||||
ShiftType.Evening => "عصر",
|
||||
ShiftType.Night => "شب",
|
||||
_ => "آنکال",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace JobsMedical.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the current anonymous visitor id for the request. The id is created and written to
|
||||
/// the <c>hk_vid</c> cookie by <see cref="VisitorCookieMiddleware"/>; this scoped accessor just
|
||||
/// reads it back out of <c>HttpContext.Items</c>.
|
||||
/// </summary>
|
||||
public class VisitorContext
|
||||
{
|
||||
public const string CookieName = "hk_vid";
|
||||
public const string ItemKey = "VisitorId";
|
||||
|
||||
private readonly IHttpContextAccessor _http;
|
||||
public VisitorContext(IHttpContextAccessor http) => _http = http;
|
||||
|
||||
public string VisitorId =>
|
||||
_http.HttpContext?.Items[ItemKey] as string ?? "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures every visitor carries a stable <c>hk_vid</c> cookie (a GUID) so we can track interest
|
||||
/// from the first visit, before any login. On login we link this id to the user account.
|
||||
/// </summary>
|
||||
public class VisitorCookieMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
public VisitorCookieMiddleware(RequestDelegate next) => _next = next;
|
||||
|
||||
public async Task InvokeAsync(HttpContext ctx)
|
||||
{
|
||||
var id = ctx.Request.Cookies[VisitorContext.CookieName];
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = Guid.NewGuid().ToString();
|
||||
ctx.Response.Cookies.Append(VisitorContext.CookieName, id, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
IsEssential = true, // functional, not tracking-consent gated
|
||||
SameSite = SameSiteMode.Lax,
|
||||
MaxAge = TimeSpan.FromDays(365),
|
||||
});
|
||||
}
|
||||
ctx.Items[VisitorContext.ItemKey] = id;
|
||||
await _next(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"DetailedErrors": true,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5433;Database=jobsmedical;Username=jobsmedical;Password=jobsmedical_dev"
|
||||
},
|
||||
"Auth": {
|
||||
"AdminPhone": "09120000000"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/* Self-hosted Vazirmatn — no external CDN (must work behind filtering).
|
||||
font-display: swap → text renders immediately in Tahoma, swaps to Vazir when ready. */
|
||||
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 400;
|
||||
src: url("/fonts/Vazirmatn-Regular.woff2") format("woff2"); font-display: swap; }
|
||||
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 500;
|
||||
src: url("/fonts/Vazirmatn-Medium.woff2") format("woff2"); font-display: swap; }
|
||||
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 600;
|
||||
src: url("/fonts/Vazirmatn-SemiBold.woff2") format("woff2"); font-display: swap; }
|
||||
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 700;
|
||||
src: url("/fonts/Vazirmatn-Bold.woff2") format("woff2"); font-display: swap; }
|
||||
@font-face { font-family: "Vazirmatn"; font-style: normal; font-weight: 800 900;
|
||||
src: url("/fonts/Vazirmatn-Black.woff2") format("woff2"); font-display: swap; }
|
||||
|
||||
:root {
|
||||
--bg: #f4f7f9;
|
||||
--surface: #ffffff;
|
||||
--ink: #16242e;
|
||||
--muted: #6b7c88;
|
||||
--line: #e4eaee;
|
||||
--primary: #0e8f8a; /* medical teal */
|
||||
--primary-dark: #0a6f6b;
|
||||
--primary-soft: #e3f4f3;
|
||||
--accent: #f0843e;
|
||||
--danger: #d65745;
|
||||
--night: #3d4f9e;
|
||||
--radius: 14px;
|
||||
--shadow: 0 2px 10px rgba(16, 40, 50, .06);
|
||||
--shadow-lg: 0 8px 28px rgba(16, 40, 50, .10);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Vazirmatn", Tahoma, "Segoe UI", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
line-height: 1.7;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.container { max-width: 1120px; margin: 0 auto; padding: 0 20px; }
|
||||
.muted { color: var(--muted); }
|
||||
.center { text-align: center; }
|
||||
|
||||
/* ---------- Header ---------- */
|
||||
.site-header {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.header-inner { display: flex; align-items: center; gap: 28px; height: 64px; }
|
||||
.brand { display: flex; align-items: center; gap: 8px; font-weight: 800; font-size: 19px; }
|
||||
.brand-mark {
|
||||
display: inline-grid; place-items: center;
|
||||
width: 30px; height: 30px; border-radius: 9px;
|
||||
background: var(--primary); color: #fff; font-weight: 900; font-size: 22px;
|
||||
}
|
||||
.brand-mark.sm { width: 24px; height: 24px; font-size: 17px; border-radius: 7px; }
|
||||
.main-nav { display: flex; gap: 22px; margin-inline-start: 8px; flex: 1; }
|
||||
.main-nav a { color: var(--muted); font-weight: 600; transition: color .15s; }
|
||||
.main-nav a:hover { color: var(--primary); }
|
||||
.header-actions { margin-inline-start: auto; }
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 9px 18px; border-radius: 10px; font-weight: 700; font-size: 14px;
|
||||
border: 1px solid transparent; cursor: pointer; transition: all .15s;
|
||||
}
|
||||
.btn-primary { background: var(--primary); color: #fff; }
|
||||
.btn-primary:hover { background: var(--primary-dark); }
|
||||
.btn-accent { background: var(--accent); color: #fff; }
|
||||
.btn-accent:hover { background: #d96f28; }
|
||||
.btn-outline { background: transparent; border-color: var(--primary); color: var(--primary); }
|
||||
.btn-outline:hover { background: var(--primary-soft); }
|
||||
.btn-block { width: 100%; justify-content: center; }
|
||||
.btn-lg { padding: 13px 26px; font-size: 16px; }
|
||||
|
||||
/* ---------- Hero ---------- */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #0e8f8a 0%, #0a6f6b 100%);
|
||||
color: #fff; padding: 64px 0 80px; text-align: center;
|
||||
}
|
||||
.hero h1 { font-size: 34px; font-weight: 900; margin: 0 0 14px; line-height: 1.4; }
|
||||
.hero p { font-size: 17px; opacity: .92; max-width: 620px; margin: 0 auto 30px; }
|
||||
|
||||
/* search box on hero */
|
||||
.search-card {
|
||||
background: var(--surface); color: var(--ink);
|
||||
border-radius: var(--radius); box-shadow: var(--shadow-lg);
|
||||
padding: 18px; max-width: 860px; margin: 0 auto;
|
||||
display: grid; grid-template-columns: 1.4fr 1fr 1fr auto; gap: 12px; align-items: end;
|
||||
}
|
||||
.search-card .field { display: flex; flex-direction: column; gap: 5px; text-align: start; }
|
||||
.search-card label { font-size: 12px; font-weight: 700; color: var(--muted); }
|
||||
|
||||
/* ---------- Forms ---------- */
|
||||
select, input[type="text"], input[type="tel"], input[type="number"], textarea {
|
||||
width: 100%; padding: 10px 12px; border: 1px solid var(--line); border-radius: 10px;
|
||||
font-family: inherit; font-size: 14px; background: #fff; color: var(--ink);
|
||||
}
|
||||
select:focus, input:focus, textarea:focus { outline: none; border-color: var(--primary); }
|
||||
label { font-size: 13px; }
|
||||
|
||||
/* ---------- Sections / grid ---------- */
|
||||
.section { padding: 48px 0; }
|
||||
.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 22px; }
|
||||
.section-head h2 { font-size: 23px; font-weight: 800; margin: 0; }
|
||||
.section-head a { color: var(--primary); font-weight: 700; font-size: 14px; }
|
||||
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
|
||||
/* ---------- Cards ---------- */
|
||||
.card {
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
|
||||
}
|
||||
.card-pad { padding: 18px; }
|
||||
|
||||
.shift-card { display: flex; flex-direction: column; gap: 10px; transition: box-shadow .15s, transform .15s; }
|
||||
.shift-card:hover { box-shadow: var(--shadow-lg); transform: translateY(-2px); }
|
||||
.shift-card .facility { font-weight: 800; font-size: 16px; }
|
||||
.shift-card .row { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13.5px; }
|
||||
.shift-card .pay { font-weight: 800; color: var(--primary-dark); font-size: 15px; }
|
||||
.shift-card .foot { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--line); padding-top: 12px; margin-top: 4px; }
|
||||
|
||||
/* ---------- Badges / chips ---------- */
|
||||
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||
.badge-day { background: #fff4e8; color: #c5670f; }
|
||||
.badge-evening { background: #fdeede; color: #b25c12; }
|
||||
.badge-night { background: #e9ecfb; color: var(--night); }
|
||||
.badge-oncall { background: #f0eaf6; color: #6b3fa0; }
|
||||
.badge-verified { background: var(--primary-soft); color: var(--primary-dark); }
|
||||
.badge-type { background: #eef3f6; color: var(--muted); }
|
||||
.badge-distance { background: #fff1e8; color: #c5670f; }
|
||||
.badge-job { background: #eaf3ff; color: #2563eb; }
|
||||
|
||||
/* ---------- Filters layout ---------- */
|
||||
.layout-2 { display: grid; grid-template-columns: 270px 1fr; gap: 24px; align-items: start; }
|
||||
.filter-card { position: sticky; top: 84px; }
|
||||
.filter-card h3 { font-size: 15px; margin: 0 0 14px; }
|
||||
.filter-group { margin-bottom: 16px; }
|
||||
.filter-group > label { display: block; font-size: 13px; font-weight: 700; margin-bottom: 6px; }
|
||||
|
||||
/* ---------- Calendar ---------- */
|
||||
.cal { width: 100%; border-collapse: separate; border-spacing: 8px; }
|
||||
.cal th { font-size: 13px; color: var(--muted); font-weight: 700; padding-bottom: 6px; }
|
||||
.cal td {
|
||||
vertical-align: top; background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: 12px; padding: 10px; height: 130px; width: 14.28%;
|
||||
}
|
||||
.cal .day-num { font-weight: 800; font-size: 15px; margin-bottom: 8px; }
|
||||
.cal td.today { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-soft); }
|
||||
.cal td.empty { background: #fafcfc; }
|
||||
.cal-chip {
|
||||
display: block; font-size: 12px; padding: 4px 7px; border-radius: 7px;
|
||||
margin-bottom: 5px; font-weight: 700; border: 1px solid transparent;
|
||||
}
|
||||
.cal-chip.day { background: #fff4e8; color: #c5670f; }
|
||||
.cal-chip.evening { background: #fdeede; color: #b25c12; }
|
||||
.cal-chip.night { background: #e9ecfb; color: var(--night); }
|
||||
.cal-chip.oncall { background: #f0eaf6; color: #6b3fa0; }
|
||||
.cal-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
|
||||
|
||||
/* ---------- Misc ---------- */
|
||||
.page-head { background: var(--surface); border-bottom: 1px solid var(--line); padding: 28px 0; }
|
||||
.page-head h1 { margin: 0; font-size: 26px; }
|
||||
.empty-state { text-align: center; padding: 60px 20px; color: var(--muted); }
|
||||
.detail-grid { display: grid; grid-template-columns: 1fr 340px; gap: 24px; align-items: start; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 11px 0; border-bottom: 1px solid var(--line); }
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-row .k { color: var(--muted); }
|
||||
.info-row .v { font-weight: 700; }
|
||||
.stat-pills { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; margin-top: 26px; }
|
||||
.stat-pill { background: rgba(255,255,255,.14); border-radius: 12px; padding: 12px 20px; }
|
||||
.stat-pill .n { font-size: 24px; font-weight: 900; display: block; }
|
||||
.stat-pill .l { font-size: 13px; opacity: .9; }
|
||||
.site-footer { background: var(--surface); border-top: 1px solid var(--line); margin-top: 48px; padding: 28px 0; }
|
||||
.footer-inner { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; }
|
||||
.footer-inner p { margin: 4px 0 0; font-size: 13px; }
|
||||
.alert { padding: 12px 16px; border-radius: 10px; margin-bottom: 16px; font-weight: 600; }
|
||||
.alert-success { background: var(--primary-soft); color: var(--primary-dark); }
|
||||
|
||||
/* recommendation reason chips */
|
||||
.rec-reasons { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; }
|
||||
.rec-reason { font-size: 12px; color: var(--primary-dark); font-weight: 600; }
|
||||
.rec-banner { background: linear-gradient(135deg,#0e8f8a 0%,#0a6f6b 100%); color:#fff;
|
||||
border-radius: var(--radius); padding: 20px 22px; margin-bottom: 18px; display:flex;
|
||||
align-items:center; justify-content:space-between; gap:14px; flex-wrap:wrap; }
|
||||
.rec-banner .btn-outline { border-color:#fff; color:#fff; }
|
||||
.rec-banner .btn-outline:hover { background: rgba(255,255,255,.15); }
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (max-width: 860px) {
|
||||
.grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); }
|
||||
.search-card { grid-template-columns: 1fr 1fr; }
|
||||
.layout-2 { grid-template-columns: 1fr; }
|
||||
.detail-grid { grid-template-columns: 1fr; }
|
||||
.main-nav { display: none; }
|
||||
.cal { border-spacing: 4px; }
|
||||
.cal td { height: auto; min-height: 80px; padding: 6px; }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.grid-3, .grid-4, .search-card { grid-template-columns: 1fr; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||
// for details on configuring this project to bundle and minify static web assets.
|
||||
|
||||
// Write your JavaScript code.
|
||||
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2021 Twitter, Inc.
|
||||
Copyright (c) 2011-2021 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
+1
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user