Skip to content

Commit 4b6b86c

Browse files
authored
feat: upload several files (#22)
* chore: revenue upload updated * feat: upload several files * chore: some updates
1 parent 8bbc39a commit 4b6b86c

4 files changed

Lines changed: 157 additions & 119 deletions

File tree

apps/web-app/app/components/form/UploadKitchenRevenue.vue

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@
2020
</UFormField>
2121

2222
<UFormField
23-
label="Файл"
24-
name="file"
23+
label="Файлы"
24+
name="files"
2525
description="Не более 20 МБ"
2626
required
2727
>
2828
<UFileUpload
29-
v-model="state.file"
29+
v-model="state.files"
3030
color="neutral"
3131
highlight
32-
label="Перетащите свой файл сюда"
32+
multiple
33+
label="Перетащите свои файлы сюда"
3334
description="XLSX"
3435
class="min-h-28"
3536
/>
@@ -73,7 +74,7 @@ const actionToast = useActionToast()
7374
const postStore = usePostStore()
7475
7576
const state = ref<Partial<UploadFile>>({
76-
file: undefined,
77+
files: [],
7778
})
7879
7980
const availableTypes = [
@@ -87,7 +88,9 @@ async function onSubmit(event: FormSubmitEvent<UploadFile>) {
8788
8889
try {
8990
const formData = new FormData()
90-
formData.append('file', event.data.file)
91+
for (const file of event.data.files) {
92+
formData.append('files', file)
93+
}
9194
9295
const data = await $fetch(`/api/kitchen/revenue/${selectedType.value?.value}`, {
9396
method: 'POST',
@@ -97,11 +100,9 @@ async function onSubmit(event: FormSubmitEvent<UploadFile>) {
97100
await postStore.update()
98101
99102
const errorMessage = data.result.errors.length > 0 ? `Ошибки: ${data.result.errors.join(', ')}` : ''
103+
const description = `Было обновлено ${data.result.rowsUpdated} ${pluralizationRu(data.result.rowsUpdated, ['запись', 'записи', 'записей'])}. ${errorMessage}`
100104
101-
actionToast.success(
102-
toastId,
103-
t('toast.file-loaded'),
104-
`Было добавлено ${data.result.rowsUpdated} ${pluralizationRu(data.result.rowsUpdated, ['запись', 'записи', 'записей'])}. ${errorMessage}`)
105+
actionToast.success(toastId, t('toast.files-handled'), description)
105106
emit('success')
106107
} catch (error) {
107108
console.error(error)

apps/web-app/i18n/locales/ru-RU.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@
338338
"variant-deleted": "Вариация продукта удалена",
339339
"photo-loaded": "Фото загружено",
340340
"file-loaded": "Файл загружен",
341+
"files-handled": "Файлы обработаны",
341342
"category-updated": "Категория обновлена",
342343
"category-deleted": "Категория удалена",
343344
"task-created": "Задача создана",
Lines changed: 142 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,175 @@
1+
import type { Buffer } from 'node:buffer'
2+
import { ACCEPTED_FILE_TYPES, MAX_FILE_SIZE } from '#shared/services/file'
13
import { repository } from '@roll-stack/database'
24
import xlsx from 'node-xlsx'
35

6+
interface MultiPartData {
7+
data: Buffer
8+
name?: string
9+
filename?: string
10+
type?: string
11+
}
12+
413
export default defineEventHandler(async (event) => {
514
try {
6-
const logger = useLogger('kitchen-revenue-iiko-daily')
7-
815
const files = await readMultipartFormData(event)
9-
const file = files?.[0]
10-
if (!files?.length || !file) {
16+
if (!files?.length) {
1117
throw createError({
1218
statusCode: 400,
13-
message: 'Missing file',
19+
message: 'Missing files',
1420
})
1521
}
1622

17-
const maxFileSize = 20 * 1024 * 1024 // 20MB
18-
if (file.data.length > maxFileSize) {
19-
throw createError({
20-
statusCode: 413,
21-
message: 'File too large',
22-
})
23-
}
23+
let rowsUpdated = 0
24+
const errors: string[] = []
2425

25-
const allowedMimeTypes = [
26-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
27-
'application/vnd.ms-excel',
28-
]
29-
if (file.type && !allowedMimeTypes.includes(file.type)) {
30-
throw createError({
31-
statusCode: 400,
32-
message: 'Invalid file type',
33-
})
26+
for (const file of files) {
27+
const res = await parseFileAndUpdateData(file)
28+
rowsUpdated += res.rowsUpdated
29+
errors.push(...res.errors)
3430
}
3531

36-
const workSheetsFromFile = xlsx.parse(file.data.buffer)
37-
if (!workSheetsFromFile[0]) {
38-
throw createError({
39-
statusCode: 404,
40-
message: 'File not found',
41-
})
32+
return {
33+
ok: true,
34+
result: {
35+
rowsUpdated,
36+
errors,
37+
},
4238
}
39+
} catch (error) {
40+
throw errorResolver(error)
41+
}
42+
})
4343

44-
const data = workSheetsFromFile[0].data
45-
const dateRow = data[2] // 3rd row
46-
if (!dateRow || typeof dateRow[0] !== 'string' || !dateRow[0].startsWith('Дата')) {
47-
throw createError({
48-
statusCode: 400,
49-
message: 'Invalid date',
50-
})
51-
}
44+
async function parseFileAndUpdateData(file: MultiPartData) {
45+
const logger = useLogger('kitchen-revenue-iiko-daily')
5246

53-
const dateMatch = dateRow[0].match(/Дата:\s*(\d{1,2})\.(\d{1,2})\.(\d{4})/)
54-
if (!dateMatch) {
55-
throw createError({
56-
statusCode: 400,
57-
message: 'Invalid date format. Expected "Дата: DD.MM.YYYY"',
58-
})
59-
}
47+
if (file.data.length > MAX_FILE_SIZE) {
48+
throw createError({
49+
statusCode: 413,
50+
message: 'File too large',
51+
})
52+
}
6053

61-
const [, day, month, year] = dateMatch
62-
const dateOnly = `${year}-${month?.padStart(2, '0')}-${day?.padStart(2, '0')}`
63-
const date = new Date(`${dateOnly}T12:00:00.000Z`)
54+
if (file.type && !ACCEPTED_FILE_TYPES.includes(file.type)) {
55+
throw createError({
56+
statusCode: 400,
57+
message: 'Invalid file type',
58+
})
59+
}
6460

65-
if (Number.isNaN(date.getTime())) {
66-
throw createError({
67-
statusCode: 400,
68-
message: 'Invalid date values',
69-
})
70-
}
61+
const workSheetsFromFile = xlsx.parse(file.data.buffer)
62+
if (!workSheetsFromFile[0]) {
63+
throw createError({
64+
statusCode: 404,
65+
message: 'File not found',
66+
})
67+
}
7168

72-
// Remove first 4 rows and last row
73-
const dataRows = data.slice(4, data.length - 1)
74-
if (!dataRows) {
75-
throw createError({
76-
statusCode: 400,
77-
message: 'Invalid data',
78-
})
79-
}
69+
const data = workSheetsFromFile[0].data
70+
const dateRow = data[2] // 3rd row
71+
if (!dateRow || typeof dateRow[0] !== 'string' || !dateRow[0].startsWith('Дата')) {
72+
throw createError({
73+
statusCode: 400,
74+
message: 'Invalid date',
75+
})
76+
}
8077

81-
const parsedKitchens: { name: string, total: number }[] = []
78+
const dictionary = data[3]
79+
if (!dictionary) {
80+
throw createError({
81+
statusCode: 400,
82+
message: 'Invalid dictionary',
83+
})
84+
}
8285

83-
for (const row of dataRows) {
84-
const name = row[2] // 3rd column
85-
const total = row[4] // 5th column
86+
const indexOfName = dictionary.indexOf('Группа')
87+
const indexOfTotal = dictionary.indexOf('Сумма со скидкой, р. Всего')
88+
if (!dictionary || indexOfName < 0 || indexOfTotal < 0) {
89+
throw createError({
90+
statusCode: 400,
91+
message: 'Invalid dictionary',
92+
})
93+
}
8694

87-
if (typeof name !== 'string' || typeof total !== 'number') {
88-
continue
89-
}
95+
const dateMatch = dateRow[0].match(/Дата:\s*(\d{1,2})\.(\d{1,2})\.(\d{4})/)
96+
if (!dateMatch) {
97+
throw createError({
98+
statusCode: 400,
99+
message: 'Invalid date format. Expected "Дата: DD.MM.YYYY"',
100+
})
101+
}
90102

91-
parsedKitchens.push({
92-
name,
93-
total,
94-
})
103+
const [, day, month, year] = dateMatch
104+
const dateOnly = `${year}-${month?.padStart(2, '0')}-${day?.padStart(2, '0')}`
105+
const date = new Date(`${dateOnly}T12:00:00.000Z`)
106+
107+
if (Number.isNaN(date.getTime())) {
108+
throw createError({
109+
statusCode: 400,
110+
message: 'Invalid date values',
111+
})
112+
}
113+
114+
// Remove first 4 rows and last row
115+
const dataRows = data.slice(4, data.length - 1)
116+
if (!dataRows) {
117+
throw createError({
118+
statusCode: 400,
119+
message: 'Invalid data',
120+
})
121+
}
122+
123+
const parsedKitchens: { name: string, total: number }[] = []
124+
125+
for (const row of dataRows) {
126+
const name = row[indexOfName]
127+
const total = row[indexOfTotal]
128+
129+
if (typeof name !== 'string' || typeof total !== 'number') {
130+
continue
95131
}
96132

97-
// Every kitchen: find in DB and add amount for this day
98-
const kitchens = await repository.kitchen.list()
99-
let rowsUpdated = 0
100-
const errors: string[] = []
133+
parsedKitchens.push({
134+
name,
135+
total,
136+
})
137+
}
101138

102-
for (const kitchen of parsedKitchens) {
103-
const found = kitchens.find((k) => k.iikoAlias === kitchen.name)
104-
if (found) {
105-
// Create or update
106-
const revenue = await repository.kitchen.findRevenueByKitchenAndDate(found.id, date)
107-
if (!revenue) {
108-
await repository.kitchen.createRevenue({
109-
kitchenId: found.id,
110-
date: dateOnly,
111-
total: kitchen.total,
112-
})
113-
} else {
114-
await repository.kitchen.updateRevenue(revenue.id, {
115-
total: kitchen.total,
116-
})
117-
}
118-
119-
rowsUpdated++
120-
continue
139+
// Every kitchen: find in DB and add amount for this day
140+
const kitchens = await repository.kitchen.list()
141+
let rowsUpdated = 0
142+
const errors: string[] = []
143+
144+
for (const kitchen of parsedKitchens) {
145+
const found = kitchens.find((k) => k.iikoAlias === kitchen.name)
146+
if (found) {
147+
// Create or update
148+
const revenue = await repository.kitchen.findRevenueByKitchenAndDate(found.id, date)
149+
if (!revenue) {
150+
await repository.kitchen.createRevenue({
151+
kitchenId: found.id,
152+
date: dateOnly,
153+
total: kitchen.total,
154+
})
155+
} else {
156+
await repository.kitchen.updateRevenue(revenue.id, {
157+
total: kitchen.total,
158+
})
121159
}
122160

123-
logger.warn(`Kitchen "${kitchen.name}" from file not found`)
124-
errors.push(`"${kitchen.name}" не найдена.`)
161+
rowsUpdated++
162+
continue
125163
}
126164

127-
logger.log(rowsUpdated, date, parsedKitchens)
165+
logger.warn(`Kitchen "${kitchen.name}" from file not found`)
166+
errors.push(`"${kitchen.name}" не найдена.`)
167+
}
128168

129-
return {
130-
ok: true,
131-
result: {
132-
rowsUpdated,
133-
errors,
134-
},
135-
}
136-
} catch (error) {
137-
throw errorResolver(error)
169+
logger.log(rowsUpdated, date, parsedKitchens)
170+
171+
return {
172+
rowsUpdated,
173+
errors,
138174
}
139-
})
175+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type } from 'arktype'
22

3-
const MAX_FILE_SIZE = 20000000
4-
const ACCEPTED_FILE_TYPES = [
3+
export const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
4+
export const ACCEPTED_FILE_TYPES = [
55
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
66
'application/vnd.ms-excel',
77
]
@@ -11,6 +11,6 @@ const FileSchema = type('File')
1111
.describe('error.file-size-or-type')
1212

1313
export const uploadFileSchema = type({
14-
file: FileSchema,
14+
files: FileSchema.array(),
1515
})
1616
export type UploadFile = typeof uploadFileSchema.infer

0 commit comments

Comments
 (0)