Skip to content

Commit bc7a423

Browse files
contacts
1 parent db32496 commit bc7a423

File tree

5 files changed

+434
-149
lines changed

5 files changed

+434
-149
lines changed

apps/desktop/src/components/main/body/contacts/index.tsx

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@hypr/ui/components/ui/resizable";
2+
13
import { Contact2Icon } from "lucide-react";
2-
import { useCallback } from "react";
4+
import { useCallback, useEffect } from "react";
35

46
import * as persisted from "../../../../store/tinybase/persisted";
57
import { type Tab, useTabs } from "../../../../store/zustand/tabs";
68
import { StandardTabWrapper } from "../index";
79
import { type TabItem, TabItemBase } from "../shared";
810
import { DetailsColumn } from "./details";
11+
import { OrganizationDetailsColumn } from "./organization-details";
912
import { OrganizationsColumn } from "./organizations";
10-
import { PeopleColumn } from "./people";
13+
import { PeopleColumn, useSortedHumanIds } from "./people";
1114

1215
export const TabItemContact: TabItem = (
1316
{
@@ -54,11 +57,19 @@ function ContactView({ tab }: { tab: Tab }) {
5457
const { selectedOrganization, selectedPerson } = tab.state;
5558

5659
const setSelectedOrganization = useCallback((value: string | null) => {
57-
updateContactsTabState(tab, { ...tab.state, selectedOrganization: value });
60+
updateContactsTabState(tab, {
61+
...tab.state,
62+
selectedOrganization: value,
63+
// Clear selected person when selecting an organization
64+
selectedPerson: value ? null : tab.state.selectedPerson,
65+
});
5866
}, [updateContactsTabState, tab]);
5967

6068
const setSelectedPerson = useCallback((value: string | null) => {
61-
updateContactsTabState(tab, { ...tab.state, selectedPerson: value });
69+
updateContactsTabState(tab, {
70+
...tab.state,
71+
selectedPerson: value,
72+
});
6273
}, [updateContactsTabState, tab]);
6374

6475
const handleSessionClick = useCallback((id: string) => {
@@ -71,22 +82,60 @@ function ContactView({ tab }: { tab: Tab }) {
7182
persisted.STORE_ID,
7283
);
7384

85+
const handleDeleteOrganization = persisted.UI.useDelRowCallback(
86+
"organizations",
87+
(org_id: string) => org_id,
88+
persisted.STORE_ID,
89+
);
90+
91+
// Get the list of humanIds to auto-select the first person (only when no org is selected)
92+
const { humanIds } = useSortedHumanIds(selectedOrganization);
93+
94+
// Auto-select first person on load if no person is selected and no org is selected
95+
useEffect(() => {
96+
if (!selectedOrganization && !selectedPerson && humanIds.length > 0) {
97+
setSelectedPerson(humanIds[0]);
98+
}
99+
}, [humanIds, selectedPerson, selectedOrganization, setSelectedPerson]);
100+
101+
const isViewingOrgDetails = selectedOrganization && !selectedPerson;
102+
74103
return (
75-
<div className="flex h-full">
76-
<OrganizationsColumn
77-
selectedOrganization={selectedOrganization}
78-
setSelectedOrganization={setSelectedOrganization}
79-
/>
80-
<PeopleColumn
81-
currentOrgId={selectedOrganization}
82-
currentHumanId={selectedPerson}
83-
setSelectedPerson={setSelectedPerson}
84-
/>
85-
<DetailsColumn
86-
selectedHumanId={selectedPerson}
87-
handleDeletePerson={handleDeletePerson}
88-
handleSessionClick={handleSessionClick}
89-
/>
90-
</div>
104+
<ResizablePanelGroup direction="horizontal" className="h-full">
105+
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
106+
<OrganizationsColumn
107+
selectedOrganization={selectedOrganization}
108+
setSelectedOrganization={setSelectedOrganization}
109+
isViewingOrgDetails={!!isViewingOrgDetails}
110+
/>
111+
</ResizablePanel>
112+
<ResizableHandle />
113+
<ResizablePanel defaultSize={25} minSize={20} maxSize={40}>
114+
<PeopleColumn
115+
currentOrgId={selectedOrganization}
116+
currentHumanId={selectedPerson}
117+
setSelectedPerson={setSelectedPerson}
118+
/>
119+
</ResizablePanel>
120+
<ResizableHandle />
121+
<ResizablePanel defaultSize={55} minSize={30}>
122+
{selectedOrganization && !selectedPerson
123+
? (
124+
// Show organization details when org is selected but no person is selected
125+
<OrganizationDetailsColumn
126+
selectedOrganizationId={selectedOrganization}
127+
handleDeleteOrganization={handleDeleteOrganization}
128+
/>
129+
)
130+
: (
131+
// Show person details when a person is selected or no org is selected
132+
<DetailsColumn
133+
selectedHumanId={selectedPerson}
134+
handleDeletePerson={handleDeletePerson}
135+
handleSessionClick={handleSessionClick}
136+
/>
137+
)}
138+
</ResizablePanel>
139+
</ResizablePanelGroup>
91140
);
92141
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { Button } from "@hypr/ui/components/ui/button";
2+
import { Input } from "@hypr/ui/components/ui/input";
3+
4+
import { Building2, Pencil, TrashIcon } from "lucide-react";
5+
import React, { useState } from "react";
6+
7+
import * as persisted from "../../../../store/tinybase/persisted";
8+
import { getInitials } from "./shared";
9+
10+
export function OrganizationDetailsColumn({
11+
selectedOrganizationId,
12+
handleDeleteOrganization,
13+
}: {
14+
selectedOrganizationId?: string | null;
15+
handleDeleteOrganization: (id: string) => void;
16+
}) {
17+
const [editingOrganization, setEditingOrganization] = useState<string | null>(null);
18+
const selectedOrgData = persisted.UI.useRow("organizations", selectedOrganizationId ?? "", persisted.STORE_ID);
19+
20+
const peopleInOrg = persisted.UI.useSliceRowIds(
21+
persisted.INDEXES.humansByOrg,
22+
selectedOrganizationId ?? "",
23+
persisted.STORE_ID,
24+
);
25+
26+
const allHumans = persisted.UI.useTable("humans", persisted.STORE_ID);
27+
28+
return (
29+
<div className="flex-1 flex flex-col">
30+
{selectedOrgData && selectedOrganizationId
31+
? (
32+
editingOrganization === selectedOrganizationId
33+
? (
34+
<EditOrganizationForm
35+
organizationId={selectedOrganizationId}
36+
onSave={() => setEditingOrganization(null)}
37+
onCancel={() => setEditingOrganization(null)}
38+
/>
39+
)
40+
: (
41+
<>
42+
<div className="px-6 py-4 border-b border-neutral-200">
43+
<div className="flex items-start gap-4">
44+
<div className="w-12 h-12 rounded-full bg-neutral-200 flex items-center justify-center">
45+
<Building2 className="h-6 w-6 text-neutral-600" />
46+
</div>
47+
<div className="flex-1">
48+
<div className="flex items-start justify-between">
49+
<div>
50+
<h2 className="text-lg font-semibold flex items-center gap-2">
51+
{selectedOrgData.name || "Unnamed Organization"}
52+
</h2>
53+
<p className="text-sm text-neutral-500 mt-1">
54+
{peopleInOrg.length} {peopleInOrg.length === 1 ? "person" : "people"}
55+
</p>
56+
</div>
57+
<div className="flex gap-2">
58+
<button
59+
onClick={() => setEditingOrganization(selectedOrganizationId)}
60+
className="p-2 rounded-md hover:bg-neutral-100 transition-colors"
61+
title="Edit organization"
62+
>
63+
<Pencil className="h-4 w-4 text-neutral-500" />
64+
</button>
65+
<button
66+
onClick={(e) => {
67+
e.preventDefault();
68+
e.stopPropagation();
69+
handleDeleteOrganization(selectedOrganizationId);
70+
}}
71+
className="p-2 rounded-md hover:bg-red-50 transition-colors"
72+
title="Delete organization"
73+
>
74+
<TrashIcon className="h-4 w-4 text-red-500 hover:text-red-600" />
75+
</button>
76+
</div>
77+
</div>
78+
</div>
79+
</div>
80+
</div>
81+
82+
<div className="flex-1 p-6">
83+
<h3 className="text-sm font-medium text-neutral-600 mb-4 pl-3">People</h3>
84+
<div className="overflow-y-auto" style={{ maxHeight: "65vh" }}>
85+
<div className="space-y-2">
86+
{peopleInOrg.length > 0
87+
? (
88+
peopleInOrg.map((humanId: string) => {
89+
const human = allHumans[humanId];
90+
if (!human) {
91+
return null;
92+
}
93+
94+
return (
95+
<div
96+
key={humanId}
97+
className="p-3 rounded-md border border-neutral-200 hover:bg-neutral-50 transition-colors"
98+
>
99+
<div className="flex items-center gap-3">
100+
<div className="w-8 h-8 rounded-full bg-neutral-200 flex items-center justify-center flex-shrink-0">
101+
<span className="text-xs font-medium text-neutral-600">
102+
{getInitials(human.name as string || human.email as string)}
103+
</span>
104+
</div>
105+
<div className="flex-1 min-w-0">
106+
<div className="font-medium text-sm truncate">
107+
{human.name || human.email || "Unnamed"}
108+
</div>
109+
{human.email && human.name && (
110+
<div className="text-xs text-neutral-500 truncate">{human.email as string}</div>
111+
)}
112+
</div>
113+
</div>
114+
</div>
115+
);
116+
})
117+
)
118+
: <p className="text-sm text-neutral-500 pl-3">No people in this organization</p>}
119+
</div>
120+
</div>
121+
</div>
122+
</>
123+
)
124+
)
125+
: (
126+
<div className="flex-1 flex items-center justify-center">
127+
<p className="text-sm text-neutral-500">Select an organization to view details</p>
128+
</div>
129+
)}
130+
</div>
131+
);
132+
}
133+
134+
function EditOrganizationForm({
135+
organizationId,
136+
onSave,
137+
onCancel,
138+
}: {
139+
organizationId: string;
140+
onSave: () => void;
141+
onCancel: () => void;
142+
}) {
143+
const orgData = persisted.UI.useRow("organizations", organizationId, persisted.STORE_ID);
144+
145+
if (!orgData) {
146+
return null;
147+
}
148+
149+
return (
150+
<div className="flex-1 flex flex-col">
151+
<div className="px-6 py-4 border-b border-neutral-200">
152+
<div className="flex items-center justify-between">
153+
<h3 className="text-lg font-semibold">Edit Organization</h3>
154+
<div className="flex gap-2">
155+
<Button
156+
type="button"
157+
variant="ghost"
158+
size="sm"
159+
onClick={onCancel}
160+
className="hover:bg-neutral-100 text-neutral-700"
161+
>
162+
Cancel
163+
</Button>
164+
<Button
165+
onClick={onSave}
166+
variant="ghost"
167+
size="sm"
168+
className="bg-neutral-100 hover:bg-neutral-200 text-neutral-700"
169+
>
170+
Save
171+
</Button>
172+
</div>
173+
</div>
174+
</div>
175+
176+
<div className="flex-1 overflow-y-auto">
177+
<div className="flex flex-col items-center py-6">
178+
<div className="w-24 h-24 mb-3 bg-neutral-200 rounded-full flex items-center justify-center">
179+
<Building2 className="h-12 w-12 text-neutral-600" />
180+
</div>
181+
</div>
182+
183+
<div className="border-t border-neutral-200">
184+
<EditOrganizationNameField organizationId={organizationId} />
185+
</div>
186+
</div>
187+
</div>
188+
);
189+
}
190+
191+
function EditOrganizationNameField({ organizationId }: { organizationId: string }) {
192+
const value = persisted.UI.useCell("organizations", organizationId, "name", persisted.STORE_ID);
193+
194+
const handleChange = persisted.UI.useSetCellCallback(
195+
"organizations",
196+
organizationId,
197+
"name",
198+
(e: React.ChangeEvent<HTMLInputElement>) => e.target.value,
199+
[],
200+
persisted.STORE_ID,
201+
);
202+
203+
return (
204+
<div className="flex items-center px-4 py-3 border-b border-neutral-200">
205+
<div className="w-28 text-sm text-neutral-500">Name</div>
206+
<div className="flex-1">
207+
<Input
208+
value={(value as string) || ""}
209+
onChange={handleChange}
210+
placeholder="Organization name"
211+
className="border-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0"
212+
/>
213+
</div>
214+
</div>
215+
);
216+
}

0 commit comments

Comments
 (0)