add calendar
This commit is contained in:
parent
69fbe1a7a1
commit
98d1743def
@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreDeceasedDocumentRequest;
|
||||
use App\Http\Requests\UpdateDeceasedDocumentRequest;
|
||||
use App\Http\Resources\Deceased\DeceasedDocumentResource;
|
||||
use App\Http\Resources\Deceased\DeceasedDocumentCollection;
|
||||
use App\Repositories\DeceasedDocumentRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DeceasedDocumentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DeceasedDocumentRepositoryInterface $deceasedDocumentRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of deceased documents.
|
||||
*/
|
||||
public function index(Request $request): DeceasedDocumentCollection|JsonResponse
|
||||
{
|
||||
try {
|
||||
$perPage = $request->get('per_page', 15);
|
||||
$filters = [
|
||||
'search' => $request->get('search'),
|
||||
'deceased_id' => $request->get('deceased_id'),
|
||||
'doc_type' => $request->get('doc_type'),
|
||||
'file_id' => $request->get('file_id'),
|
||||
'sort_by' => $request->get('sort_by', 'created_at'),
|
||||
'sort_direction' => $request->get('sort_direction', 'desc'),
|
||||
];
|
||||
|
||||
// Remove null filters
|
||||
$filters = array_filter($filters, function ($value) {
|
||||
return $value !== null && $value !== '';
|
||||
});
|
||||
|
||||
$documents = $this->deceasedDocumentRepository->paginate($perPage, $filters);
|
||||
|
||||
return new DeceasedDocumentCollection($documents);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la récupération des documents du défunt: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération des documents du défunt.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents by deceased ID.
|
||||
*/
|
||||
public function byDeceased(string $deceasedId): DeceasedDocumentCollection|JsonResponse
|
||||
{
|
||||
try {
|
||||
$documents = $this->deceasedDocumentRepository->getByDeceasedId((int) $deceasedId);
|
||||
return new DeceasedDocumentCollection($documents);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la récupération des documents par défunt: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'deceased_id' => $deceasedId,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération des documents du défunt.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents by document type.
|
||||
*/
|
||||
public function byDocType(Request $request): DeceasedDocumentCollection|JsonResponse
|
||||
{
|
||||
try {
|
||||
$docType = $request->get('doc_type');
|
||||
|
||||
if (!$docType) {
|
||||
return response()->json([
|
||||
'message' => 'Le paramètre doc_type est requis.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$documents = $this->deceasedDocumentRepository->getByDocType($docType);
|
||||
return new DeceasedDocumentCollection($documents);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la récupération des documents par type: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'doc_type' => $request->get('doc_type'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération des documents par type.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents by file ID.
|
||||
*/
|
||||
public function byFile(string $fileId): DeceasedDocumentCollection|JsonResponse
|
||||
{
|
||||
try {
|
||||
$documents = $this->deceasedDocumentRepository->getByFileId((int) $fileId);
|
||||
return new DeceasedDocumentCollection($documents);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la récupération des documents par fichier: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'file_id' => $fileId,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération des documents par fichier.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search documents by various criteria.
|
||||
*/
|
||||
public function search(Request $request): DeceasedDocumentCollection|JsonResponse
|
||||
{
|
||||
try {
|
||||
$criteria = [
|
||||
'deceased_id' => $request->get('deceased_id'),
|
||||
'doc_type' => $request->get('doc_type'),
|
||||
'file_id' => $request->get('file_id'),
|
||||
'generated_from' => $request->get('generated_from'),
|
||||
'generated_to' => $request->get('generated_to'),
|
||||
];
|
||||
|
||||
// Remove null criteria
|
||||
$criteria = array_filter($criteria, function ($value) {
|
||||
return $value !== null && $value !== '';
|
||||
});
|
||||
|
||||
$documents = $this->deceasedDocumentRepository->search($criteria);
|
||||
return new DeceasedDocumentCollection($documents);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la recherche de documents: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'criteria' => $request->all(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la recherche de documents.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created deceased document.
|
||||
*/
|
||||
public function store(StoreDeceasedDocumentRequest $request): DeceasedDocumentResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$document = $this->deceasedDocumentRepository->create($request->validated());
|
||||
return new DeceasedDocumentResource($document);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la création du document du défunt: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'data' => $request->validated(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la création du document du défunt.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified deceased document.
|
||||
*/
|
||||
public function show(string $id): DeceasedDocumentResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$document = $this->deceasedDocumentRepository->find($id);
|
||||
|
||||
if (!$document) {
|
||||
return response()->json([
|
||||
'message' => 'Document du défunt non trouvé.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return new DeceasedDocumentResource($document);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la récupération du document du défunt: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'document_id' => $id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération du document du défunt.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified deceased document.
|
||||
*/
|
||||
public function update(UpdateDeceasedDocumentRequest $request, string $id): DeceasedDocumentResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$updated = $this->deceasedDocumentRepository->update($id, $request->validated());
|
||||
|
||||
if (!$updated) {
|
||||
return response()->json([
|
||||
'message' => 'Document du défunt non trouvé ou échec de la mise à jour.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$document = $this->deceasedDocumentRepository->find($id);
|
||||
return new DeceasedDocumentResource($document);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la mise à jour du document du défunt: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'document_id' => $id,
|
||||
'data' => $request->validated(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la mise à jour du document du défunt.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified deceased document.
|
||||
*/
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deleted = $this->deceasedDocumentRepository->delete($id);
|
||||
|
||||
if (!$deleted) {
|
||||
return response()->json([
|
||||
'message' => 'Document du défunt non trouvé ou échec de la suppression.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Document du défunt supprimé avec succès.',
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Erreur lors de la suppression du document du défunt: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'document_id' => $id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la suppression du document du défunt.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -173,4 +173,40 @@ class InterventionController extends Controller
|
||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interventions for a specific month.
|
||||
*/
|
||||
public function byMonth(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'year' => 'required|integer|min:2000|max:2100',
|
||||
'month' => 'required|integer|min:1|max:12'
|
||||
]);
|
||||
|
||||
$interventions = $this->interventionRepository->getByMonth(
|
||||
$validated['year'],
|
||||
$validated['month']
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'data' => $interventions->map(function ($intervention) {
|
||||
return new InterventionResource($intervention);
|
||||
}),
|
||||
'meta' => [
|
||||
'total' => $interventions->count(),
|
||||
'year' => $validated['year'],
|
||||
'month' => $validated['month'],
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching interventions by month: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération des interventions du mois.',
|
||||
'error' => $e->getMessage()
|
||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreDeceasedDocumentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Add your authorization logic here
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'deceased_id' => 'required|exists:deceased,id',
|
||||
'doc_type' => 'required|string|max:191',
|
||||
'file_id' => 'nullable|exists:files,id',
|
||||
'generated_at' => 'nullable|date',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error messages for the defined validation rules.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'deceased_id.required' => 'Le défunt est obligatoire.',
|
||||
'deceased_id.exists' => 'Le défunt sélectionné n\'existe pas.',
|
||||
'doc_type.required' => 'Le type de document est obligatoire.',
|
||||
'doc_type.string' => 'Le type de document doit être une chaîne de caractères.',
|
||||
'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.',
|
||||
'file_id.exists' => 'Le fichier sélectionné n\'existe pas.',
|
||||
'generated_at.date' => 'La date de génération doit être une date valide.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateDeceasedDocumentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Add your authorization logic here
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'deceased_id' => 'sometimes|required|exists:deceased,id',
|
||||
'doc_type' => 'sometimes|required|string|max:191',
|
||||
'file_id' => 'nullable|exists:files,id',
|
||||
'generated_at' => 'nullable|date',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error messages for the defined validation rules.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'deceased_id.required' => 'Le défunt est obligatoire.',
|
||||
'deceased_id.exists' => 'Le défunt sélectionné n\'existe pas.',
|
||||
'doc_type.required' => 'Le type de document est obligatoire.',
|
||||
'doc_type.string' => 'Le type de document doit être une chaîne de caractères.',
|
||||
'doc_type.max' => 'Le type de document ne peut pas dépasser :max caractères.',
|
||||
'file_id.exists' => 'Le fichier sélectionné n\'existe pas.',
|
||||
'generated_at.date' => 'La date de génération doit être une date valide.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Deceased;
|
||||
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class DeceasedDocumentCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->collection->map(function ($document) {
|
||||
return [
|
||||
'id' => $document->id,
|
||||
'deceased_id' => $document->deceased_id,
|
||||
'doc_type' => $document->doc_type,
|
||||
'file_id' => $document->file_id,
|
||||
'generated_at' => $document->generated_at?->format('Y-m-d H:i:s'),
|
||||
'created_at' => $document->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $document->updated_at?->format('Y-m-d H:i:s'),
|
||||
|
||||
// Relations
|
||||
'deceased' => $document->deceased ? [
|
||||
'id' => $document->deceased->id,
|
||||
'first_name' => $document->deceased->first_name,
|
||||
'last_name' => $document->deceased->last_name,
|
||||
'full_name' => $document->deceased->first_name . ' ' . $document->deceased->last_name,
|
||||
'date_of_birth' => $document->deceased->date_of_birth?->format('Y-m-d'),
|
||||
'date_of_death' => $document->deceased->date_of_death?->format('Y-m-d'),
|
||||
] : null,
|
||||
'file' => $document->file ? [
|
||||
'id' => $document->file->id,
|
||||
'filename' => $document->file->filename ?? null,
|
||||
'path' => $document->file->path ?? null,
|
||||
'mime_type' => $document->file->mime_type ?? null,
|
||||
'size' => $document->file->size ?? null,
|
||||
] : null,
|
||||
];
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Deceased;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class DeceasedDocumentResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'deceased_id' => $this->deceased_id,
|
||||
'doc_type' => $this->doc_type,
|
||||
'file_id' => $this->file_id,
|
||||
'generated_at' => $this->generated_at?->format('Y-m-d H:i:s'),
|
||||
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'),
|
||||
|
||||
// Relations
|
||||
'deceased' => $this->when(
|
||||
$this->relationLoaded('deceased'),
|
||||
function () {
|
||||
return [
|
||||
'id' => $this->deceased->id,
|
||||
'first_name' => $this->deceased->first_name,
|
||||
'last_name' => $this->deceased->last_name,
|
||||
'full_name' => $this->deceased->first_name . ' ' . $this->deceased->last_name,
|
||||
'date_of_birth' => $this->deceased->date_of_birth?->format('Y-m-d'),
|
||||
'date_of_death' => $this->deceased->date_of_death?->format('Y-m-d'),
|
||||
];
|
||||
}
|
||||
),
|
||||
'file' => $this->when(
|
||||
$this->relationLoaded('file'),
|
||||
function () {
|
||||
return $this->file ? [
|
||||
'id' => $this->file->id,
|
||||
'filename' => $this->file->filename ?? null,
|
||||
'path' => $this->file->path ?? null,
|
||||
'mime_type' => $this->file->mime_type ?? null,
|
||||
'size' => $this->file->size ?? null,
|
||||
] : null;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,8 @@ namespace App\Providers;
|
||||
|
||||
use App\Repositories\DeceasedRepositoryInterface;
|
||||
use App\Repositories\DeceasedRepository;
|
||||
use App\Repositories\DeceasedDocumentRepositoryInterface;
|
||||
use App\Repositories\DeceasedDocumentRepository;
|
||||
use App\Repositories\InterventionRepositoryInterface;
|
||||
use App\Repositories\InterventionRepository;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
@ -16,6 +18,7 @@ class RepositoryServiceProvider extends ServiceProvider
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(DeceasedRepositoryInterface::class, DeceasedRepository::class);
|
||||
$this->app->bind(DeceasedDocumentRepositoryInterface::class, DeceasedDocumentRepository::class);
|
||||
$this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class);
|
||||
}
|
||||
|
||||
|
||||
121
thanasoft-back/app/Repositories/DeceasedDocumentRepository.php
Normal file
121
thanasoft-back/app/Repositories/DeceasedDocumentRepository.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\DeceasedDocument;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class DeceasedDocumentRepository extends BaseRepository implements DeceasedDocumentRepositoryInterface
|
||||
{
|
||||
public function __construct(DeceasedDocument $model)
|
||||
{
|
||||
parent::__construct($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated deceased documents with optional filters
|
||||
*/
|
||||
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$query = $this->model->newQuery()->with(['deceased', 'file']);
|
||||
|
||||
// Apply filters
|
||||
if (!empty($filters['search'])) {
|
||||
$query->where(function ($q) use ($filters) {
|
||||
$q->where('doc_type', 'like', '%' . $filters['search'] . '%')
|
||||
->orWhereHas('deceased', function ($q) use ($filters) {
|
||||
$q->where('first_name', 'like', '%' . $filters['search'] . '%')
|
||||
->orWhere('last_name', 'like', '%' . $filters['search'] . '%');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($filters['deceased_id'])) {
|
||||
$query->where('deceased_id', $filters['deceased_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['doc_type'])) {
|
||||
$query->where('doc_type', $filters['doc_type']);
|
||||
}
|
||||
|
||||
if (!empty($filters['file_id'])) {
|
||||
$query->where('file_id', $filters['file_id']);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
$sortField = $filters['sort_by'] ?? 'created_at';
|
||||
$sortDirection = $filters['sort_direction'] ?? 'desc';
|
||||
$query->orderBy($sortField, $sortDirection);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents by deceased ID
|
||||
*/
|
||||
public function getByDeceasedId(int $deceasedId): Collection
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->with(['deceased', 'file'])
|
||||
->where('deceased_id', $deceasedId)
|
||||
->orderBy('generated_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents by document type
|
||||
*/
|
||||
public function getByDocType(string $docType): Collection
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->with(['deceased', 'file'])
|
||||
->where('doc_type', $docType)
|
||||
->orderBy('generated_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get documents by file ID
|
||||
*/
|
||||
public function getByFileId(int $fileId): Collection
|
||||
{
|
||||
return $this->model->newQuery()
|
||||
->with(['deceased', 'file'])
|
||||
->where('file_id', $fileId)
|
||||
->orderBy('generated_at', 'desc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search documents by various criteria
|
||||
*/
|
||||
public function search(array $criteria): Collection
|
||||
{
|
||||
$query = $this->model->newQuery()->with(['deceased', 'file']);
|
||||
|
||||
if (!empty($criteria['deceased_id'])) {
|
||||
$query->where('deceased_id', $criteria['deceased_id']);
|
||||
}
|
||||
|
||||
if (!empty($criteria['doc_type'])) {
|
||||
$query->where('doc_type', $criteria['doc_type']);
|
||||
}
|
||||
|
||||
if (!empty($criteria['file_id'])) {
|
||||
$query->where('file_id', $criteria['file_id']);
|
||||
}
|
||||
|
||||
if (!empty($criteria['generated_from'])) {
|
||||
$query->where('generated_at', '>=', $criteria['generated_from']);
|
||||
}
|
||||
|
||||
if (!empty($criteria['generated_to'])) {
|
||||
$query->where('generated_at', '<=', $criteria['generated_to']);
|
||||
}
|
||||
|
||||
return $query->orderBy('generated_at', 'desc')->get();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Interface for DeceasedDocument repository operations.
|
||||
*/
|
||||
interface DeceasedDocumentRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Get paginated deceased documents with optional filters
|
||||
*/
|
||||
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Get documents by deceased ID
|
||||
*/
|
||||
public function getByDeceasedId(int $deceasedId): \Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* Get documents by document type
|
||||
*/
|
||||
public function getByDocType(string $docType): \Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* Get documents by file ID
|
||||
*/
|
||||
public function getByFileId(int $fileId): \Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* Search documents by various criteria
|
||||
*/
|
||||
public function search(array $criteria): \Illuminate\Database\Eloquent\Collection;
|
||||
}
|
||||
@ -134,4 +134,23 @@ class InterventionRepository implements InterventionRepositoryInterface
|
||||
return $intervention;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interventions for a specific month
|
||||
*
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function getByMonth(int $year, int $month): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
$startDate = sprintf('%04d-%02d-01', $year, $month);
|
||||
$endDate = date('Y-m-t', strtotime($startDate));
|
||||
|
||||
return Intervention::query()
|
||||
->whereBetween('scheduled_at', [$startDate . ' 00:00:00', $endDate . ' 23:59:59'])
|
||||
->with(['client', 'deceased', 'location', 'assignedPractitioner'])
|
||||
->orderBy('scheduled_at', 'asc')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,4 +57,13 @@ interface InterventionRepositoryInterface
|
||||
* @return Intervention
|
||||
*/
|
||||
public function changeStatus(Intervention $intervention, string $status): Intervention;
|
||||
|
||||
/**
|
||||
* Get interventions for a specific month
|
||||
*
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function getByMonth(int $year, int $month): \Illuminate\Database\Eloquent\Collection;
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ use App\Http\Controllers\Api\EmployeeController;
|
||||
use App\Http\Controllers\Api\ThanatopractitionerController;
|
||||
use App\Http\Controllers\Api\PractitionerDocumentController;
|
||||
use App\Http\Controllers\Api\DeceasedController;
|
||||
use App\Http\Controllers\Api\DeceasedDocumentController;
|
||||
use App\Http\Controllers\Api\InterventionController;
|
||||
|
||||
|
||||
@ -106,8 +107,22 @@ Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::delete('/{deceased}', [DeceasedController::class, 'destroy']);
|
||||
});
|
||||
|
||||
// Deceased Document Routes
|
||||
Route::prefix('deceased-documents')->group(function () {
|
||||
Route::get('/by-deceased/{deceasedId}', [DeceasedDocumentController::class, 'byDeceased']);
|
||||
Route::get('/by-doc-type', [DeceasedDocumentController::class, 'byDocType']);
|
||||
Route::get('/by-file/{fileId}', [DeceasedDocumentController::class, 'byFile']);
|
||||
Route::get('/search', [DeceasedDocumentController::class, 'search']);
|
||||
Route::get('/', [DeceasedDocumentController::class, 'index']);
|
||||
Route::post('/', [DeceasedDocumentController::class, 'store']);
|
||||
Route::get('/{id}', [DeceasedDocumentController::class, 'show']);
|
||||
Route::put('/{id}', [DeceasedDocumentController::class, 'update']);
|
||||
Route::delete('/{id}', [DeceasedDocumentController::class, 'destroy']);
|
||||
});
|
||||
|
||||
// Intervention Routes
|
||||
Route::prefix('interventions')->group(function () {
|
||||
Route::get('/by-month', [InterventionController::class, 'byMonth']);
|
||||
Route::get('/', [InterventionController::class, 'index']);
|
||||
Route::post('/', [InterventionController::class, 'store']);
|
||||
Route::get('/{intervention}', [InterventionController::class, 'show']);
|
||||
|
||||
@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<agenda-template>
|
||||
<template #agenda-title>
|
||||
<h5 class="mb-0">Agenda des Interventions</h5>
|
||||
<p class="text-sm text-muted mb-0">
|
||||
Gérez vos interventions et rendez-vous
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template #agenda-actions>
|
||||
<add-intervention-button
|
||||
text="Nouvelle Intervention"
|
||||
@click="handleAddIntervention"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #agenda-calendar>
|
||||
<agenda-calendar
|
||||
:events="calendarEvents"
|
||||
@date-click="handleDateClick"
|
||||
@event-click="handleEventClick"
|
||||
@month-change="handleMonthChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #agenda-sidebar>
|
||||
<upcoming-interventions :interventions="upcomingInterventions" />
|
||||
</template>
|
||||
</agenda-template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import AgendaTemplate from "@/components/templates/Agenda/AgendaTemplate.vue";
|
||||
import AddInterventionButton from "@/components/atoms/Agenda/AddInterventionButton.vue";
|
||||
import AgendaCalendar from "@/components/molecules/Agenda/AgendaCalendar.vue";
|
||||
import UpcomingInterventions from "@/components/molecules/Agenda/UpcomingInterventions.vue";
|
||||
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
interventions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"add-intervention",
|
||||
"date-click",
|
||||
"event-click",
|
||||
"edit-intervention",
|
||||
"month-change",
|
||||
]);
|
||||
|
||||
// Transform interventions to calendar events format
|
||||
const calendarEvents = computed(() => {
|
||||
return props.interventions.map((intervention) => {
|
||||
const statusColors = {
|
||||
scheduled: "bg-gradient-info",
|
||||
"in-progress": "bg-gradient-warning",
|
||||
completed: "bg-gradient-success",
|
||||
cancelled: "bg-gradient-secondary",
|
||||
};
|
||||
|
||||
return {
|
||||
id: intervention.id,
|
||||
title: intervention.type || "Intervention",
|
||||
start: intervention.scheduled_at,
|
||||
end: intervention.scheduled_at,
|
||||
className: statusColors[intervention.status] || "bg-gradient-info",
|
||||
extendedProps: {
|
||||
deceased: intervention.deceased,
|
||||
status: intervention.status,
|
||||
practitioner: intervention.assignedPractitioner,
|
||||
type: intervention.type,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Get upcoming interventions (next 7 days)
|
||||
const upcomingInterventions = computed(() => {
|
||||
const now = new Date();
|
||||
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
return props.interventions
|
||||
.filter((intervention) => {
|
||||
const date = new Date(intervention.scheduled_at);
|
||||
return (
|
||||
date >= now && date <= nextWeek && intervention.status !== "annule"
|
||||
);
|
||||
})
|
||||
.sort((a, b) => new Date(a.scheduled_at) - new Date(b.scheduled_at))
|
||||
.slice(0, 5)
|
||||
.map((intervention) => ({
|
||||
id: intervention.id,
|
||||
title: intervention.type || "Intervention",
|
||||
date: intervention.scheduled_at,
|
||||
status: intervention.status,
|
||||
deceased: intervention.deceased
|
||||
? `${intervention.deceased.first_name} ${intervention.deceased.last_name}`
|
||||
: null,
|
||||
}));
|
||||
});
|
||||
|
||||
const handleAddIntervention = () => {
|
||||
emit("add-intervention");
|
||||
};
|
||||
|
||||
const handleDateClick = (info) => {
|
||||
emit("date-click", info);
|
||||
};
|
||||
|
||||
const handleEventClick = (info) => {
|
||||
emit("event-click", info.event);
|
||||
};
|
||||
|
||||
const handleMonthChange = (dateInfo) => {
|
||||
emit("month-change", dateInfo);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Additional styles if needed */
|
||||
</style>
|
||||
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<button
|
||||
class="btn bg-gradient-primary mb-0"
|
||||
@click="$emit('click')"
|
||||
type="button"
|
||||
>
|
||||
<i class="fas fa-plus me-2"></i>
|
||||
{{ text }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: "Nouvelle Intervention",
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(["click"]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.btn {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.15s ease-in;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<span :class="['badge', badgeClass, 'px-3 py-2']">
|
||||
<i :class="iconClass" class="me-1"></i>
|
||||
{{ label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: "scheduled", // scheduled, in-progress, completed, cancelled
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
const classes = {
|
||||
scheduled: "bg-gradient-info",
|
||||
"in-progress": "bg-gradient-warning",
|
||||
completed: "bg-gradient-success",
|
||||
cancelled: "bg-gradient-secondary",
|
||||
};
|
||||
return classes[props.type] || "bg-gradient-info";
|
||||
});
|
||||
|
||||
const iconClass = computed(() => {
|
||||
const icons = {
|
||||
scheduled: "fas fa-clock",
|
||||
"in-progress": "fas fa-spinner",
|
||||
completed: "fas fa-check-circle",
|
||||
cancelled: "fas fa-times-circle",
|
||||
};
|
||||
return icons[props.type] || "fas fa-clock";
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="card widget-calendar">
|
||||
<div class="p-3 pb-0 card-header">
|
||||
<h6 class="mb-0">{{ title }}</h6>
|
||||
<div class="d-flex">
|
||||
<div class="mb-0 text-sm p font-weight-bold widget-calendar-day">
|
||||
{{ currentDay }}
|
||||
</div>
|
||||
<span>, </span>
|
||||
<div class="mb-1 text-sm p font-weight-bold widget-calendar-year">
|
||||
{{ currentYear }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 card-body">
|
||||
<div :id="calendarId" data-toggle="widget-calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
|
||||
import { Calendar } from "@fullcalendar/core";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import frLocale from "@fullcalendar/core/locales/fr";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "Agenda",
|
||||
},
|
||||
events: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
calendarId: {
|
||||
type: String,
|
||||
default: "agenda-calendar",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["date-click", "event-click", "month-change"]);
|
||||
|
||||
let calendar = null;
|
||||
const currentDay = ref("");
|
||||
const currentYear = ref("");
|
||||
|
||||
const updateCurrentDate = () => {
|
||||
const now = new Date();
|
||||
const days = [
|
||||
"Dimanche",
|
||||
"Lundi",
|
||||
"Mardi",
|
||||
"Mercredi",
|
||||
"Jeudi",
|
||||
"Vendredi",
|
||||
"Samedi",
|
||||
];
|
||||
const months = [
|
||||
"Janvier",
|
||||
"Février",
|
||||
"Mars",
|
||||
"Avril",
|
||||
"Mai",
|
||||
"Juin",
|
||||
"Juillet",
|
||||
"Août",
|
||||
"Septembre",
|
||||
"Octobre",
|
||||
"Novembre",
|
||||
"Décembre",
|
||||
];
|
||||
|
||||
currentDay.value = `${days[now.getDay()]}, ${now.getDate()} ${
|
||||
months[now.getMonth()]
|
||||
}`;
|
||||
currentYear.value = now.getFullYear().toString();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updateCurrentDate();
|
||||
|
||||
calendar = new Calendar(document.getElementById(props.calendarId), {
|
||||
contentHeight: "auto",
|
||||
plugins: [dayGridPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
locale: frLocale,
|
||||
selectable: true,
|
||||
editable: true,
|
||||
events: props.events,
|
||||
headerToolbar: {
|
||||
left: "prev,next today",
|
||||
center: "title",
|
||||
right: "dayGridMonth,dayGridWeek",
|
||||
},
|
||||
dateClick: (info) => {
|
||||
emit("date-click", info);
|
||||
},
|
||||
eventClick: (info) => {
|
||||
emit("event-click", info);
|
||||
},
|
||||
datesSet: (dateInfo) => {
|
||||
// Emit month change when user navigates to different month
|
||||
const date = dateInfo.view.currentStart;
|
||||
emit("month-change", {
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
});
|
||||
},
|
||||
views: {
|
||||
month: {
|
||||
titleFormat: {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
},
|
||||
},
|
||||
week: {
|
||||
titleFormat: {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
day: "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (calendar) {
|
||||
calendar.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.widget-calendar {
|
||||
box-shadow: 0 20px 27px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.widget-calendar-day,
|
||||
.widget-calendar-year {
|
||||
color: #344767;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-header pb-0">
|
||||
<h6 class="mb-0">Interventions à venir</h6>
|
||||
<p class="text-sm mb-0">{{ interventions.length }} intervention(s)</p>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div v-if="interventions.length === 0" class="text-center py-4">
|
||||
<i class="fas fa-calendar-check fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted mb-0">Aucune intervention prévue</p>
|
||||
</div>
|
||||
<ul v-else class="list-group list-group-flush">
|
||||
<li
|
||||
v-for="intervention in interventions"
|
||||
:key="intervention.id"
|
||||
class="list-group-item border-0 d-flex p-3 mb-2 bg-gray-100 border-radius-lg"
|
||||
>
|
||||
<div class="d-flex flex-column w-100">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h6 class="mb-0 text-sm">{{ intervention.title }}</h6>
|
||||
<intervention-badge
|
||||
:type="intervention.status"
|
||||
:label="getStatusLabel(intervention.status)"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex align-items-center text-sm text-muted">
|
||||
<i class="fas fa-calendar me-2"></i>
|
||||
<span>{{ formatDate(intervention.date) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="intervention.deceased"
|
||||
class="d-flex align-items-center text-sm text-muted mt-1"
|
||||
>
|
||||
<i class="fas fa-user me-2"></i>
|
||||
<span>{{ intervention.deceased }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InterventionBadge from "@/components/atoms/Agenda/InterventionBadge.vue";
|
||||
import { defineProps } from "vue";
|
||||
|
||||
defineProps({
|
||||
interventions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
scheduled: "Planifiée",
|
||||
"in-progress": "En cours",
|
||||
completed: "Terminée",
|
||||
cancelled: "Annulée",
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return "";
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString("fr-FR", {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-group-item {
|
||||
transition: all 0.15s ease-in;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-sm-flex justify-content-between mb-4">
|
||||
<div>
|
||||
<slot name="agenda-title"></slot>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<slot name="agenda-actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xl-9 col-lg-8">
|
||||
<slot name="agenda-calendar"></slot>
|
||||
</div>
|
||||
<div class="col-xl-3 col-lg-4">
|
||||
<slot name="agenda-sidebar"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// Template component for Agenda page layout
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-fluid {
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@ -232,6 +232,22 @@ export const InterventionService = {
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get interventions by month
|
||||
*/
|
||||
async getInterventionsByMonth(
|
||||
year: number,
|
||||
month: number
|
||||
): Promise<InterventionListResponse> {
|
||||
const response = await request<InterventionListResponse>({
|
||||
url: "/api/interventions/by-month",
|
||||
method: "get",
|
||||
params: { year, month },
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
export default InterventionService;
|
||||
|
||||
@ -458,6 +458,36 @@ export const useInterventionStore = defineStore("intervention", () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get interventions by month
|
||||
*/
|
||||
const fetchInterventionsByMonth = async (year: number, month: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const response = await InterventionService.getInterventionsByMonth(
|
||||
year,
|
||||
month
|
||||
);
|
||||
setInterventions(response.data);
|
||||
if (response.meta) {
|
||||
setPagination(response.meta);
|
||||
}
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Échec du chargement des interventions du mois";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the state
|
||||
*/
|
||||
@ -517,6 +547,7 @@ export const useInterventionStore = defineStore("intervention", () => {
|
||||
searchInterventions,
|
||||
updateInterventionStatus,
|
||||
assignPractitioner,
|
||||
fetchInterventionsByMonth,
|
||||
resetState,
|
||||
};
|
||||
});
|
||||
|
||||
@ -1,11 +1,287 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Agenda</h1>
|
||||
<agenda-presentation
|
||||
:interventions="interventions"
|
||||
:loading="loading"
|
||||
@add-intervention="openCreateModal"
|
||||
@date-click="handleDateClick"
|
||||
@event-click="handleEventClick"
|
||||
/>
|
||||
|
||||
<!-- Create/Edit Intervention Modal -->
|
||||
<div
|
||||
class="modal fade"
|
||||
id="interventionModal"
|
||||
tabindex="-1"
|
||||
aria-labelledby="interventionModalLabel"
|
||||
aria-hidden="true"
|
||||
ref="modalRef"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="interventionModalLabel">
|
||||
{{
|
||||
isEditing ? "Modifier l'intervention" : "Nouvelle Intervention"
|
||||
}}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Titre de l'intervention *</label>
|
||||
<input
|
||||
v-model="form.title"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Ex: Soins de conservation"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Date et heure *</label>
|
||||
<input
|
||||
v-model="form.scheduled_date"
|
||||
type="datetime-local"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Statut</label>
|
||||
<select v-model="form.status" class="form-select">
|
||||
<option value="scheduled">Planifiée</option>
|
||||
<option value="in-progress">En cours</option>
|
||||
<option value="completed">Terminée</option>
|
||||
<option value="cancelled">Annulée</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Défunt</label>
|
||||
<select v-model="form.deceased_id" class="form-select">
|
||||
<option value="">Sélectionner un défunt</option>
|
||||
<option
|
||||
v-for="deceased in deceasedList"
|
||||
:key="deceased.id"
|
||||
:value="deceased.id"
|
||||
>
|
||||
{{ deceased.first_name }} {{ deceased.last_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Thanatopracteur</label>
|
||||
<select v-model="form.practitioner_id" class="form-select">
|
||||
<option value="">Sélectionner un thanatopracteur</option>
|
||||
<option
|
||||
v-for="practitioner in practitioners"
|
||||
:key="practitioner.id"
|
||||
:value="practitioner.id"
|
||||
>
|
||||
{{ practitioner.employee?.first_name }}
|
||||
{{ practitioner.employee?.last_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Détails de l'intervention..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Lieu</label>
|
||||
<input
|
||||
v-model="form.location"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Adresse du lieu"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn bg-gradient-primary"
|
||||
@click="handleSubmit"
|
||||
:disabled="submitting"
|
||||
>
|
||||
<span v-if="submitting">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>
|
||||
Enregistrement...
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ isEditing ? "Mettre à jour" : "Créer" }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Agenda",
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import AgendaPresentation from "@/components/Organism/Agenda/AgendaPresentation.vue";
|
||||
import { useInterventionStore } from "@/stores/interventionStore";
|
||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||
import { useThanatopractitionerStore } from "@/stores/thanatopractitionerStore";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const router = useRouter();
|
||||
const interventionStore = useInterventionStore();
|
||||
const deceasedStore = useDeceasedStore();
|
||||
const thanatopractitionerStore = useThanatopractitionerStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const modalRef = ref(null);
|
||||
const modalInstance = ref(null);
|
||||
const isEditing = ref(false);
|
||||
|
||||
const form = ref({
|
||||
title: "",
|
||||
scheduled_date: "",
|
||||
status: "scheduled",
|
||||
deceased_id: "",
|
||||
practitioner_id: "",
|
||||
description: "",
|
||||
location: "",
|
||||
});
|
||||
|
||||
const interventions = computed(() => interventionStore.interventions || []);
|
||||
const deceasedList = computed(() => deceasedStore.deceased || []);
|
||||
const practitioners = computed(
|
||||
() => thanatopractitionerStore.thanatopractitioners || []
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
interventionStore.fetchInterventions(),
|
||||
deceasedStore.fetchDeceased(),
|
||||
thanatopractitionerStore.fetchThanatopractitioners(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// Initialize Bootstrap modal
|
||||
if (modalRef.value) {
|
||||
modalInstance.value = new Modal(modalRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
const openCreateModal = () => {
|
||||
isEditing.value = false;
|
||||
resetForm();
|
||||
modalInstance.value?.show();
|
||||
};
|
||||
|
||||
const handleDateClick = (info) => {
|
||||
isEditing.value = false;
|
||||
resetForm();
|
||||
// Pre-fill the date from calendar click
|
||||
const date = new Date(info.dateStr);
|
||||
form.value.scheduled_date = date.toISOString().slice(0, 16);
|
||||
modalInstance.value?.show();
|
||||
};
|
||||
|
||||
const handleEventClick = (event) => {
|
||||
// Navigate to intervention details or open edit modal
|
||||
if (event.id) {
|
||||
router.push({
|
||||
name: "Intervention Details",
|
||||
params: { id: event.id },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
title: "",
|
||||
scheduled_date: "",
|
||||
status: "scheduled",
|
||||
deceased_id: "",
|
||||
practitioner_id: "",
|
||||
description: "",
|
||||
location: "",
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await interventionStore.updateIntervention(form.value.id, form.value);
|
||||
} else {
|
||||
await interventionStore.createIntervention(form.value);
|
||||
}
|
||||
modalInstance.value?.hide();
|
||||
resetForm();
|
||||
await interventionStore.fetchInterventions();
|
||||
} catch (error) {
|
||||
console.error("Error saving intervention:", error);
|
||||
alert("Erreur lors de l'enregistrement de l'intervention");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-content {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #344767;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border: 1px solid #d2d6da;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #cb0c9f;
|
||||
box-shadow: 0 0 0 2px rgba(203, 12, 159, 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
446
thanasoft-front/src/views/pages/Agenda/Agenda.vue
Normal file
446
thanasoft-front/src/views/pages/Agenda/Agenda.vue
Normal file
@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<div>
|
||||
<agenda-presentation
|
||||
:interventions="interventions"
|
||||
:loading="loading"
|
||||
@add-intervention="openCreateModal"
|
||||
@date-click="handleDateClick"
|
||||
@event-click="handleEventClick"
|
||||
@month-change="handleMonthChange"
|
||||
/>
|
||||
|
||||
<!-- Create/Edit Intervention Modal -->
|
||||
<div
|
||||
class="modal fade"
|
||||
id="interventionModal"
|
||||
tabindex="-1"
|
||||
aria-labelledby="interventionModalLabel"
|
||||
aria-hidden="true"
|
||||
ref="modalRef"
|
||||
>
|
||||
<div
|
||||
class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="interventionModalLabel">
|
||||
{{
|
||||
isEditing ? "Modifier l'intervention" : "Nouvelle Intervention"
|
||||
}}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Type d'intervention *</label>
|
||||
<select v-model="form.type" class="form-select" required>
|
||||
<option value="">Sélectionner un type</option>
|
||||
<option value="thanatopraxie">Thanatopraxie</option>
|
||||
<option value="toilette_mortuaire">
|
||||
Toilette mortuaire
|
||||
</option>
|
||||
<option value="exhumation">Exhumation</option>
|
||||
<option value="retrait_pacemaker">Retrait pacemaker</option>
|
||||
<option value="retrait_bijoux">Retrait bijoux</option>
|
||||
<option value="autre">Autre</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Date et heure *</label>
|
||||
<input
|
||||
v-model="form.scheduled_at"
|
||||
type="datetime-local"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Statut</label>
|
||||
<select v-model="form.status" class="form-select">
|
||||
<option value="demande">Demande</option>
|
||||
<option value="planifie">Planifiée</option>
|
||||
<option value="en_cours">En cours</option>
|
||||
<option value="termine">Terminée</option>
|
||||
<option value="annule">Annulée</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Défunt *</label>
|
||||
<select
|
||||
v-model="form.deceased_id"
|
||||
class="form-select"
|
||||
required
|
||||
>
|
||||
<option value="">Sélectionner un défunt</option>
|
||||
<option
|
||||
v-for="deceased in deceasedList"
|
||||
:key="deceased.id"
|
||||
:value="deceased.id"
|
||||
>
|
||||
{{ deceased.first_name }} {{ deceased.last_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Client *</label>
|
||||
<input
|
||||
v-model="form.client_id"
|
||||
type="number"
|
||||
class="form-control"
|
||||
placeholder="ID du client"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Thanatopracteur</label>
|
||||
<select
|
||||
v-model="form.assigned_practitioner_id"
|
||||
class="form-select"
|
||||
>
|
||||
<option value="">Sélectionner un thanatopracteur</option>
|
||||
<option
|
||||
v-for="practitioner in practitioners"
|
||||
:key="practitioner.id"
|
||||
:value="practitioner.id"
|
||||
>
|
||||
{{ practitioner.employee?.first_name }}
|
||||
{{ practitioner.employee?.last_name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Durée (minutes)</label>
|
||||
<input
|
||||
v-model="form.duration_min"
|
||||
type="number"
|
||||
class="form-control"
|
||||
placeholder="Ex: 60"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Donneur d'ordre</label>
|
||||
<input
|
||||
v-model="form.order_giver"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Nom du donneur d'ordre"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea
|
||||
v-model="form.notes"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="Notes et détails de l'intervention..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn bg-gradient-primary"
|
||||
@click="handleSubmit"
|
||||
:disabled="submitting"
|
||||
>
|
||||
<span v-if="submitting">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>
|
||||
Enregistrement...
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ isEditing ? "Mettre à jour" : "Créer" }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import AgendaPresentation from "@/components/Organism/Agenda/AgendaPresentation.vue";
|
||||
import { useInterventionStore } from "@/stores/interventionStore";
|
||||
import { useDeceasedStore } from "@/stores/deceasedStore";
|
||||
import { useThanatopractitionerStore } from "@/stores/thanatopractitionerStore";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const router = useRouter();
|
||||
const interventionStore = useInterventionStore();
|
||||
const deceasedStore = useDeceasedStore();
|
||||
const thanatopractitionerStore = useThanatopractitionerStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const modalRef = ref(null);
|
||||
const modalInstance = ref(null);
|
||||
const isEditing = ref(false);
|
||||
|
||||
const form = ref({
|
||||
type: "",
|
||||
scheduled_at: "",
|
||||
status: "demande",
|
||||
deceased_id: "",
|
||||
client_id: "",
|
||||
assigned_practitioner_id: "",
|
||||
notes: "",
|
||||
location_id: "",
|
||||
order_giver: "",
|
||||
duration_min: null,
|
||||
});
|
||||
|
||||
const interventions = computed(() => interventionStore.interventions || []);
|
||||
const deceasedList = computed(() => deceasedStore.deceased || []);
|
||||
const practitioners = computed(
|
||||
() => thanatopractitionerStore.thanatopractitioners || []
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// Get current month and year
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
const currentMonth = now.getMonth() + 1; // JavaScript months are 0-indexed
|
||||
|
||||
await Promise.all([
|
||||
interventionStore.fetchInterventionsByMonth(currentYear, currentMonth),
|
||||
deceasedStore.fetchDeceased(),
|
||||
thanatopractitionerStore.fetchThanatopractitioners(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// Initialize Bootstrap modal
|
||||
if (modalRef.value) {
|
||||
modalInstance.value = new Modal(modalRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
const openCreateModal = () => {
|
||||
isEditing.value = false;
|
||||
resetForm();
|
||||
modalInstance.value?.show();
|
||||
};
|
||||
|
||||
const handleDateClick = (info) => {
|
||||
isEditing.value = false;
|
||||
resetForm();
|
||||
// Pre-fill the date from calendar click
|
||||
const date = new Date(info.dateStr);
|
||||
form.value.scheduled_at = date.toISOString().slice(0, 16);
|
||||
modalInstance.value?.show();
|
||||
};
|
||||
|
||||
const handleEventClick = (event) => {
|
||||
// Navigate to intervention details or open edit modal
|
||||
if (event.id) {
|
||||
router.push({
|
||||
name: "Intervention Details",
|
||||
params: { id: event.id },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value = {
|
||||
type: "",
|
||||
scheduled_at: "",
|
||||
status: "demande",
|
||||
deceased_id: "",
|
||||
client_id: "",
|
||||
assigned_practitioner_id: "",
|
||||
notes: "",
|
||||
location_id: "",
|
||||
order_giver: "",
|
||||
duration_min: null,
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
await interventionStore.updateIntervention(form.value.id, form.value);
|
||||
} else {
|
||||
await interventionStore.createIntervention(form.value);
|
||||
}
|
||||
modalInstance.value?.hide();
|
||||
resetForm();
|
||||
// Reload current month interventions
|
||||
const now = new Date();
|
||||
await interventionStore.fetchInterventionsByMonth(
|
||||
now.getFullYear(),
|
||||
now.getMonth() + 1
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error saving intervention:", error);
|
||||
alert("Erreur lors de l'enregistrement de l'intervention");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonthChange = async (dateInfo) => {
|
||||
// Fetch interventions for the new month when user navigates
|
||||
try {
|
||||
await interventionStore.fetchInterventionsByMonth(
|
||||
dateInfo.year,
|
||||
dateInfo.month
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error loading interventions for month:", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Modal responsive styles */
|
||||
.modal-dialog {
|
||||
max-height: calc(100vh - 3.5rem);
|
||||
margin: 1.75rem auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 1rem;
|
||||
max-height: calc(100vh - 3.5rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid #e9ecef;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #344767;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border: 1px solid #d2d6da;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #cb0c9f;
|
||||
box-shadow: 0 0 0 2px rgba(203, 12, 159, 0.1);
|
||||
}
|
||||
|
||||
/* Responsive adjustments for smaller screens */
|
||||
@media (max-height: 768px) {
|
||||
.modal-dialog {
|
||||
max-height: calc(100vh - 2rem);
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-height: calc(100vh - 2rem);
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-footer {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.modal-dialog {
|
||||
margin: 0.5rem;
|
||||
max-width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.modal-header h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure modal is always visible on small screens */
|
||||
@media (max-height: 600px) {
|
||||
.modal-dialog {
|
||||
max-height: calc(100vh - 1rem);
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-height: calc(100vh - 1rem);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user