diff --git a/src/JobsMedical.Web/Migrations/20260604181750_UserProfileMedia.Designer.cs b/src/JobsMedical.Web/Migrations/20260604181750_UserProfileMedia.Designer.cs new file mode 100644 index 0000000..edd8f80 --- /dev/null +++ b/src/JobsMedical.Web/Migrations/20260604181750_UserProfileMedia.Designer.cs @@ -0,0 +1,1248 @@ +// +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("20260604181750_UserProfileMedia")] + partial class UserProfileMedia + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("JobsMedical.Web.Models.AppSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AiApiKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AiAutoApprove") + .HasColumnType("boolean"); + + b.Property("AiEnabled") + .HasColumnType("boolean"); + + b.Property("AiEndpoint") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("AiModel") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("AiSystemPrompt") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("AutoIngestEnabled") + .HasColumnType("boolean"); + + b.Property("AutoPublishMinConfidence") + .HasColumnType("integer"); + + b.Property("BaleBotToken") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("BaleEnabled") + .HasColumnType("boolean"); + + b.Property("BaleUseProxy") + .HasColumnType("boolean"); + + b.Property("DemoMode") + .HasColumnType("boolean"); + + b.Property("DivarCity") + .HasMaxLength(60) + .HasColumnType("character varying(60)"); + + b.Property("DivarEnabled") + .HasColumnType("boolean"); + + b.Property("DivarQueries") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DivarUseProxy") + .HasColumnType("boolean"); + + b.Property("IngestIntervalMinutes") + .HasColumnType("integer"); + + b.Property("IngestProxyEnabled") + .HasColumnType("boolean"); + + b.Property("IngestProxyUrl") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MedjobsEnabled") + .HasColumnType("boolean"); + + b.Property("MedjobsMaxAds") + .HasColumnType("integer"); + + b.Property("MedjobsUseProxy") + .HasColumnType("boolean"); + + b.Property("Mode") + .HasColumnType("integer"); + + b.Property("NeshanMapKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PushEnabled") + .HasColumnType("boolean"); + + b.Property("SmsApiKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SmsEnabled") + .HasColumnType("boolean"); + + b.Property("SmsSender") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("SmsTemplate") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TelegramChannels") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("TelegramEnabled") + .HasColumnType("boolean"); + + b.Property("TelegramUseProxy") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VapidPrivateKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("VapidPublicKey") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("VapidSubject") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("WebNotificationsEnabled") + .HasColumnType("boolean"); + + b.Property("WebsiteUrls") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("WebsitesEnabled") + .HasColumnType("boolean"); + + b.Property("WebsitesUseProxy") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("AppSettings"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DoctorId") + .HasColumnType("integer"); + + b.Property("Message") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ShiftId") + .HasColumnType("integer"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Province") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.ToTable("Cities"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.District", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bio") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("LicenseNo") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("Specialty") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("BaleId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DistrictId") + .HasColumnType("integer"); + + b.Property("IsDemo") + .HasColumnType("boolean"); + + b.Property("IsVerified") + .HasColumnType("boolean"); + + b.Property("Lat") + .HasColumnType("double precision"); + + b.Property("Lng") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerUserId") + .HasColumnType("integer"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Verification") + .HasColumnType("integer"); + + b.Property("VerificationNote") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VerificationRequestedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("DistrictId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Facilities"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.FacilityDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FacilityId") + .HasColumnType("integer"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FacilityId"); + + b.ToTable("FacilityDocuments"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.InterestEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("JobOpeningId") + .HasColumnType("integer"); + + b.Property("ShiftId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("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.JobAlert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DistrictId") + .HasColumnType("integer"); + + b.Property("EmploymentType") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Label") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("MinPay") + .HasColumnType("bigint"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("Scope") + .HasColumnType("integer"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CityId"); + + b.HasIndex("IsActive"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("JobAlerts"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.JobOpening", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EmploymentType") + .HasColumnType("integer"); + + b.Property("FacilityId") + .HasColumnType("integer"); + + b.Property("GenderRequirement") + .HasColumnType("integer"); + + b.Property("Requirements") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("SalaryMax") + .HasColumnType("bigint"); + + b.Property("SalaryMin") + .HasColumnType("bigint"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("SourceUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("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.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Body") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Url") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "IsRead", "CreatedAt"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.RawListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Confidence") + .HasColumnType("integer"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FetchedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LinkedShiftId") + .HasColumnType("integer"); + + b.Property("ParsedJson") + .HasColumnType("text"); + + b.Property("RawText") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceChannel") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("ValidationNotes") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.HasKey("Id"); + + b.HasIndex("ContentHash"); + + b.HasIndex("LinkedShiftId"); + + b.HasIndex("Status"); + + b.ToTable("RawListings"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.Report", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ReporterUserId") + .HasColumnType("integer"); + + b.Property("ReporterVisitorId") + .HasMaxLength(36) + .HasColumnType("character varying(36)"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TargetId") + .HasColumnType("integer"); + + b.Property("TargetLabel") + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("TargetType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.ToTable("Reports"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .HasMaxLength(1500) + .HasColumnType("character varying(1500)"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("FacilityId") + .HasColumnType("integer"); + + b.Property("GenderRequirement") + .HasColumnType("integer"); + + b.Property("PayAmount") + .HasColumnType("bigint"); + + b.Property("PayType") + .HasColumnType("integer"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("SharePercent") + .HasColumnType("integer"); + + b.Property("ShiftType") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("SourceUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("SpecialtyRequired") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasColumnType("bytea"); + + b.Property("AvatarContentType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("BanReason") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FullName") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("IsBanned") + .HasColumnType("boolean"); + + b.Property("IsPhoneVerified") + .HasColumnType("boolean"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Resume") + .HasColumnType("bytea"); + + b.Property("ResumeContentType") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ResumeFileName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Phone") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.UserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CityId") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("MinPay") + .HasColumnType("bigint"); + + b.Property("PreferredShiftType") + .HasColumnType("integer"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("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("Id") + .HasMaxLength(36) + .HasColumnType("character varying(36)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Visitors"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Auth") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Endpoint") + .IsRequired() + .HasMaxLength(600) + .HasColumnType("character varying(600)"); + + b.Property("P256dh") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("VisitorId") + .HasMaxLength(36) + .HasColumnType("character varying(36)"); + + b.HasKey("Id"); + + b.HasIndex("Endpoint") + .IsUnique(); + + b.ToTable("WebPushSubscriptions"); + }); + + 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.FacilityDocument", b => + { + b.HasOne("JobsMedical.Web.Models.Facility", "Facility") + .WithMany("Documents") + .HasForeignKey("FacilityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Facility"); + }); + + modelBuilder.Entity("JobsMedical.Web.Models.InterestEvent", b => + { + b.HasOne("JobsMedical.Web.Models.JobOpening", "JobOpening") + .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.JobAlert", b => + { + b.HasOne("JobsMedical.Web.Models.City", "City") + .WithMany() + .HasForeignKey("CityId"); + + b.HasOne("JobsMedical.Web.Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("JobsMedical.Web.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("City"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + 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.Notification", b => + { + b.HasOne("JobsMedical.Web.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + 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("Documents"); + + 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 + } + } +} diff --git a/src/JobsMedical.Web/Migrations/20260604181750_UserProfileMedia.cs b/src/JobsMedical.Web/Migrations/20260604181750_UserProfileMedia.cs new file mode 100644 index 0000000..7d4ae9d --- /dev/null +++ b/src/JobsMedical.Web/Migrations/20260604181750_UserProfileMedia.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JobsMedical.Web.Migrations +{ + /// + public partial class UserProfileMedia : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Avatar", + table: "Users", + type: "bytea", + nullable: true); + + migrationBuilder.AddColumn( + name: "AvatarContentType", + table: "Users", + type: "character varying(100)", + maxLength: 100, + nullable: true); + + migrationBuilder.AddColumn( + name: "Resume", + table: "Users", + type: "bytea", + nullable: true); + + migrationBuilder.AddColumn( + name: "ResumeContentType", + table: "Users", + type: "character varying(120)", + maxLength: 120, + nullable: true); + + migrationBuilder.AddColumn( + name: "ResumeFileName", + table: "Users", + type: "character varying(200)", + maxLength: 200, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Avatar", + table: "Users"); + + migrationBuilder.DropColumn( + name: "AvatarContentType", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Resume", + table: "Users"); + + migrationBuilder.DropColumn( + name: "ResumeContentType", + table: "Users"); + + migrationBuilder.DropColumn( + name: "ResumeFileName", + table: "Users"); + } + } +} diff --git a/src/JobsMedical.Web/Migrations/AppDbContextModelSnapshot.cs b/src/JobsMedical.Web/Migrations/AppDbContextModelSnapshot.cs index 06d828d..f6e16b5 100644 --- a/src/JobsMedical.Web/Migrations/AppDbContextModelSnapshot.cs +++ b/src/JobsMedical.Web/Migrations/AppDbContextModelSnapshot.cs @@ -817,6 +817,13 @@ namespace JobsMedical.Web.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Avatar") + .HasColumnType("bytea"); + + b.Property("AvatarContentType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("BanReason") .HasMaxLength(300) .HasColumnType("character varying(300)"); @@ -839,6 +846,17 @@ namespace JobsMedical.Web.Migrations .HasMaxLength(20) .HasColumnType("character varying(20)"); + b.Property("Resume") + .HasColumnType("bytea"); + + b.Property("ResumeContentType") + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("ResumeFileName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + b.Property("Role") .HasColumnType("integer"); diff --git a/src/JobsMedical.Web/Models/User.cs b/src/JobsMedical.Web/Models/User.cs index 8329762..ce61d99 100644 --- a/src/JobsMedical.Web/Models/User.cs +++ b/src/JobsMedical.Web/Models/User.cs @@ -26,6 +26,13 @@ public class User public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + // --- Profile media (editable at /Me/Profile; stored in-DB so it survives deploys) --- + public byte[]? Avatar { get; set; } + [MaxLength(100)] public string? AvatarContentType { get; set; } + public byte[]? Resume { get; set; } + [MaxLength(200)] public string? ResumeFileName { get; set; } + [MaxLength(120)] public string? ResumeContentType { get; set; } + // Navigation public DoctorProfile? DoctorProfile { get; set; } public ICollection Applications { get; set; } = new List(); diff --git a/src/JobsMedical.Web/Pages/Me/Profile.cshtml b/src/JobsMedical.Web/Pages/Me/Profile.cshtml new file mode 100644 index 0000000..2637d65 --- /dev/null +++ b/src/JobsMedical.Web/Pages/Me/Profile.cshtml @@ -0,0 +1,98 @@ +@page +@model JobsMedical.Web.Pages.Me.ProfileModel +@{ + ViewData["Title"] = "پروفایل من"; +} + + + + پروفایل من + ← پنل کارجو + + + + + @if (Model.Msg is not null) { @Model.Msg } + + + + + @if (Model.HasAvatar) + { + + } + else + { + @((Model.FullName ?? Model.Phone).Trim().Substring(0,1)) + } + + + تصویر پروفایل + + JPG/PNG/WebP، حداکثر ۲ مگابایت. + @if (Model.HasAvatar) + { + حذف تصویر + } + + + + + نام و نام خانوادگی + + + + شماره موبایل + + + + + نقش + + انتخاب نشده + @foreach (var r in Model.Roles) { @r.Name } + + + + شهر + + انتخاب نشده + @foreach (var c in Model.Cities) { @c.Name } + + + + + + تخصص / سمت + + + + سابقه (سال) + + + + + شماره نظام پزشکی/پرستاری (اختیاری) + + + + درباره من + @Model.Bio + + + + رزومه (رزومه شغلی) + @if (Model.ResumeName is not null) + { + + 📎 @Model.ResumeName + حذف + + } + + PDF یا تصویر، حداکثر ۵ مگابایت. مراکز درمانی هنگام بررسی درخواست شما میتوانند آن را ببینند. + + + ذخیره پروفایل + + diff --git a/src/JobsMedical.Web/Pages/Me/Profile.cshtml.cs b/src/JobsMedical.Web/Pages/Me/Profile.cshtml.cs new file mode 100644 index 0000000..6c189d0 --- /dev/null +++ b/src/JobsMedical.Web/Pages/Me/Profile.cshtml.cs @@ -0,0 +1,123 @@ +using System.Security.Claims; +using JobsMedical.Web.Data; +using JobsMedical.Web.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace JobsMedical.Web.Pages.Me; + +[Authorize] +public class ProfileModel : PageModel +{ + private readonly AppDbContext _db; + public ProfileModel(AppDbContext db) => _db = db; + + public List Roles { get; private set; } = new(); + public List Cities { get; private set; } = new(); + public bool HasAvatar { get; private set; } + public string? ResumeName { get; private set; } + public string Phone { get; private set; } = ""; + [TempData] public string? Msg { get; set; } + + [BindProperty] public string? FullName { get; set; } + [BindProperty] public int? RoleId { get; set; } + [BindProperty] public int? CityId { get; set; } + [BindProperty] public string? Specialty { get; set; } + [BindProperty] public string? LicenseNo { get; set; } + [BindProperty] public int YearsExperience { get; set; } + [BindProperty] public string? Bio { get; set; } + + private int Uid => int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + private static readonly string[] ImgTypes = { "image/jpeg", "image/png", "image/webp" }; + private static readonly string[] DocTypes = { "image/jpeg", "image/png", "image/webp", "application/pdf" }; + private const long MaxImg = 2 * 1024 * 1024; // 2 MB + private const long MaxDoc = 5 * 1024 * 1024; // 5 MB + + public async Task OnGetAsync() + { + await LoadListsAsync(); + var u = await _db.Users.Include(x => x.DoctorProfile).FirstAsync(x => x.Id == Uid); + Phone = u.Phone; + FullName = u.FullName; + HasAvatar = u.Avatar != null; + ResumeName = u.ResumeFileName; + var p = u.DoctorProfile; + RoleId = p?.RoleId; + CityId = p?.CityId; + Specialty = p?.Specialty; + LicenseNo = p?.LicenseNo; + YearsExperience = p?.YearsExperience ?? 0; + Bio = p?.Bio; + } + + public async Task OnPostAsync(IFormFile? avatar, IFormFile? resume) + { + var u = await _db.Users.Include(x => x.DoctorProfile).FirstAsync(x => x.Id == Uid); + + u.FullName = string.IsNullOrWhiteSpace(FullName) ? null : FullName.Trim(); + var p = u.DoctorProfile ??= new DoctorProfile { UserId = Uid }; + p.RoleId = RoleId; + p.CityId = CityId; + p.Specialty = string.IsNullOrWhiteSpace(Specialty) ? "پزشک عمومی" : Specialty.Trim(); + p.LicenseNo = LicenseNo?.Trim(); + p.YearsExperience = Math.Clamp(YearsExperience, 0, 70); + p.Bio = Bio?.Trim(); + + string? warn = null; + if (avatar is { Length: > 0 }) + { + if (avatar.Length > MaxImg || !ImgTypes.Contains((avatar.ContentType ?? "").ToLowerInvariant())) + warn = "تصویر باید JPG/PNG/WebP و کمتر از ۲ مگابایت باشد."; + else + { + using var ms = new MemoryStream(); + await avatar.CopyToAsync(ms); + u.Avatar = ms.ToArray(); + u.AvatarContentType = avatar.ContentType!.ToLowerInvariant(); + } + } + if (resume is { Length: > 0 }) + { + if (resume.Length > MaxDoc || !DocTypes.Contains((resume.ContentType ?? "").ToLowerInvariant())) + warn = (warn is null ? "" : warn + " ") + "رزومه باید PDF یا تصویر و کمتر از ۵ مگابایت باشد."; + else + { + using var ms = new MemoryStream(); + await resume.CopyToAsync(ms); + u.Resume = ms.ToArray(); + u.ResumeContentType = resume.ContentType!.ToLowerInvariant(); + u.ResumeFileName = Path.GetFileName(resume.FileName); + } + } + + await _db.SaveChangesAsync(); + Msg = warn ?? "پروفایل ذخیره شد."; + return RedirectToPage(); + } + + public async Task OnPostDeleteResumeAsync() + { + var u = await _db.Users.FirstAsync(x => x.Id == Uid); + u.Resume = null; u.ResumeFileName = null; u.ResumeContentType = null; + await _db.SaveChangesAsync(); + Msg = "رزومه حذف شد."; + return RedirectToPage(); + } + + public async Task OnPostDeleteAvatarAsync() + { + var u = await _db.Users.FirstAsync(x => x.Id == Uid); + u.Avatar = null; u.AvatarContentType = null; + await _db.SaveChangesAsync(); + Msg = "تصویر حذف شد."; + return RedirectToPage(); + } + + 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(); + } +} diff --git a/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml b/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml index 7d437a9..d05c7cd 100644 --- a/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml +++ b/src/JobsMedical.Web/Pages/Shared/_Layout.cshtml @@ -1,12 +1,22 @@ @using System.Security.Claims +@using Microsoft.EntityFrameworkCore @inject JobsMedical.Web.Services.NotificationService Notifications +@inject JobsMedical.Web.Data.AppDbContext Db @{ var title = ViewData["Title"] as string; int unreadCount = 0; - if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var _uid)) + int meId = 0; + string? meName = null; + bool meHasAvatar = false; + if (User.Identity?.IsAuthenticated == true && int.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out meId)) { - unreadCount = await Notifications.UnreadCountAsync(_uid); + unreadCount = await Notifications.UnreadCountAsync(meId); + var info = await Db.Users.Where(u => u.Id == meId) + .Select(u => new { u.FullName, u.Phone, HasAvatar = u.Avatar != null }).FirstOrDefaultAsync(); + meName = string.IsNullOrWhiteSpace(info?.FullName) ? info?.Phone : info!.FullName; + meHasAvatar = info?.HasAvatar ?? false; } + var meInitial = string.IsNullOrWhiteSpace(meName) ? "؟" : meName!.Trim().Substring(0, 1); } @@ -62,20 +72,44 @@ @if (User.Identity?.IsAuthenticated == true) { - @if (User.IsInRole("Admin")) - { - پنل مدیریت - تنظیمات - } - @if (User.IsInRole("FacilityAdmin")) - { - پنل کارفرما - } 🔔اعلانها@if (unreadCount > 0) {@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())} - پنل کارجو - - خروج - + + + + + @if (meHasAvatar) + { + + } + else + { + @meInitial + } + ▾ + + + @meName + 👤 ویرایش پروفایل + 🗂️ پنل کارجو + 🔎 هشدارهای شغلی + ⭐ علاقهمندیها + 🔔 اعلانها@if (unreadCount > 0) {@JalaliDate.ToPersianDigits(unreadCount > 99 ? "99+" : unreadCount.ToString())} + @if (User.IsInRole("FacilityAdmin")) + { + 🏥 پنل کارفرما + } + @if (User.IsInRole("Admin")) + { + + 🛠️ پنل مدیریت + ⚙️ تنظیمات + } + + + 🚪 خروج + + + } else { @@ -122,6 +156,14 @@ @* Self-hosted guided app tour (no CDN). Auto-runs once for new visitors; re-runnable from /Help. *@ + @* Close the profile dropdown when clicking outside it. *@ + + @* Live in-app notifications over SSE (our own origin — works in Iran, no Google push). Updates the bell badge, shows a toast, and fires a local OS notification when allowed. *@ @if (User.Identity?.IsAuthenticated == true) diff --git a/src/JobsMedical.Web/Program.cs b/src/JobsMedical.Web/Program.cs index d7799f0..5a1a536 100644 --- a/src/JobsMedical.Web/Program.cs +++ b/src/JobsMedical.Web/Program.cs @@ -221,6 +221,36 @@ app.MapGet("/facility-doc/{id:int}", async (int id, HttpContext ctx, AppDbContex return Results.File(doc.Data, doc.ContentType, doc.FileName); }).RequireAuthorization(); +// Profile avatar — public (low-sensitivity), cached. 404 when the user has none. +app.MapGet("/avatar/{id:int}", async (int id, AppDbContext db) => +{ + var u = await db.Users.Where(x => x.Id == id) + .Select(x => new { x.Avatar, x.AvatarContentType }).FirstOrDefaultAsync(); + if (u?.Avatar is null) return Results.NotFound(); + return Results.File(u.Avatar, u.AvatarContentType ?? "image/jpeg"); +}); + +// Résumé — readable by the owner, an admin, or an employer who received this user's application. +app.MapGet("/resume/{id:int}", async (int id, HttpContext ctx, AppDbContext db) => +{ + var u = await db.Users.Where(x => x.Id == id) + .Select(x => new { x.Resume, x.ResumeContentType, x.ResumeFileName }).FirstOrDefaultAsync(); + if (u?.Resume is null) return Results.NotFound(); + + var meId = int.TryParse(ctx.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value, out var n) ? n : (int?)null; + var allowed = ctx.User.IsInRole("Admin") || meId == id; + if (!allowed && meId is int viewer) + { + var vIds = await db.Visitors.Where(v => v.UserId == id).Select(v => v.Id).ToListAsync(); + allowed = await db.InterestEvents.AnyAsync(e => e.EventType == InterestEventType.Apply + && vIds.Contains(e.VisitorId) + && ((e.Shift != null && e.Shift.Facility.OwnerUserId == viewer) + || (e.JobOpening != null && e.JobOpening.Facility.OwnerUserId == viewer))); + } + if (!allowed) return Results.Forbid(); + return Results.File(u.Resume, u.ResumeContentType ?? "application/octet-stream", u.ResumeFileName ?? "resume"); +}).RequireAuthorization(); + // User-submitted report against a listing (abuse/fake/wrong info). app.MapPost("/report", async (HttpContext ctx, AppDbContext db, VisitorContext vc, [FromForm] string targetType, [FromForm] int targetId, [FromForm] string reason, diff --git a/src/JobsMedical.Web/wwwroot/css/site.css b/src/JobsMedical.Web/wwwroot/css/site.css index bbcae90..5002a2a 100644 --- a/src/JobsMedical.Web/wwwroot/css/site.css +++ b/src/JobsMedical.Web/wwwroot/css/site.css @@ -88,6 +88,35 @@ a { color: inherit; text-decoration: none; } .nav-toggle:checked ~ .nav-burger span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } .bell-mobile { position: relative; font-size: 20px; margin-inline-start: auto; line-height: 1; } +/* ---------- Profile avatar + dropdown ---------- */ +.profile-menu { position: relative; } +.avatar-btn { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; } +.avatar-img, .avatar-fallback { width: 34px; height: 34px; border-radius: 50%; object-fit: cover; display: block; } +.avatar-fallback { background: var(--primary); color: #fff; display: grid; place-items: center; font-weight: 800; } +.avatar-caret { color: var(--muted); font-size: 11px; } +.profile-dropdown { + position: absolute; top: calc(100% + 8px); inset-inline-end: 0; min-width: 230px; z-index: 60; + background: var(--surface); border: 1px solid var(--line); border-radius: 14px; + box-shadow: 0 16px 38px rgba(0,0,0,.16); padding: 6px; display: none; +} +.profile-toggle:checked ~ .profile-dropdown { display: block; animation: fadeIn .12s ease; } +.profile-dropdown a, .pd-logout { + display: flex; align-items: center; gap: 8px; width: 100%; text-align: start; + padding: 9px 12px; border-radius: 9px; color: var(--ink); font-weight: 600; font-size: 14px; + background: none; border: none; cursor: pointer; font-family: inherit; +} +.profile-dropdown a:hover, .pd-logout:hover { background: var(--primary-soft); color: var(--primary-dark); } +.profile-dropdown form { margin: 0; } +.pd-head { padding: 8px 12px; font-weight: 800; color: var(--muted); font-size: 13px; } +.pd-sep { height: 1px; background: var(--line); margin: 4px 0; } +.pd-logout { color: var(--danger); } + +/* Large avatar on the profile editor page */ +.avatar-lg { width: 84px; height: 84px; border-radius: 50%; overflow: hidden; flex: 0 0 auto; + background: var(--primary-soft); display: grid; place-items: center; } +.avatar-lg img { width: 100%; height: 100%; object-fit: cover; } +.avatar-lg span { font-size: 34px; font-weight: 800; color: var(--primary-dark); } + /* ---------- Live notification toasts (SSE) ---------- */ .toast-host { position: fixed; inset-block-end: 16px; inset-inline-start: 16px; z-index: 200; @@ -400,6 +429,14 @@ label { font-size: 13px; } .bell-inline .bell-label { display: inline; } .header-actions .btn { width: 100%; justify-content: center; padding: 12px; font-size: 15px; margin-top: 6px; } + /* On mobile the avatar button is hidden and the menu items show stacked in the burger panel. */ + .avatar-btn { display: none; } + .profile-menu { width: 100%; } + .profile-dropdown { position: static; display: block; box-shadow: none; border: none; padding: 0; min-width: 0; } + .profile-dropdown a, .pd-logout { padding: 12px 6px; font-size: 15px; } + .pd-head { display: none; } + .pd-sep { display: none; } + .cal { border-spacing: 4px; } .cal td { height: auto; min-height: 80px; padding: 6px; } }
← پنل کارجو
JPG/PNG/WebP، حداکثر ۲ مگابایت.
+ 📎 @Model.ResumeName + حذف +
PDF یا تصویر، حداکثر ۵ مگابایت. مراکز درمانی هنگام بررسی درخواست شما میتوانند آن را ببینند.