группы

This commit is contained in:
Artem
2025-08-03 15:19:07 +03:00
parent 95e89b27bf
commit 4d69217967
17 changed files with 3549 additions and 84 deletions

View File

@@ -0,0 +1,142 @@
import { Group } from '@/type/template'
// Интерфейсы для API групп
export interface GetGroupsOutput {
groups: Group[]
}
export interface GetGroupOutput {
group: Group | null
}
export interface CreateGroupInput {
name: string
order?: number
}
export interface CreateGroupOutput {
id: string
}
export interface PatchGroupInput {
name?: string | null
order?: number | null
}
export interface PatchGroupOutput extends Group {}
// API получение всех групп
export async function getGroupsApi(): Promise<GetGroupsOutput> {
try {
const response = await fetch('/api/groups/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result: GetGroupsOutput = await response.json()
return result
} catch (error) {
console.error('Ошибка при получении групп:', error)
throw new Error('Ошибка при получении групп с сервера')
}
}
// API получение конкретной группы
export async function getGroupApi(groupId: string): Promise<GetGroupOutput> {
try {
const response = await fetch(`/api/groups/${groupId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result: GetGroupOutput = await response.json()
return result
} catch (error) {
console.error('Ошибка при получении группы:', error)
throw new Error('Ошибка при получении группы с сервера')
}
}
// API создание группы
export async function createGroupApi(
group: CreateGroupInput
): Promise<CreateGroupOutput> {
try {
const response = await fetch('/api/groups/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(group),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result: CreateGroupOutput = await response.json()
return result
} catch (error) {
console.error('Ошибка при создании группы:', error)
throw new Error('Ошибка при создании группы на сервере')
}
}
// API обновление группы
export async function patchGroupApi(
groupId: string,
group: PatchGroupInput
): Promise<PatchGroupOutput> {
try {
const response = await fetch(`/api/groups/${groupId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(group),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result: PatchGroupOutput = await response.json()
return result
} catch (error) {
console.error('Ошибка при обновлении группы:', error)
throw new Error('Ошибка при обновлении группы на сервере')
}
}
// API удаление группы (если потребуется)
export async function deleteGroupApi(groupId: string): Promise<{success: boolean}> {
try {
const response = await fetch(`/api/groups/${groupId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return { success: true }
} catch (error) {
console.error('Ошибка при удалении группы:', error)
throw new Error('Ошибка при удалении группы на сервере')
}
}

View File

@@ -0,0 +1,137 @@
import {
createGroupApi,
CreateGroupInput,
deleteGroupApi,
getGroupsApi,
patchGroupApi,
PatchGroupInput
} from '@/entity/group/api/groupApiService'
import { Group } from '@/type/template'
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { createContext, ReactNode, useContext, useMemo } from 'react'
export const queryClient = new QueryClient()
interface GroupContextType {
groups: Group[]
isLoading: boolean
error: Error | null
refetchGroups: () => void
addGroup: (data: CreateGroupInput) => Promise<Group>
updateGroup: (
groupId: string,
data: PatchGroupInput
) => Promise<Group>
deleteGroup: (groupId: string) => Promise<string>
getGroupById: (groupId: string) => Group | undefined
}
const GroupContext = createContext<GroupContextType | undefined>(undefined)
export const GroupProvider: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const client = useQueryClient()
const {
data: groups = [],
status: listStatus,
error: errorList,
refetch,
} = useQuery<Group[], Error>({
queryKey: ['groups'],
queryFn: async () => {
const { groups: apiList } = await getGroupsApi()
return apiList.sort((a, b) => a.order - b.order)
},
})
const createMutation = useMutation({
mutationFn: async (group: CreateGroupInput) => {
const { id } = await createGroupApi(group)
// Возвращаем созданную группу, получив её с сервера
const { groups: updatedGroups } = await getGroupsApi()
const newGroup = updatedGroups.find(g => g.id === id)
if (!newGroup) throw new Error('Группа не найдена после создания')
return newGroup
},
onSuccess: () => {
client.invalidateQueries({ queryKey: ['groups'] })
},
})
const updateMutation = useMutation({
mutationFn: async ({
groupId,
data,
}: {
groupId: string
data: PatchGroupInput
}) => {
return await patchGroupApi(groupId, data)
},
onSuccess: () => {
client.invalidateQueries({ queryKey: ['groups'] })
},
})
const deleteMutation = useMutation({
mutationFn: async (groupId: string) => {
await deleteGroupApi(groupId)
return groupId
},
onSuccess: () => {
client.invalidateQueries({ queryKey: ['groups'] })
},
})
const getGroupById = (groupId: string) =>
groups.find(group => group.id === groupId)
const value = useMemo(
() => ({
groups,
isLoading:
listStatus === 'pending' ||
createMutation.status === 'pending' ||
updateMutation.status === 'pending' ||
deleteMutation.status === 'pending',
error:
(errorList as Error) ||
(createMutation.error as Error) ||
(updateMutation.error as Error) ||
(deleteMutation.error as Error) ||
null,
refetchGroups: refetch,
addGroup: createMutation.mutateAsync,
updateGroup: async (groupId: string, data: PatchGroupInput) =>
updateMutation.mutateAsync({ groupId, data }),
deleteGroup: deleteMutation.mutateAsync,
getGroupById,
}),
[
groups,
listStatus,
errorList,
createMutation,
updateMutation,
deleteMutation,
refetch,
]
)
return <GroupContext.Provider value={value}>{children}</GroupContext.Provider>
}
export const useGroupContext = (): GroupContextType => {
const context = useContext(GroupContext)
if (!context) {
throw new Error('useGroupContext must be used within a GroupProvider')
}
return context
}

View File

@@ -6,6 +6,8 @@ export interface CreateTemplateInput {
attribute_id: string
value: string | null
}>
group_id?: string | null
order?: number | null
}
export interface CreateTemplateOutput {
@@ -32,6 +34,8 @@ export interface PatchTemplateInput {
attribute_id: string
value: string | null
}> | null
group_id?: string | null | "ungroup"
order?: number | null
}
export interface PatchTemplateOutput {
@@ -43,6 +47,8 @@ export interface ApiTemplate {
name: string
description?: string | null
elements: Record<string, any>
group_id?: string | null
order?: number | null
created_at: string
updated_at: string
deleted_at?: string | null
@@ -62,9 +68,9 @@ export interface ApiTemplateWithAttributes extends ApiTemplate {
// Импортируем типы для преобразования
import { getDefaultLayoutSettings, migrateTemplateElement } from '@/lib/utils'
import {
Template,
TemplateAttributeDetail,
TemplateElement,
Template,
TemplateAttributeDetail,
TemplateElement,
} from '@/type/template'
// Утилитарные функции для преобразования данных
@@ -74,7 +80,7 @@ export function apiTemplateToTemplate(apiTemplate: ApiTemplate): Template {
// Преобразуем elements из Record<string, any> в TemplateElement[]
const elements: TemplateElement[] = Object.values(apiTemplate.elements || {})
.map(el => migrateTemplateElement(el))
.sort((a, b) => (a.order || 0) - (b.order || 0))
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
return {
id: apiTemplate.id,
@@ -82,6 +88,8 @@ export function apiTemplateToTemplate(apiTemplate: ApiTemplate): Template {
description: apiTemplate.description || undefined,
elements,
layoutSettings: getDefaultLayoutSettings(),
group_id: apiTemplate.group_id,
order: apiTemplate.order, // ВАЖНО: добавляем order!
createdAt: new Date(apiTemplate.created_at),
updatedAt: new Date(apiTemplate.updated_at),
}
@@ -120,6 +128,8 @@ export function templateToCreateInput(
name: template.name,
description: template.description,
elements,
group_id: template.group_id,
order: template.order,
}
}
@@ -134,6 +144,8 @@ export function templateToPatchInput(template: Template): PatchTemplateInput {
name: template.name,
description: template.description,
elements,
group_id: template.group_id,
order: template.order,
// attributes будут добавлены отдельно в TemplateContext при необходимости
}
}
@@ -166,6 +178,8 @@ export async function createTemplateApi(
// API получение всех шаблонов
export async function getTemplatesApi(): Promise<GetTemplatesOutput> {
try {
console.log('🌐 GET API call to /api/templates/')
const response = await fetch('/api/templates/', {
method: 'GET',
headers: {
@@ -173,11 +187,23 @@ export async function getTemplatesApi(): Promise<GetTemplatesOutput> {
},
})
console.log(`🌐 GET API response status: ${response.status}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result: GetTemplatesOutput = await response.json()
console.log('🌐 GET API raw response:', result)
// Дополнительная отладка order полей
console.log('🌐 Templates with order info:', result.templates.map(t => ({
id: t.id,
name: t.name,
order: t.order,
orderType: typeof t.order
})))
return result
} catch (error) {
console.error('Ошибка при получении шаблонов:', error)
@@ -239,6 +265,8 @@ export async function patchTemplateApi(
template: PatchTemplateInput
): Promise<PatchTemplateOutput> {
try {
console.log(`🌐 PATCH API call to /api/templates/${templateId}`, template)
const response = await fetch(`/api/templates/${templateId}`, {
method: 'PATCH',
headers: {
@@ -247,11 +275,16 @@ export async function patchTemplateApi(
body: JSON.stringify(template),
})
console.log(`🌐 PATCH API response status: ${response.status}`)
if (!response.ok) {
const errorText = await response.text()
console.error(`🌐 PATCH API error response:`, errorText)
throw new Error(`HTTP error! status: ${response.status}`)
}
const result: PatchTemplateOutput = await response.json()
console.log(`🌐 PATCH API success result:`, result)
return result
} catch (error) {
console.error('Ошибка при обновлении шаблона:', error)
@@ -276,6 +309,8 @@ export interface CopyTemplateInput {
attribute_id: string
value: string | null
}> | null
group_id?: string | null
copy_first_sheet_empty?: boolean
}
export interface CopyTemplateOutput {

View File

@@ -1,22 +1,22 @@
import {
apiTemplateToTemplate,
apiTemplateWithAttributesToTemplate,
copyTemplateApi,
CopyTemplateInput,
createTemplateApi,
deleteTemplateApi,
getTemplateApi,
getTemplatesApi,
patchTemplateApi,
templateToCreateInput,
templateToPatchInput,
apiTemplateToTemplate,
apiTemplateWithAttributesToTemplate,
copyTemplateApi,
CopyTemplateInput,
createTemplateApi,
deleteTemplateApi,
getTemplateApi,
getTemplatesApi,
patchTemplateApi,
templateToCreateInput,
templateToPatchInput,
} from '@/entity/template/api/templateApiService'
import { Template } from '@/type/template'
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import React, { createContext, ReactNode, useContext, useMemo } from 'react'
@@ -40,8 +40,12 @@ interface TemplateContextType {
templateId: string,
newName: string,
newDescription?: string,
attributes?: Array<{ attributeId: string; value: string | null }>
attributes?: Array<{ attributeId: string; value: string | null }>,
groupId?: string | null,
copyFirstSheetEmpty?: boolean
) => Promise<Template>
updateTemplateOrder: (templateId: string, newOrder: number) => Promise<void>
moveTemplateToGroup: (templateId: string, groupId: string | null) => Promise<void>
}
const TemplateContext = createContext<TemplateContextType | undefined>(
@@ -61,8 +65,60 @@ export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
} = useQuery<Template[], Error>({
queryKey: ['templates'],
queryFn: async () => {
console.log('🔄 RELOADING TEMPLATES FROM API')
const { templates: apiList } = await getTemplatesApi()
return apiList.map(apiTemplateWithAttributesToTemplate)
console.log('📡 Raw API templates:', apiList.map(t => ({ id: t.id, name: t.name, order: t.order, group_id: t.group_id })))
const processedTemplates = apiList.map(apiTemplateWithAttributesToTemplate)
console.log('🔧 Processed templates:', processedTemplates.map(t => ({ id: t.id, name: t.name, order: t.order, group_id: t.group_id })))
// Проверяем если есть шаблоны без order (null, undefined или 0) и автоматически их исправляем
const templatesWithoutOrder = processedTemplates.filter(t => t.order == null || t.order === 0)
if (templatesWithoutOrder.length > 0) {
console.log('Found templates without valid order, fixing:', templatesWithoutOrder.map(t => ({ id: t.id, name: t.name, order: t.order })))
// Группируем по группам и задаем order
const templatesByGroup: Record<string, Template[]> = {}
processedTemplates.forEach(template => {
const groupKey = template.group_id || 'ungrouped'
if (!templatesByGroup[groupKey]) templatesByGroup[groupKey] = []
templatesByGroup[groupKey].push(template)
})
// Для каждой группы обновляем шаблоны без order
for (const [groupKey, groupTemplates] of Object.entries(templatesByGroup)) {
const templatesWithOrder = groupTemplates.filter(t => t.order != null && t.order > 0)
const templatesWithoutOrderInGroup = groupTemplates.filter(t => t.order == null || t.order === 0)
if (templatesWithoutOrderInGroup.length > 0) {
const maxOrder = Math.max(0, ...templatesWithOrder.map(t => t.order ?? 0))
// Обновляем каждый шаблон без order
for (let i = 0; i < templatesWithoutOrderInGroup.length; i++) {
const template = templatesWithoutOrderInGroup[i]
const oldOrder = template.order
const newOrder = maxOrder + (i + 1)
try {
// Обновляем в базе данных
await patchTemplateApi(template.id, {
name: template.name,
order: newOrder
})
// Обновляем локально
template.order = newOrder
console.log(`Updated template ${template.id} order from ${oldOrder} to ${newOrder}`)
} catch (error) {
console.error(`Failed to update order for template ${template.id}:`, error)
}
}
}
}
}
console.log('✅ FINAL TEMPLATES RESULT:', processedTemplates.map(t => ({ id: t.id, name: t.name, order: t.order, group_id: t.group_id })))
return processedTemplates
},
})
@@ -140,11 +196,15 @@ export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
newName,
newDescription,
attributes,
groupId,
copyFirstSheetEmpty,
}: {
templateId: string
newName: string
newDescription?: string
attributes?: Array<{ attributeId: string; value: string | null }>
groupId?: string | null
copyFirstSheetEmpty?: boolean
}) => {
const input: CopyTemplateInput = {
template_id: templateId,
@@ -155,6 +215,8 @@ export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
attribute_id: attr.attributeId,
value: attr.value,
})) || null,
group_id: groupId,
copy_first_sheet_empty: copyFirstSheetEmpty,
}
const { new_template_id } = await copyTemplateApi(input)
const { template: apiTpl } = await getTemplateApi(new_template_id)
@@ -166,6 +228,70 @@ export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
},
})
// Мутация для обновления порядка шаблона
const updateOrderMutation = useMutation({
mutationFn: async ({ templateId, newOrder }: { templateId: string; newOrder: number }) => {
const template = templates.find(t => t.id === templateId)
if (!template) throw new Error('Template not found')
console.log('🔄 Updating template order:', { templateId, newOrder, template: { id: template.id, name: template.name, currentOrder: template.order } })
// Обновляем только порядок, но включаем обязательные поля
const patchInput: PatchTemplateInput = {
name: template.name, // Включаем name как обязательное поле
order: newOrder
}
console.log('📤 Sending to API:', patchInput)
try {
const result = await patchTemplateApi(templateId, patchInput)
console.log('Order update successful:', result)
return result
} catch (error) {
console.error('Order update failed:', error)
throw error
}
},
onSuccess: () => {
client.invalidateQueries({ queryKey: ['templates'] })
},
onError: (error) => {
console.error('Update order mutation error:', error)
},
})
// Мутация для перемещения шаблона в другую группу
const moveToGroupMutation = useMutation({
mutationFn: async ({ templateId, groupId }: { templateId: string; groupId: string | null }) => {
const template = templates.find(t => t.id === templateId)
if (!template) throw new Error('Template not found')
console.log('Moving template to group:', { templateId, groupId, template })
// Если groupId равен null, отправляем "ungroup" для удаления из группы
const patchInput: PatchTemplateInput = {
name: template.name, // Включаем name как обязательное поле
group_id: groupId === null ? "ungroup" : groupId
}
try {
const result = await patchTemplateApi(templateId, patchInput)
console.log('Group move successful:', result)
return result
} catch (error) {
console.error('Group move failed:', error)
throw error
}
},
onSuccess: () => {
client.invalidateQueries({ queryKey: ['templates'] })
},
onError: (error) => {
console.error('Move to group mutation error:', error)
},
})
const value = useMemo(
() => ({
templates,
@@ -174,13 +300,17 @@ export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
createMutation.status === 'pending' ||
updateMutation.status === 'pending' ||
deleteMutation.status === 'pending' ||
copyMutation.status === 'pending',
copyMutation.status === 'pending' ||
updateOrderMutation.status === 'pending' ||
moveToGroupMutation.status === 'pending',
error:
(errorList as Error) ||
(createMutation.error as Error) ||
(updateMutation.error as Error) ||
(deleteMutation.error as Error) ||
(copyMutation.error as Error) ||
(updateOrderMutation.error as Error) ||
(moveToGroupMutation.error as Error) ||
null,
refetchTemplates: refetch,
addTemplate: async (
@@ -204,14 +334,22 @@ export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
templateId: string,
newName: string,
newDescription?: string,
attributes?: Array<{ attributeId: string; value: string | null }>
attributes?: Array<{ attributeId: string; value: string | null }>,
groupId?: string | null,
copyFirstSheetEmpty?: boolean
) =>
copyMutation.mutateAsync({
templateId,
newName,
newDescription,
attributes,
groupId,
copyFirstSheetEmpty,
}),
updateTemplateOrder: async (templateId: string, newOrder: number) =>
updateOrderMutation.mutateAsync({ templateId, newOrder }),
moveTemplateToGroup: async (templateId: string, groupId: string | null) =>
moveToGroupMutation.mutateAsync({ templateId, groupId }),
}),
[
templates,
@@ -225,11 +363,17 @@ export const TemplateProvider: React.FC<{ children: ReactNode }> = ({
deleteMutation.error,
copyMutation.status,
copyMutation.error,
updateOrderMutation.status,
updateOrderMutation.error,
moveToGroupMutation.status,
moveToGroupMutation.error,
refetch,
createMutation.mutateAsync,
updateMutation.mutateAsync,
deleteMutation.mutateAsync,
copyMutation.mutateAsync,
updateOrderMutation.mutateAsync,
moveToGroupMutation.mutateAsync,
]
)