feat: team org panel, admin CRUD, local deploy tools, bidirectional data sync
Add TeamMember model and APIs, team status UI, /admin page, local server bats, and scripts to sync data between local PostgreSQL and Render. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
14
frontend/src/hooks/useTeamMembers.ts
Normal file
14
frontend/src/hooks/useTeamMembers.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '../lib/apiClient';
|
||||
import type { TeamMember } from '../types';
|
||||
|
||||
export function useTeamMembers() {
|
||||
return useQuery({
|
||||
queryKey: ['team-members'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<TeamMember[]>('/team-members');
|
||||
return data;
|
||||
},
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
85
frontend/src/hooks/useTeamMembersAdmin.ts
Normal file
85
frontend/src/hooks/useTeamMembersAdmin.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient, getApiErrorMessage } from '../lib/apiClient';
|
||||
import type { TeamMember } from '../types';
|
||||
|
||||
export interface TeamMemberForm {
|
||||
name: string;
|
||||
rank: string;
|
||||
role: string;
|
||||
cell: string;
|
||||
contact: string;
|
||||
photoUrl: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export const EMPTY_MEMBER_FORM: TeamMemberForm = {
|
||||
name: '',
|
||||
rank: '',
|
||||
role: '',
|
||||
cell: 'HR',
|
||||
contact: '',
|
||||
photoUrl: '',
|
||||
sortOrder: 0,
|
||||
};
|
||||
|
||||
export function memberToForm(member: TeamMember): TeamMemberForm {
|
||||
return {
|
||||
name: member.name,
|
||||
rank: member.rank ?? '',
|
||||
role: member.role ?? '',
|
||||
cell: member.cell ?? '리더',
|
||||
contact: member.contact ?? '',
|
||||
photoUrl: member.photoUrl ?? '',
|
||||
sortOrder: member.sortOrder ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function formToPayload(form: TeamMemberForm) {
|
||||
return {
|
||||
name: form.name.trim(),
|
||||
rank: form.rank.trim() || null,
|
||||
role: form.role.trim() || null,
|
||||
cell: form.cell.trim() || null,
|
||||
contact: form.contact.trim() || null,
|
||||
photoUrl: form.photoUrl.trim() || null,
|
||||
sortOrder: Number(form.sortOrder) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function useTeamMembersAdmin() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['team-members', 'admin'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<TeamMember[]>('/team-members', {
|
||||
params: { all: '1' },
|
||||
});
|
||||
return data.filter((m) => m.isActive !== false);
|
||||
},
|
||||
});
|
||||
|
||||
const invalidate = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['team-members'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['team-members', 'admin'] });
|
||||
};
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: (form: TeamMemberForm) =>
|
||||
apiClient.post('/team-members', formToPayload(form)),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
|
||||
const update = useMutation({
|
||||
mutationFn: ({ id, form }: { id: string; form: TeamMemberForm }) =>
|
||||
apiClient.patch(`/team-members/${id}`, formToPayload(form)),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
|
||||
const remove = useMutation({
|
||||
mutationFn: (id: string) => apiClient.delete(`/team-members/${id}`),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
|
||||
return { query, create, update, remove, getApiErrorMessage };
|
||||
}
|
||||
Reference in New Issue
Block a user