Replace OnTheFly.js with OnTheFly.ts and introduce ThirdPartyEdit.vue for enhanced third-party entity management

- Migrated API functions in `OnTheFly.js` to `OnTheFly.ts` for improved type safety using TypeScript.
- Added `ThirdPartyEdit.vue` to streamline third-party creation and editing with a Vue component structure.
- Updated third-party-related types in `types.ts` to improve consistency and explicitly distinguish `company`, `contact`, and `child` entities.
- Enhanced `AddPersons.vue` and dependent components to support adding third-party contacts.
- Adjusted styles and removed `.btn-tpchild` for consistency in button definitions across SCSS.
This commit is contained in:
2025-10-24 14:21:39 +02:00
parent 1be82b3049
commit 4234377b7e
23 changed files with 816 additions and 691 deletions

View File

@@ -41,6 +41,7 @@
"typescript": "^5.6.3", "typescript": "^5.6.3",
"typescript-eslint": "^8.13.0", "typescript-eslint": "^8.13.0",
"vue-loader": "^17.0.0", "vue-loader": "^17.0.0",
"vue-tsc": "^3.1.1",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
}, },

View File

@@ -54,7 +54,6 @@ $chill-theme-buttons: (
&.btn-unlink, &.btn-unlink,
&.btn-action, &.btn-action,
&.btn-edit, &.btn-edit,
&.btn-tpchild,
&.btn-wopilink, &.btn-wopilink,
&.btn-update { &.btn-update {
&, &:hover { &, &:hover {
@@ -82,7 +81,6 @@ $chill-theme-buttons: (
&.btn-remove::before, &.btn-remove::before,
&.btn-choose::before, &.btn-choose::before,
&.btn-notify::before, &.btn-notify::before,
&.btn-tpchild::before,
&.btn-download::before, &.btn-download::before,
&.btn-search::before, &.btn-search::before,
&.btn-cancel::before { &.btn-cancel::before {
@@ -112,7 +110,6 @@ $chill-theme-buttons: (
&.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o &.btn-choose::before { content: "\f00c"; } // fa-check // f046 fa-check-square-o
&.btn-unlink::before { content: "\f127"; } // fa-chain-broken &.btn-unlink::before { content: "\f127"; } // fa-chain-broken
&.btn-notify::before { content: "\f1d8"; } // fa-paper-plane &.btn-notify::before { content: "\f1d8"; } // fa-paper-plane
&.btn-tpchild::before { content: "\f007"; } // fa-user
&.btn-download::before { content: "\f019"; } // fa-download &.btn-download::before { content: "\f019"; } // fa-download
&.btn-search::before { content: "\f002"; } // fa-search &.btn-search::before { content: "\f002"; } // fa-search
} }

View File

@@ -1,6 +1,7 @@
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc"; import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types"; import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
import { CreatableEntityType } from "ChillPersonAssets/types"; import { CreatableEntityType } from "ChillPersonAssets/types";
import {ThirdpartyCompany} from "../../../ChillThirdPartyBundle/Resources/public/types";
export interface DateTime { export interface DateTime {
datetime: string; datetime: string;
@@ -413,11 +414,22 @@ export interface TabDefinition {
counter: () => number; counter: () => number;
} }
export type CreateComponentConfigGeneral = {
action: 'create';
allowedTypes: CreatableEntityType[];
query: string;
parent: null;
}
export type CreateComponentThirdPartyAddContact = {
action: 'addContact';
allowedTypes: readonly ['thirdparty'];
query: string;
parent: ThirdpartyCompany;
}
/** /**
* Configuration for the CreateModal and Create component * Configuration for the CreateModal and Create component
*/ */
export interface CreateComponentConfig { export type CreateComponentConfig = CreateComponentConfigGeneral | CreateComponentThirdPartyAddContact;
action?: string;
allowedTypes: CreatableEntityType[];
query?: string;
}

View File

@@ -36,21 +36,21 @@
action="create" action="create"
:query="query" :query="query"
ref="castPerson" ref="castPerson"
@onPersonCreated="(payload) => emit('onPersonCreated', payload)" @onPersonCreated="onPersonCreated"
/> />
<on-the-fly-thirdparty <ThirdPartyEdit
v-if="type === 'thirdparty'" v-if="type === 'thirdparty'"
:action="action" :action="action"
:query="query" :query="query"
ref="castThirdparty" ref="castThirdparty"
@onThirdPartyCreated="onThirdPartyCreated"
type=""
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import OnTheFlyPerson from "ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue";
import OnTheFlyThirdparty from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue";
import { import {
ONTHEFLY_CREATE_PERSON, ONTHEFLY_CREATE_PERSON,
ONTHEFLY_CREATE_THIRDPARTY, ONTHEFLY_CREATE_THIRDPARTY,
@@ -59,15 +59,20 @@ import {
import { CreatableEntityType, Person } from "ChillPersonAssets/types"; import { CreatableEntityType, Person } from "ChillPersonAssets/types";
import { CreateComponentConfig } from "ChillMainAssets/types"; import { CreateComponentConfig } from "ChillMainAssets/types";
import PersonEdit from "ChillPersonAssets/vuejs/_components/OnTheFly/PersonEdit.vue"; import PersonEdit from "ChillPersonAssets/vuejs/_components/OnTheFly/PersonEdit.vue";
import ThirdPartyEdit from "ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdPartyEdit.vue";
import {Thirdparty} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
const props = withDefaults(defineProps<CreateComponentConfig>(), { const props = withDefaults(defineProps<CreateComponentConfig>(), {
allowedTypes: ["person"],
action: "create", action: "create",
query: "", query: "",
parent: null,
}); });
const emit = const emit =
defineEmits<(e: "onPersonCreated", payload: { person: Person }) => void>(); defineEmits<{
(e: "onPersonCreated", payload: { person: Person }): void,
(e: "onThirdPartyCreated", payload: { thirdParty: Thirdparty }): void,
}>();
const type = ref<CreatableEntityType | null>(null); const type = ref<CreatableEntityType | null>(null);
@@ -80,14 +85,10 @@ const radioType = computed<CreatableEntityType | null>({
}); });
type PersonEditComponent = InstanceType<typeof PersonEdit>; type PersonEditComponent = InstanceType<typeof PersonEdit>;
type ThirdPartyEditComponent = InstanceType<typeof ThirdPartyEdit>;
type AnyComponentInstance = const castPerson = ref<PersonEditComponent|null>(null);
| InstanceType<typeof OnTheFlyPerson> const castThirdparty = ref<ThirdPartyEditComponent|null>(null);
| InstanceType<typeof OnTheFlyThirdparty>
| null;
const castPerson = ref<PersonEditComponent>(null);
const castThirdparty = ref<AnyComponentInstance>(null);
onMounted(() => { onMounted(() => {
type.value = type.value =
@@ -104,11 +105,26 @@ const containsThirdParty = computed<boolean>(() =>
props.allowedTypes.includes("thirdparty"), props.allowedTypes.includes("thirdparty"),
); );
const containsPerson = computed<boolean>(() => { const containsPerson = computed<boolean>(() => {
if (props.action === 'addContact') {
return false;
}
return props.allowedTypes.includes("person"); return props.allowedTypes.includes("person");
}); });
function save(): void { function save(): void {
castPerson.value.postPerson(); if (radioType.value === "person" && castPerson.value !== null) {
castPerson.value.postPerson();
} else if (radioType.value === "thirdparty" && castThirdparty.value !== null) {
castThirdparty.value.postThirdParty();
}
}
function onThirdPartyCreated(payload: { thirdParty: Thirdparty }) {
emit("onThirdPartyCreated", payload);
}
function onPersonCreated(payload: { person: Person }) {
emit("onPersonCreated", payload);
} }
defineExpose({ save }); defineExpose({ save });

View File

@@ -5,19 +5,29 @@ import { CreateComponentConfig } from "ChillMainAssets/types";
import { trans, SAVE } from "translator"; import { trans, SAVE } from "translator";
import { useTemplateRef } from "vue"; import { useTemplateRef } from "vue";
import { Person } from "ChillPersonAssets/types"; import { Person } from "ChillPersonAssets/types";
import {Thirdparty} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
const emit = defineEmits<{ const emit = defineEmits<{
(e: "onPersonCreated", payload: { person: Person }): void; (e: "onPersonCreated", payload: { person: Person }): void;
(e: "onThirdPartyCreated", payload: { thirdParty: Thirdparty }): void;
(e: "close"): void; (e: "close"): void;
}>(); }>();
const props = defineProps<CreateComponentConfig>(); const props = defineProps<CreateComponentConfig & {modalTitle: string}>();
const modalDialogClass = { "modal-xl": true, "modal-scrollable": true }; const modalDialogClass = { "modal-xl": true, "modal-scrollable": true };
type CreateComponentType = InstanceType<typeof Create>; type CreateComponentType = InstanceType<typeof Create>;
const create = useTemplateRef<CreateComponentType>("create"); const create = useTemplateRef<CreateComponentType>("create");
const onPersonCreated = ({person}: {person: Person}): void => {
emit("onPersonCreated", {person});
};
const onThirdPartyCreated = ({tp}: {tp: Thirdparty}): void => {
emit("onThirdPartyCreated", {thirdParty: tp});
}
function save(): void { function save(): void {
console.log("save from CreateModal"); console.log("save from CreateModal");
create.value?.save(); create.value?.save();
@@ -43,7 +53,8 @@ defineExpose({ save });
:allowedTypes="props.allowedTypes" :allowedTypes="props.allowedTypes"
:action="props.action" :action="props.action"
:query="props.query" :query="props.query"
@onPersonCreated="(payload) => emit('onPersonCreated', payload)" @onPersonCreated="onPersonCreated"
@onThirdPartyCreated="onThirdPartyCreated"
></Create> ></Create>
</div> </div>
</template> </template>

View File

@@ -9,7 +9,7 @@
class="btn btn-sm" class="btn btn-sm"
target="_blank" target="_blank"
:class="classAction" :class="classAction"
:title="trans(titleAction)" :title="titleAction"
@click="openModal" @click="openModal"
> >
{{ buttonText }}<span v-if="isDead"> ()</span> {{ buttonText }}<span v-if="isDead"> ()</span>
@@ -23,14 +23,14 @@
> >
<template #header> <template #header>
<h3 v-if="parent" class="modal-title"> <h3 v-if="parent" class="modal-title">
{{ trans(titleModal, { q: parent.text }) }} {{ titleModal }}
</h3> </h3>
<h3 v-else class="modal-title"> <h3 v-else class="modal-title">
{{ trans(titleModal) }} {{ titleModal }}
</h3> </h3>
</template> </template>
<template #body v-if="type === 'person'"> <template #body v-if="type === 'person' && action !== 'addContact'">
<on-the-fly-person <on-the-fly-person
:id="id" :id="id"
:type="type" :type="type"
@@ -40,7 +40,7 @@
<div v-if="hasResourceComment"> <div v-if="hasResourceComment">
<h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3> <h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3>
<blockquote class="chill-user-quote"> <blockquote class="chill-user-quote">
{{ parent.comment }} {{ parent?.comment }}
</blockquote> </blockquote>
</div> </div>
</template> </template>
@@ -55,7 +55,7 @@
<div v-if="hasResourceComment"> <div v-if="hasResourceComment">
<h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3> <h3>{{ trans(ONTHEFLY_RESOURCE_COMMENT_TITLE) }}</h3>
<blockquote class="chill-user-quote"> <blockquote class="chill-user-quote">
{{ parent.comment }} {{ parent?.comment }}
</blockquote> </blockquote>
</div> </div>
</template> </template>
@@ -73,27 +73,23 @@
<on-the-fly-create <on-the-fly-create
:action="action" :action="action"
:allowed-types="allowedTypes" :allowed-types="allowedTypes"
:query="query" :query="query || ''"
ref="castNew" ref="castNew"
/> />
</template> </template>
<template #footer> <template #footer>
<a <a
v-if="action === 'show'"
:href="buildLocation(id, type)" :href="buildLocation(id, type)"
:title="trans(titleMessage)" :title="titleMessage"
class="btn btn-show" class="btn btn-show"
>{{ trans(buttonMessage) }} >{{ buttonMessage }}
</a>
<a v-else class="btn btn-save" @click="saveAction">
{{ trans(ACTION_SAVE) }}
</a> </a>
</template> </template>
</modal> </modal>
</teleport> </teleport>
</template> </template>
<script setup> <script setup lang="ts">
import { ref, computed, defineEmits, defineProps } from "vue"; import { ref, computed, defineEmits, defineProps } from "vue";
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue"; import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
import OnTheFlyCreate from "./Create.vue"; import OnTheFlyCreate from "./Create.vue";
@@ -104,7 +100,6 @@ import {
ACTION_SHOW, ACTION_SHOW,
ACTION_EDIT, ACTION_EDIT,
ACTION_CREATE, ACTION_CREATE,
ACTION_ADDCONTACT,
ONTHEFLY_CREATE_TITLE_DEFAULT, ONTHEFLY_CREATE_TITLE_DEFAULT,
ONTHEFLY_CREATE_TITLE_PERSON, ONTHEFLY_CREATE_TITLE_PERSON,
ONTHEFLY_CREATE_TITLE_THIRDPARTY, ONTHEFLY_CREATE_TITLE_THIRDPARTY,
@@ -112,40 +107,58 @@ import {
ONTHEFLY_SHOW_THIRDPARTY, ONTHEFLY_SHOW_THIRDPARTY,
ONTHEFLY_EDIT_PERSON, ONTHEFLY_EDIT_PERSON,
ONTHEFLY_EDIT_THIRDPARTY, ONTHEFLY_EDIT_THIRDPARTY,
ONTHEFLY_ADDCONTACT_TITLE,
ACTION_REDIRECT_PERSON, ACTION_REDIRECT_PERSON,
ACTION_REDIRECT_THIRDPARTY, ACTION_REDIRECT_THIRDPARTY,
ONTHEFLY_SHOW_FILE_PERSON, ONTHEFLY_SHOW_FILE_PERSON,
ONTHEFLY_SHOW_FILE_THIRDPARTY, ONTHEFLY_SHOW_FILE_THIRDPARTY,
ONTHEFLY_SHOW_FILE_DEFAULT, ONTHEFLY_SHOW_FILE_DEFAULT,
ONTHEFLY_RESOURCE_COMMENT_TITLE, ONTHEFLY_RESOURCE_COMMENT_TITLE,
ACTION_SAVE, THIRDPARTY_ADDCONTACT,
THIRDPARTY_ADDCONTACT_TITLE,
} from "translator"; } from "translator";
const props = defineProps({ // Types
type: String, type EntityType = "person" | "thirdparty";
id: [String, Number], type ActionType = "show" | "edit" | "create" | "addContact";
action: String,
buttonText: String, interface ParentRef {
displayBadge: Boolean, type: string;
isDead: Boolean, id: string | number;
parent: Object, text?: string;
allowedTypes: Array, comment?: string | null;
query: String, }
interface OnTheFlyComponentProps {
type: EntityType;
id: number;
action: ActionType;
buttonText?: string | null;
displayBadge?: boolean;
isDead?: boolean;
parent?: ParentRef | null;
allowedTypes?: EntityType[];
query?: string;
}
const props = withDefaults(defineProps<OnTheFlyComponentProps>(), {
buttonText: null,
displayBadge: false,
isDead: false,
parent: null,
allowedTypes: () => ["person", "thirdparty"] as EntityType[],
query: "",
}); });
const emit = defineEmits(["saveFormOnTheFly"]); const emit = defineEmits<{
(e: "saveFormOnTheFly", payload: { type: string | undefined; data: any }): void;
}>();
const modal = ref({ const modal = ref<{ showModal: boolean; modalDialogClass: string }>({
showModal: false, showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl", modalDialogClass: "modal-dialog-scrollable modal-xl",
}); });
const castPerson = ref(); const hasResourceComment = computed<boolean>(() => {
const castThirdparty = ref();
const castNew = ref();
const hasResourceComment = computed(() => {
return ( return (
typeof props.parent !== "undefined" && typeof props.parent !== "undefined" &&
props.parent !== null && props.parent !== null &&
@@ -156,7 +169,7 @@ const hasResourceComment = computed(() => {
); );
}); });
const classAction = computed(() => { const classAction = computed<string>(() => {
switch (props.action) { switch (props.action) {
case "show": case "show":
return "btn-show"; return "btn-show";
@@ -171,174 +184,112 @@ const classAction = computed(() => {
} }
}); });
const titleAction = computed(() => { const titleAction = computed<string>(() => {
switch (props.action) { switch (props.action) {
case "show": case "show":
return ACTION_SHOW; return ACTION_SHOW as unknown as string;
case "edit": case "edit":
return ACTION_EDIT; return ACTION_EDIT as unknown as string;
case "create": case "create":
return ACTION_CREATE; return ACTION_CREATE as unknown as string;
case "addContact": case "addContact":
return ACTION_ADDCONTACT; return THIRDPARTY_ADDCONTACT as unknown as string;
default: default:
return ""; return "";
} }
}); });
const titleCreate = computed(() => { const titleCreate = computed<string>(() => {
if (typeof props.allowedTypes === "undefined") { if (typeof props.allowedTypes === "undefined") {
return ONTHEFLY_CREATE_TITLE_DEFAULT; return trans(ONTHEFLY_CREATE_TITLE_DEFAULT)
} }
return props.allowedTypes.every((t) => t === "person") return props.allowedTypes.every((t: EntityType) => t === "person")
? ONTHEFLY_CREATE_TITLE_PERSON ? (trans(ONTHEFLY_CREATE_TITLE_PERSON))
: props.allowedTypes.every((t) => t === "thirdparty") : props.allowedTypes.every((t: EntityType) => t === "thirdparty")
? ONTHEFLY_CREATE_TITLE_THIRDPARTY ? (trans(ONTHEFLY_CREATE_TITLE_THIRDPARTY))
: ONTHEFLY_CREATE_TITLE_DEFAULT; : (trans(ONTHEFLY_CREATE_TITLE_DEFAULT));
}); });
const titleModal = computed(() => { const titleModal = computed<string>(() => {
switch (props.action) { switch (props.action) {
case "show": case "show":
if (props.type == "person") { if (props.type == "person") {
return ONTHEFLY_SHOW_PERSON; return trans(ONTHEFLY_SHOW_PERSON)
} else if (props.type == "thirdparty") { } else if (props.type == "thirdparty") {
return ONTHEFLY_SHOW_THIRDPARTY; return trans(ONTHEFLY_SHOW_THIRDPARTY)
} }
break; break;
case "edit": case "edit":
if (props.type == "person") { if (props.type == "person") {
return ONTHEFLY_EDIT_PERSON; return trans(ONTHEFLY_EDIT_PERSON)
} else if (props.type == "thirdparty") { } else if (props.type == "thirdparty") {
return ONTHEFLY_EDIT_THIRDPARTY; return trans(ONTHEFLY_EDIT_THIRDPARTY)
} }
break; break;
case "create": case "create":
return titleCreate.value; return titleCreate.value;
case "addContact": case "addContact":
return ONTHEFLY_ADDCONTACT_TITLE; return trans(THIRDPARTY_ADDCONTACT_TITLE)
default: default:
break; break;
} }
return ""; return "";
}); });
const titleMessage = computed(() => { const titleMessage = computed<string>(() => {
switch (props.type) { switch (props.type) {
case "person": case "person":
return ACTION_REDIRECT_PERSON; return trans(ACTION_REDIRECT_PERSON);
case "thirdparty": case "thirdparty":
return ACTION_REDIRECT_THIRDPARTY; return trans(ACTION_REDIRECT_THIRDPARTY);
default: default:
return ""; return "";
} }
}); });
const buttonMessage = computed(() => { const buttonMessage = computed<string>(() => {
switch (props.type) { switch (props.type) {
case "person": case "person":
return ONTHEFLY_SHOW_FILE_PERSON; return trans(ONTHEFLY_SHOW_FILE_PERSON);
case "thirdparty": case "thirdparty":
return ONTHEFLY_SHOW_FILE_THIRDPARTY; return trans(ONTHEFLY_SHOW_FILE_THIRDPARTY);
default: default:
return ONTHEFLY_SHOW_FILE_DEFAULT; return trans(ONTHEFLY_SHOW_FILE_DEFAULT);
} }
}); });
const isDisplayBadge = computed(() => { const isDisplayBadge = computed<boolean>(() => {
return props.displayBadge === true && props.buttonText !== null; return props.displayBadge && props.buttonText !== null;
}); });
const badgeType = computed(() => { const badgeType = computed<string>(() => {
return "entity-" + props.type + " badge-" + props.type; return "entity-" + (props.type ?? "") + " badge-" + (props.type ?? "");
}); });
const getReturnPath = computed(() => { const getReturnPath = computed<string>(() => {
return `?returnPath=${window.location.pathname}${window.location.search}${window.location.hash}`; return `?returnPath=${window.location.pathname}${window.location.search}${window.location.hash}`;
}); });
function closeModal() { function closeModal(): void {
modal.value.showModal = false; modal.value.showModal = false;
} }
function openModal() { function openModal(): void {
modal.value.showModal = true; modal.value.showModal = true;
} }
function changeActionTo(action) { function buildLocation(id: string | number | undefined, type: EntityType | undefined): string | undefined {
console.log(action);
// Not reactive in setup, but you can emit or use a ref if needed
}
function saveAction() {
let type = props.type,
data = {};
switch (type) {
case "person":
data = castPerson.value?.$data.person;
break;
case "thirdparty":
data = castThirdparty.value?.$data.thirdparty;
break;
default:
if (typeof props.type === "undefined") {
if (props.action === "addContact") {
type = "thirdparty";
data = castThirdparty.value?.$data.thirdparty;
data.parent = {
type: "thirdparty",
id: props.parent.id,
};
data.civility =
data.civility !== null
? {
type: "chill_main_civility",
id: data.civility.id,
}
: null;
data.profession = data.profession !== "" ? data.profession : "";
} else {
type = castNew.value.radioType;
data = castNew.value.castDataByType();
if (typeof data.civility !== "undefined" && null !== data.civility) {
data.civility =
data.civility !== null
? {
type: "chill_main_civility",
id: data.civility.id,
}
: null;
}
if (
typeof data.profession !== "undefined" &&
"" !== data.profession
) {
data.profession = data.profession !== "" ? data.profession : "";
}
}
} else {
throw "error with object type";
}
}
emit("saveFormOnTheFly", { type: type, data: data });
}
function buildLocation(id, type) {
if (type === "person") { if (type === "person") {
return encodeURI(`/fr/person/${id}/general${getReturnPath.value}`); return encodeURI(`/fr/person/${id}/general${getReturnPath.value}`);
} else if (type === "thirdparty") { } else if (type === "thirdparty") {
return encodeURI(`/fr/3party/3party/${id}/view${getReturnPath.value}`); return encodeURI(`/fr/3party/3party/${id}/view${getReturnPath.value}`);
} }
return undefined;
} }
defineExpose({ defineExpose({
openModal, openModal,
closeModal, closeModal,
changeActionTo,
saveAction,
castPerson,
castThirdparty,
castNew,
hasResourceComment, hasResourceComment,
modal, modal,
isDisplayBadge, isDisplayBadge,

View File

@@ -935,6 +935,7 @@ onthefly:
thirdparty: Détails du tiers thirdparty: Détails du tiers
file_person: Ouvrir la fiche de l'usager file_person: Ouvrir la fiche de l'usager
file_thirdparty: Voir le Tiers file_thirdparty: Voir le Tiers
file_default: Voir
edit: edit:
person: Modifier un usager person: Modifier un usager
thirdparty: Modifier un tiers thirdparty: Modifier un tiers

View File

@@ -369,7 +369,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
} }
/** /**
* Entity types that a user can create * Entity types that a user can create through AddPersons component
*/ */
export type CreatableEntityType = "person" | "thirdparty"; export type CreatableEntityType = "person" | "thirdparty";
@@ -382,15 +382,32 @@ export type EntityType =
| "user" | "user"
| "household"; | "household";
export type Entities = (UserGroup | User | Person | Thirdparty | Household) & { export type Entities = (UserGroup | User | Person | Thirdparty | Household);
address?: Address | null;
kind?: string;
text?: string;
profession?: string;
};
export type EntitiesOrMe = "me" | Entities; export type EntitiesOrMe = "me" | Entities;
// Type guards to discriminate Suggestions by their result kind
export function isSuggestionForUserGroup(s: Suggestion): s is Suggestion & { result: UserGroup } {
return (s as any)?.result?.type === "user_group";
}
export function isSuggestionForUser(s: Suggestion): s is Suggestion & { result: User } {
return (s as any)?.result?.type === "user";
}
export function isSuggestionForPerson(s: Suggestion): s is Suggestion & { result: Person } {
return (s as any)?.result?.type === "person";
}
export function isSuggestionForThirdParty(s: Suggestion): s is Suggestion & { result: Thirdparty } {
return (s as any)?.result?.type === "thirdparty";
}
export function isSuggestionForHousehold(s: Suggestion): s is Suggestion & { result: Household } {
return (s as any)?.result?.type === "household";
}
export type AddPersonResult = Entities & { export type AddPersonResult = Entities & {
parent?: Entities | null; parent?: Entities | null;
}; };
@@ -398,7 +415,7 @@ export type AddPersonResult = Entities & {
export interface Suggestion { export interface Suggestion {
key: string; key: string;
relevance: number; relevance: number;
result: AddPersonResult; result: Entities;
} }
export interface SearchPagination { export interface SearchPagination {

View File

@@ -19,12 +19,27 @@
@close="closeModalChoose" @close="closeModalChoose"
@addNewPersons="(payload) => emit('addNewPersons', payload)" @addNewPersons="(payload) => emit('addNewPersons', payload)"
@onAskForCreate="onAskForCreate" @onAskForCreate="onAskForCreate"
@triggerAddContact="triggerAddContact"
/> />
<CreateModal <CreateModal
v-if="creatableEntityTypes.length > 0 && showModalCreate" v-if="creatableEntityTypes.length > 0 && showModalCreate && null == thirdPartyParentAddContact"
action="create"
:allowed-types="creatableEntityTypes" :allowed-types="creatableEntityTypes"
:query="query" :query="query"
:parent="null"
modalTitle="test"
@close="closeModalCreate"
@onPersonCreated="onPersonCreated"
></CreateModal>
<CreateModal
v-if="showModalCreate && thirdPartyParentAddContact !== null"
:allowed-types="['thirdparty']"
action="addContact"
modalTitle="test"
:parent="thirdPartyParentAddContact"
:query="''"
@close="closeModalCreate" @close="closeModalCreate"
@onPersonCreated="onPersonCreated" @onPersonCreated="onPersonCreated"
></CreateModal> ></CreateModal>
@@ -43,6 +58,7 @@ import type {
import { marked } from "marked"; import { marked } from "marked";
import options = marked.options; import options = marked.options;
import CreateModal from "ChillMainAssets/vuejs/OnTheFly/components/CreateModal.vue"; import CreateModal from "ChillMainAssets/vuejs/OnTheFly/components/CreateModal.vue";
import {ThirdpartyCompany} from "../../../../../ChillThirdPartyBundle/Resources/public/types";
interface AddPersonsConfig { interface AddPersonsConfig {
suggested?: Suggestion[]; suggested?: Suggestion[];
@@ -69,6 +85,8 @@ const emit =
const showModalChoose = ref(false); const showModalChoose = ref(false);
const showModalCreate = ref(false); const showModalCreate = ref(false);
const query = ref(""); const query = ref("");
const thirdPartyParentAddContact = ref<ThirdpartyCompany|null>(null);
const allowedTypesAddContact = ['addContact'];
const getClassButton = computed(() => { const getClassButton = computed(() => {
const size = props.options?.button?.size ?? ""; const size = props.options?.button?.size ?? "";
@@ -95,7 +113,7 @@ const creatableEntityTypes = computed<CreatableEntityType[]>(() => {
function onAskForCreate(payload: { query: string }) { function onAskForCreate(payload: { query: string }) {
query.value = payload.query; query.value = payload.query;
showModalChoose.value = false; closeModalChoose();
showModalCreate.value = true; showModalCreate.value = true;
} }
@@ -108,9 +126,19 @@ function closeModalChoose() {
} }
function closeModalCreate() { function closeModalCreate() {
if (null !== thirdPartyParentAddContact) {
thirdPartyParentAddContact.value = null;
}
showModalCreate.value = false; showModalCreate.value = false;
} }
function triggerAddContact({parent}: {parent: ThirdpartyCompany}) {
console.log("triggerAddContact", parent);
closeModalChoose();
thirdPartyParentAddContact.value = parent;
showModalCreate.value = true;
}
function onPersonCreated(payload: { person: Person }) { function onPersonCreated(payload: { person: Person }) {
console.log("onPersonCreated", payload); console.log("onPersonCreated", payload);
showModalCreate.value = false; showModalCreate.value = false;

View File

@@ -64,8 +64,8 @@
:item="item" :item="item"
:search="search" :search="search"
:type="checkUniq" :type="checkUniq"
@new-prior-suggestion="newPriorSuggestion"
@update-selected="updateSelected" @update-selected="updateSelected"
@trigger-add-contact="(payload) => emit('triggerAddContact', payload)"
/> />
<div <div
@@ -114,12 +114,11 @@ import {
import type { import type {
Suggestion, Suggestion,
Search, Search,
AddPersonResult as OriginalResult, EntityType,
SearchOptions, SearchOptions,
EntitiesOrMe, EntitiesOrMe,
} from "ChillPersonAssets/types"; } from "ChillPersonAssets/types";
import {ThirdpartyCompany} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
type Result = OriginalResult & { addressId?: number };
interface Props { interface Props {
modalTitle: string; modalTitle: string;
@@ -143,6 +142,7 @@ const emit = defineEmits<{
(e: "addNewPersons", payload: { selected: Suggestion[] }): void; (e: "addNewPersons", payload: { selected: Suggestion[] }): void;
(e: "onPickEntities", payload: { selected: EntitiesOrMe[] }): void; (e: "onPickEntities", payload: { selected: EntitiesOrMe[] }): void;
(e: "onAskForCreate", payload: { query: string }): void; (e: "onAskForCreate", payload: { query: string }): void;
(e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
}>(); }>();
const searchRef = ref<HTMLInputElement | null>(null); const searchRef = ref<HTMLInputElement | null>(null);
@@ -200,7 +200,6 @@ function addPriorSuggestion() {
if (hasPriorSuggestion.value) { if (hasPriorSuggestion.value) {
search.suggested.unshift(priorSuggestion.value as Suggestion); search.suggested.unshift(priorSuggestion.value as Suggestion);
search.selected.unshift(priorSuggestion.value as Suggestion); search.selected.unshift(priorSuggestion.value as Suggestion);
newPriorSuggestion(null);
} }
} }
@@ -285,17 +284,8 @@ function selectAll() {
}); });
} }
function newPriorSuggestion(entity: Result | null) { function triggerAddContact(payload: {parent: ThirdpartyCompany}) {
if (entity !== null) { emit("triggerAddContact", payload);
const suggestion: Suggestion = {
key: entity.type + entity.id,
relevance: 0.5,
result: entity,
} as Suggestion;
search.priorSuggestion = suggestion;
} else {
search.priorSuggestion = {};
}
} }
/** /**

View File

@@ -12,28 +12,28 @@
</div> </div>
<suggestion-person <suggestion-person
v-if="item.result.type === 'person'" v-if="isSuggestionForPerson(item)"
:item="item" :item="item"
></suggestion-person> ></suggestion-person>
<suggestion-third-party <suggestion-third-party
v-if="item.result.type === 'thirdparty'" v-if="isSuggestionForThirdParty(item)"
@newPriorSuggestion="newPriorSuggestion" @trigger-add-contact="triggerAddContact"
:item="item" :item="item"
></suggestion-third-party> ></suggestion-third-party>
<suggestion-user <suggestion-user
v-if="item.result.type === 'user'" v-if="isSuggestionForUser(item)"
:item="item" :item="item"
></suggestion-user> ></suggestion-user>
<suggestion-user-group <suggestion-user-group
v-if="item.result.type === 'user_group'" v-if="isSuggestionForUserGroup(item)"
:item="item" :item="item"
></suggestion-user-group> ></suggestion-user-group>
<suggestion-household <suggestion-household
v-if="item.result.type === 'household'" v-if="isSuggestionForHousehold(item)"
:item="item" :item="item"
></suggestion-household> ></suggestion-household>
</label> </label>
@@ -51,14 +51,24 @@ import SuggestionHousehold from "./TypeHousehold.vue";
import SuggestionUserGroup from "./TypeUserGroup.vue"; import SuggestionUserGroup from "./TypeUserGroup.vue";
// Types // Types
import { Result, Suggestion } from "ChillPersonAssets/types"; import {
isSuggestionForHousehold,
isSuggestionForPerson,
isSuggestionForThirdParty, isSuggestionForUser,
isSuggestionForUserGroup,
Suggestion
} from "ChillPersonAssets/types";
import {ThirdpartyCompany} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
const props = defineProps<{ const props = defineProps<{
item: Suggestion; item: Suggestion;
search: { selected: Suggestion[] }; search: { selected: Suggestion[] };
type: string; type: string;
}>(); }>();
const emit = defineEmits(["updateSelected", "newPriorSuggestion"]); const emit = defineEmits<{
(e: "updateSelected", payload: Suggestion[]): void;
(e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
}>();
// v-model for selected // v-model for selected
const selected = computed({ const selected = computed({
@@ -74,8 +84,8 @@ function setValueByType(value: Suggestion, type: string) {
return type === "radio" ? [value] : value; return type === "radio" ? [value] : value;
} }
function newPriorSuggestion(response: Result) { function triggerAddContact(payload: {parent: ThirdpartyCompany}) {
emit("newPriorSuggestion", response); emit("triggerAddContact", payload);
} }
</script> </script>

View File

@@ -16,9 +16,10 @@ import { defineProps } from "vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import HouseholdRenderBox from "ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue"; import HouseholdRenderBox from "ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue";
import { Suggestion } from "ChillPersonAssets/types"; import { Suggestion } from "ChillPersonAssets/types";
import {Household} from "ChillMainAssets/types";
interface TypeHouseholdProps { interface TypeHouseholdProps {
item: Suggestion; item: Suggestion & {result: Household};
} }
defineProps<TypeHouseholdProps>(); defineProps<TypeHouseholdProps>();

View File

@@ -23,7 +23,7 @@ import { computed, defineProps } from "vue";
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue"; import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue"; import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
import { Person } from "ChillPersonAssets/types"; import {Person, Suggestion} from "ChillPersonAssets/types";
function formatDate(dateString: string | undefined, format: string) { function formatDate(dateString: string | undefined, format: string) {
if (!dateString) return ""; if (!dateString) return "";
@@ -36,9 +36,7 @@ function formatDate(dateString: string | undefined, format: string) {
} }
const props = defineProps<{ const props = defineProps<{
item: { item: Suggestion & { result: Person },
result: Person; // add other fields as needed
};
}>(); }>();
const hasBirthdate = computed(() => props.item.result.birthdate !== null); const hasBirthdate = computed(() => props.item.result.birthdate !== null);

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="container tpartycontainer"> <div class="container tpartycontainer">
<div class="tparty-identification"> <div class="tparty-identification">
<span v-if="item.result.profession" class="profession">{{ <span v-if="(isThirdpartyChild(item.result) || isThirdpartyContact(item.result)) && item.result.profession" class="profession">{{
item.result.profession item.result.profession
}}</span> }}</span>
<span class="name"> {{ item.result.text }}&nbsp; </span> <span class="name"> {{ item.result.text }}&nbsp; </span>
@@ -12,20 +12,18 @@
</template> </template>
</span> </span>
</div> </div>
<div class="tpartyparent" v-if="hasParent"> <div class="tpartyparent" v-if="isThirdpartyChild(item.result) && null !== item.result.parent">
<span class="name"> &gt; {{ item.result.parent?.text }} </span> <span class="name"> &gt; {{ item.result.parent.text }} </span>
</div> </div>
</div> </div>
<div class="right_actions"> <div class="right_actions">
<badge-entity :entity="item.result" :options="{ displayLong: true }" /> <badge-entity :entity="item.result" :options="{ displayLong: true }" />
<on-the-fly <a
v-if="item.result.kind === 'company'" v-if="item.result.type === 'thirdparty' && item.result.kind === 'company'"
:parent="item.result" class="btn btn-tpchild"
@save-form-on-the-fly="saveFormOnTheFly" @click="emit('triggerAddContact', {parent: item.result})"
action="addContact" ><i class="bi bi-person-fill-add"></i></a>
ref="onTheFly"
/>
<on-the-fly type="thirdparty" :id="item.result.id" action="show" /> <on-the-fly type="thirdparty" :id="item.result.id" action="show" />
</div> </div>
</template> </template>
@@ -37,15 +35,19 @@ import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { useToast } from "vue-toast-notification"; import { useToast } from "vue-toast-notification";
import { Result, Suggestion } from "ChillPersonAssets/types"; import { Result, Suggestion } from "ChillPersonAssets/types";
import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types"; import {
isThirdpartyChild,
isThirdpartyContact,
Thirdparty, ThirdpartyCompany
} from "./../../../../../../ChillThirdPartyBundle/Resources/public/types";
interface TypeThirdPartyProps { interface TypeThirdPartyProps {
item: Suggestion; item: Suggestion & {result: Thirdparty};
} }
const props = defineProps<TypeThirdPartyProps>(); const props = defineProps<TypeThirdPartyProps>();
const emit = defineEmits<(e: "newPriorSuggestion", payload: unknown) => void>(); const emit = defineEmits<(e: "triggerAddContact", payload: {parent: ThirdpartyCompany}) => void>();
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null); const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
const toast = useToast(); const toast = useToast();
@@ -54,47 +56,23 @@ const hasAddress = computed(() => {
if (props.item.result.address !== null) { if (props.item.result.address !== null) {
return true; return true;
} }
if (props.item.result.parent !== null) { if (isThirdpartyChild(props.item.result) && props.item.result.parent !== null) {
if (props.item.result.parent) { return props.item.result.parent.address !== null;
return props.item.result.parent.address !== null;
}
} }
return false;
});
const hasParent = computed(() => { return false;
return props.item.result.parent !== null;
}); });
const getAddress = computed(() => { const getAddress = computed(() => {
if (props.item.result.address !== null) { if (props.item.result.address !== null) {
return props.item.result.address; return props.item.result.address;
} }
if (props.item.result.parent && props.item.result.parent.address !== null) { if (isThirdpartyChild(props.item.result) && props.item.result.parent !== null && props.item.result.parent.address !== null) {
return props.item.result.parent.address; return props.item.result.parent.address;
} }
return null; return null;
}); });
function saveFormOnTheFly({ data }: { data: Thirdparty }) {
makeFetch("POST", "/api/1.0/thirdparty/thirdparty.json", data)
.then((response: unknown) => {
const result = response as Result;
emit("newPriorSuggestion", result);
if (onTheFly.value) onTheFly.value.closeModal();
})
.catch((error: unknown) => {
const errorResponse = error as { name: string; violations: string[] };
if (errorResponse.name === "ValidationException") {
for (let v of errorResponse.violations) {
if (toast) toast.open({ message: v });
}
} else {
if (toast) toast.open({ message: "An error occurred" });
}
});
}
// i18n config (if needed elsewhere) // i18n config (if needed elsewhere)
const i18n = { const i18n = {
messages: { messages: {

View File

@@ -14,9 +14,10 @@ import { computed, defineProps } from "vue";
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue"; import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue"; import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
import { Suggestion } from "ChillPersonAssets/types"; import { Suggestion } from "ChillPersonAssets/types";
import {User} from "ChillMainAssets/types";
interface TypeUserProps { interface TypeUserProps {
item: Suggestion; item: Suggestion & {result: User};
} }
const props = defineProps<TypeUserProps>(); const props = defineProps<TypeUserProps>();

View File

@@ -15,7 +15,7 @@ import UserGroupRenderBox from "ChillMainAssets/vuejs/_components/Entity/UserGro
import { Suggestion } from "ChillPersonAssets/types"; import { Suggestion } from "ChillPersonAssets/types";
interface TypeUserGroupProps { interface TypeUserGroupProps {
item: Suggestion; item: Suggestion & {result: UserGroup};
} }
const props = defineProps<TypeUserGroupProps>(); const props = defineProps<TypeUserGroupProps>();

View File

@@ -21,7 +21,7 @@
</div> </div>
</div> </div>
<div v-else-if="action === 'edit' || action === 'create'"> <div v-else-if="props.action === 'edit' || props.action === 'create'">
<PersonEdit <PersonEdit
:id="props.id" :id="props.id"
:type="props.type" :type="props.type"
@@ -38,13 +38,13 @@ import PersonEdit from "./PersonEdit.vue";
import type { Person } from "ChillPersonAssets/types"; import type { Person } from "ChillPersonAssets/types";
interface Props { interface Props {
id: string | number; id: number;
type?: string; type?: string;
action: "show" | "edit" | "create"; action: "show" | "edit" | "create";
query?: string; query?: string;
} }
const props = defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {query: ""});
const person = ref<Person | null>(null); const person = ref<Person | null>(null);
@@ -52,7 +52,7 @@ function loadData(): void {
if (props.id === undefined || props.id === null) { if (props.id === undefined || props.id === null) {
return; return;
} }
const idNum = typeof props.id === "string" ? Number(props.id) : props.id; const idNum = props.id;
if (!Number.isFinite(idNum)) { if (!Number.isFinite(idNum)) {
return; return;
} }

View File

@@ -6,35 +6,96 @@ import {
User, User,
} from "ChillMainAssets/types"; } from "ChillMainAssets/types";
export interface Thirdparty { export type ThirdPartyKind = "contact" | "child" | "company";
export interface BaseThirdParty {
type: "thirdparty"; type: "thirdparty";
kind: ""|ThirdPartyKind;
text: string; text: string;
acronym: string | null; acronym: string | null;
active: boolean; active: boolean;
address: Address | null; address: Address | null;
canonicalized: string | null;
categories: ThirdpartyCategory[];
centers: Center[];
children: Thirdparty[];
civility: Civility | null;
comment: string | null;
contactDataAnonymous: boolean;
createdAt: DateTime; createdAt: DateTime;
createdBy: User | null; createdBy: User | null;
email: string | null; email: string | null;
firstname: string | null; firstname: string | null;
id: number | null; id: number;
kind: string;
name: string;
nameCompany: string | null; nameCompany: string | null;
parent: Thirdparty | null;
profession: string;
telephone: string | null; telephone: string | null;
thirdPartyTypes: ThirdpartyType[] | null;
updatedAt: DateTime | null; updatedAt: DateTime | null;
updatedBy: User | null; updatedBy: User | null;
} }
export interface ThirdpartyCompany extends BaseThirdParty {
kind: "company";
text: string;
acronym: string | null;
children: Thirdparty[];
categories: ThirdpartyCategory[];
thirdPartyTypes: ThirdpartyType[] | null;
address: Address | null;
}
// Type guard to distinguish a ThirdpartyCompany
export function isThirdpartyCompany(
t: BaseThirdParty
): t is ThirdpartyCompany {
return (
t.type === "thirdparty" &&
t.kind === "company"
);
}
export interface ThirdpartyChild extends BaseThirdParty {
kind: "child";
civility: Civility | null;
contactDataAnonymous: boolean;
parent: Thirdparty | null;
profession: string;
firstname: string;
/**
* the lastname for "Contact" and "Child", the name
*/
name: string;
comment: string | null;
}
// Type guard to distinguish a ThirdpartyChild
export function isThirdpartyChild(
t: BaseThirdParty
): t is ThirdpartyChild {
return (
t.type === "thirdparty" &&
t.kind === "child"
);
}
export interface ThirdpartyContact extends BaseThirdParty {
kind: "contact";
civility: Civility | null;
categories: ThirdpartyCategory[];
thirdPartyTypes: ThirdpartyType[] | null;
profession: string;
firstname: string;
/**
* the lastname for "Contact" and "Child", the name
*/
name: string;
address: Address | null;
}
// Type guard to distinguish a ThirdpartyContact
export function isThirdpartyContact(
t: BaseThirdParty
): t is ThirdpartyContact {
return (
t.type === "thirdparty" &&
t.kind === "contact"
);
}
export type Thirdparty = ThirdpartyCompany | ThirdpartyContact | ThirdpartyChild;
interface ThirdpartyType { interface ThirdpartyType {
key: string; key: string;
value: string; value: string;
@@ -47,3 +108,22 @@ export interface ThirdpartyCategory {
fr: string; fr: string;
}; };
} }
export interface ThirdPartyWrite {
readonly type: "thirdparty";
kind: ThirdPartyKind;
civility: Civility | null;
profession: string;
firstname: string;
/**
* the lastname
*/
name: string;
email: string;
telephone: string;
telephone2: string;
address: null|{
address_id: number;
}
comment: string;
}

View File

@@ -1,52 +0,0 @@
/*
* GET a thirdparty by id
*/
const getThirdparty = (id) => {
const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
/*
* POST a new thirdparty
*/
const postThirdparty = (body) => {
const url = `/api/1.0/thirdparty/thirdparty.json`;
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
/*
* PATCH an existing thirdparty
*/
const patchThirdparty = (id, body) => {
const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
return fetch(url, {
method: "PATCH",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify(body),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
export { getThirdparty, postThirdparty, patchThirdparty };

View File

@@ -0,0 +1,32 @@
/*
* GET a thirdparty by id
*/
import {Thirdparty, ThirdPartyWrite} from '../../types';
import {makeFetch} from "ChillMainAssets/lib/api/apiMethods";
export const getThirdparty = async (id: number) : Promise<Thirdparty> => {
const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
return fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw Error("Error with request resource response");
});
};
/*
* POST a new thirdparty
*/
export const createThirdParty = async (body: ThirdPartyWrite) => {
const url = `/api/1.0/thirdparty/thirdparty.json`;
return makeFetch<ThirdPartyWrite, Thirdparty>('POST', url, body);
};
/*
* PATCH an existing thirdparty
*/
export const patchThirdparty = async (id: number, body: ThirdPartyWrite) => {
const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
return makeFetch('PATCH', url, body);
};

View File

@@ -24,401 +24,35 @@
action === 'edit' || action === 'create' || action === 'addContact' action === 'edit' || action === 'create' || action === 'addContact'
" "
> >
<div v-if="parent"> <ThirdPartyEdit :id="id" :type="type" :action="action" :query="query" :parent="parent" />
<div class="parent-info">
<i class="fa fa-li fa-hand-o-right" />
<b class="me-2">{{ trans(THIRDPARTY_MESSAGES_CHILD_OF) }}</b>
<span class="chill-entity badge-thirdparty">{{ parent.text }}</span>
</div>
</div>
<div class="form-floating mb-3" v-else-if="kind.value !== 'child'">
<div class="form-check">
<input
class="form-check-input mt-0"
type="radio"
v-model="kind.value"
value="company"
id="tpartyKindInstitution"
/>
<label for="tpartyKindInstitution" class="required">
<badge-entity
:entity="{ type: 'thirdparty', kind: 'company' }"
:options="{ displayLong: true }"
/>
</label>
</div>
<div class="form-check">
<input
class="form-check-input mt-0"
type="radio"
v-model="kind.value"
value="contact"
id="tpartyKindContact"
/>
<label for="tpartyKindContact" class="required">
<badge-entity
:entity="{ type: 'thirdparty', kind: 'contact' }"
:options="{ displayLong: true }"
/>
</label>
</div>
</div>
<div v-else>
<p>Contact de&nbsp;:</p>
<third-party-render-box
:thirdparty="thirdparty.parent"
:options="{
addInfo: true,
addEntity: false,
addAltNames: true,
addId: false,
addLink: false,
addAge: false,
hLevel: 4,
addCenter: false,
addNoData: true,
isMultiline: false,
}"
/>
</div>
<div v-if="thirdparty.kind === 'child' || thirdparty.kind === 'contact'">
<div class="child-info">
<div class="input-group mb-3 input-section">
<select
class="form-select form-select-lg"
id="civility"
v-model="thirdparty.civility"
>
<option selected disabled :value="null">
{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY) }}
</option>
<option
v-for="civility in civilities"
:key="civility.id"
:value="civility"
>
{{ localizeString(civility.name) }}
</option>
</select>
</div>
<div class="input-group mb-3 input-section">
<input
class="form-control form-control-lg"
v-model="thirdparty.profession"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
aria-describedby="profession"
/>
</div>
</div>
<div class="child-info">
<div class="input-section">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="firstname"
v-model="thirdparty.firstname"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)"
/>
<label for="firstname">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('firstName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
</div>
<div class="input-section">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="name"
v-model="thirdparty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('name', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div v-if="thirdparty.kind === 'company'">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="name"
v-model="thirdparty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)
}}</label>
</div>
<div v-if="query">
<ul class="list-suggest add-items inline">
<li @click="addQuery(query)">
<span class="person-text">{{ query }}</span>
</li>
</ul>
</div>
</div>
<template v-if="thirdparty.kind !== 'child'">
<AddAddress
key="thirdparty"
:context="context"
:options="addAddress.options"
:address-changed-callback="submitAddress"
ref="addAddress"
/>
</template>
<div class="input-group mb-3">
<span class="input-group-text" id="email"
><i class="fa fa-fw fa-envelope"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdparty.email"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
aria-describedby="email"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber"
><i class="fa fa-fw fa-phone"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdparty.telephone"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
aria-describedby="phonenumber"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber2"
><i class="fa fa-fw fa-phone"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdparty.telephone2"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
aria-describedby="phonenumber2"
/>
</div>
<div v-if="parent">
<div class="input-group mb-3">
<span class="input-group-text" id="comment"
><i class="fa fa-fw fa-pencil"
/></span>
<textarea
class="form-control form-control-lg"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_COMMENT)"
v-model="thirdparty.comment"
/>
</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, computed, onMounted, getCurrentInstance } from "vue"; import { reactive, onMounted } from 'vue'
import ThirdPartyRenderBox from "../Entity/ThirdPartyRenderBox.vue"; import ThirdPartyRenderBox from '../Entity/ThirdPartyRenderBox.vue'
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress"; import ThirdPartyEdit from './ThirdPartyEdit.vue'
import { getThirdparty } from "../../_api/OnTheFly"; import { getThirdparty } from '../../_api/OnTheFly'
import BadgeEntity from "ChillMainAssets/vuejs/_components/BadgeEntity.vue";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { localizeString as _localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
import {
trans,
THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME,
THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME,
THIRDPARTY_MESSAGES_THIRDPARTY_NAME,
THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL,
THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER,
THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2,
THIRDPARTY_MESSAGES_THIRDPARTY_COMMENT,
THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION,
THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY,
THIRDPARTY_MESSAGES_CHILD_OF,
} from "translator";
// Props
const props = defineProps(["id", "type", "action", "query", "parent"]);
// Instance for $t and $toast const props = defineProps(['id', 'type', 'action', 'query', 'parent'])
const { proxy } = getCurrentInstance();
// State
const thirdparty = reactive({ const thirdparty = reactive({
type: "thirdparty", type: 'thirdparty',
address: null, })
kind: "company",
firstname: "",
name: "",
telephone: "",
telephone2: "",
civility: null,
profession: "",
comment: "",
parent: props.parent ? props.parent : undefined,
});
const civilities = ref([]);
const addAddress = reactive({
options: {
openPanesInModal: true,
onlyButton: false,
button: {
size: "btn-sm",
},
title: {
create: "add_an_address_title",
edit: "edit_address",
},
},
});
const addAddressRef = ref(null);
// Kind as computed ref
const kind = computed({
get() {
return thirdparty.kind !== undefined ? thirdparty.kind : "company";
},
set(v) {
thirdparty.kind = v;
},
});
// Context as computed
const context = computed(() => {
let ctx = {
target: {
name: props.type,
id: props.id,
},
edit: false,
addressId: null,
defaults: window.addaddress,
};
if (
!(thirdparty.address === undefined || thirdparty.address === null) &&
thirdparty.address.address_id !== null
) {
ctx.addressId = thirdparty.address.address_id;
ctx.edit = true;
}
return ctx;
});
// Query items
const queryItems = computed(() =>
props.query ? props.query.split(" ") : null,
);
// Methods
function localizeString(str) {
return _localizeString(str);
}
function loadData() { function loadData() {
return getThirdparty(props.id).then( if (!props.id) return Promise.resolve()
(tp) => return getThirdparty(props.id).then((tp) => {
new Promise((resolve) => { Object.assign(thirdparty, tp)
Object.assign(thirdparty, tp); })
thirdparty.kind = tp.kind;
if (props.action !== "show") {
if (tp.address !== null && addAddressRef.value) {
addAddressRef.value.getInitialAddress(tp.address.address_id);
}
}
resolve();
}),
);
} }
function loadCivilities() {
const url = `/api/1.0/main/civility.json`;
return makeFetch("GET", url)
.then((response) => {
civilities.value = response.results;
return Promise.resolve();
})
.catch((error) => {
console.log(error);
proxy.$toast.open({ message: error.body });
});
}
function submitAddress(payload) {
if (typeof payload.addressId !== "undefined") {
context.value.edit = true;
context.value.addressId = payload.addressId;
thirdparty.address = payload.address;
}
}
function addQueryItem(field, queryItem) {
switch (field) {
case "name":
if (thirdparty.name) {
thirdparty.name += ` ${queryItem}`;
} else {
thirdparty.name = queryItem;
}
break;
case "firstName":
thirdparty.firstname = queryItem;
break;
}
}
function addQuery(query) {
thirdparty.name = query;
}
// Lifecycle
onMounted(() => { onMounted(() => {
let dependencies = []; if (props.action === 'show' && props.id) {
dependencies.push(loadCivilities()); loadData()
if (props.action !== "create") {
if (props.id) {
dependencies.push(loadData());
}
if (props.action === "addContact") {
thirdparty.kind = "child";
thirdparty.address = null;
}
} else {
thirdparty.kind = "company";
} }
}); })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -0,0 +1,415 @@
<template>
<div>
<div v-if="parent">
<div class="parent-info">
<i class="fa fa-li fa-hand-o-right" />
<b class="me-2">{{ trans(THIRDPARTY_MESSAGES_CHILD_OF) }}</b>
<span class="chill-entity badge-thirdparty">{{ parent.text }}</span>
</div>
</div>
<div class="form-floating mb-3" v-else-if="kind !== 'child'">
<div class="form-check">
<input
class="form-check-input mt-0"
type="radio"
v-model="kind"
value="company"
id="tpartyKindInstitution"
/>
<label for="tpartyKindInstitution" class="required">
<badge-entity
:entity="{ type: 'thirdparty', kind: 'company' }"
:options="{ displayLong: true }"
/>
</label>
</div>
<div class="form-check">
<input
class="form-check-input mt-0"
type="radio"
v-model="kind"
value="contact"
id="tpartyKindContact"
/>
<label for="tpartyKindContact" class="required">
<badge-entity
:entity="{ type: 'thirdparty', kind: 'contact' }"
:options="{ displayLong: true }"
/>
</label>
</div>
</div>
<div v-else>
<p>Contact de&nbsp;:</p>
<third-party-render-box
:thirdparty="props.parent"
:options="{
addInfo: true,
addEntity: false,
addAltNames: true,
addId: false,
addLink: false,
addAge: false,
hLevel: 4,
addCenter: false,
addNoData: true,
isMultiline: false,
}"
/>
</div>
<div v-if="thirdParty.kind === 'child' || thirdParty.kind === 'contact'">
<div class="child-info">
<div class="input-group mb-3 input-section">
<select
class="form-select form-select-lg"
id="civility"
v-model="thirdParty.civility"
>
<option selected disabled :value="null">
{{ trans(THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY) }}
</option>
<option
v-for="civility in civilities"
:key="civility.id"
:value="civility"
>
{{ localizeString(civility.name) }}
</option>
</select>
</div>
<div class="input-group mb-3 input-section">
<input
class="form-control form-control-lg"
v-model="thirdParty.profession"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION)"
aria-describedby="profession"
/>
</div>
</div>
<div class="child-info">
<div class="input-section">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="firstname"
v-model="thirdParty.firstname"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)"
/>
<label for="firstname">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('firstName', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
</div>
<div class="input-section">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="name"
v-model="thirdParty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME)
}}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li
v-for="(qi, i) in queryItems"
:key="i"
@click="addQueryItem('name', qi)"
>
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div v-if="thirdParty.kind === 'company'">
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
id="name"
v-model="thirdParty.name"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)"
/>
<label for="name">{{
trans(THIRDPARTY_MESSAGES_THIRDPARTY_NAME)
}}</label>
</div>
<div v-if="query">
<ul class="list-suggest add-items inline">
<li @click="addQuery(query)">
<span class="person-text">{{ query }}</span>
</li>
</ul>
</div>
</div>
<template v-if="thirdParty.kind !== 'child'">
<AddAddress
key="thirdparty"
:context="context"
:options="addAddress.options"
:address-changed-callback="submitAddress"
ref="addAddressRef"
/>
</template>
<div class="input-group mb-3">
<span class="input-group-text" id="email"
><i class="fa fa-fw fa-envelope"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdParty.email"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL)"
aria-describedby="email"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber"
><i class="fa fa-fw fa-phone"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdParty.telephone"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER)"
aria-describedby="phonenumber"
/>
</div>
<div class="input-group mb-3">
<span class="input-group-text" id="phonenumber2"
><i class="fa fa-fw fa-phone"
/></span>
<input
class="form-control form-control-lg"
v-model="thirdParty.telephone2"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
:aria-label="trans(THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2)"
aria-describedby="phonenumber2"
/>
</div>
<div v-if="parent">
<div class="input-group mb-3">
<span class="input-group-text" id="comment"
><i class="fa fa-fw fa-pencil"
/></span>
<textarea
class="form-control form-control-lg"
:placeholder="trans(THIRDPARTY_MESSAGES_THIRDPARTY_COMMENT)"
v-model="thirdParty.comment"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
import ThirdPartyRenderBox from '../Entity/ThirdPartyRenderBox.vue'
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import {createThirdParty, getThirdparty} from '../../_api/OnTheFly'
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue'
import { localizeString as _localizeString } from 'ChillMainAssets/lib/localizationHelper/localizationHelper'
import {
trans,
THIRDPARTY_MESSAGES_THIRDPARTY_FIRSTNAME,
THIRDPARTY_MESSAGES_THIRDPARTY_LASTNAME,
THIRDPARTY_MESSAGES_THIRDPARTY_NAME,
THIRDPARTY_MESSAGES_THIRDPARTY_EMAIL,
THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER,
THIRDPARTY_MESSAGES_THIRDPARTY_PHONENUMBER2,
THIRDPARTY_MESSAGES_THIRDPARTY_COMMENT,
THIRDPARTY_MESSAGES_THIRDPARTY_PROFESSION,
THIRDPARTY_MESSAGES_THIRDPARTY_CIVILITY,
THIRDPARTY_MESSAGES_CHILD_OF, PERSON_EDIT_ERROR_WHILE_SAVING,
} from 'translator'
import {
createPerson,
getCivilities, WritePersonViolationMap,
} from "ChillPersonAssets/vuejs/_api/OnTheFly";
import {Thirdparty, ThirdPartyKind, ThirdPartyWrite} from "../../../types";
import {Civility} from "ChillMainAssets/types";
import {isValidationException} from "ChillMainAssets/lib/api/apiMethods";
interface ThirdPartyEditProps {
id?: number;
type?: string|null;
action: 'edit' | 'create' | 'addContact';
query?: string|null;
parent?: Thirdparty|null;
}
const props = withDefaults(defineProps<ThirdPartyEditProps>(), {type: null, query: null, parent: null});
const emit =
defineEmits<(e: "onThirdPartyCreated", payload: { thirdParty: Thirdparty }) => void>();
defineExpose({ postThirdParty });
const thirdParty = ref<ThirdPartyWrite>({
type: "thirdparty",
kind: 'company',
address: null,
civility: null,
email: "",
firstname: "",
name: "",
profession: "",
telephone: "",
telephone2: "",
comment: "",
});
const civilities = ref<Civility[]>([])
const addAddress = reactive({
options: {
openPanesInModal: true,
onlyButton: false,
button: { size: 'btn-sm' },
title: { create: 'add_an_address_title', edit: 'edit_address' },
},
})
const addAddressRef = ref<any>(null)
/**
* We need a specific computed for the kind
*/
const kind = computed<ThirdPartyKind>({
get() {
return thirdParty.value.kind;
},
set(v) {
thirdParty.value.kind = v
},
});
const context = computed(() => {
const ctx: any = {
target: { name: props.type, id: props.id },
edit: false,
addressId: null as number | null,
defaults: (window as any).addaddress,
}
if (thirdParty.value.address) {
ctx.addressId = thirdParty.value.address.address_id
ctx.edit = true
}
return ctx
})
/**
* Find the query items to display for suggestion
*/
const queryItems = computed(() => {
const words: null | string[] = props.query ? props.query.split(" ") : null;
if (null === words) {
return null;
}
const firstNameWords = (thirdParty.value.firstname || "")
.trim()
.toLowerCase()
.split(" ");
const lastNameWords = (thirdParty.value.name || "").trim().toLowerCase().split(" ");
return words
.filter((word) => !firstNameWords.includes(word.toLowerCase()))
.filter((word) => !lastNameWords.includes(word.toLowerCase()));
});
function localizeString(str: any) {
return _localizeString(str)
}
onMounted(() => {
getCivilities().then((cv) => {
civilities.value = cv;
});
if (props.action !== "create") {
loadData();
}
if (props.action === 'addContact') {
thirdParty.value.kind = 'child'
thirdParty.value.address = null
}
});
async function loadData(): Promise<void> {
if (!props.id) return Promise.resolve()
}
function submitAddress(payload: { address_id: number }) {
// TODO
}
function addQueryItem(field: 'name' | 'firstName', queryItem: string) {
switch (field) {
case 'name':
if (thirdParty.value.name) {
thirdParty.value.name += ` ${queryItem}`
} else {
thirdParty.value.name = queryItem
}
break
case 'firstName':
thirdParty.value.firstname = queryItem
break
}
}
function addQuery(query: string) {
thirdParty.value.name = query
}
async function postThirdParty(): Promise<void> {
try {
const tp = await createThirdParty(thirdParty.value);
emit('onThirdPartyCreated', { thirdParty: tp });
} catch (e: unknown) {
throw e;
}
}
</script>
<style scoped lang="scss">
.parent-info {
margin-bottom: 1rem;
}
.child-info {
display: flex;
justify-content: space-between;
.input-section {
width: 49%;
}
}
</style>

View File

@@ -68,6 +68,10 @@ Remove a contact: Supprimer
Contacts: Contacts Contacts: Contacts
No contacts associated: Aucun contact No contacts associated: Aucun contact
thirdparty:
addcontact: Ajouter un contact
addcontact_title: Ajouter un contact
No nameCompany given: Aucune raison sociale renseignée No nameCompany given: Aucune raison sociale renseignée
No acronym given: Aucun sigle renseigné No acronym given: Aucun sigle renseigné
No phone given: Aucun téléphone renseigné No phone given: Aucun téléphone renseigné