Tworzysz aplikację przy użyciu Vue.js. Dla klasy PrinterPosnet napisz pełną implementację dodawania nowych drukarek Posnet (klasa printer-posnet). Napisz implementację wszystkich plików niezbędnych do tej implementacji. Funkcjonalność i widoki powinny być analogiczne do dodawania nowych elementów dla klasy OccupationalFactor.

/// printer-posnet.ts 

import { GenericEntity } from "@/views/commerce/common/named-item";
import PrinterPosnetStatus from "@/views/commerce/printers-posnet/printer-posnet-status";
import PrinterPosnetTarget from "@/views/commerce/printers-posnet/printer-posnet-target";
import type { SelectPanelOption } from "@/components/action-buttons/select-panel-option";

export default class PrinterPosnet extends GenericEntity implements SelectPanelOption {
  id: string;
  orgUnitId: number;
  name: string | null;
  target: PrinterPosnetTarget = PrinterPosnetTarget.instance.reception;
  status: PrinterPosnetStatus = PrinterPosnetStatus.empty();
  ipAddress: string | null = null;

  public static fromDto(dto: any): PrinterPosnet {
    const entity = new PrinterPosnet();
    entity.readDto(dto);
    return entity;
  }

  protected readDto(dto: any) {
    super.readDto(dto);
    this.id = dto.id;
    this.orgUnitId = dto.orgUnitId;
    this.name = dto.name;
    this.target = PrinterPosnetTarget.getById(dto.target);
    this.status = dto.status ? PrinterPosnetStatus.fromDto(dto.status) : PrinterPosnetStatus.empty();
    this.ipAddress = dto.ipAddress;
    this.model = dto.model;
  }

  get displayName() {
    if (this.name) return `${this.name} (${this.id})`;
    return this.id;
  }

  get dto(): any {
    return {
      id: this.id,
      orgUnitId: this.orgUnitId,
      name: this.name,
      target: this.target.id,
      status: this.status,
      ipAddress: this.ipAddress,
      model: this.model
    };
  }
}

/// printer-posnet-card.vue

<template>
    <titled-card :title="title" toolbar close-button @close="discard">
      <loading-container
        :is-loaded="isLoaded"
        fluid
        class="ma-1 pa-0 pr-3"
        style="min-width: min(700px, 95vw)">
        <v2-form :validator="formRef">
          <v-row dense>
            <v-col>
              <v2-text-field
                v-model="model.name"
                :rules="nameRules"
                :readonly="readonly"
                class="mb-2"
                label="Nazwa" />
            </v-col>
            <v-col>
              <v2-text-field
                v-model="model.model"
                :rules="modelRules"
                :readonly="readonly"
                class="mb-2"
                label="Model" />
            </v-col>
          </v-row>

          <v-row dense>
            <v-col>
              <v2-text-field
                v-model="model.ipAddress"
                :rules="ipAddressRules"
                :readonly="readonly"
                class="mb-2"
                label="Adres IP" />
            </v-col>
            <v-col>
              <v2-autocomplete
                v-model="model.target.id"
                :items="printerTargets"
                :readonly="readonly"
                label="Cel"
                placeholder="Wybierz z listy" />
            </v-col>
          </v-row>

          <v-row dense>
            <v-col>
              <v2-select
                v-model="model.status.id"
                :items="printerStatuses"
                :readonly="readonly"
                label="Status"
                placeholder="Wybierz status" />
            </v-col>
            <v-col>
              <v2-text-field
                v-model="model.orgUnitId"
                :rules="orgUnitIdRules"
                :readonly="readonly"
                class="mb-2"
                label="Jednostka Organizacyjna ID" />
            </v-col>
          </v-row>

          <v-row justify="end">
            <v-col cols="auto">
              <submit-discard-async-buttons :submit="submit" @discard="discard" />
            </v-col>
          </v-row>
        </v2-form>
      </loading-container>
    </titled-card>
  </template>

  <script lang="ts">
  import { computed, defineComponent, onMounted, ref } from "@vue/composition-api";
  import LoadingContainer from "@/components/loading/loading-container.vue";
  import SubmitDiscardAsyncButtons from "@/components/action-buttons/submit-discard-async-buttons.vue";
  import axios from "@/lib/axios";
  import TitledCard from "@/components/simple/titled-card.vue";
  import { RulesUtils } from "@/lib/rules-utils";
  import PrinterPosnet from "@/views/commerce/printers-posnet/printer-posnet";
  import PrinterPosnetTarget from "@/views/commerce/printers-posnet/printer-posnet-target";
  import PrinterPosnetStatus from "@/views/commerce/printers-posnet/printer-posnet-status";
  import V2Form from "@/compatibility-layer/components/v2-form.vue";
  import V2TextField from "@/compatibility-layer/components/v2-text-field.vue";
  import V2Autocomplete from "@/compatibility-layer/components/v2-autocomplete.vue";
  import V2Select from "@/compatibility-layer/components/v2-select.vue";
  import V2FormValidator from "@/compatibility-layer/components/v2-form-validator";

  const PrinterPosnetCard = defineComponent({
    components: {
      TitledCard,
      SubmitDiscardAsyncButtons,
      V2Autocomplete,
      V2TextField,
      V2Form,
      V2Select,
      LoadingContainer
    },

    props: {
      value: {
        type: String,
        required: false,
        default: null
      },

      readonly: {
        type: Boolean,
        required: false,
        default: false
      }
    },

    emits: ["close"],

    setup(props, context) {
      const isLoaded = ref(false);
      const model = ref(props.value ? new PrinterPosnet() : PrinterPosnet.empty());
      const formRef = ref(V2FormValidator.create());

      const printerTargets = ref(PrinterPosnetTarget.All);
      const printerStatuses = ref(PrinterPosnetStatus.All);

      onMounted(async () => {
        await loadData();
      });

      const loadData = async () => {
        if (props.value) {
          const response = await axios.get("/api/services/app/PrinterPosnet/GetPrinter", {
            params: { id: props.value }
          });
          model.value = PrinterPosnet.fromDto(response.data.result);
        }

        isLoaded.value = true;
      };

      const isNew = computed(() => !props.value);

      const submit = async () => {
        if (!formRef.value.validate()) return;

        if (isNew.value) {
          await axios.post("/api/services/app/PrinterPosnet/CreatePrinter", model.value.dto);
        } else {
          await axios.put("/api/services/app/PrinterPosnet/UpdatePrinter", model.value.dto);
        }

        context.emit("close", true);
      };

      const title = computed(() => {
        const prefix = "Zarządzanie Drukarkami Posnet";
        const suffix = isNew.value ? "Nowa drukarka" : `Edycja drukarki: ${model.value.name}`;
        return `${prefix} - ${suffix}`;
      });

      const nameRules = RulesUtils.MandatoryString(128);
      const modelRules = RulesUtils.MandatoryString(64);
      const ipAddressRules = [
        v => /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(v) || "Niepoprawny adres IP"
      ];
      const orgUnitIdRules = [
        v => (!!v && Number(v) > 0) || "Niepoprawne ID jednostki organizacyjnej"
      ];

      return {
        isLoaded,
        model,
        formRef,
        submit,
        discard: () => context.emit("close", false),
        nameRules,
        modelRules,
        ipAddressRules,
        orgUnitIdRules,
        printerTargets: printerTargets.value,
        printerStatuses: printerStatuses.value,
        title
      };
    }
  });

  export default PrinterPosnetCard;
  </script>
  
/// printer-posnet-status-card.vue

<template>
  <titled-card
    toolbar
    :title="value.displayName"
    close-button
    high
    color="primary darken-2"
    @close="close">
    <loading-container :is-loaded="isLoaded" style="min-width: 500px">
      <printer-posnet-status-content v-if="printerPosnetStatus.hasValidStatus" :value="printerPosnetStatus" />
      <template v-else>
        <div style="width: 100%; text-align: center">
          <v-icon x-large color="error">
            mdi-alert
          </v-icon>
        </div>
        <div style="width: 100%; text-align: center" class="error--text text-body-2">
          Błąd komunikacji z drukarką
        </div>
      </template>
      <v-row dense justify="space-between" align="end">
        <v-col cols="auto">
          <submit-button :action="restart">
            Restart
          </submit-button>
        </v-col>
        <v-col v-if="printerPosnetStatus.lastRestartTime" cols="auto" class="text-caption">
          Ostatni restart: {{ printerPosnetStatus.lastRestartTime | formatTime }}
        </v-col>
      </v-row>
    </loading-container>
  </titled-card>
</template>
<script lang="ts">
import type { PropType} from "@vue/composition-api";
import { defineComponent, onMounted, ref } from "@vue/composition-api";
import TitledCard from "@/components/simple/titled-card.vue";
import LoadingContainer from "@/components/loading/loading-container.vue";
import TerminalHelloDataContent from "@/views/commerce/terminals/terminal-hello-data-content.vue";
import axios from "@/lib/axios";
import PrinterPosnetStatusContent from "@/views/commerce/printers-posnet/printer-posnet-status-content.vue";
import PrinterPosnet from "@/views/commerce/printers-posnet/printer-posnet";
import PrinterPosnetStatus from "@/views/commerce/printers-posnet/printer-posnet-status";
import SubmitButton from "@/components/action-buttons/submit-button.vue";

const TerminalStatusCard = defineComponent({
  components: {
    SubmitButton,
    PrinterPosnetStatusContent,
    TerminalHelloDataContent,
    LoadingContainer,
    TitledCard
  },

  props: {
    value: {
      type: Object as PropType<PrinterPosnet>,
      required: true
    }
  },

  emits: ["close"],

  setup(props, context) {
    const printerStatus = ref(PrinterPosnetStatus.empty() as PrinterPosnetStatus);
    const isLoaded = ref(false);

    onMounted(async () => {
      try {
        await load();
      } finally {
        isLoaded.value = true;
      }
    });

    const restart = async () => {
      const response = await axios.post("/api/services/app/Printers/RestartProxy", {
        id: props.value.id
      });
      printerStatus.value = PrinterPosnet.fromDto(response.data.result).status;
    };

    const load = async () => {
      const response = await axios.get("/api/services/app/Printers/GetPrinter", {
        params: { id: props.value.id }
      });
      printerStatus.value = PrinterPosnet.fromDto(response.data.result).status;
    };

    const close = () => context.emit("close", true);
    return { close, isLoaded, printerPosnetStatus, restart };
  }
});

export default TerminalStatusCard;
</script>


/// occupational-factor.ts

import { GenericNamedGuidEntity } from "@/views/commerce/common/named-item";
import OccupationalConfigItemType from "@/views/commerce/occupational-medicine/occupational-config-item-type";
import { OccupationalFactorValiditySetup } from "@/views/commerce/occupational-medicine/occupational-factor-validity-setup";

export class OccupationalFactor extends GenericNamedGuidEntity {
  category: number;
  surveyName: string;
  type: OccupationalConfigItemType = OccupationalConfigItemType.Factor;
  statementValiditySetup: OccupationalFactorValiditySetup = OccupationalFactorValiditySetup.empty();
  options = 0;

  get typeModel() {
    return this.type.id;
  }

  set typeModel(val) {
    this.type = OccupationalConfigItemType.getById(val);
  }

  public static fromDto(dto: any): OccupationalFactor {
    const entity = new OccupationalFactor();
    entity.readDto(dto);
    return entity;
  }

  public static empty(category: number): OccupationalFactor {
    const entity = new OccupationalFactor();
    entity.category = category;

    return entity;
  }

  protected readDto(dto: any) {
    super.readDto(dto);
    this.type = OccupationalConfigItemType.getById(dto.type);
    this.category = dto.category;
    this.options = dto.options;
    this.surveyName = dto.surveyName || dto.name;
    this.statementValiditySetup = dto.statementValiditySetup
      ? OccupationalFactorValiditySetup.fromDto(dto.statementValiditySetup)
      : OccupationalFactorValiditySetup.empty();
  }

  public get dto(): any {
    return {
      ...super.dto,
      type: this.type.id,
      category: this.category,
      options: this.options,
      surveyName: this.surveyName || this.name,
      statementValiditySetup: this.statementValiditySetup.dto
    };
  }
}

/// occupational-factor-query.ts

import { PagedTextQuery } from "@/views/commerce/common/paged-text-query";

export class OccupationalFactorQuery extends PagedTextQuery {
  category: number | null = null;

  get serializableQuery(): any {
    return this.nonDefaultValues(new OccupationalFactorQuery());
  }

  get dto(): any {
    return {
      ...super.dto,
      category: this.category
    };
  }
}


/// occupational-factor-row.vue

<template>
  <v-row dense align="center">
    <v-col cols="auto" class="px-2">
      <span class="text-body-2"><strong>{{ value.name }}</strong>
        <span class="text-caption ml-2">{{ value.type.name }}</span>
      </span>
    </v-col>
    <v-spacer />

    <v-col cols="auto" class="pt-2">
      <edit-async-delete-buttons :readonly="readonly" :delete-action="onDelete" @select="select" />
    </v-col>
  </v-row>
</template>
<script lang="ts">
import type { PropType} from "@vue/composition-api";
import { defineComponent, inject, ref } from "@vue/composition-api";
import axios from "@/lib/axios";
import AppState from "@/views/main-wrap/app-state";
import StateMappings from "@/views/main-wrap/state-mappings";
import SubmitButton from "@/components/action-buttons/submit-button.vue";
import TooltipIcon from "@/components/simple/tooltip-icon.vue";
import InlineIcon from "@/components/simple/inline-icon.vue";
import TextUtils from "@/lib/text-utils";
import type { OccupationalFactor } from "@/views/commerce/occupational-medicine/occupational-factor";
import EditAsyncDeleteButtons from "@/components/action-buttons/edit-async-delete-buttons.vue";

const OccupationalFactorRow = defineComponent({
  components: {
    EditAsyncDeleteButtons,
    InlineIcon,
    TooltipIcon,
    SubmitButton
  },
  props: {
    value: {
      type: Object as PropType<OccupationalFactor>,
      required: true
    },
    readonly: {
      type: Boolean,
      required: false,
      default: false
    }
  },

  emits: ["update", "select"],

  setup(props, context) {
    const appState = inject(AppState.Key) as AppState;
    const stateMappings = ref(StateMappings.create(appState));

    const onDelete = async () => {
      await axios.delete("/api/services/app/OccupationalMedicine/DeleteFactor", {
        params: { id: props.value.id }
      });
      context.emit("update");
    };

    return {
      stateMappings,
      onDelete,
      trim: TextUtils.trim,
      select: () => context.emit("select")
    };
  }
});

export default OccupationalFactorRow;
</script>

/// occupational-factor-card.vue

<template>
  <titled-card :title="title" toolbar close-button @close="discard">
    <loading-container
      :is-loaded="isLoaded"
      fluid
      class="ma-1 pa-0 pr-3"
      style="min-width: min(700px, 95vw)">
      <v2-form :validator="formRef">
        <v-row dense>
          <v-col>
            <v2-text-field
              v-model="model.name"
              :rules="nameRules"
              :readonly="readonly"
              class="mb-2"
              label="Nazwa" />
          </v-col>
          <v-col>
            <v2-text-field
              v-model="model.surveyName"
              :rules="optionalNameRules"
              :readonly="readonly"
              class="mb-2"
              clearable
              label="Nazwa (ankieta)" />
          </v-col>
        </v-row>

        <v-row dense align="center">
          <v-col>
            <v2-autocomplete
              v-model="model.typeModel"
              :items="itemTypes"
              :readonly="readonly"
              label="Rodzaj"
              placeholder="Wybierz z listy" />
          </v-col>
          <v-col>
            <mask-selector v-model="model.options" :options="factorOptions" />
          </v-col>
        </v-row>
        <occupational-factor-validity-setup-panel
          v-model="model.statementValiditySetup"
          class="my-2"
          :readonly="readonly" />
        <v-row justify="end">
          <v-col cols="auto">
            <submit-discard-async-buttons :submit="submit" @discard="discard" />
          </v-col>
        </v-row>
      </v2-form>
    </loading-container>
  </titled-card>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from "@vue/composition-api";
import LoadingContainer from "@/components/loading/loading-container.vue";
import SubmitDiscardAsyncButtons from "@/components/action-buttons/submit-discard-async-buttons.vue";
import axios from "@/lib/axios";
import TitledCard from "@/components/simple/titled-card.vue";
import { RulesUtils } from "@/lib/rules-utils";
import { OccupationalFactor } from "@/views/commerce/occupational-medicine/occupational-factor";
import OccupationalCategory from "@/views/commerce/occupational-medicine/occupational-category";
import OccupationalConfigItemType from "@/views/commerce/occupational-medicine/occupational-config-item-type";
import OccupationalFactorValiditySetupPanel
  from "@/views/commerce/occupational-medicine/occupational-factor-validity-setup-panel.vue";
import OccupationalFactorOptions from "@/views/commerce/occupational-medicine/occupational-factor-options";
import MaskSelector from "@/components/enums/mask-selector.vue";
import V2Form from "@/compatibility-layer/components/v2-form.vue";
import V2TextField from "@/compatibility-layer/components/v2-text-field.vue";
import V2Autocomplete from "@/compatibility-layer/components/v2-autocomplete.vue";
import V2FormValidator from "@/compatibility-layer/components/v2-form-validator";

const OccupationalFactorCard = defineComponent({
  components: {
    TitledCard,
    SubmitDiscardAsyncButtons,
    OccupationalFactorValiditySetupPanel,
    V2Autocomplete,
    V2TextField,
    V2Form,
    MaskSelector,
    LoadingContainer
  },

  props: {
    value: {
      type: String,
      required: false,
      default: null
    },

    category: {
      type: Number,
      required: true
    },
    readonly: {
      type: Boolean,
      required: false,
      default: false
    }
  },

  emits: ["close"],

  setup(props, context) {
    const isLoaded = ref(false);
    const model = ref(OccupationalFactor.empty(props.category));
    const formRef = ref(V2FormValidator.create());

    onMounted(async () => {
      await loadData();
    });

    const loadData = async () => {
      if (props.value) {
        const response = await axios.get("/api/services/app/OccupationalMedicine/GetFactor", {
          params: { id: props.value }
        });
        model.value = OccupationalFactor.fromDto(response.data.result);
      }

      isLoaded.value = true;
    };

    const isNew = computed(() => !props.value);

    const submit = async () => {
      if (!formRef.value.validate()) return;

      if (isNew.value) {
        await axios.post("/api/services/app/OccupationalMedicine/CreateFactor", model.value.dto);
      } else {
        await axios.put("/api/services/app/OccupationalMedicine/UpdateFactor", model.value.dto);
      }

      context.emit("close", true);
    };


    const title = computed(() => {
      const prefix = OccupationalCategory.getById(props.category).name;
      const suffix = isNew.value ? "Nowy wpis" : `Edycja wpisu: ${model.value.name}`;
      return `${prefix} - ${suffix}`;
    });

    return {
      isLoaded,
      model,
      formRef,
      submit,
      discard: () => context.emit("close", false),
      nameRules: RulesUtils.MandatoryString(128),
      optionalNameRules: RulesUtils.OptionalString(128),
      itemTypes: OccupationalConfigItemType.All,
      factorOptions: OccupationalFactorOptions.All,
      title
    };
  }
});

export default OccupationalFactorCard;
</script>

/// occupational-factors-view.vue

<template>
  <loading-container :is-loaded="pagedState.isLoaded">
    <v-overlay v-if="pagedState.isCardVisible" z-index="10" :dark="false">
      <occupational-factor-card
        :readonly="pagedState.readonly"
        :value="pagedState.selectedId"
        :category="pagedState.query.category"
        @close="v => pagedState.onCardClose(v)" />
    </v-overlay>
    <v-row dense align="center">
      <v-col cols="auto">
        <v-btn-toggle v-model="pagedState.query.category" dense mandatory color="primary">
          <v-btn v-for="(opt, optIdx) in categories" :key="optIdx" text>
            {{ opt.name }}
          </v-btn>
        </v-btn-toggle>
      </v-col>
      <v-spacer />
      <search-input v-model="pagedState.query.searchPhrase" />
    </v-row>

    <paged-data-iterator v-model="pagedState.query.pagedModel" :paged-items="pagedState">
      <template #default="{ item }">
        <occupational-factor-row
          :key="idx"
          :value="item"
          :readonly="pagedState.readonly"
          @select="() => pagedState.select(item.id)"
          @update="() => pagedState.onSearchUpdate()" />
      </template>
    </paged-data-iterator>

  </loading-container>
</template>
<script lang="ts">
import { defineComponent } from "@vue/composition-api";
import LoadingContainer from "@/components/loading/loading-container.vue";
import SearchInput from "@/components/simple/search-input.vue";
import AddItemButton from "@/components/simple/add-item-button.vue";
import OccupationalCategory from "@/views/commerce/occupational-medicine/occupational-category";
import { OccupationalFactor } from "@/views/commerce/occupational-medicine/occupational-factor";
import { OccupationalFactorQuery } from "@/views/commerce/occupational-medicine/occupational-factor-query";
import OccupationalFactorCard from "@/views/commerce/occupational-medicine/occupational-factor-card.vue";
import OccupationalFactorRow from "@/views/commerce/occupational-medicine/occupational-factor-row.vue";
import PagedDataIterator from "@/components/paging/paged-data-iterator.vue";
import { usePagedSearch } from "@/components/paging/paged-view-helpers";
import { PermissionNames } from "@/lib/permissions";
import type { PagedSearchOptions } from "@/components/paging/paged-search-options";

const OccupationalFactorsView = defineComponent({
  components: {
    PagedDataIterator,
    OccupationalFactorRow,
    OccupationalFactorCard,
    AddItemButton,
    SearchInput,
    LoadingContainer
  },

  props: {},

  setup(props, context) {
    const options = {
      addItemButton: true,
      editPermission: PermissionNames.OccupationalMedicine_Configure,
      fixedQuerySetup: q => {
        if (q.category === null) q.category = OccupationalCategory.OccupationalMedicine.id;
      }
    } as PagedSearchOptions<OccupationalFactorQuery>;

    const pagedState = usePagedSearch(
      OccupationalFactor.fromDto,
      OccupationalFactorQuery,
      "/api/services/app/OccupationalMedicine/SearchFactors",
      context,
      options
    );

    return {
      pagedState,
      categories: OccupationalCategory.All
    };
  }
});

export default OccupationalFactorsView;
</script>

/// occupational-factor-validity-setup-panel.vue

<template>
  <titled-card
    title="Czas ważności orzeczenia"
    toolbar
    high
    :add-button="model.entries.length < 10 && !readonly"
    @add="add">
    <template v-for="(item, idx) in model.entries">
      <occupational-factor-validity-entry-row
        :key="idx"
        v-model="item.model"
        :readonly="readonly"
        class="my-2"
        @delete="remove(idx)" />
      <v-divider v-if="idx < model.entries.length - 1" :key="idx" />
    </template>

    <div v-if="model.entries.length === 0" class="text-body-2 grey--text">
      Brak
    </div>
    <v-text-field
      v-model="model.reviewComment"
      dense
      class="mt-4"
      :readonly="readonly"
      :filled="readonly"
      hide-details
      autocomplete="off"
      clearable
      outlined
      label="Reguły niestandardowe" />
  </titled-card>
</template>
<script lang="ts">
import type { PropType} from "@vue/composition-api";
import { defineComponent, ref, watch } from "@vue/composition-api";
import EditDeleteButtons from "@/components/action-buttons/edit-delete-buttons.vue";
import TitledCard from "@/components/simple/titled-card.vue";
import OccupationalFactorValidityEntryRow from "@/views/commerce/occupational-medicine/occupational-factor-validity-entry-row.vue";
import type { OccupationalFactorValiditySetup } from "@/views/commerce/occupational-medicine/occupational-factor-validity-setup";
import { OccupationalFactorValidityEntry } from "./occupational-factor-validity-entry";

const OccupationalFactorValiditySetupPanel = defineComponent({
  components: {
    OccupationalFactorValidityEntryRow,
    TitledCard,
    EditDeleteButtons
  },

  props: {
    value: {
      type: Object as PropType<OccupationalFactorValiditySetup>,
      required: true
    },

    readonly: {
      type: Boolean,
      required: false,
      default: false
    }
  },

  emits: ["input"],

  setup(props, context) {
    const model = ref(props.value as OccupationalFactorValiditySetup);
    const lastEmittedDto = ref({});
    watch(
      model,
      (nv, _) => {
        if (JSON.stringify(nv.dto) !== JSON.stringify(lastEmittedDto.value)) {
          lastEmittedDto.value = nv.dto;
          context.emit("input", nv);
        }
      },
      { deep: true }
    );

    watch(
      () => props.value,
      (nv, _) => {
        if (!nv.hasSameDto(model.value as OccupationalFactorValiditySetup)) model.value = nv;
      }
    );

    const add = () => {
      model.value.entries.push(OccupationalFactorValidityEntry.empty());
    };

    const remove = idx => model.value.entries.splice(idx);

    return {
      model,
      remove,
      add
    };
  }
});

export default OccupationalFactorValiditySetupPanel;
</script>

/// occupational-factor-validity-setup.ts

import { GenericEntity } from "@/views/commerce/common/named-item";
import { OccupationalFactorValidityEntry } from "@/views/commerce/occupational-medicine/occupational-factor-validity-entry";

export class OccupationalFactorValiditySetup extends GenericEntity {
  entries: OccupationalFactorValidityEntry[] = [];
  reviewComment: string | null = null;

  public static fromDto(dto: any) {
    const entity = new OccupationalFactorValiditySetup();
    entity.readDto(dto);
    return entity;
  }

  public static empty() {
    return new OccupationalFactorValiditySetup();
  }

  protected readDto(dto: any) {
    super.readDto(dto);
    this.reviewComment = dto.reviewComment;
    this.entries = dto.entries?.map(OccupationalFactorValidityEntry.fromDto) || [];
  }

  public get dto(): any {
    return {
      reviewComment: this.reviewComment,
      entries: this.entries.map(v => v.dto)
    };
  }
}

/// occupational-factor-validity-entry-row.vue

<template>
  <v-row dense align="center">
    <v-col cols="auto">
      <v-text-field
        v-model.number="model.years"
        label="Ilość lat"
        :rules="yearCountRules"
        :filled="readonly"
        :readonly="readonly"
        hide-details="auto"
        outlined
        dense
        style="max-width: 150px" />
    </v-col>
    <v-col cols="auto">
      <v-text-field
        v-model.number="model.months"
        label="miesięcy"
        :rules="monthCountRules"
        :filled="readonly"
        :readonly="readonly"
        hide-details="auto"
        outlined
        dense
        style="max-width: 150px" />
    </v-col>
    <v-col cols="auto">
      <v-text-field
        v-model.number="model.fromAge"
        label="Od wieku"
        :rules="ageRules"
        :filled="readonly"
        :readonly="readonly"
        style="max-width: 150px"
        hide-details="auto"
        outlined
        clearable
        dense
        suffix="lat" />
    </v-col>
    <v-spacer />
    <v-col cols="auto">
      <edit-delete-buttons :edit-visible="false" :delete-disabled="readonly" @delete="onDelete" />
    </v-col>
  </v-row>
</template>
<script lang="ts">
import type { PropType} from "@vue/composition-api";
import { defineComponent, ref, watch } from "@vue/composition-api";
import EditDeleteButtons from "@/components/action-buttons/edit-delete-buttons.vue";
import { isInteger } from "lodash";
import type { OccupationalFactorValidityEntry } from "@/views/commerce/occupational-medicine/occupational-factor-validity-entry";

const OccupationalFactorValidityEntryRow = defineComponent({
  components: { EditDeleteButtons },

  props: {
    value: {
      type: Object as PropType<OccupationalFactorValidityEntry>,
      required: true
    },

    readonly: {
      type: Boolean,
      required: false,
      default: false
    }
  },

  emits: ["input", "delete"],

  setup(props, context) {
    const model = ref(props.value as OccupationalFactorValidityEntry);
    const lastEmittedDto = ref({});
    watch(
      model,
      (nv, _) => {
        if (JSON.stringify(nv.dto) !== JSON.stringify(lastEmittedDto.value)) {
          lastEmittedDto.value = nv.dto;
          context.emit("input", nv);
        }
      },
      { deep: true }
    );

    watch(
      () => props.value,
      (nv, _) => {
        if (!nv.hasSameDto(model.value as OccupationalFactorValidityEntry)) model.value = nv;
      }
    );

    const yearCountRules = [
      v => (!!v && Number(v) > 0 && Number(v) <= 50 && isInteger(v)) || "Niepoprawna wartość"
    ];

    const monthCountRules = [
      v => !v || (!!v && Number(v) > 0 && Number(v) <= 50 && isInteger(v)) || "Niepoprawna wartość"
    ];

    const ageRules = [
      v => !v || (!!v && Number(v) > 0 && Number(v) <= 100 && isInteger(v)) || "Niepoprawna wartość"
    ];

    return {
      model,
      onDelete: () => context.emit("delete"),
      ageRules,
      yearCountRules,
      monthCountRules
    };
  }
});

export default OccupationalFactorValidityEntryRow;
</script>

/// occupational-factor-validity-entry.ts

import { GenericEntity } from "@/views/commerce/common/named-item";

export class OccupationalFactorValidityEntry extends GenericEntity {
  years: number;
  months = 0;
  fromAge: number | null = null;

  public static fromDto(dto: any) {
    const entity = new OccupationalFactorValidityEntry();
    entity.readDto(dto);
    return entity;
  }

  public static empty() {
    const entity = new OccupationalFactorValidityEntry();
    entity.years = 5;
    return entity;
  }

  protected readDto(dto: any) {
    super.readDto(dto);
    this.years = dto.years;
    this.months = dto.months ?? 0;
    this.fromAge = dto.fromAge;
  }

  public get dto(): any {
    return {
      years: this.years,
      months: this.months,
      fromAge: this.fromAge
    };
  }
}