|
|
@@ -140,8 +140,20 @@ export default function ComposeScreen() {
|
|
|
|
staleTime: 60000,
|
|
|
|
staleTime: 60000,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Group: workspace → flat list of integrations
|
|
|
|
type CustomerGroup = {
|
|
|
|
const grouped = useMemo(() => {
|
|
|
|
customerId: string;
|
|
|
|
|
|
|
|
customerName: string;
|
|
|
|
|
|
|
|
channels: IntegrationWithWorkspace[];
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type WorkspaceGroup = {
|
|
|
|
|
|
|
|
workspace: PostizWorkspace;
|
|
|
|
|
|
|
|
customers: CustomerGroup[];
|
|
|
|
|
|
|
|
allChannels: IntegrationWithWorkspace[];
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Group: workspace → customers → channels
|
|
|
|
|
|
|
|
const grouped = useMemo((): WorkspaceGroup[] => {
|
|
|
|
if (!allIntegrations) return [];
|
|
|
|
if (!allIntegrations) return [];
|
|
|
|
const byWorkspace = new Map<string, IntegrationWithWorkspace[]>();
|
|
|
|
const byWorkspace = new Map<string, IntegrationWithWorkspace[]>();
|
|
|
|
for (const intg of allIntegrations) {
|
|
|
|
for (const intg of allIntegrations) {
|
|
|
@@ -150,12 +162,21 @@ export default function ComposeScreen() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return workspaces
|
|
|
|
return workspaces
|
|
|
|
.filter((ws) => byWorkspace.has(ws.id))
|
|
|
|
.filter((ws) => byWorkspace.has(ws.id))
|
|
|
|
.map((ws) => ({ workspace: ws, channels: byWorkspace.get(ws.id)! }));
|
|
|
|
.map((ws) => {
|
|
|
|
|
|
|
|
const allChannels = byWorkspace.get(ws.id)!;
|
|
|
|
|
|
|
|
const byCustomer = new Map<string, CustomerGroup>();
|
|
|
|
|
|
|
|
for (const ch of allChannels) {
|
|
|
|
|
|
|
|
const cId = ch.customer?.id ?? "__default__";
|
|
|
|
|
|
|
|
const cName = ch.customer?.name ?? ws.name;
|
|
|
|
|
|
|
|
if (!byCustomer.has(cId)) byCustomer.set(cId, { customerId: cId, customerName: cName, channels: [] });
|
|
|
|
|
|
|
|
byCustomer.get(cId)!.channels.push(ch);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return { workspace: ws, customers: Array.from(byCustomer.values()), allChannels };
|
|
|
|
|
|
|
|
});
|
|
|
|
}, [allIntegrations, workspaces]);
|
|
|
|
}, [allIntegrations, workspaces]);
|
|
|
|
|
|
|
|
|
|
|
|
const toggleWorkspace = (wsId: string) => {
|
|
|
|
const toggleWorkspace = (wsId: string) => {
|
|
|
|
const wsChannels = grouped.find((g) => g.workspace.id === wsId)?.channels ?? [];
|
|
|
|
const allIds = (grouped.find((g) => g.workspace.id === wsId)?.allChannels ?? []).map((c) => c.id);
|
|
|
|
const allIds = wsChannels.map((c) => c.id);
|
|
|
|
|
|
|
|
const allSelected = allIds.every((id) => selectedChannels.includes(id));
|
|
|
|
const allSelected = allIds.every((id) => selectedChannels.includes(id));
|
|
|
|
if (allSelected) {
|
|
|
|
if (allSelected) {
|
|
|
|
setSelectedChannels((prev) => prev.filter((id) => !allIds.includes(id)));
|
|
|
|
setSelectedChannels((prev) => prev.filter((id) => !allIds.includes(id)));
|
|
|
@@ -164,6 +185,15 @@ export default function ComposeScreen() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const toggleCustomer = (customerIds: string[]) => {
|
|
|
|
|
|
|
|
const allSelected = customerIds.every((id) => selectedChannels.includes(id));
|
|
|
|
|
|
|
|
if (allSelected) {
|
|
|
|
|
|
|
|
setSelectedChannels((prev) => prev.filter((id) => !customerIds.includes(id)));
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
setSelectedChannels((prev) => [...new Set([...prev, ...customerIds])]);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const effectiveCharLimit = useMemo(() => {
|
|
|
|
const effectiveCharLimit = useMemo(() => {
|
|
|
|
if (selectedChannels.length === 0 || !allIntegrations) return 3000;
|
|
|
|
if (selectedChannels.length === 0 || !allIntegrations) return 3000;
|
|
|
|
const selected = allIntegrations.filter((i) => selectedChannels.includes(i.id));
|
|
|
|
const selected = allIntegrations.filter((i) => selectedChannels.includes(i.id));
|
|
|
@@ -535,11 +565,11 @@ export default function ComposeScreen() {
|
|
|
|
</Text>
|
|
|
|
</Text>
|
|
|
|
) : (
|
|
|
|
) : (
|
|
|
|
<View style={styles.channelGroups}>
|
|
|
|
<View style={styles.channelGroups}>
|
|
|
|
{grouped.map(({ workspace, channels }, wsIdx) => {
|
|
|
|
{grouped.map(({ workspace, customers, allChannels }, wsIdx) => {
|
|
|
|
const allIds = channels.map((c) => c.id);
|
|
|
|
const wsAllIds = allChannels.map((c) => c.id);
|
|
|
|
const selectedCount = allIds.filter((id) => selectedChannels.includes(id)).length;
|
|
|
|
const wsSelectedCount = wsAllIds.filter((id) => selectedChannels.includes(id)).length;
|
|
|
|
const allSelected = selectedCount === allIds.length;
|
|
|
|
const wsAllSelected = wsSelectedCount === wsAllIds.length;
|
|
|
|
const someSelected = selectedCount > 0 && !allSelected;
|
|
|
|
const wsSomeSelected = wsSelectedCount > 0 && !wsAllSelected;
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<View
|
|
|
|
<View
|
|
|
|
key={workspace.id}
|
|
|
|
key={workspace.id}
|
|
|
@@ -550,33 +580,65 @@ export default function ComposeScreen() {
|
|
|
|
]}
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{/* Workspace header — tap to select/deselect all */}
|
|
|
|
{/* Workspace header — tap to select/deselect all */}
|
|
|
|
<TouchableOpacity
|
|
|
|
{workspaces.length > 1 && (
|
|
|
|
activeOpacity={0.7}
|
|
|
|
<TouchableOpacity
|
|
|
|
onPress={() => toggleWorkspace(workspace.id)}
|
|
|
|
activeOpacity={0.7}
|
|
|
|
style={[styles.workspaceHeader, { borderBottomColor: colors.border }]}
|
|
|
|
onPress={() => toggleWorkspace(workspace.id)}
|
|
|
|
>
|
|
|
|
style={[styles.workspaceHeader, { borderBottomColor: colors.border }]}
|
|
|
|
<Feather name="briefcase" size={12} color={colors.primary} />
|
|
|
|
>
|
|
|
|
<Text style={[styles.workspaceName, { color: colors.primary, flex: 1 }]}>
|
|
|
|
<Feather name="briefcase" size={12} color={colors.primary} />
|
|
|
|
{workspace.name}
|
|
|
|
<Text style={[styles.workspaceName, { color: colors.primary, flex: 1 }]}>
|
|
|
|
</Text>
|
|
|
|
{workspace.name}
|
|
|
|
<Feather
|
|
|
|
</Text>
|
|
|
|
name={allSelected ? "check-square" : someSelected ? "minus-square" : "square"}
|
|
|
|
<Feather
|
|
|
|
size={14}
|
|
|
|
name={wsAllSelected ? "check-square" : wsSomeSelected ? "minus-square" : "square"}
|
|
|
|
color={allSelected || someSelected ? colors.primary : colors.mutedForeground}
|
|
|
|
size={14}
|
|
|
|
/>
|
|
|
|
color={wsAllSelected || wsSomeSelected ? colors.primary : colors.mutedForeground}
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Flat channel chips */}
|
|
|
|
|
|
|
|
<View style={styles.chipRow}>
|
|
|
|
|
|
|
|
{channels.map((intg) => (
|
|
|
|
|
|
|
|
<ChannelChip
|
|
|
|
|
|
|
|
key={intg.id}
|
|
|
|
|
|
|
|
integration={intg}
|
|
|
|
|
|
|
|
selected={selectedChannels.includes(intg.id)}
|
|
|
|
|
|
|
|
onToggle={() => toggleChannel(intg.id)}
|
|
|
|
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</TouchableOpacity>
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Customer sub-sections */}
|
|
|
|
|
|
|
|
{customers.map((cust, cIdx) => {
|
|
|
|
|
|
|
|
const custIds = cust.channels.map((c) => c.id);
|
|
|
|
|
|
|
|
const custSelectedCount = custIds.filter((id) => selectedChannels.includes(id)).length;
|
|
|
|
|
|
|
|
const custAllSelected = custSelectedCount === custIds.length;
|
|
|
|
|
|
|
|
const custSomeSelected = custSelectedCount > 0 && !custAllSelected;
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
<View
|
|
|
|
|
|
|
|
key={cust.customerId}
|
|
|
|
|
|
|
|
style={cIdx > 0 ? [styles.customerSection, { borderTopColor: colors.border }] : undefined}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{/* Customer header — tap to select/deselect all channels for this customer */}
|
|
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
|
|
onPress={() => toggleCustomer(custIds)}
|
|
|
|
|
|
|
|
style={styles.customerHeader}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<Text style={[styles.customerName, { color: colors.foreground }]}>
|
|
|
|
|
|
|
|
{cust.customerName}
|
|
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
<Feather
|
|
|
|
|
|
|
|
name={custAllSelected ? "check-square" : custSomeSelected ? "minus-square" : "square"}
|
|
|
|
|
|
|
|
size={14}
|
|
|
|
|
|
|
|
color={custAllSelected || custSomeSelected ? colors.primary : colors.mutedForeground}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{/* Channel chips for this customer */}
|
|
|
|
|
|
|
|
<View style={styles.chipRow}>
|
|
|
|
|
|
|
|
{cust.channels.map((intg) => (
|
|
|
|
|
|
|
|
<ChannelChip
|
|
|
|
|
|
|
|
key={intg.id}
|
|
|
|
|
|
|
|
integration={intg}
|
|
|
|
|
|
|
|
selected={selectedChannels.includes(intg.id)}
|
|
|
|
|
|
|
|
onToggle={() => toggleChannel(intg.id)}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
})}
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
})}
|
|
|
@@ -761,7 +823,13 @@ const styles = StyleSheet.create({
|
|
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
workspaceName: { fontSize: 11, fontFamily: "Inter_600SemiBold", letterSpacing: 0.5 },
|
|
|
|
workspaceName: { fontSize: 11, fontFamily: "Inter_600SemiBold", letterSpacing: 0.5 },
|
|
|
|
chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 6, padding: 10 },
|
|
|
|
customerSection: { borderTopWidth: StyleSheet.hairlineWidth },
|
|
|
|
|
|
|
|
customerHeader: {
|
|
|
|
|
|
|
|
flexDirection: "row", alignItems: "center", justifyContent: "space-between",
|
|
|
|
|
|
|
|
paddingHorizontal: 12, paddingVertical: 8,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
customerName: { fontSize: 13, fontFamily: "Inter_600SemiBold" },
|
|
|
|
|
|
|
|
chipRow: { flexDirection: "row", flexWrap: "wrap", gap: 6, paddingHorizontal: 10, paddingBottom: 10 },
|
|
|
|
scheduleRow: {
|
|
|
|
scheduleRow: {
|
|
|
|
flexDirection: "row", alignItems: "center", justifyContent: "space-between",
|
|
|
|
flexDirection: "row", alignItems: "center", justifyContent: "space-between",
|
|
|
|
paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12, borderWidth: 1,
|
|
|
|
paddingHorizontal: 16, paddingVertical: 12, borderRadius: 12, borderWidth: 1,
|
|
|
|