diff --git a/API/FamilyTreeAPI/Controllers/FileUploadController.cs b/API/FamilyTreeAPI/Controllers/FileUploadController.cs index e52182d..61d000b 100644 --- a/API/FamilyTreeAPI/Controllers/FileUploadController.cs +++ b/API/FamilyTreeAPI/Controllers/FileUploadController.cs @@ -3,8 +3,9 @@ using FamilyTreeAPI.Repository; namespace FamilyTreeAPI.Controllers; - +using FamilyTreeAPI.Interface; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using System.IO; using System.Threading.Tasks; @@ -15,11 +16,15 @@ public class FileUploadController : ControllerBase private readonly IWebHostEnvironment _hostingEnvironment; private readonly ImportPersonRepository _importPersonRepository; private readonly IConfiguration _config; - public FileUploadController(IWebHostEnvironment hostingEnvironment, ImportPersonRepository importPersonRepository, IConfiguration config) + private readonly IPersonPhoto _personPhoto; + public FileUploadController(IWebHostEnvironment hostingEnvironment, + IPersonPhoto personPhoto, + ImportPersonRepository importPersonRepository, IConfiguration config) { _hostingEnvironment = hostingEnvironment; _importPersonRepository = importPersonRepository; _config = config; + _personPhoto = personPhoto; } [HttpPost("[action]")] @@ -79,4 +84,51 @@ public class FileUploadController : ControllerBase var rev = await _importPersonRepository.DownloadFile(criteria); return Ok(rev); } + + [HttpPost("[action]")] + public async Task SavePersonPhoto(List files) + { + var keys = Request.Form; + var ffiles = Request.Form.Files; + ResultModel ret = new(); + if (files.Count > 0) + { + StringValues sdocCId = ""; + keys.TryGetValue("personId", out sdocCId); + for (int i = 0; i < files.Count; i++) + { + var file = files[i]; + UploadCriteria criteria = new(); + criteria.File = file; + criteria.PersonId = int.Parse(sdocCId); + criteria.FileName = file.FileName; + + ret = await _personPhoto.SaveAsync(criteria); + } + + } + return Ok(ret); + } + + + [HttpPost("[action]")] + public async Task DownloadPersonPhoto(DownloadFileCriteria criteria) + { + + var result = await _personPhoto.DownloadPersonPhoto(criteria.Id, criteria.FileName); + + return File(result.Data.Content, result.Data.ContentType, result.Data.FileName); + } + [HttpPost("[action]")] + public async Task DeletePersonPhoto(DeleteFileCriteria criteria) + { + + ResultModel ret = new(); + if ( criteria.Id > 0) + { + + ret = await _personPhoto.DeletePersonPhoto(criteria.Id); + } + return Ok(ret); + } } diff --git a/API/FamilyTreeAPI/Controllers/PersonController.cs b/API/FamilyTreeAPI/Controllers/PersonController.cs index 809d8b4..83d5343 100644 --- a/API/FamilyTreeAPI/Controllers/PersonController.cs +++ b/API/FamilyTreeAPI/Controllers/PersonController.cs @@ -24,11 +24,11 @@ namespace FamilyTreeAPI.Controllers { StringValues familyId = ""; - keys.TryGetValue("familyId", out familyId); + keys.TryGetValue("personId", out familyId); UploadCriteria criteria = new(); criteria.File = file; - criteria.FamilyId = familyId; + criteria.PersonId = int.Parse(familyId); criteria.FileName = file.FileName; diff --git a/API/FamilyTreeAPI/Entities/FileContent.cs b/API/FamilyTreeAPI/Entities/FileContent.cs index 29e959a..faeeda9 100644 --- a/API/FamilyTreeAPI/Entities/FileContent.cs +++ b/API/FamilyTreeAPI/Entities/FileContent.cs @@ -9,20 +9,22 @@ public class FileContent { public byte[] Content { get; set; } public string FileName { get; set; } + public string ContentType { get; set; } } public class DownloadFileCriteria { public string FileName { get; set; } + public int Id { get; set; } } public class UploadCriteria { public string FileName { get; set; } - public string FamilyId { get; set; } + public int PersonId { get; set; } public IFormFile File { get; set; } } public class DeleteFileCriteria { - public int FamilyId { get; set; } + public int Id { get; set; } public string Filename { get; set; } } \ No newline at end of file diff --git a/API/FamilyTreeAPI/Entities/PersonDto.cs b/API/FamilyTreeAPI/Entities/PersonDto.cs index 105b9bf..f543ac8 100644 --- a/API/FamilyTreeAPI/Entities/PersonDto.cs +++ b/API/FamilyTreeAPI/Entities/PersonDto.cs @@ -49,5 +49,6 @@ public partial class PersonDto public int? MotherId { get; set; } public List? RelationShips { get; set; } + public List? PersonPhotos { get; set; } } diff --git a/API/FamilyTreeAPI/Entities/PersonPhotoDto.cs b/API/FamilyTreeAPI/Entities/PersonPhotoDto.cs new file mode 100644 index 0000000..1e529f4 --- /dev/null +++ b/API/FamilyTreeAPI/Entities/PersonPhotoDto.cs @@ -0,0 +1,10 @@ +namespace FamilyTreeAPI.Entities; + +public class PersonPhotoDto +{ + public int Id { get; set; } + + public int PersonId { get; set; } + public string Photo { get; set; } + public string? PhotoType { get; set; } +} \ No newline at end of file diff --git a/API/FamilyTreeAPI/Helper/Helpers.cs b/API/FamilyTreeAPI/Helper/Helpers.cs index 2ede587..32d826b 100644 --- a/API/FamilyTreeAPI/Helper/Helpers.cs +++ b/API/FamilyTreeAPI/Helper/Helpers.cs @@ -2,6 +2,80 @@ { public class Helpers { + public static string GetFileContent(string extension) + { + string contentType = ""; + /* + https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types + */ + switch (extension) + { + case ".pdf": + contentType = "application/pdf"; + break; + case ".txt": + contentType = "text/plain"; + break; + case ".xlsx": + contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + break; + case ".xls": + contentType = "application/vnd.ms-excel"; + break; + case ".doc": + contentType = "application/msword"; + break; + case ".odt": + contentType = "application/vnd.oasis.opendocument.text"; + break; + case ".docx": + contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + break; + case ".gif": + contentType = "image/gif"; + break; + case ".ppt": + contentType = "application/vnd.ms-powerpoint"; + break; + case ".ico": + contentType = "image/vnd.microsoft.icon"; + break; + case ".png": + contentType = "image/png"; + break; + case ".rar": + contentType = "application/vnd.rar"; + break; + case ".zip": + contentType = "application/zip"; + break; + case ".tar": + contentType = "application/x-tar"; + break; + case ".mp4": + contentType = "video/mp4"; + break; + case ".jpeg": + case ".jpg": + contentType = "image/jpeg"; + break; + case ".3gp": + contentType = "video/3gpp"; + break; + case ".7z": + contentType = "application/x-7z-compressed"; + break; + case ".tif": + case ".tiff": + contentType = "image/tiff"; + break; + // and so on + // default: + // throw new ArgumentOutOfRangeException($"Unable to find Content Type for file name {file.NameWithExtension}."); + } + + return contentType; + } public static string? DateToStr(DateTime? date) { string? result = null; diff --git a/API/FamilyTreeAPI/Interface/IPersonPhoto.cs b/API/FamilyTreeAPI/Interface/IPersonPhoto.cs new file mode 100644 index 0000000..8c6f038 --- /dev/null +++ b/API/FamilyTreeAPI/Interface/IPersonPhoto.cs @@ -0,0 +1,12 @@ +using FamilyTreeAPI.Entities; + +namespace FamilyTreeAPI.Interface; + +public interface IPersonPhoto +{ + Task> SaveAsync(UploadCriteria criteria); + Task> DownloadPersonPhoto(int id, string? filename); + + Task> DeletePersonPhoto(int id); + Task>> LoadPersonPhoto(int personId); +} diff --git a/API/FamilyTreeAPI/Models/FamilyTreeDBContext.cs b/API/FamilyTreeAPI/Models/FamilyTreeDBContext.cs index 594d434..deda6df 100644 --- a/API/FamilyTreeAPI/Models/FamilyTreeDBContext.cs +++ b/API/FamilyTreeAPI/Models/FamilyTreeDBContext.cs @@ -80,7 +80,11 @@ namespace FamilyTreeAPI.Models entity.Property(e => e.Id).HasColumnName("id"); entity.Property(e => e.PersonId).HasColumnName("personid"); - entity.Property(e => e.Photo).HasColumnName("photo"); + entity.Property(e => e.Photo) + .HasColumnName("photo"); + entity.Property(e => e.PhotoType) + .HasMaxLength(20) + .HasColumnName("phototype"); entity.Property(e => e.ImagePhoto).HasColumnName("imagephot"); //try to store binary in table. "bytea" }); diff --git a/API/FamilyTreeAPI/Models/PersonPhoto.cs b/API/FamilyTreeAPI/Models/PersonPhoto.cs index 252d385..7ed66fa 100644 --- a/API/FamilyTreeAPI/Models/PersonPhoto.cs +++ b/API/FamilyTreeAPI/Models/PersonPhoto.cs @@ -6,6 +6,7 @@ public int PersonId { get; set; } public string Photo { get; set; } + public string? PhotoType { get; set; } public byte[] ImagePhoto { get; set; } diff --git a/API/FamilyTreeAPI/Program.cs b/API/FamilyTreeAPI/Program.cs index 0c93d29..117d54d 100644 --- a/API/FamilyTreeAPI/Program.cs +++ b/API/FamilyTreeAPI/Program.cs @@ -71,7 +71,7 @@ services.AddGraphQLServer() services.AddScoped(); services.AddScoped(); - +services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/FamilyTreeAPI/Repository/PersonPhotoRepository.cs b/API/FamilyTreeAPI/Repository/PersonPhotoRepository.cs new file mode 100644 index 0000000..5945de0 --- /dev/null +++ b/API/FamilyTreeAPI/Repository/PersonPhotoRepository.cs @@ -0,0 +1,197 @@ +using DocumentFormat.OpenXml.Office2010.Excel; +using FamilyTreeAPI.Entities; +using FamilyTreeAPI.Helper; +using FamilyTreeAPI.Interface; +using FamilyTreeAPI.Models; +using Microsoft.EntityFrameworkCore; + +namespace FamilyTreeAPI.Repository +{ + public class PersonPhotoRepository: IPersonPhoto + { + private readonly FamilyTreeDBContext _context; + private readonly IHttpContextAccessor _httpContext; + private readonly IConfiguration _config; + public PersonPhotoRepository(IConfiguration config, + FamilyTreeDBContext context, + IRelationShipd relationship, + IHttpContextAccessor httpContext) + { + _context = context; + _config = config; + _httpContext = httpContext; + } + + private PersonPhotoDto FillDto(PersonPhoto model) + { + PersonPhotoDto dto = new(); + dto.Id = model.Id; + dto.PersonId = model.PersonId; + dto.Photo = model.Photo; + dto.PhotoType = model.PhotoType; + return dto; + } + private PersonPhoto FillModel(PersonPhoto model, int personId, string filename) + { + + model.PersonId = personId; + model.Photo = filename; + model.PhotoType = System.IO.Path.GetExtension(filename); + + return model; + } + public async Task> SaveAsync(UploadCriteria criteria) + { + int result = default(int); + int statuscode = 0; + string error = ""; + + HttpContext? httpContext = _httpContext.HttpContext; + string loginName = ""; + if (httpContext != null) + { + UserDto? user = (UserDto?)httpContext.Items["User"]; + if (user != null) + loginName = user.FirstName + " " + user.LastName; + } + + try + { + IFormFile formfile = criteria.File; + + PersonPhoto model; + + model = new(); + + model = FillModel(model,criteria.PersonId, formfile.FileName); + + using (var stream = new MemoryStream()) + { + await formfile.CopyToAsync(stream); + model.ImagePhoto = stream.ToArray(); + } + _context.PersonPhotos.Add(model); + await _context.SaveChangesAsync(); + + statuscode = 1; + + } + catch (Exception ex) + { + error = ex.ToString(); + statuscode = -1; + } + //var dto = await Task.Run(() => result); + return new ResultModel() + { + Data = result, + StatusCode = statuscode, + Message = error + }; + } + + public async Task> DownloadPersonPhoto(int id, string? filename) + { + + int statusCode = -1; + string error = ""; + PersonPhoto? model = null; + FileContent fileContent = new(); + if (id < 1) + { + var rlist = await _context.PersonPhotos.Where(x => x.Photo == filename).ToListAsync(); + if (rlist != null) + { + if (rlist.Count > 0) + { + model = rlist[0]; + } + } + } + else + { + model = await _context.PersonPhotos.FindAsync(id); + } + + if (model != null) + { + fileContent.ContentType = Helpers.GetFileContent(model.PhotoType); + fileContent.Content = (byte[])model.ImagePhoto; + fileContent.FileName = model.Photo; + statusCode = 1; + } + + return new ResultModel() + { + Data = fileContent, + StatusCode = statusCode, + Message = error + }; + } + + public async Task> DeletePersonPhoto(int id) + { + int result = default(int); + int statuscode = 0; + string error = ""; + try + { + PersonPhoto? model = await _context.PersonPhotos.FindAsync(id); + if (model != null) + { + _context.PersonPhotos.Remove(model); + await _context.SaveChangesAsync(); + statuscode = 1; + } + } + catch (Exception ex) + { + error = ex.ToString(); + statuscode = -1; + } + //var dto = await Task.Run(() => result); + return new ResultModel() + { + Data = result, + StatusCode = statuscode, + Message = error + }; + } + + + public async Task>> LoadPersonPhoto(int personId) + { + List result = new(); + int statuscode = 0; + string error = ""; + PersonPhotoDto dto; + PersonPhoto? model; + try + { + List mlist = await _context.PersonPhotos.Where( x => x.PersonId == personId ).ToListAsync(); + for (int i = 0; i< mlist.Count; i++) + { + model = mlist[i]; + if (model != null) + { + dto = FillDto(model); + result.Add(dto); + } + statuscode = 1; + } + } + catch (Exception ex) + { + error = ex.ToString(); + statuscode = -1; + } + + return new ResultModel>() + { + Data = result, + StatusCode = statuscode, + Message = error + }; + } + } +} diff --git a/API/FamilyTreeAPI/Repository/PersonRepository.cs b/API/FamilyTreeAPI/Repository/PersonRepository.cs index dcb5d0e..9dcff79 100644 --- a/API/FamilyTreeAPI/Repository/PersonRepository.cs +++ b/API/FamilyTreeAPI/Repository/PersonRepository.cs @@ -20,12 +20,15 @@ public partial class PersonRepository : IPerson private readonly FamilyTreeDBContext _context; private readonly IRelationShipd _relationship; private readonly IHttpContextAccessor _httpContext; + private readonly IPersonPhoto _personPhoto; private readonly IConfiguration _config; const string dateFormat = "yyyy-MM-dd"; public PersonRepository(IConfiguration config, FamilyTreeDBContext context, IRelationShipd relationship, + IPersonPhoto personPhoto, IHttpContextAccessor httpContext) { + _personPhoto = personPhoto; _context = context; _relationship = relationship; _config = config; @@ -410,6 +413,8 @@ public partial class PersonRepository : IPerson statuscode = 1; ResultModel> rlist = await _relationship.GetByPersonIdAsync(dto.Id); dto.RelationShips = rlist.Data; + ResultModel> personPhoto = await _personPhoto.LoadPersonPhoto(id); + dto.PersonPhotos = personPhoto.Data; statuscode = rlist.StatusCode; error = rlist.Message; } @@ -626,7 +631,7 @@ public partial class PersonRepository : IPerson Directory.CreateDirectory(path); } extention = System.IO.Path.GetExtension(criteria.FileName); - filename = criteria.FamilyId + "_" + sdate + extention; + filename = criteria.PersonId + "_" + sdate + extention; using (var fileStream = new FileStream(System.IO.Path.Combine(path, filename), FileMode.Create)) { @@ -670,7 +675,7 @@ public partial class PersonRepository : IPerson result = 1; statusCode = 1; - Person? model = _context.Persons.Find(criteria.FamilyId); + Person? model = _context.Persons.Find(criteria.Id); if (model != null) { model.Image = null; diff --git a/UI/src/app/attachphoto/add.photo.css b/UI/src/app/attachphoto/add.photo.css new file mode 100644 index 0000000..e69de29 diff --git a/UI/src/app/attachphoto/add.photo.html b/UI/src/app/attachphoto/add.photo.html new file mode 100644 index 0000000..8e34464 --- /dev/null +++ b/UI/src/app/attachphoto/add.photo.html @@ -0,0 +1,20 @@ +
+
+ +
+ + +
+
+ @if (files) + { + + } + +
+
+
diff --git a/UI/src/app/attachphoto/add.photo.ts b/UI/src/app/attachphoto/add.photo.ts new file mode 100644 index 0000000..c0ebe09 --- /dev/null +++ b/UI/src/app/attachphoto/add.photo.ts @@ -0,0 +1,171 @@ +import { ChangeDetectorRef, Component, inject, Inject, OnDestroy, OnInit, signal } from '@angular/core'; +import { DynamicDialogModule, DynamicDialogRef ,DynamicDialogConfig} from 'primeng/dynamicdialog'; + +import { LookupEdit } from '../models'; +import { UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { Subject, Subscription } from 'rxjs'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { LookupService } from '../shares'; +import { CommonModule } from '@angular/common'; +import { CheckboxModule } from 'primeng/checkbox'; +import { InputTextModule } from 'primeng/inputtext'; +import { ButtonModule } from 'primeng/button'; +@Component({ + selector: 'add-photo', + imports: [CommonModule, ReactiveFormsModule, + ButtonModule, InputTextModule, + DynamicDialogModule, CheckboxModule], + templateUrl: './add.photo.html', + styleUrls: ['./add.photo.css'], + providers: [ConfirmationService] +}) +export class AddPhoto implements OnInit, OnDestroy { + isChange = true; // disable use false//true for not disable. make sure it true is disable button. + selType = ""; + _error = ""; + downloadFile = signal(false); + files: File[] = []; + fileName = ""; + disabled: boolean = true; + subChanged$ = new Subject(); + private formBuilder = inject(UntypedFormBuilder); + private cdr = inject(ChangeDetectorRef); + private subscription: Subscription = new Subscription(); + lookupForm = this.formBuilder.group({ + id: [0], //Validators.required + description: ['', [Validators.required, Validators.maxLength(50)]], + codeId: ['', [Validators.required]], + parentId: [0], + + active: [false], //Validators.required + }); + + constructor( + + private lookupService: LookupService, + private messageService: MessageService, + private confirmationService: ConfirmationService, + public ref: DynamicDialogRef, public config: DynamicDialogConfig) { } + + ngOnInit(): void { + const id = this.config.data.id; + this.selType = this.config.data.type; + const maxcodeId = this.config.data.maxCodeId; + if (id > 0) { + this.lookupService.loadLookupById(id, this.selType).subscribe({ + next: x => { + this.assignValue(x.data); + this.cdr.markForCheck(); + this.subscription.add(this.lookupForm.valueChanges.subscribe(x => this.isChange = false)); + this.subscription.add(this.subChanged$.subscribe(x => this.isChange = x)); + + }, + error: e => { + console.error("error", e); + this.messageService.add({ severity: 'error', summary: 'Error on load cleaner ', detail: e.message }); + } + }); + } + else { + //new cleaner + const ward:LookupEdit = { + id: -1, description: '', codeId: maxcodeId, type: this.selType, active: true, + + }; + this.assignValue(ward); + this.subscription.add(this.lookupForm.valueChanges.subscribe(x => this.isChange = false)); + this.subscription.add(this.subChanged$.subscribe(x => this.isChange = x)); + } + } + + getClassForRequire(prev: string, name: string) { + const notok = !this.lookupForm.controls[name].valid && + this.lookupForm.controls[name].touched; + let str = prev; + if (notok) + str += " ng-invalid ng-dirty"; + return str; + } + get isFieldsChange() { + const chan = this.isChange || !this.lookupForm.valid; // this disable so need true valid = true not + //console.log(this.msg + 'is fields change', chan); + return chan; + } + + assignValue(item: LookupEdit): void { + this.lookupForm.patchValue({ + id: item.id, + codeId: item.codeId, + description: item.description, + active: item.active, + + + }); + // disable use false//true for not disable. + this.subChanged$.next(true); + } + validate(item: LookupEdit): boolean { + let result = true; + if (item.description!.trim() == "") { + this._error = "Description is empty or blank"; + result = false; + } + return result; + + } + deleteFile() : void { + //const filename = this.requestForm.value.attachmentFile; + // const data = { filename }; + this.fileName =""; + //this.requestForm.patchValue({ attachmentFile: "" }); + /* + const delete$ = this.tradePersonService.deleteUploadFile(data); + this.subscription.add(delete$.subscribe( + { + next: x => { + if (x.statusCode == 1) { + { + this.requestForm.patchValue({ attachmentFile: "" }); + } + } + else { + this.messageService.add({ severity: 'error', summary: 'Error delete upload file', detail: 'Fail to delete upload file: ' + x.message }); + } + }, + error: e => { + this.messageService.add({ severity: 'error', summary: 'Error delete upload file', detail: 'Fail to delete upload file: ' + e }); + } + } + )); + */ +} + onFileSelected(event: any) { + + let i =0; + for (i = 0; i < event.target.files.length; i++) + { + const file: File = event.target.files[i]; + this.files.push(file); + if (this.fileName != "") + this.fileName = this.fileName + "," + file.name; + else + this.fileName = file.name; + + this.subChanged$.next(false); + + } + } +upload(e: Event): void { + e.preventDefault(); + this.ref.close(this.files); +} + + cancel(e: Event): void { + e.preventDefault(); + this.ref.close(null); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } +} diff --git a/UI/src/app/attachphoto/index.ts b/UI/src/app/attachphoto/index.ts new file mode 100644 index 0000000..bc58118 --- /dev/null +++ b/UI/src/app/attachphoto/index.ts @@ -0,0 +1,2 @@ +export * from './photolist'; +export * from './add.photo'; \ No newline at end of file diff --git a/UI/src/app/attachphoto/photolist.css b/UI/src/app/attachphoto/photolist.css new file mode 100644 index 0000000..e69de29 diff --git a/UI/src/app/attachphoto/photolist.html b/UI/src/app/attachphoto/photolist.html new file mode 100644 index 0000000..6acbf52 --- /dev/null +++ b/UI/src/app/attachphoto/photolist.html @@ -0,0 +1,46 @@ +
+
+

please click save at the end

+
+ +
+
+ +
+ + + + + Id + Description + + + Action + + + + + + {{item.id}} + {{item.photo}} + + + + + @if (item.id > 0) { + + } + + + + + +
+ +
+
\ No newline at end of file diff --git a/UI/src/app/attachphoto/photolist.ts b/UI/src/app/attachphoto/photolist.ts new file mode 100644 index 0000000..4f6d21e --- /dev/null +++ b/UI/src/app/attachphoto/photolist.ts @@ -0,0 +1,160 @@ +//import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Component, OnInit, OnDestroy, inject, ChangeDetectorRef, signal, output } from '@angular/core'; +import { catchError, EMPTY, finalize, Subscription } from 'rxjs'; +import { CommonUtilities, HttpUtility, LookupService } from '../shares'; +import { DialogService ,DynamicDialogConfig,DynamicDialogModule, DynamicDialogRef} from 'primeng/dynamicdialog'; +import { MessageService } from 'primeng/api'; +import { AddPhoto } from './add.photo'; +import { LookupEdit, PersonPhotoDto } from '../models'; + +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { TableModule } from 'primeng/table'; +import { ButtonModule } from 'primeng/button'; +import { AuthenticationService } from '../user-services'; +import { HttpResponse } from '@angular/common/http'; +import { PersonService } from '../person'; + +@Component({ + selector: 'photo-list', + imports: [CommonModule, FormsModule, DynamicDialogModule, + ButtonModule, TableModule], + templateUrl: './photolist.html', + styleUrls: ['./photolist.css'], + providers: [DialogService] +}) +export class PhotoList implements OnInit, OnDestroy { + FileList: PersonPhotoDto[] =[] + loading = false; + _id = -1; + deletePersonPhoto: number[] = []; + private authenticationService = inject(AuthenticationService); + private cdr = inject(ChangeDetectorRef); + private messageService = inject(MessageService); + private subscription: Subscription = new Subscription(); + downloadFile = signal(false); + constructor( + private http: HttpUtility, + private personService: PersonService, + + public ref: DynamicDialogRef, public config: DynamicDialogConfig, + public dialogService: DialogService + ) { } + + ngOnInit(): void { + const id = this.config.data.id; + const olist = this.config.data.personPhotos; + if (olist && olist.length > 0) + { + this.FileList = olist; + this.assignFileId(olist); + } + } + assignFileId(files: any[]): void { + let i = 0; + let id = -1; + let mx = 1; + for (i = 0; i < files.length; i++) + { + id = files[i].id; + if (id < mx) + { + mx = id; + } + } + this._id = -1 + mx; + } + downloadAttachment(id: number): void { + //GetReportFile + + let typeofCall = "personId_" + id.toString(); + + let criteria:any ={id, fileName:''}; + this.downloadFile.set(false); + this.http.getFileResponse("api/FileUpload/downloadPersonPhoto", criteria).pipe( + catchError(err => { + console.error(err.message); + return EMPTY; + }), + finalize(() => { + // this.toastr.success("download completed ok to view now"); + this.downloadFile.set(false); + + })).subscribe((response: HttpResponse) => { + if (response) { + console.log("the download report response", response); + CommonUtilities.downloadFile(response, typeofCall); + } + }); + } + +onClose(event: Event): void { + const nlist = this.FileList.filter( x => x.id < 1); + this.ref.close({list: nlist, deleteIds: this.deletePersonPhoto}); +} +newSupportDoc(event: Event): void { + this.showEdit(this._id--); +} + +remove(id: number): void { + if (id < 0) + { + const nlist = this.FileList.filter(x => x.id != id); + this.FileList = [...nlist]; + this.cdr.markForCheck(); + } + else + { + const deletepersonPhoto$ = this.personService.deletePersonPhotoFile(id); + this.subscription.add(deletepersonPhoto$.subscribe( + { + next: x => { + if (x.statusCode == 1) + { + const nlist = this.FileList.filter(x => x.id != id); + this.FileList = [...nlist]; + this.deletePersonPhoto.push(id); + this.cdr.markForCheck(); + this.messageService.add({severity:'success', summary: 'delete person photo', detail: "person photo Id " + id }); + } + }, + error:e => console.error("error in client delete person photo", e) + } + )); + } + + + +} + showEdit(id: number) { + const ref = this.dialogService.open(AddPhoto, { + data: { + id, + }, + header: 'Add File', + width: '70%', + maximizable: true, + draggable: true + }); + if (ref) + { + ref.onClose.subscribe((list: File[]) => { + if (list) { + //console.log("after close ward edit", item); + this.messageService.add({ severity: 'success', summary: 'Attact File', detail: list.length.toString() }); + let it = id; + for (let i = 0; i < list.length; i++) + { + const item = list[i]; + this.FileList.push({id: it--, photo: item.name, photoType:'', file: item}); + } + this.cdr.markForCheck(); + } + }); + } + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} diff --git a/UI/src/app/import.com/load_and_preview.txt b/UI/src/app/import.com/load_and_preview.txt new file mode 100644 index 0000000..1d24b19 --- /dev/null +++ b/UI/src/app/import.com/load_and_preview.txt @@ -0,0 +1,12 @@ +
+ + your image +
+ + +imgInp.onchange = evt => { + const [file] = imgInp.files + if (file) { + blah.src = URL.createObjectURL(file) + } +} diff --git a/UI/src/app/models/index.ts b/UI/src/app/models/index.ts index 68ef389..66d90ec 100644 --- a/UI/src/app/models/index.ts +++ b/UI/src/app/models/index.ts @@ -7,4 +7,5 @@ export * from './lookup'; export * from './staff'; export * from './person'; export * from './job'; -export * from './relationship'; \ No newline at end of file +export * from './relationship'; +export * from './personphotodto'; \ No newline at end of file diff --git a/UI/src/app/models/person.ts b/UI/src/app/models/person.ts index 67163c2..081535f 100644 --- a/UI/src/app/models/person.ts +++ b/UI/src/app/models/person.ts @@ -1,3 +1,4 @@ +import { PersonPhotoDto } from "./personphotodto"; import { RelationShip } from "./relationship"; export interface FamilySearch { @@ -24,6 +25,7 @@ export interface Person { fatherName?:string |null; motherName?:string |null; relationShips?: RelationShip[]; + personPhotos?: PersonPhotoDto[]; } export interface PersonContainer diff --git a/UI/src/app/models/personphotodto.ts b/UI/src/app/models/personphotodto.ts new file mode 100644 index 0000000..9dbe284 --- /dev/null +++ b/UI/src/app/models/personphotodto.ts @@ -0,0 +1,6 @@ +export interface PersonPhotoDto { + id: number; + photo:string|null | undefined; + photoType:string |null | undefined; + file: File | null | undefined; +} \ No newline at end of file diff --git a/UI/src/app/person/person.edit.css b/UI/src/app/person/person.edit.css index 139597f..91b1568 100644 --- a/UI/src/app/person/person.edit.css +++ b/UI/src/app/person/person.edit.css @@ -1,2 +1,13 @@ +.profilePhotoBorder +{ + + border-color: gray; + border-width: 2px; + border-radius: 15%; +} - +.profilePhotoWH +{ + width: 150px; + height: 100px; +} \ No newline at end of file diff --git a/UI/src/app/person/person.edit.html b/UI/src/app/person/person.edit.html index f3d9c78..09f8fbd 100644 --- a/UI/src/app/person/person.edit.html +++ b/UI/src/app/person/person.edit.html @@ -1,28 +1,69 @@
-
-
- - -
-
- - -
-
- - -
-
- - +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+
+ + + @if (adminuserForm.value.image != "" && adminuserForm.value.image != null) + { + your image + + } + @else + { +
+ +
+ } +
+ @if (adminuserForm.value.image != "" && adminuserForm.value.image != null) + { + + } + +
+ +
+ + + +
Alive
-
- -
-
-
- @if (adminuserForm.value.image != "" && adminuserForm.value.image != null) - { -
- - - -
- } - @else - { -
- -
- - -
-
- } -
-
-
-
- +
+
+
+ + +
diff --git a/UI/src/app/person/person.edit.ts b/UI/src/app/person/person.edit.ts index e646013..5379433 100644 --- a/UI/src/app/person/person.edit.ts +++ b/UI/src/app/person/person.edit.ts @@ -6,7 +6,7 @@ import { FormControl, ReactiveFormsModule, UntypedFormBuilder, Validators } from import { Subject, Subscription} from 'rxjs'; import { ConfirmationService, MessageService } from 'primeng/api'; import { DatePickerModule } from 'primeng/datepicker'; -import { Code, RelationShipView, Person, mState, RelationShip} from '../models'; +import { Code, RelationShipView, Person, mState, RelationShip, PersonPhotoDto} from '../models'; import { AppSettingService, LookupService, Utils } from '../shares'; import { PersonService } from './person.service'; import { ButtonModule } from 'primeng/button'; @@ -19,6 +19,9 @@ import { TableModule } from 'primeng/table'; import { Pickperson } from '../pickperson/pickperson'; import { ImageDisplayComponent } from '../pickperson/image.display'; import { TooltipModule } from 'primeng/tooltip'; +import { AutoFocusModule } from 'primeng/autofocus'; +import { DomSanitizer } from '@angular/platform-browser'; +import { PhotoList } from '../attachphoto/photolist'; export interface FileUploadEvent { originalEvent: HttpEvent; @@ -32,6 +35,7 @@ export interface FileUploadHandlerEvent { templateUrl: 'person.edit.html', selector: 'person-edit', imports:[ButtonModule,TableModule,ReactiveFormsModule,TooltipModule, + AutoFocusModule, SelectModule,CheckboxModule, InputTextModule,DatePickerModule], styleUrls: ['person.edit.css'], providers: [DialogService] @@ -39,6 +43,8 @@ providers: [DialogService] }) export class PersonEdit implements OnInit, OnDestroy { editRef: DynamicDialogRef | undefined; + private sanitizer = inject(DomSanitizer); + imageDataUrl = signal(""); returnUrl =''; loginUser =''; hostsite =''; @@ -49,8 +55,9 @@ export class PersonEdit implements OnInit, OnDestroy { motherList: Code[] =[]; sexList:Code[] =[]; familyList: Person[] =[]; - file: File | null = null; - fileName = ''; + profileFile: File | null = null; + photoList: PersonPhotoDto[] = []; + fileName = ''; isNew = false; validationPoints?: Code[]; partners = signal([]); @@ -126,15 +133,27 @@ getClassForRequire(prev:string,name:string){ str += " ng-invalid ng-dirty"; return str; } +dislayImage(): any { + if (this.profileFile) + { + return URL.createObjectURL(this.profileFile); + } + else if (this.imageDataUrl() != "") + { + return this.imageDataUrl(); + } + + return null; +} onFileSelected(event: any) { const file: File = event.target.files[0]; if (file) { - this.file = file; + this.profileFile = file; //this.messageService.add({ severity: 'info', summary: 'Success', detail: 'File Uploaded!' }); - this.adminuserForm.patchValue({ image: this.file.name }); + this.adminuserForm.patchValue({ image: this.profileFile.name }); this.fileName = file.name; this.subChanged$.next(false); } @@ -165,6 +184,14 @@ deleteFile(): void { doDeleteFile(): void { const fileName = this.adminuserForm.value.image; + if (fileName) + { + this.doDeleteImage(fileName); + } + else if (this.profileFile) + this.profileFile = null; +} +doDeleteImage(fileName:string) : void { const familyId = this.adminuserForm.value.id; const data = { fileName, familyId }; const delete$ = this.personService.deleteUploadFile(data); @@ -187,8 +214,6 @@ doDeleteFile(): void { } } )); - - } getFileToSave(file: File): FormData { @@ -284,8 +309,7 @@ doViewImage(imageName:string): void { const ref = this.dialogService.open(ImageDisplayComponent, { data: { imageName, - }, - + }, header: 'View Image', draggable: true, width: '70%', @@ -308,6 +332,10 @@ assignValue(item:Person): void { this.fileName = item.image!; if (item.relationShips) this.populatePartner(item.relationShips, item.id); + if (item.personPhotos) + { + this.photoList = item.personPhotos; + } this.adminuserForm.patchValue({ id: item.id, email: item.email, @@ -328,12 +356,42 @@ assignValue(item:Person): void { dob: moment(dob).toDate() }); } + if (item.image && item.image.length > 0) + { + this.loadImage(item.image); + } // disable use false//true for not disable. this.subChanged$.next(true); } // convenience getter for easy access in form fields get f() { return this.adminuserForm.controls;} + + loadImage(fileName: string| null): void { + const download = this.personService.downloadFile(fileName!); + this.subscription.add(download.subscribe({ + next: x => { + if (x.statusCode == 1) + { + + this.displayIamge64(x.data); + + } + else + { + console.log("error in download in api ", x.message); + } + }, + error: e => console.error("error in download image", e) + })); + } + displayIamge64(baseImage: string): void + { + const changeUrl = (this.sanitizer.bypassSecurityTrustResourceUrl(baseImage) as any).changingThisBreaksApplicationSecurity; + console.log('this is bypassSecurityTrustResourceUrl', changeUrl); + const fullDataUri = `data:image/png;base64,${changeUrl}`; + this.imageDataUrl.set(fullDataUri); + } validate(adminuser:Person): boolean { let result = true; console.log("validate", adminuser); @@ -383,8 +441,6 @@ assignValue(item:Person): void { } console.log("get partner for save in partner list ", vitem); } - - return rlist; } @@ -423,10 +479,10 @@ assignValue(item:Person): void { let container:any = { person: item }; - if (this.file != null) + if (this.profileFile != null) { - container.formData = await Utils.toBase64(this.file); - container.fileName = this.file.name; + container.formData = await Utils.toBase64(this.profileFile); + container.fileName = this.profileFile.name; console.log('image as base64 ', container); } container.person.relationShips = this.getPartnerForSave(); @@ -439,7 +495,13 @@ assignValue(item:Person): void { // this.messageService.add({severity:'success', summary: 'Save user', detail: item.firstName + " " + item.lastName }); //this.router.navigate([this.returnUrl]); console.log("the person after save", item); - this.ref.close(item); + const list = this.getPhotoListforSave(); + if (list.length > 0) + { + this.savePhotoList(list, item); + } + else + this.ref.close(item); } else { @@ -498,7 +560,9 @@ populatePartner(list: RelationShip[], personId: number): void { if (vlist.length > 0) this.partners.set(vlist); } - +viewAttachment(): void { + this.showAttachment("Add family photo or other"); +} addPartner(): void { const title = "Partner"; @@ -575,6 +639,113 @@ showPickPerson(title:string, callback:(id: Person) => void) :void { }); } +showAttachment(title:string): void { + const ref = this.dialogService.open(PhotoList, { + data: { + id: this._id, + personPhotos: this.photoList + }, + header: title, + width: '80%', + draggable: true, + maximizable: true + }); + + ref.onClose.subscribe((ritem: any) => { + const item = ritem.list; + const deleteIds = ritem.deleteIds; + if (item) { + console.log("after close " + title, item); + this.messageService.add({severity:'success', summary: title, detail: "photo attachment " + item.length}); + //update the current list + // callback(item); + this.updatePhotoList(item); + } + if (deleteIds && deleteIds.length > 0) + { + let j = 0; + + for (let i = 0; i < deleteIds.length; i++) + { + const id = deleteIds[i]; + j = this.photoList.findIndex(x => x.id == id); + if (j > -1) + { + this.photoList.splice(j,1); + } + + } + this.cdr.markForCheck(); + } + + }); +} + +updatePhotoList(list: PersonPhotoDto[]): void { + for (let i = 0; i < list.length; i++) + { + const item = list[i]; + const idx = this.photoList.findIndex(x => x.id == item.id); + if (idx && idx < 0) + this.photoList.push(item); + else + { + let eitem = this.photoList[idx]; + eitem.photo = item.photo; + eitem.photoType = item.photoType; + eitem.file = item.file; + } + } + + console.log("the photos after close", this.photoList); + this.subChanged$.next(false);//make save button to enable. + this.cdr.markForCheck(); +} + +getPhotoListforSave(): PersonPhotoDto[] { + let result: PersonPhotoDto[] =[]; + for (let i = 0; i < this.photoList.length; i++) + { + if (this.photoList[i].id < 0) + { + result.push(this.photoList[i]); + } + } + return result; +} + +savePhotoList(list: PersonPhotoDto[], item:Person): void { + + const formData = new FormData(); + for (let i = 0; i < list.length; i++) + { + const file = list[i].file; + if (file) + formData.append("files", file, file.name); + } + formData.append('personId', this._id.toString()); + + const personPhoto$ = this.personService.savePersonPhotoList(formData); + this.subscription.add(personPhoto$.subscribe( + { + next: x => { + if (x.statusCode == 1) { + //const filename = x.data; + //this.adminuserForm.patchValue({ image: filename }); + this.ref.close(item); + //this.cdr.detectChanges(); + } + else + this.messageService.add({ severity: 'error', summary: 'Error save attach photos files', detail: 'Fail to photos file: ' + x.message }); + }, + error: e => + this.messageService.add({ severity: 'error', summary: 'Error attach photos file', detail: 'Fail to photos file: ' }) + + } + )); + +} + cancel(e:Event):void { e.preventDefault(); this.ref.close(null); diff --git a/UI/src/app/person/person.service.ts b/UI/src/app/person/person.service.ts index 1bd4ba4..e008cdd 100644 --- a/UI/src/app/person/person.service.ts +++ b/UI/src/app/person/person.service.ts @@ -89,4 +89,21 @@ export class PersonService { const baseUrl = this.appSetting.appSetting.baseUrl + "/" + ConfigureUrl.personUrl + "/DeleteUploadFile"; return this.http.post>(baseUrl, data); } + deletePersonPhotoFile(id: number): Observable> { + const data ={id, fileName:''}; + const baseUrl = this.appSetting.appSetting.baseUrl + "/" + ConfigureUrl.FileUploadUrl + "/DeletePersonPhoto"; + return this.http.post>(baseUrl, data); + } + savePersonPhotoList(data:FormData): Observable> { + const baseUrl = this.appSetting.appSetting.baseUrl + "/" + ConfigureUrl.FileUploadUrl + "/SavePersonPhoto"; + return this.http.post>(baseUrl, data); + } + downloadPersonPhoto(id: number): Observable> { + const baseUrl = this.appSetting.appSetting.baseUrl + "/" + ConfigureUrl.FileUploadUrl + "/DownloadPersonPhoto"; + const data = { + id, + fileName: '' + }; + return this.http.post>(baseUrl, data); + } } diff --git a/UI/src/app/shares/utils.ts b/UI/src/app/shares/utils.ts index 047e628..6c5ac8f 100644 --- a/UI/src/app/shares/utils.ts +++ b/UI/src/app/shares/utils.ts @@ -160,7 +160,13 @@ static formatNode(item:Person): TreeNode return node; } - +static getFileExtension(filename: string): string { + const lastDotIndex = filename.lastIndexOf('.'); + if (lastDotIndex !== -1 && lastDotIndex < filename.length - 1) { // Ensure a dot exists and is not the last character + return filename.substring(lastDotIndex + 1); + } + return ''; // No extension found +} static getChildForParentId(proName: MyProName , pid: number,childressNodes: Person[]): TreeNode[] { let result: TreeNode[] =[]; let tree_node_child: TreeNode[];