Browse Source

Добавлена поддержка файлов

Vadim 3 months ago
parent
commit
4ad5ca425c

+ 2 - 1
src/api/current-api.ts

@@ -1 +1,2 @@
-export { api } from './v_0.1.0/api.js';
+export { api } from "./v_0.1.0/api.js";
+export { apiTypes } from "./v_0.1.0/api.js";

+ 80 - 30
src/api/v_0.1.0/client/client-pe-api.ts

@@ -1,10 +1,14 @@
-import { CustomFieldWithUserCopyValue, CustomFieldWithValue } from '../types/pe-types.js';
-import { z } from 'zod';
+import {
+  CustomFieldWithUserCopyValue,
+  CustomFieldWithValue,
+  InputFieldValue,
+} from "../types/custom-fields-types.js";
+import { z } from "zod";
 
 class ClientPartEntitiesApi {
   GET_EventPeTypes = {
     res: z.object({
-      code: z.enum(['success']),
+      code: z.enum(["success"]),
       peTypes: z.array(
         z.object({
           peTypeId: z.string().uuid(),
@@ -20,19 +24,23 @@ class ClientPartEntitiesApi {
       peTypeCode: z.string(),
     }),
     res: z.object({
-      code: z.enum(['success']),
+      code: z.enum(["success"]),
       peType: z.object({
         peTypeId: z.string().uuid(),
         code: z.string(),
         name: z.string(),
-        fields: z.array(CustomFieldWithUserCopyValue.extend({ isCopyUserValue: z.boolean() })),
+        fields: z.array(
+          CustomFieldWithUserCopyValue.extend({
+            peFfId: z.string(),
+          }),
+        ),
       }),
     }),
   };
 
   GET_MyPes = {
     res: z.object({
-      code: z.enum(['success']),
+      code: z.enum(["success"]),
       owner: z.array(
         z.object({
           peId: z.string().uuid(),
@@ -63,10 +71,10 @@ class ClientPartEntitiesApi {
       peId: z.string().uuid(),
     }),
     res: z.object({
-      code: z.enum(['success']),
-      pe: z.discriminatedUnion('userRole', [
+      code: z.enum(["success"]),
+      pe: z.discriminatedUnion("userRole", [
         z.object({
-          userRole: z.literal('owner'),
+          userRole: z.literal("owner"),
           peId: z.string().uuid(),
           peTypeId: z.string().uuid(),
           peTypeCode: z.string(),
@@ -74,13 +82,17 @@ class ClientPartEntitiesApi {
           eventInstId: z.string().uuid(),
           name: z.string(),
           ownerId: z.string().uuid(),
-          fields: z.array(CustomFieldWithValue.extend({ peFfId: z.string().uuid() })),
+          fields: z.array(
+            CustomFieldWithValue.extend({ peFfId: z.string().uuid() }),
+          ),
           members: z.array(
             z.object({
               peMemberId: z.string(),
               userId: z.string().uuid(),
               email: z.string().email(),
-              fields: z.array(CustomFieldWithValue.extend({ userEfId: z.string().uuid() })),
+              fields: z.array(
+                CustomFieldWithValue.extend({ userEfId: z.string().uuid() }),
+              ),
             }),
           ),
           invites: z.array(
@@ -96,7 +108,7 @@ class ClientPartEntitiesApi {
         }),
 
         z.object({
-          userRole: z.literal('member'),
+          userRole: z.literal("member"),
           peMemberId: z.string().uuid(),
           peId: z.string().uuid(),
           peTypeId: z.string().uuid(),
@@ -111,18 +123,22 @@ class ClientPartEntitiesApi {
   };
 
   POST_PartEntity = {
-    req: z.object({
-      peTypeId: z.string().uuid(),
-      name: z.string(),
-      form: z.array(
-        z.object({
-          peFfId: z.string().uuid(),
-          value: z.string().nullable(),
+    req: {
+      formData: {
+        body: z.object({
+          peTypeCode: z.string(),
+          name: z.string(),
+          fields: z.array(
+            z.object({
+              peFfId: z.string().uuid(),
+              value: InputFieldValue,
+            }),
+          ),
         }),
-      ),
-    }),
+      },
+    },
     res: z.object({
-      code: z.enum(['success']),
+      code: z.enum(["success"]),
       peId: z.string().uuid(),
     }),
   };
@@ -139,7 +155,7 @@ class ClientPartEntitiesApi {
       }),
     },
     res: z.object({
-      code: z.enum(['success']),
+      code: z.enum(["success"]),
       peInviteId: z.string().uuid(),
     }),
   };
@@ -149,7 +165,7 @@ class ClientPartEntitiesApi {
       peId: z.string().uuid(),
     }),
     res: z.object({
-      code: z.enum(['success']),
+      code: z.enum(["success"]),
       invites: z.array(
         z.object({
           peInviteId: z.string().uuid(),
@@ -170,7 +186,7 @@ class ClientPartEntitiesApi {
       }),
     },
     res: z.object({
-      code: z.enum(['success']),
+      code: z.enum(["success"]),
       invite: z.object({
         peInviteId: z.string().uuid(),
         peInviteUuid: z.string().uuid(),
@@ -187,22 +203,56 @@ class ClientPartEntitiesApi {
         peInviteUuid: z.string().uuid(),
       }),
     },
-    res: z.discriminatedUnion('code', [
+    res: z.discriminatedUnion("code", [
       z.object({
-        code: z.literal('success'),
+        code: z.literal("success"),
         peId: z.string().uuid(),
       }),
       z.object({
-        code: z.literal('inviteNotFound'),
+        code: z.literal("inviteNotFound"),
       }),
       z.object({
-        code: z.literal('inviteLimitExceeded'),
+        code: z.literal("inviteLimitExceeded"),
       }),
       z.object({
-        code: z.literal('peMemberAlreadyExists'),
+        code: z.literal("peMemberAlreadyExists"),
       }),
     ]),
   };
+
+  GET_MyPesForActivity = {
+    res: z.object({
+      code: z.enum(["success"]),
+      pes: z.array(
+        z.object({
+          peId: z.string().uuid(),
+          peTypeId: z.string().uuid(),
+          peTypeCode: z.string(),
+          peTypeName: z.string(),
+          eventInstId: z.string().uuid(),
+          ownerId: z.string().uuid(),
+          name: z.string(),
+          members: z.array(
+            z.object({
+              peMemberId: z.string().uuid(),
+              userId: z.string().uuid(),
+              email: z.string().email(),
+              fields: z.array(
+                CustomFieldWithValue.extend({
+                  userEfId: z.string().uuid(),
+                }),
+              ),
+            }),
+          ),
+          fields: z.array(
+            CustomFieldWithValue.extend({
+              peFfId: z.string().uuid(),
+            }),
+          ),
+        }),
+      ),
+    }),
+  };
 }
 
 export const clientPartEntitiesApi = new ClientPartEntitiesApi();

+ 22 - 14
src/api/v_0.1.0/client/client-users-api.ts

@@ -1,28 +1,36 @@
-import { CustomFieldWithValidators } from "../types/pe-types.js";
+import {
+  CustomFieldWithValidatorsAndValue,
+  InputFieldValue,
+} from "../types/custom-fields-types.js";
 import { z } from "zod";
 
 class ClientUsersApi {
   GET_UserEventData = {
     res: z.object({
       code: z.enum(["success"]),
-      userData: z.array(
-        CustomFieldWithValidators.extend({
-          userEfId: z.string().uuid(),
-          value: z.string().nullable(),
-        }),
-      ),
+      userData: z.object({
+        fields: z.array(
+          CustomFieldWithValidatorsAndValue.extend({
+            userEfId: z.string().uuid(),
+          }),
+        ),
+      }),
     }),
   };
 
   PATCH_UserEventData = {
-    req: z.object({
-      userData: z.array(
-        z.object({
-          userEfId: z.string().uuid(),
-          value: z.string().nullable(),
+    req: {
+      formData: {
+        body: z.object({
+          fields: z.array(
+            z.object({
+              userEfId: z.string().uuid(),
+              value: InputFieldValue,
+            }),
+          ),
         }),
-      ),
-    }),
+      },
+    },
     res: z.object({
       code: z.enum(["success"]),
     }),

+ 59 - 0
src/api/v_0.1.0/types/custom-fields-types.ts

@@ -0,0 +1,59 @@
+import { z } from 'zod';
+
+export const FieldTypeCode = z.enum(['string', 'number', 'checkbox', 'audio', 'date']);
+export const Validator = z.object({
+  validatorId: z.string().uuid(),
+  code: z.string(),
+  fieldTypeCode: FieldTypeCode,
+  name: z.string(),
+  value: z.string().nullable(),
+});
+
+export const CustomField = z.object({
+  fieldDefinitionId: z.string().uuid(),
+  code: z.string().nullable(),
+  fieldTypeCode: FieldTypeCode,
+  title: z.string(),
+  mask: z.string().nullable(),
+  options: z.array(z.string()).nullable(),
+});
+
+export const CustomFieldWithValidators = CustomField.extend({
+  validators: z.array(Validator),
+});
+
+export const SavedFieldValue = z.union([
+  z.string(),
+  z.number(),
+  z.boolean(),
+  z.object({
+    originalName: z.string(),
+    fileName: z.string(),
+    type: z.enum(['audio', 'img', 'video', 'pdf', 'other']),
+  }),
+  z.null(),
+]);
+
+export const InputFieldValue = z.union([
+  z.string(),
+  z.number(),
+  z.boolean(),
+  z.object({
+    fileName: z.string(),
+    type: z.enum(['audio', 'img', 'video', 'pdf', 'other']),
+  }),
+  z.null(),
+]);
+
+export const CustomFieldWithValidatorsAndValue = CustomFieldWithValidators.extend({
+  value: SavedFieldValue,
+});
+
+export const CustomFieldWithValue = CustomField.extend({
+  value: SavedFieldValue,
+});
+
+export const CustomFieldWithUserCopyValue = CustomFieldWithValidators.extend({
+  userCopyValue: z.string().nullable(),
+  isCopyUserValue: z.boolean(),
+});

+ 1 - 41
src/api/v_0.1.0/types/pe-types.ts

@@ -1,46 +1,6 @@
 import { z } from "zod";
 
-export const FieldTypeCode = z.enum([
-  "string",
-  "number",
-  "checkbox",
-  "audio",
-  "date",
-]);
-export const Validator = z.object({
-  validatorId: z.string().uuid(),
-  code: z.string(),
-  fieldTypeCode: FieldTypeCode,
-  name: z.string(),
-  value: z.string().nullable(),
-});
-
-export const CustomField = z.object({
-  fieldDefinitionId: z.string().uuid(),
-  code: z.string().nullable(),
-  fieldTypeCode: FieldTypeCode,
-  title: z.string(),
-  mask: z.string().nullable(),
-  options: z.array(z.string()).nullable(),
-});
-
-export const CustomFieldWithValidators = CustomField.extend({
-  validators: z.array(Validator),
-});
-
-export const CustomFieldWithValidatorsAndValue =
-  CustomFieldWithValidators.extend({
-    value: z.string().nullable(),
-  });
-
-export const CustomFieldWithValue = CustomField.extend({
-  value: z.string().nullable(),
-});
-
-export const CustomFieldWithUserCopyValue = CustomFieldWithValidators.extend({
-  peFfId: z.string(),
-  userCopyValue: z.string().nullable(),
-});
+import { CustomFieldWithValidators } from "./custom-fields-types.js";
 
 export const PeTypeWithFields = z.object({
   peTypeId: z.string(),

+ 25 - 16
src/components/custom-form/CustomForm.vue

@@ -1,13 +1,15 @@
 <template>
   <q-form @submit="submit()">
+    <slot name="before"></slot>
     <CustomFormField
       v-for="field in formWithValues"
       :key="field.fieldDefinitionId"
       :field="field"
       v-model="field.value"
+      @changed="fieldIsChanged(field)"
     />
 
-    <q-btn :disable="disableBtnBeforeChange && !isChanged" type="submit">Подтвердить</q-btn>
+    <q-btn :disable="editMode && !changedFields.length" type="submit">Подтвердить</q-btn>
   </q-form>
 </template>
 
@@ -16,9 +18,9 @@ import { ref, watch } from 'vue';
 import CustomFormField from './CustomFormField.vue';
 import type { FormFieldWithValidators, FromFieldWithValidatorsAndValue } from './custom-form-types';
 
-const { fields, disableBtnBeforeChange = false } = defineProps<{
+const { fields, editMode = false } = defineProps<{
   fields: FormFieldWithValidators[] | FromFieldWithValidatorsAndValue[];
-  disableBtnBeforeChange?: boolean;
+  editMode?: boolean;
 }>();
 
 const emit = defineEmits<{
@@ -28,8 +30,6 @@ const emit = defineEmits<{
 const formWithValues = ref<FromFieldWithValidatorsAndValue[]>([]);
 
 const addValues = () => {
-  console.log('fields', fields);
-
   formWithValues.value = fields.map((field) => ({
     ...field,
     value: 'value' in field ? field.value : null,
@@ -46,21 +46,30 @@ watch(
 );
 
 // проверка на изменение
-const isChanged = ref(false);
+const changedFields = ref<FromFieldWithValidatorsAndValue[]>([]);
+
+// TODO: исправить опечатку from
+const fieldIsChanged = (field: FromFieldWithValidatorsAndValue) => {
+  console.log('fieldIsChanged', changedFields.value);
 
-if (disableBtnBeforeChange) {
-  watch(
-    formWithValues,
-    () => {
-      isChanged.value = true;
-    },
-    { deep: true },
-  );
-}
+  if (editMode) {
+    if (changedFields.value.includes(field)) {
+      return;
+    }
+
+    changedFields.value.push(field);
+  }
+};
 
 //
 const submit = () => {
+  if (editMode) {
+    emit('submit', changedFields.value);
+    changedFields.value = [];
+    return;
+  }
+
   emit('submit', formWithValues.value);
-  isChanged.value = false;
+  changedFields.value = [];
 };
 </script>

+ 49 - 7
src/components/custom-form/CustomFormField.vue

@@ -2,7 +2,9 @@
   {{ field.validators }}
   <q-input
     v-model="fieldValue"
-    v-if="field.fieldTypeCode === 'string'"
+    v-if="
+      field.fieldTypeCode === 'string' && (typeof fieldValue === 'string' || fieldValue === null)
+    "
     type="text"
     :label="field.title"
     :mask="field.mask || undefined"
@@ -10,7 +12,9 @@
   ></q-input>
   <q-input
     v-model="fieldValue"
-    v-if="field.fieldTypeCode === 'number'"
+    v-if="
+      field.fieldTypeCode === 'number' && (typeof fieldValue === 'string' || fieldValue === null)
+    "
     type="number"
     :title="field.title"
     :mask="field.mask || undefined"
@@ -18,26 +22,53 @@
   ></q-input>
   <q-checkbox
     v-model="fieldValue"
-    v-if="field.fieldTypeCode === 'checkbox'"
+    v-if="
+      field.fieldTypeCode === 'checkbox' && (typeof fieldValue === 'string' || fieldValue === null)
+    "
     :title="field.title"
     :rules="rules"
   ></q-checkbox>
 
-  <DatePicker v-if="field.fieldTypeCode === 'date'" v-model="fieldValue" :rules="rules" />
+  <DatePicker
+    v-if="field.fieldTypeCode === 'date' && (typeof fieldValue === 'string' || fieldValue === null)"
+    v-model="fieldValue"
+    :rules="rules"
+  />
+
+  <!-- file -->
+  <template v-if="field.fieldTypeCode === 'audio'">
+    {{ field.title }}
+    <q-btn
+      v-if="fieldValue && !filedValuIsFile(fieldValue) && typeof fieldValue === 'object'"
+      label="Изменить файл"
+      @click="fieldValue = null"
+    ></q-btn>
+    <q-file
+      v-if="filedValuIsFile(fieldValue) || fieldValue === null"
+      v-model="fieldValue"
+      :rules="rules"
+      accept=".mp3"
+      clearable
+    />
+  </template>
 </template>
 
 <script setup lang="ts">
 import type { ValidationRule } from 'quasar';
-import type { FormFieldWithValidators, FieldValue } from './custom-form-types';
+import type { FormFieldWithValidators, InputFieldValue } from './custom-form-types';
 import { getValidationFunc } from './validation-functions';
-import { ref } from 'vue';
+import { ref, watch } from 'vue';
 import DatePicker from '../pickers/date-picker/DatePicker.vue';
 
 const { field } = defineProps<{
   field: FormFieldWithValidators;
 }>();
 
-const fieldValue = defineModel<FieldValue>({ required: true });
+const fieldValue = defineModel<InputFieldValue>({ required: true });
+
+const filedValuIsFile = (fieldValue: InputFieldValue): fieldValue is File => {
+  return fieldValue instanceof File;
+};
 
 // rules
 const rules = ref<ValidationRule[]>([]);
@@ -51,4 +82,15 @@ field.validators.forEach((v) => {
 
   rules.value.push(validator);
 });
+
+// проверка на изменение
+const emit = defineEmits<{ changed: [boolean] }>();
+watch(
+  () => fieldValue.value,
+  (newValue, oldValue) => {
+    if (JSON.stringify(newValue) === JSON.stringify(oldValue)) return;
+
+    emit('changed', true);
+  },
+);
 </script>

+ 30 - 5
src/components/custom-form/custom-form-types.ts

@@ -1,4 +1,13 @@
-export type FieldTypeCode = 'string' | 'number' | 'checkbox' | 'date' | 'audio';
+export type FieldTypeCode =
+  | 'string'
+  | 'number'
+  | 'checkbox'
+  | 'date'
+  | 'audio'
+  | 'img'
+  | 'video'
+  | 'pdf'
+  | 'other';
 export type Validator = {
   validatorId: string;
   code: string;
@@ -16,18 +25,34 @@ export type CustomField = {
   options: string[] | null;
 };
 
-export type FieldValue = string | null;
+export type InputFieldValue = string | number | boolean | File | null;
+
+export type SavedFieldValue =
+  | string
+  | number
+  | boolean
+  | {
+      fileName: string;
+      type: 'audio' | 'img' | 'video' | 'pdf' | 'other';
+      originalName: string;
+    }
+  | null;
 
 export type CustomFieldWithValidators = CustomField & {
   validators: Validator[];
 };
 
+export type CustomFieldWithUserCopyValue = CustomFieldWithValidators & {
+  userCopyValue: SavedFieldValue;
+  isCopyUserValue: boolean;
+};
+
 export type CustomFieldWithValue = CustomField & {
-  value: FieldValue;
+  value: SavedFieldValue;
 };
 
 export type CustomFieldWithValidatorsAndValue = CustomFieldWithValidators & {
-  value: FieldValue;
+  value: SavedFieldValue;
 };
 
 // form
@@ -41,5 +66,5 @@ export type FormFieldWithValidators = FormField & {
 };
 
 export type FromFieldWithValidatorsAndValue = FormFieldWithValidators & {
-  value: FieldValue;
+  value: InputFieldValue;
 };

+ 140 - 95
src/components/custom-form/validation-functions.ts

@@ -1,10 +1,9 @@
-import type { ValidationRule } from 'quasar';
 import type { Validator } from './custom-form-types';
-import { z } from 'zod';
-import { isEmail } from 'validator';
+import validatorjs from 'validator';
 import type { Dayjs } from 'src/plugins/dayjs/dayjs';
 import { dayjs } from 'src/plugins/dayjs/dayjs';
 
+// утилиты
 function validateLeastYearsAgo(date: string, years: number): boolean {
   // 1. Проверяем, что minAge является неотрицательным числом
   if (typeof years !== 'number' || years < 0 || !Number.isInteger(years)) {
@@ -38,111 +37,157 @@ function validateLeastYearsAgo(date: string, years: number): boolean {
   return calculatedAge >= years;
 }
 
-export const getValidationFunc = (validator: Validator): ValidationRule | undefined => {
-  switch (validator.code) {
-    case 'required':
-      return (v: string) => (v ? true : `Поле должно быть заполнено`);
+const getTypeError = (expected: 'string' | 'number' | 'date' | 'audio') => {
+  const types = {
+    string: 'строкой',
+    number: 'числом',
+    date: 'датой',
+    audio: 'аудио файлом',
+  };
+  return `Поле должно быть ${types[expected]}`;
+};
+const parseValidatorValue = (value: unknown): number | null => {
+  const num = Number(value);
+  return Number.isFinite(num) ? num : null;
+};
 
-    // string
-    case 'maxString': {
-      const parsedValue = z.number().parse(Number(validator.value));
-      return (v: string) =>
-        v.length <= parsedValue || `Поле должно содержать максимум ${parsedValue} символов`;
+// Декларация для поддержки webkitAudioContext в TypeScript
+declare global {
+  interface Window {
+    webkitAudioContext?: typeof AudioContext;
+  }
+}
+async function getAudioDuration(file: File): Promise<number> {
+  return new Promise((resolve, reject) => {
+    const objectUrl = URL.createObjectURL(file);
+
+    // Создаем AudioContext с проверкой на webkit-префикс
+    const AudioContextConstructor = window.AudioContext || window.webkitAudioContext;
+    if (!AudioContextConstructor) {
+      URL.revokeObjectURL(objectUrl);
+      return reject(new Error('Web Audio API is not supported in this browser'));
     }
 
+    const audioContext = new AudioContextConstructor();
+
+    const fileReader = new FileReader();
+
+    fileReader.onload = async () => {
+      try {
+        const arrayBuffer = fileReader.result as ArrayBuffer;
+        const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+        resolve(audioBuffer.duration);
+      } catch (error) {
+        reject(
+          new Error(
+            `Error decoding audio: ${error instanceof Error ? error.message : String(error)}`,
+          ),
+        );
+      } finally {
+        URL.revokeObjectURL(objectUrl);
+        if (audioContext.state !== 'closed') {
+          await audioContext.close();
+        }
+      }
+    };
+
+    fileReader.onerror = async () => {
+      URL.revokeObjectURL(objectUrl);
+      if (audioContext.state !== 'closed') {
+        await audioContext.close();
+      }
+      reject(new Error('FileReader error'));
+    };
+
+    fileReader.readAsArrayBuffer(file);
+  });
+}
+
+//
+//
+//
+//
+export const getValidationFunc = (
+  validator: Validator,
+): ((v: unknown) => (string | true) | Promise<string | true>) | undefined => {
+  const { code, value } = validator;
+
+  switch (validator.code) {
+    case 'required':
+      return (v: unknown) =>
+        v !== undefined && v !== null && v !== '' ? true : 'Поле должно быть заполнено';
+
+    // Строковые валидаторы
+    case 'maxString':
     case 'minString': {
-      const parsedValue = z.number().parse(Number(validator.value));
-      return (v: string) =>
-        v.length >= parsedValue || `Поле должно содержать минимум ${parsedValue} символов`;
+      const parsedValue = parseValidatorValue(value);
+      if (parsedValue === null) {
+        return () => `Некорректное значение валидатора: ${value}`;
+      }
+
+      return (v: unknown) => {
+        if (v === null) return true;
+        if (typeof v !== 'string') return getTypeError('string');
+        return code === 'maxString'
+          ? v.length <= parsedValue || `Максимум ${parsedValue} символов`
+          : v.length >= parsedValue || `Минимум ${parsedValue} символов`;
+      };
     }
 
-    case 'isMail': {
-      return (v: string) => isEmail(v) || `Введите корректуню почту`;
-    }
+    case 'isMail':
+      return (v: unknown) => {
+        if (v === null) return true;
+        if (typeof v !== 'string') return getTypeError('string');
+        return validatorjs.isEmail(v) || 'Введите корректную почту';
+      };
 
     // date
     case 'leastYearsAgo': {
-      const parsedValue = z.number().parse(Number(validator.value));
-      return (v: string) =>
-        validateLeastYearsAgo(v, parsedValue) || `Должно пройти минимум ${parsedValue} лет`;
+      const parsedValue = parseValidatorValue(value);
+      if (parsedValue === null) {
+        return () => `Некорректное значение валидатора: ${value}`;
+      }
+
+      return (v: unknown) => {
+        if (v === null) return true;
+        if (typeof v !== 'string') return getTypeError('string');
+        return validateLeastYearsAgo(v, parsedValue) || `Минимум ${parsedValue} лет}`;
+      };
     }
 
-    // number
-    case 'max': {
-      const parsedValue = z.number().parse(Number(validator.value));
-      return (v: number) => v <= parsedValue || `Значение не должно быть больше ${parsedValue}`;
+    // Числовые валидаторы
+    case 'max':
+    case 'min': {
+      const parsedValue = parseValidatorValue(value);
+      if (parsedValue === null) {
+        return () => `Некорректное значение валидатора: ${value}`;
+      }
+
+      return (v: unknown) => {
+        if (v === null) return true;
+        if (typeof v !== 'number') return getTypeError('number');
+        return code === 'max'
+          ? v <= parsedValue || `Не больше ${parsedValue}`
+          : v >= parsedValue || `Не меньше ${parsedValue}`;
+      };
     }
 
-    case 'min': {
-      const parsedValue = z.number().parse(Number(validator.value));
-      return (v: number) => v >= parsedValue || `Значение не должно быть меньше ${parsedValue}`;
+    // Музыка
+    case 'audioMaxSec': {
+      const parsedValue = parseValidatorValue(value);
+      if (parsedValue === null) {
+        return () => `Некорректное значение валидатора: ${value}`;
+      }
+
+      return async (v: unknown) => {
+        if (v === null) return true;
+        if (!(v instanceof File)) return getTypeError('audio');
+        const duration = await getAudioDuration(v);
+        return duration <= parsedValue || `Превышен лимит длительности ${parsedValue} секунд`;
+      };
     }
+
+    default:
+      return () => `Неизвестный валидатор: ${code}`;
   }
 };
-
-// import type { ValidationRule } from 'quasar';
-// import type { Validator } from './custom-form-types';
-// import { z } from 'zod';
-// import { isEmail } from 'validator';
-
-// export const getValidationFunc = (validator: Validator): ValidationRule | undefined => {
-//   switch (validator.fieldTypeCode) {
-//     case 'string':
-//       return getStringFunc(validator as Validator & { fieldTypeCode: 'string' });
-//     case 'number':
-//       return getNumberFunc(validator as Validator & { fieldTypeCode: 'number' });
-//     case 'checkbox':
-//       return getCheckboxFunc(validator as Validator & { fieldTypeCode: 'checkbox' });
-//   }
-// };
-
-// const getStringFunc = (
-//   validator: Validator & { fieldTypeCode: 'string' },
-// ): ValidationRule | undefined => {
-//   switch (validator.code) {
-//     case 'requiredSring': {
-//       return (v: string) => v || `Поле должно быть заполнено`;
-//     }
-
-//     case 'maxString': {
-//       const parsedValue = z.number().parse(validator.value);
-//       return (v: string) =>
-//         v.length <= parsedValue || `Поле должно содержать максимум ${parsedValue} символов`;
-//     }
-
-//     case 'minString': {
-//       const parsedValue = z.number().parse(validator.value);
-//       return (v: string) =>
-//         v.length >= parsedValue || `Поле должно содержать минимум ${parsedValue} символов`;
-//     }
-
-//     case 'isMail': {
-//       return (v: string) => (v && isEmail(v)) || `Введите корректуню почту`;
-//     }
-//   }
-// };
-
-// const getNumberFunc = (
-//   validator: Validator & { fieldTypeCode: 'number' },
-// ): ValidationRule | undefined => {
-//   switch (validator.code) {
-//     case 'max': {
-//       const parsedValue = z.number().parse(validator.value);
-//       return (v: number) => v <= parsedValue || `Значение не должно быть больше ${parsedValue}`;
-//     }
-
-//     case 'min': {
-//       const parsedValue = z.number().parse(validator.value);
-//       return (v: number) => v >= parsedValue || `Значение не должно быть меньше ${parsedValue}`;
-//     }
-//   }
-// };
-
-// const getCheckboxFunc = (
-//   validator: Validator & { fieldTypeCode: 'checkbox' },
-// ): ValidationRule | undefined => {
-//   switch (validator.code) {
-//     case 'required':
-//       return (v: boolean) => v || `Поле должно быть заполнено`;
-//   }
-// };

+ 4 - 2
src/modules/part-entities/CreatePePage.vue

@@ -1,6 +1,7 @@
 <template>
   <div>
     <q-card v-if="formFields">
+      <!-- TODO: запихнуть внутрь -->
       <q-input v-model="name" label="Внутреннее название" />
 
       <q-card-section>
@@ -37,15 +38,16 @@ void createPeStore.getCurrentPeCreateTypeData();
 const router = useRouter();
 const submit = async (fields: FromFieldWithValidatorsAndValue[]) => {
   if (!currentCreatePeData.value) throw Error('currentCreatePeData не определён');
-  const peTypeId = currentCreatePeData.value.peTypeId;
+  const peTypeCode = currentCreatePeData.value.code;
 
   const form = fields.map((f) => {
     return {
       peFfId: f.customFieldId,
       value: f.value,
+      fieldType: f.fieldTypeCode,
     };
   });
-  const peId = await createPeStore.createPe(form, peTypeId, name.value);
+  const peId = await createPeStore.createPe(form, peTypeCode, name.value);
   if (!peId) return;
 
   await router.push({ name: 'part-entity', params: { peId } });

+ 58 - 6
src/modules/part-entities/create-pe-store.ts

@@ -6,6 +6,7 @@ import errorMiddleware from 'src/utils/error-middleware';
 import type { z } from 'zod';
 import { useRoute, useRouter } from 'vue-router';
 import type { api } from 'src/api/current-api';
+import type { FieldTypeCode, InputFieldValue } from 'src/components/custom-form/custom-form-types';
 
 export const useCreatePeStore = defineStore('create-pe-store', () => {
   const route = useRoute();
@@ -69,22 +70,73 @@ export const useCreatePeStore = defineStore('create-pe-store', () => {
 
   const createPe = async (
     form: {
-      value: string | null;
+      value: InputFieldValue;
       peFfId: string;
+      fieldType: FieldTypeCode;
     }[],
-    peTypeId: string,
+    peTypeCode: string,
     name: string,
   ) => {
     try {
-      const req: z.infer<typeof api.client.pe.POST_PartEntity.req> = {
-        form,
-        peTypeId,
+      const files = form
+        .map((f) => {
+          if (f.value instanceof File)
+            return {
+              value: f.value,
+              peFfId: f.peFfId,
+            };
+        })
+        .filter((f) => f !== undefined);
+
+      const formatedForm = form.map((f) => {
+        if (
+          f.value instanceof File &&
+          ['audio', 'img', 'video', 'pdf', 'other'].includes(f.fieldType)
+        )
+          return {
+            value: {
+              fileName: f.value.name,
+              type: f.fieldType as 'audio' | 'img' | 'video' | 'pdf' | 'other',
+            },
+            peFfId: f.peFfId,
+          };
+
+        if (
+          typeof f.value === 'string' ||
+          typeof f.value === 'number' ||
+          typeof f.value === 'boolean' ||
+          f.value === null
+        ) {
+          return {
+            value: f.value,
+            peFfId: f.peFfId,
+          };
+        }
+
+        throw new Error(`неизвестный тип значения ${typeof f.value}`);
+      });
+
+      const formData = new FormData();
+      files.forEach((f) => {
+        formData.append(f.peFfId, f.value);
+      });
+
+      const reqBody: z.infer<typeof api.client.pe.POST_PartEntity.req.formData.body> = {
+        fields: formatedForm,
+        peTypeCode,
         name,
       };
 
+      formData.append('body', JSON.stringify(reqBody));
+
       const { data } = await $api.post<z.infer<typeof api.client.pe.POST_PartEntity.res>>(
         `/client/pe/create`,
-        req,
+        formData,
+        {
+          headers: {
+            'Content-Type': 'multipart/form-data',
+          },
+        },
       );
 
       const peId = data.peId;

+ 4 - 1
src/modules/users/profile/ProfilePage.vue

@@ -4,6 +4,7 @@
       @submit="patchUserData($event)"
       :fields="userDataForm"
       :disable-btn-before-change="true"
+      :edit-mode="true"
     />
   </q-page>
 </template>
@@ -24,15 +25,17 @@ const patchUserData = async (fields: FromFieldWithValidatorsAndValue[]) => {
   const ud = fields.map((f) => ({
     userEfId: f.customFieldId,
     value: f.value,
+    fieldType: f.fieldTypeCode,
   }));
 
   await profileStore.patchUserData(ud);
+  await profileStore.getUserData();
 };
 
 const userDataForm = computed(() => {
   if (!userData.value) return null;
 
-  return userData.value.map((f) => {
+  return userData.value.fields.map((f) => {
     return { ...f, customFieldId: f.userEfId };
   });
 });

+ 63 - 18
src/modules/users/profile/profile-store.ts

@@ -5,23 +5,11 @@ import { $api } from 'src/boot/axios';
 import errorMiddleware from 'src/utils/error-middleware';
 import type { z } from 'zod';
 import type { api } from 'src/api/current-api';
-import type { FieldTypeCode, Validator } from 'src/components/custom-form/custom-form-types';
+import type { ProfileData } from './profile-types';
+import type { FieldTypeCode, InputFieldValue } from 'src/components/custom-form/custom-form-types';
 
 export const useProfileStore = defineStore('profile-store', () => {
-  const userData = ref<
-    | {
-        fieldDefinitionId: string;
-        userEfId: string;
-        code: string | null;
-        fieldTypeCode: FieldTypeCode;
-        title: string;
-        mask: string | null;
-        options: string[] | null;
-        validators: Validator[];
-        value: string | null;
-      }[]
-    | null
-  >(null);
+  const userData = ref<ProfileData | null>(null);
 
   const getUserData = async () => {
     try {
@@ -34,10 +22,67 @@ export const useProfileStore = defineStore('profile-store', () => {
     }
   };
 
-  const patchUserData = async (userData: { userEfId: string; value: string | null }[]) => {
-    const req: z.infer<typeof api.client.users.PATCH_UserEventData.req> = { userData };
+  const patchUserData = async (
+    fields: { userEfId: string; value: InputFieldValue; fieldType: FieldTypeCode }[],
+  ) => {
+    try {
+      // TODO: рефакторинг с pe и act
+      const files = fields
+        .map((f) => {
+          if (f.value instanceof File) {
+            return {
+              value: f.value,
+              userEfId: f.userEfId,
+            };
+          }
+        })
+        .filter((f) => f !== undefined);
+
+      const formatedForm = fields.map((f) => {
+        if (
+          f.value instanceof File &&
+          ['audio', 'img', 'video', 'pdf', 'other'].includes(f.fieldType)
+        )
+          return {
+            value: f.value.name,
+            userEfId: f.userEfId,
+            fieldType: f.fieldType,
+          };
+
+        if (
+          f.value === null ||
+          typeof f.value === 'string' ||
+          typeof f.value === 'number' ||
+          typeof f.value === 'boolean'
+        ) {
+          return {
+            value: f.value,
+            userEfId: f.userEfId,
+          };
+        }
+
+        throw new Error(`неизвестный тип значения ${typeof f.value}`);
+      });
+
+      const formData = new FormData();
+      files.forEach((f) => {
+        formData.append(f.userEfId, f.value);
+      });
 
-    await $api.patch(`/client/users/userEventData`, req);
+      const reqBody: z.infer<typeof api.client.users.PATCH_UserEventData.req.formData.body> = {
+        fields: formatedForm,
+      };
+
+      formData.append('body', JSON.stringify(reqBody));
+
+      await $api.patch(`/client/users/userEventData`, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data',
+        },
+      });
+    } catch (e) {
+      errorMiddleware(e);
+    }
   };
 
   return { getUserData, userData, patchUserData };

+ 7 - 0
src/modules/users/profile/profile-types.ts

@@ -0,0 +1,7 @@
+import type { CustomFieldWithValidatorsAndValue } from 'src/components/custom-form/custom-form-types';
+
+export type ProfileData = {
+  fields: (CustomFieldWithValidatorsAndValue & {
+    userEfId: string;
+  })[];
+};

+ 23 - 22
tsconfig.json

@@ -1,24 +1,25 @@
 {
-  "extends": "./.quasar/tsconfig.json"
-  // "compilerOptions": {
-  //   // "baseUrl": "./",
-  //   // "checkJs": true
-  //   // "paths": {
-  //   //   "@/*": ["src/*"]
-  //   // }
-  //   // "strict": true, // Включает все строгие проверки
-  //   // "strictNullChecks": true, // Проверка `null` и `undefined`
-  //   // "strictFunctionTypes": true // Улучшенная проверка типов функций
-  //   // "strictPropertyInitialization": true, // Гарантия инициализации свойств в классах
-  //   // "noImplicitAny": true, // Запрещает `any`, если не указан тип
-  //   // "noImplicitThis": true, // Ошибки, если `this` имеет неявный тип `any`
-  //   // "exactOptionalPropertyTypes": true, // Запрещает лишние опциональные свойства
-  //   // "useUnknownInCatchVariables": true, // В catch не `any`, а `unknown`
-  //   // "noUncheckedIndexedAccess": true, // Проверка доступа к массивам и объектам
-  //   // "noImplicitReturns": true, // Гарантия, что все функции что-то возвращают
-  //   // "noFallthroughCasesInSwitch": true, // Защита от падения в `switch-case`
-  //   // "esModuleInterop": true, // Импорт CommonJS-модулей без проблем
-  //   // "skipLibCheck": true, // Ускоряет компиляцию, не проверяя зависимости
-  //   // "forceConsistentCasingInFileNames": true // Избегает ошибок в регистрах файлов
-  // }
+  "extends": "./.quasar/tsconfig.json",
+  "compilerOptions": {
+    "lib": ["ES2022", "DOM"]
+    //   // "baseUrl": "./",
+    //   // "checkJs": true
+    //   // "paths": {
+    //   //   "@/*": ["src/*"]
+    //   // }
+    //   // "strict": true, // Включает все строгие проверки
+    //   // "strictNullChecks": true, // Проверка `null` и `undefined`
+    //   // "strictFunctionTypes": true // Улучшенная проверка типов функций
+    //   // "strictPropertyInitialization": true, // Гарантия инициализации свойств в классах
+    //   // "noImplicitAny": true, // Запрещает `any`, если не указан тип
+    //   // "noImplicitThis": true, // Ошибки, если `this` имеет неявный тип `any`
+    //   // "exactOptionalPropertyTypes": true, // Запрещает лишние опциональные свойства
+    //   // "useUnknownInCatchVariables": true, // В catch не `any`, а `unknown`
+    //   // "noUncheckedIndexedAccess": true, // Проверка доступа к массивам и объектам
+    //   // "noImplicitReturns": true, // Гарантия, что все функции что-то возвращают
+    //   // "noFallthroughCasesInSwitch": true, // Защита от падения в `switch-case`
+    //   // "esModuleInterop": true, // Импорт CommonJS-модулей без проблем
+    //   // "skipLibCheck": true, // Ускоряет компиляцию, не проверяя зависимости
+    //   // "forceConsistentCasingInFileNames": true // Избегает ошибок в регистрах файлов
+  }
 }