Merge remote-tracking branch 'origin/master' into cire16

This commit is contained in:
2022-12-22 10:22:58 +01:00
801 changed files with 39243 additions and 6591 deletions

View File

@@ -80,7 +80,9 @@ header {
margin: 0;
padding: 0;
border-radius: 0;
z-index: 1500;
a.dropdown-item {
padding: 0.5rem 1rem;
width: 120%;
border: 0;
border-bottom: 1px solid $gray-200;
@@ -517,3 +519,16 @@ div.popover {
div.v-toast {
z-index: 10000!important;
}
// export index page
div.exports-list {
div.flex-bloc .item-bloc {
flex-basis: 33%;
@include media-breakpoint-down(lg) { flex-basis: 50%; }
@include media-breakpoint-down(sm) { flex-basis: 100%; }
div:last-child,
p:last-child {
margin-top: auto;
}
}
}

View File

@@ -24,7 +24,7 @@ require('./chillmain.scss');
import { chill } from './js/chill.js';
global.chill = chill;
require('./js/date.js');
require('./js/date');
require('./js/counter.js');
/// Load fonts

View File

@@ -12,7 +12,7 @@
* Do not take time into account
*
*/
const dateToISO = (date) => {
export const dateToISO = (date: Date|null): string|null => {
if (null === date) {
return null;
}
@@ -29,7 +29,7 @@ const dateToISO = (date) => {
*
* **Experimental**
*/
const ISOToDate = (str) => {
export const ISOToDate = (str: string|null): Date|null => {
if (null === str) {
return null;
}
@@ -38,25 +38,25 @@ const ISOToDate = (str) => {
}
let
[year, month, day] = str.split('-');
[year, month, day] = str.split('-').map(p => parseInt(p));
return new Date(year, month-1, day);
return new Date(year, month-1, day, 0, 0, 0, 0);
}
/**
* Return a date object from iso string formatted as YYYY-mm-dd:HH:MM:ss+01:00
*
*/
const ISOToDatetime = (str) => {
export const ISOToDatetime = (str: string|null): Date|null => {
if (null === str) {
return null;
}
let
[cal, times] = str.split('T'),
[year, month, date] = cal.split('-'),
[year, month, date] = cal.split('-').map(s => parseInt(s)),
[time, timezone] = times.split(times.charAt(8)),
[hours, minutes, seconds] = time.split(':')
[hours, minutes, seconds] = time.split(':').map(s => parseInt(s));
;
return new Date(year, month-1, date, hours, minutes, seconds);
@@ -66,7 +66,7 @@ const ISOToDatetime = (str) => {
* Convert a date to ISO8601, valid for usage in api
*
*/
const datetimeToISO = (date) => {
export const datetimeToISO = (date: Date): string => {
let cal, time, offset;
cal = [
date.getFullYear(),
@@ -92,7 +92,7 @@ const datetimeToISO = (date) => {
return x;
};
const intervalDaysToISO = (days) => {
export const intervalDaysToISO = (days: number|string|null): string => {
if (null === days) {
return 'P0D';
}
@@ -100,7 +100,7 @@ const intervalDaysToISO = (days) => {
return `P${days}D`;
}
const intervalISOToDays = (str) => {
export const intervalISOToDays = (str: string|null): number|null => {
if (null === str) {
return null
}
@@ -154,12 +154,3 @@ const intervalISOToDays = (str) => {
return days;
}
export {
dateToISO,
ISOToDate,
ISOToDatetime,
datetimeToISO,
intervalISOToDays,
intervalDaysToISO,
};

View File

@@ -5,6 +5,10 @@ ul.record_actions {
justify-content: flex-end;
padding: 0.5em 0;
&.inline {
display: inline-block;
}
&.column {
flex-direction: column;
}
@@ -18,6 +22,13 @@ ul.record_actions {
padding-right: 1em;
}
&.small {
.btn {
padding: .25rem .5rem;
font-size: .75rem;
}
}
li {
display: inline-block;
list-style-type: none;

View File

@@ -0,0 +1,4 @@
export function fetchResults<T>(uri: string, params: {item_per_page?: number}): Promise<T[]>;
export function makeFetch<T, B>(method: "GET"|"POST"|"PATCH"|"DELETE", url: string, body: B, options: {[key: string]: string}): Promise<T>;

View File

@@ -1,110 +0,0 @@
/**
* Generic api method that can be adapted to any fetch request
*/
const makeFetch = (method, url, body, options) => {
let opts = {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: (body !== null) ? JSON.stringify(body) : null
};
if (typeof options !== 'undefined') {
opts = Object.assign(opts, options);
}
return fetch(url, opts)
.then(response => {
if (response.ok) {
return response.json();
}
if (response.status === 422) {
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
throw AccessException(response);
}
throw {
name: 'Exception',
sta: response.status,
txt: response.statusText,
err: new Error(),
violations: response.body
};
});
}
/**
* Fetch results with certain parameters
*/
const _fetchAction = (page, uri, params) => {
const item_per_page = 50;
if (params === undefined) {
params = {};
}
let url = uri + '?' + new URLSearchParams({ item_per_page, page, ...params });
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then(response => {
if (response.ok) { return response.json(); }
throw Error({ m: response.statusText });
});
};
const fetchResults = async (uri, params) => {
let promises = [],
page = 1;
let firstData = await _fetchAction(page, uri, params);
promises.push(Promise.resolve(firstData.results));
if (firstData.pagination.more) {
do {
page = ++page;
promises.push(_fetchAction(page, uri, params).then(r => Promise.resolve(r.results)));
} while (page * firstData.pagination.items_per_page < firstData.count)
}
return Promise.all(promises).then(values => values.flat());
};
const fetchScopes = () => {
return fetchResults('/api/1.0/main/scope.json');
};
/**
* Error objects to be thrown
*/
const ValidationException = (response) => {
const error = {};
error.name = 'ValidationException';
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
return error;
}
const AccessException = (response) => {
const error = {};
error.name = 'AccessException';
error.violations = ['You are not allowed to perform this action'];
return error;
}
export {
makeFetch,
fetchResults,
fetchScopes
}

View File

@@ -0,0 +1,223 @@
import {Scope} from '../../types';
export type body = {[key: string]: boolean|string|number|null};
export type fetchOption = {[key: string]: boolean|string|number|null};
export interface Params {
[key: string]: number|string
}
export interface PaginationResponse<T> {
pagination: {
more: boolean;
items_per_page: number;
};
results: T[];
count: number;
}
export interface FetchParams {
[K: string]: string|number|null;
};
export interface TransportExceptionInterface {
name: string;
}
export interface ValidationExceptionInterface extends TransportExceptionInterface {
name: 'ValidationException';
error: object;
violations: string[];
titles: string[];
propertyPaths: string[];
}
export interface ValidationErrorResponse extends TransportExceptionInterface {
violations: {
title: string;
propertyPath: string;
}[];
}
export interface AccessExceptionInterface extends TransportExceptionInterface {
name: 'AccessException';
violations: string[];
}
export interface NotFoundExceptionInterface extends TransportExceptionInterface {
name: 'NotFoundException';
}
export interface ServerExceptionInterface extends TransportExceptionInterface {
name: 'ServerException';
message: string;
code: number;
body: string;
}
/**
* Generic api method that can be adapted to any fetch request
*/
export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DELETE', url: string, body?: body | Input | null, options?: FetchParams): Promise<Output> => {
let opts = {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
};
if (body !== null || typeof body !== 'undefined') {
Object.assign(opts, {body: JSON.stringify(body)})
}
if (typeof options !== 'undefined') {
opts = Object.assign(opts, options);
}
return fetch(url, opts)
.then(response => {
if (response.ok) {
return response.json();
}
if (response.status === 422) {
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
throw AccessException(response);
}
throw {
name: 'Exception',
sta: response.status,
txt: response.statusText,
err: new Error(),
violations: response.body
};
});
}
/**
* Fetch results with certain parameters
*/
function _fetchAction<T>(page: number, uri: string, params?: FetchParams): Promise<PaginationResponse<T>> {
const item_per_page: number = 50;
let searchParams = new URLSearchParams();
searchParams.append('item_per_page', item_per_page.toString());
searchParams.append('page', page.toString());
if (params !== undefined) {
Object.keys(params).forEach(key => {
let v = params[key];
if (typeof v === 'string') {
searchParams.append(key, v);
} else if (typeof v === 'number') {
searchParams.append(key, v.toString());
} else if (v === null) {
searchParams.append(key, '');
}
});
}
let url = uri + '?' + searchParams.toString();
return fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then((response) => {
if (response.ok) { return response.json(); }
if (response.status === 404) {
throw NotFoundException(response);
}
if (response.status === 422) {
return response.json().then(response => {
throw ValidationException(response)
});
}
if (response.status === 403) {
throw AccessException(response);
}
if (response.status >= 500) {
return response.text().then(body => {
throw ServerException(response.status, body);
});
}
throw new Error("other network error");
}).catch((reason: any) => {
console.error(reason);
throw new Error(reason);
});
};
export const fetchResults = async<T> (uri: string, params?: FetchParams): Promise<T[]> => {
let promises: Promise<T[]>[] = [],
page = 1;
let firstData: PaginationResponse<T> = await _fetchAction(page, uri, params) as PaginationResponse<T>;
promises.push(Promise.resolve(firstData.results));
if (firstData.pagination.more) {
do {
page = ++page;
promises.push(
_fetchAction<T>(page, uri, params)
.then(r => Promise.resolve(r.results))
);
} while (page * firstData.pagination.items_per_page < firstData.count)
}
return Promise.all(promises).then((values) => values.flat());
};
export const fetchScopes = (): Promise<Scope[]> => {
return fetchResults('/api/1.0/main/scope.json');
};
/**
* Error objects to be thrown
*/
const ValidationException = (response: ValidationErrorResponse): ValidationExceptionInterface => {
const error = {} as ValidationExceptionInterface;
error.name = 'ValidationException';
error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
return error;
}
const AccessException = (response: Response): AccessExceptionInterface => {
const error = {} as AccessExceptionInterface;
error.name = 'AccessException';
error.violations = ['You are not allowed to perform this action'];
return error;
}
const NotFoundException = (response: Response): NotFoundExceptionInterface => {
const error = {} as NotFoundExceptionInterface;
error.name = 'NotFoundException';
return error;
}
const ServerException = (code: number, body: string): ServerExceptionInterface => {
const error = {} as ServerExceptionInterface;
error.name = 'ServerException';
error.code = code;
error.body = body;
return error;
}

View File

@@ -0,0 +1,6 @@
import {fetchResults} from "./apiMethods";
import {Location, LocationType} from "../../types";
export const getLocations = (): Promise<Location[]> => fetchResults('/api/1.0/main/location.json');
export const getLocationTypes = (): Promise<LocationType[]> => fetchResults('/api/1.0/main/location-type.json');

View File

@@ -0,0 +1,25 @@
import {User} from "../../types";
import {makeFetch} from "./apiMethods";
export const whoami = (): Promise<User> => {
const url = `/api/1.0/main/whoami.json`;
return fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw {
msg: 'Error while getting whoami.',
sta: response.status,
txt: response.statusText,
err: new Error(),
body: response.body
};
});
};
export const whereami = (): Promise<Location | null> => {
const url = `/api/1.0/main/user-current-location.json`;
return makeFetch<null, Location|null>("GET", url);
}

View File

@@ -1,4 +1,4 @@
/*
/*
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
@@ -15,12 +15,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var mime = require('mime-types')
var mime = require('mime')
var download_report = (url, container) => {
var download_text = container.dataset.downloadText,
alias = container.dataset.alias;
window.fetch(url, { credentials: 'same-origin' })
.then(response => {
if (!response.ok) {
@@ -29,21 +29,21 @@ var download_report = (url, container) => {
return response.blob();
}).then(blob => {
var content = URL.createObjectURL(blob),
link = document.createElement("a"),
type = blob.type,
hasForcedType = 'mimeType' in container.dataset,
extension;
if (hasForcedType) {
// force a type
type = container.dataset.mimeType;
blob = new Blob([ blob ], { 'type': type });
content = URL.createObjectURL(blob);
}
extension = mime.extension(type);
extension = mime.getExtension(type);
link.appendChild(document.createTextNode(download_text));
link.classList.add("btn", "btn-action");
@@ -56,7 +56,7 @@ var download_report = (url, container) => {
container.appendChild(link);
}).catch(function(error) {
console.log(error);
var problem_text =
var problem_text =
document.createTextNode("Problem during download");
container
@@ -64,4 +64,4 @@ var download_report = (url, container) => {
});
};
module.exports = download_report;
module.exports = download_report;

View File

@@ -14,9 +14,11 @@
// 4. Include any default map overrides here
@import "custom/_maps";
@import "bootstrap/scss/maps";
// 5. Include remainder of required parts
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/root";

View File

@@ -6,6 +6,7 @@ import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
const i18n = _createI18n(appMessages);
let appsOnPage = new Map();
let appsPerInput = new Map();
function loadDynamicPicker(element) {
@@ -78,13 +79,14 @@ function loadDynamicPicker(element) {
.mount(el);
appsOnPage.set(uniqId, app);
appsPerInput.set(input.name, app);
});
}
document.addEventListener('show-hide-show', function(e) {
loadDynamicPicker(e.detail.container)
})
});
document.addEventListener('show-hide-hide', function(e) {
console.log('hiding event caught')
@@ -95,13 +97,25 @@ document.addEventListener('show-hide-hide', function(e) {
appsOnPage.delete(uniqId);
}
})
})
});
document.addEventListener('pick-entity-type-action', function (e) {
console.log('pick entity event', e);
if (!appsPerInput.has(e.detail.name)) {
console.error('no app with this name');
return;
}
const app = appsPerInput.get(e.detail.name);
if (e.detail.action === 'add') {
app.addNewEntity(e.detail.entity);
} else if (e.detail.action === 'remove') {
app.removeEntity(e.detail.entity);
} else {
console.error('action not supported: '+e.detail.action);
}
});
document.addEventListener('DOMContentLoaded', function(e) {
loadDynamicPicker(document)
})

View File

@@ -0,0 +1,28 @@
import {ShowHide} from 'ChillMainAssets/lib/show_hide/index';
document.addEventListener('DOMContentLoaded', function(_e) {
console.log('pick-rolling-date');
document.querySelectorAll('div[data-rolling-date]').forEach( (picker) => {
const
roll_wrapper = picker.querySelector('div.roll-wrapper'),
fixed_wrapper = picker.querySelector('div.fixed-wrapper');
new ShowHide({
froms: [roll_wrapper],
container: [fixed_wrapper],
test: function (elems) {
console.log('testing');
console.log('elems', elems);
for (let el of elems) {
for (let select_roll of el.querySelectorAll('select[data-roll-picker]')) {
console.log('select_roll', select_roll);
console.log('value', select_roll.value);
return select_roll.value === 'fixed_date';
}
}
return false;
}
})
});
});

View File

@@ -0,0 +1,139 @@
export interface DateTime {
datetime: string;
datetime8601: string
}
export interface Civility {
id: number;
// TODO
}
export interface Job {
id: number;
type: "user_job";
label: {
"fr": string; // could have other key. How to do that in ts ?
}
}
export interface Center {
id: number;
type: "center";
name: string;
}
export interface Scope {
id: number;
type: "scope";
name: {
"fr": string
}
}
export interface User {
type: "user";
id: number;
username: string;
text: string;
email: string;
user_job: Job;
label: string;
// todo: mainCenter; mainJob; etc..
}
export interface UserAssociatedInterface {
type: "user";
id: number;
};
export type TranslatableString = {
fr?: string;
nl?: string;
}
export interface Postcode {
id: number;
name: string;
code: string;
center: Point;
}
export type Point = {
type: "Point";
coordinates: [lat: number, lon: number];
}
export interface Country {
id: number;
name: TranslatableString;
code: string;
}
export interface Address {
type: "address";
address_id: number;
text: string;
street: string;
streetNumber: string;
postcode: Postcode;
country: Country;
floor: string | null;
corridor: string | null;
steps: string | null;
flat: string | null;
buildingName: string | null;
distribution: string | null;
extra: string | null;
confidential: boolean;
lines: string[];
addressReference: AddressReference | null;
validFrom: DateTime;
validTo: DateTime | null;
}
export interface AddressReference {
id: number;
createdAt: DateTime | null;
deletedAt: DateTime | null;
municipalityCode: string;
point: Point;
postcode: Postcode;
refId: string;
source: string;
street: string;
streetNumber: string;
updatedAt: DateTime | null;
}
export interface Location {
type: "location";
id: number;
active: boolean;
address: Address | null;
availableForUsers: boolean;
createdAt: DateTime | null;
createdBy: User | null;
updatedAt: DateTime | null;
updatedBy: User | null;
email: string | null
name: string;
phonenumber1: string | null;
phonenumber2: string | null;
locationType: LocationType;
}
export interface LocationAssociated {
type: "location";
id: number;
}
export interface LocationType {
type: "location-type";
id: number;
active: boolean;
addressRequired: "optional" | "required";
availableForUsers: boolean;
editableByUsers: boolean;
contactData: "optional" | "required";
title: TranslatableString;
}

View File

@@ -54,7 +54,7 @@
</template>
<script>
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date.js';
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import ActionButtons from './ActionButtons.vue';

View File

@@ -146,6 +146,9 @@ export default {
}
},
titleCreate() {
if (typeof this.allowedTypes === 'undefined') {
return 'onthefly.create.title.default';
}
return this.allowedTypes.every(t => t === 'person')
? 'onthefly.create.title.person'
: this.allowedTypes.every(t => t === 'thirdparty')

View File

@@ -1,5 +1,5 @@
<template>
<ul class="list-suggest remove-items" v-if="picked.length">
<ul :class="listClasses" v-if="picked.length && displayPicked">
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
<span class="chill_denomination">{{ p.text }}</span>
</li>
@@ -40,6 +40,15 @@ export default {
uniqid: {
type: String,
required: true,
},
removableIfSet: {
type: Boolean,
default: true,
},
displayPicked: {
// display picked entities.
type: Boolean,
default: true,
}
},
emits: ['addNewEntity', 'removeEntity'],
@@ -78,7 +87,13 @@ export default {
} else {
return appMessages.fr.pick_entity.modal_title_one + trans.join(', ');
}
}
},
listClasses() {
return {
'list-suggest': true,
'remove-items': this.$props.removableIfSet,
};
},
},
methods: {
addNewEntity({ selected, modal }) {
@@ -90,6 +105,9 @@ export default {
modal.showModal = false;
},
removeEntity(entity) {
if (!this.$props.removableIfSet) {
return;
}
this.$emit('removeEntity', entity);
}
},

View File

@@ -20,7 +20,7 @@
</template>
<script>
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.ts';
export default {
name: "EntityWorkflowVueSubscriber",

View File

@@ -2,57 +2,65 @@
<transition name="modal">
<div class="modal-mask">
<!-- :: styles bootstrap :: -->
<div class="modal-dialog" :class="modalDialogClass">
<div class="modal fade show" style="display: block" aria-modal="true" role="dialog">
<div class="modal-dialog" :class="modalDialogClass">
<div class="modal-content">
<div class="modal-header">
<slot name="header"></slot>
<button class="close btn" @click="$emit('close')">
<i class="fa fa-times" aria-hidden="true"></i></button>
</div>
<div class="body-head">
<div class="modal-header">
<slot name="header"></slot>
<button class="close btn" @click="$emit('close')">
<i class="fa fa-times" aria-hidden="true"></i></button>
</div>
<div class="modal-body">
<div class="body-head">
<slot name="body-head"></slot>
</div>
<div class="modal-body">
<slot name="body"></slot>
</div>
<div class="modal-footer" v-if="!hideFooter">
<button class="btn btn-cancel" @click="$emit('close')">{{ $t('action.close') }}</button>
<slot name="footer"></slot>
</div>
</div>
<slot name="body"></slot>
</div>
<div class="modal-footer" v-if="!hideFooter">
<button class="btn btn-cancel" @click="$emit('close')">{{ $t('action.close') }}</button>
<slot name="footer"></slot>
</div>
</div>
</div>
</div>
</div>
<!-- :: end styles bootstrap :: -->
</div>
</transition>
</template>
<script>
<script lang="ts">
import {defineComponent} from "vue";
/*
* This Modal component is a mix between Vue3 modal implementation
* [+] with 'v-if:showModal' directive:parameter, html scope is added/removed not just shown/hidden
* [+] with slot we can pass content from parent component
* [+] some classes are passed from parent component
* and Bootstrap 4.6 _modal.scss module
* and Bootstrap 5 _modal.scss module
* [+] using bootstrap css classes, the modal have a responsive behaviour,
* [+] modal design can be configured using css classes (size, scroll)
*/
export default {
export default defineComponent({
name: 'Modal',
props: {
modalDialogClass: {
type: String,
required: false
type: Object,
required: false,
default: {},
},
hideFooter: {
type: Boolean,
required: false
required: false,
default: false
}
},
emits: ['close']
}
});
</script>
<style lang="scss">
/**
* This is a mask behind the modal.
*/
.modal-mask {
position: fixed;
z-index: 9998;

View File

@@ -41,7 +41,7 @@
</template>
<script>
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.ts';
export default {
name: "NotificationReadToggle",

View File

@@ -1,27 +1,6 @@
import { createI18n } from 'vue-i18n'
import { createI18n } from 'vue-i18n';
import datetimeFormats from '../i18n/datetimeFormats';
const datetimeFormats = {
fr: {
short: {
year: "numeric",
month: "numeric",
day: "numeric"
},
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: false
}
}
};
const messages = {
fr: {
action: {
@@ -76,11 +55,13 @@ const messages = {
}
};
const _createI18n = (appMessages) => {
const _createI18n = (appMessages: any, legacy?: boolean) => {
Object.assign(messages.fr, appMessages.fr);
return createI18n({
legacy: typeof legacy === undefined ? true : legacy,
locale: 'fr',
fallbackLocale: 'fr',
// @ts-ignore
datetimeFormats,
messages,
})

View File

@@ -0,0 +1,27 @@
export default {
fr: {
short: {
year: "numeric",
month: "numeric",
day: "numeric"
},
text: {
year: "numeric",
month: "long",
day: "numeric",
},
long: {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: false
},
hoursOnly: {
hour: "numeric",
minute: "numeric",
hour12: false,
}
}
};

View File

@@ -0,0 +1,6 @@
<h6>
<a href="{{ path('chill_main_export_index') }}" title="{{ 'Back to the list'|trans }}">
<i class="fa fa-folder-open-o fa-fw"></i>
</a>
{{ export_group|trans }}
</h6>

View File

@@ -0,0 +1,12 @@
<ul class="nav nav-pills justify-content-center">
<li class="nav-item">
<a href="{{ chill_path_forward_return_path('chill_main_export_index') }}" class="nav-link {% if current == 'common' %}active{% endif %}">
{{ 'Exports list'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ chill_path_forward_return_path('chill_main_export_saved_list_my') }}" class="nav-link {% if current == 'my' %}active{% endif %}">
{{ 'saved_export.My saved exports'|trans }}
</a>
</li>
</ul>

View File

@@ -36,10 +36,7 @@ window.addEventListener("DOMContentLoaded", function(e) {
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>
<h2>{{ "Download export"|trans }}</h2>
@@ -52,5 +49,14 @@ window.addEventListener("DOMContentLoaded", function(e) {
data-download-text="{{ "Download your report"|trans|escape('html_attr') }}"
><span id="waiting_text">{{ "Waiting for your report"|trans ~ '...' }}</span></div>
</div>
<ul class="record_actions sticky-form-buttons">
<li class="cancel"><a href="{{ chill_return_path_or('chill_main_export_index') }}" class="btn btn-cancel">{{ 'Back to the list'|trans }}</a></li>
{% if not app.request.query.has('prevent_save') %}
<li>
<a href="{{ chill_path_add_return_path('chill_main_export_save_from_key', { alias: alias, key: app.request.query.get('key')}) }}" class="btn btn-save">{{ 'Save'|trans }}</a>
</li>
{% endif %}
</ul>
</div>
{% endblock content %}

View File

@@ -22,23 +22,27 @@
{% block content %}
<div class="col-md-10">
<h1>{{ 'Exports list'|trans }}</h1>
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'common'}) }}
<div class="col-md-10 exports-list">
<div class="container mt-4">
{% for group, exports in grouped_exports %}{% if group != '_' %}
<h2 class="display-6">{{ group|trans }}</h2>
<div class="row grouped">
<div class="row flex-bloc">
{% for export_alias, export in exports %}
<div class="col-6 col-md-4 mb-3">
<h2>{{ export.title|trans }}</h2>
<p>{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
<div class="item-bloc">
<div class="item-row card-body">
<h2 class="card-title">{{ export.title|trans }}</h2>
<p class="card-text my-3">{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
</div>
</div>
{% endfor %}
</div>
@@ -48,17 +52,19 @@
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
{% endif %}
<div class="row ungrouped">
<div class="row flex-bloc">
{% for export_alias,export in grouped_exports['_'] %}
<div class="col-6 col-md-4 mb-3">
<h2>{{ export.title|trans }}</h2>
<p>{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
<div class="item-bloc">
<div class="item-row card-body">
<h2 class="card-title">{{ export.title|trans }}</h2>
<p class="card-text my-3">{{ export.description|trans }}</p>
<p>
<a class="btn btn-action" href="{{ path('chill_main_export_new', { 'alias': export_alias } ) }}">
{{ 'Create an export'|trans }}
</a>
</p>
</div>
</div>
{% endfor %}

View File

@@ -20,17 +20,24 @@
{% block title %}{{ export.title|trans }}{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{{ encore_entry_link_tags('mod_pick_rolling_date') }}
{% endblock %}
{% block js %}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{{ encore_entry_script_tags('page_export') }}
{% if export_alias == 'count_social_work_actions' %}
{{ encore_entry_script_tags('vue_export_action_goal_result') }}
{% endif %}
{{ encore_entry_script_tags('mod_pick_rolling_date') }}
{% endblock js %}
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>

View File

@@ -22,11 +22,8 @@
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>

View File

@@ -23,10 +23,7 @@
{% block content %}
<div class="col-md-10">
<h6>
<i class="fa fa-folder-open-o fa-fw"></i>
{{ export_group|trans }}
</h6>
{{ include('@ChillMain/Export/_breadcrumb.html.twig') }}
<h1>{{ export.title|trans }}</h1>
@@ -36,19 +33,21 @@
<section class="formatter mb-4">
<h2>{{ 'Formatter'| trans }}</h2>
<div>
{% if form.children.formatter.children|length == 0 %}
<p>
<span class="chill-no-data-statement">{{ "No options availables. Your report is fully configured."|trans }}</span>
</p>
{{ form_widget(form.children.formatter) }}
{% else %}
{# we always have to render children, to mark as rendered #}
{% for input in form.children.formatter.children %}
{{ form_row(input) }}
{% endfor %}
<div class="container py-4">
{# we always have to render children, to mark as rendered #}
{% for input in form.children.formatter.children %}
<div class="row">
{{ form_row(input) }}
</div>
{% endfor %}
</div>
{% endif %}
</div>
</section>
<div class="mb-4">

View File

@@ -10,6 +10,30 @@
</div>
{% endif %}
</div>
{% if form.dateRanges is defined %}
{% if form.dateRanges|length > 0 %}
{% for dateRangeName, _o in form.dateRanges %}
<div class="row gx-2 justify-content-center">
{% if form.dateRanges[dateRangeName].vars.label is not same as(false) %}
<div class="col-md-5">
{{ form_label(form.dateRanges[dateRangeName])}}
</div>
{% endif %}
<div class="col-md-6">
<div class="input-group mb-3">
<span class="input-group-text">{{ 'chill_calendar.From'|trans }}</span>
{{ form_widget(form.dateRanges[dateRangeName]['from']) }}
<span class="input-group-text">{{ 'chill_calendar.To'|trans }}</span>
{{ form_widget(form.dateRanges[dateRangeName]['to']) }}
</div>
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-misc"><i class="fa fa-filter"></i></button>
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.checkboxes is defined %}
{% if form.checkboxes|length > 0 %}
{% for checkbox_name, options in form.checkboxes %}

View File

@@ -233,9 +233,22 @@
{% endif %}
{% endblock %}
{% block pick_entity_dynamic_row %}
<div class="row">
<div class="col-md-12">
{{ form_label(form) }}
{{ form_help(form) }}
</div>
</div>
<div class="row justify-content-end">
<div class="col-md-7 col-sm-12">
{{ form_widget(form) }}
</div>
</div>
{% endblock %}
{% block pick_entity_dynamic_widget %}
{{ form_help(form)}}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}
@@ -244,3 +257,16 @@
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-postal-code" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}
{% block pick_rolling_date_widget %}
<div data-rolling-date="{{ form.vars['uniqid'] }}" class="row">
<div class="roll-wrapper col-sm-6">
{{ form_widget(form.roll, { 'attr': { 'data-roll-picker': 'data-roll-picker'}}) }}
{{ form_errors(form.roll) }}
</div>
<div class="fixed-wrapper col-sm-6">
{{ form_widget(form.fixedDate) }}
{{ form_errors(form.fixedDate) }}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title 'saved_export.Delete saved ?'|trans %}
{% block display_content %}
<div class="col-10">
<h3>{{ saved_export.title }}</h3>
<p>{{ saved_export.description|chill_markdown_to_html }}</p>
</div>
{% endblock %}
{% block content %}
<div class="container chill-md-10">
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'saved_export.Delete saved ?'|trans,
'confirm_question' : 'saved_export.Are you sure you want to delete this saved ?'|trans,
'display_content' : block('display_content'),
'cancel_route' : 'chill_main_export_saved_list_my',
'cancel_parameters' : {},
'form' : delete_form
} ) }}
</div>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.Edit'|trans }}{% endblock %}
{% block content %}
<div class="col-10">
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.description) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@@ -0,0 +1,78 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
{% block content %}
<div class="col-md-10 exports-list">
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
<div class="container mt-4">
{% if total == 0 %}
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>
{% endif %}
{% for group, saveds in grouped_exports %}
{% if group != '_' %}
<h2 class="display-6">{{ group }}</h2>
<div class="row flex-bloc">
{% for s in saveds %}
<div class="item-bloc">
<div class="item-row card-body">
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<h2 class="card-title">{{ s.saved.title }}</h2>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<div class="card-text my-3">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div>
<ul class="record_actions">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% if grouped_exports|keys|length > 1 and grouped_exports['_']|default([])|length > 0 %}
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
{% endif %}
<div class="row flex-bloc">
{% for saveds in grouped_exports['_']|default([]) %}
{% for s in saveds %}
<div class="item-bloc">
<div class="item-row card-body">
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<h2 class="card-title">{{ s.saved.title }}</h2>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<div class="card-text my-3">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div>
<ul class="record_actions">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
</ul>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title %}{{ 'saved_export.New'|trans }}{% endblock %}
{% block content %}
<div class="col-10">
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.description) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
</div>
{% endblock %}