feat(org): adicionar visibilidade publica e privada com protecao em buscas e join requests
This commit is contained in:
parent
e7703f34e7
commit
3dff934d61
26 changed files with 149 additions and 26 deletions
|
|
@ -7,6 +7,7 @@ import net.beetechgroup.beetask.entities.Project;
|
||||||
public class Organization {
|
public class Organization {
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
private String name;
|
||||||
|
private boolean isPrivate;
|
||||||
private List<Project> projects;
|
private List<Project> projects;
|
||||||
private List<UserOrganization> users;
|
private List<UserOrganization> users;
|
||||||
|
|
||||||
|
|
@ -26,6 +27,14 @@ public class Organization {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPrivate() {
|
||||||
|
return isPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrivate(boolean isPrivate) {
|
||||||
|
this.isPrivate = isPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Project> getProjects() {
|
public List<Project> getProjects() {
|
||||||
return projects;
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ public class OrganizationEntity {
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
private String name;
|
private String name;
|
||||||
|
@Column(name = "is_private", nullable = false)
|
||||||
|
private boolean isPrivate;
|
||||||
@Column(name = "deleted_at")
|
@Column(name = "deleted_at")
|
||||||
private java.time.LocalDateTime deletedAt;
|
private java.time.LocalDateTime deletedAt;
|
||||||
@OneToMany(mappedBy = "organization")
|
@OneToMany(mappedBy = "organization")
|
||||||
|
|
@ -50,6 +52,14 @@ public class OrganizationEntity {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPrivate() {
|
||||||
|
return isPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPrivate(boolean isPrivate) {
|
||||||
|
this.isPrivate = isPrivate;
|
||||||
|
}
|
||||||
|
|
||||||
public java.time.LocalDateTime getDeletedAt() {
|
public java.time.LocalDateTime getDeletedAt() {
|
||||||
return deletedAt;
|
return deletedAt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ public class OrganizationEntityMapper {
|
||||||
OrganizationEntity entity = new OrganizationEntity();
|
OrganizationEntity entity = new OrganizationEntity();
|
||||||
entity.setId(organization.getId());
|
entity.setId(organization.getId());
|
||||||
entity.setName(organization.getName());
|
entity.setName(organization.getName());
|
||||||
|
entity.setPrivate(organization.isPrivate());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ public class OrganizationEntityMapper {
|
||||||
Organization organization = new Organization();
|
Organization organization = new Organization();
|
||||||
organization.setId(entity.getId());
|
organization.setId(entity.getId());
|
||||||
organization.setName(entity.getName());
|
organization.setName(entity.getName());
|
||||||
|
organization.setPrivate(entity.isPrivate());
|
||||||
return organization;
|
return organization;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,17 @@ public class OrganizationRepositoryImpl implements OrganizationRepository, Panac
|
||||||
@Override
|
@Override
|
||||||
public List<Organization> search(String query) {
|
public List<Organization> search(String query) {
|
||||||
if (Objects.isNull(query) || query.isBlank()) {
|
if (Objects.isNull(query) || query.isBlank()) {
|
||||||
List<Organization> organizations = findAll().list().stream()
|
List<Organization> organizations = find("isPrivate = false").list().stream()
|
||||||
.map(OrganizationEntityMapper::toDomain)
|
.map(OrganizationEntityMapper::toDomain)
|
||||||
.toList();
|
.toList();
|
||||||
LOGGER.infof("Loaded %d organizations with empty search query", organizations.size());
|
LOGGER.infof("Loaded %d public organizations with empty search query", organizations.size());
|
||||||
return organizations;
|
return organizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Organization> organizations = find("lower(name) like ?1", "%" + query.toLowerCase() + "%").stream()
|
List<Organization> organizations = find("isPrivate = false and lower(name) like ?1", "%" + query.toLowerCase() + "%").stream()
|
||||||
.map(OrganizationEntityMapper::toDomain)
|
.map(OrganizationEntityMapper::toDomain)
|
||||||
.toList();
|
.toList();
|
||||||
LOGGER.infof("Loaded %d organizations for search query '%s'", organizations.size(), query);
|
LOGGER.infof("Loaded %d public organizations for search query '%s'", organizations.size(), query);
|
||||||
return organizations;
|
return organizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ public class AuthControllerMapper {
|
||||||
output.expiresIn(),
|
output.expiresIn(),
|
||||||
output.issuedAt(),
|
output.issuedAt(),
|
||||||
output.organizations().stream()
|
output.organizations().stream()
|
||||||
.map(org -> new LoginResponse.Organization(org.id(), org.name(), org.roles()))
|
.map(org -> new LoginResponse.Organization(org.id(), org.name(), org.roles(), org.isPrivate()))
|
||||||
.collect(Collectors.toList())
|
.collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public record LoginResponse(
|
||||||
public record Organization(
|
public record Organization(
|
||||||
Long id,
|
Long id,
|
||||||
String name,
|
String name,
|
||||||
List<String> roles
|
List<String> roles,
|
||||||
|
boolean isPrivate
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package net.beetechgroup.beetask.interfaceadapters.controllers.organization;
|
package net.beetechgroup.beetask.interfaceadapters.controllers.organization;
|
||||||
|
|
||||||
public record CreateOrganizationRequest(
|
public record CreateOrganizationRequest(
|
||||||
String name) {
|
String name,
|
||||||
|
Boolean isPrivate) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package net.beetechgroup.beetask.interfaceadapters.controllers.organization;
|
package net.beetechgroup.beetask.interfaceadapters.controllers.organization;
|
||||||
|
|
||||||
public record CreateOrganizationResponse(Long id, String name) {
|
public record CreateOrganizationResponse(Long id, String name, boolean isPrivate) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ import net.beetechgroup.beetask.usecase.organization.create.CreateOrganizationOu
|
||||||
|
|
||||||
public class OrganizationControllerMapper {
|
public class OrganizationControllerMapper {
|
||||||
public static CreateOrganizationInput toCreateOrganizationInput(CreateOrganizationRequest request, String userEmail) {
|
public static CreateOrganizationInput toCreateOrganizationInput(CreateOrganizationRequest request, String userEmail) {
|
||||||
return new CreateOrganizationInput(request.name(), userEmail);
|
boolean isPrivate = request.isPrivate() != null ? request.isPrivate() : false;
|
||||||
|
return new CreateOrganizationInput(request.name(), userEmail, isPrivate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CreateOrganizationResponse toCreateOrganizationResponse(CreateOrganizationOutput output) {
|
public static CreateOrganizationResponse toCreateOrganizationResponse(CreateOrganizationOutput output) {
|
||||||
return new CreateOrganizationResponse(output.id(), output.name());
|
return new CreateOrganizationResponse(output.id(), output.name(), output.isPrivate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public record LoginOutput(
|
||||||
public record OrganizationOutput(
|
public record OrganizationOutput(
|
||||||
Long id,
|
Long id,
|
||||||
String name,
|
String name,
|
||||||
List<String> roles
|
List<String> roles,
|
||||||
|
boolean isPrivate
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ public class LoginUseCase {
|
||||||
return new LoginOutput.OrganizationOutput(
|
return new LoginOutput.OrganizationOutput(
|
||||||
entry.getKey(),
|
entry.getKey(),
|
||||||
first.getOrganization().getName(),
|
first.getOrganization().getName(),
|
||||||
entry.getValue().stream().map(uo -> uo.getRole().name()).toList()
|
entry.getValue().stream().map(uo -> uo.getRole().name()).toList(),
|
||||||
|
first.getOrganization().isPrivate()
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ public class RefreshTokenUseCase {
|
||||||
return new LoginOutput.OrganizationOutput(
|
return new LoginOutput.OrganizationOutput(
|
||||||
entry.getKey(),
|
entry.getKey(),
|
||||||
first.getOrganization().getName(),
|
first.getOrganization().getName(),
|
||||||
entry.getValue().stream().map(uo -> uo.getRole().name()).toList()
|
entry.getValue().stream().map(uo -> uo.getRole().name()).toList(),
|
||||||
|
first.getOrganization().isPrivate()
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package net.beetechgroup.beetask.usecase.organization.create;
|
package net.beetechgroup.beetask.usecase.organization.create;
|
||||||
|
|
||||||
public record CreateOrganizationInput(String name, String userEmail) {
|
public record CreateOrganizationInput(String name, String userEmail, boolean isPrivate) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ import net.beetechgroup.beetask.entities.organization.Organization;
|
||||||
|
|
||||||
public class CreateOrganizationMapper {
|
public class CreateOrganizationMapper {
|
||||||
public static CreateOrganizationOutput toCreateOrganizationOutput(Organization organization) {
|
public static CreateOrganizationOutput toCreateOrganizationOutput(Organization organization) {
|
||||||
return new CreateOrganizationOutput(organization.getId(), organization.getName());
|
return new CreateOrganizationOutput(organization.getId(), organization.getName(), organization.isPrivate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package net.beetechgroup.beetask.usecase.organization.create;
|
package net.beetechgroup.beetask.usecase.organization.create;
|
||||||
|
|
||||||
public record CreateOrganizationOutput(Long id, String name) {
|
public record CreateOrganizationOutput(Long id, String name, boolean isPrivate) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,10 @@ public class CreateOrganizationUseCase {
|
||||||
}
|
}
|
||||||
User user = userOptional.get();
|
User user = userOptional.get();
|
||||||
|
|
||||||
LOGGER.infof("Creating organization '%s' for owner %s", trimmedName, input.userEmail());
|
LOGGER.infof("Creating organization '%s' (private=%b) for owner %s", trimmedName, input.isPrivate(), input.userEmail());
|
||||||
Organization organization = new Organization();
|
Organization organization = new Organization();
|
||||||
organization.setName(trimmedName);
|
organization.setName(trimmedName);
|
||||||
|
organization.setPrivate(input.isPrivate());
|
||||||
organization = organizationRepository.saveOrganization(organization);
|
organization = organizationRepository.saveOrganization(organization);
|
||||||
|
|
||||||
UserOrganization userOrganization = new UserOrganization();
|
UserOrganization userOrganization = new UserOrganization();
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ public class RequestJoinOrganizationUseCase {
|
||||||
User user = userOptional.get();
|
User user = userOptional.get();
|
||||||
Organization organization = orgOptional.get();
|
Organization organization = orgOptional.get();
|
||||||
|
|
||||||
|
if (organization.isPrivate()) {
|
||||||
|
LOGGER.warnf("User %s attempted to request join into private organization %d ('%s')", userEmail, organizationId, organization.getName());
|
||||||
|
throw new IllegalStateException("Esta organização é privada e não permite solicitações de entrada.");
|
||||||
|
}
|
||||||
|
|
||||||
Optional<UserOrganization> existing = userOrganizationRepository.findByUserAndOrganization(user.getId(), organization.getId());
|
Optional<UserOrganization> existing = userOrganizationRepository.findByUserAndOrganization(user.getId(), organization.getId());
|
||||||
if (existing.isPresent()) {
|
if (existing.isPresent()) {
|
||||||
if (existing.get().getStatus() == UserOrganizationStatus.ACTIVE) {
|
if (existing.get().getStatus() == UserOrganizationStatus.ACTIVE) {
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,10 @@ public class CreateUserUseCase {
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.infof("Creating initial workspace '%s' for newly registered user %d (%s)", uniqueWorkspaceName, user.getId(), user.getEmail());
|
LOGGER.infof("Creating initial private workspace '%s' for newly registered user %d (%s)", uniqueWorkspaceName, user.getId(), user.getEmail());
|
||||||
Organization organization = new Organization();
|
Organization organization = new Organization();
|
||||||
organization.setName(uniqueWorkspaceName);
|
organization.setName(uniqueWorkspaceName);
|
||||||
|
organization.setPrivate(true); // Workspace pessoal do usuário é sempre privado
|
||||||
organization = organizationRepository.saveOrganization(organization);
|
organization = organizationRepository.saveOrganization(organization);
|
||||||
|
|
||||||
UserOrganization userOrganization = new UserOrganization();
|
UserOrganization userOrganization = new UserOrganization();
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ public class GetUserProfileUseCase {
|
||||||
return new UserProfileOutput.OrganizationProfileOutput(
|
return new UserProfileOutput.OrganizationProfileOutput(
|
||||||
entry.getKey(),
|
entry.getKey(),
|
||||||
firstUo.getOrganization().getName(),
|
firstUo.getOrganization().getName(),
|
||||||
entry.getValue().stream().map(uo -> uo.getRole().name()).collect(Collectors.toList())
|
entry.getValue().stream().map(uo -> uo.getRole().name()).collect(Collectors.toList()),
|
||||||
|
firstUo.getOrganization().isPrivate()
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ public record UserProfileOutput(
|
||||||
public record OrganizationProfileOutput(
|
public record OrganizationProfileOutput(
|
||||||
Long id,
|
Long id,
|
||||||
String name,
|
String name,
|
||||||
List<String> roles
|
List<String> roles,
|
||||||
|
boolean isPrivate
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- Add is_private column to organizations table
|
||||||
|
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS is_private BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- Personal organizations must always be private
|
||||||
|
UPDATE organizations
|
||||||
|
SET is_private = true
|
||||||
|
WHERE lower(name) LIKE '%pessoal%' OR lower(name) LIKE 'workspace de%';
|
||||||
|
|
||||||
|
-- Index for fast search filtering of public organizations
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_organizations_public_search
|
||||||
|
ON organizations (lower(name))
|
||||||
|
WHERE is_private = false AND deleted_at IS NULL;
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
Square,
|
Square,
|
||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
|
Lock,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useAuth } from '../../contexts/AuthContext'
|
import { useAuth } from '../../contexts/AuthContext'
|
||||||
import { useTheme } from '../../contexts/ThemeContext'
|
import { useTheme } from '../../contexts/ThemeContext'
|
||||||
|
|
@ -152,7 +153,10 @@ export const Header = ({ onToggleMobileSidebar }: HeaderProps) => {
|
||||||
{org.name.slice(0, 2).toUpperCase()}
|
{org.name.slice(0, 2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
<p className="truncate leading-tight">{org.name}</p>
|
<p className="truncate leading-tight flex items-center gap-1">
|
||||||
|
<span>{org.name}</span>
|
||||||
|
{org.isPrivate && <Lock className="w-2.5 h-2.5 text-amber-500/80 shrink-0" />}
|
||||||
|
</p>
|
||||||
<p className="text-[10px] opacity-75 font-medium">
|
<p className="text-[10px] opacity-75 font-medium">
|
||||||
{org.roles.join(', ')}
|
{org.roles.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
User,
|
User,
|
||||||
LogOut,
|
LogOut,
|
||||||
FileText,
|
FileText,
|
||||||
|
Lock,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Chip, ChipLabel } from '@heroui/react'
|
import { Chip, ChipLabel } from '@heroui/react'
|
||||||
import { useAuth } from '../../contexts/AuthContext'
|
import { useAuth } from '../../contexts/AuthContext'
|
||||||
|
|
@ -211,7 +212,10 @@ export const Sidebar = ({ isMobileOpen, onCloseMobile }: SidebarProps) => {
|
||||||
{org.name.slice(0, 2).toUpperCase()}
|
{org.name.slice(0, 2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
<p className="truncate leading-tight">{org.name}</p>
|
<p className="truncate leading-tight flex items-center gap-1">
|
||||||
|
<span>{org.name}</span>
|
||||||
|
{org.isPrivate && <Lock className="w-2.5 h-2.5 text-amber-500/80 shrink-0" />}
|
||||||
|
</p>
|
||||||
<p className="text-[9px] text-muted font-normal">
|
<p className="text-[9px] text-muted font-normal">
|
||||||
{org.roles.join(', ')}
|
{org.roles.join(', ')}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import {
|
||||||
Check,
|
Check,
|
||||||
Loader2,
|
Loader2,
|
||||||
Inbox,
|
Inbox,
|
||||||
|
Lock,
|
||||||
|
Globe,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import {
|
import {
|
||||||
|
|
@ -32,6 +34,7 @@ export const OrganizationsPage = () => {
|
||||||
|
|
||||||
// Create Org states
|
// Create Org states
|
||||||
const [newOrgName, setNewOrgName] = useState('')
|
const [newOrgName, setNewOrgName] = useState('')
|
||||||
|
const [isPrivateOrg, setIsPrivateOrg] = useState(false)
|
||||||
const [isCreating, setIsCreating] = useState(false)
|
const [isCreating, setIsCreating] = useState(false)
|
||||||
|
|
||||||
// Search & Join states
|
// Search & Join states
|
||||||
|
|
@ -75,11 +78,12 @@ export const OrganizationsPage = () => {
|
||||||
setSuccessMessage(null)
|
setSuccessMessage(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const created = await organizationService.create(newOrgName.trim())
|
const created = await organizationService.create(newOrgName.trim(), isPrivateOrg)
|
||||||
await refreshUser()
|
await refreshUser()
|
||||||
setActiveOrg(created.id)
|
setActiveOrg(created.id)
|
||||||
setSuccessMessage(`Workspace "${newOrgName}" criado com sucesso!`)
|
setSuccessMessage(`Workspace "${newOrgName}" criado com sucesso!`)
|
||||||
setNewOrgName('')
|
setNewOrgName('')
|
||||||
|
setIsPrivateOrg(false)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/dashboard')
|
navigate('/dashboard')
|
||||||
}, 800)
|
}, 800)
|
||||||
|
|
@ -324,6 +328,17 @@ export const OrganizationsPage = () => {
|
||||||
Ativo
|
Ativo
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{org.isPrivate ? (
|
||||||
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-amber-500/10 text-amber-300 text-[10px] font-medium border border-amber-500/20">
|
||||||
|
<Lock className="w-2.5 h-2.5" />
|
||||||
|
<span>Privado</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-blue-500/10 text-blue-400 text-[10px] font-medium border border-blue-500/20">
|
||||||
|
<Globe className="w-2.5 h-2.5" />
|
||||||
|
<span>Público</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted mt-0.5 font-medium">
|
<p className="text-xs text-muted mt-0.5 font-medium">
|
||||||
Papel:{' '}
|
Papel:{' '}
|
||||||
|
|
@ -642,6 +657,57 @@ export const OrganizationsPage = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5 pt-1">
|
||||||
|
<label className="text-xs font-bold text-foreground block">
|
||||||
|
Visibilidade do Workspace
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPrivateOrg(false)}
|
||||||
|
className={`p-3 rounded-2xl border text-left transition-all cursor-pointer flex items-start gap-2.5 ${
|
||||||
|
!isPrivateOrg
|
||||||
|
? 'border-brand bg-brand/10 ring-1 ring-brand/40 shadow-xs'
|
||||||
|
: 'border-border-soft bg-surface hover:bg-surface-raised'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-1.5 rounded-lg shrink-0 ${!isPrivateOrg ? 'bg-brand text-white' : 'bg-surface-sunken text-muted'}`}>
|
||||||
|
<Globe className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className={`text-xs font-bold ${!isPrivateOrg ? 'text-brand' : 'text-foreground'}`}>
|
||||||
|
Público (Corporativo)
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted leading-tight mt-0.5">
|
||||||
|
Aparece nas buscas para seus colaboradores solicitarem entrada.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsPrivateOrg(true)}
|
||||||
|
className={`p-3 rounded-2xl border text-left transition-all cursor-pointer flex items-start gap-2.5 ${
|
||||||
|
isPrivateOrg
|
||||||
|
? 'border-amber-500/80 bg-amber-500/10 ring-1 ring-amber-500/40 shadow-xs'
|
||||||
|
: 'border-border-soft bg-surface hover:bg-surface-raised'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-1.5 rounded-lg shrink-0 ${isPrivateOrg ? 'bg-amber-500 text-white' : 'bg-surface-sunken text-muted'}`}>
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className={`text-xs font-bold ${isPrivateOrg ? 'text-amber-400' : 'text-foreground'}`}>
|
||||||
|
Privado
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] text-muted leading-tight mt-0.5">
|
||||||
|
Oculto nas buscas. Não aceita pedidos de entrada externos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isCreating || !newOrgName.trim()}
|
disabled={isCreating || !newOrgName.trim()}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export type OrganizationMembership = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
roles: string[]
|
roles: string[]
|
||||||
|
isPrivate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoginResponse = {
|
export type LoginResponse = {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { apiFetch } from '../lib/api'
|
||||||
export type CreateOrgResponse = {
|
export type CreateOrgResponse = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
ownerId: number
|
isPrivate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchOrgOutput = {
|
export type SearchOrgOutput = {
|
||||||
|
|
@ -33,10 +33,10 @@ export type PendingJoinRequest = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const organizationService = {
|
export const organizationService = {
|
||||||
async create(name: string): Promise<CreateOrgResponse> {
|
async create(name: string, isPrivate: boolean = false): Promise<CreateOrgResponse> {
|
||||||
return apiFetch<CreateOrgResponse>('/organizations', {
|
return apiFetch<CreateOrgResponse>('/organizations', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { name },
|
body: { name, isPrivate },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue