basic phone number support
Some checks failed
/ integration-test--firefox (push) Failing after 3m8s

This commit is contained in:
Robert Perce 2026-01-31 21:01:01 -06:00
parent 84c41dda4d
commit 2e1fbd00be
3 changed files with 90 additions and 2 deletions

View file

@ -0,0 +1,5 @@
create table if not exists phone_numbers (
contact_id integer not null references contacts(id) on delete cascade,
label text,
phone_number text not null
);

View file

@ -97,7 +97,7 @@ impl HydratedContact {
let raw = sqlx::query_as!( let raw = sqlx::query_as!(
RawHydratedContact, RawHydratedContact,
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", ( r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
select string_agg(name,'\x1c' order by sort) select string_agg(name,x'1c' order by sort)
from names where contact_id = c.id from names where contact_id = c.id
) as names, ( ) as names, (
select jes.date from journal_entries jes select jes.date from journal_entries jes
@ -124,7 +124,7 @@ impl HydratedContact {
let contacts = sqlx::query_as!( let contacts = sqlx::query_as!(
RawHydratedContact, RawHydratedContact,
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", ( r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
select string_agg(name,'\x1c' order by sort) select string_agg(name,x'1c' order by sort)
from names where contact_id = c.id from names where contact_id = c.id
) as names, ( ) as names, (
select jes.date from journal_entries jes select jes.date from journal_entries jes

View file

@ -37,6 +37,13 @@ pub struct Group {
pub slug: String, pub slug: String,
} }
#[derive(serde::Serialize, Debug)]
pub struct PhoneNumber {
pub contact_id: DbId,
pub label: Option<String>,
pub phone_number: String,
}
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/contact/new", post(self::post::contact)) .route("/contact/new", post(self::post::contact))
@ -93,6 +100,12 @@ mod get {
.fetch_all(pool) .fetch_all(pool)
.await?; .await?;
let phone_numbers: Vec<PhoneNumber> = sqlx::query_as!(
PhoneNumber,
"select * from phone_numbers where contact_id = $1",
contact_id
).fetch_all(pool).await?;
let lives_with = if contact.lives_with.len() > 1 { let lives_with = if contact.lives_with.len() > 1 {
let mention_host = MentionHost { let mention_host = MentionHost {
entity_id: contact_id, entity_id: contact_id,
@ -162,6 +175,19 @@ mod get {
} }
} }
@if phone_numbers.len() > 0 {
label { "phone" }
#phone_numbers {
@for phone_number in phone_numbers {
@let lbl = phone_number.label.unwrap_or(String::new());
.label data-is-empty=(lbl.len() == 0) { (lbl) }
.phone_nunber {
a href=(format!("tel:{}", phone_number.phone_number)) { (phone_number.phone_number) }
}
}
}
}
@if let Some(lives_with) = lives_with { @if let Some(lives_with) = lives_with {
label { "lives with" } label { "lives with" }
div { (lives_with) } div { (lives_with) }
@ -223,6 +249,12 @@ mod get {
let pool = &state.db(&auth_session.user.unwrap()).pool; let pool = &state.db(&auth_session.user.unwrap()).pool;
let contact = HydratedContact::load(contact_id, pool).await?; let contact = HydratedContact::load(contact_id, pool).await?;
let phone_numbers: Vec<PhoneNumber> = sqlx::query_as!(
PhoneNumber,
"select * from phone_numbers where contact_id = $1",
contact_id
).fetch_all(pool).await?;
let addresses: Vec<Address> = sqlx::query_as!( let addresses: Vec<Address> = sqlx::query_as!(
Address, Address,
"select * from addresses where contact_id = $1", "select * from addresses where contact_id = $1",
@ -290,6 +322,20 @@ mod get {
span x-text="date.length ? date.split('T')[0] : '(never)'" {} span x-text="date.length ? date.split('T')[0] : '(never)'" {}
input type="button" value="Mark fresh now" x-on:click="date = new Date().toISOString()"; input type="button" value="Mark fresh now" x-on:click="date = new Date().toISOString()";
} }
label { "phone" }
#phone_numbers x-data=(json!({ "phones": phone_numbers, "new_label": "", "new_number": "" })) {
template x-for="(phone, index) in phones" x-bind:key="index" {
.phone_input {
input name="phone_label" x-model="phone.label" placeholder="home/work/mobile";
input name="phone_number" x-model="phone.phone_number" placeholder="number";
}
}
.phone_input {
input name="phone_label" x-model="new_label" placeholder="home/work/mobile";
input name="phone_number" x-model="new_number" placeholder="number";
}
input type="button" value="Add" x-on:click="phones.push({ label: new_label, phone_number: new_number }); new_label=''; new_number = ''";
}
label { "lives with" } label { "lives with" }
div { div {
input name="lives_with" value=(contact.lives_with); input name="lives_with" value=(contact.lives_with);
@ -367,6 +413,8 @@ mod put {
birthday: String, birthday: String,
manually_freshened_at: String, manually_freshened_at: String,
lives_with: String, lives_with: String,
phone_label: Option<Vec<String>>,
phone_number: Option<Vec<String>>,
address_label: Option<Vec<String>>, address_label: Option<Vec<String>>,
address_value: Option<Vec<String>>, address_value: Option<Vec<String>>,
group: Option<Vec<String>>, group: Option<Vec<String>>,
@ -473,6 +521,41 @@ mod put {
// these blocks are not in functions because payload gets progressively // these blocks are not in functions because payload gets progressively
// partially moved as we handle each field and i don't want to deal with it // partially moved as we handle each field and i don't want to deal with it
{
// update phone numbers
let new_numbers = payload.phone_number.clone().map_or(vec![], |numbers| {
let labels: Vec<String> = payload.phone_label.clone().unwrap();
// TODO sanitize down to linkable on input
labels
.into_iter()
.zip(numbers)
.filter(|(_, val)| val.len() > 0)
.collect::<Vec<(String, String)>>()
});
let old_numbers: Vec<(String, String)> =
sqlx::query_as("select label, phone_number from phone_numbers where contact_id = $1")
.bind(contact_id)
.fetch_all(pool)
.await?;
if new_numbers != old_numbers {
sqlx::query!("delete from phone_numbers where contact_id = $1", contact_id)
.execute(pool)
.await?;
// trailing space in query intentional
QueryBuilder::new("insert into phone_numbers (contact_id, label, phone_number) ")
.push_values(new_numbers, |mut b, (label, phone_number)| {
b.push_bind(contact_id).push_bind(label).push_bind(phone_number);
})
.build()
.execute(pool)
.await?;
}
}
{ {
// update addresses // update addresses
let new_addresses = payload.address_value.clone().map(|values| { let new_addresses = payload.address_value.clone().map(|values| {