feat: add Devis and BesoinInformatique modules

- Devis: CRUD complet, génération PDF (dompdf), paramètres (DevisSetting)
- BesoinInformatique: CRUD complet avec gestion matériels/licences
- Migrations pour les 3 nouvelles tables
- Navigation mise à jour dans AuthenticatedLayout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mrKamoo
2026-06-19 14:46:16 +02:00
co-authored by Claude Sonnet 4.6
parent 030d76af53
commit 7312c7640b
24 changed files with 2887 additions and 4 deletions
@@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers;
use App\Models\BesoinInformatique;
use Illuminate\Http\Request;
use Inertia\Inertia;
class BesoinInformatiqueController extends Controller
{
public function index()
{
$besoins = BesoinInformatique::latest()->get();
return Inertia::render('BesoinsInformatiques/Index', [
'besoins' => $besoins
]);
}
public function create()
{
return Inertia::render('BesoinsInformatiques/Create');
}
public function store(Request $request)
{
$validated = $request->validate([
'collectivite' => 'nullable|string|max:255',
'nom_referent' => 'nullable|string|max:255',
'descriptif_projet' => 'nullable|string',
'materiels_support' => 'nullable|array',
'licences_bureautique' => 'nullable|array',
'equipements_reseaux' => 'nullable|string',
'cablage_reseau' => 'nullable|string',
'telephonie_ip' => 'nullable|string',
'nouveau_batiment' => 'nullable|string',
'logiciels_metiers' => 'nullable|string',
'licences_projets' => 'nullable|array',
'date_validation' => 'nullable|date',
'validation_dgs' => 'nullable|string|max:255',
]);
BesoinInformatique::create($validated);
return redirect()->route('besoins-informatiques.index')->with('success', 'Besoin informatique créé avec succès.');
}
public function show(BesoinInformatique $besoinInformatique)
{
return Inertia::render('BesoinsInformatiques/Show', [
'besoin' => $besoinInformatique
]);
}
public function edit(BesoinInformatique $besoinInformatique)
{
return Inertia::render('BesoinsInformatiques/Edit', [
'besoin' => $besoinInformatique
]);
}
public function update(Request $request, BesoinInformatique $besoinInformatique)
{
$validated = $request->validate([
'collectivite' => 'nullable|string|max:255',
'nom_referent' => 'nullable|string|max:255',
'descriptif_projet' => 'nullable|string',
'materiels_support' => 'nullable|array',
'licences_bureautique' => 'nullable|array',
'equipements_reseaux' => 'nullable|string',
'cablage_reseau' => 'nullable|string',
'telephonie_ip' => 'nullable|string',
'nouveau_batiment' => 'nullable|string',
'logiciels_metiers' => 'nullable|string',
'licences_projets' => 'nullable|array',
'date_validation' => 'nullable|date',
'validation_dgs' => 'nullable|string|max:255',
]);
$besoinInformatique->update($validated);
return redirect()->route('besoins-informatiques.index')->with('success', 'Besoin informatique mis à jour avec succès.');
}
public function destroy(BesoinInformatique $besoinInformatique)
{
$besoinInformatique->delete();
return redirect()->route('besoins-informatiques.index')->with('success', 'Besoin informatique supprimé avec succès.');
}
}
+177
View File
@@ -0,0 +1,177 @@
<?php
namespace App\Http\Controllers;
use App\Models\Devis;
use App\Models\DevisSetting;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Barryvdh\DomPDF\Facade\Pdf;
class DevisController extends Controller
{
public function index()
{
$devis = Devis::latest()->get();
return Inertia::render('Devis/Index', [
'devis' => $devis
]);
}
public function create()
{
return Inertia::render('Devis/Create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'destinataire_type' => 'required|string|in:commune,service_interne',
'destinataire_nom' => 'required|string|max:255',
'referent' => 'nullable|string|max:255',
'date_devis' => 'required|date',
'status' => 'required|string|in:draft,sent,accepted,rejected',
'items' => 'nullable|array',
'tva_rate' => 'nullable|numeric|min:0|max:100',
'notes' => 'nullable|string',
]);
// Calculer les montants
$totalHt = 0;
if (!empty($validated['items'])) {
foreach ($validated['items'] as $item) {
$qty = floatval($item['quantite'] ?? 0);
$price = floatval($item['prix_unitaire'] ?? 0);
$totalHt += $qty * $price;
}
}
$tvaRate = floatval($validated['tva_rate'] ?? 20);
$totalTtc = $totalHt * (1 + ($tvaRate / 100));
$validated['total_ht'] = $totalHt;
$validated['total_ttc'] = $totalTtc;
Devis::create($validated);
return redirect()->route('devis.index')->with('success', 'Devis créé avec succès.');
}
public function show(Devis $devi)
{
$settings = DevisSetting::first() ?? DevisSetting::create([
'entity_name' => "Direction des Systèmes d'Information",
'entity_sub' => "Communauté d'Agglomération Béziers Méditerranée",
'email' => "support.informatique@beziers-mediterranee.fr",
'telephone' => "04.67.01.60.00",
'adresse' => "Béziers, France",
]);
return Inertia::render('Devis/Show', [
'devis' => $devi,
'settings' => $settings
]);
}
public function edit(Devis $devi)
{
return Inertia::render('Devis/Edit', [
'devis' => $devi
]);
}
public function update(Request $request, Devis $devi)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'destinataire_type' => 'required|string|in:commune,service_interne',
'destinataire_nom' => 'required|string|max:255',
'referent' => 'nullable|string|max:255',
'date_devis' => 'required|date',
'status' => 'required|string|in:draft,sent,accepted,rejected',
'items' => 'nullable|array',
'tva_rate' => 'nullable|numeric|min:0|max:100',
'notes' => 'nullable|string',
]);
// Calculer les montants
$totalHt = 0;
if (!empty($validated['items'])) {
foreach ($validated['items'] as $item) {
$qty = floatval($item['quantite'] ?? 0);
$price = floatval($item['prix_unitaire'] ?? 0);
$totalHt += $qty * $price;
}
}
$tvaRate = floatval($validated['tva_rate'] ?? 20);
$totalTtc = $totalHt * (1 + ($tvaRate / 100));
$validated['total_ht'] = $totalHt;
$validated['total_ttc'] = $totalTtc;
$devi->update($validated);
return redirect()->route('devis.index')->with('success', 'Devis mis à jour avec succès.');
}
public function destroy(Devis $devi)
{
$devi->delete();
return redirect()->route('devis.index')->with('success', 'Devis supprimé avec succès.');
}
public function settings()
{
$settings = DevisSetting::first() ?? DevisSetting::create([
'entity_name' => "Direction des Systèmes d'Information",
'entity_sub' => "Communauté d'Agglomération Béziers Méditerranée",
'email' => "support.informatique@beziers-mediterranee.fr",
'telephone' => "04.67.01.60.00",
'adresse' => "Béziers, France",
]);
return Inertia::render('Devis/Settings', [
'settings' => $settings
]);
}
public function updateSettings(Request $request)
{
$validated = $request->validate([
'entity_name' => 'required|string|max:255',
'entity_sub' => 'required|string|max:255',
'email' => 'required|email|max:255',
'telephone' => 'required|string|max:50',
'adresse' => 'nullable|string|max:255',
]);
$settings = DevisSetting::first();
if ($settings) {
$settings->update($validated);
} else {
DevisSetting::create($validated);
}
return redirect()->route('devis.index')->with('success', 'Paramètres du devis mis à jour avec succès.');
}
public function downloadPdf(Devis $devi)
{
$settings = DevisSetting::first() ?? DevisSetting::create([
'entity_name' => "Direction des Systèmes d'Information",
'entity_sub' => "Communauté d'Agglomération Béziers Méditerranée",
'email' => "support.informatique@beziers-mediterranee.fr",
'telephone' => "04.67.01.60.00",
'adresse' => "Béziers, France",
]);
$pdf = Pdf::loadView('pdf.devis', [
'devis' => $devi,
'settings' => $settings,
]);
return $pdf->download("devis-{$devi->number}.pdf");
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BesoinInformatique extends Model
{
protected $guarded = [];
protected $casts = [
'materiels_support' => 'array',
'licences_bureautique' => 'array',
'licences_projets' => 'array',
'date_validation' => 'date',
];
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Devis extends Model
{
protected $guarded = [];
protected $casts = [
'items' => 'array',
'date_devis' => 'date',
'total_ht' => 'decimal:2',
'total_ttc' => 'decimal:2',
'tva_rate' => 'decimal:2',
];
protected static function booted()
{
static::creating(function ($devis) {
if (empty($devis->number)) {
$year = date('Y');
$prefix = sprintf('DEV-%s-', $year);
$last = static::where('number', 'like', $prefix . '%')
->max('number');
$next = $last ? (intval(substr($last, strlen($prefix))) + 1) : 1;
$devis->number = $prefix . sprintf('%04d', $next);
}
});
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DevisSetting extends Model
{
protected $guarded = [];
}
+1
View File
@@ -7,6 +7,7 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.3", "php": "^8.3",
"barryvdh/laravel-dompdf": "^3.1",
"inertiajs/inertia-laravel": "^2.0", "inertiajs/inertia-laravel": "^2.0",
"laravel/framework": "^13.8", "laravel/framework": "^13.8",
"laravel/sanctum": "^4.0", "laravel/sanctum": "^4.0",
Generated
+523 -1
View File
@@ -4,8 +4,85 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "467a021a92214a0f1ddff604c3db2f6d", "content-hash": "4f62a75d8d199b96cde494e8eb771347",
"packages": [ "packages": [
{
"name": "barryvdh/laravel-dompdf",
"version": "v3.1.2",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-dompdf.git",
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^3.0",
"illuminate/support": "^9|^10|^11|^12|^13.0",
"php": "^8.1"
},
"require-dev": {
"larastan/larastan": "^2.7|^3.0",
"orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
"phpro/grumphp": "^2.5",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
},
"providers": [
"Barryvdh\\DomPDF\\ServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Barryvdh\\DomPDF\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "A DOMPDF Wrapper for Laravel",
"keywords": [
"dompdf",
"laravel",
"pdf"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2026-02-21T08:51:10+00:00"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.14.8", "version": "0.14.8",
@@ -377,6 +454,161 @@
], ],
"time": "2024-02-05T11:56:58+00:00" "time": "2024-02-05T11:56:58+00:00"
}, },
{
"name": "dompdf/dompdf",
"version": "v3.1.5",
"source": {
"type": "git",
"url": "https://github.com/dompdf/dompdf.git",
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
"shasum": ""
},
"require": {
"dompdf/php-font-lib": "^1.0.0",
"dompdf/php-svg-lib": "^1.0.0",
"ext-dom": "*",
"ext-mbstring": "*",
"masterminds/html5": "^2.0",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"ext-gd": "*",
"ext-json": "*",
"ext-zip": "*",
"mockery/mockery": "^1.3",
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
"squizlabs/php_codesniffer": "^3.5",
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
},
"suggest": {
"ext-gd": "Needed to process images",
"ext-gmagick": "Improves image processing performance",
"ext-imagick": "Improves image processing performance",
"ext-zlib": "Needed for pdf stream compression"
},
"type": "library",
"autoload": {
"psr-4": {
"Dompdf\\": "src/"
},
"classmap": [
"lib/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "The Dompdf Community",
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
}
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
"support": {
"issues": "https://github.com/dompdf/dompdf/issues",
"source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
},
"time": "2026-03-03T13:54:37+00:00"
},
{
"name": "dompdf/php-font-lib",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-font-lib.git",
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
},
"type": "library",
"autoload": {
"psr-4": {
"FontLib\\": "src/FontLib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "The FontLib Community",
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/dompdf/php-font-lib",
"support": {
"issues": "https://github.com/dompdf/php-font-lib/issues",
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
},
"time": "2026-01-20T14:10:26+00:00"
},
{
"name": "dompdf/php-svg-lib",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/dompdf/php-svg-lib.git",
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^7.1 || ^8.0",
"sabberworm/php-css-parser": "^8.4 || ^9.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
},
"type": "library",
"autoload": {
"psr-4": {
"Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "The SvgLib Community",
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
}
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/dompdf/php-svg-lib",
"support": {
"issues": "https://github.com/dompdf/php-svg-lib/issues",
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
},
"time": "2026-01-02T16:01:13+00:00"
},
{ {
"name": "dragonmantank/cron-expression", "name": "dragonmantank/cron-expression",
"version": "v3.6.0", "version": "v3.6.0",
@@ -2166,6 +2398,73 @@
], ],
"time": "2026-03-08T20:05:35+00:00" "time": "2026-03-08T20:05:35+00:00"
}, },
{
"name": "masterminds/html5",
"version": "2.10.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
},
"time": "2025-07-25T09:04:22+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.10.0", "version": "3.10.0",
@@ -3441,6 +3740,86 @@
}, },
"time": "2025-12-14T04:43:48+00:00" "time": "2025-12-14T04:43:48+00:00"
}, },
{
"name": "sabberworm/php-css-parser",
"version": "v9.3.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "1.4.0",
"phpstan/extension-installer": "1.4.3",
"phpstan/phpstan": "1.12.32 || 2.1.32",
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
"phpunit/phpunit": "8.5.52",
"rawr/phpunit-data-provider": "3.3.1",
"rector/rector": "1.2.10 || 2.2.8",
"rector/type-perfect": "1.0.0 || 2.1.0",
"squizlabs/php_codesniffer": "4.0.1",
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "9.4.x-dev"
}
},
"autoload": {
"files": [
"src/Rule/Rule.php",
"src/RuleSet/RuleContainer.php"
],
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Jake Hotson",
"email": "jake.github@qzdesign.co.uk"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"support": {
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0"
},
"time": "2026-03-03T17:31:43+00:00"
},
{ {
"name": "symfony/clock", "name": "symfony/clock",
"version": "v7.4.8", "version": "v7.4.8",
@@ -6030,6 +6409,149 @@
], ],
"time": "2026-03-30T13:44:50+00:00" "time": "2026-03-30T13:44:50+00:00"
}, },
{
"name": "thecodingmachine/safe",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/thecodingmachine/safe.git",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpstan/phpstan": "^2",
"phpunit/phpunit": "^10",
"squizlabs/php_codesniffer": "^3.2"
},
"type": "library",
"autoload": {
"files": [
"lib/special_cases.php",
"generated/apache.php",
"generated/apcu.php",
"generated/array.php",
"generated/bzip2.php",
"generated/calendar.php",
"generated/classobj.php",
"generated/com.php",
"generated/cubrid.php",
"generated/curl.php",
"generated/datetime.php",
"generated/dir.php",
"generated/eio.php",
"generated/errorfunc.php",
"generated/exec.php",
"generated/fileinfo.php",
"generated/filesystem.php",
"generated/filter.php",
"generated/fpm.php",
"generated/ftp.php",
"generated/funchand.php",
"generated/gettext.php",
"generated/gmp.php",
"generated/gnupg.php",
"generated/hash.php",
"generated/ibase.php",
"generated/ibmDb2.php",
"generated/iconv.php",
"generated/image.php",
"generated/imap.php",
"generated/info.php",
"generated/inotify.php",
"generated/json.php",
"generated/ldap.php",
"generated/libxml.php",
"generated/lzf.php",
"generated/mailparse.php",
"generated/mbstring.php",
"generated/misc.php",
"generated/mysql.php",
"generated/mysqli.php",
"generated/network.php",
"generated/oci8.php",
"generated/opcache.php",
"generated/openssl.php",
"generated/outcontrol.php",
"generated/pcntl.php",
"generated/pcre.php",
"generated/pgsql.php",
"generated/posix.php",
"generated/ps.php",
"generated/pspell.php",
"generated/readline.php",
"generated/rnp.php",
"generated/rpminfo.php",
"generated/rrd.php",
"generated/sem.php",
"generated/session.php",
"generated/shmop.php",
"generated/sockets.php",
"generated/sodium.php",
"generated/solr.php",
"generated/spl.php",
"generated/sqlsrv.php",
"generated/ssdeep.php",
"generated/ssh2.php",
"generated/stream.php",
"generated/strings.php",
"generated/swoole.php",
"generated/uodbc.php",
"generated/uopz.php",
"generated/url.php",
"generated/var.php",
"generated/xdiff.php",
"generated/xml.php",
"generated/xmlrpc.php",
"generated/yaml.php",
"generated/yaz.php",
"generated/zip.php",
"generated/zlib.php"
],
"classmap": [
"lib/DateTime.php",
"lib/DateTimeImmutable.php",
"lib/Exceptions/",
"generated/Exceptions/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
"support": {
"issues": "https://github.com/thecodingmachine/safe/issues",
"source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
},
"funding": [
{
"url": "https://github.com/OskarStark",
"type": "github"
},
{
"url": "https://github.com/shish",
"type": "github"
},
{
"url": "https://github.com/silasjoisten",
"type": "github"
},
{
"url": "https://github.com/staabm",
"type": "github"
}
],
"time": "2026-02-04T18:08:13+00:00"
},
{ {
"name": "tightenco/ziggy", "name": "tightenco/ziggy",
"version": "v2.6.2", "version": "v2.6.2",
@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('besoin_informatiques', function (Blueprint $table) {
$table->id();
// Entête
$table->string('collectivite')->nullable();
$table->string('nom_referent')->nullable();
$table->text('descriptif_projet')->nullable();
// Service Support et Relations Usagers
$table->json('materiels_support')->nullable(); // Liste PC / Portables / Ecran etc.
$table->json('licences_bureautique')->nullable(); // Liste Licences Bureautique
// Infrastructures Système et Télécommunications
$table->text('equipements_reseaux')->nullable();
$table->text('cablage_reseau')->nullable();
$table->text('telephonie_ip')->nullable();
$table->text('nouveau_batiment')->nullable();
// Etudes Projets et Dématérialisation
$table->text('logiciels_metiers')->nullable();
$table->json('licences_projets')->nullable(); // Liste Renouvellement ou achat
// Validation
$table->date('date_validation')->nullable();
$table->string('validation_dgs')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('besoin_informatiques');
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('devis', function (Blueprint $table) {
$table->id();
$table->string('number')->unique();
$table->string('title');
$table->string('destinataire_type'); // 'commune', 'service_interne'
$table->string('destinataire_nom');
$table->string('referent')->nullable();
$table->date('date_devis');
$table->string('status')->default('draft'); // 'draft', 'sent', 'accepted', 'rejected'
$table->json('items')->nullable(); // details of items (type, designation, quantity, unit_price, total_price)
$table->decimal('total_ht', 15, 2)->default(0.00);
$table->decimal('tva_rate', 5, 2)->default(20.00);
$table->decimal('total_ttc', 15, 2)->default(0.00);
$table->text('notes')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('devis');
}
};
@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('devis_settings', function (Blueprint $table) {
$table->id();
$table->string('entity_name')->default("Direction des Systèmes d'Information");
$table->string('entity_sub')->default("Communauté d'Agglomération Béziers Méditerranée");
$table->string('email')->default("support.informatique@beziers-mediterranee.fr");
$table->string('telephone')->default("04.67.01.60.00");
$table->string('adresse')->nullable()->default("Béziers, France");
$table->timestamps();
});
\DB::table('devis_settings')->insert([
'entity_name' => "Direction des Systèmes d'Information",
'entity_sub' => "Communauté d'Agglomération Béziers Méditerranée",
'email' => "support.informatique@beziers-mediterranee.fr",
'telephone' => "04.67.01.60.00",
'adresse' => "Béziers, France",
'created_at' => now(),
'updated_at' => now(),
]);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('devis_settings');
}
};
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "SuivisCommande", "name": "OrderCheck",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

@@ -51,6 +51,18 @@ const showingNavigationDropdown = ref(false);
> >
Matériels Matériels
</NavLink> </NavLink>
<NavLink
:href="route('besoins-informatiques.index')"
:active="route().current('besoins-informatiques.*')"
>
Besoins Informatiques
</NavLink>
<NavLink
:href="route('devis.index')"
:active="route().current('devis.*')"
>
Gestion des Devis
</NavLink>
</div> </div>
</div> </div>
@@ -170,6 +182,18 @@ const showingNavigationDropdown = ref(false);
> >
Matériels Matériels
</ResponsiveNavLink> </ResponsiveNavLink>
<ResponsiveNavLink
:href="route('besoins-informatiques.index')"
:active="route().current('besoins-informatiques.*')"
>
Besoins Informatiques
</ResponsiveNavLink>
<ResponsiveNavLink
:href="route('devis.index')"
:active="route().current('devis.*')"
>
Gestion des Devis
</ResponsiveNavLink>
</div> </div>
<!-- Responsive Settings Options --> <!-- Responsive Settings Options -->
@@ -0,0 +1,219 @@
<script setup>
import { Head, useForm, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const form = useForm({
collectivite: '',
nom_referent: '',
descriptif_projet: '',
materiels_support: [
{ type: '', agent: '' }
],
licences_bureautique: [
{ licence: '', type: '', destination: '', quantite: 1 }
],
equipements_reseaux: '',
cablage_reseau: '',
telephonie_ip: '',
nouveau_batiment: '',
logiciels_metiers: '',
licences_projets: [
{ action: '', nom: '', domaine: '', quantite: 1 }
],
});
const submit = () => {
form.post(route('besoins-informatiques.store'));
};
const addMateriel = () => form.materiels_support.push({ type: '', agent: '' });
const removeMateriel = (index) => form.materiels_support.splice(index, 1);
const addLicenceBureautique = () => form.licences_bureautique.push({ licence: '', type: '', destination: '', quantite: 1 });
const removeLicenceBureautique = (index) => form.licences_bureautique.splice(index, 1);
const addLicenceProjet = () => form.licences_projets.push({ action: '', nom: '', domaine: '', quantite: 1 });
const removeLicenceProjet = (index) => form.licences_projets.splice(index, 1);
</script>
<template>
<Head title="Nouveau Besoin Informatique" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Saisir un besoin informatique (Mutualisation)
</h2>
<Link
:href="route('besoins-informatiques.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
>
Retour
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<form @submit.prevent="submit" class="space-y-6">
<!-- Entête du budget -->
<div class="bg-[#000066] text-white rounded-xl shadow-sm overflow-hidden p-6 text-center">
<h1 class="text-lg font-bold tracking-wider uppercase">PRÉPARATION BUDGET 2027</h1>
<p class="text-sm font-semibold mt-1">FICHE Synthèse des Besoins Informatiques MUTUALISATION</p>
</div>
<!-- Informations Générales -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="p-6 space-y-6">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 border-b border-slate-100 dark:border-slate-850 pb-3">Informations Générales</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-bold text-slate-700 dark:text-slate-300 mb-1">Collectivité :</label>
<input v-model="form.collectivite" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
</div>
<div>
<label class="block text-sm font-bold text-slate-700 dark:text-slate-300 mb-1">Nom du référent :</label>
<input v-model="form.nom_referent" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-700 dark:text-slate-300 mb-1">Descriptif / objectif du projet :</label>
<textarea v-model="form.descriptif_projet" rows="3" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"></textarea>
</div>
</div>
</div>
</div>
<!-- Service Support -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden flex flex-col md:flex-row">
<div class="bg-[#002060] p-6 md:w-56 flex-shrink-0 flex flex-col justify-center text-white">
<h3 class="text-md font-bold tracking-wide text-center md:text-left">Service Support et Relations Usagers</h3>
<p class="text-xxs opacity-80 mt-2 hidden md:block">Contact : Frédéric FERNANDEZ fredericfernandez@beziers-mediterranee.fr</p>
</div>
<div class="p-6 flex-1 space-y-6">
<!-- Matériel -->
<div>
<div class="flex items-center justify-between mb-3 border-b border-slate-100 dark:border-slate-850 pb-2">
<h4 class="text-sm font-bold text-slate-850 dark:text-slate-250">Type de matériel (PC / Portables / Ecran / Tel...)</h4>
<button type="button" @click="addMateriel" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-2 py-1 rounded-md border border-sky-100 dark:border-sky-900/60">+ Ajouter</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.materiels_support" :key="index" class="flex items-start gap-2">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-2">
<input v-model="item.type" type="text" placeholder="Type de matériel" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.agent" type="text" placeholder="Nom de l'agent / Service" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<button type="button" @click="removeMateriel(index)" class="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
<!-- Licences -->
<div>
<div class="flex items-center justify-between mb-3 border-b border-slate-100 dark:border-slate-850 pb-2">
<h4 class="text-sm font-bold text-slate-850 dark:text-slate-250">Besoins de Licences Bureautique</h4>
<button type="button" @click="addLicenceBureautique" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-2 py-1 rounded-md border border-sky-100 dark:border-sky-900/60">+ Ajouter</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.licences_bureautique" :key="index" class="flex items-start gap-2">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-4 gap-2">
<input v-model="item.licence" type="text" placeholder="Licence bureautique" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.type" type="text" placeholder="Type de Licence" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.destination" type="text" placeholder="PC neuf ou existant" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.quantite" type="number" min="1" placeholder="Quantité" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<button type="button" @click="removeLicenceBureautique(index)" class="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Infrastructures -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden flex flex-col md:flex-row">
<div class="bg-[#2f5597] p-6 md:w-56 flex-shrink-0 flex flex-col justify-center text-white">
<h3 class="text-md font-bold tracking-wide text-center md:text-left">Infrastructures Système et Télécommunications</h3>
<p class="text-xxs opacity-80 mt-2 hidden md:block">Contact : Jérémy BAYSE 04.99.41.33.18. jeremybayse@beziers-mediterranee.fr</p>
</div>
<div class="p-6 flex-1 space-y-4">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Équipements réseaux - serveur :</label>
<input v-model="form.equipements_reseaux" type="text" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Travaux de câblage réseau :</label>
<input v-model="form.cablage_reseau" type="text" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Projet dans le domaine téléphonie IP :</label>
<input v-model="form.telephonie_ip" type="text" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<div>
<label class="block text-xs font-bold text-rose-600 dark:text-rose-450 mb-1">Nouvelle structure ou bâtiment planifié en 2027 et nécessitant un raccordement informatique :</label>
<textarea v-model="form.nouveau_batiment" rows="2" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"></textarea>
</div>
</div>
</div>
<!-- Etudes Projets -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden flex flex-col md:flex-row">
<div class="bg-[#8faadc] p-6 md:w-56 flex-shrink-0 flex flex-col justify-center text-slate-900">
<h3 class="text-md font-bold tracking-wide text-center md:text-left">Etudes Projets et Dématérialisation</h3>
<p class="text-xxs opacity-85 mt-2 hidden md:block">Contact projets logiciels : Gaëtan PRULHIERE gaetanprulhiere@beziers-mediterranee.fr</p>
</div>
<div class="p-6 flex-1 space-y-6">
<div>
<label class="block text-xs font-bold text-slate-750 dark:text-slate-300 mb-1">Projet d'acquisition de logiciels métiers ou projet de dématérialisation :</label>
<p class="text-xxs text-slate-500 dark:text-slate-400 italic mb-2">Toute demande devra intégrer un chiffrage à l'appui de la demande</p>
<textarea v-model="form.logiciels_metiers" rows="3" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"></textarea>
</div>
<div>
<div class="flex items-center justify-between mb-3 border-b border-slate-100 dark:border-slate-850 pb-2">
<h4 class="text-sm font-bold text-slate-850 dark:text-slate-250">Besoins ou Renouvellement de Licences</h4>
<button type="button" @click="addLicenceProjet" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-2 py-1 rounded-md border border-sky-100 dark:border-sky-900/60">+ Ajouter</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.licences_projets" :key="index" class="flex items-start gap-2">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-4 gap-2">
<input v-model="item.action" type="text" placeholder="Renouvellement ou achat" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.nom" type="text" placeholder="Nom de la Licence (« Adobe », « AutoCAD », GED...)" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.domaine" type="text" placeholder="Domaine (Enfance, RH, Police...)" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.quantite" type="number" min="1" placeholder="Quantité" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<button type="button" @click="removeLicenceProjet(index)" class="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Barre d'actions -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-800">
<Link
:href="route('besoins-informatiques.index')"
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
Annuler
</Link>
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors disabled:opacity-50"
>
Enregistrer la Fiche
</button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
@@ -0,0 +1,231 @@
<script setup>
import { Head, useForm, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { onMounted } from 'vue';
const props = defineProps({
besoin: Object,
});
const form = useForm({
collectivite: props.besoin.collectivite || '',
nom_referent: props.besoin.nom_referent || '',
descriptif_projet: props.besoin.descriptif_projet || '',
materiels_support: props.besoin.materiels_support || [{ type: '', agent: '' }],
licences_bureautique: props.besoin.licences_bureautique || [{ licence: '', type: '', destination: '', quantite: 1 }],
equipements_reseaux: props.besoin.equipements_reseaux || '',
cablage_reseau: props.besoin.cablage_reseau || '',
telephonie_ip: props.besoin.telephonie_ip || '',
nouveau_batiment: props.besoin.nouveau_batiment || '',
logiciels_metiers: props.besoin.logiciels_metiers || '',
licences_projets: props.besoin.licences_projets || [{ action: '', nom: '', domaine: '', quantite: 1 }],
});
// Sécurités pour éviter que le formulaire crashe si les arrays sont vides depuis la DB
onMounted(() => {
if (!form.materiels_support || form.materiels_support.length === 0) {
form.materiels_support = [{ type: '', agent: '' }];
}
if (!form.licences_bureautique || form.licences_bureautique.length === 0) {
form.licences_bureautique = [{ licence: '', type: '', destination: '', quantite: 1 }];
}
if (!form.licences_projets || form.licences_projets.length === 0) {
form.licences_projets = [{ action: '', nom: '', domaine: '', quantite: 1 }];
}
});
const submit = () => {
form.put(route('besoins-informatiques.update', props.besoin.id));
};
const addMateriel = () => form.materiels_support.push({ type: '', agent: '' });
const removeMateriel = (index) => form.materiels_support.splice(index, 1);
const addLicenceBureautique = () => form.licences_bureautique.push({ licence: '', type: '', destination: '', quantite: 1 });
const removeLicenceBureautique = (index) => form.licences_bureautique.splice(index, 1);
const addLicenceProjet = () => form.licences_projets.push({ action: '', nom: '', domaine: '', quantite: 1 });
const removeLicenceProjet = (index) => form.licences_projets.splice(index, 1);
</script>
<template>
<Head title="Modifier le Besoin Informatique" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Modifier la fiche de synthèse (Mutualisation)
</h2>
<Link
:href="route('besoins-informatiques.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
>
Retour
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<form @submit.prevent="submit" class="space-y-6">
<!-- Entête du budget -->
<div class="bg-[#000066] text-white rounded-xl shadow-sm overflow-hidden p-6 text-center">
<h1 class="text-lg font-bold tracking-wider uppercase">PRÉPARATION BUDGET 2027</h1>
<p class="text-sm font-semibold mt-1">FICHE Synthèse des Besoins Informatiques MUTUALISATION</p>
</div>
<!-- Informations Générales -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="p-6 space-y-6">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 border-b border-slate-100 dark:border-slate-850 pb-3">Informations Générales</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-bold text-slate-700 dark:text-slate-300 mb-1">Collectivité :</label>
<input v-model="form.collectivite" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
</div>
<div>
<label class="block text-sm font-bold text-slate-700 dark:text-slate-300 mb-1">Nom du référent :</label>
<input v-model="form.nom_referent" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-700 dark:text-slate-300 mb-1">Descriptif / objectif du projet :</label>
<textarea v-model="form.descriptif_projet" rows="3" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"></textarea>
</div>
</div>
</div>
</div>
<!-- Service Support -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden flex flex-col md:flex-row">
<div class="bg-[#002060] p-6 md:w-56 flex-shrink-0 flex flex-col justify-center text-white">
<h3 class="text-md font-bold tracking-wide text-center md:text-left">Service Support et Relations Usagers</h3>
<p class="text-xxs opacity-80 mt-2 hidden md:block">Contact : Frédéric FERNANDEZ fredericfernandez@beziers-mediterranee.fr</p>
</div>
<div class="p-6 flex-1 space-y-6">
<!-- Matériel -->
<div>
<div class="flex items-center justify-between mb-3 border-b border-slate-100 dark:border-slate-850 pb-2">
<h4 class="text-sm font-bold text-slate-850 dark:text-slate-250">Type de matériel (PC / Portables / Ecran / Tel...)</h4>
<button type="button" @click="addMateriel" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-2 py-1 rounded-md border border-sky-100 dark:border-sky-900/60">+ Ajouter</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.materiels_support" :key="index" class="flex items-start gap-2">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-2 gap-2">
<input v-model="item.type" type="text" placeholder="Type de matériel" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.agent" type="text" placeholder="Nom de l'agent / Service" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<button type="button" @click="removeMateriel(index)" class="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
<!-- Licences -->
<div>
<div class="flex items-center justify-between mb-3 border-b border-slate-100 dark:border-slate-850 pb-2">
<h4 class="text-sm font-bold text-slate-850 dark:text-slate-250">Besoins de Licences Bureautique</h4>
<button type="button" @click="addLicenceBureautique" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-2 py-1 rounded-md border border-sky-100 dark:border-sky-900/60">+ Ajouter</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.licences_bureautique" :key="index" class="flex items-start gap-2">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-4 gap-2">
<input v-model="item.licence" type="text" placeholder="Licence bureautique" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.type" type="text" placeholder="Type de Licence" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.destination" type="text" placeholder="PC neuf ou existant" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.quantite" type="number" min="1" placeholder="Quantité" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<button type="button" @click="removeLicenceBureautique(index)" class="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Infrastructures -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden flex flex-col md:flex-row">
<div class="bg-[#2f5597] p-6 md:w-56 flex-shrink-0 flex flex-col justify-center text-white">
<h3 class="text-md font-bold tracking-wide text-center md:text-left">Infrastructures Système et Télécommunications</h3>
<p class="text-xxs opacity-80 mt-2 hidden md:block">Contact : Jérémy BAYSE 04.99.41.33.18. jeremybayse@beziers-mediterranee.fr</p>
</div>
<div class="p-6 flex-1 space-y-4">
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Équipements réseaux - serveur :</label>
<input v-model="form.equipements_reseaux" type="text" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Travaux de câblage réseau :</label>
<input v-model="form.cablage_reseau" type="text" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<div>
<label class="block text-xs font-bold text-slate-700 dark:text-slate-300 mb-1">Projet dans le domaine téléphonie IP :</label>
<input v-model="form.telephonie_ip" type="text" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<div>
<label class="block text-xs font-bold text-rose-600 dark:text-rose-450 mb-1">Nouvelle structure ou bâtiment planifié en 2027 et nécessitant un raccordement informatique :</label>
<textarea v-model="form.nouveau_batiment" rows="2" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"></textarea>
</div>
</div>
</div>
<!-- Etudes Projets -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden flex flex-col md:flex-row">
<div class="bg-[#8faadc] p-6 md:w-56 flex-shrink-0 flex flex-col justify-center text-slate-900">
<h3 class="text-md font-bold tracking-wide text-center md:text-left">Etudes Projets et Dématérialisation</h3>
<p class="text-xxs opacity-85 mt-2 hidden md:block">Contact projets logiciels : Gaëtan PRULHIERE gaetanprulhiere@beziers-mediterranee.fr</p>
</div>
<div class="p-6 flex-1 space-y-6">
<div>
<label class="block text-xs font-bold text-slate-750 dark:text-slate-300 mb-1">Projet d'acquisition de logiciels métiers ou projet de dématérialisation :</label>
<p class="text-xxs text-slate-500 dark:text-slate-400 italic mb-2">Toute demande devra intégrer un chiffrage à l'appui de la demande</p>
<textarea v-model="form.logiciels_metiers" rows="3" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"></textarea>
</div>
<div>
<div class="flex items-center justify-between mb-3 border-b border-slate-100 dark:border-slate-850 pb-2">
<h4 class="text-sm font-bold text-slate-850 dark:text-slate-250">Besoins ou Renouvellement de Licences</h4>
<button type="button" @click="addLicenceProjet" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-2 py-1 rounded-md border border-sky-100 dark:border-sky-900/60">+ Ajouter</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.licences_projets" :key="index" class="flex items-start gap-2">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-4 gap-2">
<input v-model="item.action" type="text" placeholder="Renouvellement ou achat" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.nom" type="text" placeholder="Nom de la Licence (« Adobe », « AutoCAD », GED...)" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.domaine" type="text" placeholder="Domaine (Enfance, RH, Police...)" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<input v-model="item.quantite" type="number" min="1" placeholder="Quantité" class="text-xs w-full rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
<button type="button" @click="removeLicenceProjet(index)" class="p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Barre d'actions -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-800">
<Link
:href="route('besoins-informatiques.index')"
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
Annuler
</Link>
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors disabled:opacity-50"
>
Mettre à jour
</button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
@@ -0,0 +1,113 @@
<script setup>
import { Head, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
defineProps({
besoins: Array,
});
</script>
<template>
<Head title="Besoins Informatiques (Mutualisation)" />
<AuthenticatedLayout>
<template #header>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Besoins Informatiques (Mutualisation)
</h2>
<div class="flex gap-2">
<Link
:href="route('besoins-informatiques.create')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Saisir un besoin
</Link>
</div>
</div>
</template>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Notifications flash -->
<div v-if="$page.props.flash?.success" class="mb-6 p-4 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-lg dark:bg-emerald-950/40 dark:border-emerald-900 dark:text-emerald-300 flex items-center shadow-sm">
<svg class="w-5 h-5 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span>{{ $page.props.flash?.success }}</span>
</div>
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-50 border-b border-slate-100 dark:bg-slate-950/40 dark:border-slate-850 text-xxs font-semibold uppercase tracking-wider text-slate-400">
<th class="px-6 py-4">Collectivité</th>
<th class="px-6 py-4">Référent</th>
<th class="px-6 py-4">Date de saisie</th>
<th class="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-850 text-sm">
<tr
v-for="besoin in besoins"
:key="besoin.id"
class="hover:bg-slate-50/50 dark:hover:bg-slate-950/20 transition-colors"
>
<td class="px-6 py-4 font-bold text-slate-800 dark:text-slate-200">
{{ besoin.collectivite || 'Non spécifié' }}
</td>
<td class="px-6 py-4 text-slate-800 dark:text-slate-200 font-medium">
{{ besoin.nom_referent }}
</td>
<td class="px-6 py-4 font-mono text-xs text-slate-600 dark:text-slate-350">
{{ new Date(besoin.created_at).toLocaleDateString() }}
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-3">
<Link
:href="route('besoins-informatiques.edit', besoin.id)"
class="text-slate-500 hover:text-amber-500 dark:text-slate-400 dark:hover:text-amber-400"
title="Modifier la fiche"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 18.75a4.48 4.48 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</Link>
<Link
:href="route('besoins-informatiques.destroy', besoin.id)"
method="delete"
as="button"
class="text-slate-500 hover:text-rose-600 dark:text-slate-400 dark:hover:text-rose-500"
title="Supprimer la fiche"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</Link>
</div>
</td>
</tr>
<tr v-if="besoins.length === 0">
<td colspan="4" class="px-6 py-12 text-center">
<div class="flex flex-col items-center justify-center text-slate-500 dark:text-slate-400">
<svg class="w-12 h-12 mb-4 text-slate-300 dark:text-slate-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
<p class="text-lg font-medium">Aucun besoin informatique enregistré</p>
<p class="text-sm mt-1">Commencez par créer une nouvelle fiche de synthèse.</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
+229
View File
@@ -0,0 +1,229 @@
<script setup>
import { ref, computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const form = useForm({
title: '',
destinataire_type: 'commune',
destinataire_nom: '',
referent: '',
date_devis: new Date().toISOString().split('T')[0],
status: 'draft',
items: [
{ type: 'Matériel', designation: '', quantite: 1, prix_unitaire: 0 }
],
tva_rate: 20,
notes: '',
});
const itemTypes = ['Matériel', 'Licence', 'Prestation', 'Infrastructure', 'Autre'];
const addItem = () => {
form.items.push({ type: 'Matériel', designation: '', quantite: 1, prix_unitaire: 0 });
};
const removeItem = (index) => {
form.items.splice(index, 1);
if (form.items.length === 0) {
addItem();
}
};
// Computes totals on the client side
const totalHt = computed(() => {
return form.items.reduce((sum, item) => {
const qty = parseFloat(item.quantite) || 0;
const price = parseFloat(item.prix_unitaire) || 0;
return sum + (qty * price);
}, 0);
});
const tvaAmount = computed(() => {
const rate = parseFloat(form.tva_rate) || 0;
return totalHt.value * (rate / 100);
});
const totalTtc = computed(() => {
return totalHt.value + tvaAmount.value;
});
const submit = () => {
form.post(route('devis.store'));
};
const formatCurrency = (value) => {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
};
</script>
<template>
<Head title="Créer un Devis" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Nouveau Devis
</h2>
<Link
:href="route('devis.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
>
Retour
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<form @submit.prevent="submit" class="space-y-6">
<!-- Fiche / En-tête -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6 space-y-6">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 border-b border-slate-100 dark:border-slate-850 pb-3">
Informations Générales
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Objet / Titre -->
<div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-750 dark:text-slate-300 mb-1">Objet / Description du devis :</label>
<input v-model="form.title" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Devis pour renouvellement de postes de travail" required>
</div>
<!-- Type Destinataire -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Type de destinataire :</label>
<select v-model="form.destinataire_type" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<option value="commune">Commune (Collectivité extérieure)</option>
<option value="service_interne">Service Interne (Direction / Service)</option>
</select>
</div>
<!-- Nom Destinataire -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Nom du destinataire :</label>
<input v-model="form.destinataire_nom" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Mairie de Corneilhan, Service Finance..." required>
</div>
<!-- Contact / Référent -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Référent / Interlocuteur :</label>
<input v-model="form.referent" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Jean Dupont">
</div>
<!-- Date du Devis -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Date du devis :</label>
<input v-model="form.date_devis" type="date" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
</div>
<!-- Statut -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Statut initial :</label>
<select v-model="form.status" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<option value="draft">Brouillon</option>
<option value="sent">Envoyé</option>
<option value="accepted">Accepté</option>
<option value="rejected">Refusé</option>
</select>
</div>
<!-- Taux TVA -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Taux TVA (%) :</label>
<input v-model="form.tva_rate" type="number" min="0" max="100" step="0.1" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
</div>
</div>
<!-- Items du Devis -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6 space-y-4">
<div class="flex items-center justify-between border-b border-slate-100 dark:border-slate-850 pb-3">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200">
Lignes du Devis
</h3>
<button type="button" @click="addItem" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-3 py-1.5 rounded-lg border border-sky-100 dark:border-sky-900/60 transition-colors">
+ Ajouter une ligne
</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.items" :key="index" class="flex items-start gap-3 bg-slate-50 dark:bg-slate-950/50 p-4 rounded-xl border border-slate-100 dark:border-slate-850">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-12 gap-3">
<!-- Type -->
<div class="sm:col-span-3">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Catégorie</label>
<select v-model="item.type" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200">
<option v-for="t in itemTypes" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<!-- Designation -->
<div class="sm:col-span-5">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Description / Matériel / Prestation</label>
<input v-model="item.designation" type="text" placeholder="Désignation" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
<!-- Quantité -->
<div class="sm:col-span-2">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Quantité</label>
<input v-model="item.quantite" type="number" min="1" step="1" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
<!-- Prix unitaire -->
<div class="sm:col-span-2">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">P.U. HT ()</label>
<input v-model="item.prix_unitaire" type="number" min="0" step="0.01" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
</div>
<button type="button" @click="removeItem(index)" class="mt-5 p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<!-- Récapitulatif Financier -->
<div class="bg-slate-50 dark:bg-slate-950 p-4 rounded-xl border border-slate-200 dark:border-slate-800 flex flex-col items-end space-y-2 text-sm">
<div class="flex justify-between w-64">
<span class="text-slate-500">Total HT :</span>
<span class="font-bold text-slate-850 dark:text-slate-200">{{ formatCurrency(totalHt) }}</span>
</div>
<div class="flex justify-between w-64 border-b border-slate-200 dark:border-slate-800 pb-2">
<span class="text-slate-500">TVA ({{ form.tva_rate }}%) :</span>
<span class="font-semibold text-slate-850 dark:text-slate-200">{{ formatCurrency(tvaAmount) }}</span>
</div>
<div class="flex justify-between w-64 text-base font-extrabold text-sky-600 dark:text-sky-400 pt-1">
<span>Total TTC :</span>
<span>{{ formatCurrency(totalTtc) }}</span>
</div>
</div>
</div>
<!-- Notes / Conditions -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6">
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Notes / Conditions de livraison / Remarques :</label>
<textarea v-model="form.notes" rows="3" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Devis valable 30 jours. Matériel disponible sous 15 jours."></textarea>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-800">
<Link
:href="route('devis.index')"
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
Annuler
</Link>
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors disabled:opacity-50"
>
Enregistrer le Devis
</button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
+233
View File
@@ -0,0 +1,233 @@
<script setup>
import { ref, computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const props = defineProps({
devis: Object,
});
const form = useForm({
title: props.devis.title,
destinataire_type: props.devis.destinataire_type,
destinataire_nom: props.devis.destinataire_nom,
referent: props.devis.referent || '',
date_devis: props.devis.date_devis ? new Date(props.devis.date_devis).toISOString().split('T')[0] : '',
status: props.devis.status,
items: props.devis.items || [
{ type: 'Matériel', designation: '', quantite: 1, prix_unitaire: 0 }
],
tva_rate: parseFloat(props.devis.tva_rate) || 20,
notes: props.devis.notes || '',
});
const itemTypes = ['Matériel', 'Licence', 'Prestation', 'Infrastructure', 'Autre'];
const addItem = () => {
form.items.push({ type: 'Matériel', designation: '', quantite: 1, prix_unitaire: 0 });
};
const removeItem = (index) => {
form.items.splice(index, 1);
if (form.items.length === 0) {
addItem();
}
};
// Computes totals on the client side
const totalHt = computed(() => {
return form.items.reduce((sum, item) => {
const qty = parseFloat(item.quantite) || 0;
const price = parseFloat(item.prix_unitaire) || 0;
return sum + (qty * price);
}, 0);
});
const tvaAmount = computed(() => {
const rate = parseFloat(form.tva_rate) || 0;
return totalHt.value * (rate / 100);
});
const totalTtc = computed(() => {
return totalHt.value + tvaAmount.value;
});
const submit = () => {
form.put(route('devis.update', props.devis.id));
};
const formatCurrency = (value) => {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
};
</script>
<template>
<Head :title="`Modifier le Devis ${devis.number}`" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Modifier le Devis {{ devis.number }}
</h2>
<Link
:href="route('devis.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
>
Retour
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<form @submit.prevent="submit" class="space-y-6">
<!-- Fiche / En-tête -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6 space-y-6">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 border-b border-slate-100 dark:border-slate-850 pb-3">
Informations Générales
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Objet / Titre -->
<div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-750 dark:text-slate-300 mb-1">Objet / Description du devis :</label>
<input v-model="form.title" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Devis pour renouvellement de postes de travail" required>
</div>
<!-- Type Destinataire -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Type de destinataire :</label>
<select v-model="form.destinataire_type" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<option value="commune">Commune (Collectivité extérieure)</option>
<option value="service_interne">Service Interne (Direction / Service)</option>
</select>
</div>
<!-- Nom Destinataire -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Nom du destinataire :</label>
<input v-model="form.destinataire_nom" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Mairie de Corneilhan, Service Finance..." required>
</div>
<!-- Contact / Référent -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Référent / Interlocuteur :</label>
<input v-model="form.referent" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Jean Dupont">
</div>
<!-- Date du Devis -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Date du devis :</label>
<input v-model="form.date_devis" type="date" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
</div>
<!-- Statut -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Statut :</label>
<select v-model="form.status" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<option value="draft">Brouillon</option>
<option value="sent">Envoyé</option>
<option value="accepted">Accepté</option>
<option value="rejected">Refusé</option>
</select>
</div>
<!-- Taux TVA -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Taux TVA (%) :</label>
<input v-model="form.tva_rate" type="number" min="0" max="100" step="0.1" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
</div>
</div>
</div>
<!-- Items du Devis -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6 space-y-4">
<div class="flex items-center justify-between border-b border-slate-100 dark:border-slate-850 pb-3">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200">
Lignes du Devis
</h3>
<button type="button" @click="addItem" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-3 py-1.5 rounded-lg border border-sky-100 dark:border-sky-900/60 transition-colors">
+ Ajouter une ligne
</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.items" :key="index" class="flex items-start gap-3 bg-slate-50 dark:bg-slate-950/50 p-4 rounded-xl border border-slate-100 dark:border-slate-850">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-12 gap-3">
<!-- Type -->
<div class="sm:col-span-3">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Catégorie</label>
<select v-model="item.type" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200">
<option v-for="t in itemTypes" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<!-- Designation -->
<div class="sm:col-span-5">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Description / Matériel / Prestation</label>
<input v-model="item.designation" type="text" placeholder="Désignation" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
<!-- Quantité -->
<div class="sm:col-span-2">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Quantité</label>
<input v-model="item.quantite" type="number" min="1" step="1" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
<!-- Prix unitaire -->
<div class="sm:col-span-2">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">P.U. HT ()</label>
<input v-model="item.prix_unitaire" type="number" min="0" step="0.01" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
</div>
<button type="button" @click="removeItem(index)" class="mt-5 p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<!-- Récapitulatif Financier -->
<div class="bg-slate-50 dark:bg-slate-950 p-4 rounded-xl border border-slate-200 dark:border-slate-800 flex flex-col items-end space-y-2 text-sm">
<div class="flex justify-between w-64">
<span class="text-slate-500">Total HT :</span>
<span class="font-bold text-slate-850 dark:text-slate-200">{{ formatCurrency(totalHt) }}</span>
</div>
<div class="flex justify-between w-64 border-b border-slate-200 dark:border-slate-800 pb-2">
<span class="text-slate-500">TVA ({{ form.tva_rate }}%) :</span>
<span class="font-semibold text-slate-850 dark:text-slate-200">{{ formatCurrency(tvaAmount) }}</span>
</div>
<div class="flex justify-between w-64 text-base font-extrabold text-sky-600 dark:text-sky-400 pt-1">
<span>Total TTC :</span>
<span>{{ formatCurrency(totalTtc) }}</span>
</div>
</div>
</div>
<!-- Notes / Conditions -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6">
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Notes / Conditions de livraison / Remarques :</label>
<textarea v-model="form.notes" rows="3" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" placeholder="Ex: Devis valable 30 jours. Matériel disponible sous 15 jours."></textarea>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-800">
<Link
:href="route('devis.index')"
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
Annuler
</Link>
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors disabled:opacity-50"
>
Enregistrer les Modifications
</button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
+190
View File
@@ -0,0 +1,190 @@
<script setup>
import { Head, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
defineProps({
devis: Array,
});
const getStatusClasses = (status) => {
switch (status) {
case 'draft':
return 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-900/60 dark:text-slate-400 dark:border-slate-800';
case 'sent':
return 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900/60';
case 'accepted':
return 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900/60';
case 'rejected':
return 'bg-rose-50 text-rose-800 border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900/60';
default:
return 'bg-slate-100 text-slate-800 border-slate-200';
}
};
const getStatusLabel = (status) => {
switch (status) {
case 'draft': return 'Brouillon';
case 'sent': return 'Envoyé';
case 'accepted': return 'Accepté';
case 'rejected': return 'Refusé';
default: return status;
}
};
const formatCurrency = (value) => {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
};
</script>
<template>
<Head title="Gestion des Devis" />
<AuthenticatedLayout>
<template #header>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Gestion des Devis (Matériels & Services)
</h2>
<div class="flex gap-2">
<Link
:href="route('devis.settings')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.43l-1.003.828c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.43l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
Paramètres
</Link>
<Link
:href="route('devis.create')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Nouveau devis
</Link>
</div>
</div>
</template>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Notifications flash -->
<div v-if="$page.props.flash?.success" class="mb-6 p-4 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-lg dark:bg-emerald-950/40 dark:border-emerald-900 dark:text-emerald-300 flex items-center shadow-sm">
<svg class="w-5 h-5 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span>{{ $page.props.flash?.success }}</span>
</div>
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-50 border-b border-slate-100 dark:bg-slate-950/40 dark:border-slate-850 text-xxs font-semibold uppercase tracking-wider text-slate-400">
<th class="px-6 py-4">Numéro</th>
<th class="px-6 py-4">Objet</th>
<th class="px-6 py-4">Destinataire</th>
<th class="px-6 py-4">Date</th>
<th class="px-6 py-4 text-right">Montant TTC</th>
<th class="px-6 py-4 text-center">Statut</th>
<th class="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-850 text-sm">
<tr
v-for="d in devis"
:key="d.id"
class="hover:bg-slate-50/50 dark:hover:bg-slate-950/20 transition-colors"
>
<!-- Numéro -->
<td class="px-6 py-4 font-mono text-xs font-bold text-sky-600 dark:text-sky-400">
<Link :href="route('devis.show', d.id)" class="hover:underline">
{{ d.number }}
</Link>
</td>
<!-- Objet -->
<td class="px-6 py-4 font-medium text-slate-800 dark:text-slate-200">
{{ d.title }}
</td>
<!-- Destinataire -->
<td class="px-6 py-4 text-slate-800 dark:text-slate-200">
<div class="flex flex-col">
<span class="font-semibold">{{ d.destinataire_nom }}</span>
<span class="text-xxs text-slate-450 dark:text-slate-400 uppercase tracking-wide">
{{ d.destinataire_type === 'commune' ? 'Commune' : 'Service Interne' }}
</span>
</div>
</td>
<!-- Date -->
<td class="px-6 py-4 text-xs text-slate-600 dark:text-slate-350">
{{ new Date(d.date_devis).toLocaleDateString() }}
</td>
<!-- Montant TTC -->
<td class="px-6 py-4 text-right font-bold text-slate-900 dark:text-slate-100">
{{ formatCurrency(d.total_ttc) }}
</td>
<!-- Statut -->
<td class="px-6 py-4 text-center">
<span :class="`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold border ${getStatusClasses(d.status)}`">
{{ getStatusLabel(d.status) }}
</span>
</td>
<!-- Actions -->
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-3">
<Link
:href="route('devis.show', d.id)"
class="text-slate-500 hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400"
title="Voir le devis / Imprimer"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</Link>
<Link
:href="route('devis.edit', d.id)"
class="text-slate-500 hover:text-amber-500 dark:text-slate-400 dark:hover:text-amber-400"
title="Modifier le devis"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 18.75a4.48 4.48 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</Link>
<Link
:href="route('devis.destroy', d.id)"
method="delete"
as="button"
class="text-slate-500 hover:text-rose-600 dark:text-slate-400 dark:hover:text-rose-500"
title="Supprimer le devis"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</Link>
</div>
</td>
</tr>
<tr v-if="devis.length === 0">
<td colspan="7" class="px-6 py-12 text-center">
<div class="flex flex-col items-center justify-center text-slate-500 dark:text-slate-400">
<svg class="w-12 h-12 mb-4 text-slate-300 dark:text-slate-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
<p class="text-lg font-medium">Aucun devis enregistré</p>
<p class="text-sm mt-1">Commencez par créer un nouveau devis pour une commune ou un service.</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
+124
View File
@@ -0,0 +1,124 @@
<script setup>
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const props = defineProps({
settings: Object,
});
const form = useForm({
entity_name: props.settings.entity_name || '',
entity_sub: props.settings.entity_sub || '',
email: props.settings.email || '',
telephone: props.settings.telephone || '',
adresse: props.settings.adresse || '',
});
const submit = () => {
form.put(route('devis.settings.update'));
};
</script>
<template>
<Head title="Paramètres des Devis" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Paramètres d'édition des Devis
</h2>
<Link
:href="route('devis.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
>
Retour
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Notification flash de succès -->
<div v-if="$page.props.flash?.success" class="mb-6 p-4 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-lg dark:bg-emerald-950/40 dark:border-emerald-900 dark:text-emerald-300 flex items-center shadow-sm">
<svg class="w-5 h-5 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span>{{ $page.props.flash?.success }}</span>
</div>
<form @submit.prevent="submit" class="space-y-6">
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6 space-y-6">
<div class="border-b border-slate-100 dark:border-slate-850 pb-3">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200">
Coordonnées de l'Émetteur
</h3>
<p class="text-xs text-slate-400 mt-1">
Ces informations apparaîtront dans l'en-tête de tous les devis générés par la DSI.
</p>
</div>
<div class="space-y-4">
<!-- Nom de l'entité -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Nom de l'entité principale :</label>
<input v-model="form.entity_name" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
<div v-if="form.errors.entity_name" class="text-rose-500 text-xs mt-1">{{ form.errors.entity_name }}</div>
</div>
<!-- Sous-titre / Organisation -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Sous-titre / Organisation parente :</label>
<input v-model="form.entity_sub" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
<div v-if="form.errors.entity_sub" class="text-rose-500 text-xs mt-1">{{ form.errors.entity_sub }}</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Adresse Email -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Adresse Email :</label>
<input v-model="form.email" type="email" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
<div v-if="form.errors.email" class="text-rose-500 text-xs mt-1">{{ form.errors.email }}</div>
</div>
<!-- Téléphone -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Numéro de Téléphone :</label>
<input v-model="form.telephone" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200" required>
<div v-if="form.errors.telephone" class="text-rose-500 text-xs mt-1">{{ form.errors.telephone }}</div>
</div>
</div>
<!-- Adresse postale -->
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Adresse postale (optionnel) :</label>
<input v-model="form.adresse" type="text" class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200">
<div v-if="form.errors.adresse" class="text-rose-500 text-xs mt-1">{{ form.errors.adresse }}</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-800">
<Link
:href="route('devis.index')"
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
Annuler
</Link>
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors disabled:opacity-50"
>
Enregistrer les Paramètres
</button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
+266
View File
@@ -0,0 +1,266 @@
<script setup>
import { Head, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const props = defineProps({
devis: Object,
settings: Object,
});
const getStatusClasses = (status) => {
switch (status) {
case 'draft':
return 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-900/60 dark:text-slate-400 dark:border-slate-800';
case 'sent':
return 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900/60';
case 'accepted':
return 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900/60';
case 'rejected':
return 'bg-rose-50 text-rose-800 border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900/60';
default:
return 'bg-slate-100 text-slate-800 border-slate-200';
}
};
const getStatusLabel = (status) => {
switch (status) {
case 'draft': return 'Brouillon';
case 'sent': return 'Envoyé';
case 'accepted': return 'Accepté';
case 'rejected': return 'Refusé';
default: return status;
}
};
const formatCurrency = (value) => {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
};
const printDevis = () => {
window.print();
};
</script>
<template>
<Head :title="`Devis ${devis.number}`" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between no-print">
<div class="flex items-center gap-3">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Visualisation du Devis {{ devis.number }}
</h2>
<span :class="`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold border ${getStatusClasses(devis.status)}`">
{{ getStatusLabel(devis.status) }}
</span>
</div>
<div class="flex items-center gap-2">
<Link
:href="route('devis.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
>
Retour
</Link>
<Link
:href="route('devis.edit', devis.id)"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-amber-500 hover:bg-amber-600 rounded-lg transition-colors shadow-sm"
>
Modifier
</Link>
<a
:href="route('devis.pdf', devis.id)"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-550 rounded-lg transition-colors shadow-sm"
target="_blank"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Télécharger PDF
</a>
<button
@click="printDevis"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0a2.25 2.25 0 0 1-2.25 2.25H8.59A2.25 2.25 0 0 1 6.34 18m11.318-4.171A2.25 2.25 0 0 0 19.5 11.625V8.25a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 8.25v3.375c0 .921.555 1.714 1.342 2.054M15 9V4.5A1.5 1.5 0 0 0 13.5 3h-3A1.5 1.5 0 0 0 9 4.5V9m9 3h.008v.008H18V12Zm-1.5 0h.008v.008H16.5V12Z" />
</svg>
Imprimer
</button>
</div>
</div>
</template>
<div class="py-10 print:py-0">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 print:px-0 print:max-w-full flex justify-center">
<!-- Format Devis Papier / A4 style (Designer Look) -->
<div class="print-container w-full max-w-4xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl shadow-sm overflow-hidden p-12 md:p-16 space-y-14">
<!-- En-tête Devis -->
<table class="w-full border-b border-slate-200 dark:border-slate-800 pb-10 mb-2 text-sm">
<tbody><tr>
<td class="align-top text-left pb-4">
<div class="flex items-center gap-6">
<img src="/logo.png" alt="Logo Béziers Méditerranée" class="h-24 w-auto object-contain shrink-0">
<div class="space-y-2 text-left">
<div class="text-slate-900 dark:text-white font-black text-2xl tracking-tight">
{{ settings?.entity_name || "Direction des Systèmes d'Information" }}
</div>
<!-- Decorative squares row in "Studio Shodwe" style -->
<div class="flex justify-start gap-1.5 mt-3">
<span class="w-4 h-3.5 bg-emerald-700 inline-block"></span>
<span class="w-4 h-3.5 bg-teal-500 inline-block"></span>
<span class="w-4 h-3.5 bg-amber-500 inline-block"></span>
<span class="w-4 h-3.5 bg-rose-500 inline-block"></span>
<span class="w-4 h-3.5 bg-pink-400 inline-block"></span>
</div>
</div>
</div>
</td>
<td class="align-top text-right pb-4 text-slate-500 dark:text-slate-400 leading-loose font-semibold" style="text-align: right !important;">
<div class="inline-block text-right" style="text-align: right !important;">
<p class="font-extrabold text-slate-850 dark:text-slate-200 text-right mb-1" style="text-align: right !important;">{{ settings?.entity_sub || "Communauté d'Agglomération Béziers Méditerranée" }}</p>
<p v-if="settings?.telephone" class="text-right" style="text-align: right !important;">Téléphone : {{ settings.telephone }}</p>
<p v-if="settings?.email" class="text-right" style="text-align: right !important;">E-mail : {{ settings.email }}</p>
<p v-if="settings?.adresse" class="text-right" style="text-align: right !important;">Adresse : {{ settings.adresse }}</p>
</div>
</td>
</tr></tbody>
</table>
<!-- Blocs Destinataire et Émetteur (Designer thick outline rounded cards) -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
<div class="border-2 border-slate-900 dark:border-slate-700 rounded-[2rem] p-9 space-y-5">
<h3 class="text-base font-black uppercase tracking-tight text-slate-900 dark:text-slate-200">
DEVIS N° : {{ devis.number }}
</h3>
<div class="space-y-3 text-sm text-slate-700 dark:text-slate-350">
<p><span class="font-black text-slate-900 dark:text-slate-250">DATE :</span> {{ new Date(devis.date_devis).toLocaleDateString() }}</p>
<p><span class="font-black text-slate-900 dark:text-slate-250">OBJET :</span> {{ devis.title }}</p>
<p v-if="devis.notes"><span class="font-black text-slate-900 dark:text-slate-250">CONDITIONS :</span> {{ devis.notes }}</p>
</div>
</div>
<div class="border-2 border-slate-900 dark:border-slate-700 rounded-[2rem] p-9 space-y-5">
<h3 class="text-base font-black uppercase tracking-tight text-slate-900 dark:text-slate-200">
DESTINATAIRE :
</h3>
<div class="space-y-3 text-sm text-slate-700 dark:text-slate-350">
<p class="font-black text-base text-slate-900 dark:text-white">{{ devis.destinataire_nom }}</p>
<p class="font-bold text-slate-500">
{{ devis.destinataire_type === 'commune' ? 'Commune / Collectivité' : 'Service Interne' }}
</p>
<p v-if="devis.referent" class="border-t border-slate-200 dark:border-slate-800 pt-3 mt-3">
<span class="font-black text-slate-900 dark:text-slate-250">À L'ATTENTION DE :</span> {{ devis.referent }}
</p>
</div>
</div>
</div>
<!-- Tableau des Lignes du Devis (Clean rows, grey header bar, dotted lines) -->
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse text-sm">
<thead>
<tr class="bg-slate-100 dark:bg-slate-800 text-xs font-black uppercase tracking-wider text-slate-900 dark:text-slate-200">
<th class="px-7 py-4">Description :</th>
<th class="px-7 py-4 text-right">Prix Unitaire :</th>
<th class="px-7 py-4 text-center">Quantité :</th>
<th class="px-7 py-4 text-right">Total :</th>
</tr>
</thead>
<tbody class="text-slate-750 dark:text-slate-250">
<tr v-for="(item, index) in devis.items" :key="index" class="border-b-2 border-dotted border-slate-300 dark:border-slate-750">
<td class="px-7 py-6">
<div class="font-black text-slate-900 dark:text-slate-100 text-base mb-1">
{{ item.designation }}
</div>
<span class="text-xs font-bold uppercase tracking-wider text-slate-400">
{{ item.type }}
</span>
</td>
<td class="px-7 py-6 text-right font-medium">
{{ formatCurrency(item.prix_unitaire) }}
</td>
<td class="px-7 py-6 text-center font-bold text-slate-900 dark:text-slate-100">
{{ item.quantite }}
</td>
<td class="px-7 py-6 text-right font-black text-slate-900 dark:text-slate-100 text-base">
{{ formatCurrency(item.quantite * item.prix_unitaire) }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- Totaux (Compact design block) -->
<div class="flex justify-end pt-4">
<div class="w-80 space-y-3 text-right text-slate-900 dark:text-slate-100 font-black text-sm">
<div class="flex justify-between">
<span>TOTAL HT :</span>
<span>{{ formatCurrency(devis.total_ht) }}</span>
</div>
<div class="flex justify-between">
<span>TVA ({{ devis.tva_rate }}%) :</span>
<span>{{ formatCurrency(devis.total_ttc - devis.total_ht) }}</span>
</div>
<div class="flex justify-between text-base border-t-2 border-slate-900 dark:border-slate-700 pt-4 mt-1">
<span>TOTAL TTC :</span>
<span class="text-sky-600 dark:text-sky-400 font-extrabold">{{ formatCurrency(devis.total_ttc) }}</span>
</div>
</div>
</div>
<!-- Signatures and Modalités de paiement (Two columns, separated by vertical bar) -->
<div class="grid grid-cols-1 md:grid-cols-1 gap-14 pt-14 border-t border-slate-200 dark:border-slate-800 text-sm">
<div class="space-y-5 text-slate-650 dark:text-slate-350 pr-4">
<div class="pt-1">
<p class="font-bold text-xs uppercase tracking-wider text-slate-400 mb-2">Pour le Destinataire (Bon pour accord)</p>
<div class="border-b border-dashed border-slate-400 w-56 h-12 mb-4"></div>
<p class="text-xs italic text-slate-400">Nom, Date et Signature</p>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
<style>
/* Global print styles: non-scoped to guarantee layout hiding works */
@media print {
/* Hide the primary navigation bar and header wrappers in AuthenticatedLayout */
nav, header, aside, .no-print {
display: none !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
.min-h-screen {
min-height: auto !important;
background-color: transparent !important;
}
body, html {
background: white !important;
color: black !important;
}
/* Preserve the padding inside print container so it does not hug the edge of the sheet */
.print-container {
border: none !important;
box-shadow: none !important;
padding: 3rem !important; /* ~48px margins inside the paper sheet */
margin: 0 !important;
background: white !important;
color: black !important;
width: 100% !important;
max-width: 100% !important;
}
}
</style>
+2 -2
View File
@@ -317,7 +317,7 @@ const getWarrantyClasses = (hw) => {
class="text-slate-500 hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400" class="text-slate-500 hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400"
title="Voir les détails" title="Voir les détails"
> >
<svg class="w-4.5 h-4.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"> <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg> </svg>
@@ -327,7 +327,7 @@ const getWarrantyClasses = (hw) => {
class="text-slate-500 hover:text-amber-500 dark:text-slate-400 dark:hover:text-amber-400" class="text-slate-500 hover:text-amber-500 dark:text-slate-400 dark:hover:text-amber-400"
title="Modifier la fiche" title="Modifier la fiche"
> >
<svg class="w-4.5 h-4.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"> <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 18.75a4.48 4.48 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" /> <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 18.75a4.48 4.48 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg> </svg>
</Link> </Link>
+259
View File
@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Devis {{ $devis->number }}</title>
<style>
@page { margin: 0; size: A4; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'DejaVu Sans', sans-serif;
color: #334155;
font-size: 8.5pt;
line-height: 1.5;
background: #ffffff;
}
/* ─── PAGE WRAPPER ─────────────────────────────────── */
.page { padding: 32px 40px; }
/* ─── SECTION SPACER ────────────────────────────────── */
.section-gap { height: 22px; }
.section-gap-sm { height: 12px; }
/* ─── HEADER ────────────────────────────────────────── */
.header-table {
width: 100%;
border-collapse: collapse;
border-bottom: 1px solid #e2e8f0;
}
.header-table td { border: none; vertical-align: middle; padding: 0 0 14px 0; }
.header-logo { height: 70px; width: auto; vertical-align: middle; margin-right: 14px; }
.header-entity-name {
font-size: 13pt;
font-weight: bold;
color: #0f172a;
letter-spacing: -0.5px;
display: block;
margin-bottom: 5px;
}
.deco-squares { margin-top: 3px; }
.deco-sq { display: inline-block; width: 13px; height: 11px; margin-right: 3px; }
.header-right {
text-align: right;
font-size: 7.5pt;
color: #64748b;
line-height: 1.65;
font-weight: 600;
}
.header-right .entity-sub { font-weight: 800; color: #1e293b; display: block; margin-bottom: 2px; }
/* ─── INFO CARDS ────────────────────────────────────── */
.cards-table { width: 100%; border-collapse: collapse; }
.cards-table td { border: none; padding: 0; vertical-align: top; }
.card { border: 2px solid #0f172a; border-radius: 24px; padding: 20px 24px; }
.card-title {
font-size: 9pt;
font-weight: bold;
text-transform: uppercase;
color: #0f172a;
letter-spacing: -0.3px;
margin-bottom: 10px;
}
.card-body { font-size: 8pt; color: #334155; line-height: 1.7; }
.card-label { font-weight: bold; color: #0f172a; }
.card-dest-name { font-weight: bold; color: #0f172a; font-size: 9pt; display: block; margin-bottom: 3px; }
.card-dest-type { font-weight: 700; color: #64748b; font-size: 7.5pt; display: block; margin-bottom: 7px; }
.card-referent-sep { border-top: 1px solid #e2e8f0; margin-top: 7px; padding-top: 7px; }
/* ─── ITEMS TABLE ───────────────────────────────────── */
.items-table { width: 100%; border-collapse: collapse; }
.items-table thead tr { background-color: #f1f5f9; }
.items-table th {
font-size: 7.5pt;
font-weight: bold;
text-transform: uppercase;
color: #0f172a;
letter-spacing: 0.4px;
padding: 10px 20px;
text-align: left;
border: none;
}
.items-table td {
padding: 13px 20px;
border-bottom: 2px dotted #cbd5e1;
vertical-align: middle;
border-top: none; border-left: none; border-right: none;
}
.item-designation { font-weight: bold; color: #0f172a; font-size: 9pt; margin-bottom: 2px; }
.item-type { font-weight: bold; color: #94a3b8; font-size: 7pt; text-transform: uppercase; letter-spacing: 0.4px; }
.col-price { text-align: right; font-size: 8.5pt; }
.col-qty { text-align: center; font-weight: bold; color: #0f172a; font-size: 8.5pt; }
.col-total { text-align: right; font-weight: bold; color: #0f172a; font-size: 9pt; }
/* ─── TOTAUX ────────────────────────────────────────── */
.sig-label {
font-size: 7pt;
font-weight: bold;
text-transform: uppercase;
color: #94a3b8;
letter-spacing: 0.4px;
display: block;
margin-bottom: 22px;
}
.sig-line { border-bottom: 1px dashed #94a3b8; width: 200px; height: 8px; margin-bottom: 5px; }
.sig-note { font-size: 6.5pt; color: #94a3b8; font-style: italic; }
/* ─── FOOTER ────────────────────────────────────────── */
.footer-table {
width: 100%;
border-collapse: collapse;
border-top: 1px solid #e2e8f0;
margin-top: 20px;
}
.footer-table td { border: none; padding: 0; vertical-align: top; }
</style>
</head>
<body>
<div class="page">
{{-- ─── HEADER ─────────────────────────────────────────── --}}
<table class="header-table">
<tbody>
<tr>
<td style="width: 60%;">
<table style="border-collapse: collapse; width: 100%;"><tbody><tr>
<td style="border: none; padding: 0; vertical-align: middle; width: 1%;">
@if(file_exists(public_path('logo.png')))
<img src="data:image/png;base64,{{ base64_encode(file_get_contents(public_path('logo.png'))) }}"
class="header-logo" alt="Logo">
@endif
</td>
<td style="border: none; padding: 0; vertical-align: middle; padding-left: 20px;">
<span class="header-entity-name">{{ $settings->entity_name ?? "Direction des Systèmes d'Information" }}</span>
<div class="deco-squares">
<span class="deco-sq" style="background-color:#047857;"></span>
<span class="deco-sq" style="background-color:#14b8a6;"></span>
<span class="deco-sq" style="background-color:#f59e0b;"></span>
<span class="deco-sq" style="background-color:#f43f5e;"></span>
<span class="deco-sq" style="background-color:#f472b6;"></span>
</div>
</td>
</tr></tbody></table>
</td>
<td style="width: 40%; text-align: right;" class="header-right">
<span class="entity-sub">{{ $settings->entity_sub ?? "Communauté d'Agglomération Béziers Méditerranée" }}</span>
@if(!empty($settings->telephone))<span>Téléphone : {{ $settings->telephone }}</span><br>@endif
@if(!empty($settings->email))<span>E-mail : {{ $settings->email }}</span><br>@endif
@if(!empty($settings->adresse))<span>Adresse : {{ $settings->adresse }}</span>@endif
</td>
</tr>
</tbody>
</table>
<div class="section-gap"></div>
{{-- ─── INFO CARDS ──────────────────────────────────────── --}}
<table class="cards-table">
<tbody><tr>
<td style="width: 48%;">
<div class="card">
<div class="card-title">DEVIS : {{ $devis->number }}</div>
<div class="card-body">
<span class="card-label">DATE :</span> {{ \Carbon\Carbon::parse($devis->date_devis)->format('d/m/Y') }}<br>
<span class="card-label">OBJET :</span> {{ $devis->title }}
@if(!empty($devis->notes))
<br><span class="card-label">CONDITIONS :</span> {{ $devis->notes }}
@endif
</div>
</div>
</td>
<td style="width: 4%;"></td>
<td style="width: 48%;">
<div class="card">
<div class="card-title">DESTINATAIRE :</div>
<div class="card-body">
<span class="card-dest-name">{{ $devis->destinataire_nom }}</span>
<span class="card-dest-type">
{{ $devis->destinataire_type === 'commune' ? 'Commune / Collectivité' : 'Service Interne' }}
</span>
@if(!empty($devis->referent))
<div class="card-referent-sep">
<span class="card-label">À L'ATTENTION DE :</span> {{ $devis->referent }}
</div>
@endif
</div>
</div>
</td>
</tr></tbody>
</table>
<div class="section-gap"></div>
{{-- ─── TABLEAU LIGNES ──────────────────────────────────── --}}
<table class="items-table">
<thead>
<tr>
<th>Description :</th>
<th style="text-align: right;">Prix Unitaire :</th>
<th style="text-align: center;">Quantité :</th>
<th style="text-align: right;">Total :</th>
</tr>
</thead>
<tbody>
@foreach($devis->items as $item)
<tr>
<td>
<div class="item-designation">{{ $item['designation'] ?? '' }}</div>
<div class="item-type">{{ $item['type'] ?? '' }}</div>
</td>
<td class="col-price">{{ number_format($item['prix_unitaire'] ?? 0, 2, ',', "\u{202F}") }} </td>
<td class="col-qty">{{ $item['quantite'] ?? 0 }}</td>
<td class="col-total">{{ number_format(($item['quantite'] ?? 0) * ($item['prix_unitaire'] ?? 0), 2, ',', "\u{202F}") }} </td>
</tr>
@endforeach
</tbody>
</table>
<div class="section-gap-sm"></div>
{{-- ─── TOTAUX ───────────────────────────────────────────── --}}
<table style="width: 100%; border-collapse: collapse;">
<tbody><tr>
<td style="border: none; padding: 0;"></td>
<td style="border: none; padding: 0; width: 280px; text-align: right;">
<table style="width: 100%; border-collapse: collapse;">
<tbody>
<tr>
<td style="border: none; padding: 4px 0; font-weight: bold; font-size: 8.5pt; color: #0f172a; text-align: left;">TOTAL HT :</td>
<td style="border: none; padding: 4px 0; font-weight: bold; font-size: 8.5pt; color: #0f172a; text-align: right;">{{ number_format($devis->total_ht, 2, ',', "\u{202F}") }} </td>
</tr>
<tr>
<td style="border: none; padding: 4px 0; font-weight: bold; font-size: 8.5pt; color: #0f172a; text-align: left;">TVA ({{ $devis->tva_rate }}%) :</td>
<td style="border: none; padding: 4px 0; font-weight: bold; font-size: 8.5pt; color: #0f172a; text-align: right;">{{ number_format($devis->total_ttc - $devis->total_ht, 2, ',', "\u{202F}") }} </td>
</tr>
<tr>
<td style="border: none; border-top: 2px solid #0f172a; padding: 9px 0 4px 0; font-weight: bold; font-size: 10.5pt; color: #0f172a; text-align: left;">TOTAL TTC :</td>
<td style="border: none; border-top: 2px solid #0f172a; padding: 9px 0 4px 0; font-weight: 800; font-size: 10.5pt; color: #0284c7; text-align: right;">{{ number_format($devis->total_ttc, 2, ',', "\u{202F}") }} </td>
</tr>
</tbody>
</table>
</td>
</tr></tbody>
</table>
{{-- ─── FOOTER : SIGNATURE DESTINATAIRE ────────────────── --}}
<table class="footer-table">
<tbody><tr>
<td style="vertical-align: top; padding-top: 22px;">
<span class="sig-label">Pour le Destinataire (Bon pour accord)</span>
<div class="sig-line"></div>
<span class="sig-note">Nom, Date et Signature</span>
</td>
</tr></tbody>
</table>
</div>
</body>
</html>
+15
View File
@@ -8,6 +8,8 @@ use Inertia\Inertia;
use App\Http\Controllers\OrderController; use App\Http\Controllers\OrderController;
use App\Http\Controllers\AttachmentController; use App\Http\Controllers\AttachmentController;
use App\Http\Controllers\HardwareController; use App\Http\Controllers\HardwareController;
use App\Http\Controllers\BesoinInformatiqueController;
use App\Http\Controllers\DevisController;
Route::get('/', function () { Route::get('/', function () {
return redirect()->route('login'); return redirect()->route('login');
@@ -70,6 +72,19 @@ Route::middleware('auth')->group(function () {
// Gestion de matériel d'infrastructure // Gestion de matériel d'infrastructure
Route::resource('materiels', HardwareController::class); Route::resource('materiels', HardwareController::class);
// Besoins Informatiques (Mutualisation)
Route::resource('besoins-informatiques', BesoinInformatiqueController::class)->parameters([
'besoins-informatiques' => 'besoin_informatique',
]);
// Gestion des Devis
Route::get('/devis/settings', [DevisController::class, 'settings'])->name('devis.settings');
Route::put('/devis/settings', [DevisController::class, 'updateSettings'])->name('devis.settings.update');
Route::get('/devis/{devi}/pdf', [DevisController::class, 'downloadPdf'])->name('devis.pdf');
Route::resource('devis', DevisController::class)->parameters([
'devis' => 'devi',
]);
}); });
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';