Merge branch 'issue-32-allow-images-in-documents' into 'develop'

Issue 32 allow images in documents

Closes #32

See merge request ldc-web/projets/startcoq!24
This commit is contained in:
Vincent BRIDIER 2023-10-20 14:23:22 +00:00
commit a230697de0
9 changed files with 339 additions and 427 deletions

View file

@ -11,7 +11,7 @@ use App\Enums\Products\RangeEnum;
use App\Enums\Products\RayEnum;
use App\Enums\Products\SourceEnum;
use App\Lib\Helpers\ToolboxHelper;
use App\Traits\Requests\HasDocument;
use App\Traits\Requests\HasDocumentAndPictureList;
use App\Traits\Requests\HasPicture;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Enum;
@ -20,7 +20,7 @@ use Illuminate\Validation\ValidationException;
class StoreProductRequest extends FormRequest
{
use HasPicture;
use HasDocument;
use HasDocumentAndPictureList;
/**
* Determine if the user is authorized to make this request.
@ -110,7 +110,7 @@ class StoreProductRequest extends FormRequest
return \array_merge(
$rules,
$this->pictureRules('picture', false, 1200, 900, 1200, 900),
$this->documentsRules('docs', false),
$this->documentsRules('docs'),
);
}

View file

@ -4,24 +4,43 @@ namespace App\Traits\Requests;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
trait HasDocument
trait HasDocumentAndPictureList
{
/**
* Autorised mimtypes
*
* @var string[]
*/
private $autorisedMimTypes = [
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
'text/plain',
];
/**
* Images rules
*
* @param string $name
* @param boolean $required
* @param string $name
* @return array
*/
public function documentsRules(
string $name = 'documents',
?bool $required = null
string $name = 'docs',
): array {
return [
"{$name}" => ($required ? 'required' : 'nullable') . '|array',
"{$name}" => 'present|array',
"{$name}.*" => [
$required ? 'required' : 'nullable',
'required',
function (string $attribute, $value, callable $fail) {
return $this->validateDocument($attribute, $value, $fail);
}
@ -40,29 +59,32 @@ trait HasDocument
*/
private function validateDocument(string $attribute, mixed $value, callable $fail)
{
// phpcs:enable
if (!($useStorage = Storage::exists($value)) and !File::exists($value)) {
$fail(trans(":attribute n'existe pas dans le système de fichier"));
return;
}
if (
!\in_array($this->getUploadedFileMimeType($value, $useStorage), [
'application/pdf'
])
) {
$fail(trans(":attribute doit être une image de type 'application/pdf'"));
if (!\in_array($this->getUploadedFileMimeType($value, $useStorage), $this->autorisedMimTypes)) {
$fail(trans(':attribute doit être un document de type :mimetypes', [
'mimetypes' => Str::of($this->autorisedMimTypes)->implode(' ou ')
]));
}
$maxSize = 6 * 1024 * 1024;
if (File::size($value) > $maxSize) {
$fail(trans(':attribute doit être inférieur à :size Mo', ['size' => $maxSize / (1024 * 1024)]));
}
}
/**
* Merges documents fields with request data.
* Merges docs fields with request data.
*
* @param string $name
* @return void
*/
protected function mergeDocuments(string $name = 'documents'): void
protected function mergeDocuments(string $name = 'docs'): void
{
$documents = $this->{$name};
$this->merge(['doc' => $this->docs ?? []]);
$this->merge([$name => $this->{$name} ?? []]);
$this->merge(["{$name}Validate" => []]);
if (is_array($documents)) {
foreach ($documents as $key => $documentPath) {

View file

@ -138,6 +138,11 @@ trait HasPicture
) {
$fail(trans(":attribute doit être une image de type 'image/jpeg,image/jpg,image/png,image/gif'"));
}
$filesize = \filesize($fileFullPath);
$maxSize = 6 * 1024 * 1024;
if ($filesize === false || $filesize > $maxSize) {
$fail(trans(':attribute doit être inférieur à :size Mo', ['size' => $maxSize / (1024 * 1024)]));
}
if (!($dimensions = \getimagesize($fileFullPath)) or !isset($dimensions[0]) or !isset($dimensions[1])) {
$fail(trans(":attribute impossible de valider les dimensions"));
} else {

View file

@ -26,3 +26,9 @@
display: block;
}
}
.pagination-custom .dropdown-menu {
overflow: auto;
min-width: 5rem;
max-height: 178px;
}

View file

@ -7,11 +7,11 @@
<div class="col-12 d-flex form-group">
<div class="input-group">
<button
@click.prevent="chooseFiles"
@click.prevent="($refs.actualDocuments as HTMLInputElement).click()"
class="btn btn-secondary"
type="button"
:title="__('Modifier les documents sources')"
data-bs-tooltip="tooltip"
data-bs-toggle="tooltip"
:disabled="intValues.length >= intItemLimit[1]"
>
<i class="fa-solid fa-folder-open" />
@ -19,16 +19,16 @@
<input
:id="intId"
ref="actualDocuments"
@change="changedDocuments"
@change="loadDocuments([].slice.call(($refs.actualDocuments as HTMLInputElement).files))"
type="file"
class="d-none"
name="multipleInputDocuments"
accept=".pdf"
:accept="intAccepts"
:aria-describedby="`Help${intId}`"
multiple
>
<input
@click.prevent="chooseFiles"
@click.prevent="($refs.actualDocuments as HTMLInputElement).click()"
type="text"
:value="
'Actuellement ' +
@ -39,7 +39,7 @@
"
class="form-control right-aligned"
:title="__('Modifier les documents sources')"
data-bs-tooltip="tooltip"
data-bs-toggle="tooltip"
:aria-describedby="`Help${intId}`"
:disabled="intValues.length >= intItemLimit[1]"
readonly
@ -50,7 +50,7 @@
class="btn btn-danger"
type="button"
:title="__('Supprimer tous les documents')"
data-bs-tooltip="tooltip"
data-bs-toggle="tooltip"
:disabled="intValues.length <= 0"
>
<i class="fas fa-eraser" />
@ -61,150 +61,89 @@
<!-- End main buttons -->
<!-- Documents list -->
<div
id="multiple-documents"
v-for="(value, valueIndex) of intValues"
:key="valueIndex"
class="row w-100 mx-auto my-2"
>
<div class="row w-100 mx-auto my-2">
<div class="col-12 form-group text-center px-1">
<div
v-for="n = 0 in intValues.length"
:key="n"
class="col-12 form-group text-center px-1"
class="d-flex justify-content-between align-items-center pb-1"
:class="valueIndex != 1 ? 'border-top' : ''"
>
<div
class="d-flex justify-content-between align-items-center pb-1"
:class="n != 1 ? 'border-top' : ''"
<label
v-if="value"
class="col-form-label d-flex"
>
<label
v-if="intValues[n - 1]"
class="col-form-label d-flex"
<span
class="text-start text-ellipsis"
v-if="intOriginalFileName[valueIndex]"
>
<span class="text-start text-ellipsis">
{{ intOriginalFileName[n - 1] }}
</span>
</label>
<small
v-if="intValues[n - 1]"
:id="`PreviewHelp${intId}`"
class="form-text text-muted d-block m-0"
>
<span v-if="intOriginalFile[n - 1]">
{{ humanFileSize(intOriginalFile[n - 1].size) }}.
</span>
</small>
<div class="input-group justify-content-center mt-1 w-auto">
<button
@click.prevent="viewDocumentSource(n - 1)"
class="btn btn-warning"
type="button"
data-bs-toggle="modal"
:data-bs-target="`#modalViewer${intId}`"
:title="__('Afficher le document')"
data-bs-tooltip="tooltip"
>
<i class="fa-solid fa-eye" />
</button>
<button
@click.prevent="chooseAFile(n - 1)"
class="btn btn-secondary"
type="button"
:title="__('Modifier le document source')"
data-bs-tooltip="tooltip"
>
<i class="fa-solid fa-folder-open" />
</button>
<input
:id="intId"
ref="actualDocument"
@change="changedDocument($event.target as HTMLInputElement, n - 1)"
type="file"
class="d-none"
:name="intName.replace('[]', '') + `[${n - 1}]`"
accept=".pdf"
:aria-describedby="`Help${intId}`"
:disabled="intPreviousFiles.has(intOriginalFileName[n - 1])"
>
<input
@change="changedDocument(getInput(intId), n - 1)"
type="text"
class="d-none"
:name="intName.replace('[]', '') + `[${n - 1}]`"
:aria-describedby="`Help${intId}`"
:disabled="!intPreviousFiles.has(intOriginalFileName[n - 1])"
:value="intValues[n - 1]"
>
<button
v-if="intValues.length > intItemLimit[0]"
@click.prevent="removeFile(n - 1)"
class="btn btn-danger"
type="button"
:title="__('Supprimer le document')"
data-bs-tooltip="tooltip"
>
<i class="fas fa-trash" />
</button>
</div>
</div>
<p class="text-danger m-0">
{{
parseValidationInput(
`${intName.replace(/[\[\]']+/g, "")}.${n - 1}`,
intAllErrors
)
}}
</p>
</div>
<!-- Viewer modal -->
<div
:id="`modalViewer${intId}`"
class="modal"
tabindex="-1"
role="dialog"
data-bs-backdrop="static"
data-bs-keyboard="false"
>
<div
class="modal-dialog modal-xl d-flex align-items-center h-100 my-0 py-4"
{{ intOriginalFileName[valueIndex].split('.').pop() }}
&nbsp;
</span>
<span class="text-start text-ellipsis">
{{ intOriginalFileName[valueIndex] }}
</span>
</label>
<small
v-if="value"
:id="`PreviewHelp${intId}`"
class="form-text text-muted d-block m-0"
>
<div class="modal-content h-100">
<div class="modal-header">
<h5 class="modal-title">
{{ __("Prévisualisation du document") }}
</h5>
<button
@click.prevent="removeDocumentSource"
type="button"
class="btn-close"
data-bs-dismiss="modal"
:aria-label="__('Fermer la fenêtre')"
:title="__('Fermer la fenêtre')"
data-bs-tooltip="tooltip"
/>
</div>
<div class="modal-body text-center">
<div
v-if="intViewerLoadDocument"
class="text-center w-100 h-100"
>
<div
class="spinner-border text-secondary"
role="status"
>
<span class="visually-hidden">
{{ __("Chargement du document") }}
</span>
</div>
</div>
<object
class="w-100 h-100"
:class="intViewerLoadDocument ? 'd-none' : ''"
type="application/pdf"
data=""
ref="documentViewer"
:title="__('Document enregistré')"
/>
</div>
</div>
<span v-if="intOriginalFile[valueIndex]">
{{ humanFileSize(intOriginalFile[valueIndex].size) }}.
</span>
</small>
<div class="input-group justify-content-center mt-1 w-auto">
<button
@click.prevent="viewDocumentSource(valueIndex)"
class="btn btn-warning"
type="button"
:title="__('Afficher le document')"
data-bs-toggle="tooltip"
>
<i class="fa-solid fa-eye" />
</button>
<input
:id="intId"
ref="actualDocument"
@change="changedDocument(($refs.actualDocument as HTMLInputElement[])[valueIndex], valueIndex)"
type="file"
class="d-none"
:name="intName.replace('[]', '') + `[${valueIndex}]`"
:accept="intAccepts"
:aria-describedby="`Help${intId}`"
:disabled="intPreviousFiles.has(intOriginalFileName[valueIndex])"
>
<input
@change="changedDocument(($refs.actualDocument as HTMLInputElement[])[valueIndex], valueIndex)"
type="text"
class="d-none"
:name="intName.replace('[]', '') + `[${valueIndex}]`"
:aria-describedby="`Help${intId}`"
:disabled="!intPreviousFiles.has(intOriginalFileName[valueIndex])"
:value="intValues[valueIndex]"
>
<button
v-if="intValues.length > intItemLimit[0]"
@click.prevent="removeFile(valueIndex)"
class="btn btn-danger"
type="button"
:title="__('Supprimer le document')"
data-bs-toggle="tooltip"
>
<i class="fas fa-trash" />
</button>
</div>
</div>
<p class="text-danger m-0">
{{
parseValidationInput(
`${intName.replace(/[\[\]']+/g, "")}.${n - 1}`,
intAllErrors
)
}}
</p>
</div>
</div>
<!-- End documents list -->
@ -218,8 +157,8 @@
</template>
<script lang="ts">
import { Tooltip } from "bootstrap";
import { defineComponent } from "vue";
import bsTooltips from "./../modules/bsTooltips";
import error from "./../modules/parseLaravelErrorMessages";
import trans from "./../modules/trans";
@ -235,38 +174,36 @@ const BYTES_PER_KB = 1024;
export default defineComponent({
name: "MultipleDocumentsInputComponent",
mixins: [error, trans],
mixins: [error, trans, bsTooltips],
components: {
},
data(): {
/** The file ext accepted */
intAccepts: string;
/** The file ext accepted */
intId: string;
intName: string;
intHasDocument: boolean[];
intHasDocuments: boolean;
intPreviousFiles: Set<string>;
intOriginalFile: File[];
intOriginalFileName: string[];
intValues: string[];
intHelper: string;
intTooltipList: HTMLButtonElement[];
intLoopLoadDocuments: number;
intViewerLoadDocument: boolean;
intInputDocuments: HTMLInputElement[];
intItemLimit: number[];
intAllErrors: Record<string, string[]>;
} {
return {
intAccepts: ".xls,.csv,.xlsx,.png,.jpg,.jpeg,.gif,.doc,.docx,.ppt,.pdf,.txt",
intId: "",
intName: "",
intHasDocument: [],
intHasDocuments: false,
intPreviousFiles: new Set<string>(),
intOriginalFile: [],
intOriginalFileName: [],
intValues: [],
intHelper: "",
intTooltipList: [],
intLoopLoadDocuments: 0,
intViewerLoadDocument: false,
intInputDocuments: [],
intItemLimit: [0, 0],
intAllErrors: {},
};
@ -282,154 +219,87 @@ export default defineComponent({
this.intAllErrors = data.errors ?? {};
this.$nextTick(() => {
this.initComponent();
this.updateBootstrapTooltip();
});
this.$nextTick(() => {
this.initTooltips();
});
},
computed: {
documentInputs(): HTMLInputElement[] {
return this.$refs.actualDocument as HTMLInputElement[];
}
},
methods: {
/**
* Initialise the component with previously documents registered.
*/
/** Initialize the component with previously documents registered. */
initComponent() {
if (this.intValues.length > 0) {
this.intValues.forEach((element, index) => {
element.charAt(0) != "/" ? "/".concat(element) : "";
this.intValues.splice(index, 1, element.replace(/^\/|^/, "/"));
});
this.intHasDocument = Array(this.intValues.length).fill(false);
this.intHasDocuments = true;
this.initDocumentsSaved();
} else {
this.intHasDocuments = false;
}
},
/**
* Assign previously documents registered to respective input.
*/
/** Assign previously documents registered to respective input. */
initDocumentsSaved() {
var actualDocuments = this.$refs.actualDocument as HTMLInputElement[];
this.intValues.forEach((element, index) => {
const documentName = element.substring(element.lastIndexOf("/") + 1);
this.intPreviousFiles.add(documentName);
this.createFile(
element,
`${documentName}`,
"application/pdf"
`${documentName}`
).then((file: File) => {
const container = new DataTransfer();
container.items.add(file);
actualDocuments[index].files = container.files;
this.documentInputs[index].files = container.files;
this.intOriginalFile[index] = container.files[0];
this.intOriginalFileName[index] = container.files[0].name;
});
});
},
/**
* Create a file from a path.
*/
async createFile(path: string, name: string, type: string): Promise<File> {
let response = await fetch(
/** Create a file from a path. */
async createFile(path: string, name: string): Promise<File> {
const response = await fetch(
window.location.protocol + "//" + window.location.host + "/" + path
);
let data = await response.blob();
let metadata = {
const data = await response.blob();
const type = await response.headers.get("content-type") ?? "application/pdf";
const metadata = {
type: type,
};
return new File([data], name, metadata);
},
/**
* Update Bootstrap tooltips.
*/
updateBootstrapTooltip() {
let newTooltipList = [].slice.call(
document.querySelectorAll(
".multiple-documents-input-" +
this.intId +
" [data-bs-tooltip=\"tooltip\"]"
)
) as HTMLButtonElement[];
let tmp = newTooltipList.filter((x) => !this.intTooltipList.includes(x));
tmp.map((tooltip) => {
return new Tooltip(tooltip);
});
this.intTooltipList = newTooltipList;
this.closeBootstrapTooltip();
},
/**
* Close all Bootstrap tooltips.
*/
closeBootstrapTooltip() {
this.intTooltipList.forEach((tooltip) => {
tooltip.blur();
Tooltip.getInstance(tooltip)?.hide();
});
},
/**
* Link button change document to the input file document.
*/
chooseAFile(n: number) {
var actualDocuments = this.$refs.actualDocument as HTMLInputElement[],
actualDocument = actualDocuments[n] as HTMLInputElement;
actualDocument.click();
},
/**
* Link button change documents to the input file documents.
*/
chooseFiles() {
const actualDocuments = this.$refs.actualDocuments as HTMLInputElement;
actualDocuments.click();
},
/**
* Remove one specific document.
*/
/** Remove one specific document. */
removeFile(n: number) {
if(this.intPreviousFiles.has(this.intOriginalFileName[n])) {
this.intPreviousFiles.delete(this.intOriginalFileName[n]);
}
this.intValues.splice(n, 1);
this.intHasDocument.splice(n, 1);
this.intOriginalFile.splice(n, 1);
this.intOriginalFileName.splice(n, 1);
if (this.intValues.length <= 0) {
this.intHasDocuments = false;
}
this.intInputDocuments = this.$refs.actualDocument as HTMLInputElement[];
this.intInputDocuments.forEach((element, index) => {
this.documentInputs.forEach((element, index) => {
if (this.intOriginalFile[index] != undefined) {
var dt = new DataTransfer();
const dt = new DataTransfer();
dt.items.add(this.intOriginalFile[index]);
element.files = dt.files;
}
});
this.closeBootstrapTooltip();
this.$nextTick(() => this.initTooltips());
},
/**
* Remove all documents.
*/
/** Remove all documents. */
removeFiles() {
this.intValues.length = this.intItemLimit[0];
this.intHasDocument.length = this.intItemLimit[0];
this.intOriginalFile.length = this.intItemLimit[0];
this.intOriginalFileName.length = this.intItemLimit[0];
this.intItemLimit[0] > 0
? (this.intHasDocuments = true)
: (this.intHasDocuments = false);
this.intAllErrors = {};
this.closeBootstrapTooltip();
this.$nextTick(() => this.initTooltips());
},
/**
* Change one specific document.
*/
/** Change one specific document. */
changedDocument(el: HTMLInputElement, n: number) {
const reader = new FileReader();
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
if (!el.files) {
throw new Error();
throw new Error("No files");
}
this.intHasDocument[n] = true;
reader.addEventListener(
"load",
() => {
@ -444,79 +314,35 @@ export default defineComponent({
reader.readAsDataURL(el.files[0]);
}
},
/**
* Add documents to exists document(s).
*/
changedDocuments(e: Event) {
const el = e.target;
if (!el || !(el instanceof HTMLInputElement)) {
throw new Error(
"confirmDoneJS can only be executed on an exising form"
);
}
if (!el.files) {
throw new Error();
}
this.intLoopLoadDocuments = this.intValues.length;
this.loadDocuments([].slice.call(el.files));
this.intHasDocuments = true;
},
/**
* Update inputs file.
*/
/** Update inputs file. */
loadDocuments(files: Array<File>) {
if (files.length > 0) {
if (this.intValues.length + files.length <= this.intItemLimit[1]) {
let actualFile = files.shift() as File;
const actualFile = files.shift() as File;
const reader = new FileReader();
reader.readAsDataURL(actualFile);
reader.onload = () => {
// On convertit l'document en une chaîne de caractères base64.
this.intValues.push(String(reader.result));
this.intOriginalFileName[this.intValues.length - 1] = "";
};
reader.onloadend = () => {
this.intInputDocuments = this.$refs
.actualDocument as HTMLInputElement[];
var dt = new DataTransfer();
const dt = new DataTransfer();
dt.items.add(actualFile);
this.intInputDocuments[this.intLoopLoadDocuments].files = dt.files;
this.intOriginalFile[this.intLoopLoadDocuments] = actualFile;
this.intHasDocument[this.intValues.length - 1] = true;
this.intOriginalFileName[this.intValues.length - 1] =
actualFile.name;
this.intLoopLoadDocuments++;
this.documentInputs[this.documentInputs.length - 1].files = dt.files;
this.intOriginalFile[this.documentInputs.length - 1] = actualFile;
this.intOriginalFileName[this.documentInputs.length - 1] = actualFile.name;
this.loadDocuments(files);
this.initTooltips();
};
}
} else {
this.updateBootstrapTooltip();
this.intLoopLoadDocuments = this.intValues.length;
}
},
/**
* Update document source in the viewer.
*/
/** Update document source in the viewer. */
viewDocumentSource(n: number) {
const documentViewer = this.$refs.documentViewer as HTMLObjectElement;
this.intViewerLoadDocument = true;
setTimeout(() => {
documentViewer.data = this.intValues[n];
documentViewer.onload = () => {
this.intViewerLoadDocument = false;
};
}, 10);
window.open(URL.createObjectURL(this.intOriginalFile[n]));
},
/**
* Remove document source in the viewer.
*/
removeDocumentSource() {
this.intViewerLoadDocument = true;
const documentViewer = this.$refs.documentViewer as HTMLObjectElement;
documentViewer.data = "";
},
/**
* Get size of a file.
*/
/** Get size of a file. */
humanFileSize(sizeBytes: number | bigint): string {
let size = Math.abs(Number(sizeBytes));
let u = 0;
@ -531,9 +357,6 @@ export default defineComponent({
maximumFractionDigits: 1,
}).format(size);
},
getInput(intId: string): HTMLInputElement {
return document.getElementById(intId) as HTMLInputElement;
}
},
});
</script>

View file

@ -0,0 +1,27 @@
import { Tooltip } from "bootstrap";
let tooltips: Array<bootstrap.Tooltip> = [];
/** ! Ignore here because there is no good TS vue mixin implementation at this time */
export default {
methods: {
initTooltips() {
// Disable tooltips on reboot.
tooltips.forEach((tooltip) => {
tooltip.disable();
tooltip.hide();
});
tooltips = [].slice
.call(document.querySelectorAll("[data-bs-toggle=\"tooltip\"]"))
.map(function (tooltipTriggerEl) {
return new Tooltip(tooltipTriggerEl, {
customClass: "large-tooltip",
delay: {
show: 1250,
hide: 0,
},
});
});
},
},
};

View file

@ -1,98 +1,115 @@
<nav class="d-flex justify-content-between align-items-center my-3">
{{-- SELECT ITEMS PER PAGE --}}
<div class="dropup-center dropup d-flex justify-content-center align-items-center input-group w-fit">
<span class="btn-md input-group-text" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.paginate_list') }}">
<i class="fa-solid fa-list-ul"></i>
</span>
<button class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" type="button" aria-expanded="false">
{{ Cache::get('pagination.' .Str::slug(request()->route()->getName())) }}
</button>
<ul class="dropdown-menu">
@foreach (\App\Enums\Pagination\ItemsPerPaginationEnum::toArray() as $itemsPerPaginationEnum)
<li>
<a class="dropdown-item text-center"
href="{{ request()->fullUrlWithQuery(['pagination' => $itemsPerPaginationEnum->value]) }}">
{{ $itemsPerPaginationEnum->value }}
</a>
</li>
@endforeach
</ul>
</div>
@if ($paginator->hasPages())
{{-- PAGINATION --}}
<div class="d-flex justify-content-end align-items-center">
<ul class="pagination m-0">
{{-- PREVIOUS PAGE --}}
<li class="page-item @if ($paginator->onFirstPage()) disabled @endif">
<a class="page-link d-flex align-items-center h-100" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.previous') }}" aria-label="@lang('crud.pagination.previous')"
@if (!$paginator->onFirstPage()) href="{{ $paginator->previousPageUrl() }}" @endif rel="prev"
@if ($paginator->onFirstPage()) aria-hidden="true" @endif>
<i class="fa-solid fa-chevron-left fa-2xs mt-1"></i>
</a>
</li>
{{-- FIRST PAGE --}}
@if ($paginator->currentPage() > 3)
<li class="page-item" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => 1]) }}">
<a class="page-link" href="{{ $paginator->url(1) }}">1</a>
{{-- GET ACTUAL PAGINATION --}}
@php
$pagination = intval(
Cache::get(
'pagination.' .
\Illuminate\Support\Str::slug(
request()
->route()
->getName(),
),
),
);
@endphp
@if ($paginator->items())
<nav class="pagination-custom d-flex justify-content-between align-items-center">
{{-- SELECT ITEMS PER PAGE --}}
<div class="dropup-center d-flex justify-content-center align-items-center input-group w-fit">
<span class="btn-md input-group-text" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.paginate_list') }}">
<i class="fa-solid fa-list-ul"></i>
</span>
<button class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" type="button" aria-expanded="false">
{{ $pagination }}
</button>
<ul class="dropdown-menu">
@foreach (\App\Enums\Pagination\ItemsPerPaginationEnum::toArray() as $itemsPerPaginationEnum)
<li>
<a class="dropdown-item user-select-none @if ($pagination === $itemsPerPaginationEnum->value) active @endif text-center"
href="{{ request()->fullUrlWithQuery(['page' => 1, 'pagination' => $itemsPerPaginationEnum->value]) }}"
role="button">
{{ $itemsPerPaginationEnum->value }}
</a>
</li>
@endif
{{-- HIDE PAGES TOO FAR --}}
@if ($paginator->currentPage() > 4)
<li class="page-item disabled"><span class="page-link">...</span></li>
@endif
{{-- SHOW ACTUAL PAGES + N-2/N+2 --}}
@foreach (range(1, $paginator->lastPage()) as $i)
@if ($i >= $paginator->currentPage() - 2 && $i <= $paginator->currentPage() + 2)
@if ($i == $paginator->currentPage())
<li class="page-item">
<div class="dropup-center dropup">
<button class="btn btn-primary dropdown-toggle rounded-0" data-bs-toggle="dropdown" type="button"
aria-expanded="false">
{{ $i }}
</button>
<ul class="dropdown-menu">
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
<li>
<a class="dropdown-item text-center"
href="{{ request()->fullUrlWithQuery(['page' => $i]) }}">
{{ $i }}
</a>
</li>
@endfor
</ul>
</div>
</li>
@else
<li class="page-item" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => $i]) }}">
<a class="page-link" href="{{ $paginator->url($i) }}">{{ $i }}</a>
</li>
@endif
@endif
@endforeach
{{-- HIDE PAGES TOO FAR --}}
@if ($paginator->currentPage() < $paginator->lastPage() - 3)
<li class="page-item disabled"><span class="page-link">...</span></li>
@endif
{{-- LAST PAGE --}}
@if ($paginator->currentPage() < $paginator->lastPage() - 2)
<li class="page-item" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => $paginator->lastPage()]) }}">
<a class="page-link" href="{{ $paginator->url($paginator->lastPage()) }}">{{ $paginator->lastPage() }}</a>
</li>
@endif
{{-- NEXT PAGE --}}
<li class="page-item @if (!$paginator->hasMorePages()) disabled @endif">
<a class="page-link d-flex align-items-center h-100" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.next') }}" aria-label="@lang('crud.pagination.next')"
@if ($paginator->hasMorePages()) href="{{ $paginator->nextPageUrl() }}" @endif rel="next"
@if (!$paginator->hasMorePages()) aria-hidden="true" @endif>
<i class="fa-solid fa-chevron-right fa-2xs mt-1"></i></a>
</li>
</ul>
</div>
@endif
</nav>
@if ($paginator->hasPages())
{{-- PAGINATION --}}
<div class="d-flex justify-content-end align-items-center">
<ul class="pagination m-0">
{{-- PREVIOUS PAGE --}}
<li class="page-item @if ($paginator->onFirstPage()) disabled @endif">
<a class="page-link d-flex align-items-center h-100" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.previous') }}" aria-label="@lang('crud.pagination.previous')"
@if (!$paginator->onFirstPage()) href="{{ request()->fullUrlWithQuery(['page' => $paginator->currentPage() - 1]) }}" @endif
rel="prev" @if ($paginator->onFirstPage()) aria-hidden="true" disabled @endif>
<i class="fa-solid fa-chevron-left fa-2xs mt-1"></i>
</a>
</li>
{{-- FIRST PAGE --}}
@if ($paginator->currentPage() > 3)
<li class="page-item d-none d-sm-block" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => 1]) }}">
<a class="page-link" href="{{ request()->fullUrlWithQuery(['page' => 1]) }}">1</a>
</li>
@endif
{{-- HIDE PAGES TOO FAR --}}
@if ($paginator->currentPage() > 4)
<li class="page-item d-none d-sm-block disabled"><span class="page-link">...</span></li>
@endif
{{-- SHOW ACTUAL PAGES + N-2/N+2 --}}
@foreach (range(1, $paginator->lastPage()) as $i)
@if ($i >= $paginator->currentPage() - 2 && $i <= $paginator->currentPage() + 2)
@if ($i == $paginator->currentPage())
<li class="page-item">
<div class="dropup-center">
<button class="btn btn-primary dropdown-toggle rounded-0" data-bs-toggle="dropdown" type="button"
aria-expanded="false">
{{ $i }}
</button>
<ul class="dropdown-menu">
@for ($i = 1; $i <= $paginator->lastPage(); $i++)
<li>
<a class="dropdown-item user-select-none @if ($paginator->currentPage() === $i) active @endif text-center"
href="{{ request()->fullUrlWithQuery(['page' => $i]) }}" role="button">
{{ $i }}
</a>
</li>
@endfor
</ul>
</div>
</li>
@else
<li class="page-item d-none d-md-block" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => $i]) }}">
<a class="page-link" href="{{ request()->fullUrlWithQuery(['page' => $i]) }}">{{ $i }}</a>
</li>
@endif
@endif
@endforeach
{{-- HIDE PAGES TOO FAR --}}
@if ($paginator->currentPage() < $paginator->lastPage() - 3)
<li class="page-item d-none d-sm-block disabled"><span class="page-link">...</span></li>
@endif
{{-- LAST PAGE --}}
@if ($paginator->currentPage() < $paginator->lastPage() - 2)
<li class="page-item d-none d-sm-block" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.specific_page', ['id' => $paginator->lastPage()]) }}">
<a class="page-link"
href="{{ request()->fullUrlWithQuery(['page' => $paginator->lastPage()]) }}">{{ $paginator->lastPage() }}</a>
</li>
@endif
{{-- NEXT PAGE --}}
<li class="page-item @if (!$paginator->hasMorePages()) disabled @endif">
<a class="page-link d-flex align-items-center h-100" data-bs-toggle="tooltip" data-bs-placement="top"
title="{{ __('crud.pagination.next') }}" aria-label="{{ __('crud.pagination.next') }}"
@if ($paginator->hasMorePages()) href="{{ request()->fullUrlWithQuery(['page' => $paginator->currentPage() + 1]) }}" @endif
rel="next" @if (!$paginator->hasMorePages()) aria-hidden="true" disabled @endif>
<i class="fa-solid fa-chevron-right fa-2xs mt-1"></i></a>
</li>
</ul>
</div>
@endif
</nav>
@endif

View file

@ -137,13 +137,27 @@
<legend>{{ __('Documents complémentaires') }}</legend>
<div class="row">
<div class="col-12">
{{-- application/msword
application/vnd.openxmlformats-officedocument.wordprocessingml.document
image/jpeg
image/png
application/pdf
application/vnd.ms-powerpoint
application/vnd.openxmlformats-officedocument.presentationml.presentation
application/vnd.ms-excel
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
text/csv
image/gif
text/plain pour .txt --}}
@php
$data = [
'id' => 'productDocs',
'name' => 'docs[]',
'value' => $productModel->docs ?? [],
'limit' => [0, 5],
'helper' => __('Doit respecter le format :format.', ['format' => '.pdf']),
'helper' => __('Doit respecter le format :format.', [
'format' => '.xls,.csv,.xlsx,.png,.jpg,.jpeg,.gif,.doc,.docx,.ppt,.pdf,.txt',
]),
'errors' => $errors->getBag('default')->getMessages(),
];
@endphp

View file

@ -9,16 +9,15 @@
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<span class="fw-bold">{{ trans_choice('models.classes.products_monitoring', 2) }}</span>&nbsp;<span class="text-muted">{{ Str::ucfirst(__('liste')) }}</span>
<h1 class="text-md-start h2 text-center">
<span class="fw-bold">{{ trans_choice('models.classes.products_monitoring', 2) }}</span>&nbsp;<span
class="text-muted">{{ Str::ucfirst(__('liste')) }}</span>
</h1>
</div>
<div class="col-md-3">
<div class="input-group justify-content-end">
<a href="{{ route('products.create') }}"
class="btn btn-success"
title="{{ __('Ajouter une :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}"
data-bs="tooltip">
<a class="btn btn-success" data-bs="tooltip" href="{{ route('products.create') }}"
title="{{ __('Ajouter une :model', ['model' => trans_choice('models.classes.products_monitoring', 1)]) }}">
<i class="fa-solid fa-plus"></i>
</a>
</div>
@ -28,7 +27,7 @@
<div class="card-body">
@include('pages.products.product-filters')
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0">
<div class="col-12 col-md-6 p-3">
{{ $products->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
@ -38,11 +37,10 @@
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0">
<div class="col-12 col-md-6 p-3">
{{ $products->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
</div>
</div>
@endsection