Merge branch 'master' into ticket-app-master

# Conflicts:
#	src/Bundle/ChillMainBundle/Export/Formatter/CSVFormatter.php
#	src/Bundle/ChillMainBundle/Export/Formatter/CSVListFormatter.php
#	src/Bundle/ChillMainBundle/Export/Formatter/SpreadsheetListFormatter.php
#	src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue
#	src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/GeographicalUnitStatAggregator.php
#	src/Bundle/ChillPersonBundle/Resources/public/types.ts
#	src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
This commit is contained in:
2025-07-09 13:44:23 +02:00
464 changed files with 14544 additions and 4119 deletions

View File

@@ -0,0 +1,18 @@
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import { ExportGeneration } from "ChillMainAssets/types";
export const fetchExportGenerationStatus = async (
exportGenerationId: string,
): Promise<ExportGeneration> =>
makeFetch(
"GET",
`/api/1.0/main/export-generation/${exportGenerationId}/object`,
);
export const generateFromSavedExport = async (
savedExportUuid: string,
): Promise<ExportGeneration> =>
makeFetch(
"POST",
`/api/1.0/main/export/export-generation/create-from-saved-export/${savedExportUuid}`,
);

View File

@@ -0,0 +1,3 @@
export function buildReturnPath(location: Location): string {
return location.pathname + location.search;
}

View File

@@ -12,6 +12,11 @@ function loadDynamicPicker(element) {
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
apps.forEach(function (el) {
let suggested;
let as_id;
let submit_on_adding_new_entity;
let label;
let isCurrentUserPicker;
const isMultiple = parseInt(el.dataset.multiple) === 1,
uniqId = el.dataset.uniqid,
input = element.querySelector(
@@ -22,12 +27,13 @@ function loadDynamicPicker(element) {
? JSON.parse(input.value)
: input.value === "[]" || input.value === ""
? null
: [JSON.parse(input.value)],
suggested = JSON.parse(el.dataset.suggested),
as_id = parseInt(el.dataset.asId) === 1,
submit_on_adding_new_entity =
parseInt(el.dataset.submitOnAddingNewEntity) === 1,
label = el.dataset.label;
: [JSON.parse(input.value)];
suggested = JSON.parse(el.dataset.suggested);
as_id = parseInt(el.dataset.asId) === 1;
submit_on_adding_new_entity =
parseInt(el.dataset.submitOnAddingNewEntity) === 1;
label = el.dataset.label;
isCurrentUserPicker = uniqId.startsWith("pick_user_or_me_dyn");
if (!isMultiple) {
if (input.value === "[]") {
@@ -44,6 +50,7 @@ function loadDynamicPicker(element) {
':uniqid="uniqid" ' +
':suggested="notPickedSuggested" ' +
':label="label" ' +
':isCurrentUserPicker="isCurrentUserPicker" ' +
'@addNewEntity="addNewEntity" ' +
'@removeEntity="removeEntity" ' +
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
@@ -61,6 +68,7 @@ function loadDynamicPicker(element) {
as_id,
submit_on_adding_new_entity,
label,
isCurrentUserPicker,
};
},
computed: {
@@ -89,7 +97,8 @@ function loadDynamicPicker(element) {
const ids = this.picked.map((el) => el.id);
input.value = ids.join(",");
}
console.log(entity);
console.log(this.picked);
// console.log(entity);
}
} else {
if (

View File

@@ -1,14 +0,0 @@
import { download_report } from "../../lib/download-report/download-report";
window.addEventListener("DOMContentLoaded", function (e) {
const export_generate_url = window.export_generate_url;
if (typeof export_generate_url === "undefined") {
console.error("Alias not found!");
throw new Error("Alias not found!");
}
const query = window.location.search,
container = document.querySelector("#download_container");
download_report(export_generate_url + query.toString(), container);
});

View File

@@ -1,4 +1,5 @@
import { GenericDoc } from "ChillDocStoreAssets/types/generic_doc";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
export interface DateTime {
datetime: string;
@@ -206,6 +207,16 @@ export interface WorkflowAttachment {
genericDoc: null | GenericDoc;
}
export interface ExportGeneration {
id: string;
type: "export_generation";
exportAlias: string;
createdBy: User | null;
createdAt: DateTime | null;
status: StoredObjectStatus;
storedObject: StoredObject;
}
export interface PrivateCommentEmbeddable {
comments: Record<number, string>;
}

View File

@@ -0,0 +1,141 @@
<script setup lang="ts">
import {
trans,
EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING,
EXPORT_GENERATION_TOO_MANY_RETRIES,
EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT,
EXPORT_GENERATION_EXPORT_READY,
} from "translator";
import { computed, onMounted, ref } from "vue";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
import { fetchExportGenerationStatus } from "ChillMainAssets/lib/api/export";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
import { ExportGeneration } from "ChillMainAssets/types";
interface AppProps {
exportGenerationId: string;
title: string;
createdDate: string;
}
const props = defineProps<AppProps>();
const exportGeneration = ref<ExportGeneration | null>(null);
const status = computed<StoredObjectStatus>(
() => exportGeneration.value?.status ?? "pending",
);
const storedObject = computed<null | StoredObject>(() => {
if (exportGeneration.value === null) {
return null;
}
return exportGeneration.value?.storedObject;
});
const isPending = computed<boolean>(() => status.value === "pending");
const isFetching = computed<boolean>(
() => tryiesForReady.value < maxTryiesForReady,
);
const isReady = computed<boolean>(() => status.value === "ready");
const isFailure = computed<boolean>(() => status.value === "failure");
const filename = computed<string>(() => `${props.title}-${props.createdDate}`);
/**
* counter for the number of times that we check for a new status
*/
let tryiesForReady = ref<number>(0);
/**
* how many times we may check for a new status, once loaded
*/
const maxTryiesForReady = 120;
const checkForReady = function (): void {
if (
"ready" === status.value ||
"empty" === status.value ||
"failure" === status.value ||
// stop reloading if the page stays opened for a long time
tryiesForReady.value > maxTryiesForReady
) {
return;
}
tryiesForReady.value = tryiesForReady.value + 1;
setTimeout(onObjectNewStatusCallback, 5000);
};
const onObjectNewStatusCallback = async function (): Promise<void> {
exportGeneration.value = await fetchExportGenerationStatus(
props.exportGenerationId,
);
if (isPending.value) {
checkForReady();
return Promise.resolve();
}
return Promise.resolve();
};
onMounted(() => {
onObjectNewStatusCallback();
});
</script>
<template>
<div id="waiting-screen">
<div
v-if="isPending && isFetching"
class="alert alert-danger text-center"
>
<div>
<p>
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
</p>
</div>
<div>
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-if="isPending && !isFetching" class="alert alert-info">
<div>
<p>
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
</p>
</div>
</div>
<div v-if="isFailure" class="alert alert-danger text-center">
<div>
<p>
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
</p>
</div>
</div>
<div v-if="isReady" class="alert alert-success text-center">
<div>
<p>
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
</p>
<p v-if="storedObject !== null">
<document-action-buttons-group
:stored-object="storedObject"
:filename="filename"
></document-action-buttons-group>
</p>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
#waiting-screen {
> .alert {
min-height: 350px;
}
}
</style>

View File

@@ -0,0 +1,15 @@
import { createApp } from "vue";
import App from "./App.vue";
const el = document.getElementById("app");
if (null === el) {
console.error("div element app was not found");
throw new Error("div element app was not found");
}
const exportGenerationId = el?.dataset.exportGenerationId as string;
const title = el?.dataset.exportTitle as string;
const createdDate = el?.dataset.exportGenerationDate as string;
createApp(App, { exportGenerationId, title, createdDate }).mount(el);

View File

@@ -2,7 +2,13 @@
<div class="grey-card">
<ul :class="listClasses" v-if="picked.length && displayPicked">
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type + p.id">
<span
v-if="'me' === p"
class="chill_denomination current-user updatedBy"
>{{ trans(USER_CURRENT_USER) }}</span
>
<span
v-else
:class="getBadgeClass(p)"
class="chill_denomination"
:style="getBadgeStyle(p)"
@@ -12,7 +18,18 @@
</li>
</ul>
<ul class="record_actions">
<li class="add-persons">
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
<label class="flex items-center gap-2">
<input
:checked="picked.indexOf('me') >= 0 ? true : null"
ref="itsMeCheckbox"
:type="multiple ? 'checkbox' : 'radio'"
@change="selectItsMe"
/>
{{ trans(USER_CURRENT_USER) }}
</label>
</li>
<li class="add-persons">
<add-persons
:options="addPersonsOptions"
:key="uniqid"
@@ -44,6 +61,7 @@ import {
PICK_ENTITY_USER_GROUP,
PICK_ENTITY_PERSON,
PICK_ENTITY_THIRDPARTY,
USER_CURRENT_USER,
trans,
} from "translator";
import { addNewEntities } from "ChillMainAssets/types";
@@ -62,6 +80,7 @@ const props = defineProps<{
displayPicked?: boolean;
suggested?: Entities[];
label?: string;
isCurrentUserPicker: boolean // must default to false
}>();
const emits = defineEmits<{
@@ -70,6 +89,7 @@ const emits = defineEmits<{
(e: "addNewEntityProcessEnded"): void;
}>();
const itsMeCheckbox = ref(null);
const addPersons = ref();
const addPersonsOptions = computed(
@@ -118,10 +138,13 @@ const translatedListOfTypes = computed(() => {
const listClasses = computed(() => ({
"badge-suggest": true,
"remove-items": props.removableIfSet !== false,
"remove-items": props.removableIfSet,
inline: true,
}));
const selectItsMe = (event) =>
event.target.checked ? addNewSuggested("me") : removeEntity("me");
function addNewSuggested(entity: Entities) {
emits("addNewEntity", { entity });
}
@@ -135,30 +158,32 @@ function addNewEntity({ selected }: addNewEntities) {
emits("addNewEntityProcessEnded");
}
function removeEntity(entity: Entities) {
if (props.removableIfSet === false) {
return;
}
emits("removeEntity", { entity });
}
const removeEntity = (entity) => {
if (!props.removableIfSet) return;
if (entity === "me" && itsMeCheckbox.value) {
itsMeCheckbox.value.checked = false;
}
emits("removeEntity", { entity });
};
function getBadgeClass(entities: Entities) {
if (entities.type !== "user_group") {
return entities.type;
}
return "";
if (entities.type !== "user_group") {
return entities.type;
}
return "";
}
function getBadgeStyle(entities: Entities) {
if (entities.type === "user_group") {
return [
`ul.badge-suggest li > span {
if (entities.type === "user_group") {
return [
`ul.badge-suggest li > span {
color: ${entities.foregroundColor}!important;
border-bottom-color: ${entities.backgroundColor};
}`,
];
}
return [];
];
}
return [];
}
</script>
@@ -265,4 +290,8 @@ ul.badge-suggest li > span.person {
ul.badge-suggest li > span.thirdparty {
border-bottom-color: rgb(198.9, 72, 98.1);
}
.current-user {
color: var(--bs-body-color);
background-color: var(--bs-chill-l-gray) !important;
}
</style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import GenerateButton from "ChillMainAssets/vuejs/SavedExportButtons/Component/GenerateButton.vue";
interface SavedExportButtonsConfig {
savedExportUuid: string;
savedExportAlias: string;
}
const props = defineProps<SavedExportButtonsConfig>();
</script>
<template>
<generate-button
:saved-export-uuid="props.savedExportUuid"
></generate-button>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,186 @@
<script setup lang="ts">
import {
trans,
SAVED_EXPORT_EXECUTE,
EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING_SHORT,
EXPORT_GENERATION_TOO_MANY_RETRIES,
EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT,
EXPORT_GENERATION_EXPORT_READY,
} from "translator";
import {
fetchExportGenerationStatus,
generateFromSavedExport,
} from "ChillMainAssets/lib/api/export";
import { computed, ref } from "vue";
import { ExportGeneration } from "ChillMainAssets/types";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
import { useToast } from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
interface SavedExportButtonGenerateConfig {
savedExportUuid: string;
}
const props = defineProps<SavedExportButtonGenerateConfig>();
const emits = defineEmits<{
(e: "generate");
}>();
const toast = useToast();
const exportGeneration = ref<ExportGeneration | null>(null);
const status = computed<StoredObjectStatus | "inactive">(
() => exportGeneration.value?.status ?? "inactive",
);
const storedObject = computed<null | StoredObject>(() => {
if (exportGeneration.value === null) {
return null;
}
return exportGeneration.value?.storedObject;
});
const isInactive = computed<boolean>(() => status.value === "inactive");
const isPending = computed<boolean>(() => status.value === "pending");
const isFetching = computed<boolean>(
() => tryiesForReady.value < maxTryiesForReady,
);
const isReady = computed<boolean>(() => status.value === "ready");
const isFailure = computed<boolean>(() => status.value === "failure");
const filename = computed<string>(() => {
if (null === exportGeneration.value) {
return "";
}
return `${exportGeneration.value?.storedObject.title}-${exportGeneration.value?.createdAt?.datetime8601}`;
});
const externalDownloadLink = computed<string>(
() => `/fr/main/export-generation/${exportGeneration.value?.id}/wait`,
);
const classes = computed<Record<string, boolean>>(() => {
return {};
});
const buttonClasses = computed<Record<string, boolean>>(() => {
return { btn: true, "btn-outline-primary": true };
});
/**
* counter for the number of times that we check for a new status
*/
let tryiesForReady = ref<number>(0);
/**
* how many times we may check for a new status, once loaded
*/
const maxTryiesForReady = 120;
const checkForReady = function (): void {
if (
"ready" === status.value ||
"empty" === status.value ||
"failure" === status.value ||
// stop reloading if the page stays opened for a long time
tryiesForReady.value > maxTryiesForReady
) {
return;
}
tryiesForReady.value = tryiesForReady.value + 1;
setTimeout(
onObjectNewStatusCallback,
tryiesForReady.value < 10 ? 1500 : 5000,
);
};
const onExportGenerationSuccess = function (): void {
toast.success(trans(EXPORT_GENERATION_EXPORT_READY));
};
const onObjectNewStatusCallback = async function (): Promise<void> {
if (null === exportGeneration.value) {
checkForReady();
return Promise.resolve();
}
const newExportGeneration = await fetchExportGenerationStatus(
exportGeneration.value?.id,
);
if (newExportGeneration.status !== exportGeneration.value.status) {
if (newExportGeneration.status === "ready") {
onExportGenerationSuccess();
}
}
exportGeneration.value = newExportGeneration;
if (isPending.value) {
checkForReady();
return Promise.resolve();
}
return Promise.resolve();
};
const onClickGenerate = async (): Promise<void> => {
emits("generate");
exportGeneration.value = await generateFromSavedExport(
props.savedExportUuid,
);
onObjectNewStatusCallback();
return Promise.resolve();
};
</script>
<template>
<button
v-if="isInactive"
:class="buttonClasses"
type="button"
@click="onClickGenerate"
>
<i class="fa fa-cog"></i> {{ trans(SAVED_EXPORT_EXECUTE) }}
</button>
<template v-if="isPending && isFetching">
<span class="btn">
<i class="fa fa-cog fa-spin fa-fw"></i>
<span class="pending-message">{{
trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING_SHORT)
}}</span>
<a :href="externalDownloadLink" class="externalDownloadLink">
<i class="bi bi-box-arrow-up-right"></i>
</a>
</span>
</template>
<div v-if="isPending && !isFetching" :class="buttonClasses">
<span>{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}</span>
</div>
<download-button
v-else-if="isReady && storedObject?.currentVersion !== null"
:classes="buttonClasses"
:stored-object="storedObject"
:at-version="storedObject?.currentVersion"
:filename="filename"
></download-button>
<div v-else-if="isFailure" :class="classes">
<span class="btn">
<i class="bi bi-exclamation-triangle"></i>
<span class="pending-message">{{
trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT)
}}</span>
</span>
</div>
</template>
<style scoped lang="scss">
.pending-message {
font-style: italic;
}
.externalDownloadLink {
margin-left: 1rem;
}
</style>

View File

@@ -0,0 +1,13 @@
import { createApp } from "vue";
import App from "./App.vue";
const buttons = document.querySelectorAll<HTMLDivElement>(
"[data-generate-export-button]",
);
buttons.forEach((button) => {
const savedExportUuid = button.dataset.savedExportUuid as string;
createApp(App, { savedExportUuid, savedExportAlias: "" }).mount(button);
});