Genealogy as a feature (#175)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Start our journey into turning GeneIT as afully featured family intranet by making genealogy a feature that can be disabled by family admins Reviewed-on: #175
This commit is contained in:
242
geneit_app/src/api/genealogy/CoupleApi.ts
Normal file
242
geneit_app/src/api/genealogy/CoupleApi.ts
Normal file
@ -0,0 +1,242 @@
|
||||
import { APIClient } from "../ApiClient";
|
||||
import { DateValue, Member } from "./MemberApi";
|
||||
import { ServerApi } from "../ServerApi";
|
||||
|
||||
interface CoupleApiInterface {
|
||||
id: number;
|
||||
family_id: number;
|
||||
wife?: number;
|
||||
husband?: number;
|
||||
state?: string;
|
||||
photo_id?: string;
|
||||
signed_photo_id?: string;
|
||||
time_create?: number;
|
||||
time_update?: number;
|
||||
wedding_year?: number;
|
||||
wedding_month?: number;
|
||||
wedding_day?: number;
|
||||
divorce_year?: number;
|
||||
divorce_month?: number;
|
||||
divorce_day?: number;
|
||||
}
|
||||
|
||||
export class Couple implements CoupleApiInterface {
|
||||
id: number;
|
||||
family_id: number;
|
||||
wife?: number;
|
||||
husband?: number;
|
||||
state?: string;
|
||||
photo_id?: string;
|
||||
signed_photo_id?: string;
|
||||
time_create?: number;
|
||||
time_update?: number;
|
||||
wedding_year?: number;
|
||||
wedding_month?: number;
|
||||
wedding_day?: number;
|
||||
divorce_year?: number;
|
||||
divorce_month?: number;
|
||||
divorce_day?: number;
|
||||
|
||||
constructor(int: CoupleApiInterface) {
|
||||
this.id = int.id;
|
||||
this.family_id = int.family_id;
|
||||
this.wife = int.wife;
|
||||
this.husband = int.husband;
|
||||
this.state = int.state;
|
||||
this.photo_id = int.photo_id;
|
||||
this.signed_photo_id = int.signed_photo_id;
|
||||
this.time_create = int.time_create;
|
||||
this.time_update = int.time_update;
|
||||
this.wedding_year = int.wedding_year;
|
||||
this.wedding_month = int.wedding_month;
|
||||
this.wedding_day = int.wedding_day;
|
||||
this.divorce_year = int.divorce_year;
|
||||
this.divorce_month = int.divorce_month;
|
||||
this.divorce_day = int.divorce_day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty couple object
|
||||
*/
|
||||
static New(family_id: number): Couple {
|
||||
return new Couple({
|
||||
id: 0,
|
||||
family_id: family_id,
|
||||
});
|
||||
}
|
||||
|
||||
get hasPhoto(): boolean {
|
||||
return this.photo_id !== null;
|
||||
}
|
||||
|
||||
get photoURL(): string | null {
|
||||
if (!this.signed_photo_id) return null;
|
||||
return `${APIClient.backendURL()}/photo/${this.signed_photo_id}`;
|
||||
}
|
||||
|
||||
get thumbnailURL(): string | null {
|
||||
if (!this.signed_photo_id) return null;
|
||||
return `${APIClient.backendURL()}/photo/${this.signed_photo_id}/thumbnail`;
|
||||
}
|
||||
|
||||
get dateOfWedding(): DateValue | undefined {
|
||||
if (!this.wedding_day && !this.wedding_month && !this.wedding_year)
|
||||
return undefined;
|
||||
|
||||
return {
|
||||
year: this.wedding_year,
|
||||
month: this.wedding_month,
|
||||
day: this.wedding_day,
|
||||
};
|
||||
}
|
||||
|
||||
get dateOfDivorce(): DateValue | undefined {
|
||||
if (!this.divorce_day && !this.divorce_month && !this.divorce_year)
|
||||
return undefined;
|
||||
|
||||
return {
|
||||
year: this.divorce_year,
|
||||
month: this.divorce_month,
|
||||
day: this.divorce_day,
|
||||
};
|
||||
}
|
||||
|
||||
otherPersonID(id: number): number | undefined {
|
||||
return id === this.wife ? this.husband : this.wife;
|
||||
}
|
||||
|
||||
get stateFr(): string {
|
||||
return (
|
||||
ServerApi.Config.couples_states.find((c) => c.code === this.state)?.fr ??
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CouplesList {
|
||||
private list: Couple[];
|
||||
private map: Map<number, Couple>;
|
||||
|
||||
constructor(list: Couple[]) {
|
||||
this.list = list;
|
||||
this.map = new Map();
|
||||
|
||||
for (const m of list) {
|
||||
this.map.set(m.id, m);
|
||||
}
|
||||
}
|
||||
|
||||
public get isEmpty(): boolean {
|
||||
return this.list.length === 0;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
public get fullList(): Couple[] {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
filter(predicate: (m: Couple) => boolean): Couple[] {
|
||||
return this.list.filter(predicate);
|
||||
}
|
||||
|
||||
get(id: number): Couple | undefined {
|
||||
return this.map.get(id);
|
||||
}
|
||||
|
||||
getAllOf(m: Member): Couple[] {
|
||||
return this.filter((c) => c.husband === m.id || c.wife === m.id);
|
||||
}
|
||||
|
||||
getFirstMariedOf(m: Member): Couple | undefined {
|
||||
return this.getAllOf(m).find((c) => (c.state = "M"));
|
||||
}
|
||||
}
|
||||
|
||||
export class CoupleApi {
|
||||
/**
|
||||
* Create a new couple
|
||||
*/
|
||||
static async Create(m: Couple): Promise<Couple> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/couple/create`,
|
||||
method: "POST",
|
||||
jsonData: m,
|
||||
});
|
||||
|
||||
return new Couple(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the information about a single couple
|
||||
*/
|
||||
static async GetSingle(
|
||||
family_id: number,
|
||||
couple_id: number
|
||||
): Promise<Couple> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/couple/${couple_id}`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new Couple(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entire list of couples of a family
|
||||
*/
|
||||
static async GetEntireList(family_id: number): Promise<CouplesList> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/couples`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new CouplesList(res.data.map((d: any) => new Couple(d)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a couple information
|
||||
*/
|
||||
static async Update(m: Couple): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
|
||||
method: "PUT",
|
||||
jsonData: m,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new photo for a couple
|
||||
*/
|
||||
static async SetCouplePhoto(m: Couple, b: Blob): Promise<void> {
|
||||
const fd = new FormData();
|
||||
fd.append("photo", b);
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
|
||||
method: "PUT",
|
||||
formData: fd,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the photo of a couple
|
||||
*/
|
||||
static async RemoveCouplePhoto(m: Couple): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a family couple
|
||||
*/
|
||||
static async Delete(m: Couple): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
}
|
31
geneit_app/src/api/genealogy/DataApi.ts
Normal file
31
geneit_app/src/api/genealogy/DataApi.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { APIClient } from "../ApiClient";
|
||||
|
||||
/**
|
||||
* Data management api client
|
||||
*/
|
||||
export class DataApi {
|
||||
/**
|
||||
* Export the data of a family
|
||||
*/
|
||||
static async ExportData(family_id: number): Promise<Blob> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/data/export`,
|
||||
method: "GET",
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the data of a family
|
||||
*/
|
||||
static async ImportData(family_id: number, archive: Blob): Promise<Blob> {
|
||||
const fd = new FormData();
|
||||
fd.append("archive", archive);
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/data/import`,
|
||||
method: "PUT",
|
||||
formData: fd,
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
}
|
359
geneit_app/src/api/genealogy/MemberApi.ts
Normal file
359
geneit_app/src/api/genealogy/MemberApi.ts
Normal file
@ -0,0 +1,359 @@
|
||||
import { APIClient } from "../ApiClient";
|
||||
import { Couple } from "./CoupleApi";
|
||||
|
||||
export type Sex = "M" | "F";
|
||||
|
||||
export interface MemberDataApi {
|
||||
id: number;
|
||||
family_id: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
birth_last_name?: string;
|
||||
photo_id?: number;
|
||||
signed_photo_id?: number;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
postal_code?: string;
|
||||
country?: string;
|
||||
sex?: Sex;
|
||||
time_create?: number;
|
||||
time_update?: number;
|
||||
mother?: number;
|
||||
father?: number;
|
||||
birth_year?: number;
|
||||
birth_month?: number;
|
||||
birth_day?: number;
|
||||
dead: boolean;
|
||||
death_year?: number;
|
||||
death_month?: number;
|
||||
death_day?: number;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface DateValue {
|
||||
year?: number;
|
||||
month?: number;
|
||||
day?: number;
|
||||
}
|
||||
|
||||
export class Member implements MemberDataApi {
|
||||
id: number;
|
||||
family_id: number;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
birth_last_name?: string;
|
||||
photo_id?: number;
|
||||
signed_photo_id?: number;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
postal_code?: string;
|
||||
country?: string;
|
||||
sex?: Sex;
|
||||
time_create?: number;
|
||||
time_update?: number;
|
||||
mother?: number;
|
||||
father?: number;
|
||||
birth_year?: number;
|
||||
birth_month?: number;
|
||||
birth_day?: number;
|
||||
dead!: boolean;
|
||||
death_year?: number;
|
||||
death_month?: number;
|
||||
death_day?: number;
|
||||
note?: string;
|
||||
|
||||
constructor(m: MemberDataApi) {
|
||||
this.id = m.id;
|
||||
this.family_id = m.family_id;
|
||||
this.first_name = m.first_name;
|
||||
this.last_name = m.last_name;
|
||||
this.birth_last_name = m.birth_last_name;
|
||||
this.photo_id = m.photo_id;
|
||||
this.signed_photo_id = m.signed_photo_id;
|
||||
this.email = m.email;
|
||||
this.phone = m.phone;
|
||||
this.address = m.address;
|
||||
this.city = m.city;
|
||||
this.postal_code = m.postal_code;
|
||||
this.country = m.country;
|
||||
this.sex = m.sex;
|
||||
this.time_create = m.time_create;
|
||||
this.time_update = m.time_update;
|
||||
this.mother = m.mother;
|
||||
this.father = m.father;
|
||||
this.birth_year = m.birth_year;
|
||||
this.birth_month = m.birth_month;
|
||||
this.birth_day = m.birth_day;
|
||||
this.dead = m.dead;
|
||||
this.death_year = m.death_year;
|
||||
this.death_month = m.death_month;
|
||||
this.death_day = m.death_day;
|
||||
this.note = m.note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty member object
|
||||
*/
|
||||
static New(family_id: number): Member {
|
||||
return new Member({
|
||||
id: 0,
|
||||
dead: false,
|
||||
family_id: family_id,
|
||||
sex: "M",
|
||||
});
|
||||
}
|
||||
|
||||
get lastNameUpperCase(): string | undefined {
|
||||
return this.last_name?.toUpperCase();
|
||||
}
|
||||
|
||||
get fullName(): string {
|
||||
const firstName = this.first_name ?? "";
|
||||
|
||||
return firstName.length === 0
|
||||
? this.last_name ?? ""
|
||||
: `${firstName} ${this.last_name?.toUpperCase() ?? ""}`;
|
||||
}
|
||||
|
||||
get invertedFullName(): string {
|
||||
const lastName = this.last_name ?? "";
|
||||
|
||||
return lastName.length === 0
|
||||
? this.last_name ?? ""
|
||||
: `${lastName} ${this.first_name ?? ""}`;
|
||||
}
|
||||
|
||||
get hasPhoto(): boolean {
|
||||
return this.photo_id !== null;
|
||||
}
|
||||
|
||||
get photoURL(): string | null {
|
||||
if (!this.signed_photo_id) return null;
|
||||
return `${APIClient.backendURL()}/photo/${this.signed_photo_id}`;
|
||||
}
|
||||
|
||||
get thumbnailURL(): string | null {
|
||||
if (!this.signed_photo_id) return null;
|
||||
return `${APIClient.backendURL()}/photo/${this.signed_photo_id}/thumbnail`;
|
||||
}
|
||||
|
||||
get dateOfBirth(): DateValue | undefined {
|
||||
if (!this.birth_day && !this.birth_month && !this.birth_year)
|
||||
return undefined;
|
||||
|
||||
return {
|
||||
year: this.birth_year,
|
||||
month: this.birth_month,
|
||||
day: this.birth_day,
|
||||
};
|
||||
}
|
||||
|
||||
get dateOfDeath(): DateValue | undefined {
|
||||
if (!this.death_day && !this.death_month && !this.death_year)
|
||||
return undefined;
|
||||
|
||||
return {
|
||||
year: this.death_year,
|
||||
month: this.death_month,
|
||||
day: this.death_day,
|
||||
};
|
||||
}
|
||||
|
||||
get displayBirthDeath(): string {
|
||||
let birthDeath = [];
|
||||
if (this.dateOfBirth) birthDeath.push(fmtDate(this.dateOfBirth));
|
||||
if (this.dateOfDeath) birthDeath.push(fmtDate(this.dateOfDeath));
|
||||
return birthDeath.join(" - ");
|
||||
}
|
||||
|
||||
get displayBirthDeathShort(): string {
|
||||
let birthDeath = [];
|
||||
if (this.birth_year) birthDeath.push(this.birth_year.toString());
|
||||
if (this.death_year) birthDeath.push(this.death_year.toString());
|
||||
return birthDeath.join(" - ");
|
||||
}
|
||||
|
||||
get hasContactInfo(): boolean {
|
||||
return this.email ||
|
||||
this.phone ||
|
||||
this.address ||
|
||||
this.city ||
|
||||
this.postal_code ||
|
||||
this.country
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
|
||||
get hasNote(): boolean {
|
||||
return (this.note?.length ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function fmtDate(d?: DateValue): string {
|
||||
if (d?.year && !d.month && !d.day)
|
||||
return d?.year?.toString().padStart(4, "0");
|
||||
|
||||
return `${d?.day?.toString().padStart(2, "0") ?? "__"}/${
|
||||
d?.month?.toString().padStart(2, "0") ?? "__"
|
||||
}/${d?.year?.toString().padStart(4, "0") ?? "__"}`;
|
||||
}
|
||||
|
||||
const OLD_TIME = -58991812735;
|
||||
export function dateTimestamp(d?: DateValue): number {
|
||||
if (!d) return OLD_TIME;
|
||||
|
||||
const date = new Date();
|
||||
date.setFullYear(d.year ?? 1010, (d.month ?? 1) - 1, d.day ?? 1);
|
||||
return date.getTime() / 1000;
|
||||
}
|
||||
|
||||
export class MembersList {
|
||||
private list: Member[];
|
||||
private map: Map<number, Member>;
|
||||
|
||||
constructor(list: Member[]) {
|
||||
this.list = list;
|
||||
this.map = new Map();
|
||||
|
||||
for (const m of list) {
|
||||
this.map.set(m.id, m);
|
||||
}
|
||||
|
||||
this.list.sort((a, b) =>
|
||||
a.invertedFullName
|
||||
.toLowerCase()
|
||||
.localeCompare(b.invertedFullName.toLocaleLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
public get isEmpty(): boolean {
|
||||
return this.list.length === 0;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.list.length;
|
||||
}
|
||||
|
||||
public get fullList(): Member[] {
|
||||
return this.list;
|
||||
}
|
||||
|
||||
filter(predicate: (m: Member) => boolean): Member[] {
|
||||
return this.list.filter(predicate);
|
||||
}
|
||||
|
||||
get(id: number): Member | undefined {
|
||||
return this.map.get(id);
|
||||
}
|
||||
|
||||
children(id: number): Member[] {
|
||||
return this.list.filter((m) => m.mother === id || m.father === id);
|
||||
}
|
||||
|
||||
childrenOfCouple(c: Couple): Member[] {
|
||||
if (!c.husband && !c.wife) return [];
|
||||
return this.list.filter(
|
||||
(m) => m.mother === c.wife && m.father === c.husband
|
||||
);
|
||||
}
|
||||
|
||||
siblings(id: number): Member[] {
|
||||
const p = this.get(id);
|
||||
return this.list.filter(
|
||||
(m) =>
|
||||
m.id !== p?.id &&
|
||||
((m.mother && m.mother === p?.mother) ||
|
||||
(m.father && m.father === p?.father))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class MemberApi {
|
||||
/**
|
||||
* Create a new member
|
||||
*/
|
||||
static async Create(m: Member): Promise<Member> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/member/create`,
|
||||
method: "POST",
|
||||
jsonData: m,
|
||||
});
|
||||
|
||||
return new Member(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the information about a single member
|
||||
*/
|
||||
static async GetSingle(
|
||||
family_id: number,
|
||||
member_id: number
|
||||
): Promise<Member> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/member/${member_id}`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new Member(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entire list of family members of a family
|
||||
*/
|
||||
static async GetEntireList(family_id: number): Promise<MembersList> {
|
||||
const res = await APIClient.exec({
|
||||
uri: `/family/${family_id}/genealogy/members`,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return new MembersList(res.data.map((d: any) => new Member(d)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a member information
|
||||
*/
|
||||
static async Update(m: Member): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
|
||||
method: "PUT",
|
||||
jsonData: m,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new photo for a member
|
||||
*/
|
||||
static async SetMemberPhoto(m: Member, b: Blob): Promise<void> {
|
||||
const fd = new FormData();
|
||||
fd.append("photo", b);
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
|
||||
method: "PUT",
|
||||
formData: fd,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the photo of a member
|
||||
*/
|
||||
static async RemoveMemberPhoto(m: Member): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a family member
|
||||
*/
|
||||
static async Delete(m: Member): Promise<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user